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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user