fix(acp): keep todo indicator alive across session resume (#2315)
This commit is contained in:
@@ -506,21 +506,7 @@ func (a *Adapter) LoadSession(ctx context.Context, sessionID string, mcpServers
|
||||
// the frontend doesn't render a stuck "watching" card.
|
||||
a.sweepMonitorsOnReplayEnd(sessionID)
|
||||
|
||||
if replayPlan != nil {
|
||||
entries := make([]PlanEntry, len(replayPlan.Entries))
|
||||
for i, e := range replayPlan.Entries {
|
||||
entries[i] = PlanEntry{
|
||||
Description: e.Content,
|
||||
Status: string(e.Status),
|
||||
Priority: string(e.Priority),
|
||||
}
|
||||
}
|
||||
a.sendUpdate(AgentEvent{
|
||||
Type: streams.EventTypePlan,
|
||||
SessionID: sessionID,
|
||||
PlanEntries: entries,
|
||||
})
|
||||
}
|
||||
a.emitReplayPlan(sessionID, replayPlan)
|
||||
|
||||
// Emit session status event to normalize with other adapters.
|
||||
// This eliminates the need for ReportsStatusViaStream flag.
|
||||
@@ -544,6 +530,31 @@ func (a *Adapter) ResetSession(ctx context.Context, mcpServers []types.McpServer
|
||||
return a.NewSession(ctx, mcpServers)
|
||||
}
|
||||
|
||||
// emitReplayPlan re-emits the plan captured during session/load replay so the todo
|
||||
// indicator survives a resume. An empty plan is dropped: on the ACP ingest path an
|
||||
// empty entry list carries no information (same reasoning as extractTodoItems), and
|
||||
// re-emitting it would both wipe the live indicator and persist an empty todo snapshot
|
||||
// that shadows the last real one in the chat timeline. A genuine "agent cleared the
|
||||
// todos" still arrives as a live plan update and keeps its persisted empty snapshot.
|
||||
func (a *Adapter) emitReplayPlan(sessionID string, replayPlan *acp.SessionUpdatePlan) {
|
||||
if replayPlan == nil || len(replayPlan.Entries) == 0 {
|
||||
return
|
||||
}
|
||||
entries := make([]PlanEntry, len(replayPlan.Entries))
|
||||
for i, e := range replayPlan.Entries {
|
||||
entries[i] = PlanEntry{
|
||||
Description: e.Content,
|
||||
Status: string(e.Status),
|
||||
Priority: string(e.Priority),
|
||||
}
|
||||
}
|
||||
a.sendUpdate(AgentEvent{
|
||||
Type: streams.EventTypePlan,
|
||||
SessionID: sessionID,
|
||||
PlanEntries: entries,
|
||||
})
|
||||
}
|
||||
|
||||
// emitInitialModeState emits a session_mode event from the session response's Modes field.
|
||||
// Called after session/new and session/load to provide the initial mode state.
|
||||
func (a *Adapter) emitInitialModeState(modes *acp.SessionModeState) {
|
||||
|
||||
+14
-15
@@ -212,21 +212,7 @@ func TestLoadSuppression_PlanReemittedAfterLoad(t *testing.T) {
|
||||
a.isLoadingSession = false
|
||||
a.mu.Unlock()
|
||||
|
||||
if replayPlan != nil {
|
||||
entries := make([]PlanEntry, len(replayPlan.Entries))
|
||||
for i, e := range replayPlan.Entries {
|
||||
entries[i] = PlanEntry{
|
||||
Description: e.Content,
|
||||
Status: string(e.Status),
|
||||
Priority: string(e.Priority),
|
||||
}
|
||||
}
|
||||
a.sendUpdate(AgentEvent{
|
||||
Type: streams.EventTypePlan,
|
||||
SessionID: "s1",
|
||||
PlanEntries: entries,
|
||||
})
|
||||
}
|
||||
a.emitReplayPlan("s1", replayPlan)
|
||||
|
||||
// Now we should see the re-emitted plan.
|
||||
events = drainEvents(a)
|
||||
@@ -254,6 +240,19 @@ func TestLoadSuppression_PlanReemittedAfterLoad(t *testing.T) {
|
||||
a.mu.RUnlock()
|
||||
}
|
||||
|
||||
func TestLoadSuppression_EmptyReplayPlanNotReemitted(t *testing.T) {
|
||||
a := newTestAdapter()
|
||||
|
||||
// A replay that ends with an empty plan must not re-emit it: the event would
|
||||
// clear the live todo indicator and persist an empty todo snapshot that hides
|
||||
// the last real one.
|
||||
a.emitReplayPlan("s1", &acp.SessionUpdatePlan{Entries: []acp.PlanEntry{}})
|
||||
|
||||
if events := drainEvents(a); len(events) != 0 {
|
||||
t.Fatalf("expected no event for an empty replay plan, got %d: %+v", len(events), events)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSuppression_PostReplayEventsPassThrough(t *testing.T) {
|
||||
a := newTestAdapter()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user