diff --git a/src/story-mode.ts b/src/story-mode.ts index 90c9222f..f3d29e9a 100644 --- a/src/story-mode.ts +++ b/src/story-mode.ts @@ -2104,11 +2104,7 @@ function groupPanels(events: StoryEvent[]): { const kind = ev.todos && ev.intent === 'say' ? 'plan' : PANEL_FOR[ev.intent]; if (!kind) return; const last = panels[panels.length - 1]; - const advisorMix = - kind === 'backup' && - last?.kind === 'backup' && - (events[last.events[0]].tool === 'Advisor') !== (ev.tool === 'Advisor'); - if (last && last.kind === kind && MERGEABLE.has(kind) && !advisorMix) { + if (last && last.kind === kind && MERGEABLE.has(kind)) { last.events.push(idx); takeOpinions(last); if (pendingSay && !last.balloon) last.balloon = { who: 'claude', text: pendingSay }; @@ -2424,8 +2420,9 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde break; } case 'backup': { + // A second opinion riding on this panel is no helper of its own. const helpers = draft.events - .filter((i) => events[i].helper) + .filter((i) => events[i].helper && (!panel.secondOpinion || events[i].tool !== 'Advisor')) .map((i) => ({ name: (events[i].helper as StoryHelper).name, type: (events[i].helper as StoryHelper).type, diff --git a/test/fixtures/story-corpus/bash-script-background-helper.expected.json b/test/fixtures/story-corpus/bash-script-background-helper.expected.json index 4d536665..6794ca43 100644 --- a/test/fixtures/story-corpus/bash-script-background-helper.expected.json +++ b/test/fixtures/story-corpus/bash-script-background-helper.expected.json @@ -51,7 +51,7 @@ { "kind": "backup", "ok": null, - "story": "Claude calls in 2 helpers.", + "story": "Claude sends 1 helper off to work in the background.", "director": "Agent · general-purpose · 6 calls inside · after 1 second opinion", "balloon": null, "tests": null, diff --git a/test/story-mode.test.ts b/test/story-mode.test.ts index a8aa10b3..b4b94a0d 100644 --- a/test/story-mode.test.ts +++ b/test/story-mode.test.ts @@ -1312,6 +1312,32 @@ describe('buildStoryPage', () => { ); }); + it('a second opinion in front of two helpers neither splits them nor counts as one', () => { + const advisor = { + type: 'assistant', + timestamp: '2026-10-18T09:41:20.000Z', + message: { + content: [ + { type: 'server_tool_use', id: 'srv1', name: 'advisor', input: {} }, + { type: 'advisor_tool_result', tool_use_id: 'srv1', content: { type: 'advisor_redacted_result' } }, + ], + }, + }; + const page = pageOf([ + prompt('go'), + advisor, + tool('a1', 'Agent', { description: 'A', subagent_type: 'general-purpose', prompt: 'x' }), + result('a1', 'done A'), + tool('a2', 'Agent', { description: 'B', subagent_type: 'general-purpose', prompt: 'y' }), + result('a2', 'done B'), + end(1000), + ]); + const backups = page.panels.filter((p) => p.kind === 'backup'); + expect(backups).toHaveLength(1); + expect(backups[0]).toMatchObject({ secondOpinion: 1, caption: { story: 'Claude calls in 2 helpers.' } }); + expect(backups[0].helpers?.map((h) => h.task)).toEqual(['A', 'B']); + }); + it('the advisor (a server tool) is a helper giving a second opinion', () => { const page = pageOf([ prompt('think hard'),