diff --git a/docs/api-reference.md b/docs/api-reference.md index 0ab02f61..3dee5372 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1083,7 +1083,7 @@ A Bash call sent to the background (its result is only `Command running in backg A `page` carries the turn's summary fields plus: - `events`: one per prompt, sentence Claude wrote and tool call. Each has `intent`, `tool`, `label` and, when the call returned, `ok` (`false` when the result was flagged as an error, or reported failed tests or lint problems a pipe kept from the exit code). Depending on the call: `edit` (an Edit/Write, hunks in Claude Code's own `structuredPatch` shape), `edits` (files a Bash call wrote through here-documents or `printf`/`echo` redirects, that a python or node script fed by a here-document opens for writing, or that Claude Code says it changed; the last two have no lines, since what changed is not in the command; an append's hunk has `atEnd: true` and no line numbers, since where it lands is not in the command), the tail of `output`, `tests` (`{ passed, failed, errors? }`, counted on the whole output before it is cut), `helper`, `files` (files a Bash call printed with `cat`/`head`/`tail`, resolved against the command's folder), `headline` (the part of a one-liner that decided its intent), `commits`, `subject` (the first commit's message, when the command spells it out with `-m` or a `git commit -F -` here-document), `ships` (`push` / `pr`), `running`, `background` and `finishedLater` (see Background work), `keyPoints` (for a sentence Claude wrote: up to two sentences from further into a long message that a reader must not lose, such as an open problem, something not yet done or checked, or a word on what you asked for; read on the whole message, past the `label`'s clip, and used by the final panel's balloon), `question` and, for prompts, `origin`. The advisor (a server tool) is an event with tool `Advisor` and intent `helper`; its answer is encrypted in the transcript, so it is never a panel of its own: the panel of the step after it (or the last one, when nothing follows) holds its events and counts them in `secondOpinion`. In plan mode the plan file under `~/.claude/plans/` is planning (intent `plan`, its numbered steps as `todos`), and `ExitPlanMode`'s result carries the plan (`output`) and your answer (`Approved` / `Sent back`). -- `panels`: at most 12, each `{ kind, events, caption: { story, director }, balloon?, files?, notes?, helpers?, todos?, tests?, ok?, rounds?, roundsOk?, question?, live? }`, with `events` as indices into `events`. A run panel's `printed` holds the first lines its last command printed (its last lines when it failed), for the terminal card: the card is drawn on the canvas and goes into a saved image, so lines that look like secrets are left out and addresses and long token-like strings are masked. `balloon.who` is `you`, `claude` or `note` (words that start a turn but are not yours: a background job's report, another session's message). A loop's `roundsOk` lists every test run in order, `true` where it passed and `null` for one still running, so a failure inside a round that passed in the end still shows, and a live loop shows the runs that finished. Kinds: `mission`, `investigate`, `research`, `plan`, `backup`, `fix`, `check`, `loop` (fix and check rounds folded), `run`, `save`, `ask`, `compact`, `montage` (quieter panels folded to fit) and `complete`. +- `panels`: at most 12, each `{ kind, events, caption: { story, director }, balloon?, files?, notes?, helpers?, todos?, tests?, ok?, rounds?, roundsOk?, question?, live? }`, with `events` as indices into `events`. A run panel's `printed` holds the first lines its last command printed (its last lines when it failed), for the terminal card: the card is drawn on the canvas and goes into a saved image, so lines that look like secrets are left out and addresses and long token-like strings are masked. `balloon.who` is `you`, `claude` or `note` (words that start a turn but are not yours: a background job's report, another session's message); a helper's report keeps its own words there (`"" reports: …`, its first sentence and key points, absolute paths shortened to their last two parts). A loop's `roundsOk` lists every test run in order, `true` where it passed and `null` for one still running, so a failure inside a round that passed in the end still shows, and a live loop shows the runs that finished. Kinds: `mission`, `investigate`, `research`, `plan`, `backup`, `fix`, `check`, `loop` (fix and check rounds folded), `run`, `save`, `ask`, `compact`, `montage` (quieter panels folded to fit) and `complete`. - `filesChanged`, `added`, `removed` (summed per edit), `helpers` (the turn's own helper calls; a report turn's prompt carries the helper it answers, which is not counted), `helpersRunning`, `noneWorked` (every tool call of the turn failed, so the last panel is no celebration; a command whose last check only found nothing, exit code 1 after its answer, does not count as failed), `durationMs`, `partial` (the turn began before the part that was read) and `truncated` (older outputs, hunks and helper texts were dropped to keep the page small: a page carries at most 120,000 characters of them, newest first; an edit that lost its lines keeps its counts and an empty `hunks`). ## Git worktrees of a case diff --git a/src/story-mode.ts b/src/story-mode.ts index 5dc6da7a..f43f26c2 100644 --- a/src/story-mode.ts +++ b/src/story-mode.ts @@ -2376,6 +2376,26 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde : lines[origin]; // Words that started the turn but are not yours are a note, not your balloon. if (panel.balloon && !command && origin !== 'human') panel.balloon = { ...panel.balloon, who: 'note' }; + // A helper reporting back: its own words, not only the line saying it finished, so what it + // did not do ("I haven't run them against the real file") is on the page. + const report = origin === 'task' && evs[0]?.helper?.result?.trim(); + if (panel.balloon && report) { + // A report that is only a path says which file it wrote: its name is enough. + const said = ( + /^\S+$/.test(report) && report.includes('/') ? basename(report) : closingText(report, keyPointsOf(report)) + ) + // An absolute path is mostly room: its last two parts ("tests/test_extra.py") say which file. + // Run on the clipped balloon (560 characters at most), so the scan stays short. + .replace(/(? { expect(panel).toBeTruthy(); }); + it('a helper reporting back is heard in its own words, not only "finished" (round 9)', () => { + const [, readme, extra] = pagesOf('background-helpers-reports'); + expect(readme.panels[0].balloon).toBe('"Helper A: write README.md" reports: README.md'); + // What the helper did not do is on the page, and a long path does not crowd it out. + expect(extra.panels[0].balloon).toMatch( + /^"Helper B: write extra pytest tests" reports: I wrote the file: tests\/test_extra\.py / + ); + expect(extra.panels[0].balloon).toMatch(/I haven't run it against the real wordcount\.py\.$/); + }); + it('a blocked command ends a turn where nothing worked, without a celebration', () => { const last = pagesOf('teammates-then-interrupt')[4]; expect(last.panels.map((p) => p.story)).toEqual([