Slice 4: graceful project handoff (pending switch + ack)

Add pending-switch coordination to internal/activity (RequestSwitch/PendingSwitch/AckSwitch/CancelSwitch); AckSwitch atomically sets the active project and records switch-completed with Claude's summary. New MCP tools get_pending_switch and ack_switch (request is user-only via HTTP). HTTP GET/POST/DELETE /api/switch. New <handoff-bar> component (ask/waiting/cancel/completed) over SSE; activity-feed reflects switch-completed. Test covers request->ack->clear. Verified live: request/waiting/cancel over SSE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 08:27:22 -04:00
parent 65daedc902
commit 2b77e15b36
13 changed files with 449 additions and 10 deletions
+42
View File
@@ -159,6 +159,48 @@ func TestMCPRoundTrip(t *testing.T) {
if !found {
t.Fatalf("expected an active-project-changed event by claude, got %+v", act.Events)
}
// --- graceful handoff (§8.3): user requests, Claude acks -----------------
svc.RequestSwitch(activity.ActorUser, states[0].Path, "fix a bug")
res, _ = cs.CallTool(ctx, &mcpsdk.CallToolParams{Name: "get_pending_switch"})
var ps struct {
Pending bool `json:"pending"`
Target string `json:"target"`
}
decodeResult(t, res, &ps)
if !ps.Pending || ps.Target != states[0].Path {
t.Fatalf("expected pending switch to %q, got %+v", states[0].Path, ps)
}
res, err = cs.CallTool(ctx, &mcpsdk.CallToolParams{
Name: "ack_switch",
Arguments: map[string]any{"summary": "left tests green"},
})
if err != nil {
t.Fatalf("ack_switch: %v", err)
}
if res.IsError {
t.Fatalf("ack_switch tool error: %+v", res.Content)
}
res, _ = cs.CallTool(ctx, &mcpsdk.CallToolParams{Name: "get_pending_switch"})
decodeResult(t, res, &ps)
if ps.Pending {
t.Fatalf("expected no pending switch after ack, got %+v", ps)
}
// ack with nothing pending is a tool error.
res, err = cs.CallTool(ctx, &mcpsdk.CallToolParams{
Name: "ack_switch",
Arguments: map[string]any{"summary": "nothing"},
})
if err != nil {
t.Fatalf("ack_switch(empty) protocol error: %v", err)
}
if !res.IsError {
t.Fatalf("expected IsError acking with no pending switch")
}
}
// decodeResult unmarshals the JSON text content of a tool result into v.