Repository navigation
Conversation
Story Mode is an opt-in header button (showStoryButton, per device, default off, kept off phones) that opens the active Claude Code session as comic pages: one page per prompt, panels for reading code, planning, helpers, edits, tests (POW/KRAK), questions and saving, each with a plain story caption and a Director's cut caption naming the tools. Files open in the existing file preview, the code page opens the exact diff, a test board opens the output, and a running turn redraws as Claude works. Server: GET /api/sessions/:id/story (experimental) reads a bounded tail of the session's own transcript. A pure core (src/story-mode.ts) reduces lines, joins tool calls with their results, splits turns at prompts and turn_duration, and groups events into at most 12 panels; Bash calls are sorted by what the command does. The reader (src/story-reader.ts) caches reduced entries and reads only appended bytes while the file grows. Frontend: story-mode.js (canvas frame engine, live refresh from transcript and hook SSE events plus a poll while a turn runs, details drawer, the wire) and story-mode.css, with two self-hosted OFL comic fonts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codeman's file preview has no Escape of its own outside the folder grid, so with Story Mode on top it stayed open and blocked the page. While Story Mode is open, Escape now closes one layer per press: the preview, then the drawer, then the overlay. A folder grid and a preview in edit mode keep their own keys. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…turn bigger than the tail GET /story now sends a weak ETag (server start, transcript size and mtime, turn) with Cache-Control: no-cache, and answers 304 before building anything when the transcript has not changed, so a live view's polls cost a stat. A turn can be bigger than the 8 MB tail on its own (tool results carry whole files, ~60 KB per result line on large local transcripts), which cut its first panels. When the window holds no prompt, the reader re-reads a 24 MB, then a 64 MB tail. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n every shape Runs the real story-mode.js against pages the real server core builds from invented transcripts: panels, captions, balloons, the turn picker, the Director's cut wire, file and diff clicks with their line numbers, output links through the terminal's own path matcher, a live refresh that keeps existing panels' clocks, activity filtering, Escape one layer per press, teardown, error notes and the setting switch. A permissive fake 2D context lets every scene draw every panel kind across its whole animation, at four sizes, in both reading levels, without throwing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cores in balloons Found by watching real workers: `node --test` was drawn as plain terminal work, `git -c user.name=x commit` was not seen as saving, and a probe such as `pytest --version` became a failing test. Commands are now split into their simple commands (quote-aware, at && || ; | and newlines); each part is judged by what it starts with after env assignments and wrappers like timeout and sudo, git's own -c/-C options are skipped, a --version/--help probe is not a run, and the strongest intent wins (test, save, build, edit, run, look). File housekeeping (mkdir, touch, cp, mv, rm) and git init/add are plain commands. Balloons dropped every _ and * as markdown (test_wcount.py read testwcount.py); now only paired emphasis at word boundaries and line-start markers go. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GET /api/sessions/:id/story/helpers/:agentId draws one helper (an Agent call)
as a page from its own transcript, <conversation>/subagents/agent-<id>.jsonl
beside the session's, with the type, description and finished flag from its
.meta.json. The file is derived from the session's own transcript path and an
id checked against [A-Za-z0-9_-]{1,64}; a helper with no file yet is page
null, not an error. The reducer gains a sidechain mode (every helper line is a
sidechain line) and keeps the agentId of Agent results, so the main page links
each helper to its steps. Same gates and ETag/304 as /story, now shared.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A real server in test mode, real Chromium, a synthetic transcript handed over through a real stop hook: off by default, the header button draws the running turn, a grown transcript plus a hook redraws it live over SSE, the code page opens the diff, Escape closes drawer then overlay, and a phone-width viewport has no sideways scroll. Listed in BROWSER_TEST_GLOBS (nightly, not the gate). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ones, and polish - Save image: the page as one PNG to share (each panel canvas, then every caption, balloon and chip redrawn from its computed style and box). - The helper drawer lists what the helper did, read live from its own transcript (GET .../story/helpers/:agentId), led by the task Claude gave it. - Session Options has a Story button (same setting), the way in on phones. - A turn a background task or another session started reads as a notice, not as your speech balloon. - Wide panels keep their scene in a centered stage instead of spreading the art to both ends; the fix panel's agent stands next to the page. - An unchanged page (same ETag) is neither parsed nor redrawn. - Paths inside the session's folder are shown short; clicks keep full paths. - Tab stays inside the overlay; a missing 2D context skips the art, not the page. - A rebindable Story Mode shortcut (no default chord). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every visible panel was repainted on every animation frame: five panels cost ~81% of one core (main thread, 2x screen, measured with Chrome's own task counters). Panels now repaint at 30 fps while their entrance plays, 15 fps for the newest panel of a running turn, and 6 fps once settled; a settled panel holds still after 20 s until the pointer passes over it. Clocks still run in real time. Measured: 11.6% in the first 10 s, 2.4% once the page has settled, 5.1% with the pointer on a panel. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ent loop The first read decoded the whole tail as one string and parsed it in one synchronous pass. Lines are now decoded one at a time straight from the buffer and the loop yields every 1 MB. Measured on the largest local transcripts (110, 64, 49 MB): a cold read takes 39-50 ms instead of 100-123 ms, and the longest event-loop stall is 8-10 ms. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three review agents watched real Claude Code workers build small programs and compared every panel with the transcript. Replaying their transcripts through the core found, and now fixes: - A Bash call was classified from its 400-character display clip, so a commit or a test run late in a long one-liner vanished (a failing pytest run drew as "saves the work"). The intent, the printed files and whether a save ships are now read from the whole command. - A test run that reports failures failed, even when `| tail` hid the exit code. With that, the real fix-test-fix-test sequence folds into one loop: "Fix and test, 2 rounds. It passes in the end." - node:test (`# pass 7`), all-failing pytest (`3 failed in 0.1s`) and pytest errors are counted. - A helper started in the background was drawn as finished, with Claude Code's internal launch notes as its answer. It now stays running until its task notification arrives; the answer, tool count and duration are joined back onto it, and the final panel says when helpers are still out. - The notification turn reported Claude Code's own duration, counted from the first prompt (2 min 38 s for a 21 s turn); a turn is capped at its span. - Logging into scratch (`| tee "$S/dry.txt"`, `> /tmp/x`) counted as an edit; redirect targets are read quote-aware, and a here-document's body is data. - A probe (`pytest --version`) reads as looking around, not running a tool. - A plan Claude writes as a numbered list (no plan tool, as in current Claude Code builds) is drawn as a plan panel; files it cats are file cards. - A balloon: code spans stay whole, an ellipsis or e.g. does not end the sentence, a remark about the check that just ran stays on that check, and a loop shows the last word of its rounds. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s that keep up From review round 1, checked against screenshots of the reviewers' real transcripts at desktop and phone width: - Claude's balloon is placed in the band above the head the scene drew, its tail pointing down at it, and clamped to stay inside the panel and off the head. Per-kind CSS offsets had covered the POW burst, the agent's head on a half-width fix panel and the first line of a terminal; a long prompt ran out of the mission panel. Save image clamps the same way. - A board only shows real counts; a check that reported none shows one verdict instead of 21 invented dots, and the loop shows its last run. - The mission panel opens the whole prompt, and on a task turn what the background job reported. - A background helper says "in the background" and "still working" until it reports back, its drawer keeps polling while it works, and the final panel counts helpers still out. - A fix made from the terminal opens what ran instead of an empty diff. - The wire files Claude's words under the panel that draws them, and its last words under the final panel. - A drawer follows its panel by the first event it showed when a refresh regroups panels (a check folding into a loop), not by kind. - Click regions reused for a new page get the new label; a new turn draws all its panels fresh. - Output links skip globs and paths a tool elided; prose links rooted paths only; paths inside the session folder read short (clicks keep the full path), and files Claude printed with cat open relative to that folder. - File cards keep a name's separators (rename_by_ / date.sh), code drops ligatures (`!==` stays three characters), counts drop "−0", and the turn picker's text stops before its chevron. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and end turns Found by replaying round-2 review transcripts while the reviews ran, then surveyed across 150 local transcripts: - A prompt typed while Claude works writes no user line in current builds, only a queued_command attachment where Claude took it in (602 of them locally, 485 of those background-task reports). It now joins the running turn; a report that lands mid-turn is a notice on the wire, not a new mission panel, and a background helper's answer joins back from it. - Some sessions never write turn_duration (13 of 135 locally): the Stop hook's stop_hook_summary (never preventedContinuation in 815 seen) now ends the turn, timed by its own span. Before, such a session's turns never ended. - A helper started as a teammate (teammate_spawned) is a background helper; its <teammate-message> reports (text or a JSON idle notification) join it by name, and the turn they start reads "readme-writer: README done". - A <cross-session-message> names its sender; pasted text loses Claude Code's <pasted_content> wrapper (it showed in balloons and the picker). - The mission panel, its balloon and its drawer follow who sent each prompt (you, a background job, another session), not who started the turn. - A caption names a script by its first line that says something, not `set -e`, a bare `cd` or `S=/tmp/...`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nd at a check
From review round 2 (a question, failing tests first, and a push):
- A run that reported failed tests is now failed on the event itself, so
its drawer and wire tags agree with the panel (the TDD red run was a red
panel with a green "ok" tag; `; echo "exit=$?"` had hidden the exit code).
- A save whose command failed says so ("Claude tries to send the work up,
and the command fails."), stamps ERROR instead of SAVED, and counts the
error in the Director's cut. A save still running stamps nothing.
- A loop ends at its last check: README, .gitignore and the like written
after the tests passed are their own fix panel instead of disappearing
into the loop's diff.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The shortcut registry gained a Story Mode entry; every registry label must translate (test/i18n-exit-run-help.test.ts, caught by the full gate). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s, from review round 2 - Files a Bash call writes through here-documents (`cat > f <<'EOF'`, `tee`) are whole-file edits on the event: in the diff drawer, on the code page, in "N files changed". Before, a worker that wrote seven files that way drew a page with zero files changed. Labels leave the bodies out (the wire row was a wall of file text), and a relative target resolves against the transcript line's cwd unless the command changed directory first. - A caption names the part of a one-liner that decided it (`node --test`, the commit), not its first line. - The advisor (a server tool, answer encrypted in the transcript) is drawn: "Claude asks the advisor for a second opinion." It is not counted as a helper. Server web search maps to the web panel. - Checks that all pass from the start read "Build and test, N rounds. Every check passes.", not as fixing. - pytest's collection errors are counted apart: "The tests don't even load." - A result remark replaces a step announcement on its check. - A turn still open when the next prompt starts has ended; an interrupted turn is timed by its span. - The mission drawer keeps prompts up to 4000 characters. - Drawer: diff headers, titles and commands read relative to the session folder (the folder itself reads "."); a bare name under an `ls dir:` header links into that folder; the chosen answer is marked, not struck through; boards count errors. - While a question waits, the pencils say "Waiting for your answer…"; balloons start below the LIVE tag; the loop's balloon points at its verdict, above the board. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…holds still
From review round 2 (a long turn, and a prompt added mid-turn):
- Commits fold last and a montage names them ("saves the work in 3
commits"); a save counts its commits ("saves the work in 6 commits").
Before, all three commits of a 16-step turn vanished into montages that
read "runs 2 commands".
- Folding a full page starts at the oldest pair and grows a montage already
there, and never takes the newest panel while another choice exists: a
live page no longer reshuffles panels the reader is watching.
- A check whose output reports problems ("1 file would be reformatted",
"Found 3 errors", "✖ 5 problems", `error TS…`) failed, though a trailing
`; ls` hid the exit code: "The tests pass, but another check fails."
- Shell structure is not a command (`for x in`, `do`, `done`, `if`), so a
`--version` probe in a loop reads as looking; `command -v` and
`git config --get` too.
- The montage draws only the kinds of steps it folded, with a box for
saves; the loop's arrow has a head and rests on a finished page; the
"MISSION COMPLETE!" title has room for its tilt; drawer commands keep
their line breaks.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
"Commits are in." landed on the check panel after the save it reports on; the rule that keeps a result remark on its check now covers saves too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A local slash command (`/compact`, `/model`) gets no reply from Claude and no turn_duration, so its turn stayed "Claude is working" until the next prompt. It now ends at the command's own output (<local-command-stdout>, kept for the mission drawer) and reads "You run /compact.". `/compact` writes its compaction marker before the command line itself; a marker that lands between turns now belongs to the turn after it (it was drawn at the end of the previous, finished turn), and one mid-turn stays where it is. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…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>
… 3 found missing
From review round 3:
- Keyboard: a file opened from a panel or drawer takes focus on the
preview's close button, Tab cycles inside the preview while it is open,
and Escape gives focus back to what opened it. Before, Tab walked out of
the overlay to the session tabs behind it. A panel rebuilt under focus (a
new turn, a fold into a loop) hands focus to its replacement.
- A commit at the end of a test one-liner is no longer lost: commits and
pushes are counted whatever intent wins, and the check says "The tests
pass, and Claude saves the work." (plus "1 commit" in the Director's cut).
- Files written by `printf/echo ... > f`, and files Claude Code says a
command changed ("This command modified 1 file you've previously read"),
count as changed files.
- A compaction says what it did ("automatic · 46.9k → 3.6k tokens · 24 s");
an automatic one reads "Claude's memory is full", and its panel opens.
- ToolSearch is looking around, not a terminal panel; EnterPlanMode reads
"Claude switches to plan mode"; the plan drawer shows the plan you
approved ("You approved the plan.").
- A finished newest page is checked every 15 s (a 304 when unchanged): a
local command like /compact raises no hook.
- Boards draw the real number of dots (5 tests, 5 dots); captions leave
here-document markers out.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A report's turn links to the helper it answers: its drawer shows what the
helper did. Reports arrive HTML-escaped (`<a@b.c>`) and now read as
text.
- Screen readers: a polite status region says when a new turn starts and
when one finishes; the note is a status, the drawer a dialog named by its
title; "Open file" buttons name their file.
- Save image draws the balloons' tails (from their CSS triangles) and is
disabled with nothing to save.
- App toasts move to the bottom while Story Mode is open: at the top they
covered its bar and turn picker on phones.
- The bar names the session as its tab does; a click on the mission balloon
opens the prompt.
- A heading ends at its line (a plan's "# Plan: add wrap" no longer runs
into its next paragraph) and a short one ("## Summary") is dropped; the
plan you approved stamps "You approved it".
- "Claude's part is done in 3 minutes. A helper is still working."; the
helper steps say "still working" while the helper runs; the advisor's
labels call it a second opinion; a check run twice the same way reads once
with a count; the session folder reads "." in commands only, never in
prose; the loop's POW keeps off its board on a narrow panel; a shell
session's note says it runs a shell or another CLI.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A turn with no end marker of its own (an abandoned or crashed turn) was closed when the next prompt started and then drawn as "Mission complete!". It now reads "To be continued…" / "The trail stops here: the next prompt starts a new turn.", and the page carries `superseded`. The Director's cut names what ended every turn: turn_duration, stop_hook_summary, a local command, an interrupt, or the next prompt. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nine transcripts from the review rounds, each a real Claude Code worker building a small program (a Python CLI with a fix loop, a web page with node:test, a bash script with a background helper, a long turn with a prompt added mid-turn, teammates then an interrupt, a question then tests first then a push, a text plan with a scratch dry run, /compact then plan mode, a test failed on purpose). Each is cut down to the lines and fields the reducer reads, with every path, user name and email rewritten and the file contents of Reads left out; replaying them before and after gives the same pages. The expected file beside each pins every page (panel kinds, captions, balloons, counts), so a change that alters a story fails with a diff of exactly what changed; STORY_CORPUS_UPDATE=1 regenerates them. The beats the reviews were about are also stated outright. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Eleven batches of findings had grown it by accretion. It now reads in parts: the routes, where turns start and end, how background work joins back, and what a page, an event and a panel carry. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ull 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>
…t of review round 4
Stepping through 40 turns made 46 long tasks of 51 to 286 ms, enough for
the app's guard to drop the terminal's WebGL renderer for the rest of the
page's life. Drawing switched off made none: the cost was canvas raster at
paint time, which the per-frame JS budget never saw. Now:
- Panel backgrounds are shared between panels and turns (kind × size ×
palette), and a new one is built in four bands, one per frame (a wide
halftone at 2x was ~35 ms in one go; a band is ~8 ms).
- At most two panels are drawn per frame, the first one served rotating so
none starves; clocks still run for all.
- Measured on 20 turns stepped 40 times at 2x: 1 long task (52 ms), against
39; the settled overlay is still ~2% of a core.
Also from round 4:
- The turn picker says when older turns were not loaded (`older` in the
response: the transcript is longer than the part that was read).
- A compaction after the last turn closes that turn; a local command's
output is the final panel's balloon ("Compacted"), without its markdown
ticks, and a turn under a second reads "Done.".
- Exact durations keep their seconds past an hour, so the two captions never
disagree on the minute ("1 h 10 min." / "1 h 9 min 39 s").
- A turn that arrives already finished is announced as new too.
- Whole files written by here-documents read "Claude writes 3 files."; the
advisor's caption reads "a second opinion" instead of repeating its name.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cold-reader review
Review round 5 had an agent read only the comic of the nine real sessions
(20 turns), write down what it believed happened, and only then compare
with the transcripts. Three panels told it something false, one hid the
failure the user asked to see:
- A push that went through, followed by a check that failed in the same
command, drew as a failed push; the output (`[new branch]`, `->`) now
decides: "Claude saves the work in 2 commits and uploads it, but a step
after that fails."
- A formatter's "would be reformatted" preview, printed before it fixed
the file, made a clean run a failure: the last verdict wins.
- A blocked command ended in confetti and "Mission complete!": a run panel
where everything failed says "Claude tries a command, and it is
blocked.", and a turn where nothing worked ends "Turn over" / "Nothing
Claude tried worked, so the job is not done." with a neutral robot.
- The loop stamped only the final verdict (POW ×2), so a first run that
failed on purpose never showed. It now marks every test run red or green,
and says it: "Fix and test: the first run fails, the second passes." /
"Build and test, 4 rounds. A check fails once, then everything passes."
Also:
- Files written inside a check or a save reach the plain caption ("Claude
writes 4 files; the tests pass.").
- Balloons keep the next sentence when the first reports a problem ("…left a
stray file. I'll remove it…") or is very short ("Not done. The harness
blocked sleep 65…"); Claude's last words keep a caveat that follows them
("Nothing is committed because this folder isn't a git repository.").
- The advisor never shares a panel with helpers that do work; ToolSearch
reads "Claude gets the tools it needs ready."; Codeman's own case file is
not drawn as a file Claude read; looking around shows what Claude meant
(the call's description), not raw commands; a montage at the start of a
turn reads "Claude gets started".
- Words: boards count tests ("19 tests"), "uploads it" replaces "sends it
up", and the prompt balloon keeps 400 characters.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From review round 5 (a phone with touch):
- Save image on a phone exported blank art for every panel not yet scrolled
into view (the frame loop draws only what is on screen). It now draws
every panel completely first, and the file name carries the turn number,
so saves of different turns no longer overwrite each other.
- A test command sent to the background ("Command running in background
with ID …") drew as "The tests pass." with POW before any test ran. It is
now running: "Claude starts the tests in the background.", tagged "in the
background" on the wire, and the final panel says a command is still
running. Its task notification settles it from the exit code, and reading
the job's output file that reports tests is a check ("The tests pass.",
7 passed) instead of "Claude looks around".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… what they saved
From review round 5 (a phone with touch):
- Touch screens: every control at least 44 px (prev/next, close, the
Story/Director's cut toggle, Save image, the final panel's chips, drawer
buttons and links), and the small type at 13 px.
- The drawer on a phone starts below the bar (it cut through it), closes by
dragging its head down, and keeps its head small in landscape.
- The turn picker says where you are ("Turn 2 of 5"), learns about new
turns while you look at an older one, and a turn you picked yourself is
not announced as news.
- A check whose command also wrote files offers "See the 2 files it wrote".
- Test counts are read from the whole output: a later command's lines had
pushed pytest's "5 passed" out of the kept tail.
- A save names its commit: "Claude saves the work: "feat: add wrap() for
modular wrapping into a range"." (from -m, -am, --message or the
here-document of git commit -F -; a trailer is never the subject).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ering the art
From review round 11: a long caption ("Claude writes pyproject.toml
and changes test_csvstat.py; the tests pass, then Claude starts a git
repository and saves the work on a new branch …") covered the test
board, the POW and the code card's file name on a two-column panel.
A caption that reaches past the band above the art now makes its panel
taller by the difference (re-fitted on resize and on the Director's cut
toggle). Every scene keeps the panel's usual height as its stage and
stands at the bottom, the background filling the space above, the way
the final panel already did; Claude's balloon is placed against the
moved head. The browser test forces a long caption and checks the panel
grew and the caption ends above the art.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hat it saved" From both round-11 readers: `git --git-dir=origin.git log -1 master; git status -sb | head -1` after a push read "Claude looks around the project", so a reader could not tell the save was verified. A look made only of git lookups (log, status, show, ls-remote, …, piped to head or tail) after a save in the same turn now reads "Claude checks what it saved." Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r loaded says so (review round 11)
From the fact-checker's last items:
- A helper's report quoted in a notice joined its first sentence to one
from paragraphs later with nothing between: where text was left out,
the balloon now says "…" (found in the report itself, so two
sentences that follow each other are never marked).
- "The first run fails on purpose, as you asked" for a run that never
loaded a test (a collection error) now reads "At first the tests don't
even load, as you asked; the second run passes.", as a test panel
already says it.
- A list intro without a count ("What failed and how I fixed it:") brings
its items while they fit in 360 characters, not 260.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ry runs (review round 12)
From a reader who has never written code:
- "a save your prompt here did not ask for" on a save that was step 3 of
the plan you approved ("Verify and commit"): a commit step in an
approved plan asks for the save. When Claude says a project rule asks
for its commits (read on its whole message), the line says "…; your
project's rules do".
- "a check after that fails" read as a failed test where it was `git
describe` finding no tags: "the version-tag lookup after that fails
(there are no tags yet)".
- The dry run you asked for never showed: the terminal card took the
last command's edge-case errors. A dry run in the panel is now what the
card shows, its "a -> b" lines, under "Claude does the dry run you
asked for: it shows what would happen, changing nothing."
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…is checking what came back From review round 12: after a helper reported back, Claude's `git log`, `git show --stat` and a diff of the README example against real output read "Claude looks around the project". In a turn a report started, a look that names no file now reads "Claude checks the work that came back." Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… says so Flagged by four readers across rounds 10 to 12: after a push, "but a check after that fails" never said that Claude repaired what the check found (the new remote pointed at a branch with no commits) and checked again. When Claude's next command runs that same check again after a change (`git -C remote.git symbolic-ref HEAD refs/heads/main && git -C remote.git log …`) and passes, the caption adds "; Claude fixes it, and the check passes next." A check re-run with no change before it, or one that fails again, claims nothing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… Story Mode The owner wants to keep working from the comic view: "on the bottom have an input field! so I can send more input in that view!". Story Mode gets a bar across the bottom (the overlay's new last grid row): a field that grows from one line to about five, a Send button and a one-line hint. - It sends to the session the overlay shows (st.sessionId), never the active tab, since Session Options opens Story for any session. Delivery reuses the mobile composer's tested path, KeyboardAccessoryBar._sendComposedPrompt: one bracketed paste on the durable exactly-once queue, then a separate delayed Enter. Text plus a carriage return in one burst would be read as a paste by Claude Code and strand the prompt. - Enter sends, Shift+Enter adds a line, an IME composition and whitespace-only text never send. - While Claude waits on a question or a permission in the terminal (a pending question on the newest page, or the permission/elicitation hook), sending is off: the message would land in the selection dialog, whose Enter picks the highlighted option. The field still takes a draft and the hint says why. A session that is gone (not in app.sessions, or a 404 from the story route), whose agent exited, or that Story cannot draw (a shell would run the text) keeps its draft readable but sends nothing. A running turn may be sent to: Claude Code queues the message. - Drafts are kept per session in memory for the life of the page. - After a send the field clears and keeps focus; a reader parked on an older turn (or held there by a drawer) goes to the newest, since activity is ignored while parked, and a refresh is scheduled. - Escape in a field holding text steps out of it first (focus to Close), an empty field closes one layer as before, and an Escape that cancels an IME composition closes nothing. - The bottom safe-area inset moves from the overlay onto the composer, so its background reaches the edge; the drawer ends above the composer and toasts sit above it (measured into --story-compose-h). Not part of Save image. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Story Mode gets a third reading level for someone who has never seen a terminal (a parent, a manager). Every panel now carries caption.simple: one short sentence in everyday words, with no file name, command, tool name, flag or jargon. The sentences are templates only, so nothing from the transcript (a path, a commit message, a branch) can reach them, and they are written from the same facts as the story caption: a failed check never reads as a pass, a failure you asked for says so, and a check that fails after one passed is a failure again. The final panel's simple caption is a plain summary: how the turn ended and roughly how long it took, how many files changed, whether the checks pass, whether the work is saved (only claimed "not saved" where saving is in play: you asked for it, or the turn used git), and what the reader must not miss (a problem left, a note, a question for them). test/story-plain-words.ts is the guard: the corpus test runs it over every panel of every page of the eleven sessions, and the unit tests cover the honest endings (failed check, catch, note, question, stopped, not done, to be continued, done for now) and the guard itself. The corpus expectations only gain "simple" lines. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…'s cut The bar's two-button group becomes three, in reading order: Simple (new, for a reader who has never seen a terminal), Advanced (the story view as it was) and Director's cut (the wire and the tool captions, unchanged). The Director's cut is the default, as the owner asked; the reader's own pick is kept per device under codeman:story-level, and only a click stores it, so opening Story Mode never writes the default back and changing the default stays a one-line change. _storySetDirector(on) stays as a shim (on: Director's cut, off: Advanced) beside the new _storySetLevel(level); st.director stays a boolean for the wire and the scenes, which see director false at Simple. At Simple each panel shows caption.simple in the letter font, a size up (calc(20px * var(--story-k, 1))). Claude's balloons are hidden (its narration is often technical; the art carries the panel) while your own prompt balloon stays. The final panel shows the plain summary instead of Claude's last message, and its chips are the time, the files changed and "Read Claude's message", which opens a new drawer view with Claude's last words as it wrote them. Balloons are hidden with the hidden attribute, so Save image (which skips hidden boxes) prints what the screen shows. On a phone the three buttons and Save image share one row. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the Director's cut as the default reading level, the story line the test lengthens is not shown, so its panel never grew and the check failed. The test now picks the Advanced level first, where that line is the caption. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…th bar row With three reading levels the third bar row (the levels and Save image) is full at 390px, and a running turn's LIVE pill wrapped onto a fourth row (the bar went from 170 to 222px). On a phone LIVE now sits on the title row, where the session name gives way to it; the bar stays at three rows either way. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Director's cut rebuilt the whole wire with replaceChildren on every refresh (every 4 s poll and SSE burst of a running turn), so it never followed new events and a rebuild could throw the reader's place, focus and hover away. Rows are now keyed by event id and headings by the row they head (a panel that changes kind or number as the turn grows keeps its heading node). Unchanged nodes are reused, a changed row is patched in place (a running tag flips to its result on the same node), new rows are inserted in order with a short transform and opacity entrance, and a new turn replaces the list. _storyEventRow still builds every row. While the wire follows, it glides to the newest row with a critically damped spring on animation frames: a burst of updates moves the target and the glide keeps its speed, so there is no stop and restart. Measured against the browser's own smooth scrollTo on a growing transcript, the spring moved a median 8 px a frame over ~25 frames with no stalls, while scrollTo snapped at ~41 px a frame over ~10 frames and its read of the new scrollHeight forced a layout inside the update (a 57 ms long task). Scrolling up by hand (wheel, touch, scroll keys, the scrollbar) stops following and shows a pill that counts the rows that came in since; clicking it, or scrolling back to the bottom, follows again. The glide only moves down and is never read as the reader scrolling up. A running newest turn, and the Director's cut turned on during one, open at the bottom, following; a finished turn opens at the top. Under prefers-reduced-motion there is no entrance and the list jumps. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A balloon that found no room beside a long caption dropped to just below it, inside the art band: on the fix loop it covered the code page and the "19 tests" label of the board, and elsewhere it sat on Claude's face (a balloon of three lines or fewer was exempt from the clamp). A sweep over the 11 corpus sessions, every turn, both modes, at 390, 820, 1100 and 1440 px and beside the diff drawer found 131 of 690 balloons covering a click region or Claude's face. Every balloon (Claude's, the loop's aimed one, the ask panel's, yours and a notice) is now placed by _storyFitBalloon from the scene's layout at its finished frame (StoryArt.layout, a dry run on a scratch canvas), so an entrance with objects still in flight never moves it or the panel's height. It must end above every click region under it (its tail above those under the tail) and above Claude's face; a click region holding his face is Claude himself and only the face rule applies to it. When two lines do not fit, the panel grows for caption and balloon together and the scene stands lower. The growth is computed at the panel's usual height (storyBalloonRoom, pure), so a refit of the grown panel asks for the same height: one extra pass, never an oscillation. The rest of a long text clamps as before. agent() now names Claude by his default colour, not by being cyan, so the cyan fourth helper no longer takes the balloon, and the check scene reports how high Claude's jump lifts his face. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…is held over On a narrow panel (a phone, a two-column page) Claude holds the lens over the investigate terminal, and its rim covered the end of the second line and the ones below it. Those lines now end with an ellipsis short of the lens's reach (over every frame of its sway); the lines above and below it keep the terminal's full width, and on a wide panel, where the lens hangs beside the terminal, nothing changes. A narrower terminal was the other option and cut every line to about 14 characters on a phone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Story Mode entry still said _storyPlaceBalloon places Claude's balloon over the head agent() last drew. Placement moved to _storyFitBalloon, from the scene's finished frame, with the panel growing for caption and balloon together; the rule now says so, and names the two things a change must keep: never place from the frame on screen, and compute the growth at the panel's usual height so a refit cannot oscillate. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The comic sheet was capped at 1240px with empty margins beside it, the Director's cut wire was a 320px box, and a short page ended half way down the window. Now the stage takes the full width (16px gutters kept), the wire is clamp(320px, 25vw, 460px) wide and runs the main area's whole visible height, sticky, its list scrolling inside, and the rows grow to fill the visible height. Row heights come from a pure function next to storyLayoutSpans (storyRowHeights): the visible height is shared evenly between the page's rows for the active column count (6 over a 1000px sheet, 2 over 600px; one column keeps its natural heights), never under 270px (a longer page keeps 270px rows and scrolls as before) and never over a cap: 540px (twice the drawn size: past it a one-row page is mostly background and the text, which stops growing at 1.45x, looks lost), and the height at which a row's narrowest panel would get a scene stage under 280 units wide (the mission's exclamation mark ran off the panel below that, which is where a span-2 panel turns into a portrait). What a capped row cannot take goes to the rows that can, mostly the final panel's full-width row; its scene keeps a stage wide enough (440 units) to stand beside its words. When the final panel's in-flow words make its row taller, the other rows give that height up, so the page still ends at the bottom. A capped page leaves blank paper, never a band of background: the sheet reaches the visible bottom. The values are plain px written from JS (each panel's --story-panel-min, the page's grid-template-rows and --story-row-h, which also feeds grid-auto-rows, so there is one 270 default), and --story-panel-min is registered as a <length>: _storySizePanel and _storyFitCaption read it with parseFloat, which a raw calc() would have turned into NaN. The comic text scales with --story-k (the shortest row over 270, at most 1.45), set on the page; the you/notice balloons sit below the caption by the same factor. It is recomputed by the existing ResizeObserver, which now also watches the main area (window, drawer padding, a bar added later) and the sheet's width (the Director's cut toggle), once per delivery, and on every render. It reads boxes that its writes cannot change, and the main area keeps its scrollbar gutter, so it never oscillates; panels still resize through _storySizePanel, whose draws the frame loop rations. Main's bottom padding is 16px. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…kground cache by bytes The rows were re-fitted when the main area or the sheet's width changed, but a change of words alone (the Director's cut toggled with the wire stacked under the sheet, another reading level, a caption that grew, the fonts arriving) leaves both alone while the final panel's in-flow words take another height. The resize observer now watches the page's height too and re-fits on the next frame: the same boxes give the same values and nothing is written, so it settles (measured: 0 passes while idle, at most 2 per change). Bigger rows mean bigger panel backgrounds: 32 of them at 2560x1440 and a 2x screen came to 342 MB. The cache keeps at most 160 MB as well; a panel that shows a background it lost keeps its own copy, only reuse is lost. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts: # src/web/public/story-mode.js
…nced The Director's cut is now the default level, so a test that turns it on right after opening changed nothing and the wire was never placed. Start from Advanced, as a reader would. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The owner asked to merge the turn picker with the new input field. The prompts you already sent (the picker and its arrows) now sit on the left of the bottom bar, the next message beside them, the hint under the field; on a narrow screen the turns get their own row above it. The top bar keeps the title, the reading levels, Save image and close, so a phone's bar is two rows. On a touch screen the ready hint (Enter and Shift+Enter) is hidden: it means nothing on a touch keyboard, and the hints that matter still show. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…line A panel ignores a growth of 2 px or less, but the clamp measured against the exact room, so a balloon that fell 0.6 px short of two lines showed one. The clamp now lets a balloon use those 2 px of the 4 px gap it keeps from the art and Claude's head, which is what the ignored growth would have bought, and rounds away float error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts: # src/web/public/story-mode.js
… picker is gone from it The picker took a row of its own and so pushed the reading levels to the next line. With it in the composer, close squeezed in beside the title and Save image wrapped alone. A full-width pseudo-element between close and the levels breaks the row; its height is the gap. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ing levels, wire and composer Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…her prompt Story Mode's prompt bar and the phone's Compose sheet send a prompt as two writes: one bracketed paste, then a bare Enter about 120 ms later. Neither is a `text\r` pair, so SubmitVerifier was never armed for them, and a send while Claude Code was still ignoring Enter (2.1.277+, the first 30 to 50 s after the composer paints) sat unsent. PastedPromptWatch pairs the two writes back up, in Session.write() (the WebSocket path) and writeViaMux() (the POST fallback's Enter), and arms the verifier with the pasted text. Nothing may come between the two, and a newer paste cancels a pending check, so Enter never sends the user's own paste. The composer match learns what a pasted prompt looks like (measured on 2.1.303): continuation lines count for a short first line, and Claude's `[Pasted text #1 +3 lines]` stand-in counts only when its line count is the prompt's own. A prompt under 24 characters must now match whole, so a short answer cannot read a dialog's highlighted `1. Yes` row as itself. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts were additive registrations (types barrel, routes barrel, server, index.html, settings keys, docs) plus three in session.ts: - master's public claudeConfigDir() method replaces the branch's getter; Story Mode's route now calls it. - writeViaMux keeps master's guarded-turn structure; the composer pairing (_notePastedPrompt) runs after _verifySubmitted in _writeViaMuxNow. - the imports keep both sides. Master now pastes a multi-line API prompt with its line breaks, so _verifySubmitted keeps them too (the same text TmuxManager.sendInput pastes): a prompt Claude Code shows as "[Pasted text #1 +3 lines]" is recognised by its line count, and the CLAUDE.md gotcha no longer says it is missed. The changeset and Settings Reference describe the three reading levels and the prompt bar. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Picks up master's frontend dead-code cleanup. The one conflict was the z-index list in src/web/CLAUDE.md: master dropped removed layers, the branch added the Story Mode overlay (2400); both kept. The new check:frontend-dead passes with Story Mode's scripts in it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Story Mode: a Claude session drawn as comic pages, live
Watching an agent work means reading a terminal that scrolls past faster than anyone reads. That works for the person who wrote the prompt and knows the code, and shuts out everyone else: a manager, a designer, a new teammate, someone glancing at a phone. Story Mode adds a second way to watch a Claude Code session. Each turn (one prompt and everything Claude did for it) becomes one comic page: panels for reading code, making a plan, calling in helpers, editing files, running tests (POW! or KRAK!), asking you something and saving the work, each with a caption anyone can follow.
It is opt-in and off by default: App Settings → Header buttons → Story Mode (per device). On a phone it opens from Session Options (same setting). It reads the session's own Claude transcript through two new, experimental, read-only routes and draws in a full-window overlay; it adds no setting that changes how sessions run.
One change reaches beyond Story Mode: the server now also checks prompts sent by the browser's composers (Story Mode's prompt bar and the phone's Compose sheet) and presses Enter again while Claude Code is still ignoring it. See The Enter check below; it is the part of this PR that touches every prompt.
What you see
file:linelinks. A helper shows what Claude asked it, what it answered and every step it took, read from the helper's own transcript (live while a background helper works).How it works
Server (
src/story-mode.tspure;src/story-reader.tsIO;src/web/routes/story-routes.ts):GET /api/sessions/:id/story[?turn=<id>]draws one turn;GET /api/sessions/:id/story/helpers/:agentIddraws one helper from its own transcript (<conversation>/subagents/agent-<agentId>.jsonl, the id checked against a strict charset). Both are documented indocs/api-reference.mdas experimental.CLAUDE_CONFIG_DIR, or the server's), else a lookup by conversation id, then Codeman id. Story Mode is the first route that sends transcript content to the browser, which is why the hook path is confined. Only a regular file is read, through one non-blocking handle, so a FIFO in its place fails at once instead of pinning a libuv worker. Only the tail is read (8 MB, widened in steps to 64 MB while that tail holds no prompt to start the turn).originalFileand images never leave this step; outputs keep a bounded tail; hunks are capped). Join each tool call with its result and split entries into turns (a background helper stays "running" until its<task-notification>arrives in a later turn, then its answer is joined back). Group a turn's events into at most 12 panels with captions for each level.&&,||,;,|and newlines outside quotes, here-document bodies are skipped, wrappers (timeout 60, env assignments,git -c) are stripped, and the strongest part wins. Calls in a row with the same intent share a panel; fix → test → fix → test folds into one "loop" panel; a long turn folds its quieter panels into a montage.{ supported: false, reason }for a CLI that writes no Claude transcript (registrycapabilities.transcript, never a mode check) and for remote or Docker sessions, whose transcript is not on this host.ETag; an unchanged transcript is a304decided from a stat, before anything is read.Frontend (
src/web/public/story-mode.js,story-mode.css): a full-screen overlay with a small canvas frame engine (each scene is a pure function of its frame number). Only new panels animate in;prefers-reduced-motiondraws every panel finished.textContentonly, insidedata-i18n-skipcontainers. On the server every helper that matches transcript text runs in linear time on crafted input (an agent can be prompt-injected, and one regex call blocks the whole server): timing each helper at its input cap found five shapes that were quadratic or worse (up to 29 s on 1 MB); they useindexOfscans now, and a test times them.openFilePreview(), which already handles workspace confinement, outside paths and remote hosts.addListeners inconnectSSE), plus a 4 s poll while a turn runs; one request in flight, debounced.font-src 'self'; licenses infonts/LICENSE-story-fonts.txt.Performance, measured on this machine: a cold read of a 110 MB transcript takes 50 ms with a longest event-loop stall of 10 ms; a cached read about 0.1 ms; building a page 1 to 7 ms. A page carries at most 120,000 characters of outputs, hunks and helper texts (newest first); the largest live page in a sweep of every local transcript is 74 KB gzipped. Panels repaint by need (30 fps entering, 15 fps for a running turn's newest panel, 6 fps settled, stopped after 20 s): five panels on a 2x screen went from 81% of a core to 2.4% once settled. Stepping through 40 turns went from 46 long tasks to 1 (enough before to make the app drop the terminal's WebGL renderer).
The Enter check, also outside Story Mode
Claude Code 2.1.277+ ignores Enter for the first 30 to 50 s after its composer paints while still taking the typed text. The server already handles that for API prompts:
SubmitVerifier(src/session-submit-verifier.ts) reads the pane on a 2 s to 60 s schedule and presses Enter again while the composer still holds the prompt. The browser's composers never send atext\rpair, though: they write one bracketed paste and a bare Enter about 120 ms later (Codex drops a key that shares a read with a paste), so that check never ran for them.PastedPromptWatchpairs those two writes back up (inSession.write()for the WebSocket, and inwriteViaMux()for the POST fallback) and arms the same check. Nothing may come between the two: any other write breaks the pair, and typed keystrokes plus Enter arm nothing. Changes to the check itself, which reach every API prompt too:❯ 1. Yes) as itself.[Pasted text #1 +3 lines]stand-in, which counts only when its line count is the prompt's own. Since master now pastes a multi-line API prompt with its line breaks,_verifySubmitted()keeps them for that count.[Pasted Content N chars]stand-in is still not matched (not measured).How it was proven: the defect did not reproduce on Claude Code 2.1.303 (5 sends 0.3 to 1 s after the composer painted, all taken on the first Enter), so this is a safety net. A stand-in
claudethat draws Claude's composer and ignores Enter for its first 25 s reproduced it: sent from the real Story prompt bar in a browser about 5 s after start, the old build left the prompt stuck for 45 s on both the WebSocket and the POST fallback; the new build pressed Enter again at 2, 5, 10, 15 and 20 s and the prompt went through once at 25 s, the first moment it could. On real Claude Code the new build caused no extra Enter, and left alone for 65 s both text typed into Claude's composer after a send and a 4-line paste dropped into the pane.Reviewing the diff
58 files, +20,294 / -27, but most of it is not code to read line by line:
test/fixtures/story-corpus/(11 recorded sessions + their pinned pages).expected.jsonto see what a page looks liketest/story-*.ts,test/routes/story-routes.test.ts, verifier tests).woff2files are binary)story-mode.js,story-mode.cssstory-mode.ts,story-reader.ts,story-routes.ts,types/story.tsThe edits to existing files are the ones that can affect anything else:
src/session-submit-verifier.ts(+91/-12) andsrc/session.ts(+36/-6): the Enter check.src/utils/claude-transcript.ts(+64): finds a recovered session's (restored-<id>) transcript, the same recoverylast-responsemakes.app.js(+21): the SSE listeners for live refresh and a Story Mode entry in Shortcuts (no default chord).index.html(+7/-2): the header button, the Session Options button, the settings chip, the script and stylesheet.server.ts,schemas.ts(showStoryButton, next toshowResponseViewer),settings-ui.js(per-device key and visibility),i18n.js(the setting's zh-CN label),config/test-suites.ts(the browser test).styles.css/mobile.css: the header button's hidden marker, kept off phones like the other new header controls.The branch was built over several days and is 109 commits, including two merges of master (
Merge master into feat/story-mode). The first resolved additive registrations plussession.ts, where master's publicclaudeConfigDir()replaced the branch's getter and the composer pairing moved into master's guarded_writeViaMuxNow; the second picked up master's frontend dead-code cleanup (one line in thesrc/web/CLAUDE.mdz-index list). Squash-merging is fine.Risk
test/story-corpus.test.tspins every page of the 11 recorded sessions, so a change in Claude Code's format or in the wording shows up as a diff.Reviewed against real sessions
Review agents started real Claude Code workers on an isolated Codeman instance, gave each a small program to build, watched Story Mode draw it live, then checked every panel against the transcript and the files on disk. Later rounds had cold readers (someone who has never written code, an engineering manager skimming, a new teammate, a fact-checker) read only the comic, write down what they believed happened, and only then compare with the recordings. Every finding is fixed with a test. Eleven of those transcripts, sanitized (paths, user names and emails rewritten, Read contents left out), are the regression corpus.
The twelve rounds and what each found
taildrew as a pass; a background helper drew as finished with Claude Code's internal launch notes as its answer; logging into scratch files drew as code changes; balloons covered the art on narrow panels.queued_commandattachment with no user line; some sessions never writeturn_duration; helpers can be teammates reporting in<teammate-message>s; files are often written through here-documents. Also commits vanishing into montages, a red test run tagged "ok", a failed push drawn as SAVED, and a full page reshuffling while live./compactthen plan mode, keyboard-only use, image export, skins, a non-Claude session): a typed/compactdrawn as two turns, a local command's turn left "working", the plan file under~/.claude/plans/counted as a code change, Tab walking out of the overlay./compactturn before it, a turn with seven added prompts drawing past the 12-panel cap, and 46 long tasks when stepping through 40 turns.Known limits
Not in this PR
Testing
test/story-mode.test.ts(165 tests): Bash classification (segments, wrappers, probes, redirects into scratch, here-documents, scripts fed by one), test-count parsing (vitest, jest, pytest incl. all-failing and collection errors, mocha, cargo, node:test), the reducer (no file contents or originals survive, hunk counts, output tails, skipped meta/compact/sidechain/local-command entries, task notifications, interrupts, background commands), turns (queued prompts, partial turns, failed and pending calls, background helpers joined back), pages (captions at all three levels, balloons, loop marks, montage capping, the text budget, live panels), the wording rules behind every caption and title, and crafted inputs timed at sizes where the old code took seconds.test/routes/story-routes.test.ts(25 tests): the newest page and an older turn by id; unsupported CLI, remote and Docker sessions; another user's session in multi-user mode; no transcript, a hook path outside the projects folder, a symlink out of it, a FIFO transcript and a FIFO meta file; a live turn growing between reads; a half-written last line, a replaced file, concurrent reads;304on an unchanged page; a turn larger than the first tail; the helper route.test/story-mode-ui.test.ts(77 tests, JSDOM with a fake canvas): the real module draws every scene in every panel shape, opens every drawer, follows a live page, switches levels, sends from the prompt bar, keeps Escape and Tab order, and pins the fixes from the review rounds.test/story-corpus.test.ts(39 tests): every page of the eleven recorded sessions, at every level;test/story-plain-words.tschecks that no Simple caption carries a path, command or jargon.test/session-submit-verifier.test.ts(28 tests): the paste shapes Claude Code draws, the paste pairing, and a session-level suite for the WebSocket and POST fallback sends, a taken prompt, a newer paste cancelling, typing arming nothing, and a multi-line API prompt shown as the stand-in.test/story-mode.browser.test.ts(6 tests, browser suite, not in the gate): a real server and Chromium; a running turn redraws live from real SSE events, the diff drawer, Escape order, Session Options → Story, Save image downloads a real PNG, no sideways scroll at 400 px, and panels that grow instead of covering or cutting off their words. Passes on the merged branch.typecheck,lint,format:check,check:frontend-syntax,check:public-assets,check:browser-excludes, master's newcheck:frontend-deadandknippass, and the fullnpm testgate: 17,440 passed, 13 skipped, 0 failed (on the first merge; CI runs it on the current head).