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.