Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
109 commits
Select commit Hold shift + click to select a range
2653db1
feat(story-mode): draw a Claude session's turns as comic pages, live
claude Oct 18, 2026
d20208b
fix(story-mode): Escape closes a file preview opened from a panel first
claude Oct 18, 2026
7c21973
perf(story-mode): 304 for an unchanged page, and a longer read for a …
claude Oct 18, 2026
dfe023d
test(story-mode): frontend overlay test in JSDOM, every scene drawn i…
claude Oct 18, 2026
fc279f9
fix(story-mode): judge each part of a Bash one-liner, and keep unders…
claude Oct 18, 2026
fb71180
feat(story-mode): a helper's own steps, from its transcript
claude Oct 18, 2026
d313b86
test(story-mode): end to end in a real browser (browser suite)
claude Oct 18, 2026
0c45682
feat(story-mode): save as image, a helper's own steps, a way in on ph…
claude Oct 18, 2026
10f92fd
perf(story-mode): repaint panels by need, not at display rate
claude Oct 18, 2026
13964e3
perf(story-mode): parse transcript tails per line and yield to the ev…
claude Oct 18, 2026
38585a8
fix(story-mode): draw what real sessions did, from review round 1
claude Oct 18, 2026
03cb3a5
fix(story-mode): balloons over the speaker, honest boards, and drawer…
claude Oct 18, 2026
8485668
fix(story-mode): read how current Claude Code builds deliver prompts …
claude Oct 18, 2026
d7a6644
fix(story-mode): one verdict per call, failed saves, and loops that e…
claude Oct 18, 2026
831599a
fix(story-mode): translate the Story Mode label for zh-CN
claude Oct 18, 2026
ca035dd
fix(story-mode): files written in Bash, the advisor, and honest check…
claude Oct 18, 2026
a2db03c
fix(story-mode): commits in montages, lint verdicts, and a page that …
claude Oct 18, 2026
ea104c8
fix(story-mode): a remark about committing stays on its save
claude Oct 18, 2026
db9eb5c
fix(story-mode): slash commands, and where a compaction belongs
claude Oct 18, 2026
dae6a52
fix(story-mode): one turn per prompt written twice, and plan mode as …
claude Oct 18, 2026
757e14e
fix(story-mode): keyboard focus with the file preview, and what round…
claude Oct 18, 2026
873b899
fix(story-mode): the rest of review round 3
claude Oct 18, 2026
f4898a6
fix(story-mode): a turn ended only by the next prompt says so
claude Oct 18, 2026
f464548
test(story-mode): real sessions as a regression corpus
claude Oct 18, 2026
50de4f1
docs(story-mode): restructure the API section
claude Oct 18, 2026
846749a
fix(story-mode): scheduled runs, work after a turn ended, and pages f…
claude Oct 18, 2026
0effbb9
perf(story-mode): no long tasks while stepping through turns; the res…
claude Oct 18, 2026
814bec7
fix(story-mode): what a non-technical reader would believe, from the …
claude Oct 18, 2026
cdb7b5d
fix(story-mode): background commands, and Save image on a phone
claude Oct 18, 2026
3f1ecfe
fix(story-mode): a phone you can use with a thumb, and saves that say…
claude Oct 18, 2026
76d5c61
fix(story-mode): a three-row bar on phones, buttons for panels out of…
claude Oct 18, 2026
6a1bac6
fix(story-mode): appends without invented line numbers, printed files…
claude Oct 18, 2026
d92e927
fix(story-mode): focus returns to the chip that opened the drawer; do…
claude Oct 18, 2026
b220d6d
test(story-mode): the compact panel's drawer, and the poll while park…
claude Oct 18, 2026
c36700a
fix(story-mode): a file a python or node script rewrites counts as ch…
claude Oct 18, 2026
c5ee5aa
fix(story-mode): a push in its own command is not a commit
claude Oct 18, 2026
7f23047
fix(story-mode): a live drawer that holds still, and panels that keep…
claude Oct 18, 2026
d4df9b1
fix(story-mode): a drawer stays with its turn, focus never falls to t…
claude Oct 18, 2026
3cdcfdd
chore(story-mode): lint clean (control-character regexes, a case block)
claude Oct 18, 2026
89c2220
fix(story-mode): text contrast of at least 4.5:1 in both skins
claude Oct 18, 2026
6e45831
fix(story-mode): read right by a screen reader, and the last of revie…
claude Oct 18, 2026
b739e8e
fix(story-mode): balloons clamp again, and the loop's stays above its…
claude Oct 18, 2026
99c1577
test(story-mode): a too-tall balloon clamps its text, not the hidden …
claude Oct 18, 2026
67595ea
perf(story-mode): a page budget that halves the longest live pages
claude Oct 18, 2026
21de403
fix(story-mode): no input shape blocks the server: linear tag and com…
claude Oct 18, 2026
7aa7106
docs(story-mode): this round's fields and the invariants behind the f…
claude Oct 18, 2026
250a77d
fix(story-mode): the reader cannot stall the server, and reads only r…
claude Oct 18, 2026
91ee063
test(story-mode): another user's session is a 404 on both Story routes
claude Oct 18, 2026
e7ce3b8
fix(story-mode): live helper steps behind an unchanged page, a clean …
claude Oct 18, 2026
42d1c75
fix(story-mode): the rest of the PR review: lookups, polling, frame b…
claude Oct 18, 2026
d2d58a5
docs(story-mode): the Bash share measured on every local transcript
claude Oct 18, 2026
a6a28d6
docs(story-mode): the changeset no longer promises an exact diff for …
claude Oct 18, 2026
61e1ad6
test(story-mode): two more real sessions in the corpus; background ru…
claude Oct 18, 2026
8daa158
docs(story-mode): the background event field
claude Oct 18, 2026
6a7d839
fix(story-mode): what the cold readers believed that was not so (revi…
claude Oct 18, 2026
faca757
docs(story-mode): keyPoints and finishedLater
claude Oct 18, 2026
88040c9
fix(story-mode): no celebration when the turn is not a clean finish
claude Oct 18, 2026
b193171
fix(story-mode): a stop says where it landed and what was saved befor…
claude Oct 18, 2026
e892278
feat(story-mode): a run panel shows what its command printed
claude Oct 18, 2026
9c81e4c
docs(story-mode): what may be drawn on a canvas, since Save image sha…
claude Oct 18, 2026
59d70ca
fix(story-mode): Claude's last words keep the sentences aimed at you …
claude Oct 18, 2026
555efef
fix(story-mode): name the check that failed, say whose plan a failure…
claude Oct 18, 2026
a806383
fix(story-mode): a second opinion is a line in the next panel, not a …
claude Oct 18, 2026
70b1ebc
fix(story-mode): name the files, say when a repository is new, and wh…
claude Oct 18, 2026
79b3712
fix(story-mode): a stop names the step Claude was on and what its to-…
claude Oct 18, 2026
6a6c7c0
fix(story-mode): a command whose last check finds nothing answered, i…
claude Oct 18, 2026
277cc88
fix(story-mode): a second opinion in front of helpers neither splits …
claude Oct 18, 2026
226a7bb
fix(story-mode): "Done, with a catch" only for a problem that is stil…
claude Oct 18, 2026
2ec7427
fix(story-mode): a helper reporting back is heard in its own words
claude Oct 18, 2026
1dd167e
fix(story-mode): a bare repository with flags first is no new project…
claude Oct 18, 2026
9ffe577
fix(story-mode): "Done, with a note" not for how things were before, …
claude Oct 18, 2026
2859b3a
fix(story-mode): two open endings the last words missed
claude Oct 18, 2026
f46369a
fix(story-mode): last words in context, the check after a push, a fre…
claude Oct 18, 2026
05dc6fb
fix(story-mode): the final panel grows to its words, never clipping a…
claude Oct 18, 2026
30b9078
fix(story-mode): whose idea a failure was, new repositories everywher…
claude Oct 18, 2026
713978e
fix(story-mode): "the tests pass, then Claude starts a git repository…
claude Oct 18, 2026
175017f
fix(story-mode): the badge counts the card it sits on, opinions mid-s…
claude Oct 18, 2026
d3c55e0
fix(story-mode): what the captions blurred for a reader who skims (re…
claude Oct 18, 2026
dc980d1
fix(story-mode): a new file counts what it ends with, a rewrite is a …
claude Oct 18, 2026
84c4be2
fix(story-mode): a long caption makes its panel taller instead of cov…
claude Oct 18, 2026
1a66dba
fix(story-mode): git lookups right after a save read "Claude checks w…
claude Oct 18, 2026
bcfef8a
fix(story-mode): a quoted report marks what it skips, a run that neve…
claude Oct 18, 2026
f6c5402
fix(story-mode): plans you approved, project rules, tag lookups and d…
claude Oct 18, 2026
b0898b5
fix(story-mode): in a turn a helper's report started, looking around …
claude Oct 18, 2026
ed6c89f
fix(story-mode): a failed check that Claude fixes in its next command…
claude Oct 18, 2026
7d0b6e1
feat(story-mode): a composer under the page, to send Claude more from…
claude Oct 19, 2026
b98deeb
feat(story-mode): a plain Simple caption on every panel
claude Oct 19, 2026
b06f8be
feat(story-mode): three reading levels, Simple, Advanced and Director…
claude Oct 19, 2026
49c443a
fix(story-mode): the browser test's caption fit reads the Advanced level
claude Oct 19, 2026
35ed5c0
fix(story-mode): on a phone LIVE sits beside the title, not on a four…
claude Oct 19, 2026
c29e323
feat(story-mode): the wire updates in place and glides after new events
claude Oct 19, 2026
24abd5a
fix(story-mode): a speech balloon never covers the art or Claude's face
claude Oct 19, 2026
6744bfc
fix(story-mode): the magnifier no longer hides the terminal lines it …
claude Oct 19, 2026
0bf6b77
fix(story-mode): the frontend rules describe how balloons are placed now
claude Oct 19, 2026
53f1852
feat(story-mode): the page fills the window, its comics grow with it
claude Oct 19, 2026
7b03dd5
fix(story-mode): re-fit the rows when the words change, bound the bac…
claude Oct 19, 2026
82a8e12
Merge branch 'story/reading-levels' into story/integrate
claude Oct 19, 2026
a5ed92e
Merge branch 'story/wire-follow' into story/integrate
claude Oct 19, 2026
9c080cb
Merge branch 'story/layout-composer' into story/integrate
claude Oct 19, 2026
f13b59c
Merge branch 'story/compose-input' into story/integrate
claude Oct 19, 2026
acf0567
test(story-mode): the wire tests turn the Director's cut on from Adva…
claude Oct 19, 2026
4e937c4
feat(story-mode): the turn picker and the composer are one prompt bar
claude Oct 19, 2026
09af5c3
fix(story-mode): a growth too small to apply never costs a balloon a …
claude Oct 19, 2026
dea8a4d
Merge branch 'story/balloon-room' into story/integrate
claude Oct 19, 2026
5c2dde2
fix(story-mode): a phone's bar breaks after close again, now the turn…
claude Oct 19, 2026
fc93343
docs(story-mode): the frontend rules cover the full-window rows, read…
claude Oct 19, 2026
a6abbaa
fix(input): a composer's paste-then-Enter send is checked like any ot…
claude Oct 19, 2026
a516a3d
Merge master into feat/story-mode
claude Nov 13, 2026
00ba363
Merge master into feat/story-mode
claude Nov 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/story-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'aicodeman': minor
---

Story Mode draws what Claude Code does as a comic. Turn on **Story Mode** in App Settings → Header buttons (per device, off by default) and its button opens the active Claude session as comic pages: one page per prompt, with panels for reading code, planning, calling in helpers, editing, running tests (POW! or KRAK!), asking you and saving, each with a plain caption anyone can follow. Three reading levels: **Simple** (plain words, for anyone), **Advanced** (the story with file names) and **Director's cut** (every step and tool, listed beside the page), the default for now. Everything is clickable: a file opens in the file viewer, the code page shows the diff of Claude's edits, a test board shows the output with clickable `file:line` links, and a helper shows what it was asked, what it answered and every step it took. A turn that is still running redraws as Claude works, new panels inking in where blue-pencil roughs stood. **Save image** downloads the page as a PNG to share with someone who never opens a terminal. A prompt bar under the page, with the turn picker, sends Claude your next prompt without leaving the comic. On phones, open it from the session's Session Options. The data comes from `GET /api/sessions/:id/story` and `GET /api/sessions/:id/story/helpers/:agentId`, experimental read-only endpoints.

A prompt sent from Story Mode's prompt bar or the phone's Compose sheet (a paste, then Enter) is now checked like an API prompt: while Claude Code is still ignoring Enter right after it starts, the server presses Enter again until the prompt leaves the composer. That check also recognises a multi-line prompt that Claude Code shows as `[Pasted text #1 +3 lines]`, and a prompt under 24 characters now has to match the composer exactly.
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph

## Common Gotchas

- **A prompt is one paste plus Enter** — `writeViaMux()` sends the text as one bracketed paste and Enter separately. ⚠️ **Input must END with `\r` or Enter is never sent**: `sendInput()` only issues `send-keys Enter` when the payload contains a carriage return, a `\r`-less `POST /api/sessions/:id/input` still succeeds (send-and-wait even reports `delivered:true`) while the text sits unsubmitted on the composer, and any `wait` burns its whole timeout on a turn that never started. Embedded newlines are kept (`\r\n` becomes `\n`): all lines go in as that ONE paste and the Enter after it submits them together, so `"echo A\necho B\r"` runs both commands in a shell (bash holds the pasted lines until the Enter; a shell without bracketed paste runs each line as it arrives). Multi-line text WITHOUT `\r` is pasted the same way; on a pane that has not enabled bracketed paste (POSIX sh, macOS `/bin/bash` 3.2) tmux delivers every line break as a CR, so every line but the last runs, where the old line-stripping typed the joined text and ran nothing. Leading and trailing line breaks are dropped. ⚠️ `SubmitVerifier` cannot reliably recognise a multi-line paste in the composer: it matches one Claude shows inline whose first line has 24+ non-space characters, but misses a short first line and the `[Pasted text #N +k lines]` placeholder, so it may not re-press a stranded Enter for one. ⚠️ **Claude Code 2.1.277+ ignores Enter for the first 30-50 s after the composer paints** while still taking the typed text (measured 2026-09-19: an Enter at 28 s stranded the prompt, one at 51 s submitted it), so text+`\r` sent at readiness sits unsent with `0 tokens` and a `wait` burns its timeout. So the SERVER verifies every programmatic write that carried a `\r`: `SubmitVerifier` (`session-submit-verifier.ts`, armed from `writeViaMux`) reads the pane on a 2 s to 60 s schedule and re-sends Enter only while the LAST composer line (the CLI's own `promptGlyph`) verifiably still holds the head of what was sent; an empty composer, other text, or no composer line at all (a shell, a direct-PTY session) ends it, and a newer write replaces the schedule. The skill's `sendwait` keeps its own copy of the loop (`_composer_text` in `skills/codeman/preamble.sh`) for servers that predate this. The `shift+tab` footer only means the composer painted, never that Enter is accepted. ⚠️ **A prompt must never be written into the pane as ONE burst**: Claude Code 2.1.283 takes a `<text>\r` burst of ~100+ chars as a paste, its `\r` lands as a NEWLINE and the prompt strands (a later raw `\r` does not recover it, a tmux `send-keys Enter` does). So `POST .../input` routes a plain prompt (`isPlainPromptInput()`, route-helpers.ts: printable text + exactly one trailing `\r`) through `writeViaMux` even without `useMux`, AWAITED so the browser's serialized POST fallback keeps frame order; raw frames and an explicit `useMux:false` keep the direct write
- **A prompt is one paste plus Enter** — `writeViaMux()` sends the text as one bracketed paste and Enter separately. ⚠️ **Input must END with `\r` or Enter is never sent**: `sendInput()` only issues `send-keys Enter` when the payload contains a carriage return, a `\r`-less `POST /api/sessions/:id/input` still succeeds (send-and-wait even reports `delivered:true`) while the text sits unsubmitted on the composer, and any `wait` burns its whole timeout on a turn that never started. Embedded newlines are kept (`\r\n` becomes `\n`): all lines go in as that ONE paste and the Enter after it submits them together, so `"echo A\necho B\r"` runs both commands in a shell (bash holds the pasted lines until the Enter; a shell without bracketed paste runs each line as it arrives). Multi-line text WITHOUT `\r` is pasted the same way; on a pane that has not enabled bracketed paste (POSIX sh, macOS `/bin/bash` 3.2) tmux delivers every line break as a CR, so every line but the last runs, where the old line-stripping typed the joined text and ran nothing. Leading and trailing line breaks are dropped. A multi-line prompt is recognised in either form Claude Code draws it: inline (the glyph line plus its indented continuation lines, so a short first line still matches) or as the `[Pasted text #N +k lines]` stand-in, which counts only when k is the prompt's own number of line breaks (`_verifySubmitted()` keeps them for that; Codex's `[Pasted Content N chars]` is not matched). ⚠️ **Claude Code 2.1.277+ ignores Enter for the first 30-50 s after the composer paints** while still taking the typed text (measured 2026-09-19: an Enter at 28 s stranded the prompt, one at 51 s submitted it), so text+`\r` sent at readiness sits unsent with `0 tokens` and a `wait` burns its timeout. So the SERVER verifies every programmatic write that carried a `\r`: `SubmitVerifier` (`session-submit-verifier.ts`, armed from `writeViaMux`, and by `PastedPromptWatch` for the browser composers' two-write send, one bracketed paste then a bare Enter, on the WebSocket and the POST fallback alike) reads the pane on a 2 s to 60 s schedule and re-sends Enter only while the LAST composer line (the CLI's own `promptGlyph`) verifiably still holds the head of what was sent (a prompt under 24 characters must match whole); an empty composer, other text, or no composer line at all (a shell, a direct-PTY session) ends it, and a newer write or a newer paste replaces or cancels the schedule. Typed keystrokes plus Enter arm nothing. The skill's `sendwait` keeps its own copy of the loop (`_composer_text` in `skills/codeman/preamble.sh`) for servers that predate this. The `shift+tab` footer only means the composer painted, never that Enter is accepted. ⚠️ **A prompt must never be written into the pane as ONE burst**: Claude Code 2.1.283 takes a `<text>\r` burst of ~100+ chars as a paste, its `\r` lands as a NEWLINE and the prompt strands (a later raw `\r` does not recover it, a tmux `send-keys Enter` does). So `POST .../input` routes a plain prompt (`isPlainPromptInput()`, route-helpers.ts: printable text + exactly one trailing `\r`) through `writeViaMux` even without `useMux`, AWAITED so the browser's serialized POST fallback keeps frame order; raw frames and an explicit `useMux:false` keep the direct write
- **ESM only** — Never `require()`, use `await import()`. `tsx` masks CJS/ESM issues in dev but production breaks
- **Package ≠ product name** — npm: `aicodeman`, product: **Codeman**. Release renames tags accordingly. Both `aicodeman` and `codeman` bin aliases are installed (`package.json` `bin`)
- **Global regex `lastIndex`** — Shared `g`-flag patterns in loops must reset `lastIndex = 0` first, or use the `execPattern()` helper in `utils/regex-patterns.ts` (resets automatically)
Expand Down Expand Up @@ -263,6 +263,8 @@ The terminal provider scans `terminalLinkLine()`, which extends `terminalPathLin

**Ultracode / workflow-run visualization** (opt-in, default OFF): the Workflow tool writes a completion artifact only at run *end*, so live in-flight runs exist solely as transcript dirs. `workflow-run-watcher.ts` therefore synthesizes ACTIVE runs from transcripts until the completion artifact appears and supersedes them. It is **STANDALONE** and deliberately never imports or touches `subagent-watcher.ts`, despite reading the same tree. Two independent toggles: `showUltracodeAgents` (docked panel) and `ultracodeFloatingWindows` (floating windows); the watcher starts if **either** is on. → [architecture-invariants#ultracode-and-workflow-run-visualization](docs/architecture-invariants.md#ultracode-and-workflow-run-visualization)

**Story Mode** (opt-in header button `showStoryButton`, per-device, default OFF): a Claude session's turns drawn as comic pages, live. `GET /api/sessions/:id/story` (EXPERIMENTAL shape) reads a bounded tail of the session's OWN transcript (hook path, else `<conversation id>.jsonl` via `findClaudeTranscriptPath()`), never a path from the request, and never adopts or persists anything. ⚠️ Gate on `capabilities.transcript === 'claude-jsonl'` and `!remote && !docker`, never the mode id. ⚠️ `createStoryReducer()` (`src/story-mode.ts`, pure) is where size is shed: Read contents, Edit/Write `originalFile` and images never leave it, outputs keep a tail, hunks are capped; the reader caches only reduced entries. ⚠️ Interactive transcripts carry no `result` entry: a turn ends at `system/turn_duration`. Frontend rules in `src/web/CLAUDE.md` (Story Mode).

**Clone a repository as a case** (issue #236, Add Case → **Clone Repo**): `POST /api/cases/clone` clones synchronously into the caller's case space (bounded by `GIT_CLONE_TIMEOUT_MS`, no job store); `POST /api/cases/clone-preflight` checks anonymous cloneability and lists refs. Core in `src/git-clone.ts`. ⚠️ **The URL is a code-execution surface**: refuse every `::` form and a leading `-`, spawn only argv arrays with `--` before operands. ⚠️ Stay non-interactive (`gitNonInteractiveEnv()`) or the open request hangs; never collect credentials, refuse `user:password@` URLs. ⚠️ Timeout kills the process GROUP, remove the destination only if this attempt created it, and repo contents win over scaffolding (existing `CLAUDE.md` kept, hooks merged, repo `.claude/settings*` warned about). ⚠️ A `path` is the folder cloned INTO (the API adds no case-name subfolder; the Clone Repo tab's custom folder is a PARENT, so the page sends `<folder>/<case name>` through `_cloneTargetPath()`, as Create New does through `_newCaseTargetPath()`) and registers a linked case: admin-only in multi-user mode, judged by `prepareNewCasePath()`, empty or new only; an empty one is cloned into directly and never removed, and emptied after a failure only when git really ran and did not refuse it. ⚠️ A destination at or inside a Docker case workspace is refused `400` on both branches: git would run the new repository's hooks and filters on the host. → [architecture-invariants#clone-a-repository-as-a-case](docs/architecture-invariants.md#clone-a-repository-as-a-case)

**Git worktrees of a case** (case picker → **Git worktrees…**; `src/git-worktrees.ts`, `routes/worktree-routes.ts`): a local repository case keeps worktrees at `<repo>/.claude/worktrees/<name>` on `worktree-<name>`, Claude Code's own `--worktree` layout. ⚠️ Only that managed path is ever created or removed, every component `lstat`ed and realpath-checked (a symlinked `.claude` is refused). ⚠️ git never runs for a Docker or remote case, nor at or inside a Docker workspace (`worktree add` runs hooks and filters on the host). ⚠️ Remove is `git worktree remove` WITHOUT `--force`, refused while a live session is inside; branch delete is `-d`, never `-D`. ⚠️ Sessions start through the existing create paths (quick-start `worktree`, or `POST /api/sessions` `workingDir`), never a new spawn path. → [architecture-invariants#git-worktrees-of-a-case](docs/architecture-invariants.md#git-worktrees-of-a-case)
Expand Down
1 change: 1 addition & 0 deletions config/test-suites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const BROWSER_TEST_GLOBS = [
'test/command-palette-agent-arrows.browser.test.ts',
'test/voice-input.browser.test.ts',
'test/tmux-capture-sgr.browser.test.ts',
'test/story-mode.browser.test.ts',
'test/tab-rail-resize.browser.test.ts',
'test/tab-activation.browser.test.ts',
'test/tab-layout-editing.browser.test.ts',
Expand Down
Loading
Loading