aicodeman 1.41.0 → 1.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -8
- package/dist/attachment-registry.d.ts +2 -0
- package/dist/attachment-registry.d.ts.map +1 -1
- package/dist/attachment-registry.js +14 -3
- package/dist/attachment-registry.js.map +1 -1
- package/dist/cli-agent.d.ts +2 -0
- package/dist/cli-agent.d.ts.map +1 -1
- package/dist/cli-agent.js +12 -8
- package/dist/cli-agent.js.map +1 -1
- package/dist/cli.d.ts +25 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +97 -11
- package/dist/cli.js.map +1 -1
- package/dist/config/buffer-limits.d.ts +9 -0
- package/dist/config/buffer-limits.d.ts.map +1 -1
- package/dist/config/buffer-limits.js +9 -0
- package/dist/config/buffer-limits.js.map +1 -1
- package/dist/config/cli-registry/schema.d.ts +8 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +12 -0
- package/dist/config/cli-registry/schema.js.map +1 -1
- package/dist/config/cli-registry/stock.d.ts.map +1 -1
- package/dist/config/cli-registry/stock.js +42 -0
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +35 -0
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/folder-listing.d.ts +94 -0
- package/dist/folder-listing.d.ts.map +1 -0
- package/dist/folder-listing.js +241 -0
- package/dist/folder-listing.js.map +1 -0
- package/dist/hooks-config.d.ts +36 -16
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +45 -20
- package/dist/hooks-config.js.map +1 -1
- package/dist/html-view-capabilities.d.ts +54 -0
- package/dist/html-view-capabilities.d.ts.map +1 -0
- package/dist/html-view-capabilities.js +103 -0
- package/dist/html-view-capabilities.js.map +1 -0
- package/dist/mcp-server.d.ts +114 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +917 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/mcp-sync.d.ts +5 -0
- package/dist/mcp-sync.d.ts.map +1 -1
- package/dist/mcp-sync.js +51 -5
- package/dist/mcp-sync.js.map +1 -1
- package/dist/mux-interface.d.ts +36 -1
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/remote-files.d.ts +37 -0
- package/dist/remote-files.d.ts.map +1 -1
- package/dist/remote-files.js +103 -0
- package/dist/remote-files.js.map +1 -1
- package/dist/remote-pane-history.d.ts +103 -0
- package/dist/remote-pane-history.d.ts.map +1 -0
- package/dist/remote-pane-history.js +258 -0
- package/dist/remote-pane-history.js.map +1 -0
- package/dist/remote-ssh-limiter.d.ts +16 -3
- package/dist/remote-ssh-limiter.d.ts.map +1 -1
- package/dist/remote-ssh-limiter.js +37 -6
- package/dist/remote-ssh-limiter.js.map +1 -1
- package/dist/session.d.ts +29 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +53 -0
- package/dist/session.js.map +1 -1
- package/dist/tab-layout-legacy-order.d.ts.map +1 -1
- package/dist/tab-layout-legacy-order.js +72 -13
- package/dist/tab-layout-legacy-order.js.map +1 -1
- package/dist/tab-layout-service.d.ts +33 -2
- package/dist/tab-layout-service.d.ts.map +1 -1
- package/dist/tab-layout-service.js +102 -7
- package/dist/tab-layout-service.js.map +1 -1
- package/dist/tab-layout.d.ts +9 -0
- package/dist/tab-layout.d.ts.map +1 -1
- package/dist/tab-layout.js +15 -2
- package/dist/tab-layout.js.map +1 -1
- package/dist/tmux-manager.d.ts +71 -2
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +169 -18
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/session.d.ts +53 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/usage-telemetry.d.ts +44 -7
- package/dist/usage-telemetry.d.ts.map +1 -1
- package/dist/usage-telemetry.js +72 -7
- package/dist/usage-telemetry.js.map +1 -1
- package/dist/web/approval-inbox.d.ts +12 -0
- package/dist/web/approval-inbox.d.ts.map +1 -1
- package/dist/web/approval-inbox.js +1 -0
- package/dist/web/approval-inbox.js.map +1 -1
- package/dist/web/middleware/auth.d.ts.map +1 -1
- package/dist/web/middleware/auth.js +33 -0
- package/dist/web/middleware/auth.js.map +1 -1
- package/dist/web/ports/session-port.d.ts +8 -1
- package/dist/web/ports/session-port.d.ts.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.24689381.js +43 -0
- package/dist/web/public/app.24689381.js.br +0 -0
- package/dist/web/public/app.24689381.js.gz +0 -0
- package/dist/web/public/approvals-ui.js +4 -0
- package/dist/web/public/approvals-ui.js.br +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.bc9c1f63.js → constants.17d77b65.js} +395 -7
- package/dist/web/public/constants.17d77b65.js.br +0 -0
- package/dist/web/public/constants.17d77b65.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/gesture/gesture-codeman.js +112 -6
- package/dist/web/public/git-status-ui.js.gz +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/host-wake-ui.js.gz +0 -0
- package/dist/web/public/i18n.9afd8e4d.js +2 -0
- package/dist/web/public/i18n.9afd8e4d.js.br +0 -0
- package/dist/web/public/i18n.9afd8e4d.js.gz +0 -0
- package/dist/web/public/{image-input.e00471c6.js → image-input.9ed1d0d2.js} +186 -16
- package/dist/web/public/image-input.9ed1d0d2.js.br +0 -0
- package/dist/web/public/image-input.9ed1d0d2.js.gz +0 -0
- package/dist/web/public/index.html +105 -23
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
- package/dist/web/public/{keyboard-accessory.c1e7c7b7.js → keyboard-accessory.c458eea8.js} +115 -35
- package/dist/web/public/keyboard-accessory.c458eea8.js.br +0 -0
- package/dist/web/public/keyboard-accessory.c458eea8.js.gz +0 -0
- package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js +11 -2
- package/dist/web/public/mobile-overview.js.br +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.2dff1580.css +1 -0
- package/dist/web/public/mobile.2dff1580.css.br +0 -0
- package/dist/web/public/mobile.2dff1580.css.gz +0 -0
- package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.71846fcb.js → panels-ui.73ec0137.js} +87 -87
- package/dist/web/public/panels-ui.73ec0137.js.br +0 -0
- package/dist/web/public/panels-ui.73ec0137.js.gz +0 -0
- package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
- package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/reboot-restore-ui.js.gz +0 -0
- package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/{session-ui.f731602c.js → session-ui.dbc6819b.js} +33 -33
- package/dist/web/public/session-ui.dbc6819b.js.br +0 -0
- package/dist/web/public/session-ui.dbc6819b.js.gz +0 -0
- package/dist/web/public/{settings-ui.4e33390f.js → settings-ui.81ac264e.js} +9 -9
- package/dist/web/public/settings-ui.81ac264e.js.br +0 -0
- package/dist/web/public/settings-ui.81ac264e.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js +11 -0
- package/dist/web/public/spreadsheet-preview-worker.js.br +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
- package/dist/web/public/{spreadsheet-preview.af82fa2f.js → spreadsheet-preview.75c751de.js} +1 -1
- package/dist/web/public/spreadsheet-preview.75c751de.js.br +0 -0
- package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js +110 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.br +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
- package/dist/web/public/styles.900a28e4.css +1 -0
- package/dist/web/public/styles.900a28e4.css.br +0 -0
- package/dist/web/public/styles.900a28e4.css.gz +0 -0
- package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
- package/dist/web/public/sw.js +6 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js +1 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js.br +0 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
- package/dist/web/public/{tab-rail-resize.a5867793.js → tab-rail-resize.7157e0bd.js} +1 -1
- package/dist/web/public/tab-rail-resize.7157e0bd.js.br +0 -0
- package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +44 -24
- package/dist/web/public/terminal-tile.js.br +0 -0
- package/dist/web/public/terminal-tile.js.gz +0 -0
- package/dist/web/public/terminal-ui.858f5da5.js +2 -0
- package/dist/web/public/terminal-ui.858f5da5.js.br +0 -0
- package/dist/web/public/terminal-ui.858f5da5.js.gz +0 -0
- package/dist/web/public/tile-grid.js +439 -18
- package/dist/web/public/tile-grid.js.br +0 -0
- package/dist/web/public/tile-grid.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
- package/dist/web/public/vendor/fflate.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js +12 -0
- package/dist/web/public/webview-tabs.js.br +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/route-helpers.d.ts +8 -0
- package/dist/web/route-helpers.d.ts.map +1 -1
- package/dist/web/route-helpers.js +8 -0
- package/dist/web/route-helpers.js.map +1 -1
- package/dist/web/routes/admin-routes.d.ts.map +1 -1
- package/dist/web/routes/admin-routes.js +4 -1
- package/dist/web/routes/admin-routes.js.map +1 -1
- package/dist/web/routes/approval-routes.d.ts.map +1 -1
- package/dist/web/routes/approval-routes.js +6 -8
- package/dist/web/routes/approval-routes.js.map +1 -1
- package/dist/web/routes/case-routes.js +1 -1
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/file-routes.d.ts +46 -1
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +214 -27
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
- package/dist/web/routes/hook-event-routes.js +8 -1
- package/dist/web/routes/hook-event-routes.js.map +1 -1
- package/dist/web/routes/html-view-routes.d.ts +60 -0
- package/dist/web/routes/html-view-routes.d.ts.map +1 -0
- package/dist/web/routes/html-view-routes.js +288 -0
- package/dist/web/routes/html-view-routes.js.map +1 -0
- package/dist/web/routes/index.d.ts +1 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +1 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/reboot-restore-routes.js +2 -2
- package/dist/web/routes/reboot-restore-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +1 -0
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +278 -56
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
- package/dist/web/routes/status-telemetry-routes.js +16 -2
- package/dist/web/routes/status-telemetry-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +29 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +65 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +6 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +62 -14
- package/dist/web/server.js.map +1 -1
- package/dist/web/sse-events.d.ts +5 -2
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +5 -2
- package/dist/web/sse-events.js.map +1 -1
- package/package.json +2 -1
- package/skills/codeman/SKILL.md +29 -12
- package/dist/web/public/app.6f8552e1.js +0 -43
- package/dist/web/public/app.6f8552e1.js.br +0 -0
- package/dist/web/public/app.6f8552e1.js.gz +0 -0
- package/dist/web/public/constants.bc9c1f63.js.br +0 -0
- package/dist/web/public/constants.bc9c1f63.js.gz +0 -0
- package/dist/web/public/i18n.a9840ea2.js +0 -2
- package/dist/web/public/i18n.a9840ea2.js.br +0 -0
- package/dist/web/public/i18n.a9840ea2.js.gz +0 -0
- package/dist/web/public/image-input.e00471c6.js.br +0 -0
- package/dist/web/public/image-input.e00471c6.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.c1e7c7b7.js.br +0 -0
- package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
- package/dist/web/public/mobile.111b187d.css +0 -1
- package/dist/web/public/mobile.111b187d.css.br +0 -0
- package/dist/web/public/mobile.111b187d.css.gz +0 -0
- package/dist/web/public/panels-ui.71846fcb.js.br +0 -0
- package/dist/web/public/panels-ui.71846fcb.js.gz +0 -0
- package/dist/web/public/session-ui.f731602c.js.br +0 -0
- package/dist/web/public/session-ui.f731602c.js.gz +0 -0
- package/dist/web/public/settings-ui.4e33390f.js.br +0 -0
- package/dist/web/public/settings-ui.4e33390f.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview.af82fa2f.js.br +0 -0
- package/dist/web/public/spreadsheet-preview.af82fa2f.js.gz +0 -0
- package/dist/web/public/styles.81a72906.css +0 -1
- package/dist/web/public/styles.81a72906.css.br +0 -0
- package/dist/web/public/styles.81a72906.css.gz +0 -0
- package/dist/web/public/tab-layout-browser.065fcba4.js +0 -1
- package/dist/web/public/tab-layout-browser.065fcba4.js.br +0 -0
- package/dist/web/public/tab-layout-browser.065fcba4.js.gz +0 -0
- package/dist/web/public/tab-rail-resize.a5867793.js.br +0 -0
- package/dist/web/public/tab-rail-resize.a5867793.js.gz +0 -0
- package/dist/web/public/terminal-ui.b7267005.js +0 -2
- package/dist/web/public/terminal-ui.b7267005.js.br +0 -0
- package/dist/web/public/terminal-ui.b7267005.js.gz +0 -0
|
@@ -116,6 +116,21 @@ const MAX_NOTIFICATION_DURATION_MS = 300000; // Longest configurable toast / bro
|
|
|
116
116
|
const THROTTLE_DELAY_MS = 100; // General UI throttle delay
|
|
117
117
|
const TERMINAL_CHUNK_SIZE = 32 * 1024; // 32KB chunks for terminal buffer loading
|
|
118
118
|
const TERMINAL_TAIL_SIZE = 1024 * 1024; // 1MB tail for initial load (more scrollback on tab switch)
|
|
119
|
+
// A shell's automatic loads (tab select, drop recovery, a tile's load) read
|
|
120
|
+
// tmux's RENDERED history, never the raw byte recording: that recording holds
|
|
121
|
+
// full-screen repaints (an ssh'd tmux, less, a TUI) drawn for the pane size of
|
|
122
|
+
// the moment, and replayed into a terminal of another height each repaint
|
|
123
|
+
// scrolls its top row into scrollback, so one line shows up dozens of times.
|
|
124
|
+
// `lines=` bounds the synchronous capture-pane (~8 ms per 1000 lines measured,
|
|
125
|
+
// so ~0.1 s here); the server reports a cut as 'tail', and scroll-to-top pulls
|
|
126
|
+
// the older lines as before. `alt=recording`: a pane in the alternate screen (a
|
|
127
|
+
// nested tmux, as in remote SSH and Docker cases, but also vim or less while
|
|
128
|
+
// open) gains no tmux history, so the server loads it the old way, visible
|
|
129
|
+
// frame over the tailed byte recording.
|
|
130
|
+
// A grid tile appends its own `lines=` to SHELL_LOAD_BASE_QUERY.
|
|
131
|
+
const SHELL_LOAD_HISTORY_LINES = 10000;
|
|
132
|
+
const SHELL_LOAD_BASE_QUERY = `full=1&tail=${TERMINAL_TAIL_SIZE}&alt=recording`;
|
|
133
|
+
const SHELL_LOAD_QUERY = `${SHELL_LOAD_BASE_QUERY}&lines=${SHELL_LOAD_HISTORY_LINES}`;
|
|
119
134
|
const SYNC_WAIT_TIMEOUT_MS = 50; // Wait timeout for terminal sync
|
|
120
135
|
const STATS_POLLING_INTERVAL_MS = 2000; // System stats polling
|
|
121
136
|
const TUI_REDRAW_SETTLE_MS = 400; // Grace for a TUI to redraw after a real resize, before fetching its buffer
|
|
@@ -709,6 +724,45 @@ function computeConnectionLossUi(input) {
|
|
|
709
724
|
};
|
|
710
725
|
}
|
|
711
726
|
|
|
727
|
+
// Session Options → Respawn → "Prompt cache": one sentence of the main
|
|
728
|
+
// conversation's cache state as Claude Code reports it on the statusline
|
|
729
|
+
// (`session.promptCache`, v2.1.251+). A clock time rather than a countdown
|
|
730
|
+
// because the row refreshes on SSE, not on a timer, and a countdown would sit
|
|
731
|
+
// stale between turns. Pure: no DOM, `now` is passed in. `statusLine` is the
|
|
732
|
+
// session's `statusLineExporter`: a launch without the exporter never reports, so
|
|
733
|
+
// it says why instead of "not reported yet" forever.
|
|
734
|
+
const PROMPT_CACHE_NOT_COLLECTED = {
|
|
735
|
+
'collection-off':
|
|
736
|
+
'Not collected: Plan Usage was off when this session started. Turn it on in App Settings; sessions started after that report it.',
|
|
737
|
+
'workspace-statusline':
|
|
738
|
+
"Not collected: this workspace's .claude/settings.local.json sets its own statusLine, which Codeman never replaces.",
|
|
739
|
+
remote: 'Not collected for remote and Docker sessions.',
|
|
740
|
+
};
|
|
741
|
+
function formatPromptCacheStatus(cache, now, statusLine) {
|
|
742
|
+
if (PROMPT_CACHE_NOT_COLLECTED[statusLine]) return PROMPT_CACHE_NOT_COLLECTED[statusLine];
|
|
743
|
+
if (!cache || typeof cache.warm !== 'boolean') {
|
|
744
|
+
return 'Not reported yet. Claude Code 2.1.251 or newer reports it after the first response.';
|
|
745
|
+
}
|
|
746
|
+
const clock = (ms) => new Date(ms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
747
|
+
const kTokens = (n) => (n >= 1000 ? `${Math.round(n / 1000)}k` : `${n}`);
|
|
748
|
+
const stake = cache.recacheTokensIfCold;
|
|
749
|
+
const tokens = Number.isFinite(stake) && stake > 0 ? kTokens(stake) : null;
|
|
750
|
+
const parts = [];
|
|
751
|
+
if (cache.warm && Number.isFinite(cache.expiresAt) && cache.expiresAt > now) {
|
|
752
|
+
const ttl = cache.ttl ? ` (${cache.ttl} cache)` : '';
|
|
753
|
+
parts.push(`Warm until ${clock(cache.expiresAt)}${ttl}.`);
|
|
754
|
+
if (tokens) parts.push(`About ${tokens} tokens would be re-cached if it goes cold.`);
|
|
755
|
+
} else {
|
|
756
|
+
parts.push(tokens ? `Cold. The next turn re-caches about ${tokens} tokens.` : 'Cold.');
|
|
757
|
+
}
|
|
758
|
+
// A compaction or a tool-result clearing leaves no re-cache stake to show until the next
|
|
759
|
+
// request; naming when it happened explains the gap. Never an alert.
|
|
760
|
+
if (Number.isFinite(cache.compactedAt)) parts.push(`Context compacted at ${clock(cache.compactedAt)}.`);
|
|
761
|
+
if (Number.isFinite(cache.hitRatio)) parts.push(`${Math.round(cache.hitRatio * 100)}% cache hit rate.`);
|
|
762
|
+
if (cache.lastMissCauses && cache.lastMissCauses.length) parts.push(`Last miss: ${cache.lastMissCauses.join(', ')}.`);
|
|
763
|
+
return parts.join(' ');
|
|
764
|
+
}
|
|
765
|
+
|
|
712
766
|
// SSE staleness policy: is this stream a zombie?
|
|
713
767
|
//
|
|
714
768
|
// An EventSource that stops delivering does not always error. A proxy that
|
|
@@ -1447,6 +1501,9 @@ if (typeof window !== 'undefined') {
|
|
|
1447
1501
|
compute: computeSseStale,
|
|
1448
1502
|
TIMEOUT_MS: SSE_STALE_TIMEOUT_MS,
|
|
1449
1503
|
};
|
|
1504
|
+
window.CodemanPromptCache = {
|
|
1505
|
+
format: formatPromptCacheStatus,
|
|
1506
|
+
};
|
|
1450
1507
|
window.CodemanSessionOrder = {
|
|
1451
1508
|
RANK: SESSION_ACTIVITY_RANK,
|
|
1452
1509
|
anchor: sessionActivityAnchor,
|
|
@@ -1507,6 +1564,15 @@ function scheduleBackground(fn) {
|
|
|
1507
1564
|
// but server-sent terminal buffers may still contain markers from Claude CLI.
|
|
1508
1565
|
const DEC_SYNC_STRIP_RE = /\x1b\[\?2026[hl]/g;
|
|
1509
1566
|
|
|
1567
|
+
// OSC 52 clipboard writes stripping — a replay re-feeds the raw byte history, and
|
|
1568
|
+
// xterm parses it after chunkedTerminalWrite has already released the load
|
|
1569
|
+
// gate, so an OSC 52 left in would re-copy old text on every tab switch. The
|
|
1570
|
+
// 8-bit C1 forms (OSC \x9d, ST \x9c) too: xterm.js still parses those.
|
|
1571
|
+
const OSC52_STRIP_RE = /(?:\x1b\]|\x9d)52;[^\x07\x1b\x9c]*(?:\x07|\x1b\\|\x9c)/g;
|
|
1572
|
+
// The same OSC 52 text arriving again within this window is one copy delivered
|
|
1573
|
+
// several ways (direct, tmux load-buffer -w, tmux set-clipboard forwarding).
|
|
1574
|
+
const OSC52_DEDUPE_MS = 1000;
|
|
1575
|
+
|
|
1510
1576
|
// Built-in respawn configuration presets
|
|
1511
1577
|
const BUILTIN_RESPAWN_PRESETS = [
|
|
1512
1578
|
{
|
|
@@ -1632,6 +1698,7 @@ const SSE_EVENTS = {
|
|
|
1632
1698
|
SESSION_INTERACTIVE: 'session:interactive',
|
|
1633
1699
|
SESSION_RUNNING: 'session:running',
|
|
1634
1700
|
SESSION_STATUS_TELEMETRY: 'session:statusTelemetry',
|
|
1701
|
+
SESSION_PROMPT_CACHE: 'session:promptCache',
|
|
1635
1702
|
|
|
1636
1703
|
// Scheduled runs
|
|
1637
1704
|
SCHEDULED_CREATED: 'scheduled:created',
|
|
@@ -1860,7 +1927,10 @@ function escapeHtml(text) {
|
|
|
1860
1927
|
function formatHistoryBytes(bytes) {
|
|
1861
1928
|
const n = typeof bytes === 'number' && isFinite(bytes) && bytes > 0 ? bytes : 0;
|
|
1862
1929
|
if (n < 1024) return 'less than 1 KB';
|
|
1863
|
-
|
|
1930
|
+
// Switch on the ROUNDED value: a 1 MiB tail cut back to a line boundary is
|
|
1931
|
+
// just under 1 MiB and used to print as "1024 KB".
|
|
1932
|
+
const kb = Math.round(n / 1024);
|
|
1933
|
+
if (kb < 1024) return `${kb} KB`;
|
|
1864
1934
|
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
1865
1935
|
}
|
|
1866
1936
|
|
|
@@ -1874,12 +1944,27 @@ function formatHistoryBytes(bytes) {
|
|
|
1874
1944
|
* - atCeiling: the FULL capture itself hit the byte ceiling
|
|
1875
1945
|
* - exhausted: a full pull was refused as a downgrade, so this is all there is
|
|
1876
1946
|
*
|
|
1947
|
+
* ⚠️ `truncated` measures the server's BYTE stream, not history a pull can
|
|
1948
|
+
* return. `paneHistoryLines` (tmux `#{history_size}`) is the latter: 0 means the
|
|
1949
|
+
* pane keeps no scrollback at all (a fullscreen CLI in the alternate screen,
|
|
1950
|
+
* whose transcript lives in the CLI and is scrolled there), so the bytes a tail
|
|
1951
|
+
* cut dropped are old repaint frames that `full=1` cannot bring back. Measured
|
|
1952
|
+
* on live fullscreen claude panes: "4.8 MB more" was ~33 copies of one frame,
|
|
1953
|
+
* and the button only ever ended in the downgrade refusal. Nothing to offer,
|
|
1954
|
+
* nothing to say. Absent means unknown and keeps the byte-based behaviour.
|
|
1955
|
+
*
|
|
1877
1956
|
* @param {{truncated?: boolean, reason?: string|null, source?: string|null,
|
|
1878
|
-
* fullSize?: number, retainedBytes?: number, exhausted?: boolean
|
|
1957
|
+
* fullSize?: number, retainedBytes?: number, exhausted?: boolean,
|
|
1958
|
+
* paneHistoryLines?: number|null, historyLinesCut?: boolean}} state
|
|
1879
1959
|
* @returns {{visible: boolean, message: string, canLoadMore: boolean}}
|
|
1880
1960
|
*/
|
|
1881
1961
|
function computeHistoryTruncationNotice(state = {}) {
|
|
1882
1962
|
if (!state.truncated) return { visible: false, message: '', canLoadMore: false };
|
|
1963
|
+
const historyLines =
|
|
1964
|
+
typeof state.paneHistoryLines === 'number' && Number.isFinite(state.paneHistoryLines)
|
|
1965
|
+
? Math.max(0, state.paneHistoryLines)
|
|
1966
|
+
: null;
|
|
1967
|
+
if (historyLines === 0) return { visible: false, message: '', canLoadMore: false };
|
|
1883
1968
|
|
|
1884
1969
|
const retained = Math.max(0, state.retainedBytes || 0);
|
|
1885
1970
|
const dropped = Math.max(0, (state.fullSize || 0) - retained);
|
|
@@ -1902,9 +1987,22 @@ function computeHistoryTruncationNotice(state = {}) {
|
|
|
1902
1987
|
canLoadMore: false,
|
|
1903
1988
|
};
|
|
1904
1989
|
}
|
|
1990
|
+
// Name what a pull can actually return when the server said: the byte gap
|
|
1991
|
+
// counts repaints and redraw bloat, and overstates it many times over.
|
|
1992
|
+
// Without that count, a `lines=`-bounded capture is cut by tmux before any
|
|
1993
|
+
// bytes are counted, so its fullSize is the window itself and the remainder's
|
|
1994
|
+
// size is unknown: say that more exists without inventing an amount. That
|
|
1995
|
+
// holds even when `tail` also cut bytes off the window (`dropped`), since tmux
|
|
1996
|
+
// still holds more beyond it (`historyLinesCut`, set by the server).
|
|
1997
|
+
const remainder =
|
|
1998
|
+
dropped > 0 && !state.historyLinesCut ? `${formatHistoryBytes(dropped)} more` : 'Earlier output';
|
|
1999
|
+
const more =
|
|
2000
|
+
historyLines !== null
|
|
2001
|
+
? `${historyLines.toLocaleString('en-US')} ${historyLines === 1 ? 'line of scrollback is' : 'lines of scrollback are'} retained.`
|
|
2002
|
+
: `${remainder} may still be retained.`;
|
|
1905
2003
|
return {
|
|
1906
2004
|
visible: true,
|
|
1907
|
-
message: `Showing the most recent ${shown} of this session. ${
|
|
2005
|
+
message: `Showing the most recent ${shown} of this session. ${more}`,
|
|
1908
2006
|
canLoadMore: true,
|
|
1909
2007
|
};
|
|
1910
2008
|
}
|
|
@@ -1949,7 +2047,7 @@ function computeRewriteScrollLine(input) {
|
|
|
1949
2047
|
* a `/g` regex, so {@link absoluteFilePathPattern} mints a fresh one per call.
|
|
1950
2048
|
*/
|
|
1951
2049
|
const FILE_PATH_LINK_PATTERN =
|
|
1952
|
-
/(\/(?:home|Users|tmp|var|private|opt|mnt|srv|media|data|workspace)\/[^\s"'<>|;&\n\x00-\x1f]*\.(?:log|txt|json|md|ya?ml|csv|xml|sh|py|tsx|ts|jsx|js|mjs|cjs|css|html
|
|
2050
|
+
/(\/(?:home|Users|tmp|var|private|opt|mnt|srv|media|data|workspace)\/[^\s"'<>|;&\n\x00-\x1f]*\.(?:log|txt|json|markdown|md|ya?ml|csv|xml|sh|py|tsx|ts|jsx|js|mjs|cjs|css|html?|toml|ini|sql|png|jpe?g|gif|webp|avif|bmp|ico|svg|pdf|docx|pptx|xlsx|mp4|webm|mov|m4v|ogv|mp3|wav))\b/g;
|
|
1953
2051
|
|
|
1954
2052
|
/** A fresh, zero-state instance of {@link FILE_PATH_LINK_PATTERN}. */
|
|
1955
2053
|
function absoluteFilePathPattern() {
|
|
@@ -1961,13 +2059,18 @@ function absoluteFilePathPattern() {
|
|
|
1961
2059
|
* points at goes to the tail/log viewer, which is the right home for a growing
|
|
1962
2060
|
* text file and the wrong one for bytes (tailing a PNG shows binary noise).
|
|
1963
2061
|
*
|
|
2062
|
+
* Markdown is here too (owner decision 2026-10-13): an agent's `.md` is a
|
|
2063
|
+
* document to read, so the preview renders it (its MD pill gives the source)
|
|
2064
|
+
* instead of the log viewer streaming it as raw text. Other text (.log, .txt,
|
|
2065
|
+
* .json, code) keeps the log viewer, which follows a file still being written.
|
|
2066
|
+
*
|
|
1964
2067
|
* The media entries mirror VIDEO_ATTACHMENT_EXTENSIONS/AUDIO_ATTACHMENT_EXTENSIONS
|
|
1965
2068
|
* (src/attachment-registry.ts, the single source) — they diverged once and an
|
|
1966
2069
|
* in-workspace `.m4a` opened as binary noise in the log viewer while the same
|
|
1967
2070
|
* file in /tmp played fine. test/media-extension-parity.test.ts pins the sync.
|
|
1968
2071
|
*/
|
|
1969
2072
|
const FILE_PREVIEW_EXTENSIONS = new Set(
|
|
1970
|
-
('png jpg jpeg gif webp avif bmp ico svg pdf docx pptx xlsx mp4 webm mov m4v ogv mp3 wav ogg oga m4a aac flac opus').split(' ')
|
|
2073
|
+
('png jpg jpeg gif webp avif bmp ico svg pdf docx pptx xlsx mp4 webm mov m4v ogv mp3 wav ogg oga m4a aac flac opus md markdown').split(' ')
|
|
1971
2074
|
);
|
|
1972
2075
|
|
|
1973
2076
|
/** Whether a path's extension is one {@link FILE_PREVIEW_EXTENSIONS} covers. */
|
|
@@ -1976,6 +2079,155 @@ function previewsInFileViewer(filePath) {
|
|
|
1976
2079
|
return FILE_PREVIEW_EXTENSIONS.has(ext);
|
|
1977
2080
|
}
|
|
1978
2081
|
|
|
2082
|
+
/**
|
|
2083
|
+
* Home-relative and relative file paths: `~/repos/x/garage.png` (how Claude Code
|
|
2084
|
+
* echoes an attached image), `builds/captures/shot.png`, `./a.md`, `../b.json`,
|
|
2085
|
+
* or a bare `look_montage.png`. Same extension list and body as
|
|
2086
|
+
* {@link FILE_PATH_LINK_PATTERN} (test/link-provider-regex.test.ts pins the two
|
|
2087
|
+
* equal).
|
|
2088
|
+
*
|
|
2089
|
+
* Without a known root to anchor on, the START is what has to be unambiguous:
|
|
2090
|
+
* group 1 is the boundary in front of the path (line start, whitespace, a quote,
|
|
2091
|
+
* an opening bracket, `=`, `,`, `>`), so a match can never begin in the middle of
|
|
2092
|
+
* a word, of an absolute path (`/nix/store/x.png` is not `nix/store/x.png`) or of
|
|
2093
|
+
* a URL. The path itself is group 2. Its first character is never `/`: absolute
|
|
2094
|
+
* paths belong to the rooted pattern, which deliberately leaves `/etc` out. A
|
|
2095
|
+
* token containing `://` is a URL, never a path. Brackets, `=` and `,` end a
|
|
2096
|
+
* relative path (unlike a rooted one): `(garage.png)` and `OUT=shots/a.png` link
|
|
2097
|
+
* the file, not the punctuation around it.
|
|
2098
|
+
*
|
|
2099
|
+
* ⚠ Like its sibling, never share an instance (`lastIndex`): use
|
|
2100
|
+
* {@link findFilePathLinks}.
|
|
2101
|
+
*/
|
|
2102
|
+
const RELATIVE_FILE_PATH_LINK_PATTERN =
|
|
2103
|
+
/(^|[\s"'`(\[{<>=,])((?![^\s"'`<>|;&()[\]{}=,\n\x00-\x1f]*:\/\/)(?:~\/|[^\s"'`<>|;&()[\]{}=,\/~\n\x00-\x1f])[^\s"'`<>|;&()[\]{}=,\n\x00-\x1f]*\.(?:log|txt|json|markdown|md|ya?ml|csv|xml|sh|py|tsx|ts|jsx|js|mjs|cjs|css|html?|toml|ini|sql|png|jpe?g|gif|webp|avif|bmp|ico|svg|pdf|docx|pptx|xlsx|mp4|webm|mov|m4v|ogv|mp3|wav))\b/g;
|
|
2104
|
+
|
|
2105
|
+
/** The `(?:log|txt|…)` extension group, read off the rooted pattern so it cannot drift. */
|
|
2106
|
+
const FILE_LINK_EXTENSION_GROUP = /\\\.(\(\?:[^()]*\))\)\\b$/.exec(FILE_PATH_LINK_PATTERN.source)[1];
|
|
2107
|
+
|
|
2108
|
+
/**
|
|
2109
|
+
* Every linkable file path in `text`, in order: absolute (rooted) paths first,
|
|
2110
|
+
* then `~/` and relative ones, then folder paths — each only where it overlaps
|
|
2111
|
+
* neither an earlier one nor a URL.
|
|
2112
|
+
*
|
|
2113
|
+
* The one entry point for both consumers (terminal link provider, response
|
|
2114
|
+
* viewer), so a path form is clickable in both or in neither.
|
|
2115
|
+
*
|
|
2116
|
+
* @param {string} text
|
|
2117
|
+
* @returns {Array<{path: string, index: number, folder?: true}>} `index` is where `path` starts in
|
|
2118
|
+
* `text`; `folder` marks a folder path (folder grid) rather than a file.
|
|
2119
|
+
*/
|
|
2120
|
+
function findFilePathLinks(text) {
|
|
2121
|
+
const value = String(text || '');
|
|
2122
|
+
const found = [];
|
|
2123
|
+
const overlaps = (index, length) => found.some((f) => index < f.index + f.path.length && f.index < index + length);
|
|
2124
|
+
// Web URLs are never cut into a path: `https://x.io/a/home/b.png` and
|
|
2125
|
+
// `?img=b.png` belong to the URL link, not to a second, overlapping file link.
|
|
2126
|
+
// (`file:///home/x.png` is not one of them: its path is the file.)
|
|
2127
|
+
const urls = [];
|
|
2128
|
+
const urlPattern = /\bhttps?:\/\/\S+/gi;
|
|
2129
|
+
let match;
|
|
2130
|
+
while ((match = urlPattern.exec(value)) !== null) urls.push({ index: match.index, path: match[0] });
|
|
2131
|
+
const inUrl = (index, length) => urls.some((u) => index < u.index + u.path.length && u.index < index + length);
|
|
2132
|
+
|
|
2133
|
+
const absolute = absoluteFilePathPattern();
|
|
2134
|
+
while ((match = absolute.exec(value)) !== null) {
|
|
2135
|
+
if (!inUrl(match.index, match[1].length)) found.push({ path: match[1], index: match.index });
|
|
2136
|
+
}
|
|
2137
|
+
const relative = new RegExp(RELATIVE_FILE_PATH_LINK_PATTERN.source, 'g');
|
|
2138
|
+
while ((match = relative.exec(value)) !== null) {
|
|
2139
|
+
const path = match[2];
|
|
2140
|
+
const index = match.index + match[1].length;
|
|
2141
|
+
if (overlaps(index, path.length) || inUrl(index, path.length)) continue;
|
|
2142
|
+
found.push({ path, index });
|
|
2143
|
+
}
|
|
2144
|
+
const folders = new RegExp(FOLDER_PATH_LINK_PATTERN.source, 'g');
|
|
2145
|
+
while ((match = folders.exec(value)) !== null) {
|
|
2146
|
+
const path = folderLinkPath(match[2]);
|
|
2147
|
+
const index = match.index + match[1].length;
|
|
2148
|
+
if (!path || overlaps(index, path.length) || inUrl(index, path.length)) continue;
|
|
2149
|
+
found.push({ path, index, folder: true });
|
|
2150
|
+
}
|
|
2151
|
+
return found.sort((a, b) => a.index - b.index);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
/**
|
|
2155
|
+
* Folder paths, for the folder grid: rooted like {@link FILE_PATH_LINK_PATTERN}
|
|
2156
|
+
* (`/mnt/raid1/neon_getaway/styleframes`), `~/renders`, or relative with a
|
|
2157
|
+
* trailing slash (`builds/captures/`). Same boundary in front as the relative
|
|
2158
|
+
* file pattern (group 1), the candidate itself is group 2 and goes through
|
|
2159
|
+
* {@link folderLinkPath}, which decides whether it reads as a folder at all.
|
|
2160
|
+
* File links are found first and win any overlap. A folder path ends at `:`,
|
|
2161
|
+
* which is also a boundary in front: `PATH=/a/bin:/b/bin` is two candidates, and
|
|
2162
|
+
* the `://` lookahead stops at the next colon instead of rescanning the token from
|
|
2163
|
+
* every one (quadratic on `a:a:a:…//`).
|
|
2164
|
+
*
|
|
2165
|
+
* ⚠ Like its siblings, never share an instance (`lastIndex`): use
|
|
2166
|
+
* {@link findFilePathLinks}.
|
|
2167
|
+
*/
|
|
2168
|
+
const FOLDER_PATH_LINK_PATTERN =
|
|
2169
|
+
/(^|[\s"'`(\[{<>=,:])((?:\/(?:home|Users|tmp|var|private|opt|mnt|srv|media|data|workspace)\/|~\/|(?![^\s"'`<>|;&()[\]{}=,:\n\x00-\x1f]*:\/\/)[^\s"'`<>|;&()[\]{}=,:\/~\n\x00-\x1f])[^\s"'`<>|;&()[\]{}=,:\n\x00-\x1f]*)/g;
|
|
2170
|
+
|
|
2171
|
+
/**
|
|
2172
|
+
* A {@link FOLDER_PATH_LINK_PATTERN} candidate as a folder link, or null.
|
|
2173
|
+
*
|
|
2174
|
+
* Sentence punctuation after the path is not part of it. A path ending in `/` is
|
|
2175
|
+
* a folder; otherwise only a rooted or `~/` path whose last segment has no
|
|
2176
|
+
* extension (`.cache` and `styleframes` do, `a.rs` does not) — a relative word
|
|
2177
|
+
* without a slash at the end is prose, not a folder.
|
|
2178
|
+
*/
|
|
2179
|
+
function folderLinkPath(candidate) {
|
|
2180
|
+
// `$` and `#` too: a shell prompt (`user@host:~/proj$ `, `root@c:/app# `) ends
|
|
2181
|
+
// its cwd with one. A folder whose name really ends in one links one short.
|
|
2182
|
+
const path = String(candidate || '').replace(/[.,:;!?'"$#]+$/, '');
|
|
2183
|
+
if (!path || path === '~/' || /^\/[^/]+\/$/.test(path)) return null;
|
|
2184
|
+
if (path.endsWith('/')) return /[^/.]/.test(path) ? path : null;
|
|
2185
|
+
if (!path.startsWith('/') && !path.startsWith('~/')) return null;
|
|
2186
|
+
const last = path.slice(path.lastIndexOf('/') + 1);
|
|
2187
|
+
return /^\.?[^.]+$/.test(last) ? path : null;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
/**
|
|
2191
|
+
* Where a clicked path points, as the preview routes need it.
|
|
2192
|
+
*
|
|
2193
|
+
* Absolute and `~/` paths pass through unchanged — `~` is the home of the
|
|
2194
|
+
* session's HOST, which only the server knows (a remote case's home is on the
|
|
2195
|
+
* remote machine), so the server expands it. A relative path is resolved
|
|
2196
|
+
* against the session's working directory, `.` and `..` collapsed, so a
|
|
2197
|
+
* `../other/x.png` ends up outside the workspace and routes like any other
|
|
2198
|
+
* external path. Without a working directory it is returned as is.
|
|
2199
|
+
*
|
|
2200
|
+
* @param {string} filePath
|
|
2201
|
+
* @param {string} [workingDir]
|
|
2202
|
+
* @returns {string}
|
|
2203
|
+
*/
|
|
2204
|
+
function resolveLinkedFilePath(filePath, workingDir) {
|
|
2205
|
+
const path = String(filePath || '');
|
|
2206
|
+
if (!path || path.startsWith('/') || path.startsWith('~/') || !workingDir) return path;
|
|
2207
|
+
const parts = [];
|
|
2208
|
+
for (const part of `${workingDir}/${path}`.split('/')) {
|
|
2209
|
+
if (!part || part === '.') continue;
|
|
2210
|
+
if (part === '..') parts.pop();
|
|
2211
|
+
else parts.push(part);
|
|
2212
|
+
}
|
|
2213
|
+
return `/${parts.join('/')}`;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
/**
|
|
2217
|
+
* Extensions the file preview renders as a PAGE, from the sandboxed
|
|
2218
|
+
* `/html-view/<cap>/` route (src/web/routes/html-view-routes.ts, whose
|
|
2219
|
+
* HTML_VIEW_EXTENSIONS this mirrors; test/file-preview-html-page.test.ts pins
|
|
2220
|
+
* the two equal). The terminal sends a click on one to the preview, never to
|
|
2221
|
+
* the log viewer, and the preview renders it with a Page pill back to source.
|
|
2222
|
+
*/
|
|
2223
|
+
const HTML_PAGE_EXTENSIONS = new Set(['html', 'htm']);
|
|
2224
|
+
|
|
2225
|
+
/** Whether a path names a file the preview renders as a page ({@link HTML_PAGE_EXTENSIONS}). */
|
|
2226
|
+
function rendersAsPage(filePath) {
|
|
2227
|
+
const ext = String(filePath || '').split('.').pop().toLowerCase();
|
|
2228
|
+
return HTML_PAGE_EXTENSIONS.has(ext);
|
|
2229
|
+
}
|
|
2230
|
+
|
|
1979
2231
|
|
|
1980
2232
|
/**
|
|
1981
2233
|
* The LOGICAL line a terminal row belongs to — the rows it spans, its text as one
|
|
@@ -2080,6 +2332,134 @@ function terminalLogicalLine(buffer, row, cols, maxRows) {
|
|
|
2080
2332
|
return { startRow, endRow, text, offsetToCell, cellToOffset };
|
|
2081
2333
|
}
|
|
2082
2334
|
|
|
2335
|
+
/** Roots of {@link FILE_PATH_LINK_PATTERN}, read off its source (`home|Users|…`). */
|
|
2336
|
+
const FILE_LINK_ROOT_GROUP = /^\(\\\/\(\?:([^()]*)\)/.exec(FILE_PATH_LINK_PATTERN.source)[1];
|
|
2337
|
+
const PATH_TOKEN_ENDS_WITH_EXTENSION = new RegExp(`\\.${FILE_LINK_EXTENSION_GROUP}$`);
|
|
2338
|
+
const PATH_TOKEN_HAS_EXTENSION = new RegExp(`\\.${FILE_LINK_EXTENSION_GROUP}\\b`);
|
|
2339
|
+
const PATH_TOKEN_STARTS_NEW_PATH = new RegExp(`^(?:~\\/|\\/(?:${FILE_LINK_ROOT_GROUP})\\/)`);
|
|
2340
|
+
/** A relative head that names its own location (`./a`, `../a`) is an entry of its own. */
|
|
2341
|
+
const PATH_TOKEN_STARTS_EXPLICIT_RELATIVE = /^\.{1,2}\//;
|
|
2342
|
+
/** First directory of a path, a leading `/` or `./` ignored: `src` for `src/a.ts` and `/src/a.ts`. */
|
|
2343
|
+
const pathFirstSegment = (path) => path.replace(/^(?:\.?\/)+/, '').split('/')[0];
|
|
2344
|
+
|
|
2345
|
+
/**
|
|
2346
|
+
* Whether the line `nextText` carries on a file path `prevText` broke off.
|
|
2347
|
+
*
|
|
2348
|
+
* Answers `'join'` when the path ends (reaches a known extension) on the next
|
|
2349
|
+
* line, `'chain'` when the next line is nothing but more of the path (a path
|
|
2350
|
+
* spanning three or more lines; only worth keeping once a later line ends it),
|
|
2351
|
+
* and `null` otherwise. The broken-off token must already look like a path in
|
|
2352
|
+
* progress: it contains a `/`, ends on a path character and does not yet end
|
|
2353
|
+
* on an extension. A next line that opens a NEW path is never glued on: a rooted
|
|
2354
|
+
* one (`/home/…`, `~/…`), `./…`/`../…`, or a relative one that opens on the same
|
|
2355
|
+
* top directory the broken token did (the next entry of a listing).
|
|
2356
|
+
*/
|
|
2357
|
+
function pathContinuation(prevText, nextText) {
|
|
2358
|
+
const tail = /[^\s"'<>|;&]+$/.exec(String(prevText || '').replace(/\s+$/, ''))?.[0] || '';
|
|
2359
|
+
if (!tail.includes('/') || !/[\w\-.~+@%/]$/.test(tail) || PATH_TOKEN_ENDS_WITH_EXTENSION.test(tail)) return null;
|
|
2360
|
+
const next = String(nextText || '').replace(/^\s+/, '');
|
|
2361
|
+
const head = /^[^\s"'<>|;&]+/.exec(next)?.[0] || '';
|
|
2362
|
+
if (!head || PATH_TOKEN_STARTS_NEW_PATH.test(head) || PATH_TOKEN_STARTS_EXPLICIT_RELATIVE.test(head)) return null;
|
|
2363
|
+
// A listing (`git ls-files`, `find`, `ls -R`) puts one path per row, so `src/components`
|
|
2364
|
+
// followed by `src/index.ts` is two entries, not one path broken after `src/components`.
|
|
2365
|
+
// A wrapped path continues INSIDE a name: its next row does not open on the same top directory.
|
|
2366
|
+
if (head.includes('/') && pathFirstSegment(head) === pathFirstSegment(tail)) return null;
|
|
2367
|
+
if (PATH_TOKEN_HAS_EXTENSION.test(head)) return 'join';
|
|
2368
|
+
return head === next.replace(/\s+$/, '') ? 'chain' : null;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
/**
|
|
2372
|
+
* {@link terminalLogicalLine}, extended across the line breaks a program puts
|
|
2373
|
+
* INSIDE a file path. Used by the link provider only; touch selection keeps the
|
|
2374
|
+
* plain logical line.
|
|
2375
|
+
*
|
|
2376
|
+
* Claude Code wraps its tool output itself, a few columns short of the terminal
|
|
2377
|
+
* edge, so neither continuation signal of terminalLogicalLine fires (no
|
|
2378
|
+
* `isWrapped`, the row does not reach the last column) and a long path was only
|
|
2379
|
+
* linked up to the break:
|
|
2380
|
+
*
|
|
2381
|
+
* ⎿ BILD /mnt/build/neon_getaway-mgr-hebel/builds/captures/2
|
|
2382
|
+
* 026-10-10/ghost1b_gameplay.png err=0 …
|
|
2383
|
+
*
|
|
2384
|
+
* A following logical line is glued on (its indent dropped, cell mapping kept
|
|
2385
|
+
* exact) when {@link pathContinuation} says it carries the path on and the row it
|
|
2386
|
+
* broke off is at least as long as the row it continues on. Bounded by the same
|
|
2387
|
+
* `maxRows` as the logical line.
|
|
2388
|
+
*
|
|
2389
|
+
* @returns Same shape as {@link terminalLogicalLine}; null when the row does not exist.
|
|
2390
|
+
*/
|
|
2391
|
+
function terminalPathLine(buffer, row, cols, maxRows) {
|
|
2392
|
+
const base = terminalLogicalLine(buffer, row, cols, maxRows);
|
|
2393
|
+
if (!base) return null;
|
|
2394
|
+
const bound = Math.max(1, maxRows || 12);
|
|
2395
|
+
const lineAt = (r) => terminalLogicalLine(buffer, r, cols, bound);
|
|
2396
|
+
const rowsOf = (line) => line.endRow - line.startRow + 1;
|
|
2397
|
+
// A program breaking a long token fills the row to its wrap width, and no
|
|
2398
|
+
// later row of that block is wider. So the row a path broke off is at least as
|
|
2399
|
+
// long as the row it continues on — which keeps `cd /mnt/foo` from being glued
|
|
2400
|
+
// to a longer ` file.png written` below it.
|
|
2401
|
+
const rowLength = (r) => (buffer.getLine(r)?.translateToString(true) || '').length;
|
|
2402
|
+
const continues = (prev, next, prevText) =>
|
|
2403
|
+
rowLength(prev.endRow) >= rowLength(next.startRow) && pathContinuation(prevText, next.text);
|
|
2404
|
+
|
|
2405
|
+
/** `first` and the lines a path carries on into, stopping at the last confirmed join. */
|
|
2406
|
+
const extend = (first) => {
|
|
2407
|
+
const parts = [first];
|
|
2408
|
+
let text = first.text;
|
|
2409
|
+
let rows = rowsOf(first);
|
|
2410
|
+
let confirmed = 1;
|
|
2411
|
+
for (;;) {
|
|
2412
|
+
const next = lineAt(parts[parts.length - 1].endRow + 1);
|
|
2413
|
+
if (!next || rows + rowsOf(next) > bound) break;
|
|
2414
|
+
const kind = continues(parts[parts.length - 1], next, text);
|
|
2415
|
+
if (!kind) break;
|
|
2416
|
+
parts.push(next);
|
|
2417
|
+
text += next.text.replace(/^\s+/, '');
|
|
2418
|
+
rows += rowsOf(next);
|
|
2419
|
+
if (kind === 'join') confirmed = parts.length;
|
|
2420
|
+
}
|
|
2421
|
+
return parts.slice(0, confirmed);
|
|
2422
|
+
};
|
|
2423
|
+
|
|
2424
|
+
// The hovered row may be the second or third line of a broken path: walk back
|
|
2425
|
+
// to where it could start, then let the forward pass decide what really joins.
|
|
2426
|
+
let first = base;
|
|
2427
|
+
let rows = rowsOf(base);
|
|
2428
|
+
while (first.startRow > 0) {
|
|
2429
|
+
const prev = lineAt(first.startRow - 1);
|
|
2430
|
+
if (!prev || rows + rowsOf(prev) > bound || !continues(prev, first, prev.text)) break;
|
|
2431
|
+
first = prev;
|
|
2432
|
+
rows += rowsOf(prev);
|
|
2433
|
+
}
|
|
2434
|
+
let parts = extend(first);
|
|
2435
|
+
if (parts[parts.length - 1].endRow < base.startRow) parts = extend(base);
|
|
2436
|
+
if (parts.length === 1) return parts[0];
|
|
2437
|
+
|
|
2438
|
+
const segments = [];
|
|
2439
|
+
let text = '';
|
|
2440
|
+
for (const [i, part] of parts.entries()) {
|
|
2441
|
+
const skip = i === 0 ? 0 : part.text.length - part.text.replace(/^\s+/, '').length;
|
|
2442
|
+
segments.push({ part, textStart: text.length, skip });
|
|
2443
|
+
text += part.text.slice(skip);
|
|
2444
|
+
}
|
|
2445
|
+
const offsetToCell = (offset) => {
|
|
2446
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
2447
|
+
const seg = segments[i];
|
|
2448
|
+
if (offset >= seg.textStart || i === 0) return seg.part.offsetToCell(offset - seg.textStart + seg.skip);
|
|
2449
|
+
}
|
|
2450
|
+
return segments[0].part.offsetToCell(offset);
|
|
2451
|
+
};
|
|
2452
|
+
const cellToOffset = (targetRow, targetCol) => {
|
|
2453
|
+
for (const seg of segments) {
|
|
2454
|
+
if (targetRow < seg.part.startRow || targetRow > seg.part.endRow) continue;
|
|
2455
|
+
const offset = seg.part.cellToOffset(targetRow, targetCol);
|
|
2456
|
+
return offset < 0 ? -1 : seg.textStart + Math.max(0, offset - seg.skip);
|
|
2457
|
+
}
|
|
2458
|
+
return -1;
|
|
2459
|
+
};
|
|
2460
|
+
return { startRow: parts[0].startRow, endRow: parts[parts.length - 1].endRow, text, offsetToCell, cellToOffset };
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2083
2463
|
// ═══════════════════════════════════════════════════════════════
|
|
2084
2464
|
// Split-Pane Sessions — pure helpers (divider math, picker list)
|
|
2085
2465
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -2992,8 +3372,16 @@ function describeSessionHarness(session, catalog) {
|
|
|
2992
3372
|
if (typeof window !== 'undefined') {
|
|
2993
3373
|
window.CodemanSessionHarness = { describeSessionHarness, SESSION_MODEL_MAX_CHARS };
|
|
2994
3374
|
window.CodemanHistoryFormat = { formatHistoryBytes, computeHistoryTruncationNotice, computeRewriteScrollLine };
|
|
2995
|
-
window.CodemanFilePaths = {
|
|
2996
|
-
|
|
3375
|
+
window.CodemanFilePaths = {
|
|
3376
|
+
absoluteFilePathPattern,
|
|
3377
|
+
findFilePathLinks,
|
|
3378
|
+
resolveLinkedFilePath,
|
|
3379
|
+
previewsInFileViewer,
|
|
3380
|
+
FILE_PREVIEW_EXTENSIONS,
|
|
3381
|
+
rendersAsPage,
|
|
3382
|
+
HTML_PAGE_EXTENSIONS,
|
|
3383
|
+
};
|
|
3384
|
+
window.CodemanTerminalLines = { terminalLogicalLine, terminalPathLine };
|
|
2997
3385
|
window.CodemanUrlSession = { sessionIdFromFragment };
|
|
2998
3386
|
window.CodemanSplitPane = {
|
|
2999
3387
|
clampDividerPercent,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|