mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 10:59:32 +01:00
feat(story-mode): draw a Claude session's turns as comic pages, live
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>
This commit is contained in:
1 parent
0ab71d61d1
commit
2653db10b5
27 files changed
+5746
-6
No files matched your search
@@ -74,3 +74,4 @@ export * from './user.js';
|
||||
export * from './webview.js';
|
||||
export * from './intent.js';
|
||||
export * from './mcp-sync.js';
|
||||
export * from './story.js';
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* @fileoverview Story Mode types: one Claude Code turn drawn as a comic page.
|
||||
*
|
||||
* Produced by the pure core in `src/story-mode.ts` and served by
|
||||
* `GET /api/sessions/:id/story` (`src/web/routes/story-routes.ts`). The shape is
|
||||
* EXPERIMENTAL (docs/api-reference.md), not part of the stable `/api/v1` contract yet.
|
||||
*
|
||||
* Every string in here comes from a transcript the agent wrote, so it is untrusted
|
||||
* text: the browser renders it with `textContent` only, and a file path in here is
|
||||
* opened through the existing file preview, never fetched directly.
|
||||
*/
|
||||
|
||||
/** What one transcript event means, read from the tool name and, for Bash, the command. */
|
||||
export type StoryIntent =
|
||||
| 'prompt'
|
||||
| 'read'
|
||||
| 'search'
|
||||
| 'look'
|
||||
| 'web'
|
||||
| 'plan'
|
||||
| 'say'
|
||||
| 'helper'
|
||||
| 'message'
|
||||
| 'edit'
|
||||
| 'test'
|
||||
| 'build'
|
||||
| 'run'
|
||||
| 'save'
|
||||
| 'ask'
|
||||
| 'compact'
|
||||
| 'interrupt'
|
||||
| 'tool';
|
||||
|
||||
/** The comic panel a run of events is drawn as. */
|
||||
export type StoryPanelKind =
|
||||
| 'mission'
|
||||
| 'investigate'
|
||||
| 'research'
|
||||
| 'plan'
|
||||
| 'backup'
|
||||
| 'fix'
|
||||
| 'check'
|
||||
| 'loop'
|
||||
| 'run'
|
||||
| 'save'
|
||||
| 'ask'
|
||||
| 'compact'
|
||||
| 'montage'
|
||||
| 'complete';
|
||||
|
||||
export type StoryActor = 'you' | 'claude' | 'helper' | 'system';
|
||||
|
||||
/** Who started a turn: you, a background task reporting back, another session, or a schedule/SDK. */
|
||||
export type StoryTurnOrigin = 'human' | 'task' | 'peer' | 'system';
|
||||
|
||||
/** One hunk of an edit, in the shape of Claude Code's `structuredPatch` (lines keep their ` `/`+`/`-` prefix). */
|
||||
export interface StoryHunk {
|
||||
oldStart: number;
|
||||
oldLines: number;
|
||||
newStart: number;
|
||||
newLines: number;
|
||||
lines: string[];
|
||||
}
|
||||
|
||||
/** What one Edit/Write call changed. Counts are exact even when `hunks` was cut to size. */
|
||||
export interface StoryEdit {
|
||||
file: string;
|
||||
added: number;
|
||||
removed: number;
|
||||
created: boolean;
|
||||
hunks: StoryHunk[];
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
/** A helper (Agent/Task/Workflow call): what it was asked and what it answered. */
|
||||
export interface StoryHelper {
|
||||
name: string;
|
||||
type: string;
|
||||
/** Claude's short description of the job ("Write a phone-size test"). */
|
||||
task?: string;
|
||||
prompt: string;
|
||||
result?: string;
|
||||
toolUses?: number;
|
||||
durationMs?: number;
|
||||
}
|
||||
|
||||
export interface StoryTests {
|
||||
passed: number;
|
||||
failed: number;
|
||||
}
|
||||
|
||||
export interface StoryTodo {
|
||||
text: string;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
/** One row of "the wire": a prompt, a sentence Claude said, or one tool call with its result. */
|
||||
export interface StoryEvent {
|
||||
id: string;
|
||||
at: string;
|
||||
actor: StoryActor;
|
||||
tool: string;
|
||||
label: string;
|
||||
intent: StoryIntent;
|
||||
/** False when the tool result was flagged as an error; absent while the call is still running. */
|
||||
ok?: boolean;
|
||||
pending?: boolean;
|
||||
/** The one file this event concerns (Read, Edit, Write), as the transcript gave it. */
|
||||
file?: string;
|
||||
line?: number;
|
||||
/** Claude's own short description of a Bash call ("Run the test suite"). */
|
||||
description?: string;
|
||||
output?: string;
|
||||
outputTruncated?: boolean;
|
||||
edit?: StoryEdit;
|
||||
helper?: StoryHelper;
|
||||
tests?: StoryTests;
|
||||
todos?: StoryTodo[];
|
||||
question?: { text: string; options: string[]; answer?: string };
|
||||
}
|
||||
|
||||
export interface StoryPanel {
|
||||
kind: StoryPanelKind;
|
||||
/** Indices into the page's `events`. */
|
||||
events: number[];
|
||||
caption: { story: string; director: string };
|
||||
balloon?: { who: 'you' | 'claude'; text: string };
|
||||
files?: string[];
|
||||
notes?: string[];
|
||||
helpers?: Array<{ name: string; type: string; task?: string; event: number }>;
|
||||
added?: number;
|
||||
removed?: number;
|
||||
ok?: boolean;
|
||||
rounds?: number;
|
||||
tests?: StoryTests;
|
||||
todos?: StoryTodo[];
|
||||
question?: { text: string; answer?: string };
|
||||
/** The turn is still running and this is its newest panel. */
|
||||
live?: boolean;
|
||||
}
|
||||
|
||||
export interface StoryTurnSummary {
|
||||
id: string;
|
||||
at: string;
|
||||
prompt: string;
|
||||
origin: StoryTurnOrigin;
|
||||
complete: boolean;
|
||||
interrupted: boolean;
|
||||
toolCalls: number;
|
||||
}
|
||||
|
||||
export interface StoryPage extends StoryTurnSummary {
|
||||
endedAt?: string;
|
||||
durationMs?: number;
|
||||
/** The turn began before the part of the transcript that was read. */
|
||||
partial: boolean;
|
||||
/** Older events, outputs or hunks were dropped to keep the page small. */
|
||||
truncated: boolean;
|
||||
panels: StoryPanel[];
|
||||
events: StoryEvent[];
|
||||
filesChanged: number;
|
||||
added: number;
|
||||
removed: number;
|
||||
helpers: number;
|
||||
}
|
||||
|
||||
export type StoryResponse =
|
||||
| { supported: false; reason: string }
|
||||
| { supported: true; turns: StoryTurnSummary[]; page: StoryPage | null };
|
||||
Reference in new issue
Block a user