diff --git a/src/main/agent-hooks/server.test.ts b/src/main/agent-hooks/server.test.ts index 169688ac5d3..275fc9b38b6 100644 --- a/src/main/agent-hooks/server.test.ts +++ b/src/main/agent-hooks/server.test.ts @@ -89,7 +89,7 @@ afterEach(() => { }) describe('AgentHookServer listener replay', () => { - it('retains the root Codex session across child-driven waiting and completion states', () => { + it('retains root Codex identity when relay child events omit it', () => { const server = new AgentHookServer() const providerSession = { key: 'session_id' as const, id: 'root-session' } @@ -97,7 +97,12 @@ describe('AgentHookServer listener replay', () => { { paneKey: PANE, providerSession, - payload: { state: 'working', prompt: 'coordinate reviewers', agentType: 'codex' } + payload: { + state: 'working', + prompt: 'coordinate reviewers', + agentType: 'codex', + model: 'gpt-5.4' + } }, 'conn-1' ) @@ -109,7 +114,10 @@ describe('AgentHookServer listener replay', () => { }, 'conn-1' ) - expect(server.getStatusSnapshot()[0].providerSession).toEqual(providerSession) + expect(server.getStatusSnapshot()[0]).toMatchObject({ + model: 'gpt-5.4', + providerSession + }) server.ingestRemote( { @@ -122,6 +130,7 @@ describe('AgentHookServer listener replay', () => { ) expect(server.getStatusSnapshot()[0]).toMatchObject({ state: 'done', + model: 'gpt-5.4', providerSession }) }) diff --git a/src/main/agent-hooks/server.ts b/src/main/agent-hooks/server.ts index 7fe09e92733..12fc82090af 100644 --- a/src/main/agent-hooks/server.ts +++ b/src/main/agent-hooks/server.ts @@ -824,14 +824,26 @@ export class AgentHookServer { this.onAgentStatus?.(enriched) return enriched } - // Why: Codex child hooks expose a child session id that normalization drops; retain the root resume identity across child-driven states. - const providerSessionPreservingPayload = + const previousCodexRoot = payload.payload.agentType === 'codex' && payload.toolAgentId && - !payload.providerSession && - previous?.payload.agentType === 'codex' && - previous.providerSession - ? { ...payload, providerSession: previous.providerSession } + previous?.payload.agentType === 'codex' + ? previous + : undefined + const preservedProviderSession = !payload.providerSession + ? previousCodexRoot?.providerSession + : undefined + const preservedRootModel = !payload.payload.model ? previousCodexRoot?.payload.model : undefined + // Why: an SSH relay restart forgets root-only fields; child hooks must not erase durable resume/model identity. + const rootContextPreservingPayload = + preservedProviderSession || preservedRootModel + ? { + ...payload, + ...(preservedProviderSession ? { providerSession: preservedProviderSession } : {}), + payload: preservedRootModel + ? { ...payload.payload, model: preservedRootModel } + : payload.payload + } : payload const identity = resolveAgentStatusIdentity({ existing: previous @@ -841,7 +853,7 @@ export class AgentHookServer { updatedAt: previous.receivedAt } : undefined, - incoming: providerSessionPreservingPayload.payload.agentType, + incoming: rootContextPreservingPayload.payload.agentType, now }) if ( @@ -854,12 +866,12 @@ export class AgentHookServer { return previous } const identityResolvedPayload = - identity.agentType === providerSessionPreservingPayload.payload.agentType - ? providerSessionPreservingPayload + identity.agentType === rootContextPreservingPayload.payload.agentType + ? rootContextPreservingPayload : { - ...providerSessionPreservingPayload, + ...rootContextPreservingPayload, payload: { - ...providerSessionPreservingPayload.payload, + ...rootContextPreservingPayload.payload, agentType: identity.agentType } } diff --git a/src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx b/src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx index 380bb9194c4..d5591b9e9d5 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx @@ -357,6 +357,28 @@ describe('WorktreeCardAgents', () => { expect(markup).not.toContain('Prompt cache expires') }) + it('does not repeat a subagent role used as its compact primary label', async () => { + const { CompactAgentRow } = await import('./worktree-card-compact-agents') + + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('reviewer') + expect(markup).not.toContain(' - Reviewer') + }) + it('marks only the focused agent row', async () => { mockAgentActivityDisplayMode = 'full' mockFocusedAgentPaneKey = 'tab-1:2' diff --git a/src/renderer/src/components/sidebar/worktree-card-compact-agent-row.tsx b/src/renderer/src/components/sidebar/worktree-card-compact-agent-row.tsx index 28608ed53df..8ce6368a780 100644 --- a/src/renderer/src/components/sidebar/worktree-card-compact-agent-row.tsx +++ b/src/renderer/src/components/sidebar/worktree-card-compact-agent-row.tsx @@ -63,7 +63,15 @@ function getCompactAgentSecondary(agent: DashboardAgentRowData): string { return toolName } } - return agent.entry.lastAssistantMessage?.trim() || formatAgentTypeLabel(agent.agentType) + const lastAssistantMessage = agent.entry.lastAssistantMessage?.trim() + if (lastAssistantMessage) { + return lastAssistantMessage + } + // Why: child rows without descriptions use their role as primary text; repeating its formatted label adds no information. + if (agent.rowSource === 'subagent' && agent.entry.prompt?.trim() === agent.agentType.trim()) { + return '' + } + return formatAgentTypeLabel(agent.agentType) } function getCompactAgentTime(agent: DashboardAgentRowData, now: number): string | null {