wind-agent-cli 1.37.2 → 1.37.3
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/LICENSE +718 -718
- package/README.md +1 -1
- package/dist/api-docs.html +901 -901
- package/dist/assets/{CodeEditor-B2Wf7Fg0.js → CodeEditor-BqmxpZ_D.js} +1 -1
- package/dist/assets/{FileTree-N2IkkHlv.js → FileTree-DLE7Ioai.js} +1 -1
- package/dist/assets/{GitPanel-DZCeuFZ2.js → GitPanel-f8RMRa8D.js} +1 -1
- package/dist/assets/{MarkdownPreview-C0tqYnVu.js → MarkdownPreview-93h_1tJa.js} +1 -1
- package/dist/assets/{PluginTabContent-Bnuf-fb8.js → PluginTabContent-DU1jjkXP.js} +1 -1
- package/dist/assets/{Shell-BfFnMZ-f.js → Shell-CN_2MRIM.js} +1 -1
- package/dist/assets/{StandaloneShell-QmyYwNAl.js → StandaloneShell-D0avS8UE.js} +1 -1
- package/dist/assets/{TaskMasterPanel-fMsmvN9L.js → TaskMasterPanel-Cpf1Q5Ph.js} +3 -3
- package/dist/assets/{abnfDiagram-N423BO3Z-DjPU7HLs.js → abnfDiagram-N423BO3Z-p3aJIm4T.js} +1 -1
- package/dist/assets/{arc-Pdq6Tvr3.js → arc-CTeud6Fl.js} +1 -1
- package/dist/assets/{architectureDiagram-T3A2C74G-D0JnT0Vh.js → architectureDiagram-T3A2C74G-CG7kRCJU.js} +1 -1
- package/dist/assets/{blockDiagram-VBNYF7ZC-DpaEf3mv.js → blockDiagram-VBNYF7ZC-oSjQ1SgE.js} +1 -1
- package/dist/assets/{c4Diagram-5PPSVZJV-MpqlPGFi.js → c4Diagram-5PPSVZJV-RkzF0DJV.js} +1 -1
- package/dist/assets/channel-BfQbarn9.js +1 -0
- package/dist/assets/{chunk-2GRJ4B5K-DoSdNo8u.js → chunk-2GRJ4B5K-B07uZ07O.js} +1 -1
- package/dist/assets/{chunk-2Q5K7J3B-CE7I3uMn.js → chunk-2Q5K7J3B-DNSfO7xS.js} +1 -1
- package/dist/assets/{chunk-5RXB4S5H-B4UQ8snD.js → chunk-5RXB4S5H-BTeYcgyb.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-B55XgqHZ.js → chunk-5VM5RSS4-B7vZV0zC.js} +1 -1
- package/dist/assets/{chunk-6Q2QTUOP-BS1KpefW.js → chunk-6Q2QTUOP-DuS_xUGG.js} +1 -1
- package/dist/assets/{chunk-GF5L2VYU-DkEPedJP.js → chunk-GF5L2VYU-CKLZ3iVD.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DOtkNhMm.js → chunk-JWPE2WC7-DD2hcXvs.js} +1 -1
- package/dist/assets/{chunk-KBJHAD2P-BXLjviqM.js → chunk-KBJHAD2P-Cz-l38qp.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-C50fjNNz.js → chunk-RYQCIY6F-DGFIU1YM.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-QBYqCkb7.js → chunk-XXDRQBXY-CmfzoYFl.js} +1 -1
- package/dist/assets/classDiagram-JCYQIIEL-Bpfe3kq7.js +1 -0
- package/dist/assets/classDiagram-v2-OCEON4UE-Bpfe3kq7.js +1 -0
- package/dist/assets/{code-xml-VK1YfShP.js → code-xml-CLpHzEOP.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-DrVTo1Nh.js → cose-bilkent-JH36ORCC-DlFcLXCB.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-DnJa_OlQ.js → cynefin-VYW2F7L2-P1HecReO.js} +1 -1
- package/dist/assets/{cynefinDiagram-MW4NZA55-CCxBjw9E.js → cynefinDiagram-MW4NZA55-COgduz5P.js} +1 -1
- package/dist/assets/{dagre-VZM6K2ZE-C_GvE4lu.js → dagre-VZM6K2ZE-Bbb-siGp.js} +1 -1
- package/dist/assets/{diagram-7IWD3JNH-pFzs9HEu.js → diagram-7IWD3JNH-9CWGFMYR.js} +1 -1
- package/dist/assets/{diagram-B4RE2ZJO-CcehQC7x.js → diagram-B4RE2ZJO-TWK59T6V.js} +1 -1
- package/dist/assets/{diagram-LBJQPF4R-hfKcrrxe.js → diagram-LBJQPF4R-ErMC9yaa.js} +1 -1
- package/dist/assets/{diagram-Q27KOJAE-C1semJfK.js → diagram-Q27KOJAE-Dt_-C7hV.js} +1 -1
- package/dist/assets/{diagram-UB23O5K3-57hvXQXO.js → diagram-UB23O5K3-BJ1DnRAF.js} +1 -1
- package/dist/assets/{ebnfDiagram-BXEA7PRR-9KIAlAVw.js → ebnfDiagram-BXEA7PRR-CE1YaQ9s.js} +1 -1
- package/dist/assets/{erDiagram-JOGREHBK-B7OzJ767.js → erDiagram-JOGREHBK-Bqk7N9aY.js} +1 -1
- package/dist/assets/{flowDiagram-UKHOOZJN-CJV8fQPR.js → flowDiagram-UKHOOZJN-DEr2II5V.js} +1 -1
- package/dist/assets/{ganttDiagram-PKOTCBZU-CbMNWWQu.js → ganttDiagram-PKOTCBZU-CWYAUeYN.js} +1 -1
- package/dist/assets/{gitGraphDiagram-DS77QQ5N-I_vZ5waO.js → gitGraphDiagram-DS77QQ5N-tdEcEIM6.js} +1 -1
- package/dist/assets/{index-ZHwIojzi.js → index-B-sTfSZq.js} +1 -1
- package/dist/assets/{index-GFhXZyl2.js → index-BSe3UiFA.js} +1 -1
- package/dist/assets/{index-DFnVoqpx.js → index-B_FDMWar.js} +5 -5
- package/dist/assets/{index-DlSeAkw_.js → index-CWJtg2pR.js} +1 -1
- package/dist/assets/{infoDiagram-6WML65LV-Dh20Tsbq.js → infoDiagram-6WML65LV-A3IqRUnO.js} +1 -1
- package/dist/assets/{ishikawaDiagram-WSZJBQD7-DH2Sm3ko.js → ishikawaDiagram-WSZJBQD7-B2uL6bkK.js} +1 -1
- package/dist/assets/{journeyDiagram-NVQOT4AX-BNFQqlVu.js → journeyDiagram-NVQOT4AX-D9JB-hRW.js} +1 -1
- package/dist/assets/{kanban-definition-27J2QSJJ-B3iL0ztJ.js → kanban-definition-27J2QSJJ-DmlgiV7K.js} +1 -1
- package/dist/assets/{linear-Cwe9dEKA.js → linear-Ct_eoJQk.js} +1 -1
- package/dist/assets/{mermaid.core-BYRQSshi.js → mermaid.core-CQ698HNn.js} +5 -5
- package/dist/assets/{mindmap-definition-FAOFIHXS-BgfBYMzr.js → mindmap-definition-FAOFIHXS-DGtMgQhf.js} +1 -1
- package/dist/assets/{pegDiagram-VL7TDLO6-CNYpECr8.js → pegDiagram-VL7TDLO6-CO6vP4n9.js} +1 -1
- package/dist/assets/{pieDiagram-7S7Q4E2Y-DeSrxc4I.js → pieDiagram-7S7Q4E2Y-DMkt_Zfr.js} +1 -1
- package/dist/assets/{quadrantDiagram-CIZ2JOQS-BXVVQsa6.js → quadrantDiagram-CIZ2JOQS-DgNxRz6t.js} +1 -1
- package/dist/assets/{railroadDiagram-AXF67PYL-JTroIW8F.js → railroadDiagram-AXF67PYL-DKz9UqA9.js} +1 -1
- package/dist/assets/{requirementDiagram-LRYGKXZP-C1Z_inLz.js → requirementDiagram-LRYGKXZP-L84XyGgV.js} +1 -1
- package/dist/assets/{sankeyDiagram-W5VNT64P-DCJgKFUk.js → sankeyDiagram-W5VNT64P-CQqWbJ3j.js} +1 -1
- package/dist/assets/{sequenceDiagram-SI44F4Z6-D8Wrx5Vc.js → sequenceDiagram-SI44F4Z6--tnzS2tb.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-CXqD68vD.js → sizeCapture-X5ZJPWSS-DujwEOtm.js} +1 -1
- package/dist/assets/{stateDiagram-OKZ733FA-CF0Ekqjo.js → stateDiagram-OKZ733FA-BK3TcZY2.js} +1 -1
- package/dist/assets/stateDiagram-v2-UEYNNEHI-_medyomK.js +1 -0
- package/dist/assets/{swimlanes-SLNWSIFB-DVIax_k4.js → swimlanes-SLNWSIFB-naX-Uv1B.js} +2 -2
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-DstIv3TC.js +8 -0
- package/dist/assets/{timeline-definition-Z64GVDOM-DSqk3ApG.js → timeline-definition-Z64GVDOM-C-vP_ddU.js} +1 -1
- package/dist/assets/{vennDiagram-T6HMQDX7-DNbTqyG5.js → vennDiagram-T6HMQDX7-DN-I7o-7.js} +1 -1
- package/dist/assets/{wardleyDiagram-T6FBY63Y-BNCpsFZV.js → wardleyDiagram-T6FBY63Y-Db9dsaXv.js} +1 -1
- package/dist/assets/{xychartDiagram-ELKLHX3M-XsVZphHr.js → xychartDiagram-ELKLHX3M-DE6OxW_p.js} +1 -1
- package/dist/clear-cache.html +85 -85
- package/dist/convert-icons.md +52 -52
- package/dist/favicon.svg +8 -8
- package/dist/generate-icons.js +48 -48
- package/dist/icons/codex-white.svg +3 -3
- package/dist/icons/codex.svg +3 -3
- package/dist/icons/cursor-white.svg +11 -11
- package/dist/icons/icon-128x128.svg +11 -11
- package/dist/icons/icon-144x144.svg +11 -11
- package/dist/icons/icon-152x152.svg +11 -11
- package/dist/icons/icon-192x192.svg +11 -11
- package/dist/icons/icon-384x384.svg +11 -11
- package/dist/icons/icon-512x512.svg +11 -11
- package/dist/icons/icon-72x72.svg +11 -11
- package/dist/icons/icon-96x96.svg +11 -11
- package/dist/icons/icon-template.svg +11 -11
- package/dist/index.html +56 -56
- package/dist/logo.svg +17 -17
- package/dist/manifest.json +60 -60
- package/dist/sw.js +124 -124
- package/dist-server/server/modules/cli/cli.service.js +36 -36
- package/dist-server/server/modules/cli/sandbox.service.js +38 -38
- package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js +74 -74
- package/dist-server/server/modules/commands/commands.routes.js +24 -24
- package/dist-server/server/modules/database/migrations.js +190 -190
- package/dist-server/server/modules/database/repositories/api-keys.js +3 -3
- package/dist-server/server/modules/database/repositories/github-tokens.js +2 -2
- package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js +28 -28
- package/dist-server/server/modules/database/repositories/notification-preferences.js +4 -4
- package/dist-server/server/modules/database/repositories/projects.db.js +61 -61
- package/dist-server/server/modules/database/repositories/provider-models.js +35 -35
- package/dist-server/server/modules/database/repositories/push-subscriptions.js +5 -5
- package/dist-server/server/modules/database/repositories/scan-state.db.js +5 -5
- package/dist-server/server/modules/database/repositories/sessions.db.js +92 -92
- package/dist-server/server/modules/database/schema.js +177 -177
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +6 -6
- package/dist-server/server/modules/git/git.routes.js +17 -17
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +12 -12
- package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +9 -9
- package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.test.js +17 -17
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +25 -25
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +27 -27
- package/dist-server/server/modules/providers/services/provider-token-usage.service.js +9 -9
- package/dist-server/server/modules/providers/tests/mcp.test.js +3 -3
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +96 -96
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +18 -18
- package/dist-server/server/modules/taskmaster/taskmaster.routes.js +419 -419
- package/electron/cloud.js +260 -260
- package/electron/desktopNotifications.js +378 -378
- package/electron/desktopWindow.js +766 -766
- package/electron/launcher/index.html +14 -14
- package/electron/launcher/launcher.css +801 -801
- package/electron/launcher/launcher.js +687 -687
- package/electron/localServer.js +549 -549
- package/electron/main.js +944 -944
- package/electron/preload.cjs +60 -60
- package/electron/scripts/generate-macos-icon.js +62 -62
- package/electron/serverInstaller.js +277 -277
- package/electron/tabs.js +87 -87
- package/electron/viewHost.js +331 -331
- package/package.json +250 -247
- package/public/api-docs.html +901 -901
- package/scripts/fix-node-pty.js +67 -67
- package/scripts/promote-dist-server.mjs +50 -50
- package/scripts/release/build-server-bundle.js +176 -176
- package/scripts/release/prepare-desktop-app.js +152 -152
- package/server/index.ts +399 -399
- package/server/load-env.ts +47 -47
- package/server/modules/agent/agent.module.ts +52 -52
- package/server/modules/agent/agent.routes.ts +1287 -1287
- package/server/modules/agent/index.ts +2 -2
- package/server/modules/agent/tests/agent.routes.test.ts +205 -205
- package/server/modules/assets/assets.routes.ts +150 -150
- package/server/modules/assets/index.ts +3 -3
- package/server/modules/assets/services/image-assets.service.ts +127 -127
- package/server/modules/assets/tests/image-assets.service.test.ts +74 -74
- package/server/modules/auth/auth.middleware.ts +163 -163
- package/server/modules/auth/auth.module.ts +38 -38
- package/server/modules/auth/auth.routes.ts +57 -57
- package/server/modules/auth/auth.service.ts +155 -155
- package/server/modules/auth/index.ts +9 -9
- package/server/modules/auth/tests/auth.service.test.ts +95 -95
- package/server/modules/browser-use/browser-use-mcp.routes.ts +120 -120
- package/server/modules/browser-use/browser-use-mcp.ts +386 -386
- package/server/modules/browser-use/browser-use-runtime.ts +10 -10
- package/server/modules/browser-use/browser-use.routes.ts +96 -96
- package/server/modules/browser-use/browser-use.service.ts +836 -836
- package/server/modules/browser-use/index.ts +11 -11
- package/server/modules/browser-use/tests/browser-use.service.test.ts +10 -10
- package/server/modules/cli/cli.module.ts +93 -93
- package/server/modules/cli/cli.service.ts +248 -248
- package/server/modules/cli/cli.ts +19 -19
- package/server/modules/cli/index.ts +2 -2
- package/server/modules/cli/sandbox.service.ts +398 -398
- package/server/modules/cli/tests/cli-environment-bootstrap.test.ts +186 -186
- package/server/modules/cli/tests/cli.service.test.ts +86 -86
- package/server/modules/cli/tests/sandbox.service.test.ts +58 -58
- package/server/modules/commands/commands.module.ts +22 -22
- package/server/modules/commands/commands.routes.ts +586 -586
- package/server/modules/commands/index.ts +2 -2
- package/server/modules/commands/tests/commands.test.ts +109 -109
- package/server/modules/database/connection.ts +143 -143
- package/server/modules/database/index.ts +17 -17
- package/server/modules/database/init-db.ts +17 -17
- package/server/modules/database/migrations.ts +515 -515
- package/server/modules/database/repositories/api-keys.ts +119 -119
- package/server/modules/database/repositories/app-config.ts +53 -53
- package/server/modules/database/repositories/credentials.ts +106 -106
- package/server/modules/database/repositories/github-tokens.ts +100 -100
- package/server/modules/database/repositories/notification-channel-endpoints.ts +153 -153
- package/server/modules/database/repositories/notification-preferences.ts +117 -117
- package/server/modules/database/repositories/projects.db.ts +196 -196
- package/server/modules/database/repositories/provider-models.ts +168 -168
- package/server/modules/database/repositories/push-subscriptions.ts +80 -80
- package/server/modules/database/repositories/scan-state.db.ts +42 -42
- package/server/modules/database/repositories/sessions.db.ts +524 -524
- package/server/modules/database/repositories/users.ts +140 -140
- package/server/modules/database/repositories/vapid-keys.ts +57 -57
- package/server/modules/database/schema.ts +210 -210
- package/server/modules/database/tests/projects.db.integration.test.ts +72 -72
- package/server/modules/database/tests/provider-models.db.integration.test.ts +125 -125
- package/server/modules/database/tests/sessions-provider-mapping.test.ts +109 -109
- package/server/modules/database/tests/sessions.db.integration.test.ts +116 -116
- package/server/modules/file-tree/file-tree.module.ts +116 -116
- package/server/modules/file-tree/file-tree.routes.ts +258 -258
- package/server/modules/file-tree/file-tree.service.ts +675 -675
- package/server/modules/file-tree/index.ts +2 -2
- package/server/modules/file-tree/tests/file-tree.routes.test.ts +157 -157
- package/server/modules/file-tree/tests/file-tree.service.test.ts +371 -371
- package/server/modules/git/git-branch.service.ts +43 -43
- package/server/modules/git/git-parsing.service.ts +66 -66
- package/server/modules/git/git.module.ts +22 -22
- package/server/modules/git/git.routes.ts +1596 -1596
- package/server/modules/git/index.ts +2 -2
- package/server/modules/git/tests/git-branch.service.test.ts +60 -60
- package/server/modules/git/tests/git-init.routes.test.ts +132 -132
- package/server/modules/git/tests/git.test.ts +106 -106
- package/server/modules/notifications/index.ts +24 -24
- package/server/modules/notifications/notifications.routes.ts +127 -127
- package/server/modules/notifications/services/desktop-notification-clients.service.ts +124 -124
- package/server/modules/notifications/services/notification-orchestrator.service.js +310 -310
- package/server/modules/notifications/tests/notification-orchestrator.integration.test.ts +55 -55
- package/server/modules/notifications/vapid-keys.service.ts +38 -38
- package/server/modules/notifications/websocket/desktop-notifications-websocket.service.ts +109 -109
- package/server/modules/plugins/index.ts +9 -9
- package/server/modules/plugins/plugin-process.service.ts +217 -217
- package/server/modules/plugins/plugin-registry.service.ts +459 -459
- package/server/modules/plugins/plugins.module.ts +33 -33
- package/server/modules/plugins/plugins.routes.ts +66 -66
- package/server/modules/plugins/plugins.service.ts +147 -147
- package/server/modules/plugins/tests/plugins.service.test.ts +32 -32
- package/server/modules/projects/index.ts +11 -11
- package/server/modules/projects/projects.routes.ts +273 -273
- package/server/modules/projects/services/project-clone.service.ts +323 -323
- package/server/modules/projects/services/project-delete.service.ts +90 -90
- package/server/modules/projects/services/project-management.service.ts +144 -144
- package/server/modules/projects/services/project-star.service.ts +78 -78
- package/server/modules/projects/services/projects-has-taskmaster.service.ts +248 -248
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +314 -314
- package/server/modules/projects/tests/project-clone.service.test.ts +183 -183
- package/server/modules/projects/tests/project-management.service.test.ts +117 -117
- package/server/modules/projects/tests/project-star.service.test.ts +123 -123
- package/server/modules/projects/tests/projects-has-taskmaster.service.test.ts +105 -105
- package/server/modules/providers/README.md +380 -380
- package/server/modules/providers/index.ts +10 -10
- package/server/modules/providers/list/claude/claude-auth.provider.ts +160 -160
- package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
- package/server/modules/providers/list/claude/claude-models.provider.ts +294 -294
- package/server/modules/providers/list/claude/claude-runtime.provider.js +1068 -1068
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +203 -203
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +684 -684
- package/server/modules/providers/list/claude/claude-skills.provider.ts +265 -265
- package/server/modules/providers/list/claude/claude.provider.ts +30 -30
- package/server/modules/providers/list/codex/codex-auth.provider.ts +100 -100
- package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
- package/server/modules/providers/list/codex/codex-models.provider.ts +123 -123
- package/server/modules/providers/list/codex/codex-runtime.provider.js +533 -533
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +203 -203
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +938 -938
- package/server/modules/providers/list/codex/codex-skills.provider.ts +73 -73
- package/server/modules/providers/list/codex/codex.provider.ts +28 -28
- package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -143
- package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
- package/server/modules/providers/list/cursor/cursor-models.provider.ts +119 -119
- package/server/modules/providers/list/cursor/cursor-runtime.provider.js +385 -385
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +159 -159
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +654 -654
- package/server/modules/providers/list/cursor/cursor-skills.provider.ts +39 -39
- package/server/modules/providers/list/cursor/cursor.provider.ts +30 -30
- package/server/modules/providers/list/opencode/opencode-auth.provider.ts +110 -110
- package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -228
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +203 -203
- package/server/modules/providers/list/opencode/opencode-runtime.provider.js +434 -434
- package/server/modules/providers/list/opencode/opencode-runtime.provider.test.js +261 -261
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +179 -179
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +500 -500
- package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -78
- package/server/modules/providers/list/opencode/opencode.provider.ts +30 -30
- package/server/modules/providers/provider.registry.ts +36 -36
- package/server/modules/providers/provider.routes.ts +877 -877
- package/server/modules/providers/services/mcp.service.ts +109 -109
- package/server/modules/providers/services/provider-auth.service.ts +26 -26
- package/server/modules/providers/services/provider-capabilities.service.ts +97 -97
- package/server/modules/providers/services/provider-models.service.ts +401 -401
- package/server/modules/providers/services/provider-runtime.service.ts +108 -108
- package/server/modules/providers/services/provider-token-usage.service.ts +357 -357
- package/server/modules/providers/services/session-conversations-search.service.ts +1321 -1321
- package/server/modules/providers/services/session-synchronizer.service.ts +74 -74
- package/server/modules/providers/services/sessions-watcher.service.ts +247 -247
- package/server/modules/providers/services/sessions.service.ts +471 -471
- package/server/modules/providers/services/skills.service.ts +39 -39
- package/server/modules/providers/shared/base/abstract.provider.ts +32 -32
- package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
- package/server/modules/providers/shared/skills/skills.provider.ts +287 -287
- package/server/modules/providers/tests/claude-auth.test.ts +150 -150
- package/server/modules/providers/tests/claude-sessions.test.ts +61 -61
- package/server/modules/providers/tests/codex-sessions.test.ts +231 -231
- package/server/modules/providers/tests/mcp.test.ts +349 -349
- package/server/modules/providers/tests/opencode-models.test.ts +41 -41
- package/server/modules/providers/tests/opencode-sessions.test.ts +522 -522
- package/server/modules/providers/tests/provider-attachment-history.test.ts +247 -247
- package/server/modules/providers/tests/provider-models.service.test.ts +353 -353
- package/server/modules/providers/tests/provider-runtime.service.test.ts +138 -138
- package/server/modules/providers/tests/provider-token-usage.service.test.ts +186 -186
- package/server/modules/providers/tests/provider.routes.test.ts +244 -244
- package/server/modules/providers/tests/sessions-details.test.ts +73 -73
- package/server/modules/providers/tests/sessions.service.test.ts +127 -127
- package/server/modules/providers/tests/skills.test.ts +692 -692
- package/server/modules/settings/index.ts +2 -2
- package/server/modules/settings/settings.module.ts +50 -50
- package/server/modules/settings/settings.routes.ts +51 -51
- package/server/modules/settings/settings.service.ts +186 -186
- package/server/modules/settings/tests/settings.service.test.ts +54 -54
- package/server/modules/system/index.ts +2 -2
- package/server/modules/system/system.module.ts +62 -62
- package/server/modules/system/system.routes.ts +21 -21
- package/server/modules/system/system.service.ts +76 -76
- package/server/modules/system/tests/system.service.test.ts +118 -118
- package/server/modules/taskmaster/index.ts +2 -2
- package/server/modules/taskmaster/taskmaster.module.ts +24 -24
- package/server/modules/taskmaster/taskmaster.routes.ts +1482 -1482
- package/server/modules/taskmaster/taskmaster.service.ts +132 -132
- package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +144 -144
- package/server/modules/taskmaster/tests/taskmaster.service.test.ts +97 -97
- package/server/modules/user/index.ts +2 -2
- package/server/modules/user/tests/user.service.test.ts +61 -61
- package/server/modules/user/user.module.ts +58 -58
- package/server/modules/user/user.routes.ts +50 -50
- package/server/modules/user/user.service.ts +85 -85
- package/server/modules/voice/index.ts +2 -2
- package/server/modules/voice/tests/voice.service.test.ts +98 -98
- package/server/modules/voice/voice.module.ts +47 -47
- package/server/modules/voice/voice.routes.ts +113 -113
- package/server/modules/voice/voice.service.ts +192 -192
- package/server/modules/websocket/README.md +273 -273
- package/server/modules/websocket/index.ts +3 -3
- package/server/modules/websocket/services/chat-run-registry.service.ts +343 -343
- package/server/modules/websocket/services/chat-session-writer.service.ts +145 -145
- package/server/modules/websocket/services/chat-websocket.service.ts +425 -425
- package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +65 -65
- package/server/modules/websocket/services/shell-websocket.service.ts +502 -502
- package/server/modules/websocket/services/websocket-auth.service.ts +59 -59
- package/server/modules/websocket/services/websocket-server.service.ts +124 -124
- package/server/modules/websocket/services/websocket-state.service.ts +16 -16
- package/server/modules/websocket/services/websocket-writer.service.ts +38 -38
- package/server/modules/websocket/tests/chat-attachment-filter.test.ts +59 -59
- package/server/modules/websocket/tests/chat-run-registry.test.ts +282 -282
- package/server/modules/websocket/tests/shell-websocket.service.test.ts +100 -100
- package/server/modules/websocket/tests/websocket-heartbeat.service.test.ts +78 -78
- package/server/modules/worktrees/index.ts +2 -2
- package/server/modules/worktrees/services/worktree-create-and-open.service.ts +53 -53
- package/server/modules/worktrees/services/worktree-create.service.ts +104 -104
- package/server/modules/worktrees/services/worktree-git.service.ts +207 -207
- package/server/modules/worktrees/services/worktree-list.service.ts +143 -143
- package/server/modules/worktrees/services/worktree-merge.service.ts +157 -157
- package/server/modules/worktrees/services/worktree-open.service.ts +85 -85
- package/server/modules/worktrees/services/worktree-remove.service.ts +88 -88
- package/server/modules/worktrees/tests/worktree-create-and-open.service.test.ts +68 -68
- package/server/modules/worktrees/tests/worktree-create.service.test.ts +142 -142
- package/server/modules/worktrees/tests/worktree-git.service.test.ts +84 -84
- package/server/modules/worktrees/tests/worktree-list.service.test.ts +45 -45
- package/server/modules/worktrees/tests/worktree-merge.service.test.ts +213 -213
- package/server/modules/worktrees/tests/worktree-open.service.test.ts +122 -122
- package/server/modules/worktrees/tests/worktree-remove.service.test.ts +227 -227
- package/server/modules/worktrees/tests/worktrees.routes.test.ts +199 -199
- package/server/modules/worktrees/worktrees.module.ts +113 -113
- package/server/modules/worktrees/worktrees.routes.ts +116 -116
- package/server/shared/claude-cli-path.ts +139 -139
- package/server/shared/frontmatter.ts +18 -18
- package/server/shared/image-attachments.ts +435 -435
- package/server/shared/interfaces.ts +179 -179
- package/server/shared/tests/claude-cli-path.test.ts +61 -61
- package/server/shared/tests/image-attachments.test.ts +353 -353
- package/server/shared/tests/slice-tail-page.test.ts +42 -42
- package/server/shared/types.ts +1226 -1226
- package/server/shared/utils.ts +1161 -1161
- package/server/tsconfig.json +40 -40
- package/shared/networkHosts.js +22 -22
- package/dist/assets/channel-Bm0ZLE4z.js +0 -1
- package/dist/assets/classDiagram-JCYQIIEL-DqEOcNxw.js +0 -1
- package/dist/assets/classDiagram-v2-OCEON4UE-DqEOcNxw.js +0 -1
- package/dist/assets/stateDiagram-v2-UEYNNEHI-DARLpEFJ.js +0 -1
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-CrgDgaY8.js +0 -8
|
@@ -1,425 +1,425 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
|
|
3
|
-
import type { WebSocket } from 'ws';
|
|
4
|
-
|
|
5
|
-
import { sessionsDb } from '@/modules/database/index.js';
|
|
6
|
-
import { providerModelsService } from '@/modules/providers/index.js';
|
|
7
|
-
import { chatRunRegistry } from '@/modules/websocket/services/chat-run-registry.service.js';
|
|
8
|
-
import { connectedClients, WS_OPEN_STATE } from '@/modules/websocket/services/websocket-state.service.js';
|
|
9
|
-
import {
|
|
10
|
-
getGlobalImageAssetsDir,
|
|
11
|
-
isImageAttachmentDescriptor,
|
|
12
|
-
normalizeAttachmentDescriptors,
|
|
13
|
-
type ChatAttachmentDescriptor,
|
|
14
|
-
} from '@/shared/image-attachments.js';
|
|
15
|
-
import type {
|
|
16
|
-
AnyRecord,
|
|
17
|
-
AuthenticatedWebSocketRequest,
|
|
18
|
-
LLMProvider,
|
|
19
|
-
ProviderPermissionDecision,
|
|
20
|
-
ProviderRuntimeWriter,
|
|
21
|
-
} from '@/shared/types.js';
|
|
22
|
-
import { parseIncomingJsonObject } from '@/shared/utils.js';
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Trust boundary for client-supplied image attachments: chat.send options come
|
|
26
|
-
* straight from the browser, and the provider runtimes read the referenced
|
|
27
|
-
* files off disk (Claude base64-encodes them into the prompt). Only images
|
|
28
|
-
* that live directly inside the global upload store (`~/.cloudcli/assets`,
|
|
29
|
-
* where POST /api/assets/images puts them) are allowed through — anything
|
|
30
|
-
* else (absolute paths elsewhere, traversal, subdirectories) is dropped.
|
|
31
|
-
*
|
|
32
|
-
* Exported for tests; `assetsRootOverride` exists only for them.
|
|
33
|
-
*/
|
|
34
|
-
export function filterAttachmentsToUploadStore(
|
|
35
|
-
attachments: unknown,
|
|
36
|
-
assetsRootOverride?: string,
|
|
37
|
-
): ChatAttachmentDescriptor[] {
|
|
38
|
-
const assetsRoot = path.resolve(assetsRootOverride ?? getGlobalImageAssetsDir());
|
|
39
|
-
|
|
40
|
-
return normalizeAttachmentDescriptors(attachments).filter((descriptor) => {
|
|
41
|
-
// Relative paths are anchored in the store; absolute ones must already be in it.
|
|
42
|
-
const resolved = path.resolve(assetsRoot, descriptor.path);
|
|
43
|
-
const relative = path.relative(assetsRoot, resolved);
|
|
44
|
-
const isDirectChild =
|
|
45
|
-
relative.length > 0 &&
|
|
46
|
-
!relative.startsWith('..') &&
|
|
47
|
-
!path.isAbsolute(relative) &&
|
|
48
|
-
!relative.includes(path.sep) &&
|
|
49
|
-
!relative.includes('/');
|
|
50
|
-
|
|
51
|
-
if (!isDirectChild) {
|
|
52
|
-
console.warn(`[Chat] Dropping attachment outside the upload store: ${descriptor.path}`);
|
|
53
|
-
}
|
|
54
|
-
return isDirectChild;
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Backward-compatible image filter consumed by existing websocket tests. */
|
|
59
|
-
export function filterImagesToUploadStore(
|
|
60
|
-
images: unknown,
|
|
61
|
-
assetsRootOverride?: string,
|
|
62
|
-
): ChatAttachmentDescriptor[] {
|
|
63
|
-
return filterAttachmentsToUploadStore(images, assetsRootOverride);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Application boundary for dispatching provider runs and approvals. */
|
|
67
|
-
type ProviderRuntimeGateway = {
|
|
68
|
-
hasRuntime(provider: string): boolean;
|
|
69
|
-
run(
|
|
70
|
-
provider: LLMProvider,
|
|
71
|
-
command: string,
|
|
72
|
-
options: AnyRecord,
|
|
73
|
-
writer: ProviderRuntimeWriter,
|
|
74
|
-
): Promise<unknown>;
|
|
75
|
-
abort(provider: LLMProvider, sessionId: string): Promise<boolean>;
|
|
76
|
-
resolveToolApproval(requestId: string, payload: ProviderPermissionDecision): void;
|
|
77
|
-
getPendingApprovalsForSession(sessionId: string): unknown[];
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
type ChatWebSocketDependencies = {
|
|
81
|
-
/** Central dispatcher for every provider SDK/CLI runtime. */
|
|
82
|
-
runtime: ProviderRuntimeGateway;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Extracts the authenticated request user id in the formats currently produced
|
|
87
|
-
* by platform and OSS auth code paths.
|
|
88
|
-
*/
|
|
89
|
-
function readRequestUserId(
|
|
90
|
-
request: AuthenticatedWebSocketRequest | undefined
|
|
91
|
-
): string | number | null {
|
|
92
|
-
const user = request?.user;
|
|
93
|
-
if (!user) {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (typeof user.id === 'string' || typeof user.id === 'number') {
|
|
98
|
-
return user.id;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (typeof user.userId === 'string' || typeof user.userId === 'number') {
|
|
102
|
-
return user.userId;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function sendJson(ws: WebSocket, payload: unknown): void {
|
|
109
|
-
if (ws.readyState === WS_OPEN_STATE) {
|
|
110
|
-
ws.send(JSON.stringify(payload));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Reports a protocol-level failure to the requesting client.
|
|
116
|
-
*
|
|
117
|
-
* Protocol errors deliberately use their own `kind` (instead of the provider
|
|
118
|
-
* `error` message kind) so the frontend can distinguish "your request was
|
|
119
|
-
* invalid" from "the model run produced an error" without inspecting text.
|
|
120
|
-
*/
|
|
121
|
-
function sendProtocolError(
|
|
122
|
-
ws: WebSocket,
|
|
123
|
-
code: string,
|
|
124
|
-
error: string,
|
|
125
|
-
sessionId?: string
|
|
126
|
-
): void {
|
|
127
|
-
sendJson(ws, {
|
|
128
|
-
kind: 'protocol_error',
|
|
129
|
-
code,
|
|
130
|
-
error,
|
|
131
|
-
sessionId: sessionId ?? null,
|
|
132
|
-
timestamp: new Date().toISOString(),
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function readRequiredSessionId(data: AnyRecord): string | null {
|
|
137
|
-
const sessionId = typeof data.sessionId === 'string' ? data.sessionId.trim() : '';
|
|
138
|
-
return sessionId.length > 0 ? sessionId : null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Handles `chat.send`: resolves the session row (provider, project path, and
|
|
143
|
-
* provider-native id all come from the database — never from the client),
|
|
144
|
-
* registers the run, and dispatches to the provider runtime.
|
|
145
|
-
*/
|
|
146
|
-
async function handleChatSend(
|
|
147
|
-
ws: WebSocket,
|
|
148
|
-
userId: string | number | null,
|
|
149
|
-
data: AnyRecord,
|
|
150
|
-
dependencies: ChatWebSocketDependencies
|
|
151
|
-
): Promise<void> {
|
|
152
|
-
const sessionId = readRequiredSessionId(data);
|
|
153
|
-
if (!sessionId) {
|
|
154
|
-
sendProtocolError(ws, 'SESSION_ID_REQUIRED', 'chat.send requires a sessionId.');
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const session = sessionsDb.getSessionById(sessionId);
|
|
159
|
-
if (!session) {
|
|
160
|
-
sendProtocolError(
|
|
161
|
-
ws,
|
|
162
|
-
'SESSION_NOT_FOUND',
|
|
163
|
-
`Session "${sessionId}" was not found. Create it via POST /api/providers/sessions first.`,
|
|
164
|
-
sessionId
|
|
165
|
-
);
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const provider = session.provider as LLMProvider;
|
|
170
|
-
if (!dependencies.runtime.hasRuntime(provider)) {
|
|
171
|
-
sendProtocolError(ws, 'UNSUPPORTED_PROVIDER', `Provider "${provider}" is not available.`, sessionId);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const run = chatRunRegistry.startRun({
|
|
176
|
-
appSessionId: sessionId,
|
|
177
|
-
provider,
|
|
178
|
-
providerSessionId: session.provider_session_id,
|
|
179
|
-
connection: ws,
|
|
180
|
-
userId,
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
if (!run) {
|
|
184
|
-
sendProtocolError(
|
|
185
|
-
ws,
|
|
186
|
-
'RUN_IN_PROGRESS',
|
|
187
|
-
`Session "${sessionId}" already has a run in progress.`,
|
|
188
|
-
sessionId
|
|
189
|
-
);
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const clientOptions = (data.options ?? {}) as AnyRecord;
|
|
194
|
-
const command = typeof data.content === 'string' ? data.content : '';
|
|
195
|
-
|
|
196
|
-
// Record what this turn runs with so reopening the session later restores the
|
|
197
|
-
// same model and reasoning effort, and so the resume path has a
|
|
198
|
-
// session-scoped model answer to use.
|
|
199
|
-
if (typeof clientOptions.model === 'string' && clientOptions.model.trim()) {
|
|
200
|
-
providerModelsService.setSessionModel(provider, sessionId, clientOptions.model);
|
|
201
|
-
}
|
|
202
|
-
if (typeof clientOptions.effort === 'string' && clientOptions.effort.trim()) {
|
|
203
|
-
providerModelsService.setSessionEffort(provider, sessionId, clientOptions.effort);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const attachmentCandidates = [
|
|
207
|
-
...normalizeAttachmentDescriptors(clientOptions.images),
|
|
208
|
-
...normalizeAttachmentDescriptors(clientOptions.files),
|
|
209
|
-
...normalizeAttachmentDescriptors(clientOptions.attachments),
|
|
210
|
-
];
|
|
211
|
-
const verifiedAttachments = filterAttachmentsToUploadStore(attachmentCandidates);
|
|
212
|
-
const uniqueAttachments = verifiedAttachments.filter(
|
|
213
|
-
(descriptor, index, all) => all.findIndex((candidate) => candidate.path === descriptor.path) === index,
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
// The provider runtimes receive the stable app session id. When their
|
|
217
|
-
// CLI/SDK needs the provider-native id for resume, they resolve it from the
|
|
218
|
-
// session row themselves (sessionsService.resolveProviderSessionId).
|
|
219
|
-
// Brand-new sessions have no provider id yet, so the runtime starts fresh
|
|
220
|
-
// and announces one, which the gateway writer captures and maps back to the
|
|
221
|
-
// app session id.
|
|
222
|
-
const runtimeOptions: AnyRecord = {
|
|
223
|
-
...clientOptions,
|
|
224
|
-
// Attachments are re-validated server-side: only direct children of the
|
|
225
|
-
// global upload store may reach provider runtimes or their file tools.
|
|
226
|
-
attachments: uniqueAttachments,
|
|
227
|
-
images: uniqueAttachments.filter(isImageAttachmentDescriptor),
|
|
228
|
-
files: uniqueAttachments.filter((descriptor) => !isImageAttachmentDescriptor(descriptor)),
|
|
229
|
-
sessionId,
|
|
230
|
-
cwd: clientOptions.cwd ?? session.project_path ?? undefined,
|
|
231
|
-
projectPath: session.project_path ?? clientOptions.projectPath,
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
await dependencies.runtime.run(provider, command, runtimeOptions, run.writer);
|
|
236
|
-
} catch (error) {
|
|
237
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
238
|
-
console.error(`[Chat] Provider runtime "${provider}" failed`, { sessionId, error: message });
|
|
239
|
-
} finally {
|
|
240
|
-
// Safety net: a runtime that crashed (or resolved) without emitting its
|
|
241
|
-
// terminal `complete` would otherwise leave the session stuck in
|
|
242
|
-
// "processing" forever on every connected client. Scoped to THIS run —
|
|
243
|
-
// a queued message can start the session's next run before this promise
|
|
244
|
-
// settles, and the session-keyed completeRun would kill that new run.
|
|
245
|
-
chatRunRegistry.completeRunIfCurrent(run, { exitCode: 1 });
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Handles `chat.abort`: cancels the run for one app session and emits the
|
|
251
|
-
* terminal `complete` on its behalf (runtimes skip their own complete for
|
|
252
|
-
* aborted runs, and the registry drops any duplicate).
|
|
253
|
-
*/
|
|
254
|
-
async function handleChatAbort(
|
|
255
|
-
ws: WebSocket,
|
|
256
|
-
data: AnyRecord,
|
|
257
|
-
dependencies: ChatWebSocketDependencies
|
|
258
|
-
): Promise<void> {
|
|
259
|
-
const sessionId = readRequiredSessionId(data);
|
|
260
|
-
if (!sessionId) {
|
|
261
|
-
sendProtocolError(ws, 'SESSION_ID_REQUIRED', 'chat.abort requires a sessionId.');
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const run = chatRunRegistry.getRun(sessionId);
|
|
266
|
-
if (!run || run.status !== 'running') {
|
|
267
|
-
sendProtocolError(ws, 'NO_ACTIVE_RUN', `Session "${sessionId}" has no active run.`, sessionId);
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const success = await dependencies.runtime.abort(run.provider, sessionId);
|
|
272
|
-
|
|
273
|
-
chatRunRegistry.completeRun(sessionId, {
|
|
274
|
-
exitCode: success ? 0 : 1,
|
|
275
|
-
aborted: true,
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Handles `chat.subscribe`: for each requested session, reports whether a run
|
|
281
|
-
* is processing, re-attaches the live stream to this socket, replays missed
|
|
282
|
-
* events (seq > lastSeq), and includes pending permission requests.
|
|
283
|
-
*
|
|
284
|
-
* This single message replaces the old `check-session-status`,
|
|
285
|
-
* `get-pending-permissions`, and Claude-only writer reconnect flows.
|
|
286
|
-
*/
|
|
287
|
-
function handleChatSubscribe(
|
|
288
|
-
ws: WebSocket,
|
|
289
|
-
data: AnyRecord,
|
|
290
|
-
dependencies: ChatWebSocketDependencies
|
|
291
|
-
): void {
|
|
292
|
-
const targets = Array.isArray(data.sessions) ? data.sessions : [];
|
|
293
|
-
|
|
294
|
-
for (const target of targets) {
|
|
295
|
-
if (!target || typeof target !== 'object') {
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const sessionId = typeof (target as AnyRecord).sessionId === 'string'
|
|
300
|
-
? ((target as AnyRecord).sessionId as string).trim()
|
|
301
|
-
: '';
|
|
302
|
-
if (!sessionId) {
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const lastSeqRaw = (target as AnyRecord).lastSeq;
|
|
307
|
-
const lastSeq = typeof lastSeqRaw === 'number' && Number.isFinite(lastSeqRaw)
|
|
308
|
-
? Math.max(0, Math.floor(lastSeqRaw))
|
|
309
|
-
: 0;
|
|
310
|
-
|
|
311
|
-
const run = chatRunRegistry.getRun(sessionId);
|
|
312
|
-
const isProcessing = chatRunRegistry.isProcessing(sessionId);
|
|
313
|
-
|
|
314
|
-
// Future live events for this run should land on the socket that asked —
|
|
315
|
-
// this is what makes mid-stream page refreshes work for all providers.
|
|
316
|
-
if (isProcessing) {
|
|
317
|
-
chatRunRegistry.attachConnection(sessionId, ws);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Pending approvals are tracked under the app session id inside the
|
|
321
|
-
// Claude runtime, so they can be looked up directly.
|
|
322
|
-
const pendingPermissions = dependencies.runtime.getPendingApprovalsForSession(sessionId);
|
|
323
|
-
|
|
324
|
-
sendJson(ws, {
|
|
325
|
-
kind: 'chat_subscribed',
|
|
326
|
-
sessionId,
|
|
327
|
-
isProcessing,
|
|
328
|
-
lastSeq: run?.lastSeq ?? 0,
|
|
329
|
-
pendingPermissions,
|
|
330
|
-
timestamp: new Date().toISOString(),
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
// Replay only for RUNNING runs, strictly after the ack. Completed runs
|
|
334
|
-
// are fully persisted to the provider transcript and served over REST —
|
|
335
|
-
// replaying them (e.g. after a page reload where the client's lastSeq is
|
|
336
|
-
// 0) would duplicate messages the history fetch already returned.
|
|
337
|
-
if (isProcessing) {
|
|
338
|
-
for (const event of chatRunRegistry.replayEvents(sessionId, lastSeq)) {
|
|
339
|
-
sendJson(ws, event);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Handles `chat.permission-response`: forwards a tool-approval decision to the
|
|
347
|
-
* pending approval resolver (Claude is the only provider with interactive
|
|
348
|
-
* approvals today, but the message is intentionally provider-neutral).
|
|
349
|
-
*/
|
|
350
|
-
function handlePermissionResponse(data: AnyRecord, dependencies: ChatWebSocketDependencies): void {
|
|
351
|
-
if (typeof data.requestId !== 'string' || data.requestId.length === 0) {
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
dependencies.runtime.resolveToolApproval(data.requestId, {
|
|
356
|
-
allow: Boolean(data.allow),
|
|
357
|
-
updatedInput: data.updatedInput,
|
|
358
|
-
message: typeof data.message === 'string' ? data.message : undefined,
|
|
359
|
-
rememberEntry: data.rememberEntry,
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Handles authenticated chat websocket messages used by the main chat panel.
|
|
365
|
-
*
|
|
366
|
-
* Inbound protocol (client to server):
|
|
367
|
-
* - `chat.send` { sessionId, content, options? }
|
|
368
|
-
* - `chat.abort` { sessionId }
|
|
369
|
-
* - `chat.subscribe` { sessions: [{ sessionId, lastSeq? }] }
|
|
370
|
-
* - `chat.permission-response` { requestId, allow, updatedInput?, message?, rememberEntry? }
|
|
371
|
-
*
|
|
372
|
-
* Outbound protocol (server to client): every frame is `kind`-based — either
|
|
373
|
-
* a provider `NormalizedMessage` (with `seq`) or a gateway event
|
|
374
|
-
* (`chat_subscribed`, `session_upserted`, `loading_progress`,
|
|
375
|
-
* `protocol_error`).
|
|
376
|
-
*/
|
|
377
|
-
export function handleChatConnection(
|
|
378
|
-
ws: WebSocket,
|
|
379
|
-
request: AuthenticatedWebSocketRequest,
|
|
380
|
-
dependencies: ChatWebSocketDependencies
|
|
381
|
-
): void {
|
|
382
|
-
console.log('[INFO] Chat WebSocket connected');
|
|
383
|
-
connectedClients.add(ws);
|
|
384
|
-
|
|
385
|
-
const userId = readRequestUserId(request);
|
|
386
|
-
|
|
387
|
-
ws.on('message', async (rawMessage) => {
|
|
388
|
-
try {
|
|
389
|
-
const parsed = parseIncomingJsonObject(rawMessage);
|
|
390
|
-
if (!parsed) {
|
|
391
|
-
throw new Error('Invalid websocket payload');
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const data = parsed as AnyRecord;
|
|
395
|
-
const messageType = typeof data.type === 'string' ? data.type : '';
|
|
396
|
-
|
|
397
|
-
switch (messageType) {
|
|
398
|
-
case 'chat.send':
|
|
399
|
-
await handleChatSend(ws, userId, data, dependencies);
|
|
400
|
-
return;
|
|
401
|
-
case 'chat.abort':
|
|
402
|
-
await handleChatAbort(ws, data, dependencies);
|
|
403
|
-
return;
|
|
404
|
-
case 'chat.subscribe':
|
|
405
|
-
handleChatSubscribe(ws, data, dependencies);
|
|
406
|
-
return;
|
|
407
|
-
case 'chat.permission-response':
|
|
408
|
-
handlePermissionResponse(data, dependencies);
|
|
409
|
-
return;
|
|
410
|
-
default:
|
|
411
|
-
sendProtocolError(ws, 'UNKNOWN_MESSAGE_TYPE', `Unknown message type "${messageType}".`);
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
} catch (error) {
|
|
415
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
416
|
-
console.error('[ERROR] Chat WebSocket error:', message);
|
|
417
|
-
sendProtocolError(ws, 'INTERNAL_ERROR', message);
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
ws.on('close', () => {
|
|
422
|
-
console.log('[INFO] Chat client disconnected');
|
|
423
|
-
connectedClients.delete(ws);
|
|
424
|
-
});
|
|
425
|
-
}
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import type { WebSocket } from 'ws';
|
|
4
|
+
|
|
5
|
+
import { sessionsDb } from '@/modules/database/index.js';
|
|
6
|
+
import { providerModelsService } from '@/modules/providers/index.js';
|
|
7
|
+
import { chatRunRegistry } from '@/modules/websocket/services/chat-run-registry.service.js';
|
|
8
|
+
import { connectedClients, WS_OPEN_STATE } from '@/modules/websocket/services/websocket-state.service.js';
|
|
9
|
+
import {
|
|
10
|
+
getGlobalImageAssetsDir,
|
|
11
|
+
isImageAttachmentDescriptor,
|
|
12
|
+
normalizeAttachmentDescriptors,
|
|
13
|
+
type ChatAttachmentDescriptor,
|
|
14
|
+
} from '@/shared/image-attachments.js';
|
|
15
|
+
import type {
|
|
16
|
+
AnyRecord,
|
|
17
|
+
AuthenticatedWebSocketRequest,
|
|
18
|
+
LLMProvider,
|
|
19
|
+
ProviderPermissionDecision,
|
|
20
|
+
ProviderRuntimeWriter,
|
|
21
|
+
} from '@/shared/types.js';
|
|
22
|
+
import { parseIncomingJsonObject } from '@/shared/utils.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Trust boundary for client-supplied image attachments: chat.send options come
|
|
26
|
+
* straight from the browser, and the provider runtimes read the referenced
|
|
27
|
+
* files off disk (Claude base64-encodes them into the prompt). Only images
|
|
28
|
+
* that live directly inside the global upload store (`~/.cloudcli/assets`,
|
|
29
|
+
* where POST /api/assets/images puts them) are allowed through — anything
|
|
30
|
+
* else (absolute paths elsewhere, traversal, subdirectories) is dropped.
|
|
31
|
+
*
|
|
32
|
+
* Exported for tests; `assetsRootOverride` exists only for them.
|
|
33
|
+
*/
|
|
34
|
+
export function filterAttachmentsToUploadStore(
|
|
35
|
+
attachments: unknown,
|
|
36
|
+
assetsRootOverride?: string,
|
|
37
|
+
): ChatAttachmentDescriptor[] {
|
|
38
|
+
const assetsRoot = path.resolve(assetsRootOverride ?? getGlobalImageAssetsDir());
|
|
39
|
+
|
|
40
|
+
return normalizeAttachmentDescriptors(attachments).filter((descriptor) => {
|
|
41
|
+
// Relative paths are anchored in the store; absolute ones must already be in it.
|
|
42
|
+
const resolved = path.resolve(assetsRoot, descriptor.path);
|
|
43
|
+
const relative = path.relative(assetsRoot, resolved);
|
|
44
|
+
const isDirectChild =
|
|
45
|
+
relative.length > 0 &&
|
|
46
|
+
!relative.startsWith('..') &&
|
|
47
|
+
!path.isAbsolute(relative) &&
|
|
48
|
+
!relative.includes(path.sep) &&
|
|
49
|
+
!relative.includes('/');
|
|
50
|
+
|
|
51
|
+
if (!isDirectChild) {
|
|
52
|
+
console.warn(`[Chat] Dropping attachment outside the upload store: ${descriptor.path}`);
|
|
53
|
+
}
|
|
54
|
+
return isDirectChild;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Backward-compatible image filter consumed by existing websocket tests. */
|
|
59
|
+
export function filterImagesToUploadStore(
|
|
60
|
+
images: unknown,
|
|
61
|
+
assetsRootOverride?: string,
|
|
62
|
+
): ChatAttachmentDescriptor[] {
|
|
63
|
+
return filterAttachmentsToUploadStore(images, assetsRootOverride);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Application boundary for dispatching provider runs and approvals. */
|
|
67
|
+
type ProviderRuntimeGateway = {
|
|
68
|
+
hasRuntime(provider: string): boolean;
|
|
69
|
+
run(
|
|
70
|
+
provider: LLMProvider,
|
|
71
|
+
command: string,
|
|
72
|
+
options: AnyRecord,
|
|
73
|
+
writer: ProviderRuntimeWriter,
|
|
74
|
+
): Promise<unknown>;
|
|
75
|
+
abort(provider: LLMProvider, sessionId: string): Promise<boolean>;
|
|
76
|
+
resolveToolApproval(requestId: string, payload: ProviderPermissionDecision): void;
|
|
77
|
+
getPendingApprovalsForSession(sessionId: string): unknown[];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type ChatWebSocketDependencies = {
|
|
81
|
+
/** Central dispatcher for every provider SDK/CLI runtime. */
|
|
82
|
+
runtime: ProviderRuntimeGateway;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extracts the authenticated request user id in the formats currently produced
|
|
87
|
+
* by platform and OSS auth code paths.
|
|
88
|
+
*/
|
|
89
|
+
function readRequestUserId(
|
|
90
|
+
request: AuthenticatedWebSocketRequest | undefined
|
|
91
|
+
): string | number | null {
|
|
92
|
+
const user = request?.user;
|
|
93
|
+
if (!user) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (typeof user.id === 'string' || typeof user.id === 'number') {
|
|
98
|
+
return user.id;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (typeof user.userId === 'string' || typeof user.userId === 'number') {
|
|
102
|
+
return user.userId;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function sendJson(ws: WebSocket, payload: unknown): void {
|
|
109
|
+
if (ws.readyState === WS_OPEN_STATE) {
|
|
110
|
+
ws.send(JSON.stringify(payload));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Reports a protocol-level failure to the requesting client.
|
|
116
|
+
*
|
|
117
|
+
* Protocol errors deliberately use their own `kind` (instead of the provider
|
|
118
|
+
* `error` message kind) so the frontend can distinguish "your request was
|
|
119
|
+
* invalid" from "the model run produced an error" without inspecting text.
|
|
120
|
+
*/
|
|
121
|
+
function sendProtocolError(
|
|
122
|
+
ws: WebSocket,
|
|
123
|
+
code: string,
|
|
124
|
+
error: string,
|
|
125
|
+
sessionId?: string
|
|
126
|
+
): void {
|
|
127
|
+
sendJson(ws, {
|
|
128
|
+
kind: 'protocol_error',
|
|
129
|
+
code,
|
|
130
|
+
error,
|
|
131
|
+
sessionId: sessionId ?? null,
|
|
132
|
+
timestamp: new Date().toISOString(),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readRequiredSessionId(data: AnyRecord): string | null {
|
|
137
|
+
const sessionId = typeof data.sessionId === 'string' ? data.sessionId.trim() : '';
|
|
138
|
+
return sessionId.length > 0 ? sessionId : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Handles `chat.send`: resolves the session row (provider, project path, and
|
|
143
|
+
* provider-native id all come from the database — never from the client),
|
|
144
|
+
* registers the run, and dispatches to the provider runtime.
|
|
145
|
+
*/
|
|
146
|
+
async function handleChatSend(
|
|
147
|
+
ws: WebSocket,
|
|
148
|
+
userId: string | number | null,
|
|
149
|
+
data: AnyRecord,
|
|
150
|
+
dependencies: ChatWebSocketDependencies
|
|
151
|
+
): Promise<void> {
|
|
152
|
+
const sessionId = readRequiredSessionId(data);
|
|
153
|
+
if (!sessionId) {
|
|
154
|
+
sendProtocolError(ws, 'SESSION_ID_REQUIRED', 'chat.send requires a sessionId.');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const session = sessionsDb.getSessionById(sessionId);
|
|
159
|
+
if (!session) {
|
|
160
|
+
sendProtocolError(
|
|
161
|
+
ws,
|
|
162
|
+
'SESSION_NOT_FOUND',
|
|
163
|
+
`Session "${sessionId}" was not found. Create it via POST /api/providers/sessions first.`,
|
|
164
|
+
sessionId
|
|
165
|
+
);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const provider = session.provider as LLMProvider;
|
|
170
|
+
if (!dependencies.runtime.hasRuntime(provider)) {
|
|
171
|
+
sendProtocolError(ws, 'UNSUPPORTED_PROVIDER', `Provider "${provider}" is not available.`, sessionId);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const run = chatRunRegistry.startRun({
|
|
176
|
+
appSessionId: sessionId,
|
|
177
|
+
provider,
|
|
178
|
+
providerSessionId: session.provider_session_id,
|
|
179
|
+
connection: ws,
|
|
180
|
+
userId,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (!run) {
|
|
184
|
+
sendProtocolError(
|
|
185
|
+
ws,
|
|
186
|
+
'RUN_IN_PROGRESS',
|
|
187
|
+
`Session "${sessionId}" already has a run in progress.`,
|
|
188
|
+
sessionId
|
|
189
|
+
);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const clientOptions = (data.options ?? {}) as AnyRecord;
|
|
194
|
+
const command = typeof data.content === 'string' ? data.content : '';
|
|
195
|
+
|
|
196
|
+
// Record what this turn runs with so reopening the session later restores the
|
|
197
|
+
// same model and reasoning effort, and so the resume path has a
|
|
198
|
+
// session-scoped model answer to use.
|
|
199
|
+
if (typeof clientOptions.model === 'string' && clientOptions.model.trim()) {
|
|
200
|
+
providerModelsService.setSessionModel(provider, sessionId, clientOptions.model);
|
|
201
|
+
}
|
|
202
|
+
if (typeof clientOptions.effort === 'string' && clientOptions.effort.trim()) {
|
|
203
|
+
providerModelsService.setSessionEffort(provider, sessionId, clientOptions.effort);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const attachmentCandidates = [
|
|
207
|
+
...normalizeAttachmentDescriptors(clientOptions.images),
|
|
208
|
+
...normalizeAttachmentDescriptors(clientOptions.files),
|
|
209
|
+
...normalizeAttachmentDescriptors(clientOptions.attachments),
|
|
210
|
+
];
|
|
211
|
+
const verifiedAttachments = filterAttachmentsToUploadStore(attachmentCandidates);
|
|
212
|
+
const uniqueAttachments = verifiedAttachments.filter(
|
|
213
|
+
(descriptor, index, all) => all.findIndex((candidate) => candidate.path === descriptor.path) === index,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// The provider runtimes receive the stable app session id. When their
|
|
217
|
+
// CLI/SDK needs the provider-native id for resume, they resolve it from the
|
|
218
|
+
// session row themselves (sessionsService.resolveProviderSessionId).
|
|
219
|
+
// Brand-new sessions have no provider id yet, so the runtime starts fresh
|
|
220
|
+
// and announces one, which the gateway writer captures and maps back to the
|
|
221
|
+
// app session id.
|
|
222
|
+
const runtimeOptions: AnyRecord = {
|
|
223
|
+
...clientOptions,
|
|
224
|
+
// Attachments are re-validated server-side: only direct children of the
|
|
225
|
+
// global upload store may reach provider runtimes or their file tools.
|
|
226
|
+
attachments: uniqueAttachments,
|
|
227
|
+
images: uniqueAttachments.filter(isImageAttachmentDescriptor),
|
|
228
|
+
files: uniqueAttachments.filter((descriptor) => !isImageAttachmentDescriptor(descriptor)),
|
|
229
|
+
sessionId,
|
|
230
|
+
cwd: clientOptions.cwd ?? session.project_path ?? undefined,
|
|
231
|
+
projectPath: session.project_path ?? clientOptions.projectPath,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
await dependencies.runtime.run(provider, command, runtimeOptions, run.writer);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
238
|
+
console.error(`[Chat] Provider runtime "${provider}" failed`, { sessionId, error: message });
|
|
239
|
+
} finally {
|
|
240
|
+
// Safety net: a runtime that crashed (or resolved) without emitting its
|
|
241
|
+
// terminal `complete` would otherwise leave the session stuck in
|
|
242
|
+
// "processing" forever on every connected client. Scoped to THIS run —
|
|
243
|
+
// a queued message can start the session's next run before this promise
|
|
244
|
+
// settles, and the session-keyed completeRun would kill that new run.
|
|
245
|
+
chatRunRegistry.completeRunIfCurrent(run, { exitCode: 1 });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Handles `chat.abort`: cancels the run for one app session and emits the
|
|
251
|
+
* terminal `complete` on its behalf (runtimes skip their own complete for
|
|
252
|
+
* aborted runs, and the registry drops any duplicate).
|
|
253
|
+
*/
|
|
254
|
+
async function handleChatAbort(
|
|
255
|
+
ws: WebSocket,
|
|
256
|
+
data: AnyRecord,
|
|
257
|
+
dependencies: ChatWebSocketDependencies
|
|
258
|
+
): Promise<void> {
|
|
259
|
+
const sessionId = readRequiredSessionId(data);
|
|
260
|
+
if (!sessionId) {
|
|
261
|
+
sendProtocolError(ws, 'SESSION_ID_REQUIRED', 'chat.abort requires a sessionId.');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const run = chatRunRegistry.getRun(sessionId);
|
|
266
|
+
if (!run || run.status !== 'running') {
|
|
267
|
+
sendProtocolError(ws, 'NO_ACTIVE_RUN', `Session "${sessionId}" has no active run.`, sessionId);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const success = await dependencies.runtime.abort(run.provider, sessionId);
|
|
272
|
+
|
|
273
|
+
chatRunRegistry.completeRun(sessionId, {
|
|
274
|
+
exitCode: success ? 0 : 1,
|
|
275
|
+
aborted: true,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Handles `chat.subscribe`: for each requested session, reports whether a run
|
|
281
|
+
* is processing, re-attaches the live stream to this socket, replays missed
|
|
282
|
+
* events (seq > lastSeq), and includes pending permission requests.
|
|
283
|
+
*
|
|
284
|
+
* This single message replaces the old `check-session-status`,
|
|
285
|
+
* `get-pending-permissions`, and Claude-only writer reconnect flows.
|
|
286
|
+
*/
|
|
287
|
+
function handleChatSubscribe(
|
|
288
|
+
ws: WebSocket,
|
|
289
|
+
data: AnyRecord,
|
|
290
|
+
dependencies: ChatWebSocketDependencies
|
|
291
|
+
): void {
|
|
292
|
+
const targets = Array.isArray(data.sessions) ? data.sessions : [];
|
|
293
|
+
|
|
294
|
+
for (const target of targets) {
|
|
295
|
+
if (!target || typeof target !== 'object') {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const sessionId = typeof (target as AnyRecord).sessionId === 'string'
|
|
300
|
+
? ((target as AnyRecord).sessionId as string).trim()
|
|
301
|
+
: '';
|
|
302
|
+
if (!sessionId) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const lastSeqRaw = (target as AnyRecord).lastSeq;
|
|
307
|
+
const lastSeq = typeof lastSeqRaw === 'number' && Number.isFinite(lastSeqRaw)
|
|
308
|
+
? Math.max(0, Math.floor(lastSeqRaw))
|
|
309
|
+
: 0;
|
|
310
|
+
|
|
311
|
+
const run = chatRunRegistry.getRun(sessionId);
|
|
312
|
+
const isProcessing = chatRunRegistry.isProcessing(sessionId);
|
|
313
|
+
|
|
314
|
+
// Future live events for this run should land on the socket that asked —
|
|
315
|
+
// this is what makes mid-stream page refreshes work for all providers.
|
|
316
|
+
if (isProcessing) {
|
|
317
|
+
chatRunRegistry.attachConnection(sessionId, ws);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Pending approvals are tracked under the app session id inside the
|
|
321
|
+
// Claude runtime, so they can be looked up directly.
|
|
322
|
+
const pendingPermissions = dependencies.runtime.getPendingApprovalsForSession(sessionId);
|
|
323
|
+
|
|
324
|
+
sendJson(ws, {
|
|
325
|
+
kind: 'chat_subscribed',
|
|
326
|
+
sessionId,
|
|
327
|
+
isProcessing,
|
|
328
|
+
lastSeq: run?.lastSeq ?? 0,
|
|
329
|
+
pendingPermissions,
|
|
330
|
+
timestamp: new Date().toISOString(),
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Replay only for RUNNING runs, strictly after the ack. Completed runs
|
|
334
|
+
// are fully persisted to the provider transcript and served over REST —
|
|
335
|
+
// replaying them (e.g. after a page reload where the client's lastSeq is
|
|
336
|
+
// 0) would duplicate messages the history fetch already returned.
|
|
337
|
+
if (isProcessing) {
|
|
338
|
+
for (const event of chatRunRegistry.replayEvents(sessionId, lastSeq)) {
|
|
339
|
+
sendJson(ws, event);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Handles `chat.permission-response`: forwards a tool-approval decision to the
|
|
347
|
+
* pending approval resolver (Claude is the only provider with interactive
|
|
348
|
+
* approvals today, but the message is intentionally provider-neutral).
|
|
349
|
+
*/
|
|
350
|
+
function handlePermissionResponse(data: AnyRecord, dependencies: ChatWebSocketDependencies): void {
|
|
351
|
+
if (typeof data.requestId !== 'string' || data.requestId.length === 0) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
dependencies.runtime.resolveToolApproval(data.requestId, {
|
|
356
|
+
allow: Boolean(data.allow),
|
|
357
|
+
updatedInput: data.updatedInput,
|
|
358
|
+
message: typeof data.message === 'string' ? data.message : undefined,
|
|
359
|
+
rememberEntry: data.rememberEntry,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Handles authenticated chat websocket messages used by the main chat panel.
|
|
365
|
+
*
|
|
366
|
+
* Inbound protocol (client to server):
|
|
367
|
+
* - `chat.send` { sessionId, content, options? }
|
|
368
|
+
* - `chat.abort` { sessionId }
|
|
369
|
+
* - `chat.subscribe` { sessions: [{ sessionId, lastSeq? }] }
|
|
370
|
+
* - `chat.permission-response` { requestId, allow, updatedInput?, message?, rememberEntry? }
|
|
371
|
+
*
|
|
372
|
+
* Outbound protocol (server to client): every frame is `kind`-based — either
|
|
373
|
+
* a provider `NormalizedMessage` (with `seq`) or a gateway event
|
|
374
|
+
* (`chat_subscribed`, `session_upserted`, `loading_progress`,
|
|
375
|
+
* `protocol_error`).
|
|
376
|
+
*/
|
|
377
|
+
export function handleChatConnection(
|
|
378
|
+
ws: WebSocket,
|
|
379
|
+
request: AuthenticatedWebSocketRequest,
|
|
380
|
+
dependencies: ChatWebSocketDependencies
|
|
381
|
+
): void {
|
|
382
|
+
console.log('[INFO] Chat WebSocket connected');
|
|
383
|
+
connectedClients.add(ws);
|
|
384
|
+
|
|
385
|
+
const userId = readRequestUserId(request);
|
|
386
|
+
|
|
387
|
+
ws.on('message', async (rawMessage) => {
|
|
388
|
+
try {
|
|
389
|
+
const parsed = parseIncomingJsonObject(rawMessage);
|
|
390
|
+
if (!parsed) {
|
|
391
|
+
throw new Error('Invalid websocket payload');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const data = parsed as AnyRecord;
|
|
395
|
+
const messageType = typeof data.type === 'string' ? data.type : '';
|
|
396
|
+
|
|
397
|
+
switch (messageType) {
|
|
398
|
+
case 'chat.send':
|
|
399
|
+
await handleChatSend(ws, userId, data, dependencies);
|
|
400
|
+
return;
|
|
401
|
+
case 'chat.abort':
|
|
402
|
+
await handleChatAbort(ws, data, dependencies);
|
|
403
|
+
return;
|
|
404
|
+
case 'chat.subscribe':
|
|
405
|
+
handleChatSubscribe(ws, data, dependencies);
|
|
406
|
+
return;
|
|
407
|
+
case 'chat.permission-response':
|
|
408
|
+
handlePermissionResponse(data, dependencies);
|
|
409
|
+
return;
|
|
410
|
+
default:
|
|
411
|
+
sendProtocolError(ws, 'UNKNOWN_MESSAGE_TYPE', `Unknown message type "${messageType}".`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
} catch (error) {
|
|
415
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
416
|
+
console.error('[ERROR] Chat WebSocket error:', message);
|
|
417
|
+
sendProtocolError(ws, 'INTERNAL_ERROR', message);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
ws.on('close', () => {
|
|
422
|
+
console.log('[INFO] Chat client disconnected');
|
|
423
|
+
connectedClients.delete(ws);
|
|
424
|
+
});
|
|
425
|
+
}
|