aicodeman 1.47.0 → 1.48.0

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 (264) hide show
  1. package/README.md +3 -0
  2. package/dist/cli-notify.d.ts +34 -0
  3. package/dist/cli-notify.d.ts.map +1 -0
  4. package/dist/cli-notify.js +100 -0
  5. package/dist/cli-notify.js.map +1 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +16 -0
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config/cli-registry/patterns.d.ts +1 -1
  10. package/dist/config/cli-registry/patterns.d.ts.map +1 -1
  11. package/dist/config/cli-registry/patterns.js +4 -0
  12. package/dist/config/cli-registry/patterns.js.map +1 -1
  13. package/dist/config/cli-registry/schema.d.ts +6 -0
  14. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  15. package/dist/config/cli-registry/schema.js +62 -2
  16. package/dist/config/cli-registry/schema.js.map +1 -1
  17. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  18. package/dist/config/cli-registry/stock.js +43 -26
  19. package/dist/config/cli-registry/stock.js.map +1 -1
  20. package/dist/config/cli-registry/types.d.ts +47 -17
  21. package/dist/config/cli-registry/types.d.ts.map +1 -1
  22. package/dist/config/path-probe.d.ts +6 -0
  23. package/dist/config/path-probe.d.ts.map +1 -1
  24. package/dist/config/path-probe.js +6 -0
  25. package/dist/config/path-probe.js.map +1 -1
  26. package/dist/config-backup.d.ts +1 -1
  27. package/dist/config-backup.d.ts.map +1 -1
  28. package/dist/config-backup.js +2 -0
  29. package/dist/config-backup.js.map +1 -1
  30. package/dist/custom-model-hosts.d.ts +32 -8
  31. package/dist/custom-model-hosts.d.ts.map +1 -1
  32. package/dist/custom-model-hosts.js +10 -0
  33. package/dist/custom-model-hosts.js.map +1 -1
  34. package/dist/custom-model-injection-apply.d.ts +19 -5
  35. package/dist/custom-model-injection-apply.d.ts.map +1 -1
  36. package/dist/custom-model-injection-apply.js +88 -8
  37. package/dist/custom-model-injection-apply.js.map +1 -1
  38. package/dist/custom-model-injection.d.ts +6 -0
  39. package/dist/custom-model-injection.d.ts.map +1 -1
  40. package/dist/custom-model-injection.js +92 -10
  41. package/dist/custom-model-injection.js.map +1 -1
  42. package/dist/discord-bridge.d.ts +338 -0
  43. package/dist/discord-bridge.d.ts.map +1 -0
  44. package/dist/discord-bridge.js +1256 -0
  45. package/dist/discord-bridge.js.map +1 -0
  46. package/dist/edit-history-local-source.d.ts +83 -0
  47. package/dist/edit-history-local-source.d.ts.map +1 -0
  48. package/dist/edit-history-local-source.js +198 -0
  49. package/dist/edit-history-local-source.js.map +1 -0
  50. package/dist/edit-history-parse.d.ts +114 -0
  51. package/dist/edit-history-parse.d.ts.map +1 -0
  52. package/dist/edit-history-parse.js +312 -0
  53. package/dist/edit-history-parse.js.map +1 -0
  54. package/dist/edit-history-store.d.ts +209 -0
  55. package/dist/edit-history-store.d.ts.map +1 -0
  56. package/dist/edit-history-store.js +599 -0
  57. package/dist/edit-history-store.js.map +1 -0
  58. package/dist/git-clone.d.ts +6 -1
  59. package/dist/git-clone.d.ts.map +1 -1
  60. package/dist/git-clone.js +79 -14
  61. package/dist/git-clone.js.map +1 -1
  62. package/dist/image-watcher.js +4 -4
  63. package/dist/image-watcher.js.map +1 -1
  64. package/dist/mux-interface.d.ts +10 -1
  65. package/dist/mux-interface.d.ts.map +1 -1
  66. package/dist/session-cli-registry-bridge.d.ts.map +1 -1
  67. package/dist/session-cli-registry-bridge.js +4 -0
  68. package/dist/session-cli-registry-bridge.js.map +1 -1
  69. package/dist/session.d.ts +70 -3
  70. package/dist/session.d.ts.map +1 -1
  71. package/dist/session.js +147 -4
  72. package/dist/session.js.map +1 -1
  73. package/dist/tmux-manager.d.ts +40 -0
  74. package/dist/tmux-manager.d.ts.map +1 -1
  75. package/dist/tmux-manager.js +62 -0
  76. package/dist/tmux-manager.js.map +1 -1
  77. package/dist/types/discord.d.ts +55 -0
  78. package/dist/types/discord.d.ts.map +1 -0
  79. package/dist/types/discord.js +18 -0
  80. package/dist/types/discord.js.map +1 -0
  81. package/dist/types/index.d.ts +2 -0
  82. package/dist/types/index.d.ts.map +1 -1
  83. package/dist/types/index.js +2 -0
  84. package/dist/types/index.js.map +1 -1
  85. package/dist/types/session.d.ts +13 -0
  86. package/dist/types/session.d.ts.map +1 -1
  87. package/dist/types/session.js.map +1 -1
  88. package/dist/web/case-path.d.ts +2 -0
  89. package/dist/web/case-path.d.ts.map +1 -1
  90. package/dist/web/case-path.js +3 -3
  91. package/dist/web/case-path.js.map +1 -1
  92. package/dist/web/edit-history-sync.d.ts +158 -0
  93. package/dist/web/edit-history-sync.d.ts.map +1 -0
  94. package/dist/web/edit-history-sync.js +445 -0
  95. package/dist/web/edit-history-sync.js.map +1 -0
  96. package/dist/web/paste-image-gc.d.ts +6 -0
  97. package/dist/web/paste-image-gc.d.ts.map +1 -1
  98. package/dist/web/paste-image-gc.js +6 -0
  99. package/dist/web/paste-image-gc.js.map +1 -1
  100. package/dist/web/ports/session-port.d.ts +13 -0
  101. package/dist/web/ports/session-port.d.ts.map +1 -1
  102. package/dist/web/public/admin-ui.js.gz +0 -0
  103. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  104. package/dist/web/public/{app.fe8a6b4c.js → app.963b5ba8.js} +6 -6
  105. package/dist/web/public/app.963b5ba8.js.br +0 -0
  106. package/dist/web/public/app.963b5ba8.js.gz +0 -0
  107. package/dist/web/public/approvals-ui.js.gz +0 -0
  108. package/dist/web/public/{constants.affcdde6.js → constants.782170e1.js} +114 -25
  109. package/dist/web/public/constants.782170e1.js.br +0 -0
  110. package/dist/web/public/constants.782170e1.js.gz +0 -0
  111. package/dist/web/public/cron-ui.js.gz +0 -0
  112. package/dist/web/public/edit-history-ui.js +630 -0
  113. package/dist/web/public/edit-history-ui.js.br +0 -0
  114. package/dist/web/public/edit-history-ui.js.gz +0 -0
  115. package/dist/web/public/entrance-animations.js.gz +0 -0
  116. package/dist/web/public/git-status-ui.js.gz +0 -0
  117. package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
  118. package/dist/web/public/home-sessions.js.gz +0 -0
  119. package/dist/web/public/host-wake-ui.js.gz +0 -0
  120. package/dist/web/public/i18n.33f930d7.js +2 -0
  121. package/dist/web/public/i18n.33f930d7.js.br +0 -0
  122. package/dist/web/public/i18n.33f930d7.js.gz +0 -0
  123. package/dist/web/public/image-input.a5d0f8f3.js.gz +0 -0
  124. package/dist/web/public/index.html +160 -14
  125. package/dist/web/public/index.html.br +0 -0
  126. package/dist/web/public/index.html.gz +0 -0
  127. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  128. package/dist/web/public/{keyboard-accessory.ff64fafc.js → keyboard-accessory.d4ec5957.js} +72 -1
  129. package/dist/web/public/keyboard-accessory.d4ec5957.js.br +0 -0
  130. package/dist/web/public/keyboard-accessory.d4ec5957.js.gz +0 -0
  131. package/dist/web/public/mobile-handlers.f7c3f692.js.gz +0 -0
  132. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  133. package/dist/web/public/mobile-overview.js.gz +0 -0
  134. package/dist/web/public/mobile.5ef2ce85.css +1 -0
  135. package/dist/web/public/mobile.5ef2ce85.css.br +0 -0
  136. package/dist/web/public/mobile.5ef2ce85.css.gz +0 -0
  137. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  138. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  139. package/dist/web/public/{panels-ui.4a56ee63.js → panels-ui.b100727d.js} +2 -2
  140. package/dist/web/public/panels-ui.b100727d.js.br +0 -0
  141. package/dist/web/public/{panels-ui.4a56ee63.js.gz → panels-ui.b100727d.js.gz} +0 -0
  142. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  143. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  144. package/dist/web/public/readmymind-ui.js.gz +0 -0
  145. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  146. package/dist/web/public/{respawn-ui.d7adcf6a.js → respawn-ui.304ba0d4.js} +1 -1
  147. package/dist/web/public/respawn-ui.304ba0d4.js.br +0 -0
  148. package/dist/web/public/respawn-ui.304ba0d4.js.gz +0 -0
  149. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  150. package/dist/web/public/security-notices.json +1 -21
  151. package/dist/web/public/session-lineage.js.gz +0 -0
  152. package/dist/web/public/{session-ui.426fb671.js → session-ui.5ac82f2c.js} +30 -30
  153. package/dist/web/public/session-ui.5ac82f2c.js.br +0 -0
  154. package/dist/web/public/session-ui.5ac82f2c.js.gz +0 -0
  155. package/dist/web/public/settings-ui.d1d4fb99.js +106 -0
  156. package/dist/web/public/settings-ui.d1d4fb99.js.br +0 -0
  157. package/dist/web/public/settings-ui.d1d4fb99.js.gz +0 -0
  158. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  159. package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
  160. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  161. package/dist/web/public/styles.77dc538f.css +1 -0
  162. package/dist/web/public/styles.77dc538f.css.br +0 -0
  163. package/dist/web/public/styles.77dc538f.css.gz +0 -0
  164. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  165. package/dist/web/public/sw.js +2 -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.gz +0 -0
  169. package/dist/web/public/tab-rail-resize.b5cdceaf.js.gz +0 -0
  170. package/dist/web/public/terminal-keycode229-recovery.98843069.js.gz +0 -0
  171. package/dist/web/public/terminal-split.js.gz +0 -0
  172. package/dist/web/public/terminal-tile.js.gz +0 -0
  173. package/dist/web/public/{terminal-ui.7e787829.js → terminal-ui.d7c75ce8.js} +3 -3
  174. package/dist/web/public/terminal-ui.d7c75ce8.js.br +0 -0
  175. package/dist/web/public/terminal-ui.d7c75ce8.js.gz +0 -0
  176. package/dist/web/public/tile-grid.js.gz +0 -0
  177. package/dist/web/public/ultracode-panel.js.gz +0 -0
  178. package/dist/web/public/ultracode-windows.js.gz +0 -0
  179. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  180. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  181. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  182. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  183. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  184. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  185. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  186. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  187. package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.gz +0 -0
  188. package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.gz +0 -0
  189. package/dist/web/public/vendor/xterm.css.gz +0 -0
  190. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  191. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  192. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  193. package/dist/web/public/webview-tabs.js.gz +0 -0
  194. package/dist/web/public/whats-new.json +9 -8
  195. package/dist/web/routes/case-routes.d.ts.map +1 -1
  196. package/dist/web/routes/case-routes.js +104 -8
  197. package/dist/web/routes/case-routes.js.map +1 -1
  198. package/dist/web/routes/config-backup-routes.d.ts +5 -1
  199. package/dist/web/routes/config-backup-routes.d.ts.map +1 -1
  200. package/dist/web/routes/config-backup-routes.js +4 -1
  201. package/dist/web/routes/config-backup-routes.js.map +1 -1
  202. package/dist/web/routes/custom-model-routes.d.ts +7 -2
  203. package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
  204. package/dist/web/routes/custom-model-routes.js +51 -6
  205. package/dist/web/routes/custom-model-routes.js.map +1 -1
  206. package/dist/web/routes/discord-routes.d.ts +25 -0
  207. package/dist/web/routes/discord-routes.d.ts.map +1 -0
  208. package/dist/web/routes/discord-routes.js +206 -0
  209. package/dist/web/routes/discord-routes.js.map +1 -0
  210. package/dist/web/routes/edit-history-routes.d.ts +43 -0
  211. package/dist/web/routes/edit-history-routes.d.ts.map +1 -0
  212. package/dist/web/routes/edit-history-routes.js +106 -0
  213. package/dist/web/routes/edit-history-routes.js.map +1 -0
  214. package/dist/web/routes/file-routes.d.ts.map +1 -1
  215. package/dist/web/routes/file-routes.js +62 -2
  216. package/dist/web/routes/file-routes.js.map +1 -1
  217. package/dist/web/routes/index.d.ts +2 -0
  218. package/dist/web/routes/index.d.ts.map +1 -1
  219. package/dist/web/routes/index.js +2 -0
  220. package/dist/web/routes/index.js.map +1 -1
  221. package/dist/web/routes/session-routes.d.ts +17 -0
  222. package/dist/web/routes/session-routes.d.ts.map +1 -1
  223. package/dist/web/routes/session-routes.js +87 -37
  224. package/dist/web/routes/session-routes.js.map +1 -1
  225. package/dist/web/routes/system-routes.d.ts.map +1 -1
  226. package/dist/web/routes/system-routes.js +1 -0
  227. package/dist/web/routes/system-routes.js.map +1 -1
  228. package/dist/web/schemas.d.ts +68 -1
  229. package/dist/web/schemas.d.ts.map +1 -1
  230. package/dist/web/schemas.js +77 -1
  231. package/dist/web/schemas.js.map +1 -1
  232. package/dist/web/sensitive-path.d.ts.map +1 -1
  233. package/dist/web/sensitive-path.js +15 -0
  234. package/dist/web/sensitive-path.js.map +1 -1
  235. package/dist/web/server.d.ts +43 -0
  236. package/dist/web/server.d.ts.map +1 -1
  237. package/dist/web/server.js +219 -11
  238. package/dist/web/server.js.map +1 -1
  239. package/package.json +1 -1
  240. package/dist/web/public/app.fe8a6b4c.js.br +0 -0
  241. package/dist/web/public/app.fe8a6b4c.js.gz +0 -0
  242. package/dist/web/public/constants.affcdde6.js.br +0 -0
  243. package/dist/web/public/constants.affcdde6.js.gz +0 -0
  244. package/dist/web/public/i18n.a618ef4e.js +0 -2
  245. package/dist/web/public/i18n.a618ef4e.js.br +0 -0
  246. package/dist/web/public/i18n.a618ef4e.js.gz +0 -0
  247. package/dist/web/public/keyboard-accessory.ff64fafc.js.br +0 -0
  248. package/dist/web/public/keyboard-accessory.ff64fafc.js.gz +0 -0
  249. package/dist/web/public/mobile.1bbd2d67.css +0 -1
  250. package/dist/web/public/mobile.1bbd2d67.css.br +0 -0
  251. package/dist/web/public/mobile.1bbd2d67.css.gz +0 -0
  252. package/dist/web/public/panels-ui.4a56ee63.js.br +0 -0
  253. package/dist/web/public/respawn-ui.d7adcf6a.js.br +0 -0
  254. package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
  255. package/dist/web/public/session-ui.426fb671.js.br +0 -0
  256. package/dist/web/public/session-ui.426fb671.js.gz +0 -0
  257. package/dist/web/public/settings-ui.a191154e.js +0 -106
  258. package/dist/web/public/settings-ui.a191154e.js.br +0 -0
  259. package/dist/web/public/settings-ui.a191154e.js.gz +0 -0
  260. package/dist/web/public/styles.4f0c67a0.css +0 -1
  261. package/dist/web/public/styles.4f0c67a0.css.br +0 -0
  262. package/dist/web/public/styles.4f0c67a0.css.gz +0 -0
  263. package/dist/web/public/terminal-ui.7e787829.js.br +0 -0
  264. package/dist/web/public/terminal-ui.7e787829.js.gz +0 -0
