fix(story-mode): appends without invented line numbers, printed files you can open, whose words start a turn

The rest of review round 5 (a phone with touch):

- Text appended with `cat >> f <<EOF` or `echo x >> f` was drawn as lines
  1 to 8 of the file; where an append lands is not in the command, so the
  diff now says "Added at the end of the file (8 lines)" with no numbers.
- A command that prints a file and does something else (`cat textstats.py
  && pytest`) kept no record of the file; it is now on the event,
  resolved against the command's own folder, and the output drawer offers
  "Open textstats.py". (A file rewritten by a python here-document still
  cannot be diffed from the command alone; Claude Code's own "modified"
  note, when it comes, is used.)
- A background job's report that starts a turn was a balloon credited to
  "you" in the API (the page already drew it as a note); its speaker is
  now "note".
- The output drawer tagged a command still running in the background
  "ok"; it says "in the background".
- The Header buttons hint says that on a phone, Story Mode opens from
  Session Options.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-18 12:55:43 +02:00
1 parent 76d5c61eda
commit 6a1bac6ebe
7 files changed
+119 -15

No files matched your search

+28
View File
@@ -535,6 +535,34 @@ describe('story-mode.js overlay', () => {
expect($$('.story-ln--add').map((l) => l.textContent)).toEqual(['1+const a = 1;']);
});
it('an append draws without line numbers, and a check offers the file it printed', async () => {
const lines = [
prompt('add and test'),
...bash(
'b1',
"cat >> notes.md <<'EOF'\nmore\nEOF\ncat stats.py && pytest -q",
'Append, show and test',
'def f(): pass\n5 passed in 0.1s'
),
end(1000),
];
(lines[1] as { cwd?: string }).cwd = '/work/demo-shop';
boot(storyOf(lines));
app.openStoryMode(SID);
await settle();
sizeAndDraw();
$('.story-panel[data-kind="check"] .story-hit')!.click();
const open = $$('.story-drawer-body .story-out-files button');
expect(open.map((b) => b.textContent)).toEqual(['Open stats.py']);
open[0].click();
expect(app.openFilePreview).toHaveBeenLastCalledWith('/work/demo-shop/stats.py', SID);
$$('.story-drawer-body button')
.find((b) => b.textContent === 'See the file it wrote')!
.click();
expect($('.story-ln--hunk')!.textContent).toBe('Added at the end of the file (1 line)');
expect($$('.story-ln--add').map((l) => l.textContent)).toEqual(['+more']);
});
it('while a question waits, the pencils say so; the chosen answer is marked, not struck', async () => {
const ask = (answered: boolean) =>
storyOf([
+40
View File
@@ -975,6 +975,46 @@ describe('buildStoryPage', () => {
expect(heredocWrites('tee -a notes.md <<EOF\nx\nEOF', '/w').map((d) => d.file)).toEqual(['/w/notes.md']);
});
it('an append has no line numbers it cannot know; a whole-file write starts at line 1', () => {
const [append] = heredocWrites("cat >> notes.md <<'EOF'\none\ntwo\nEOF", '/w');
expect(append.hunks[0]).toMatchObject({ atEnd: true, newStart: 0, newLines: 2 });
const [echoed] = heredocWrites('echo "more" >> notes.md', '/w');
expect(echoed.hunks[0]).toMatchObject({ atEnd: true, newStart: 0 });
const [whole] = heredocWrites("cat > notes.md <<'EOF'\none\nEOF", '/w');
expect(whole.hunks[0]).toMatchObject({ newStart: 1, newLines: 1 });
expect(whole.hunks[0].atEnd).toBeUndefined();
});
it('files a command prints are on its event, resolved against its cwd, whatever it does', () => {
const lines = [
prompt('rewrite and test'),
...bash('b1', 'cat textstats.py && python3 -m pytest -q', 'Show and test', 'def f(): pass\n5 passed in 0.1s'),
end(1000),
];
(lines[1] as { cwd?: string }).cwd = '/w';
const page = pageOf(lines);
expect(page.events[1]).toMatchObject({ intent: 'test', files: ['/w/textstats.py'] });
});
it('words that start a turn but are not yours are a note, not your balloon', () => {
const turns = buildStoryTurns(
entriesOf([
prompt('go'),
end(1000),
prompt(
'<task-notification>\n<task-id>bg1</task-id>\n<status>completed</status>\n<summary>Background command \"Run tests\" completed (exit code 0)</summary>\n</task-notification>',
{ turnOrigin: 'task_notification' }
),
said('All good.'),
end(1000),
])
);
expect(buildStoryPage(turns[0]).panels[0].balloon?.who).toBe('you');
const report = buildStoryPage(turns[1]).panels[0];
expect(report.caption.story).toBe('A background job reports back.');
expect(report.balloon?.who).toBe('note');
});
it('a loop keeps every run: a lint failure inside a round that passed in the end still shows', () => {
const page = pageOf([
prompt('build'),