fix(story-mode): scheduled runs, work after a turn ended, and pages full of added prompts

From review round 4 (a 110 MB real session, about 23 turns, read through
all four tail steps):

- A scheduled task's run was drawn inside the /compact turn before it (96
  minutes and 237 tool calls credited to "You run /compact."): its prompt is
  a meta line, and work after an ended turn was appended to that turn. The
  scheduled prompt now starts a turn of origin `scheduled` ("A scheduled
  task starts this turn."), and work that comes more than 5 s after a turn
  ended starts a turn of its own ("Claude carries on"); a line Claude Code
  flushed a moment late still joins.
- A turn with seven prompts added mid-turn drew 14 panels (the cap is 12):
  missions were never folded. An added prompt may now fold into a montage,
  as the last resort after quiet panels and work panels, and the montage
  says so ("Claude keeps going (you add 2 things): ..."); the turn's own
  prompt and a question to you are never folded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-18 12:00:51 +02:00
1 parent 50de4f16b3
commit 846749a912
4 files changed
+97 -12

No files matched your search

+66
View File
@@ -617,6 +617,72 @@ describe('buildStoryTurns', () => {
expect(page.filesChanged).toBe(1);
});
it('a scheduled task (its prompt a meta line) is a turn of its own, not part of the one before', () => {
const turns = buildStoryTurns(
entriesOf([
prompt('/compact', { promptId: 'p1' }),
prompt('<command-name>/compact</command-name>', { promptId: 'p1' }),
prompt('<local-command-stdout>Compacted</local-command-stdout>', { promptId: 'p1' }),
{
type: 'system',
subtype: 'scheduled_task_fire',
timestamp: '2026-10-18T11:00:00.000Z',
prompt: 'Resume the work',
},
prompt('Resume the work', {
isMeta: true,
turnOrigin: 'scheduled',
promptSource: 'system',
scheduledTaskId: 'abc',
}),
...bash('b', 'ls', 'List', 'a'),
end(5000),
])
);
expect(turns.map((t) => [t.prompt, t.origin, t.endedBy])).toEqual([
['/compact', 'human', 'local command'],
['Resume the work', 'scheduled', 'turn_duration'],
]);
expect(buildStoryPage(turns[1]).panels[0].caption.story).toBe('A scheduled task starts this turn.');
});
it('work long after a turn ended is a turn of its own; a line flushed a moment late still joins', () => {
const turns = buildStoryTurns(
entriesOf([
prompt('one'),
end(1000),
{ ...said('Late line.'), timestamp: undefined },
{
type: 'assistant',
timestamp: '2026-10-18T12:00:00.000Z',
message: { content: [{ type: 'tool_use', id: 'x', name: 'Bash', input: { command: 'ls' } }] },
},
])
);
expect(turns).toHaveLength(2);
expect(turns[1]).toMatchObject({ origin: 'system', prompt: '', ended: false });
expect(turns[1].events.map((e) => e.tool)).toEqual(['Bash']);
});
it('a page with many prompts added mid-turn still fits, folding added prompts last', () => {
const queued = (text: string, i: number) => ({
type: 'attachment',
timestamp: `2026-10-18T09:5${i}:00.000Z`,
attachment: { type: 'queued_command', prompt: text, commandMode: 'prompt', origin: { kind: 'human' } },
});
const lines: unknown[] = [prompt('big job')];
for (let i = 0; i < 8; i++) {
lines.push(queued(`also do ${i}`, i));
lines.push(...edit(`e${i}`, `/w/f${i}.ts`, ['+x']));
lines.push(...bash(`b${i}`, 'npm test', 'Run tests', ' Tests 4 passed (4)'));
}
lines.push(end(60_000));
const page = pageOf(lines);
expect(page.panels.length).toBeLessThanOrEqual(STORY_LIMITS.panelsPerPage);
expect(page.panels[0].kind).toBe('mission');
expect(page.panels.some((p) => /you add \d+ things?/.test(p.caption.story))).toBe(true);
});
it('an interrupt ends the turn', () => {
const turns = buildStoryTurns(
entriesOf([prompt('go'), ...bash('b', 'ls', 'List', 'a'), prompt('[Request interrupted by user]')])