aicodeman 1.46.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.
- package/README.md +3 -0
- package/dist/cli-notify.d.ts +34 -0
- package/dist/cli-notify.d.ts.map +1 -0
- package/dist/cli-notify.js +100 -0
- package/dist/cli-notify.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +16 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/cli-registry/patterns.d.ts +1 -1
- package/dist/config/cli-registry/patterns.d.ts.map +1 -1
- package/dist/config/cli-registry/patterns.js +4 -0
- package/dist/config/cli-registry/patterns.js.map +1 -1
- package/dist/config/cli-registry/schema.d.ts +6 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +62 -2
- 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 +43 -26
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +47 -17
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/config/path-probe.d.ts +6 -0
- package/dist/config/path-probe.d.ts.map +1 -1
- package/dist/config/path-probe.js +6 -0
- package/dist/config/path-probe.js.map +1 -1
- package/dist/config-backup.d.ts +1 -1
- package/dist/config-backup.d.ts.map +1 -1
- package/dist/config-backup.js +2 -0
- package/dist/config-backup.js.map +1 -1
- package/dist/custom-model-hosts.d.ts +32 -8
- package/dist/custom-model-hosts.d.ts.map +1 -1
- package/dist/custom-model-hosts.js +10 -0
- package/dist/custom-model-hosts.js.map +1 -1
- package/dist/custom-model-injection-apply.d.ts +19 -5
- package/dist/custom-model-injection-apply.d.ts.map +1 -1
- package/dist/custom-model-injection-apply.js +88 -8
- package/dist/custom-model-injection-apply.js.map +1 -1
- package/dist/custom-model-injection.d.ts +6 -0
- package/dist/custom-model-injection.d.ts.map +1 -1
- package/dist/custom-model-injection.js +92 -10
- package/dist/custom-model-injection.js.map +1 -1
- package/dist/discord-bridge.d.ts +338 -0
- package/dist/discord-bridge.d.ts.map +1 -0
- package/dist/discord-bridge.js +1256 -0
- package/dist/discord-bridge.js.map +1 -0
- package/dist/edit-history-local-source.d.ts +83 -0
- package/dist/edit-history-local-source.d.ts.map +1 -0
- package/dist/edit-history-local-source.js +198 -0
- package/dist/edit-history-local-source.js.map +1 -0
- package/dist/edit-history-parse.d.ts +114 -0
- package/dist/edit-history-parse.d.ts.map +1 -0
- package/dist/edit-history-parse.js +312 -0
- package/dist/edit-history-parse.js.map +1 -0
- package/dist/edit-history-store.d.ts +209 -0
- package/dist/edit-history-store.d.ts.map +1 -0
- package/dist/edit-history-store.js +599 -0
- package/dist/edit-history-store.js.map +1 -0
- package/dist/git-clone.d.ts +6 -1
- package/dist/git-clone.d.ts.map +1 -1
- package/dist/git-clone.js +79 -14
- package/dist/git-clone.js.map +1 -1
- package/dist/image-watcher.js +4 -4
- package/dist/image-watcher.js.map +1 -1
- package/dist/mux-interface.d.ts +10 -1
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/ralph-fix-plan-watcher.d.ts +13 -1
- package/dist/ralph-fix-plan-watcher.d.ts.map +1 -1
- package/dist/ralph-fix-plan-watcher.js +21 -3
- package/dist/ralph-fix-plan-watcher.js.map +1 -1
- package/dist/ralph-tracker.d.ts +6 -0
- package/dist/ralph-tracker.d.ts.map +1 -1
- package/dist/ralph-tracker.js +8 -0
- package/dist/ralph-tracker.js.map +1 -1
- package/dist/session-cli-registry-bridge.d.ts.map +1 -1
- package/dist/session-cli-registry-bridge.js +4 -0
- package/dist/session-cli-registry-bridge.js.map +1 -1
- package/dist/session.d.ts +70 -3
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +147 -4
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts +40 -0
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +62 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/discord.d.ts +55 -0
- package/dist/types/discord.d.ts.map +1 -0
- package/dist/types/discord.js +18 -0
- package/dist/types/discord.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/session.d.ts +13 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/web/case-path.d.ts +2 -0
- package/dist/web/case-path.d.ts.map +1 -1
- package/dist/web/case-path.js +3 -3
- package/dist/web/case-path.js.map +1 -1
- package/dist/web/edit-history-sync.d.ts +158 -0
- package/dist/web/edit-history-sync.d.ts.map +1 -0
- package/dist/web/edit-history-sync.js +445 -0
- package/dist/web/edit-history-sync.js.map +1 -0
- package/dist/web/paste-image-gc.d.ts +6 -0
- package/dist/web/paste-image-gc.d.ts.map +1 -1
- package/dist/web/paste-image-gc.js +6 -0
- package/dist/web/paste-image-gc.js.map +1 -1
- package/dist/web/ports/session-port.d.ts +13 -0
- 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.3093527f.js → app.963b5ba8.js} +18 -18
- package/dist/web/public/app.963b5ba8.js.br +0 -0
- package/dist/web/public/app.963b5ba8.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.affcdde6.js → constants.782170e1.js} +114 -25
- package/dist/web/public/constants.782170e1.js.br +0 -0
- package/dist/web/public/constants.782170e1.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/edit-history-ui.js +630 -0
- package/dist/web/public/edit-history-ui.js.br +0 -0
- package/dist/web/public/edit-history-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/git-status-ui.js.gz +0 -0
- package/dist/web/public/header-divider.eb2d2463.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.33f930d7.js +2 -0
- package/dist/web/public/i18n.33f930d7.js.br +0 -0
- package/dist/web/public/i18n.33f930d7.js.gz +0 -0
- package/dist/web/public/{image-input.941386e7.js → image-input.a5d0f8f3.js} +18 -9
- package/dist/web/public/image-input.a5d0f8f3.js.br +0 -0
- package/dist/web/public/image-input.a5d0f8f3.js.gz +0 -0
- package/dist/web/public/index.html +162 -16
- 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.4e8b20ac.js → keyboard-accessory.d4ec5957.js} +77 -2
- package/dist/web/public/keyboard-accessory.d4ec5957.js.br +0 -0
- package/dist/web/public/keyboard-accessory.d4ec5957.js.gz +0 -0
- package/dist/web/public/mobile-handlers.f7c3f692.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.5ef2ce85.css +1 -0
- package/dist/web/public/mobile.5ef2ce85.css.br +0 -0
- package/dist/web/public/mobile.5ef2ce85.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.4a56ee63.js → panels-ui.b100727d.js} +2 -2
- package/dist/web/public/panels-ui.b100727d.js.br +0 -0
- package/dist/web/public/{panels-ui.4a56ee63.js.gz → panels-ui.b100727d.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.d7adcf6a.js → respawn-ui.304ba0d4.js} +1 -1
- package/dist/web/public/respawn-ui.304ba0d4.js.br +0 -0
- package/dist/web/public/respawn-ui.304ba0d4.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/security-notices.json +1 -21
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/{session-ui.186f9419.js → session-ui.5ac82f2c.js} +30 -30
- package/dist/web/public/session-ui.5ac82f2c.js.br +0 -0
- package/dist/web/public/session-ui.5ac82f2c.js.gz +0 -0
- package/dist/web/public/settings-ui.d1d4fb99.js +106 -0
- package/dist/web/public/settings-ui.d1d4fb99.js.br +0 -0
- package/dist/web/public/settings-ui.d1d4fb99.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
- package/dist/web/public/styles.77dc538f.css +1 -0
- package/dist/web/public/styles.77dc538f.css.br +0 -0
- package/dist/web/public/styles.77dc538f.css.gz +0 -0
- package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
- package/dist/web/public/sw.js +2 -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.gz +0 -0
- package/dist/web/public/tab-rail-resize.b5cdceaf.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.98843069.js +1 -0
- package/dist/web/public/terminal-keycode229-recovery.98843069.js.br +0 -0
- package/dist/web/public/terminal-keycode229-recovery.98843069.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +8 -6
- 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.7e787829.js → terminal-ui.d7c75ce8.js} +3 -3
- package/dist/web/public/terminal-ui.d7c75ce8.js.br +0 -0
- package/dist/web/public/terminal-ui.d7c75ce8.js.gz +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.276b11d9.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.1be42307.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.gz +0 -0
- package/dist/web/public/whats-new.json +9 -9
- package/dist/web/request-body-parser.d.ts +23 -0
- package/dist/web/request-body-parser.d.ts.map +1 -0
- package/dist/web/request-body-parser.js +57 -0
- package/dist/web/request-body-parser.js.map +1 -0
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +104 -8
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/config-backup-routes.d.ts +5 -1
- package/dist/web/routes/config-backup-routes.d.ts.map +1 -1
- package/dist/web/routes/config-backup-routes.js +4 -1
- package/dist/web/routes/config-backup-routes.js.map +1 -1
- package/dist/web/routes/custom-model-routes.d.ts +7 -2
- package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
- package/dist/web/routes/custom-model-routes.js +51 -6
- package/dist/web/routes/custom-model-routes.js.map +1 -1
- package/dist/web/routes/discord-routes.d.ts +25 -0
- package/dist/web/routes/discord-routes.d.ts.map +1 -0
- package/dist/web/routes/discord-routes.js +206 -0
- package/dist/web/routes/discord-routes.js.map +1 -0
- package/dist/web/routes/edit-history-routes.d.ts +43 -0
- package/dist/web/routes/edit-history-routes.d.ts.map +1 -0
- package/dist/web/routes/edit-history-routes.js +106 -0
- package/dist/web/routes/edit-history-routes.js.map +1 -0
- package/dist/web/routes/file-routes.d.ts +5 -0
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +67 -3
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/index.d.ts +2 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +2 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/ralph-routes.d.ts.map +1 -1
- package/dist/web/routes/ralph-routes.js +45 -2
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +17 -0
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +92 -37
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +4 -2
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +68 -1
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +77 -1
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/sensitive-path.d.ts.map +1 -1
- package/dist/web/sensitive-path.js +15 -0
- package/dist/web/sensitive-path.js.map +1 -1
- package/dist/web/server.d.ts +43 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +237 -15
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
- package/dist/web/public/app.3093527f.js.br +0 -0
- package/dist/web/public/app.3093527f.js.gz +0 -0
- package/dist/web/public/constants.affcdde6.js.br +0 -0
- package/dist/web/public/constants.affcdde6.js.gz +0 -0
- package/dist/web/public/i18n.70265257.js +0 -2
- package/dist/web/public/i18n.70265257.js.br +0 -0
- package/dist/web/public/i18n.70265257.js.gz +0 -0
- package/dist/web/public/image-input.941386e7.js.br +0 -0
- package/dist/web/public/image-input.941386e7.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.4e8b20ac.js.br +0 -0
- package/dist/web/public/keyboard-accessory.4e8b20ac.js.gz +0 -0
- package/dist/web/public/mobile.1bbd2d67.css +0 -1
- package/dist/web/public/mobile.1bbd2d67.css.br +0 -0
- package/dist/web/public/mobile.1bbd2d67.css.gz +0 -0
- package/dist/web/public/panels-ui.4a56ee63.js.br +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.br +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
- package/dist/web/public/session-ui.186f9419.js.br +0 -0
- package/dist/web/public/session-ui.186f9419.js.gz +0 -0
- package/dist/web/public/settings-ui.a191154e.js +0 -106
- package/dist/web/public/settings-ui.a191154e.js.br +0 -0
- package/dist/web/public/settings-ui.a191154e.js.gz +0 -0
- package/dist/web/public/styles.7238eae8.css +0 -1
- package/dist/web/public/styles.7238eae8.css.br +0 -0
- package/dist/web/public/styles.7238eae8.css.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js +0 -1
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.br +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
- package/dist/web/public/terminal-ui.7e787829.js.br +0 -0
- package/dist/web/public/terminal-ui.7e787829.js.gz +0 -0
package/dist/web/server.js
CHANGED
|
@@ -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';
|
|
@@ -134,8 +139,9 @@ import { registerAuthMiddleware, registerSecurityHeaders, registerHostGuard, isL
|
|
|
134
139
|
import { isMultiUserMode } from '../config/multiuser.js';
|
|
135
140
|
import { bootstrapInitialAdmin, hasUsers, resolveClaudeModeForUsername } from '../user-store.js';
|
|
136
141
|
import { installRouteErrorHandler } from './route-error-handler.js';
|
|
142
|
+
import { installEmptyApiPostParser } from './request-body-parser.js';
|
|
137
143
|
import { isExplicitlyEnabled, isLoopbackBindHost, buildHostPolicy } from './network-auth-policy.js';
|
|
138
|
-
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';
|
|
139
145
|
import { isLostWebviewFrameNavigation } from './webview-proxy.js';
|
|
140
146
|
import { CronService } from '../cron/cron-service.js';
|
|
141
147
|
import { caseSessionSettled } from '../case-rename.js';
|
|
@@ -204,6 +210,19 @@ function getOrCreateSelfSignedCert() {
|
|
|
204
210
|
export class WebServer extends EventEmitter {
|
|
205
211
|
app;
|
|
206
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
|
+
});
|
|
207
226
|
respawnControllers = new Map();
|
|
208
227
|
respawnTimers = new Map();
|
|
209
228
|
runSummaryTrackers = new Map();
|
|
@@ -350,6 +369,29 @@ export class WebServer extends EventEmitter {
|
|
|
350
369
|
pushStore = new PushSubscriptionStore();
|
|
351
370
|
/** ntfy / Slack / Discord / generic webhook for the push events; config in webhook.json (0600). */
|
|
352
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
|
+
});
|
|
353
395
|
teamWatcher = new TeamWatcher();
|
|
354
396
|
_orchestratorLoop = null;
|
|
355
397
|
titleHostname;
|
|
@@ -681,6 +723,7 @@ export class WebServer extends EventEmitter {
|
|
|
681
723
|
addSession: this.registerSessionWithLayout.bind(this),
|
|
682
724
|
tabLayouts: this.tabLayouts,
|
|
683
725
|
cleanupSession: this.cleanupSession.bind(this),
|
|
726
|
+
forgetEditHistory: this.forgetEditHistory.bind(this),
|
|
684
727
|
setupSessionListeners: this.setupSessionListeners.bind(this),
|
|
685
728
|
persistSessionState: this.persistSessionState.bind(this),
|
|
686
729
|
persistSessionStateNow: this._persistSessionStateNow.bind(this),
|
|
@@ -817,6 +860,8 @@ export class WebServer extends EventEmitter {
|
|
|
817
860
|
this.userFailures = authState.userFailures;
|
|
818
861
|
this.userIpFailures = authState.userIpFailures;
|
|
819
862
|
}
|
|
863
|
+
// Bodyless API POSTs may arrive as empty chunked streams through a proxy.
|
|
864
|
+
installEmptyApiPostParser(this.app);
|
|
820
865
|
// WebSocket support (terminal I/O — low-latency bidirectional channel)
|
|
821
866
|
await this.app.register(fastifyWebsocket);
|
|
822
867
|
// Multipart parsing (used by paste-image). Replaces a hand-rolled
|
|
@@ -1083,6 +1128,7 @@ export class WebServer extends EventEmitter {
|
|
|
1083
1128
|
registerRebootRestoreRoutes(this.app, ctx);
|
|
1084
1129
|
registerReadMyMindRoutes(this.app, ctx);
|
|
1085
1130
|
registerGitStatusRoutes(this.app, ctx);
|
|
1131
|
+
registerEditHistoryRoutes(this.app, ctx, this.editHistorySync);
|
|
1086
1132
|
// Git fetch on an interval: this is only the wake-up, the scheduler itself decides what is due. Off in
|
|
1087
1133
|
// tests for the same reason as the polls below: a test drives `tick()` directly instead of waiting on
|
|
1088
1134
|
// a timer the harness would have to keep alive. The setting is read FRESH inside `tick()`, so turning
|
|
@@ -1101,6 +1147,8 @@ export class WebServer extends EventEmitter {
|
|
|
1101
1147
|
// The registry's lifetime is the server's: it drops per-session wake state on every
|
|
1102
1148
|
// cleanup path and resolves in-flight wakes on shutdown.
|
|
1103
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;
|
|
1104
1152
|
registerAgentSessionRoutes(this.app, ctx);
|
|
1105
1153
|
registerRespawnRoutes(this.app, ctx);
|
|
1106
1154
|
registerRalphRoutes(this.app, ctx);
|
|
@@ -1113,12 +1161,23 @@ export class WebServer extends EventEmitter {
|
|
|
1113
1161
|
registerWebviewRoutes(this.app, ctx, this.basePath);
|
|
1114
1162
|
registerTabLayoutRoutes(this.app, ctx);
|
|
1115
1163
|
registerMcpSyncRoutes(this.app);
|
|
1116
|
-
registerConfigBackupRoutes(this.app);
|
|
1164
|
+
registerConfigBackupRoutes(this.app, { reloadDiscord: () => this.discordBridge.reload() });
|
|
1117
1165
|
registerWebhookRoutes(this.app, {
|
|
1118
1166
|
notifier: this.webhookNotifier,
|
|
1119
1167
|
configDir: getDataDir(),
|
|
1120
1168
|
hostTitle: () => this.windowTitle,
|
|
1121
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
|
+
}
|
|
1122
1181
|
registerDoctorRoutes(this.app);
|
|
1123
1182
|
registerCustomModelRoutes(this.app);
|
|
1124
1183
|
registerCliRegistryRoutes(this.app);
|
|
@@ -1237,13 +1296,46 @@ export class WebServer extends EventEmitter {
|
|
|
1237
1296
|
return undefined;
|
|
1238
1297
|
}
|
|
1239
1298
|
try {
|
|
1240
|
-
|
|
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));
|
|
1241
1307
|
}
|
|
1242
1308
|
catch (err) {
|
|
1243
1309
|
console.warn('[WebServer] Failed to rebuild custom-model env on recovery:', err);
|
|
1244
1310
|
return undefined;
|
|
1245
1311
|
}
|
|
1246
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
|
+
}
|
|
1247
1339
|
/** Persists full session state including respawn config to state.json */
|
|
1248
1340
|
_persistSessionStateNow(session) {
|
|
1249
1341
|
// See session-manager.updateSessionState: __envOverrides is an internal disk-only
|
|
@@ -1493,7 +1585,9 @@ export class WebServer extends EventEmitter {
|
|
|
1493
1585
|
// Only remove from state.json if we're also killing the mux session.
|
|
1494
1586
|
// When killMux=false (server shutdown), preserve state for recovery.
|
|
1495
1587
|
if (killMux) {
|
|
1496
|
-
|
|
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);
|
|
1497
1591
|
}
|
|
1498
1592
|
}
|
|
1499
1593
|
// Release anything blocked on this session, in the documented order: 'exit'
|
|
@@ -1507,8 +1601,11 @@ export class WebServer extends EventEmitter {
|
|
|
1507
1601
|
// detach: mail posted before that is still there when the session is re-attached
|
|
1508
1602
|
// (while detached, the routes answer 404, so nothing new arrives). Before
|
|
1509
1603
|
// cancelAll, so a reader released by it reads an empty inbox.
|
|
1510
|
-
if (killMux)
|
|
1604
|
+
if (killMux) {
|
|
1511
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
|
+
}
|
|
1512
1609
|
sessionWaits.notifySignal(sessionId, 'exit');
|
|
1513
1610
|
sessionWaits.cancelAll(sessionId);
|
|
1514
1611
|
approvalInbox.resolveForSession(sessionId, 'session_ended');
|
|
@@ -1777,12 +1874,23 @@ export class WebServer extends EventEmitter {
|
|
|
1777
1874
|
const summaryTracker = new RunSummaryTracker(session.id, session.name);
|
|
1778
1875
|
this.runSummaryTrackers.set(session.id, summaryTracker);
|
|
1779
1876
|
summaryTracker.recordSessionStarted(session.mode, session.workingDir);
|
|
1780
|
-
// Set working directory for Ralph tracker to auto-load @fix_plan.md (not supported for external CLIs)
|
|
1877
|
+
// Set working directory for Ralph tracker to auto-load @fix_plan.md (not supported for external CLIs).
|
|
1878
|
+
// A remote session's workingDir is a path on the remote host, and the watcher reads and
|
|
1879
|
+
// fs.watch()es it on the LOCAL disk, so a remote session only records the path: the fix plan stays
|
|
1880
|
+
// authoritative as for a local session (keeping the output-based completion guessers off), and its
|
|
1881
|
+
// file is read over ssh only on an explicit import (fix-plan/read).
|
|
1781
1882
|
if (!isExternalCliMode(session.mode)) {
|
|
1782
|
-
|
|
1883
|
+
if (session.remote) {
|
|
1884
|
+
session.ralphTracker.setRemoteWorkingDir(session.workingDir);
|
|
1885
|
+
}
|
|
1886
|
+
else {
|
|
1887
|
+
session.ralphTracker.setWorkingDir(session.workingDir);
|
|
1888
|
+
}
|
|
1783
1889
|
}
|
|
1784
|
-
// Start watching for new images in this session's working directory (if enabled globally and per-session)
|
|
1785
|
-
|
|
1890
|
+
// Start watching for new images in this session's working directory (if enabled globally and per-session).
|
|
1891
|
+
// A remote session's workingDir is a path on the remote host, and the watcher watches it on the LOCAL
|
|
1892
|
+
// disk, so it is skipped there (the per-session and app-wide toggles skip it too).
|
|
1893
|
+
if ((await this.isImageWatcherEnabled()) && session.imageWatcherEnabled && !session.remote) {
|
|
1786
1894
|
imageWatcher.watchSession(session.id, session.workingDir);
|
|
1787
1895
|
}
|
|
1788
1896
|
// Create and attach all listener handlers via dependency injection
|
|
@@ -2223,9 +2331,25 @@ export class WebServer extends EventEmitter {
|
|
|
2223
2331
|
const lifecycleLog = getLifecycleLog();
|
|
2224
2332
|
for (const s of result.cleaned) {
|
|
2225
2333
|
lifecycleLog.log({ event: 'stale_cleaned', sessionId: s.id, name: s.name });
|
|
2334
|
+
this.forgetEditHistory(s.id);
|
|
2226
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
|
+
});
|
|
2227
2341
|
return result.count;
|
|
2228
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
|
+
}
|
|
2229
2353
|
/**
|
|
2230
2354
|
* Get lightweight state for SSE init - excludes full terminal buffers
|
|
2231
2355
|
* to prevent browser freezes. Terminal buffers are fetched on-demand.
|
|
@@ -2326,6 +2450,23 @@ export class WebServer extends EventEmitter {
|
|
|
2326
2450
|
// Multi-user: derive an ownership routing hint so an event only reaches the
|
|
2327
2451
|
// clients entitled to it (no-op in single-user — hint stays undefined).
|
|
2328
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);
|
|
2329
2470
|
}
|
|
2330
2471
|
/**
|
|
2331
2472
|
* Map an SSE event + payload to a routing hint (multi-user). Session-scoped
|
|
@@ -2433,15 +2574,26 @@ export class WebServer extends EventEmitter {
|
|
|
2433
2574
|
const getPaneExit = this.mux.getPaneExit?.bind(this.mux);
|
|
2434
2575
|
if (!getPaneExit)
|
|
2435
2576
|
return;
|
|
2577
|
+
const getPaneCurrentPath = this.mux.getPaneCurrentPath?.bind(this.mux);
|
|
2436
2578
|
for (const session of this.sessions.values()) {
|
|
2437
2579
|
const muxName = session.muxName;
|
|
2438
2580
|
// No pane, so nothing to report — and `setPaneExit()` would force UNKNOWN
|
|
2439
2581
|
// for such a session anyway.
|
|
2440
2582
|
if (!muxName)
|
|
2441
2583
|
continue;
|
|
2442
|
-
|
|
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) {
|
|
2443
2595
|
continue;
|
|
2444
|
-
|
|
2596
|
+
}
|
|
2445
2597
|
this.broadcastSessionStateDebounced(session.id);
|
|
2446
2598
|
}
|
|
2447
2599
|
this.closeCleanlyExitedSessions();
|
|
@@ -2552,6 +2704,26 @@ export class WebServer extends EventEmitter {
|
|
|
2552
2704
|
host: this.windowTitle,
|
|
2553
2705
|
})
|
|
2554
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);
|
|
2555
2727
|
const subscriptions = this.pushStore.getAll();
|
|
2556
2728
|
if (subscriptions.length === 0)
|
|
2557
2729
|
return;
|
|
@@ -2626,6 +2798,33 @@ export class WebServer extends EventEmitter {
|
|
|
2626
2798
|
});
|
|
2627
2799
|
}
|
|
2628
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
|
+
}
|
|
2629
2828
|
/** The notification body for an event (shared by Web Push and the webhook channel). */
|
|
2630
2829
|
static pushBodyText(event, data, sessionName) {
|
|
2631
2830
|
let body = sessionName ? `[${sessionName}]` : '';
|
|
@@ -2765,6 +2964,21 @@ export class WebServer extends EventEmitter {
|
|
|
2765
2964
|
console.log(`[AgentInbox] restored ${restoredMail} pending message(s)`);
|
|
2766
2965
|
const restored = await this.restoreMuxSessions();
|
|
2767
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);
|
|
2768
2982
|
// Instance-scoped reaper: after restore, `docker rm -f` managed containers of
|
|
2769
2983
|
// THIS instance whose case is gone from docker-cases.json (best-effort, never
|
|
2770
2984
|
// touches another instance's containers). Runs after restore so containers
|
|
@@ -3141,7 +3355,7 @@ export class WebServer extends EventEmitter {
|
|
|
3141
3355
|
// bookkeeping survives a restart and the values are re-derived here.
|
|
3142
3356
|
const savedCustomModel = saved.__customModel;
|
|
3143
3357
|
if (savedCustomModel) {
|
|
3144
|
-
|
|
3358
|
+
await this._restoreCustomModel(session, savedCustomModel);
|
|
3145
3359
|
}
|
|
3146
3360
|
if (saved.niceEnabled !== undefined || saved.niceValue !== undefined) {
|
|
3147
3361
|
session.setNice({ enabled: saved.niceEnabled, niceValue: saved.niceValue });
|
|
@@ -3279,6 +3493,9 @@ export class WebServer extends EventEmitter {
|
|
|
3279
3493
|
catch (err) {
|
|
3280
3494
|
console.warn(`[Server] releasing the tab layout slot failed: ${getErrorMessage(err)}`);
|
|
3281
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);
|
|
3282
3499
|
// Any `session:updated` the half-built session emitted before it failed left a
|
|
3283
3500
|
// tab on every other open board, and the client's handler is an upsert.
|
|
3284
3501
|
this.broadcast(SseEvent.SessionDeleted, { id: sessionId });
|
|
@@ -3437,7 +3654,7 @@ export class WebServer extends EventEmitter {
|
|
|
3437
3654
|
// the isolated config dir; an endpoint that has since been deleted still gets
|
|
3438
3655
|
// the bookkeeping restored, which is what a later clear needs to unset.
|
|
3439
3656
|
if (savedCustomModel) {
|
|
3440
|
-
|
|
3657
|
+
await this._restoreCustomModel(session, savedCustomModel);
|
|
3441
3658
|
}
|
|
3442
3659
|
// Update session name if it was a "Restored:" placeholder or doesn't match saved name
|
|
3443
3660
|
if (savedState?.name && muxSession.name !== savedState.name) {
|
|
@@ -3806,6 +4023,8 @@ export class WebServer extends EventEmitter {
|
|
|
3806
4023
|
// Same teardown rule: the per-endpoint llama-swap log tails are otherwise closed
|
|
3807
4024
|
// only by the periodic idle sweep, whose interval is disposed just below.
|
|
3808
4025
|
closeAllLlamaSwapLogTails();
|
|
4026
|
+
// Close the Discord gateway socket and its heartbeat, and clear the bridge's per-tab maps.
|
|
4027
|
+
this.discordBridge.stop();
|
|
3809
4028
|
// Dispose all managed timers (intervals + resettable timeouts)
|
|
3810
4029
|
this.cleanup.dispose();
|
|
3811
4030
|
// Gracefully close all SSE connections and clear batching state
|
|
@@ -3915,6 +4134,9 @@ export class WebServer extends EventEmitter {
|
|
|
3915
4134
|
this.respawnTimers.clear();
|
|
3916
4135
|
this.runSummaryTrackers.clear();
|
|
3917
4136
|
this.transcriptWatchers.clear();
|
|
4137
|
+
await this.editHistorySync.flushAll().catch((err) => {
|
|
4138
|
+
console.warn('[EditHistory] Saving on shutdown failed:', getErrorMessage(err));
|
|
4139
|
+
});
|
|
3918
4140
|
this.sessionListenerRefs.clear();
|
|
3919
4141
|
this.scheduledRuns.clear();
|
|
3920
4142
|
// Dispose StaleExpirationMaps (stops internal cleanup timers)
|