From df70eee74945dec3beb634bdc87c09ec01f1dbe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Tue, 24 Mar 2026 23:22:20 +0000 Subject: [PATCH] fix(web): merge chat status bar into single row and switch task on archive (#460) --- apps/backend/cmd/mock-agent/emitter.go | 8 + apps/backend/cmd/mock-agent/script.go | 27 ++ .../components/task/chat/chat-input-area.tsx | 35 ++- .../components/task/chat/todo-indicator.tsx | 1 + .../components/task/task-session-sidebar.tsx | 14 +- apps/web/e2e/pages/session-page.ts | 14 + apps/web/e2e/tests/chat-status-bar.spec.ts | 243 ++++++++++++++++++ apps/web/hooks/use-task-actions.ts | 25 ++ 8 files changed, 347 insertions(+), 20 deletions(-) create mode 100644 apps/web/e2e/tests/chat-status-bar.spec.ts diff --git a/apps/backend/cmd/mock-agent/emitter.go b/apps/backend/cmd/mock-agent/emitter.go index f65966d138..3903d5d1d0 100644 --- a/apps/backend/cmd/mock-agent/emitter.go +++ b/apps/backend/cmd/mock-agent/emitter.go @@ -65,6 +65,14 @@ func (e *emitter) completeTool(id acp.ToolCallId, output any) { }) } +// plan sends an ACP Plan update with the provided entries. +func (e *emitter) plan(entries []acp.PlanEntry) { + _ = e.conn.SessionUpdate(e.ctx, acp.SessionNotification{ + SessionId: e.sid, + Update: acp.UpdatePlan(entries...), + }) +} + // requestPermission asks the client for permission to proceed with a tool call. // Returns true if permission was granted, false otherwise. func (e *emitter) requestPermission(toolCallID acp.ToolCallId, title string, kind acp.ToolKind, input any) bool { diff --git a/apps/backend/cmd/mock-agent/script.go b/apps/backend/cmd/mock-agent/script.go index d9307e2550..73d5f69567 100644 --- a/apps/backend/cmd/mock-agent/script.go +++ b/apps/backend/cmd/mock-agent/script.go @@ -47,6 +47,9 @@ func executeCommand(e *emitter, fullPrompt, line string) { case strings.HasPrefix(line, "e2e:mcp:"): executeMCPCommand(e, fullPrompt, line) + case strings.HasPrefix(line, "e2e:plan("): + executePlanCommand(e, line) + case strings.HasPrefix(line, "e2e:tool_use("): executeSimulatedToolUse(e, line) @@ -100,6 +103,30 @@ func executeMCPCommand(e *emitter, fullPrompt, line string) { } } +// executePlanCommand emits an ACP Plan update. +// Format: e2e:plan([{"content":"task","status":"in_progress"},{"content":"task2","status":"pending"}]) +func executePlanCommand(e *emitter, line string) { + argsStr := extractParenContent(line, "e2e:plan(") + var rawEntries []struct { + Content string `json:"content"` + Status string `json:"status"` + Priority string `json:"priority,omitempty"` + } + if err := json.Unmarshal([]byte(argsStr), &rawEntries); err != nil { + e.text(fmt.Sprintf("Script error: bad plan JSON: %v", err)) + return + } + entries := make([]acp.PlanEntry, len(rawEntries)) + for i, r := range rawEntries { + entries[i] = acp.PlanEntry{ + Content: r.Content, + Status: acp.PlanEntryStatus(r.Status), + Priority: acp.PlanEntryPriority(r.Priority), + } + } + e.plan(entries) +} + // executeSimulatedToolUse emits a simulated tool call start (no real execution). // Format: e2e:tool_use("name", {"key":"value"}) func executeSimulatedToolUse(e *emitter, line string) { diff --git a/apps/web/components/task/chat/chat-input-area.tsx b/apps/web/components/task/chat/chat-input-area.tsx index 49913bd23d..8b04e6d4cf 100644 --- a/apps/web/components/task/chat/chat-input-area.tsx +++ b/apps/web/components/task/chat/chat-input-area.tsx @@ -20,7 +20,7 @@ import { formatPlanCommentsAsMarkdown, } from "@/lib/state/slices/comments/format"; import { setChatDraftContent } from "@/lib/local-storage"; -import { useTaskActions } from "@/hooks/use-task-actions"; +import { useArchiveAndSwitchTask } from "@/hooks/use-task-actions"; import { useToast } from "@/components/toast-provider"; import type { DiffComment } from "@/lib/diff/types"; import type { useChatPanelState } from "./use-chat-panel-state"; @@ -254,28 +254,32 @@ After completing the implementation, provide a summary of what was done. function PRMergedBanner({ taskId }: { taskId: string }) { const taskPR = useAppStore((state) => state.taskPRs.byTaskId[taskId]); const [dismissed, setDismissed] = useState(false); - const { archiveTaskById } = useTaskActions(); + const archiveAndSwitch = useArchiveAndSwitchTask(); const { toast } = useToast(); const handleArchive = useCallback(async () => { try { - await archiveTaskById(taskId); + await archiveAndSwitch(taskId); toast({ description: "Task archived" }); } catch { toast({ description: "Failed to archive task", variant: "error" }); } - }, [taskId, archiveTaskById, toast]); + }, [taskId, archiveAndSwitch, toast]); if (taskPR?.state !== "merged" || dismissed) return null; return ( -
+
PR #{taskPR.pr_number} has been merged. You can archive this task.