fix(story-mode): one turn per prompt written twice, and plan mode as planning

From review round 3 (compaction, then plan mode):

- A typed `/compact` and its <command-name> record share one promptId; they
  were two turns ("Done in 0 seconds", then a turn left running). A prompt
  carrying the open turn's promptId now joins it: one "You run /compact."
  turn of 24 s, the real compaction time. A command typed as such
  (`/model opus`) is a command too.
- Plan mode: writing the plan file under ~/.claude/plans/ is planning (a plan
  panel with its numbered steps), not a code change ("3 files changed" for
  a commit of 2). ExitPlanMode's input is empty in current builds; its
  result carries the plan and your answer: "Claude shows you its plan, and
  you approve it."
- A command's Director's caption names it ("Command · /compact").

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-18 11:26:34 +02:00
1 parent db9eb5cea3
commit dae6a52daa
3 files changed
+116 -14

No files matched your search

+48
View File
@@ -538,6 +538,54 @@ describe('buildStoryTurns', () => {
expect(model).toMatchObject({ prompt: '/model opus', ended: true });
});
it('one prompt written twice (typed /compact, then its command record) is one turn', () => {
const turns = buildStoryTurns(
entriesOf([
prompt('one'),
end(1000),
prompt('/compact', { promptId: 'p2' }),
{ type: 'system', subtype: 'compact_boundary', timestamp: '2026-10-18T09:50:00.000Z' },
prompt('<command-name>/compact</command-name><command-args></command-args>', { promptId: 'p2' }),
prompt('<local-command-stdout>Compacted</local-command-stdout>', { promptId: 'p2' }),
])
);
expect(turns).toHaveLength(2);
const page = buildStoryPage(turns[1]);
expect(page.panels.map((p) => p.kind)).toEqual(['mission', 'compact', 'complete']);
expect(page.panels[0].caption).toEqual({ story: 'You run /compact.', director: 'Command · /compact' });
expect(page.complete).toBe(true);
});
it('plan mode: the plan file is planning, and the plan and your answer come back with ExitPlanMode', () => {
const plan = '# Plan: add wrap\n\n1. Implement wrap.\n2. Add tests.\n3. Commit.';
const page = pageOf([
prompt('plan it'),
...bash('b0', 'ls', 'List', 'a'),
tool('w1', 'Write', { file_path: '/home/me/.claude/plans/radiant.md', content: plan }),
result('w1', 'File created', {
toolUseResult: {
type: 'create',
filePath: '/home/me/.claude/plans/radiant.md',
content: plan,
structuredPatch: [],
},
}),
tool('x1', 'ExitPlanMode', {}),
result('x1', 'User has approved your plan. You can now start coding.', { toolUseResult: { plan } }),
...edit('e1', '/w/a.py', ['+def wrap(): ...']),
end(1000),
]);
expect(page.panels.map((p) => p.kind)).toEqual(['mission', 'investigate', 'plan', 'ask', 'fix', 'complete']);
expect(page.panels[2].todos?.map((t) => t.text)).toEqual(['Implement wrap.', 'Add tests.', 'Commit.']);
expect(page.panels[2].caption.director).toBe('Write · plan file · 3 steps');
expect(page.panels[3].caption.story).toBe('Claude shows you its plan, and you approve it.');
expect(page.events.find((e) => e.tool === 'ExitPlanMode')).toMatchObject({
output: plan,
question: { answer: 'Approved' },
});
expect(page.filesChanged).toBe(1);
});
it('an interrupt ends the turn', () => {
const turns = buildStoryTurns(
entriesOf([prompt('go'), ...bash('b', 'ls', 'List', 'a'), prompt('[Request interrupted by user]')])