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
+3
-1
@@ -54,6 +54,8 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L
|
||||
|
||||
**Settings surface** (`#appSettingsModal` + `#sessionOptionsModal` + `#createCaseModal`): one `set-*` language shared through a single `:is(...)` id scope in styles.css. App Settings' rail is a table of contents over ONE scrolling document (`switchSettingsTab` scrolls); Session Options and Add Case really switch (`switchOptionsTab` / `switchCaseModalTab`), and their larger per-modal size blocks are the design, not drift. ⚠️ **The load/save contract is `getElementById` by id**: renaming or dropping a control id silently stops it loading or saving. ⚠️ The Session Options "Session" entry still keys off `context` (label-only rename). ⚠️ Add Case keeps its legacy `.form-row` markup via an adapter; every `<details>` there needs `.set-adv-chev` plus both marker suppressions. ⚠️ Model cards and the effort segment are views over hidden `<select>`s, which stay the source of truth. ⚠️ `.modal-tabs*` classes are retired; `admin-ui.js` needs `.set-rail-items` + `.set-doc` to survive any restructure. Guard: `test/app-settings-structure.test.ts`. → [architecture-invariants#settings-surface-app-settings-session-options-add-case](../../docs/architecture-invariants.md#settings-surface-app-settings-session-options-add-case)
|
||||
|
||||
**Story Mode** (`story-mode.js` + `story-mode.css`, `showStoryButton`, per-device, default OFF, header button hidden on phones by mobile.css): the active Claude session's turns as comic pages from `GET /api/sessions/:id/story` (server core `src/story-mode.ts`, pure; reader `src/story-reader.ts`). ⚠️ Live refresh is the transcript tool events plus the stop and prompt-submitted hooks (extra `addListener`s in `connectSSE`, so the handler map keeps one method per event) and a 4 s poll while the turn runs, one request in flight, debounced; never the watcher's `transcript:complete`, which interactive transcripts never raise. ⚠️ Every string on the page is the agent's: `textContent` only, containers carry `data-i18n-skip`; a file opens through `openFilePreview()`, never a new route. ⚠️ Canvas click regions become real buttons (`_storySyncHits`), so they are focusable and labelled; a panel keeps its clock across refreshes (only a NEW panel animates in). ⚠️ The overlay owns Escape with a WINDOW capture listener (it runs before app.js's document one): drawer first, then the overlay, and nothing while the file preview or folder grid is open on top. ⚠️ Its comic palette is its own (`--story-*` tokens, `.story-paper` on a light skin), so a skin never repaints it. Tests: `test/story-mode.test.ts`, `test/routes/story-routes.test.ts`.
|
||||
|
||||
**Header button visibility**: most header controls are opt-in and hidden by a marker class (`btn-multimonitor--hidden`, `btn-response-viewer-header--hidden`, `btn-file-viewer--hidden`, `btn-cron--hidden`, and the desktop-gated `btn-split--hidden` / `btn-tile-grid--hidden`, which also need a `@media (max-width: 1179px)` backstop) that `applyHeaderVisibilitySettings()` (settings-ui.js) toggles after settings load; the multi-monitor button is instead stripped at render by `renderIndexHtml`. ⚠️ Hiding must go through the marker class: the base rules are `display:inline-flex !important`, so an inline style cannot override them. Current desktop default is WS/CPU/MEM + File Viewer + Tiles (1180px and wider) + gear, with the token chip and lifecycle-log button OFF. ⚠️ New header controls must not leak onto phones; `test/mobile-header-buttons-policy.test.ts` is the static guard. → [architecture-invariants#header-button-visibility-multi-monitor-response-viewer-file-viewer-cron](../../docs/architecture-invariants.md#header-button-visibility-multi-monitor-response-viewer-file-viewer-cron)
|
||||
|
||||
**Terminal font weight** (`terminalFontWeight` / `terminalFontWeightBold`, per-device, default = xterm's own `normal`/`bold`): Claude Code's markdown bold is a bare `ESC[1m`, so the weight step is its only cue. `CodemanTerminalFont.resolveWeights()` (constants.js, pure) resolves each slot against **its own** xterm default. ⚠️ The `@font-face` for `fonts/jetbrains-mono-variable.woff2` must stay declared `100 800` (the browser synthesizes from the descriptor, not the file); narrowing it silently makes the setting a no-op. ⚠️ A live save must reach both echo overlays (`refreshFont()`) and open Agent Teams panes. ⚠️ Leave `_awaitTerminalFont()` untouched. Test: `test/terminal-font-weight.test.ts`. → [architecture-invariants#terminal-font-weight](../../docs/architecture-invariants.md#terminal-font-weight)
|
||||
@@ -86,6 +88,6 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L
|
||||
|
||||
**SSE staleness watchdog** (`computeSseStale()` in constants.js, `_checkSseStale()` + a 5s interval in app.js): an `EventSource` can stop delivering without erroring, so the client forces a reconnect when nothing arrives. ⚠️ The server keepalive must stay the named `sse:heartbeat` event (`cleanupDeadClients()`, sse-stream-manager.ts), never an SSE comment, which `EventSource` cannot observe; its no-op client listener must stay registered. ⚠️ Judge staleness only while `connected` and online (the loop breaker). ⚠️ The liveness stamp lives inside `addListener`. ⚠️ Clear the interval only at the top of `connectSSE()`, or intervals stack. → [architecture-invariants#sse-staleness-watchdog](../../docs/architecture-invariants.md#sse-staleness-watchdog)
|
||||
|
||||
**Z-index layers** (keep new overlays consistent with this stack): local echo overlay (7; with local echo on it also draws the iOS IME composition preview, as an underlined tail after its pending text via `setComposition`), iOS IME composition preview span when local echo is off (6 inside `.xterm-helpers`, whose own z-index 5 is its EFFECTIVE layer, so it sits UNDER the overlay and must never be used while the overlay shows text), terminal touch-selection bar (900, below floating agent windows), subagent windows + split picker menu + Tiles count menu (1000), plan agents (1100), mobile/tablet fixed header (1200), modals on ≤768px (1300, must beat the fixed header), log viewers (2000), connection-loss overlay (2500), image popups (3000), response viewer (5000, backdrop 4999), folder grid (5090, above the response viewer it opens from, below the file preview its tiles open), file-preview overlay (5100, must outrank the response viewer that launches it), toasts/path picker (10000+), custom-model center-status banner (10001; its `[hidden]` must re-assert `display: none` or `dismiss()` leaves an invisible click-blocker), custom-model swap-confirm/context-warning modals (10010). → [architecture-invariants#z-index-layers](../../docs/architecture-invariants.md#z-index-layers)
|
||||
**Z-index layers** (keep new overlays consistent with this stack): local echo overlay (7; with local echo on it also draws the iOS IME composition preview, as an underlined tail after its pending text via `setComposition`), iOS IME composition preview span when local echo is off (6 inside `.xterm-helpers`, whose own z-index 5 is its EFFECTIVE layer, so it sits UNDER the overlay and must never be used while the overlay shows text), terminal touch-selection bar (900, below floating agent windows), subagent windows + split picker menu + Tiles count menu (1000), plan agents (1100), mobile/tablet fixed header (1200), modals on ≤768px (1300, must beat the fixed header), log viewers (2000), Story Mode overlay (2400, below the connection-loss overlay so a lost server still shows), connection-loss overlay (2500), image popups (3000), response viewer (5000, backdrop 4999), folder grid (5090, above the response viewer it opens from, below the file preview its tiles open), file-preview overlay (5100, must outrank the response viewer that launches it), toasts/path picker (10000+), custom-model center-status banner (10001; its `[hidden]` must re-assert `display: none` or `dismiss()` leaves an invisible click-blocker), custom-model swap-confirm/context-warning modals (10010). → [architecture-invariants#z-index-layers](../../docs/architecture-invariants.md#z-index-layers)
|
||||
|
||||
**Keyboard shortcuts**: Escape (close), Ctrl+? (shortcut overlay), Ctrl/Cmd/Alt+K (session palette), Ctrl+Tab (next), Alt+[/] (prev/next tab), Alt+1-9 (switch tab), Ctrl+Shift+{/} (move tab left/right), Shift+Enter or Ctrl+Enter (newline), Ctrl+C (copy selection, else interrupt) / Ctrl+Shift+C (copy, never interrupts), Ctrl+L (clear), Ctrl+Shift+R (restore size), Ctrl+Shift+V (voice input), Ctrl/Cmd +/- (font), Shift+Wheel (local scrollback when mouse passthrough is active), Shift+drag (start a selection in a stripped-DECSET pane, where xterm's own Shift branch is unreachable and a Shift+drag used to select nothing; `_installShiftDragSelection`), right-click (copy the selection, the mintty/PuTTY convention, since xterm paints into a canvas and the native menu has no Copy for it; with nothing selected the native menu is left alone). Rebindable via the registry. ⚠️ **Ctrl+W is deliberately NOT bound** (owner decision): it is delete-word in every shell and agent CLI, and as Close Session it killed sessions with no confirm; `close-session` keeps `bindings: []` and stays bindable (`test/ctrl-w-never-closes.test.ts`).
|
||||
@@ -2197,6 +2197,18 @@ class CodemanApp {
|
||||
addListener(event, () => this._onSessionListMaybeChanged());
|
||||
}
|
||||
|
||||
// Story Mode (story-mode.js): any sign of transcript activity refreshes an open story for that
|
||||
// session. Extra listeners like the ones above, so the handler map stays one method per event.
|
||||
// No transcript `complete` event: interactive transcripts carry no `result` entry to raise it.
|
||||
for (const event of [
|
||||
SSE_EVENTS.TRANSCRIPT_TOOL_START,
|
||||
SSE_EVENTS.TRANSCRIPT_TOOL_END,
|
||||
SSE_EVENTS.HOOK_STOP,
|
||||
SSE_EVENTS.HOOK_PROMPT_SUBMITTED,
|
||||
]) {
|
||||
addListener(event, (e) => this._storyOnActivity?.(e));
|
||||
}
|
||||
|
||||
// Docker export/import: toast + refresh the Manage-tab exports list on completion.
|
||||
addListener(SSE_EVENTS.DOCKER_EXPORT_COMPLETE, (e) => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
Story Mode comic fonts (src/web/public/fonts/bowlby-one-sc-latin.woff2, src/web/public/fonts/patrick-hand-sc-latin.woff2).
|
||||
Both are the Latin subsets served by Google Fonts, unmodified, under the SIL Open Font License 1.1.
|
||||
|
||||
=== Bowlby One SC ===
|
||||
Copyright (c) 2011 by vernon adams (vern@newtypography.co.uk),
|
||||
with Reserved Font Names "Bowlby" "Bowlby One" and "Bowlby One SC"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
=== Patrick Hand SC ===
|
||||
Copyright 2012 The Patrick Hand Authors (mail@patrickwagesreiter.at)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
@@ -17,6 +17,7 @@
|
||||
<title>Codeman</title>
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%2360a5fa'/%3E%3Cstop offset='100%25' stop-color='%233b82f6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='32' height='32' rx='6' fill='%230a0a0a'/%3E%3Cpath d='M18 4L8 18h6l-2 10 10-14h-6z' fill='url(%23g)'/%3E%3C/svg%3E">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="story-mode.css">
|
||||
<link rel="stylesheet" href="mobile.css" media="(max-width: 1023px)">
|
||||
<!-- xterm.css loaded async — terminal won't display until xterm.js runs anyway -->
|
||||
<link rel="preload" href="vendor/xterm.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
@@ -181,6 +182,7 @@
|
||||
</div>
|
||||
<button class="btn-icon-header btn-redraw-terminal btn-redraw-terminal--hidden" onclick="app.restoreTerminalSize()" title="Redraw terminal to fit current screen (Ctrl+Shift+R)" aria-label="Redraw terminal"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="1 4 1 10 7 10"/><polyline points="23 20 23 14 17 14"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg></button>
|
||||
<button class="btn-icon-header btn-response-viewer-header btn-response-viewer-header--hidden" onclick="app.toggleResponseViewer()" title="View last response" aria-label="View last response"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
|
||||
<button class="btn-icon-header btn-story-header btn-story-header--hidden" onclick="app.toggleStoryMode()" title="Story Mode: this session as a comic" aria-label="Story Mode"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="8" height="8" rx="1"/><rect x="13" y="3" width="8" height="8" rx="1"/><rect x="3" y="13" width="18" height="8" rx="1"/></svg></button>
|
||||
<button class="btn-icon-header btn-away-digest btn-away-digest--hidden" onclick="app.openAwayDigest()" title="Away Digest" aria-label="Open away digest"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/></svg></button>
|
||||
<button class="btn-icon-header btn-session-manager btn-session-manager--hidden" onclick="app.openSessionManager()" title="Session Manager" aria-label="Open session manager"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg></button>
|
||||
<button class="btn-icon-header btn-approvals btn-approvals--hidden" id="approvalsBtn" onclick="app.toggleApprovalsInbox()" title="Approvals waiting on you" aria-label="Open approvals inbox" aria-expanded="false">
|
||||
@@ -2067,11 +2069,12 @@
|
||||
<div class="set-group-head"><h4>Header buttons</h4><span class="set-scope">device</span></div>
|
||||
<p class="set-group-hint">Tap to show a control in the header. Multi-monitor is the one entry here that syncs across devices.</p>
|
||||
<div class="set-group-body">
|
||||
<div class="set-chips" data-search="header buttons plan usage font stats lifecycle response file viewer attachments monitor session away cron redraw split tiles">
|
||||
<div class="set-chips" data-search="header buttons plan usage font stats lifecycle response story file viewer attachments monitor session away cron redraw split tiles">
|
||||
<label class="set-chip" data-preview="header" data-preview-order="1" data-preview-text="A+"><input type="checkbox" id="appSettingsShowFontControls"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20 10 5l6 15"/><path d="M6.5 15h7"/><path d="M18 12h4M20 10v4"/></svg><span>Font Size</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="2" data-preview-text="CPU 12%"><input type="checkbox" id="appSettingsShowSystemStats" checked><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12h4l2.5-7 4 14L16 12h5"/></svg><span>System Stats</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="3"><input type="checkbox" id="appSettingsShowRedrawButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="1 4 1 10 7 10"/><polyline points="23 20 23 14 17 14"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg><span>Redraw Terminal</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="4"><input type="checkbox" id="appSettingsShowResponseViewer"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg><span>Response Viewer</span></label>
|
||||
<label class="set-chip" data-search="story mode comic panels" data-preview="header" data-preview-order="4.5"><input type="checkbox" id="appSettingsShowStoryButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="8" height="8" rx="1"/><rect x="13" y="3" width="8" height="8" rx="1"/><rect x="3" y="13" width="18" height="8" rx="1"/></svg><span>Story Mode</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="5"><input type="checkbox" id="appSettingsShowAwayDigestButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/></svg><span>Away Digest</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="6"><input type="checkbox" id="appSettingsShowSessionButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg><span>Session Manager</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="9"><input type="checkbox" id="appSettingsShowAttachmentsButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg><span>Attachments</span></label>
|
||||
@@ -4272,6 +4275,7 @@
|
||||
<script defer src="mobile-overview.js"></script>
|
||||
<script defer src="home-sessions.js"></script>
|
||||
<script defer src="git-status-ui.js"></script>
|
||||
<script defer src="story-mode.js"></script>
|
||||
<script defer src="entrance-animations.js"></script>
|
||||
<script defer src="ralph-wizard.js"></script>
|
||||
<script defer src="api-client.js"></script>
|
||||
|
||||
@@ -589,7 +589,10 @@ html.mobile-init .file-browser-panel {
|
||||
/* Approvals bell: phones answer from the overview's NEEDS YOU rows instead
|
||||
(inline approve/deny in mobile-overview.js); the bell would only crowd the
|
||||
header it was designed to stay out of. */
|
||||
.btn-icon-header.btn-approvals {
|
||||
.btn-icon-header.btn-approvals,
|
||||
/* Story Mode: opt-in and desktop-first for now; the overlay itself is responsive, a phone
|
||||
entry point is a follow-up rather than one more icon in this header. */
|
||||
.btn-icon-header.btn-story-header {
|
||||
display: none !important;
|
||||
}
|
||||
/* Split-pane is hidden here too, but the AUTHORITATIVE hard gate is the
|
||||
|
||||
@@ -422,6 +422,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
document.getElementById('appSettingsHeaderStatsStyle').value = this.resolveHeaderStatsStyle(settings);
|
||||
document.getElementById('appSettingsShowLifecycleLog').checked = settings.showLifecycleLog ?? defaults.showLifecycleLog ?? false;
|
||||
document.getElementById('appSettingsShowResponseViewer').checked = settings.showResponseViewer ?? defaults.showResponseViewer ?? false;
|
||||
document.getElementById('appSettingsShowStoryButton').checked = settings.showStoryButton ?? defaults.showStoryButton ?? false;
|
||||
document.getElementById('appSettingsShowFileViewerButton').checked = settings.showFileViewerButton ?? defaults.showFileViewerButton ?? true;
|
||||
document.getElementById('appSettingsShowAttachmentsButton').checked = settings.showAttachmentsButton ?? defaults.showAttachmentsButton ?? false;
|
||||
document.getElementById('appSettingsSkin').value = settings.skin ?? defaults.skin ?? 'daylight-blue';
|
||||
@@ -2634,6 +2635,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
headerStatsStyle: document.getElementById('appSettingsHeaderStatsStyle').value,
|
||||
showLifecycleLog: document.getElementById('appSettingsShowLifecycleLog').checked,
|
||||
showResponseViewer: document.getElementById('appSettingsShowResponseViewer').checked,
|
||||
showStoryButton: document.getElementById('appSettingsShowStoryButton').checked,
|
||||
showFileViewerButton: document.getElementById('appSettingsShowFileViewerButton').checked,
|
||||
showAttachmentsButton: document.getElementById('appSettingsShowAttachmentsButton').checked,
|
||||
showMonitor: document.getElementById('appSettingsShowMonitor').checked,
|
||||
@@ -4137,6 +4139,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
responseViewerBtn.classList.toggle('btn-response-viewer-header--hidden', !showResponseViewer);
|
||||
}
|
||||
|
||||
// Story Mode (story-mode.js): opt-in, default OFF; turning it off also closes an open story.
|
||||
this.applyStoryButtonVisibility?.();
|
||||
|
||||
// Hide the attachments (history) button when disabled. Opt-in, default OFF —
|
||||
// marker class, base is display:inline-flex !important.
|
||||
const showAttachmentsButton = settings.showAttachmentsButton ?? defaults.showAttachmentsButton ?? false;
|
||||
@@ -4600,7 +4605,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
_displaySettingKeySet() {
|
||||
const displayKeys = new Set([
|
||||
'showFontControls', 'showSystemStats', 'headerStatsStyle', 'showTokenCount', 'showCost',
|
||||
'showLifecycleLog', 'showResponseViewer', 'showRedrawButton',
|
||||
'showLifecycleLog', 'showResponseViewer', 'showStoryButton', 'showRedrawButton',
|
||||
'showMonitor', 'showProjectInsights', 'showFileBrowser', 'showSubagents',
|
||||
'subagentActiveTabOnly', 'tabTwoRows', 'showTabCliLogos', 'tabOrientation', 'tabRailWidth', 'tabRailDetail', 'tabRailSort', 'tabArrangement', 'tabStateOrder', 'sessionListLayout', 'sessionSidebarFontSize', 'localEchoEnabled', 'cjkInputEnabled', 'extendedKeyboardBar',
|
||||
'skin', 'showPlanUsageLimits', 'showAttachmentsButton', 'showFileViewerButton', 'webglRendererEnabled',
|
||||
|
||||
@@ -0,0 +1,848 @@
|
||||
/* Story Mode (story-mode.js): a session's turns as comic pages.
|
||||
Layout: a full-screen overlay (bar on top, the comic sheet below, "the wire" beside it in the
|
||||
Director's cut) with a details drawer on the right. The comic keeps its own palette, the four
|
||||
process inks, so it reads the same on every Codeman skin; a light skin gets the paper variant.
|
||||
Fonts are self-hosted (CSP font-src 'self'); licenses in fonts/LICENSE-story-fonts.txt. */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Codeman Story Display';
|
||||
src: url('fonts/bowlby-one-sc-latin.woff2') format('woff2');
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Codeman Story Letter';
|
||||
src: url('fonts/patrick-hand-sc-latin.woff2') format('woff2');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
.story-overlay {
|
||||
/* Night edition: dark panels, light ink for marks drawn on them, dark line art on objects. */
|
||||
--story-bg: #0d0d12;
|
||||
--story-sheet: #15151c;
|
||||
--story-panel: #1c1c25;
|
||||
--story-ink: #edeae4;
|
||||
--story-line: #121016;
|
||||
--story-muted: #a49fae;
|
||||
--story-rule: #2e2d38;
|
||||
--story-cyan: #33b7ea;
|
||||
--story-magenta: #ff4e97;
|
||||
--story-yellow: #ffd43d;
|
||||
--story-go: #3ec27c;
|
||||
--story-pencil: #4f86b8;
|
||||
--story-balloon: #f3f1ec;
|
||||
--story-balloon-ink: #121016;
|
||||
--story-cap: #ffd43d;
|
||||
--story-cap-ink: #121016;
|
||||
--story-display: 'Codeman Story Display', Impact, 'Arial Black', sans-serif;
|
||||
--story-letter: 'Codeman Story Letter', 'Comic Sans MS', 'Chalkboard SE', cursive;
|
||||
--story-body: 'Manrope', system-ui, sans-serif;
|
||||
--story-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
|
||||
--story-drawer-w: min(560px, 44vw);
|
||||
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2400;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
background: var(--story-bg);
|
||||
color: var(--story-ink);
|
||||
font-family: var(--story-body);
|
||||
color-scheme: dark;
|
||||
padding-top: env(safe-area-inset-top, 0px);
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
.story-overlay.story-paper {
|
||||
--story-bg: #ecede9;
|
||||
--story-sheet: #fbfbf9;
|
||||
--story-panel: #ffffff;
|
||||
--story-ink: #18151d;
|
||||
--story-line: #18151d;
|
||||
--story-muted: #5f5a68;
|
||||
--story-rule: #d5d3da;
|
||||
--story-cyan: #0e9fd6;
|
||||
--story-magenta: #e0237a;
|
||||
--story-yellow: #ffd019;
|
||||
--story-go: #1a9a55;
|
||||
--story-pencil: #5fa6df;
|
||||
--story-balloon: #ffffff;
|
||||
--story-balloon-ink: #18151d;
|
||||
--story-cap: #ffd019;
|
||||
--story-cap-ink: #18151d;
|
||||
color-scheme: light;
|
||||
}
|
||||
.story-overlay[hidden] {
|
||||
display: none;
|
||||
}
|
||||
body.story-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
.story-overlay button {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.story-overlay :focus-visible {
|
||||
outline: 3px solid var(--story-cyan);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Bar */
|
||||
.story-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px 16px;
|
||||
padding: 10px max(16px, env(safe-area-inset-right, 0px)) 10px max(16px, env(safe-area-inset-left, 0px));
|
||||
border-bottom: 2px solid var(--story-ink);
|
||||
background: var(--story-sheet);
|
||||
}
|
||||
.story-brand {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
flex: 1 1 180px;
|
||||
}
|
||||
.story-title {
|
||||
font: 28px/1 var(--story-display);
|
||||
color: var(--story-yellow);
|
||||
-webkit-text-stroke: 1.5px var(--story-line);
|
||||
paint-order: stroke fill;
|
||||
text-shadow: 3px 3px 0 var(--story-magenta);
|
||||
}
|
||||
.story-session {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font: 500 12.5px/1.2 var(--story-mono);
|
||||
color: var(--story-muted);
|
||||
}
|
||||
.story-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
flex: 1 1 200px;
|
||||
}
|
||||
.story-turns {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
max-width: 420px;
|
||||
padding: 6px 8px;
|
||||
font: 500 12.5px/1.2 var(--story-mono);
|
||||
color: var(--story-ink);
|
||||
background: var(--story-panel);
|
||||
border: 2px solid var(--story-ink);
|
||||
border-radius: 0;
|
||||
}
|
||||
.story-btn {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
min-width: 34px;
|
||||
height: 34px;
|
||||
padding: 0 8px;
|
||||
font: 20px/1 var(--story-body) !important;
|
||||
background: var(--story-panel);
|
||||
border: 2px solid var(--story-ink);
|
||||
border-radius: 0;
|
||||
}
|
||||
.story-btn:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
.story-seg {
|
||||
display: inline-flex;
|
||||
border: 2.5px solid var(--story-ink);
|
||||
}
|
||||
.story-seg-btn {
|
||||
padding: 6px 12px 5px;
|
||||
font: 18px/1 var(--story-letter) !important;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
.story-seg-btn[aria-pressed='true'] {
|
||||
background: var(--story-ink);
|
||||
color: var(--story-sheet) !important;
|
||||
}
|
||||
.story-live {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font: 600 12px/1 var(--story-mono);
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--story-magenta);
|
||||
}
|
||||
.story-live[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.story-live i {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--story-magenta);
|
||||
animation: story-blink 1s steps(2, start) infinite;
|
||||
}
|
||||
@keyframes story-blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* Main: the sheet and the wire */
|
||||
.story-main {
|
||||
overflow: auto;
|
||||
padding: 16px max(16px, env(safe-area-inset-right, 0px)) 40px max(16px, env(safe-area-inset-left, 0px));
|
||||
}
|
||||
.story-overlay.has-drawer .story-main {
|
||||
padding-right: calc(var(--story-drawer-w) + 16px);
|
||||
}
|
||||
.story-note {
|
||||
max-width: 1240px;
|
||||
margin: 0 auto 14px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
color: var(--story-ink);
|
||||
border: 2px dashed var(--story-pencil);
|
||||
}
|
||||
.story-note[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.story-stage {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
max-width: 1240px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.story-overlay.is-director .story-stage {
|
||||
grid-template-columns: minmax(0, 1fr) 320px;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.story-overlay.is-director .story-stage {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
.story-sheet {
|
||||
container: story-sheet / inline-size;
|
||||
background: var(--story-sheet);
|
||||
}
|
||||
.story-page {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
grid-auto-rows: 270px;
|
||||
gap: 14px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
/* Panels */
|
||||
.story-panel {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
grid-column: span var(--story-span6, 2);
|
||||
background: var(--story-panel);
|
||||
border: 3px solid var(--story-ink);
|
||||
}
|
||||
.story-panel canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.story-panel.is-live {
|
||||
border-color: var(--story-magenta);
|
||||
}
|
||||
.story-hit {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
}
|
||||
.story-hit:hover,
|
||||
.story-hit:focus-visible {
|
||||
outline: 3px dashed var(--story-magenta) !important;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.story-cap {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
margin: 0;
|
||||
max-width: min(64%, 34ch);
|
||||
padding: 5px 9px 6px;
|
||||
background: var(--story-cap);
|
||||
color: var(--story-cap-ink);
|
||||
border: 2.5px solid var(--story-cap-ink);
|
||||
font: 17px/1.13 var(--story-letter);
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.story-cap-dir {
|
||||
display: none;
|
||||
}
|
||||
.story-overlay.is-director .story-cap {
|
||||
max-width: min(74%, 46ch);
|
||||
font: 500 11px/1.45 var(--story-mono);
|
||||
}
|
||||
.story-overlay.is-director .story-cap-story {
|
||||
display: none;
|
||||
}
|
||||
.story-overlay.is-director .story-cap-dir {
|
||||
display: inline;
|
||||
}
|
||||
.story-balloon {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
margin: 0;
|
||||
max-width: 42%;
|
||||
padding: 9px 15px 10px;
|
||||
border-radius: 22px;
|
||||
background: var(--story-balloon);
|
||||
color: var(--story-balloon-ink);
|
||||
border: 2.5px solid var(--story-balloon-ink);
|
||||
font: 17px/1.12 var(--story-letter);
|
||||
text-wrap: balance;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.story-balloon[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.story-balloon::before,
|
||||
.story-balloon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
}
|
||||
.story-balloon--claude {
|
||||
right: 18%;
|
||||
top: 28%;
|
||||
}
|
||||
.story-balloon--claude::before {
|
||||
right: 22px;
|
||||
bottom: -19px;
|
||||
width: 22px;
|
||||
height: 19px;
|
||||
background: var(--story-balloon-ink);
|
||||
clip-path: polygon(0 0, 100% 0, 85% 100%);
|
||||
}
|
||||
.story-balloon--claude::after {
|
||||
right: 25.5px;
|
||||
bottom: -14px;
|
||||
width: 15px;
|
||||
height: 16.5px;
|
||||
background: var(--story-balloon);
|
||||
clip-path: polygon(0 0, 100% 0, 84% 100%);
|
||||
}
|
||||
.story-balloon--you {
|
||||
left: 16px;
|
||||
top: 74px;
|
||||
max-width: 54%;
|
||||
}
|
||||
.story-balloon--you::before {
|
||||
left: -22px;
|
||||
top: 34%;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background: var(--story-balloon-ink);
|
||||
clip-path: polygon(100% 0, 100% 100%, 0 75%);
|
||||
}
|
||||
.story-balloon--you::after {
|
||||
left: -15px;
|
||||
top: calc(34% + 3.5px);
|
||||
width: 17.5px;
|
||||
height: 15px;
|
||||
background: var(--story-balloon);
|
||||
clip-path: polygon(100% 0, 100% 100%, 0 76%);
|
||||
}
|
||||
.story-panel[data-kind='investigate'] .story-balloon--claude {
|
||||
right: auto;
|
||||
left: 30%;
|
||||
top: 30%;
|
||||
max-width: 38%;
|
||||
}
|
||||
.story-panel[data-kind='backup'] .story-balloon--claude {
|
||||
right: auto;
|
||||
left: 56%;
|
||||
top: 25%;
|
||||
max-width: 40%;
|
||||
}
|
||||
.story-panel[data-kind='ask'] .story-balloon--claude {
|
||||
right: auto;
|
||||
left: 14px;
|
||||
top: 30%;
|
||||
max-width: 46%;
|
||||
}
|
||||
.story-answer {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
left: 14px;
|
||||
bottom: 14px;
|
||||
margin: 0;
|
||||
max-width: 50%;
|
||||
padding: 5px 9px 4px;
|
||||
font: 13px/1.2 var(--story-display);
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--story-go);
|
||||
background: var(--story-panel);
|
||||
border: 3px solid var(--story-go);
|
||||
transform: rotate(-4deg);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.story-answer[hidden],
|
||||
.story-live-tag[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.story-live-tag {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
padding: 3px 7px 2px;
|
||||
font: 600 10.5px/1 var(--story-mono);
|
||||
letter-spacing: 0.1em;
|
||||
color: #fff;
|
||||
background: var(--story-magenta);
|
||||
}
|
||||
|
||||
.story-splash {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
left: 12px;
|
||||
top: 12px;
|
||||
bottom: 12px;
|
||||
width: 46%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.story-splash .story-cap,
|
||||
.story-splash .story-balloon {
|
||||
position: static;
|
||||
align-self: flex-start;
|
||||
max-width: 100%;
|
||||
}
|
||||
.story-splash .story-balloon::before,
|
||||
.story-splash .story-balloon::after {
|
||||
display: none;
|
||||
}
|
||||
.story-splash-title {
|
||||
margin: 0;
|
||||
font: 400 clamp(28px, 4.4cqi, 44px) / 0.92 var(--story-display);
|
||||
color: var(--story-yellow);
|
||||
-webkit-text-stroke: 2.5px var(--story-line);
|
||||
paint-order: stroke fill;
|
||||
text-shadow: 4px 4px 0 var(--story-line);
|
||||
transform: rotate(-3deg);
|
||||
transform-origin: left bottom;
|
||||
}
|
||||
.story-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.story-chips li > * {
|
||||
display: block;
|
||||
padding: 4px 8px;
|
||||
font: 500 12px/1.2 var(--story-mono) !important;
|
||||
background: var(--story-balloon);
|
||||
color: var(--story-balloon-ink) !important;
|
||||
border: 2px solid var(--story-line);
|
||||
}
|
||||
.story-chips button {
|
||||
text-decoration: underline dotted;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.story-chips button:hover {
|
||||
background: var(--story-yellow);
|
||||
}
|
||||
|
||||
.story-pencils {
|
||||
grid-column: span var(--story-span6, 2);
|
||||
display: grid;
|
||||
place-content: center;
|
||||
gap: 4px;
|
||||
text-align: center;
|
||||
color: var(--story-pencil);
|
||||
border: 2.5px dashed var(--story-pencil);
|
||||
}
|
||||
.story-pencils span {
|
||||
font: 26px/1 var(--story-letter);
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.story-pencils small {
|
||||
font: 12px/1.3 var(--story-mono);
|
||||
}
|
||||
|
||||
.story-fresh .story-cap,
|
||||
.story-fresh .story-splash-title {
|
||||
animation: story-pop 0.45s cubic-bezier(0.3, 1.6, 0.5, 1) both;
|
||||
}
|
||||
.story-fresh .story-balloon,
|
||||
.story-fresh .story-chips {
|
||||
animation: story-pop 0.45s 0.35s cubic-bezier(0.3, 1.6, 0.5, 1) both;
|
||||
}
|
||||
@keyframes story-pop {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.6);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@container story-sheet (max-width: 1000px) {
|
||||
.story-page {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.story-panel {
|
||||
grid-column: span var(--story-span2, 1);
|
||||
}
|
||||
.story-pencils {
|
||||
grid-column: span var(--story-span2, 1);
|
||||
}
|
||||
}
|
||||
@container story-sheet (max-width: 600px) {
|
||||
.story-page {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-auto-rows: auto;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
.story-panel,
|
||||
.story-pencils {
|
||||
grid-column: auto;
|
||||
height: 300px;
|
||||
}
|
||||
.story-panel[data-kind='complete'] {
|
||||
height: 430px;
|
||||
}
|
||||
.story-splash {
|
||||
right: 12px;
|
||||
bottom: auto;
|
||||
width: auto;
|
||||
}
|
||||
.story-splash-title {
|
||||
font-size: 36px;
|
||||
}
|
||||
.story-balloon {
|
||||
max-width: 52%;
|
||||
}
|
||||
}
|
||||
|
||||
/* The wire */
|
||||
.story-wire {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
max-height: calc(100vh - 120px);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--story-sheet);
|
||||
border: 2.5px solid var(--story-ink);
|
||||
}
|
||||
.story-wire[hidden] {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.story-wire {
|
||||
position: static;
|
||||
max-height: 460px;
|
||||
}
|
||||
}
|
||||
.story-wire-head {
|
||||
padding: 11px 13px 10px;
|
||||
border-bottom: 2px solid var(--story-ink);
|
||||
}
|
||||
.story-wire-title {
|
||||
margin: 0;
|
||||
font: 22px/1 var(--story-letter);
|
||||
}
|
||||
.story-wire-sub {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: var(--story-muted);
|
||||
}
|
||||
.story-wire-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 4px 0 10px;
|
||||
list-style: none;
|
||||
overflow: auto;
|
||||
}
|
||||
.story-wire-group {
|
||||
padding: 12px 13px 3px;
|
||||
font: 16px/1.1 var(--story-letter);
|
||||
color: var(--story-muted);
|
||||
}
|
||||
.story-ev {
|
||||
display: grid;
|
||||
grid-template-columns: 62px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
padding: 4px 13px;
|
||||
font: 11.5px/1.45 var(--story-mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.story-ev-time {
|
||||
color: var(--story-muted);
|
||||
}
|
||||
.story-ev-body {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.story-ev-who {
|
||||
color: var(--story-muted);
|
||||
}
|
||||
.story-link {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
text-align: left;
|
||||
font: inherit !important;
|
||||
text-decoration: underline dotted;
|
||||
text-decoration-thickness: 1.5px;
|
||||
text-underline-offset: 3px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.story-link:hover {
|
||||
color: var(--story-yellow) !important;
|
||||
}
|
||||
.story-tag {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
font-style: normal;
|
||||
font-size: 10.5px;
|
||||
color: var(--story-muted);
|
||||
border: 1.5px solid currentColor;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.story-tag--bad {
|
||||
color: var(--story-magenta);
|
||||
}
|
||||
.story-tag--ok {
|
||||
color: var(--story-go);
|
||||
}
|
||||
.story-tag--pending {
|
||||
color: var(--story-yellow);
|
||||
}
|
||||
|
||||
/* Drawer */
|
||||
.story-drawer {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: var(--story-drawer-w);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: env(safe-area-inset-top, 0px);
|
||||
padding-right: env(safe-area-inset-right, 0px);
|
||||
background: var(--story-sheet);
|
||||
border-left: 3px solid var(--story-ink);
|
||||
}
|
||||
.story-drawer[hidden] {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.story-overlay {
|
||||
--story-drawer-w: 100vw;
|
||||
}
|
||||
.story-overlay.has-drawer .story-main {
|
||||
padding-right: max(16px, env(safe-area-inset-right, 0px));
|
||||
}
|
||||
.story-drawer {
|
||||
top: auto;
|
||||
left: 0;
|
||||
width: auto;
|
||||
height: 82vh;
|
||||
border-left: 0;
|
||||
border-top: 3px solid var(--story-ink);
|
||||
padding-left: env(safe-area-inset-left, 0px);
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
}
|
||||
.story-drawer-head {
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 14px 60px 12px 18px;
|
||||
border-bottom: 2px solid var(--story-ink);
|
||||
}
|
||||
.story-drawer-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.story-drawer-kind {
|
||||
padding: 3px 8px 2px;
|
||||
font: 16px/1 var(--story-letter);
|
||||
background: var(--story-cap);
|
||||
color: var(--story-cap-ink);
|
||||
border: 2px solid var(--story-cap-ink);
|
||||
}
|
||||
.story-drawer-title {
|
||||
margin: 0;
|
||||
font: 500 13.5px/1.35 var(--story-mono);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.story-drawer-sub {
|
||||
margin: 0;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.4;
|
||||
color: var(--story-muted);
|
||||
}
|
||||
.story-drawer-close {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
}
|
||||
.story-drawer-body {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 4px 0 28px;
|
||||
}
|
||||
.story-btn-s {
|
||||
padding: 4px 10px 3px;
|
||||
font: 15px/1 var(--story-letter) !important;
|
||||
background: var(--story-panel);
|
||||
border: 2px solid var(--story-ink);
|
||||
}
|
||||
.story-df-head,
|
||||
.story-out-head {
|
||||
position: sticky;
|
||||
top: -4px;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px 10px;
|
||||
padding: 12px 18px 8px;
|
||||
background: var(--story-sheet);
|
||||
border-bottom: 1.5px solid var(--story-rule);
|
||||
}
|
||||
.story-df + .story-df .story-df-head,
|
||||
.story-out + .story-out .story-out-head {
|
||||
border-top: 2px solid var(--story-ink);
|
||||
}
|
||||
.story-df-path,
|
||||
.story-out-cmd {
|
||||
flex: 1 1 220px;
|
||||
font: 500 12.5px/1.35 var(--story-mono);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.story-cnt {
|
||||
font: 500 12.5px/1 var(--story-mono);
|
||||
}
|
||||
.story-cnt-a {
|
||||
color: var(--story-go);
|
||||
}
|
||||
.story-cnt-d {
|
||||
color: var(--story-magenta);
|
||||
}
|
||||
.story-code {
|
||||
font: 12px/1.65 var(--story-mono);
|
||||
}
|
||||
.story-ln {
|
||||
display: grid;
|
||||
grid-template-columns: 3.2em 3.2em 1.6em minmax(0, 1fr);
|
||||
padding-right: 14px;
|
||||
}
|
||||
.story-ln-no {
|
||||
padding-right: 8px;
|
||||
text-align: right;
|
||||
color: var(--story-muted);
|
||||
user-select: none;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.story-ln-mk {
|
||||
text-align: center;
|
||||
color: var(--story-muted);
|
||||
user-select: none;
|
||||
}
|
||||
.story-ln-tx {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.story-ln--add {
|
||||
background: color-mix(in srgb, var(--story-go) 17%, transparent);
|
||||
}
|
||||
.story-ln--add .story-ln-mk {
|
||||
color: var(--story-go);
|
||||
}
|
||||
.story-ln--del {
|
||||
background: color-mix(in srgb, var(--story-magenta) 15%, transparent);
|
||||
}
|
||||
.story-ln--del .story-ln-mk {
|
||||
color: var(--story-magenta);
|
||||
}
|
||||
.story-ln--hunk {
|
||||
display: block;
|
||||
padding: 2px 18px;
|
||||
color: var(--story-muted);
|
||||
background: color-mix(in srgb, var(--story-cyan) 12%, transparent);
|
||||
}
|
||||
.story-arrive {
|
||||
animation: story-arrive 1.1s ease-out both;
|
||||
}
|
||||
@keyframes story-arrive {
|
||||
0% {
|
||||
clip-path: inset(0 100% 0 0);
|
||||
background: color-mix(in srgb, var(--story-yellow) 45%, transparent);
|
||||
}
|
||||
45% {
|
||||
clip-path: inset(0 0 0 0);
|
||||
background: color-mix(in srgb, var(--story-yellow) 45%, transparent);
|
||||
}
|
||||
}
|
||||
.story-out-desc,
|
||||
.story-empty,
|
||||
.story-answer-line {
|
||||
margin: 0;
|
||||
padding: 8px 18px 0;
|
||||
font-size: 13.5px;
|
||||
color: var(--story-muted);
|
||||
}
|
||||
.story-pre {
|
||||
margin: 0;
|
||||
padding: 8px 18px;
|
||||
font: 12px/1.6 var(--story-mono);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.story-h {
|
||||
margin: 16px 18px 0;
|
||||
font: 17px/1.1 var(--story-letter);
|
||||
}
|
||||
.story-todos {
|
||||
margin: 8px 0 0;
|
||||
padding: 0 18px 0 40px;
|
||||
font-size: 14.5px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.story-todos .is-done {
|
||||
color: var(--story-go);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.story-fresh *,
|
||||
.story-arrive,
|
||||
.story-live i {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -13257,6 +13257,15 @@ kbd {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* "Story Mode" header toggle (story-mode.js, App Settings → Header buttons), opt-in. Same marker
|
||||
pattern as the Response Viewer; mobile.css keeps it off phones. */
|
||||
.btn-story-header {
|
||||
display: inline-flex !important;
|
||||
}
|
||||
.btn-story-header.btn-story-header--hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* "Approvals" header bell: appears ONLY while prompts are pending (JS toggles
|
||||
the marker class on count changes), so it ships hidden and stays out of the
|
||||
default header. Same marker pattern as the attachments button. */
|
||||
|
||||
@@ -15,6 +15,7 @@ export { registerApprovalRoutes } from './approval-routes.js';
|
||||
export { registerRebootRestoreRoutes } from './reboot-restore-routes.js';
|
||||
export { registerReadMyMindRoutes } from './readmymind-routes.js';
|
||||
export { registerGitStatusRoutes } from './git-status-routes.js';
|
||||
export { registerStoryRoutes } from './story-routes.js';
|
||||
export { registerStatusTelemetryRoutes } from './status-telemetry-routes.js';
|
||||
export { registerCaseRoutes } from './case-routes.js';
|
||||
export { registerWorktreeRoutes } from './worktree-routes.js';
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @fileoverview `GET /api/sessions/:id/story`: one turn of a Claude Code session as a comic page
|
||||
* (Story Mode, `story-mode.js` in the browser). EXPERIMENTAL: the payload shape is not frozen.
|
||||
*
|
||||
* Query: `turn=<id>` picks a turn from the `turns` list; without it the newest turn is drawn.
|
||||
* Answers `{ supported: false, reason }` for a session it cannot draw, decided from the CLI's
|
||||
* registry capability (`capabilities.transcript === 'claude-jsonl'`, never the mode id) and from
|
||||
* where the transcript lives: a remote or Docker session's transcript is not on this host.
|
||||
*
|
||||
* Read-only: unlike `last-response` it never adopts a conversation id or persists anything. The file
|
||||
* read is the session's own transcript, chosen here and never named by the request: the path the
|
||||
* Claude hook reported for this session, else `<conversation id>.jsonl` under the projects directory.
|
||||
* Reading is bounded and async (story-reader.ts); what comes back is untrusted text the browser
|
||||
* renders with `textContent`, and file paths in it open through the existing file preview.
|
||||
*/
|
||||
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { ApiErrorCode, createErrorResponse, type ApiResponse, type StoryResponse } from '../../types.js';
|
||||
import { getCli } from '../../config/cli-registry/registry.js';
|
||||
import { buildStoryPage, buildStoryTurns, summarizeTurns } from '../../story-mode.js';
|
||||
import { readStoryEntries } from '../../story-reader.js';
|
||||
import { findClaudeTranscriptPath } from '../../utils/claude-transcript.js';
|
||||
import { findSessionOrFail } from '../route-helpers.js';
|
||||
import type { ConfigPort, SessionPort } from '../ports/index.js';
|
||||
|
||||
/** The hook-reported path when it looks like a transcript, else a lookup by conversation id. */
|
||||
async function resolveTranscriptPath(hookPath: string | null, conversationId: string): Promise<string | null> {
|
||||
if (hookPath && hookPath.startsWith('/') && hookPath.endsWith('.jsonl')) return hookPath;
|
||||
return findClaudeTranscriptPath(conversationId);
|
||||
}
|
||||
|
||||
export function registerStoryRoutes(app: FastifyInstance, ctx: SessionPort & ConfigPort): void {
|
||||
app.get('/api/sessions/:id/story', async (req, reply): Promise<ApiResponse<StoryResponse>> => {
|
||||
const { id } = req.params as { id: string };
|
||||
const { turn } = req.query as { turn?: unknown };
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
|
||||
if (getCli(session.mode)?.capabilities.transcript !== 'claude-jsonl') {
|
||||
return {
|
||||
success: true,
|
||||
data: { supported: false, reason: 'Story Mode draws Claude Code sessions. This session runs a different CLI.' },
|
||||
};
|
||||
}
|
||||
if (session.remote || session.docker) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
supported: false,
|
||||
reason:
|
||||
'Story Mode reads transcripts on this machine. This session keeps its transcript on another host or inside a container.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const transcriptPath = await resolveTranscriptPath(
|
||||
ctx.getTranscriptPath(id),
|
||||
session.claudeSessionId || session.id
|
||||
);
|
||||
if (!transcriptPath) return { success: true, data: { supported: true, turns: [], page: null } };
|
||||
|
||||
let entries;
|
||||
try {
|
||||
({ entries } = await readStoryEntries(transcriptPath));
|
||||
} catch {
|
||||
reply.code(500);
|
||||
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Could not read the session transcript');
|
||||
}
|
||||
|
||||
const turns = buildStoryTurns(entries);
|
||||
const wanted = typeof turn === 'string' && turn ? turns.find((t) => t.id === turn) : turns[turns.length - 1];
|
||||
return {
|
||||
success: true,
|
||||
data: { supported: true, turns: summarizeTurns(turns), page: wanted ? buildStoryPage(wanted) : null },
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1584,6 +1584,7 @@ export const SettingsUpdateSchema = z
|
||||
showCost: z.boolean().optional(),
|
||||
showLifecycleLog: z.boolean().optional(),
|
||||
showResponseViewer: z.boolean().optional(),
|
||||
showStoryButton: z.boolean().optional(),
|
||||
showMonitor: z.boolean().optional(),
|
||||
showProjectInsights: z.boolean().optional(),
|
||||
showFileBrowser: z.boolean().optional(),
|
||||
|
||||
@@ -189,6 +189,7 @@ import {
|
||||
registerRebootRestoreRoutes,
|
||||
registerReadMyMindRoutes,
|
||||
registerGitStatusRoutes,
|
||||
registerStoryRoutes,
|
||||
registerStatusTelemetryRoutes,
|
||||
registerSystemRoutes,
|
||||
registerCaseRoutes,
|
||||
@@ -1152,6 +1153,7 @@ export class WebServer extends EventEmitter {
|
||||
registerRebootRestoreRoutes(this.app, ctx);
|
||||
registerReadMyMindRoutes(this.app, ctx);
|
||||
registerGitStatusRoutes(this.app, ctx);
|
||||
registerStoryRoutes(this.app, ctx);
|
||||
registerStatusTelemetryRoutes(this.app, ctx);
|
||||
registerSystemRoutes(this.app, ctx);
|
||||
registerCaseRoutes(this.app, ctx);
|
||||
|
||||
Reference in new issue
Block a user