mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-19 04:38:50 +02:00
Compare commits
4 Commits
agent/lamb
...
agent/lamb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8d776f85a | ||
|
|
d9e5cf87dd | ||
|
|
13fe614903 | ||
|
|
2305f7d180 |
@@ -130,38 +130,40 @@ function AutopilotRow({ autopilot }: { autopilot: Autopilot }) {
|
||||
const StatusIcon = statusCfg.icon;
|
||||
|
||||
return (
|
||||
<div className="group/row flex h-11 items-center gap-2 px-5 text-sm transition-colors hover:bg-accent/40">
|
||||
<div className="group/row flex flex-col gap-2 border-b px-4 py-3 text-sm transition-colors hover:bg-accent/40 sm:h-11 sm:flex-row sm:items-center sm:gap-2 sm:border-b-0 sm:px-5 sm:py-0">
|
||||
<AppLink
|
||||
href={wsPaths.autopilotDetail(autopilot.id)}
|
||||
className="flex min-w-0 flex-1 items-center gap-2"
|
||||
className="flex min-w-0 items-center gap-2 sm:flex-1"
|
||||
>
|
||||
<Zap className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<span className="min-w-0 flex-1 truncate font-medium">{autopilot.title}</span>
|
||||
</AppLink>
|
||||
|
||||
{/* Agent */}
|
||||
<span className="flex w-32 items-center gap-1.5 shrink-0">
|
||||
<ActorAvatar actorType="agent" actorId={autopilot.assignee_id} size={18} enableHoverCard showStatusDot />
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{getActorName("agent", autopilot.assignee_id)}
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 pl-6 text-xs sm:contents sm:pl-0">
|
||||
{/* Agent */}
|
||||
<span className="flex min-w-0 items-center gap-1.5 text-muted-foreground sm:w-32 sm:shrink-0">
|
||||
<ActorAvatar actorType="agent" actorId={autopilot.assignee_id} size={18} enableHoverCard showStatusDot />
|
||||
<span className="truncate">
|
||||
{getActorName("agent", autopilot.assignee_id)}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* Mode */}
|
||||
<span className="w-24 shrink-0 text-center text-xs text-muted-foreground">
|
||||
{EXECUTION_MODE_LABELS[autopilot.execution_mode] ?? autopilot.execution_mode}
|
||||
</span>
|
||||
{/* Mode */}
|
||||
<span className="text-muted-foreground sm:w-24 sm:shrink-0 sm:text-center">
|
||||
{EXECUTION_MODE_LABELS[autopilot.execution_mode] ?? autopilot.execution_mode}
|
||||
</span>
|
||||
|
||||
{/* Status */}
|
||||
<span className={cn("flex w-20 items-center justify-center gap-1 shrink-0 text-xs", statusCfg.color)}>
|
||||
<StatusIcon className="h-3 w-3" />
|
||||
{statusCfg.label}
|
||||
</span>
|
||||
{/* Status */}
|
||||
<span className={cn("flex items-center gap-1 sm:w-20 sm:shrink-0 sm:justify-center", statusCfg.color)}>
|
||||
<StatusIcon className="h-3 w-3" />
|
||||
{statusCfg.label}
|
||||
</span>
|
||||
|
||||
{/* Last run */}
|
||||
<span className="w-20 shrink-0 text-right text-xs text-muted-foreground tabular-nums">
|
||||
{autopilot.last_run_at ? formatRelativeDate(autopilot.last_run_at) : "--"}
|
||||
</span>
|
||||
{/* Last run */}
|
||||
<span className="text-muted-foreground tabular-nums sm:w-20 sm:shrink-0 sm:text-right">
|
||||
{autopilot.last_run_at ? formatRelativeDate(autopilot.last_run_at) : "--"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -198,7 +200,7 @@ export function AutopilotsPage() {
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="sticky top-0 z-[1] flex h-8 items-center gap-2 border-b bg-muted/30 px-5">
|
||||
<div className="sticky top-0 z-[1] hidden h-8 items-center gap-2 border-b bg-muted/30 px-5 sm:flex">
|
||||
<span className="shrink-0 w-4" />
|
||||
<Skeleton className="h-3 w-12 flex-1 max-w-[48px]" />
|
||||
<Skeleton className="h-3 w-12 shrink-0" />
|
||||
@@ -206,9 +208,9 @@ export function AutopilotsPage() {
|
||||
<Skeleton className="h-3 w-10 shrink-0" />
|
||||
<Skeleton className="h-3 w-12 shrink-0" />
|
||||
</div>
|
||||
<div className="p-5 pt-1 space-y-1">
|
||||
<div className="space-y-2 p-4 sm:space-y-1 sm:p-5 sm:pt-1">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-11 w-full" />
|
||||
<Skeleton key={i} className="h-[72px] w-full sm:h-11" />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
@@ -246,7 +248,7 @@ export function AutopilotsPage() {
|
||||
) : (
|
||||
<>
|
||||
{/* Column headers */}
|
||||
<div className="sticky top-0 z-[1] flex h-8 items-center gap-2 border-b bg-muted/30 px-5 text-xs font-medium text-muted-foreground">
|
||||
<div className="sticky top-0 z-[1] hidden h-8 items-center gap-2 border-b bg-muted/30 px-5 text-xs font-medium text-muted-foreground sm:flex">
|
||||
<span className="shrink-0 w-4" />
|
||||
<span className="min-w-0 flex-1">Name</span>
|
||||
<span className="w-32 shrink-0">Agent</span>
|
||||
|
||||
@@ -77,13 +77,6 @@ export function IssuesPage() {
|
||||
const updateIssueMutation = useUpdateIssue();
|
||||
const handleMoveIssue = useCallback(
|
||||
(issueId: string, newStatus: IssueStatus, newPosition?: number) => {
|
||||
// Auto-switch to manual sort so drag ordering is preserved
|
||||
const viewState = useIssueViewStore.getState();
|
||||
if (viewState.sortBy !== "position") {
|
||||
viewState.setSortBy("position");
|
||||
viewState.setSortDirection("asc");
|
||||
}
|
||||
|
||||
const updates: Partial<{ status: IssueStatus; position: number }> = {
|
||||
status: newStatus,
|
||||
};
|
||||
|
||||
@@ -102,12 +102,6 @@ export function MyIssuesPage() {
|
||||
const updateIssueMutation = useUpdateIssue();
|
||||
const handleMoveIssue = useCallback(
|
||||
(issueId: string, newStatus: IssueStatus, newPosition?: number) => {
|
||||
const viewState = myIssuesViewStore.getState();
|
||||
if (viewState.sortBy !== "position") {
|
||||
viewState.setSortBy("position");
|
||||
viewState.setSortDirection("asc");
|
||||
}
|
||||
|
||||
const updates: Partial<{ status: IssueStatus; position: number }> = {
|
||||
status: newStatus,
|
||||
};
|
||||
|
||||
@@ -134,11 +134,6 @@ function ProjectIssuesContent({
|
||||
const updateIssueMutation = useUpdateIssue();
|
||||
const handleMoveIssue = useCallback(
|
||||
(issueId: string, newStatus: IssueStatus, newPosition?: number) => {
|
||||
const viewState = projectViewStore.getState();
|
||||
if (viewState.sortBy !== "position") {
|
||||
viewState.setSortBy("position");
|
||||
viewState.setSortDirection("asc");
|
||||
}
|
||||
const updates: Partial<{ status: IssueStatus; position: number }> = { status: newStatus };
|
||||
if (newPosition !== undefined) updates.position = newPosition;
|
||||
updateIssueMutation.mutate(
|
||||
|
||||
@@ -17,6 +17,13 @@ import (
|
||||
"github.com/multica-ai/multica/server/pkg/protocol"
|
||||
)
|
||||
|
||||
// sanitizeNullBytes removes null bytes (0x00) from strings.
|
||||
// PostgreSQL rejects null bytes in text columns with
|
||||
// "invalid byte sequence for encoding UTF8: 0x00 (SQLSTATE 22021)".
|
||||
func sanitizeNullBytes(s string) string {
|
||||
return strings.ReplaceAll(s, "\x00", "")
|
||||
}
|
||||
|
||||
// --- Response structs ---
|
||||
|
||||
type SkillResponse struct {
|
||||
@@ -289,13 +296,13 @@ func (h *Handler) UpdateSkill(w http.ResponseWriter, r *http.Request) {
|
||||
ID: parseUUID(id),
|
||||
}
|
||||
if req.Name != nil {
|
||||
params.Name = pgtype.Text{String: *req.Name, Valid: true}
|
||||
params.Name = pgtype.Text{String: sanitizeNullBytes(*req.Name), Valid: true}
|
||||
}
|
||||
if req.Description != nil {
|
||||
params.Description = pgtype.Text{String: *req.Description, Valid: true}
|
||||
params.Description = pgtype.Text{String: sanitizeNullBytes(*req.Description), Valid: true}
|
||||
}
|
||||
if req.Content != nil {
|
||||
params.Content = pgtype.Text{String: *req.Content, Valid: true}
|
||||
params.Content = pgtype.Text{String: sanitizeNullBytes(*req.Content), Valid: true}
|
||||
}
|
||||
if req.Config != nil {
|
||||
config, _ := json.Marshal(req.Config)
|
||||
@@ -323,8 +330,8 @@ func (h *Handler) UpdateSkill(w http.ResponseWriter, r *http.Request) {
|
||||
for _, f := range req.Files {
|
||||
sf, err := qtx.UpsertSkillFile(r.Context(), db.UpsertSkillFileParams{
|
||||
SkillID: skill.ID,
|
||||
Path: f.Path,
|
||||
Content: f.Content,
|
||||
Path: sanitizeNullBytes(f.Path),
|
||||
Content: sanitizeNullBytes(f.Content),
|
||||
})
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to upsert skill file: "+err.Error())
|
||||
@@ -1188,8 +1195,8 @@ func (h *Handler) UpsertSkillFile(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
sf, err := h.Queries.UpsertSkillFile(r.Context(), db.UpsertSkillFileParams{
|
||||
SkillID: skill.ID,
|
||||
Path: req.Path,
|
||||
Content: req.Content,
|
||||
Path: sanitizeNullBytes(req.Path),
|
||||
Content: sanitizeNullBytes(req.Content),
|
||||
})
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to upsert skill file: "+err.Error())
|
||||
|
||||
@@ -37,9 +37,9 @@ func (h *Handler) createSkillWithFiles(ctx context.Context, input skillCreateInp
|
||||
|
||||
skill, err := qtx.CreateSkill(ctx, db.CreateSkillParams{
|
||||
WorkspaceID: input.WorkspaceID,
|
||||
Name: input.Name,
|
||||
Description: input.Description,
|
||||
Content: input.Content,
|
||||
Name: sanitizeNullBytes(input.Name),
|
||||
Description: sanitizeNullBytes(input.Description),
|
||||
Content: sanitizeNullBytes(input.Content),
|
||||
Config: config,
|
||||
CreatedBy: input.CreatorID,
|
||||
})
|
||||
@@ -51,8 +51,8 @@ func (h *Handler) createSkillWithFiles(ctx context.Context, input skillCreateInp
|
||||
for _, f := range input.Files {
|
||||
sf, err := qtx.UpsertSkillFile(ctx, db.UpsertSkillFileParams{
|
||||
SkillID: skill.ID,
|
||||
Path: f.Path,
|
||||
Content: f.Content,
|
||||
Path: sanitizeNullBytes(f.Path),
|
||||
Content: sanitizeNullBytes(f.Content),
|
||||
})
|
||||
if err != nil {
|
||||
return SkillWithFilesResponse{}, err
|
||||
|
||||
Reference in New Issue
Block a user