@@ -44,6 +44,8 @@ import { looksLikeHostReboot, newestPersistedActivity, planRebootRestore } from
44
44
  import { rebootRestoreRegistry } from './reboot-restore-registry.js';
45
45
  import { dataPath, getDataDir, CODEMAN_INSTANCE } from '../config/instance.js';
46
46
  import { WebhookNotifier, readWebhookConfig } from '../webhook-notify.js';
47
+ import { DiscordBridge, screenTail } from '../discord-bridge.js';
48
+ import { isExternalCliTranscriptMode } from './response-viewer-transcript.js';
47
49
  import { stopHostSensors } from '../host-sensors.js';
48
50
  import { webviewFetch } from './webview-egress.js';
49
51
  import { readRemoteHosts, rehydrateRemoteHostFields } from '../remote-hosts.js';
@@ -66,8 +68,8 @@ import { attachmentRegistry, buildFileThumbnailRoute, registerExternalAttachment
66
68
  import { folderGrants } from '../folder-listing.js';
67
69
  import { getCli, enabledClis, listClis } from '../config/cli-registry/registry.js';
68
70
  import { isCliEntryInstalled, probeStockCliAvailability } from '../utils/cli-installed-probes.js';
69
- import { readCustomModelHosts } from '../custom-model-hosts.js';
70
- import { applyCustomModelInjection, customModelConfigDir, removeConfigDir } from '../custom-model-injection-apply.js';
71
+ import { readCustomModelHosts, resolveCustomModelContextLength } from '../custom-model-hosts.js';
72
+ import { applyCustomModelInjection, customModelConfigDir, removeConfigDir, } from '../custom-model-injection-apply.js';
71
73
  import { registerGeneratedArtifactAttachment } from '../generated-artifact-attachments.js';
72
74
  import { buildDetectedAttachmentHistoryItem, buildExternalAttachmentHistoryItem, } from '../session-attachment-history.js';
73
75
  import { TranscriptWatcher } from '../transcript-watcher.js';
@@ -99,6 +101,9 @@ import { probePathKind } from '../utils/bounded-path-probe.js';
99
101
  import { defaultDockerWorkspaces } from './routes/git-status-routes.js';
100
102
  import { composerIsEmpty, plainFrame } from '../session-submit-verifier.js';
101
103
  import { detectUsageLimitPause } from '../usage-limit-patterns.js';
104
+ import { EditHistorySync } from './edit-history-sync.js';
105
+ import { EditHistoryStore } from '../edit-history-store.js';
106
+ import { probeLocalPaths, readClaudeEditSources, resolveClaudeTranscript } from '../edit-history-local-source.js';
102
107
  import { stopDeepSeekWeb } from '../deepseek-web-server.js';
103
108
  import { wireRespawnListeners, setupTimedRespawn, restoreRespawnController, saveRespawnConfig, } from './respawn-event-wiring.js';
104
109
  import { reconcileUpdateOnBoot } from './self-update.js';
@@ -136,7 +141,7 @@ import { bootstrapInitialAdmin, hasUsers, resolveClaudeModeForUsername } from '.
136
141
  import { installRouteErrorHandler } from './route-error-handler.js';
137
142
  import { installEmptyApiPostParser } from './request-body-parser.js';
138
143
  import { isExplicitlyEnabled, isLoopbackBindHost, buildHostPolicy } from './network-auth-policy.js';
139
- import { registerPushRoutes, registerTeamRoutes, registerMuxRoutes, registerFileRoutes, registerHtmlViewRoutes, registerScheduledRoutes, registerHookEventRoutes, registerApprovalRoutes, registerInboxRoutes, registerRebootRestoreRoutes, registerReadMyMindRoutes, registerGitStatusRoutes, registerStatusTelemetryRoutes, registerSystemRoutes, registerCaseRoutes, registerWorktreeRoutes, registerSessionRoutes, registerAgentSessionRoutes, registerRespawnRoutes, registerRalphRoutes, registerPlanRoutes, registerClipboardRoutes, registerSearchRoutes, registerOrchestratorRoutes, registerCronRoutes, registerMeRoutes, registerAdminRoutes, registerWsRoutes, registerVoiceRoutes, registerWebviewRoutes, registerTabLayoutRoutes, registerMcpSyncRoutes, registerConfigBackupRoutes, runConfigBackup, registerWebhookRoutes, registerDoctorRoutes, registerCustomModelRoutes, refreshAllCustomModelHosts, readCustomModelEndpointsEnabled, closeAllLlamaSwapLogTails, detectCustomModelSwapDisplacements, pruneIdleLlamaSwapLogTails, tryWebviewRefererFallback, registerCliRegistryRoutes, } from './routes/index.js';
144
+ import { registerPushRoutes, registerTeamRoutes, registerMuxRoutes, registerFileRoutes, registerHtmlViewRoutes, registerScheduledRoutes, registerHookEventRoutes, registerApprovalRoutes, registerInboxRoutes, registerRebootRestoreRoutes, registerReadMyMindRoutes, registerGitStatusRoutes, registerEditHistoryRoutes, registerStatusTelemetryRoutes, registerSystemRoutes, registerCaseRoutes, registerWorktreeRoutes, registerSessionRoutes, registerAgentSessionRoutes, registerRespawnRoutes, registerRalphRoutes, registerPlanRoutes, registerClipboardRoutes, registerSearchRoutes, registerOrchestratorRoutes, registerCronRoutes, registerMeRoutes, registerAdminRoutes, registerWsRoutes, registerVoiceRoutes, registerWebviewRoutes, registerTabLayoutRoutes, registerMcpSyncRoutes, registerConfigBackupRoutes, runConfigBackup, registerWebhookRoutes, registerDiscordRoutes, registerDoctorRoutes, registerCustomModelRoutes, refreshAllCustomModelHosts, readCustomModelEndpointsEnabled, closeAllLlamaSwapLogTails, detectCustomModelSwapDisplacements, pruneIdleLlamaSwapLogTails, tryWebviewRefererFallback, registerCliRegistryRoutes, } from './routes/index.js';
140
145
  import { isLostWebviewFrameNavigation } from './webview-proxy.js';
141
146
  import { CronService } from '../cron/cron-service.js';
142
147
  import { caseSessionSettled } from '../case-rename.js';
@@ -205,6 +210,19 @@ function getOrCreateSelfSignedCert() {
205
210
  export class WebServer extends EventEmitter {
206
211
  app;
207
212
  sessions = new Map();
213
+ /**
214
+ * The edit history panel's reader (edit-history-sync.ts). Reads only when the panel asks;
215
+ * the main transcript is the one the transcript watcher follows (hook-reported path), or,
216
+ * before the first hook after a restart, a lookup under the session's claude config dir.
217
+ */
218
+ editHistorySync = new EditHistorySync({
219
+ transcriptPath: (session) => resolveClaudeTranscript(this.transcriptWatchers.get(session.id)?.getPath(), [session.claudeSessionId || session.id, session.id], session.claudeConfigDir),
220
+ readSources: readClaudeEditSources,
221
+ probe: probeLocalPaths,
222
+ loadStore: (sessionId) => EditHistoryStore.load(sessionId),
223
+ deleteStore: (sessionId) => EditHistoryStore.delete(sessionId),
224
+ sweepStores: (keep) => EditHistoryStore.sweep(keep),
225
+ });
208
226
  respawnControllers = new Map();
209
227
  respawnTimers = new Map();
210
228
  runSummaryTrackers = new Map();
@@ -351,6 +369,29 @@ export class WebServer extends EventEmitter {
351
369
  pushStore = new PushSubscriptionStore();
352
370
  /** ntfy / Slack / Discord / generic webhook for the push events; config in webhook.json (0600). */
353
371
  webhookNotifier = new WebhookNotifier(() => readWebhookConfig(getDataDir()), webviewFetch);
372
+ /** Two-way Discord bridge (thread per tab); config in discord.json (0600). Idle until enabled in Settings. */
373
+ /** Set by the session routes when they register (see SessionPort). */
374
+ lastResponseFor;
375
+ discordBridge = new DiscordBridge({
376
+ configDir: getDataDir(),
377
+ fetchImpl: webviewFetch,
378
+ sendInput: async (sessionId, text) => {
379
+ const session = this.sessions.get(sessionId);
380
+ if (!session)
381
+ return false;
382
+ // A Discord reply is a person's own input, so it names the tab like any other prompt.
383
+ if (await session.writeViaMux(text, { fromUser: true }).catch(() => false))
384
+ return true;
385
+ return session.write(text, { fromUser: true });
386
+ },
387
+ agentExited: (id) => !!this.sessions.get(id)?.paneExit,
388
+ awaitingChoice: (id) => approvalInbox.listPending().some((item) => item.sessionId === id && item.kind !== 'idle'),
389
+ // Remote (ssh) sessions keep their folder on another host, so received files have nowhere local to go.
390
+ sessionDir: (sessionId) => {
391
+ const session = this.sessions.get(sessionId);
392
+ return session && !session.remote ? session.workingDir : undefined;
393
+ },
394
+ });
354
395
  teamWatcher = new TeamWatcher();
355
396
  _orchestratorLoop = null;
356
397
  titleHostname;
@@ -682,6 +723,7 @@ export class WebServer extends EventEmitter {
682
723
  addSession: this.registerSessionWithLayout.bind(this),
683
724
  tabLayouts: this.tabLayouts,
684
725
  cleanupSession: this.cleanupSession.bind(this),
726
+ forgetEditHistory: this.forgetEditHistory.bind(this),
685
727
  setupSessionListeners: this.setupSessionListeners.bind(this),
686
728
  persistSessionState: this.persistSessionState.bind(this),
687
729
  persistSessionStateNow: this._persistSessionStateNow.bind(this),
@@ -1086,6 +1128,7 @@ export class WebServer extends EventEmitter {
1086
1128
  registerRebootRestoreRoutes(this.app, ctx);
1087
1129
  registerReadMyMindRoutes(this.app, ctx);
1088
1130
  registerGitStatusRoutes(this.app, ctx);
1131
+ registerEditHistoryRoutes(this.app, ctx, this.editHistorySync);
1089
1132
  // Git fetch on an interval: this is only the wake-up, the scheduler itself decides what is due. Off in
1090
1133
  // tests for the same reason as the polls below: a test drives `tick()` directly instead of waiting on
1091
1134
  // a timer the harness would have to keep alive. The setting is read FRESH inside `tick()`, so turning
@@ -1104,6 +1147,8 @@ export class WebServer extends EventEmitter {
1104
1147
  // The registry's lifetime is the server's: it drops per-session wake state on every
1105
1148
  // cleanup path and resolves in-flight wakes on shutdown.
1106
1149
  this.remoteWake = registerSessionRoutes(this.app, ctx);
1150
+ // The routes' ctx is a separate object: take the last-response reader they registered on it (Discord alerts).
1151
+ this.lastResponseFor = ctx.lastResponseFor;
1107
1152
  registerAgentSessionRoutes(this.app, ctx);
1108
1153
  registerRespawnRoutes(this.app, ctx);
1109
1154
  registerRalphRoutes(this.app, ctx);
@@ -1116,12 +1161,23 @@ export class WebServer extends EventEmitter {
1116
1161
  registerWebviewRoutes(this.app, ctx, this.basePath);
1117
1162
  registerTabLayoutRoutes(this.app, ctx);
1118
1163
  registerMcpSyncRoutes(this.app);
1119
- registerConfigBackupRoutes(this.app);
1164
+ registerConfigBackupRoutes(this.app, { reloadDiscord: () => this.discordBridge.reload() });
1120
1165
  registerWebhookRoutes(this.app, {
1121
1166
  notifier: this.webhookNotifier,
1122
1167
  configDir: getDataDir(),
1123
1168
  hostTitle: () => this.windowTitle,
1124
1169
  });
1170
+ registerDiscordRoutes(this.app, {
1171
+ bridge: this.discordBridge,
1172
+ configDir: getDataDir(),
1173
+ ctx,
1174
+ hostTitle: () => this.windowTitle,
1175
+ });
1176
+ if (!this.testMode) {
1177
+ // The gateway waits for restoreMuxSessions(): a reply during restore would be told the tab is gone.
1178
+ this.discordBridge.hold();
1179
+ void this.discordBridge.ensureLoaded().catch(() => undefined);
1180
+ }
1125
1181
  registerDoctorRoutes(this.app);
1126
1182
  registerCustomModelRoutes(this.app);
1127
1183
  registerCliRegistryRoutes(this.app);
@@ -1240,13 +1296,46 @@ export class WebServer extends EventEmitter {
1240
1296
  return undefined;
1241
1297
  }
1242
1298
  try {
1243
- return applyCustomModelInjection(entry, endpoint, saved.modelId, session.id, endpoint.modelContextLengths?.[saved.modelId])?.envOverrides;
1299
+ // Boot recovery omits both `workingDir` (no reason to re-answer a trust dialog that
1300
+ // already fired once) and the session-env real-home lookup: the injected VALUES are
1301
+ // never persisted, so after a real restart the session can no longer carry its
1302
+ // allowlisted `DSH_HOME` either (`getEnvOverridesForPersist()` keeps only
1303
+ // `CLAUDE_CODE_*`/`CLAUDE_CONFIG_DIR`): the lookup could only ever hit on the first
1304
+ // apply in-process. Real-home resolution therefore falls back to the SERVER's
1305
+ // `DSH_HOME` or the recipe's `realHomeFallback` (~/.dsh).
1306
+ return applyCustomModelInjection(entry, endpoint, saved.modelId, session.id, resolveCustomModelContextLength(endpoint, saved.modelId));
1244
1307
  }
1245
1308
  catch (err) {
1246
1309
  console.warn('[WebServer] Failed to rebuild custom-model env on recovery:', err);
1247
1310
  return undefined;
1248
1311
  }
1249
1312
  }
1313
+ /**
1314
+ * Restore one persisted custom-model selection (both boot-recovery paths). The rebuilt
1315
+ * bookkeeping must come from the CURRENT recipe, not from the saved record: this is the
1316
+ * first feature where a shipped recipe changed KIND (deepseek: env redirect -> per-session
1317
+ * patch home), so a selection saved by an old build restores with e.g. no `configDir`
1318
+ * while the new recipe writes one, and clearing it later could then never delete the
1319
+ * generated dir. `envKeys` is the UNION of saved and applied keys: the tmux session may
1320
+ * still carry `setenv`s from both, and a clear has to unset every one.
1321
+ */
1322
+ async _restoreCustomModel(session, saved) {
1323
+ const applied = await this._rebuildCustomModelEnv(session, saved);
1324
+ if (!applied) {
1325
+ // Endpoint gone or CLI unregistered: restore the bookkeeping exactly as saved, which
1326
+ // is what a later clear needs.
1327
+ session.setCustomModel(saved);
1328
+ return;
1329
+ }
1330
+ session.setCustomModel({
1331
+ ...saved,
1332
+ // Prefer the current recipe's dir; keep the saved one when the recipe writes none,
1333
+ // so a clear can still delete a directory an older recipe created.
1334
+ configDir: applied.configDir ?? saved.configDir,
1335
+ launchModel: applied.launchModel,
1336
+ envKeys: [...new Set([...saved.envKeys, ...applied.envKeys])],
1337
+ }, applied.envOverrides);
1338
+ }
1250
1339
  /** Persists full session state including respawn config to state.json */
1251
1340
  _persistSessionStateNow(session) {
1252
1341
  // See session-manager.updateSessionState: __envOverrides is an internal disk-only
@@ -1496,7 +1585,9 @@ export class WebServer extends EventEmitter {
1496
1585
  // Only remove from state.json if we're also killing the mux session.
1497
1586
  // When killMux=false (server shutdown), preserve state for recovery.
1498
1587
  if (killMux) {
1499
- this.store.demoteOrRemoveSession(sessionId);
1588
+ // The edit history goes with the record; a pinned session's demoted record keeps it.
1589
+ if (this.store.demoteOrRemoveSession(sessionId) !== 'preserved')
1590
+ this.forgetEditHistory(sessionId);
1500
1591
  }
1501
1592
  }
1502
1593
  // Release anything blocked on this session, in the documented order: 'exit'
@@ -1510,8 +1601,11 @@ export class WebServer extends EventEmitter {
1510
1601
  // detach: mail posted before that is still there when the session is re-attached
1511
1602
  // (while detached, the routes answer 404, so nothing new arrives). Before
1512
1603
  // cancelAll, so a reader released by it reads an empty inbox.
1513
- if (killMux)
1604
+ if (killMux) {
1514
1605
  agentInbox.drop(sessionId);
1606
+ // A real delete only: a detach (killMux=false) leaves the session running and re-attachable.
1607
+ void this.discordBridge.sessionDeleted(sessionId).catch(() => undefined);
1608
+ }
1515
1609
  sessionWaits.notifySignal(sessionId, 'exit');
1516
1610
  sessionWaits.cancelAll(sessionId);
1517
1611
  approvalInbox.resolveForSession(sessionId, 'session_ended');
@@ -2237,9 +2331,25 @@ export class WebServer extends EventEmitter {
2237
2331
  const lifecycleLog = getLifecycleLog();
2238
2332
  for (const s of result.cleaned) {
2239
2333
  lifecycleLog.log({ event: 'stale_cleaned', sessionId: s.id, name: s.name });
2334
+ this.forgetEditHistory(s.id);
2240
2335
  }
2336
+ // History files no session owns (a crash between removing a record and deleting its file).
2337
+ const exists = (id) => this.sessions.has(id) || !!this.store.getSession(id);
2338
+ await this.editHistorySync.sweep(exists).catch((err) => {
2339
+ console.warn('[EditHistory] Sweeping orphaned histories failed:', getErrorMessage(err));
2340
+ });
2241
2341
  return result.count;
2242
2342
  }
2343
+ /**
2344
+ * Delete a session's edit history (diffs can hold secrets) once its record is gone: deleted
2345
+ * live (`cleanupSession`), deleted as a record only (DELETE route), or cleaned as stale
2346
+ * (startup, `POST /api/cleanup-state`).
2347
+ */
2348
+ forgetEditHistory(sessionId) {
2349
+ void this.editHistorySync.remove(sessionId).catch((err) => {
2350
+ console.warn(`[EditHistory] Removing history of ${sessionId} failed:`, getErrorMessage(err));
2351
+ });
2352
+ }
2243
2353
  /**
2244
2354
  * Get lightweight state for SSE init - excludes full terminal buffers
2245
2355
  * to prevent browser freezes. Terminal buffers are fetched on-demand.
@@ -2340,6 +2450,23 @@ export class WebServer extends EventEmitter {
2340
2450
  // Multi-user: derive an ownership routing hint so an event only reaches the
2341
2451
  // clients entitled to it (no-op in single-user — hint stays undefined).
2342
2452
  this.sse.broadcast(event, data, isMultiUserMode() ? this.deriveSseHint(event, data) : undefined);
2453
+ this.bridgeSessionEvent(event, data);
2454
+ }
2455
+ /** Keep the Discord thread in step with the tab: created, renamed, deleted. Never throws. */
2456
+ bridgeSessionEvent(event, data) {
2457
+ // Deletion is NOT handled here: `session:deleted` also fires for a detach (killMux=false), where the
2458
+ // session keeps running and can be re-attached. The real delete calls the bridge from the cleanup path.
2459
+ if (event !== SseEvent.SessionCreated && event !== SseEvent.SessionUpdated)
2460
+ return;
2461
+ const d = (data ?? {});
2462
+ const s = d.session ?? d;
2463
+ if (!s.id)
2464
+ return;
2465
+ const id = s.id;
2466
+ const run = event === SseEvent.SessionCreated
2467
+ ? this.discordBridge.sessionCreated(id, s.name).then(() => this.discordBridge.sessionRenamed(id, s.name))
2468
+ : this.discordBridge.sessionRenamed(id, s.name);
2469
+ void run.catch(() => undefined);
2343
2470
  }
2344
2471
  /**
2345
2472
  * Map an SSE event + payload to a routing hint (multi-user). Session-scoped
@@ -2447,15 +2574,26 @@ export class WebServer extends EventEmitter {
2447
2574
  const getPaneExit = this.mux.getPaneExit?.bind(this.mux);
2448
2575
  if (!getPaneExit)
2449
2576
  return;
2577
+ const getPaneCurrentPath = this.mux.getPaneCurrentPath?.bind(this.mux);
2450
2578
  for (const session of this.sessions.values()) {
2451
2579
  const muxName = session.muxName;
2452
2580
  // No pane, so nothing to report — and `setPaneExit()` would force UNKNOWN
2453
2581
  // for such a session anyway.
2454
2582
  if (!muxName)
2455
2583
  continue;
2456
- if (!session.setPaneExit(getPaneExit(muxName)))
2584
+ // The pane's live directory rides the same read: it is the base a relative
2585
+ // path printed in the terminal resolves against. The manager's answer is
2586
+ // applied as given, `undefined` included: it keeps its map across a failed
2587
+ // or empty read and carries a blank `#{pane_current_path}` forward, so an
2588
+ // `undefined` here means an authoritative read listed no single directory
2589
+ // for the session (split into several panes, or gone from the socket).
2590
+ const paneCwdChanged = getPaneCurrentPath ? session.setPaneCwd(getPaneCurrentPath(muxName)) : false;
2591
+ if (session.setPaneExit(getPaneExit(muxName))) {
2592
+ this.persistSessionState(session);
2593
+ }
2594
+ else if (!paneCwdChanged) {
2457
2595
  continue;
2458
- this.persistSessionState(session);
2596
+ }
2459
2597
  this.broadcastSessionStateDebounced(session.id);
2460
2598
  }
2461
2599
  this.closeCleanlyExitedSessions();
@@ -2566,6 +2704,26 @@ export class WebServer extends EventEmitter {
2566
2704
  host: this.windowTitle,
2567
2705
  })
2568
2706
  .catch(() => undefined);
2707
+ // Discord: same event, same independence from Web Push. It adds the tail of what the CLI printed,
2708
+ // so the person reading the thread can see what is being asked without opening the tab.
2709
+ const dcSession = sessionId ? this.sessions.get(sessionId) : undefined;
2710
+ // A question or permission dialog lives on the screen, so it keeps the screen tail (its options are
2711
+ // there); a finished turn posts the CLI's whole last message instead, from its transcript.
2712
+ const dialogEvent = event === SseEvent.HookPermissionPrompt || event === SseEvent.HookElicitationDialog;
2713
+ void this.discordBridge
2714
+ .notify({
2715
+ event,
2716
+ title: template.title,
2717
+ body,
2718
+ urgency: template.urgency,
2719
+ sessionId: sessionId || undefined,
2720
+ sessionName: sessionName || undefined,
2721
+ // A function, so the screen is only read (a synchronous tmux exec) once the bridge has decided to post.
2722
+ tail: dcSession ? () => this.discordTail(dcSession, !dialogEvent) : undefined,
2723
+ fullText: dcSession && !dialogEvent ? () => this.discordFullText(dcSession) : undefined,
2724
+ host: this.windowTitle,
2725
+ })
2726
+ .catch(() => undefined);
2569
2727
  const subscriptions = this.pushStore.getAll();
2570
2728
  if (subscriptions.length === 0)
2571
2729
  return;
@@ -2640,6 +2798,33 @@ export class WebServer extends EventEmitter {
2640
2798
  });
2641
2799
  }
2642
2800
  }
2801
+ /**
2802
+ * What the person would see on the tab right now, for a Discord alert: the rendered tmux screen
2803
+ * (`capture-pane -p`), never the raw output stream, which a full-screen UI repaints in place and
2804
+ * reads as garbage. A session with no pane falls back to its plain text output, else nothing.
2805
+ */
2806
+ discordTail(session, cutComposer) {
2807
+ const pane = session.muxName ? (this.mux.capturePaneText?.(session.muxName) ?? null) : null;
2808
+ // The composer row and the status lines under it are screen chrome, not what the CLI said.
2809
+ const glyph = cutComposer ? getCli(session.mode)?.capabilities.workDetect?.promptGlyph : undefined;
2810
+ return screenTail(pane ?? session.textOutput ?? '', 14, 900, glyph);
2811
+ }
2812
+ /**
2813
+ * The CLI's whole last message from the response viewer's reader (the CLI's own transcript, or the pane
2814
+ * segmented into turns for the CLIs that keep none); undefined for a shell, which has no turns, and the
2815
+ * screen tail stands in whenever the reader finds nothing.
2816
+ */
2817
+ async discordFullText(session) {
2818
+ if (!this.lastResponseFor)
2819
+ return undefined;
2820
+ // A CLI with a transcript reader, or whose pane the viewer segments into turns; a shell has neither.
2821
+ if (getCli(session.mode)?.capabilities.transcript === 'none' && !isExternalCliTranscriptMode(session.mode)) {
2822
+ return undefined;
2823
+ }
2824
+ const r = await this.lastResponseFor(session);
2825
+ // The turn it came from: the transcript's stamp, else when the person last submitted (a pane has no stamp).
2826
+ return { text: r.text, version: r.timestamp || String(session.lastSubmitAt ?? '') };
2827
+ }
2643
2828
  /** The notification body for an event (shared by Web Push and the webhook channel). */
2644
2829
  static pushBodyText(event, data, sessionName) {
2645
2830
  let body = sessionName ? `[${sessionName}]` : '';
@@ -2779,6 +2964,21 @@ export class WebServer extends EventEmitter {
2779
2964
  console.log(`[AgentInbox] restored ${restoredMail} pending message(s)`);
2780
2965
  const restored = await this.restoreMuxSessions();
2781
2966
  await this.finalizeRestoredState(restored);
2967
+ void this.discordBridge
2968
+ .release()
2969
+ .then(async () => {
2970
+ // Only after a successful restore, like the agent inbox prune and the tab layout reconciliation, and
2971
+ // with the same keep set: a reboot-restore rebuild reuses the old id.
2972
+ if (!restored)
2973
+ return;
2974
+ await this.discordBridge.prune([
2975
+ ...this.sessions.keys(),
2976
+ ...Object.keys(this.store.getSessions()),
2977
+ ...rebootRestoreRegistry.list(() => true).map((entry) => entry.sessionId),
2978
+ ]);
2979
+ })
2980
+ .then(() => this.discordBridge.startStaleSweep())
2981
+ .catch(() => undefined);
2782
2982
  // Instance-scoped reaper: after restore, `docker rm -f` managed containers of
2783
2983
  // THIS instance whose case is gone from docker-cases.json (best-effort, never
2784
2984
  // touches another instance's containers). Runs after restore so containers
@@ -3155,7 +3355,7 @@ export class WebServer extends EventEmitter {
3155
3355
  // bookkeeping survives a restart and the values are re-derived here.
3156
3356
  const savedCustomModel = saved.__customModel;
3157
3357
  if (savedCustomModel) {
3158
- session.setCustomModel(savedCustomModel, await this._rebuildCustomModelEnv(session, savedCustomModel));
3358
+ await this._restoreCustomModel(session, savedCustomModel);
3159
3359
  }
3160
3360
  if (saved.niceEnabled !== undefined || saved.niceValue !== undefined) {
3161
3361
  session.setNice({ enabled: saved.niceEnabled, niceValue: saved.niceValue });
@@ -3293,6 +3493,9 @@ export class WebServer extends EventEmitter {
3293
3493
  catch (err) {
3294
3494
  console.warn(`[Server] releasing the tab layout slot failed: ${getErrorMessage(err)}`);
3295
3495
  }
3496
+ // Per-tab bridge state only: a reboot-restore rebuild reuses the old id and stays on offer for a
3497
+ // retry, so its thread must survive the discard.
3498
+ this.discordBridge.forgetTab(sessionId);
3296
3499
  // Any `session:updated` the half-built session emitted before it failed left a
3297
3500
  // tab on every other open board, and the client's handler is an upsert.
3298
3501
  this.broadcast(SseEvent.SessionDeleted, { id: sessionId });
@@ -3451,7 +3654,7 @@ export class WebServer extends EventEmitter {
3451
3654
  // the isolated config dir; an endpoint that has since been deleted still gets
3452
3655
  // the bookkeeping restored, which is what a later clear needs to unset.
3453
3656
  if (savedCustomModel) {
3454
- session.setCustomModel(savedCustomModel, await this._rebuildCustomModelEnv(session, savedCustomModel));
3657
+ await this._restoreCustomModel(session, savedCustomModel);
3455
3658
  }
3456
3659
  // Update session name if it was a "Restored:" placeholder or doesn't match saved name
3457
3660
  if (savedState?.name && muxSession.name !== savedState.name) {
@@ -3820,6 +4023,8 @@ export class WebServer extends EventEmitter {
3820
4023
  // Same teardown rule: the per-endpoint llama-swap log tails are otherwise closed
3821
4024
  // only by the periodic idle sweep, whose interval is disposed just below.
3822
4025
  closeAllLlamaSwapLogTails();
4026
+ // Close the Discord gateway socket and its heartbeat, and clear the bridge's per-tab maps.
4027
+ this.discordBridge.stop();
3823
4028
  // Dispose all managed timers (intervals + resettable timeouts)
3824
4029
  this.cleanup.dispose();
3825
4030
  // Gracefully close all SSE connections and clear batching state
@@ -3929,6 +4134,9 @@ export class WebServer extends EventEmitter {
3929
4134
  this.respawnTimers.clear();
3930
4135
  this.runSummaryTrackers.clear();
3931
4136
  this.transcriptWatchers.clear();
4137
+ await this.editHistorySync.flushAll().catch((err) => {
4138
+ console.warn('[EditHistory] Saving on shutdown failed:', getErrorMessage(err));
4139
+ });
3932
4140
  this.sessionListenerRefs.clear();
3933
4141
  this.scheduledRuns.clear();
3934
4142
  // Dispose StaleExpirationMaps (stops internal cleanup timers)