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.
Files changed (287) hide show
  1. package/README.md +58 -8
  2. package/dist/attachment-registry.d.ts +2 -0
  3. package/dist/attachment-registry.d.ts.map +1 -1
  4. package/dist/attachment-registry.js +14 -3
  5. package/dist/attachment-registry.js.map +1 -1
  6. package/dist/cli-agent.d.ts +2 -0
  7. package/dist/cli-agent.d.ts.map +1 -1
  8. package/dist/cli-agent.js +12 -8
  9. package/dist/cli-agent.js.map +1 -1
  10. package/dist/cli.d.ts +25 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +97 -11
  13. package/dist/cli.js.map +1 -1
  14. package/dist/config/buffer-limits.d.ts +9 -0
  15. package/dist/config/buffer-limits.d.ts.map +1 -1
  16. package/dist/config/buffer-limits.js +9 -0
  17. package/dist/config/buffer-limits.js.map +1 -1
  18. package/dist/config/cli-registry/schema.d.ts +8 -0
  19. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  20. package/dist/config/cli-registry/schema.js +12 -0
  21. package/dist/config/cli-registry/schema.js.map +1 -1
  22. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  23. package/dist/config/cli-registry/stock.js +42 -0
  24. package/dist/config/cli-registry/stock.js.map +1 -1
  25. package/dist/config/cli-registry/types.d.ts +35 -0
  26. package/dist/config/cli-registry/types.d.ts.map +1 -1
  27. package/dist/folder-listing.d.ts +94 -0
  28. package/dist/folder-listing.d.ts.map +1 -0
  29. package/dist/folder-listing.js +241 -0
  30. package/dist/folder-listing.js.map +1 -0
  31. package/dist/hooks-config.d.ts +36 -16
  32. package/dist/hooks-config.d.ts.map +1 -1
  33. package/dist/hooks-config.js +45 -20
  34. package/dist/hooks-config.js.map +1 -1
  35. package/dist/html-view-capabilities.d.ts +54 -0
  36. package/dist/html-view-capabilities.d.ts.map +1 -0
  37. package/dist/html-view-capabilities.js +103 -0
  38. package/dist/html-view-capabilities.js.map +1 -0
  39. package/dist/mcp-server.d.ts +114 -0
  40. package/dist/mcp-server.d.ts.map +1 -0
  41. package/dist/mcp-server.js +917 -0
  42. package/dist/mcp-server.js.map +1 -0
  43. package/dist/mcp-sync.d.ts +5 -0
  44. package/dist/mcp-sync.d.ts.map +1 -1
  45. package/dist/mcp-sync.js +51 -5
  46. package/dist/mcp-sync.js.map +1 -1
  47. package/dist/mux-interface.d.ts +36 -1
  48. package/dist/mux-interface.d.ts.map +1 -1
  49. package/dist/remote-files.d.ts +37 -0
  50. package/dist/remote-files.d.ts.map +1 -1
  51. package/dist/remote-files.js +103 -0
  52. package/dist/remote-files.js.map +1 -1
  53. package/dist/remote-pane-history.d.ts +103 -0
  54. package/dist/remote-pane-history.d.ts.map +1 -0
  55. package/dist/remote-pane-history.js +258 -0
  56. package/dist/remote-pane-history.js.map +1 -0
  57. package/dist/remote-ssh-limiter.d.ts +16 -3
  58. package/dist/remote-ssh-limiter.d.ts.map +1 -1
  59. package/dist/remote-ssh-limiter.js +37 -6
  60. package/dist/remote-ssh-limiter.js.map +1 -1
  61. package/dist/session.d.ts +29 -1
  62. package/dist/session.d.ts.map +1 -1
  63. package/dist/session.js +53 -0
  64. package/dist/session.js.map +1 -1
  65. package/dist/tab-layout-legacy-order.d.ts.map +1 -1
  66. package/dist/tab-layout-legacy-order.js +72 -13
  67. package/dist/tab-layout-legacy-order.js.map +1 -1
  68. package/dist/tab-layout-service.d.ts +33 -2
  69. package/dist/tab-layout-service.d.ts.map +1 -1
  70. package/dist/tab-layout-service.js +102 -7
  71. package/dist/tab-layout-service.js.map +1 -1
  72. package/dist/tab-layout.d.ts +9 -0
  73. package/dist/tab-layout.d.ts.map +1 -1
  74. package/dist/tab-layout.js +15 -2
  75. package/dist/tab-layout.js.map +1 -1
  76. package/dist/tmux-manager.d.ts +71 -2
  77. package/dist/tmux-manager.d.ts.map +1 -1
  78. package/dist/tmux-manager.js +169 -18
  79. package/dist/tmux-manager.js.map +1 -1
  80. package/dist/types/session.d.ts +53 -0
  81. package/dist/types/session.d.ts.map +1 -1
  82. package/dist/types/session.js.map +1 -1
  83. package/dist/usage-telemetry.d.ts +44 -7
  84. package/dist/usage-telemetry.d.ts.map +1 -1
  85. package/dist/usage-telemetry.js +72 -7
  86. package/dist/usage-telemetry.js.map +1 -1
  87. package/dist/web/approval-inbox.d.ts +12 -0
  88. package/dist/web/approval-inbox.d.ts.map +1 -1
  89. package/dist/web/approval-inbox.js +1 -0
  90. package/dist/web/approval-inbox.js.map +1 -1
  91. package/dist/web/middleware/auth.d.ts.map +1 -1
  92. package/dist/web/middleware/auth.js +33 -0
  93. package/dist/web/middleware/auth.js.map +1 -1
  94. package/dist/web/ports/session-port.d.ts +8 -1
  95. package/dist/web/ports/session-port.d.ts.map +1 -1
  96. package/dist/web/public/admin-ui.js.gz +0 -0
  97. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  98. package/dist/web/public/app.24689381.js +43 -0
  99. package/dist/web/public/app.24689381.js.br +0 -0
  100. package/dist/web/public/app.24689381.js.gz +0 -0
  101. package/dist/web/public/approvals-ui.js +4 -0
  102. package/dist/web/public/approvals-ui.js.br +0 -0
  103. package/dist/web/public/approvals-ui.js.gz +0 -0
  104. package/dist/web/public/{constants.bc9c1f63.js → constants.17d77b65.js} +395 -7
  105. package/dist/web/public/constants.17d77b65.js.br +0 -0
  106. package/dist/web/public/constants.17d77b65.js.gz +0 -0
  107. package/dist/web/public/cron-ui.js.gz +0 -0
  108. package/dist/web/public/entrance-animations.js.gz +0 -0
  109. package/dist/web/public/gesture/gesture-codeman.js +112 -6
  110. package/dist/web/public/git-status-ui.js.gz +0 -0
  111. package/dist/web/public/home-sessions.js.gz +0 -0
  112. package/dist/web/public/host-wake-ui.js.gz +0 -0
  113. package/dist/web/public/i18n.9afd8e4d.js +2 -0
  114. package/dist/web/public/i18n.9afd8e4d.js.br +0 -0
  115. package/dist/web/public/i18n.9afd8e4d.js.gz +0 -0
  116. package/dist/web/public/{image-input.e00471c6.js → image-input.9ed1d0d2.js} +186 -16
  117. package/dist/web/public/image-input.9ed1d0d2.js.br +0 -0
  118. package/dist/web/public/image-input.9ed1d0d2.js.gz +0 -0
  119. package/dist/web/public/index.html +105 -23
  120. package/dist/web/public/index.html.br +0 -0
  121. package/dist/web/public/index.html.gz +0 -0
  122. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  123. package/dist/web/public/{keyboard-accessory.c1e7c7b7.js → keyboard-accessory.c458eea8.js} +115 -35
  124. package/dist/web/public/keyboard-accessory.c458eea8.js.br +0 -0
  125. package/dist/web/public/keyboard-accessory.c458eea8.js.gz +0 -0
  126. package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
  127. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  128. package/dist/web/public/mobile-overview.js +11 -2
  129. package/dist/web/public/mobile-overview.js.br +0 -0
  130. package/dist/web/public/mobile-overview.js.gz +0 -0
  131. package/dist/web/public/mobile.2dff1580.css +1 -0
  132. package/dist/web/public/mobile.2dff1580.css.br +0 -0
  133. package/dist/web/public/mobile.2dff1580.css.gz +0 -0
  134. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  135. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  136. package/dist/web/public/{panels-ui.71846fcb.js → panels-ui.73ec0137.js} +87 -87
  137. package/dist/web/public/panels-ui.73ec0137.js.br +0 -0
  138. package/dist/web/public/panels-ui.73ec0137.js.gz +0 -0
  139. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  140. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  141. package/dist/web/public/readmymind-ui.js.gz +0 -0
  142. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  143. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  144. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  145. package/dist/web/public/session-lineage.js.gz +0 -0
  146. package/dist/web/public/{session-ui.f731602c.js → session-ui.dbc6819b.js} +33 -33
  147. package/dist/web/public/session-ui.dbc6819b.js.br +0 -0
  148. package/dist/web/public/session-ui.dbc6819b.js.gz +0 -0
  149. package/dist/web/public/{settings-ui.4e33390f.js → settings-ui.81ac264e.js} +9 -9
  150. package/dist/web/public/settings-ui.81ac264e.js.br +0 -0
  151. package/dist/web/public/settings-ui.81ac264e.js.gz +0 -0
  152. package/dist/web/public/spreadsheet-preview-worker.js +11 -0
  153. package/dist/web/public/spreadsheet-preview-worker.js.br +0 -0
  154. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  155. package/dist/web/public/{spreadsheet-preview.af82fa2f.js → spreadsheet-preview.75c751de.js} +1 -1
  156. package/dist/web/public/spreadsheet-preview.75c751de.js.br +0 -0
  157. package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
  158. package/dist/web/public/spreadsheet-xlsx-core.js +110 -0
  159. package/dist/web/public/spreadsheet-xlsx-core.js.br +0 -0
  160. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  161. package/dist/web/public/styles.900a28e4.css +1 -0
  162. package/dist/web/public/styles.900a28e4.css.br +0 -0
  163. package/dist/web/public/styles.900a28e4.css.gz +0 -0
  164. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  165. package/dist/web/public/sw.js +6 -2
  166. package/dist/web/public/sw.js.br +0 -0
  167. package/dist/web/public/sw.js.gz +0 -0
  168. package/dist/web/public/tab-layout-browser.2d6189f7.js +1 -0
  169. package/dist/web/public/tab-layout-browser.2d6189f7.js.br +0 -0
  170. package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
  171. package/dist/web/public/{tab-rail-resize.a5867793.js → tab-rail-resize.7157e0bd.js} +1 -1
  172. package/dist/web/public/tab-rail-resize.7157e0bd.js.br +0 -0
  173. package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
  174. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
  175. package/dist/web/public/terminal-split.js.gz +0 -0
  176. package/dist/web/public/terminal-tile.js +44 -24
  177. package/dist/web/public/terminal-tile.js.br +0 -0
  178. package/dist/web/public/terminal-tile.js.gz +0 -0
  179. package/dist/web/public/terminal-ui.858f5da5.js +2 -0
  180. package/dist/web/public/terminal-ui.858f5da5.js.br +0 -0
  181. package/dist/web/public/terminal-ui.858f5da5.js.gz +0 -0
  182. package/dist/web/public/tile-grid.js +439 -18
  183. package/dist/web/public/tile-grid.js.br +0 -0
  184. package/dist/web/public/tile-grid.js.gz +0 -0
  185. package/dist/web/public/ultracode-panel.js.gz +0 -0
  186. package/dist/web/public/ultracode-windows.js.gz +0 -0
  187. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  188. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  189. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  190. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  191. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  192. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  193. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  194. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  195. package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
  196. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
  197. package/dist/web/public/vendor/xterm.css.gz +0 -0
  198. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  199. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  200. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  201. package/dist/web/public/webview-tabs.js +12 -0
  202. package/dist/web/public/webview-tabs.js.br +0 -0
  203. package/dist/web/public/webview-tabs.js.gz +0 -0
  204. package/dist/web/route-helpers.d.ts +8 -0
  205. package/dist/web/route-helpers.d.ts.map +1 -1
  206. package/dist/web/route-helpers.js +8 -0
  207. package/dist/web/route-helpers.js.map +1 -1
  208. package/dist/web/routes/admin-routes.d.ts.map +1 -1
  209. package/dist/web/routes/admin-routes.js +4 -1
  210. package/dist/web/routes/admin-routes.js.map +1 -1
  211. package/dist/web/routes/approval-routes.d.ts.map +1 -1
  212. package/dist/web/routes/approval-routes.js +6 -8
  213. package/dist/web/routes/approval-routes.js.map +1 -1
  214. package/dist/web/routes/case-routes.js +1 -1
  215. package/dist/web/routes/case-routes.js.map +1 -1
  216. package/dist/web/routes/file-routes.d.ts +46 -1
  217. package/dist/web/routes/file-routes.d.ts.map +1 -1
  218. package/dist/web/routes/file-routes.js +214 -27
  219. package/dist/web/routes/file-routes.js.map +1 -1
  220. package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
  221. package/dist/web/routes/hook-event-routes.js +8 -1
  222. package/dist/web/routes/hook-event-routes.js.map +1 -1
  223. package/dist/web/routes/html-view-routes.d.ts +60 -0
  224. package/dist/web/routes/html-view-routes.d.ts.map +1 -0
  225. package/dist/web/routes/html-view-routes.js +288 -0
  226. package/dist/web/routes/html-view-routes.js.map +1 -0
  227. package/dist/web/routes/index.d.ts +1 -0
  228. package/dist/web/routes/index.d.ts.map +1 -1
  229. package/dist/web/routes/index.js +1 -0
  230. package/dist/web/routes/index.js.map +1 -1
  231. package/dist/web/routes/reboot-restore-routes.js +2 -2
  232. package/dist/web/routes/reboot-restore-routes.js.map +1 -1
  233. package/dist/web/routes/session-routes.d.ts +1 -0
  234. package/dist/web/routes/session-routes.d.ts.map +1 -1
  235. package/dist/web/routes/session-routes.js +278 -56
  236. package/dist/web/routes/session-routes.js.map +1 -1
  237. package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
  238. package/dist/web/routes/status-telemetry-routes.js +16 -2
  239. package/dist/web/routes/status-telemetry-routes.js.map +1 -1
  240. package/dist/web/schemas.d.ts +29 -0
  241. package/dist/web/schemas.d.ts.map +1 -1
  242. package/dist/web/schemas.js +65 -0
  243. package/dist/web/schemas.js.map +1 -1
  244. package/dist/web/server.d.ts +6 -1
  245. package/dist/web/server.d.ts.map +1 -1
  246. package/dist/web/server.js +62 -14
  247. package/dist/web/server.js.map +1 -1
  248. package/dist/web/sse-events.d.ts +5 -2
  249. package/dist/web/sse-events.d.ts.map +1 -1
  250. package/dist/web/sse-events.js +5 -2
  251. package/dist/web/sse-events.js.map +1 -1
  252. package/package.json +2 -1
  253. package/skills/codeman/SKILL.md +29 -12
  254. package/dist/web/public/app.6f8552e1.js +0 -43
  255. package/dist/web/public/app.6f8552e1.js.br +0 -0
  256. package/dist/web/public/app.6f8552e1.js.gz +0 -0
  257. package/dist/web/public/constants.bc9c1f63.js.br +0 -0
  258. package/dist/web/public/constants.bc9c1f63.js.gz +0 -0
  259. package/dist/web/public/i18n.a9840ea2.js +0 -2
  260. package/dist/web/public/i18n.a9840ea2.js.br +0 -0
  261. package/dist/web/public/i18n.a9840ea2.js.gz +0 -0
  262. package/dist/web/public/image-input.e00471c6.js.br +0 -0
  263. package/dist/web/public/image-input.e00471c6.js.gz +0 -0
  264. package/dist/web/public/keyboard-accessory.c1e7c7b7.js.br +0 -0
  265. package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
  266. package/dist/web/public/mobile.111b187d.css +0 -1
  267. package/dist/web/public/mobile.111b187d.css.br +0 -0
  268. package/dist/web/public/mobile.111b187d.css.gz +0 -0
  269. package/dist/web/public/panels-ui.71846fcb.js.br +0 -0
  270. package/dist/web/public/panels-ui.71846fcb.js.gz +0 -0
  271. package/dist/web/public/session-ui.f731602c.js.br +0 -0
  272. package/dist/web/public/session-ui.f731602c.js.gz +0 -0
  273. package/dist/web/public/settings-ui.4e33390f.js.br +0 -0
  274. package/dist/web/public/settings-ui.4e33390f.js.gz +0 -0
  275. package/dist/web/public/spreadsheet-preview.af82fa2f.js.br +0 -0
  276. package/dist/web/public/spreadsheet-preview.af82fa2f.js.gz +0 -0
  277. package/dist/web/public/styles.81a72906.css +0 -1
  278. package/dist/web/public/styles.81a72906.css.br +0 -0
  279. package/dist/web/public/styles.81a72906.css.gz +0 -0
  280. package/dist/web/public/tab-layout-browser.065fcba4.js +0 -1
  281. package/dist/web/public/tab-layout-browser.065fcba4.js.br +0 -0
  282. package/dist/web/public/tab-layout-browser.065fcba4.js.gz +0 -0
  283. package/dist/web/public/tab-rail-resize.a5867793.js.br +0 -0
  284. package/dist/web/public/tab-rail-resize.a5867793.js.gz +0 -0
  285. package/dist/web/public/terminal-ui.b7267005.js +0 -2
  286. package/dist/web/public/terminal-ui.b7267005.js.br +0 -0
  287. 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
- if (n < 1024 * 1024) return `${Math.round(n / 1024)} KB`;
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}} state
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. ${formatHistoryBytes(dropped)} more may still be retained.`,
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|toml|ini|sql|png|jpe?g|gif|webp|avif|bmp|ico|svg|pdf|docx|pptx|xlsx|mp4|webm|mov|mp3|wav))\b/g;
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 = { absoluteFilePathPattern, previewsInFileViewer, FILE_PREVIEW_EXTENSIONS };
2996
- window.CodemanTerminalLines = { terminalLogicalLine };
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