@cloudcli-ai/cloudcli 1.37.3 → 1.37.4
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 +1 -1
- package/dist/assets/{abnfDiagram-N423BO3Z-CJntskwA.js → abnfDiagram-N423BO3Z-Dmkwx38H.js} +1 -1
- package/dist/assets/{arc-CIfH4uMN.js → arc-DLe9AYuO.js} +1 -1
- package/dist/assets/{architectureDiagram-T3A2C74G-_IpEkukg.js → architectureDiagram-T3A2C74G-CBbbbNUW.js} +1 -1
- package/dist/assets/{blockDiagram-VBNYF7ZC-BttxrpgZ.js → blockDiagram-VBNYF7ZC-CaMEaWu4.js} +1 -1
- package/dist/assets/{c4Diagram-5PPSVZJV-g51dSRr1.js → c4Diagram-5PPSVZJV-DhjMgNnR.js} +1 -1
- package/dist/assets/channel-CWrmwsir.js +1 -0
- package/dist/assets/{chunk-2GRJ4B5K-D_-LbQUa.js → chunk-2GRJ4B5K-yjl3-F-i.js} +1 -1
- package/dist/assets/{chunk-2Q5K7J3B-BtlHzwaO.js → chunk-2Q5K7J3B-DsAYaWEP.js} +1 -1
- package/dist/assets/{chunk-5RXB4S5H-UzkpvpAT.js → chunk-5RXB4S5H-Bt5qmqOX.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BKwx5Gag.js → chunk-5VM5RSS4-yWrWVvUt.js} +1 -1
- package/dist/assets/{chunk-6Q2QTUOP-D_KEBH5f.js → chunk-6Q2QTUOP-Dt66K5xb.js} +1 -1
- package/dist/assets/{chunk-GF5L2VYU-rK2DGyto.js → chunk-GF5L2VYU-DA0vzaj4.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CeBl_N9i.js → chunk-JWPE2WC7-CUkbx96V.js} +1 -1
- package/dist/assets/{chunk-KBJHAD2P-BiQLWVIr.js → chunk-KBJHAD2P-DWKxx1WH.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-C1knVGV5.js → chunk-RYQCIY6F-BqluplN0.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CtKbJkvF.js → chunk-XXDRQBXY-DelKg3Xf.js} +1 -1
- package/dist/assets/classDiagram-JCYQIIEL-COAGCC6w.js +1 -0
- package/dist/assets/classDiagram-v2-OCEON4UE-COAGCC6w.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-DjX8y40Z.js → cose-bilkent-JH36ORCC-DHV93-Em.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-DM6YeRba.js → cynefin-VYW2F7L2-B9HZgoGX.js} +1 -1
- package/dist/assets/{cynefinDiagram-MW4NZA55-wZJ-vy5B.js → cynefinDiagram-MW4NZA55-hFjjEQg8.js} +1 -1
- package/dist/assets/{dagre-VZM6K2ZE-BzP0ejkH.js → dagre-VZM6K2ZE-CpeVOC7V.js} +1 -1
- package/dist/assets/{diagram-7IWD3JNH-BTL_j23J.js → diagram-7IWD3JNH-D0H1UoRA.js} +1 -1
- package/dist/assets/{diagram-B4RE2ZJO-Cf1Norgu.js → diagram-B4RE2ZJO-6Y7BQUup.js} +1 -1
- package/dist/assets/{diagram-LBJQPF4R-DPiUEKr-.js → diagram-LBJQPF4R-C0nNOwht.js} +1 -1
- package/dist/assets/{diagram-Q27KOJAE-CTcTpiz5.js → diagram-Q27KOJAE-En6QlOVX.js} +1 -1
- package/dist/assets/{diagram-UB23O5K3-DPMSdYm0.js → diagram-UB23O5K3-CmPiNeGD.js} +1 -1
- package/dist/assets/{ebnfDiagram-BXEA7PRR-7yvD6uiF.js → ebnfDiagram-BXEA7PRR-Bygzp1Qt.js} +1 -1
- package/dist/assets/{erDiagram-JOGREHBK-CfgeDoV6.js → erDiagram-JOGREHBK-CwScRfzl.js} +1 -1
- package/dist/assets/{flowDiagram-UKHOOZJN-B27ng5zu.js → flowDiagram-UKHOOZJN-bKX-0xP6.js} +1 -1
- package/dist/assets/{ganttDiagram-PKOTCBZU-CcC6b_7t.js → ganttDiagram-PKOTCBZU-DtZPKXGd.js} +1 -1
- package/dist/assets/{gitGraphDiagram-DS77QQ5N-BX5kPGrO.js → gitGraphDiagram-DS77QQ5N-CUOtdL1o.js} +1 -1
- package/dist/assets/index-CdjIFxM2.js +5004 -0
- package/dist/assets/index-DgafhGTF.css +32 -0
- package/dist/assets/{infoDiagram-6WML65LV-CUP-SlN4.js → infoDiagram-6WML65LV-ciRQTS0h.js} +1 -1
- package/dist/assets/{ishikawaDiagram-WSZJBQD7-BTTDhOpX.js → ishikawaDiagram-WSZJBQD7-CgjhQZ84.js} +1 -1
- package/dist/assets/{journeyDiagram-NVQOT4AX-ARXcc61m.js → journeyDiagram-NVQOT4AX-BsbQSeIW.js} +1 -1
- package/dist/assets/{kanban-definition-27J2QSJJ-BieMpqMA.js → kanban-definition-27J2QSJJ-CrwzY9Jw.js} +1 -1
- package/dist/assets/{linear-61vxjRnP.js → linear-BUaCDr6l.js} +1 -1
- package/dist/assets/{mermaid.core-nhnwkfQx.js → mermaid.core-ClgdUU0j.js} +5 -5
- package/dist/assets/{mindmap-definition-FAOFIHXS-Cm6x5vT1.js → mindmap-definition-FAOFIHXS-BictljY1.js} +1 -1
- package/dist/assets/{pegDiagram-VL7TDLO6-CcYBq_9H.js → pegDiagram-VL7TDLO6-BXdByy_U.js} +1 -1
- package/dist/assets/{pieDiagram-7S7Q4E2Y-CfvIjfzo.js → pieDiagram-7S7Q4E2Y-uZsMfkRX.js} +1 -1
- package/dist/assets/{quadrantDiagram-CIZ2JOQS-SgYoWPNb.js → quadrantDiagram-CIZ2JOQS-7lCv4QIj.js} +1 -1
- package/dist/assets/{railroadDiagram-AXF67PYL-D2Mu5E_f.js → railroadDiagram-AXF67PYL-07dxIWbi.js} +1 -1
- package/dist/assets/{requirementDiagram-LRYGKXZP-Azwe_k4Q.js → requirementDiagram-LRYGKXZP-Cb31Yv1_.js} +1 -1
- package/dist/assets/{sankeyDiagram-W5VNT64P-DN1Ju3PA.js → sankeyDiagram-W5VNT64P-X0YPSyF9.js} +1 -1
- package/dist/assets/{sequenceDiagram-SI44F4Z6-Bj4qC34S.js → sequenceDiagram-SI44F4Z6-BdBxSIlP.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-Co4PdklG.js → sizeCapture-X5ZJPWSS-DQM5xAyD.js} +1 -1
- package/dist/assets/{stateDiagram-OKZ733FA-nOvL9dRC.js → stateDiagram-OKZ733FA-DJ3xkLUF.js} +1 -1
- package/dist/assets/{stateDiagram-v2-UEYNNEHI-DNv8ppw7.js → stateDiagram-v2-UEYNNEHI-vAaE4zNo.js} +1 -1
- package/dist/assets/{swimlanes-SLNWSIFB-CCK3wMid.js → swimlanes-SLNWSIFB-rjfYdW0w.js} +2 -2
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-TV2IF4Nq.js +8 -0
- package/dist/assets/{timeline-definition-Z64GVDOM-BC29RNFm.js → timeline-definition-Z64GVDOM-DZMra5do.js} +1 -1
- package/dist/assets/{vennDiagram-T6HMQDX7-pZlZCtEo.js → vennDiagram-T6HMQDX7-BevD8uu0.js} +1 -1
- package/dist/assets/{wardleyDiagram-T6FBY63Y-De8k2-zM.js → wardleyDiagram-T6FBY63Y-DsIyXB7z.js} +1 -1
- package/dist/assets/{xychartDiagram-ELKLHX3M-Bk8ykH0w.js → xychartDiagram-ELKLHX3M-G3PlF3ko.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +4 -1
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/agent/agent.module.js +9 -2
- package/dist-server/server/modules/agent/agent.module.js.map +1 -1
- package/dist-server/server/modules/agent/agent.routes.js +175 -128
- package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
- package/dist-server/server/modules/agent/tests/agent-run-registration.test.js +306 -0
- package/dist-server/server/modules/agent/tests/agent-run-registration.test.js.map +1 -0
- package/dist-server/server/modules/agent/tests/agent.routes.test.js +18 -1
- package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +13 -0
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/provider-models.js +60 -6
- package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -1
- package/dist-server/server/modules/database/repositories/sessions.db.js +7 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/schema.js +7 -0
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +99 -0
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -1
- package/dist-server/server/modules/file-tree/file-tree.module.js +2 -1
- package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
- package/dist-server/server/modules/file-tree/file-tree.service.js +44 -7
- package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +126 -1
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
- package/dist-server/server/modules/git/git-branch-diff.service.js +142 -0
- package/dist-server/server/modules/git/git-branch-diff.service.js.map +1 -0
- package/dist-server/server/modules/git/git-branch.service.js.map +1 -1
- package/dist-server/server/modules/git/git.routes.js +92 -5
- package/dist-server/server/modules/git/git.routes.js.map +1 -1
- package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js +160 -0
- package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/project-clone.routes.js +108 -0
- package/dist-server/server/modules/projects/project-clone.routes.js.map +1 -0
- package/dist-server/server/modules/projects/projects.routes.js +13 -66
- package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-clone-request.service.js +36 -0
- package/dist-server/server/modules/projects/services/project-clone-request.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-clone.service.js +86 -32
- package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
- package/dist-server/server/modules/projects/tests/project-clone.routes.test.js +191 -0
- package/dist-server/server/modules/projects/tests/project-clone.routes.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js +275 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +42 -8
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +6 -16
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +305 -16
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +38 -11
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +686 -77
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +31 -0
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +11 -4
- package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +57 -0
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +9 -2
- package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -2
- package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +16 -3
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
- package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +19 -2
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +110 -2
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js +100 -0
- package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js +97 -0
- package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js +9 -1
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +59 -5
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-runtime.service.js +8 -0
- package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +2 -4
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-history-cache.service.js +4 -0
- package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +73 -3
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-auth.test.js +131 -0
- package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-background-work.test.js +172 -0
- package/dist-server/server/modules/providers/tests/claude-background-work.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-compaction.test.js +84 -0
- package/dist-server/server/modules/providers/tests/claude-compaction.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-message-model.test.js +123 -0
- package/dist-server/server/modules/providers/tests/claude-message-model.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js +265 -0
- package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-sessions.test.js +1368 -12
- package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js +228 -0
- package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-models.test.js +25 -0
- package/dist-server/server/modules/providers/tests/codex-models.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-runtime.test.js +32 -0
- package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js +50 -0
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/cursor-auth.test.js +167 -0
- package/dist-server/server/modules/providers/tests/cursor-auth.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +79 -0
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +19 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js +30 -0
- package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +79 -1
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider.routes.test.js +190 -1
- package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +19 -0
- package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/sessions.service.test.js +175 -1
- package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
- package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +13 -1
- package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -1
- package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +36 -1
- package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +27 -5
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +38 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +230 -20
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js +137 -0
- package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js +96 -0
- package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +420 -0
- package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
- package/dist-server/server/shared/message-unification.js +1 -1
- package/dist-server/server/shared/message-unification.js.map +1 -1
- package/dist-server/server/shared/tests/read-only-roots.test.js +124 -0
- package/dist-server/server/shared/tests/read-only-roots.test.js.map +1 -0
- package/dist-server/server/shared/tests/strip-ansi-sequences.test.js +31 -0
- package/dist-server/server/shared/tests/strip-ansi-sequences.test.js.map +1 -0
- package/dist-server/server/shared/utils.js +126 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/electron/desktopWindow.js +6 -0
- package/electron/viewHost.js +25 -2
- package/package.json +4 -4
- package/server/index.ts +5 -1
- package/server/modules/agent/agent.module.ts +11 -1
- package/server/modules/agent/agent.routes.ts +198 -140
- package/server/modules/agent/tests/agent-run-registration.test.ts +348 -0
- package/server/modules/agent/tests/agent.routes.test.ts +18 -1
- package/server/modules/database/migrations.ts +14 -0
- package/server/modules/database/repositories/provider-models.ts +79 -6
- package/server/modules/database/repositories/sessions.db.ts +8 -0
- package/server/modules/database/schema.ts +7 -0
- package/server/modules/database/tests/provider-models.db.integration.test.ts +113 -0
- package/server/modules/file-tree/file-tree.module.ts +2 -1
- package/server/modules/file-tree/file-tree.service.ts +49 -7
- package/server/modules/file-tree/tests/file-tree.service.test.ts +163 -1
- package/server/modules/git/git-branch-diff.service.ts +204 -0
- package/server/modules/git/git-branch.service.ts +2 -12
- package/server/modules/git/git.routes.ts +100 -6
- package/server/modules/git/tests/git-branch-diff.service.test.ts +210 -0
- package/server/modules/projects/project-clone.routes.ts +142 -0
- package/server/modules/projects/projects.routes.ts +16 -81
- package/server/modules/projects/services/project-clone-request.service.ts +56 -0
- package/server/modules/projects/services/project-clone.service.ts +102 -34
- package/server/modules/projects/tests/project-clone.routes.test.ts +246 -0
- package/server/modules/projects/tests/project-clone.service.test.ts +376 -0
- package/server/modules/providers/list/claude/claude-auth.provider.ts +60 -10
- package/server/modules/providers/list/claude/claude-models.provider.ts +10 -22
- package/server/modules/providers/list/claude/claude-runtime.provider.js +322 -15
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +40 -14
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +796 -88
- package/server/modules/providers/list/codex/codex-models.provider.ts +31 -0
- package/server/modules/providers/list/codex/codex-runtime.provider.ts +12 -4
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +63 -0
- package/server/modules/providers/list/cursor/cursor-auth.provider.ts +10 -2
- package/server/modules/providers/list/cursor/cursor-runtime.provider.js +9 -2
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +18 -3
- package/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +30 -2
- package/server/modules/providers/provider.routes.ts +131 -2
- package/server/modules/providers/services/claude-cli-liveness.service.ts +121 -0
- package/server/modules/providers/services/claude-settings-permissions.service.ts +127 -0
- package/server/modules/providers/services/provider-capabilities.service.ts +17 -1
- package/server/modules/providers/services/provider-models.service.ts +71 -4
- package/server/modules/providers/services/provider-runtime.service.ts +11 -0
- package/server/modules/providers/services/session-conversations-search.service.ts +2 -5
- package/server/modules/providers/services/session-history-cache.service.ts +13 -2
- package/server/modules/providers/services/sessions.service.ts +108 -8
- package/server/modules/providers/tests/claude-auth.test.ts +150 -0
- package/server/modules/providers/tests/claude-background-work.test.ts +213 -0
- package/server/modules/providers/tests/claude-compaction.test.ts +101 -0
- package/server/modules/providers/tests/claude-message-model.test.ts +160 -0
- package/server/modules/providers/tests/claude-runtime-hold.test.ts +309 -0
- package/server/modules/providers/tests/claude-sessions.test.ts +1630 -12
- package/server/modules/providers/tests/claude-settings-permissions.test.ts +271 -0
- package/server/modules/providers/tests/codex-models.test.ts +37 -0
- package/server/modules/providers/tests/codex-runtime.test.ts +36 -0
- package/server/modules/providers/tests/codex-sessions.test.ts +59 -0
- package/server/modules/providers/tests/cursor-auth.test.ts +187 -0
- package/server/modules/providers/tests/opencode-models.test.ts +100 -0
- package/server/modules/providers/tests/opencode-sessions.test.ts +40 -1
- package/server/modules/providers/tests/provider-capabilities.service.test.ts +37 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +99 -1
- package/server/modules/providers/tests/provider.routes.test.ts +230 -1
- package/server/modules/providers/tests/session-history-cache.service.test.ts +22 -0
- package/server/modules/providers/tests/sessions.service.test.ts +237 -1
- package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +14 -1
- package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +45 -1
- package/server/modules/websocket/README.md +4 -2
- package/server/modules/websocket/services/chat-run-registry.service.ts +29 -5
- package/server/modules/websocket/services/chat-websocket.service.ts +58 -1
- package/server/modules/websocket/services/shell-websocket.service.ts +269 -22
- package/server/modules/websocket/tests/chat-background-subscribe.test.ts +167 -0
- package/server/modules/websocket/tests/chat-stop-task.test.ts +127 -0
- package/server/modules/websocket/tests/shell-websocket.service.test.ts +521 -0
- package/server/shared/interfaces.ts +29 -0
- package/server/shared/message-unification.ts +1 -1
- package/server/shared/tests/read-only-roots.test.ts +139 -0
- package/server/shared/tests/strip-ansi-sequences.test.ts +42 -0
- package/server/shared/types.ts +261 -7
- package/server/shared/utils.ts +136 -0
- package/dist/assets/channel-C5xU9jMw.js +0 -1
- package/dist/assets/classDiagram-JCYQIIEL-CRrL3vp7.js +0 -1
- package/dist/assets/classDiagram-v2-OCEON4UE-CRrL3vp7.js +0 -1
- package/dist/assets/index-BTDHQ6Id.css +0 -32
- package/dist/assets/index-sFfz8iBR.js +0 -4931
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-DnfbNQEp.js +0 -8
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import pty, { type IPty } from 'node-pty';
|
|
6
6
|
import { WebSocket, type RawData } from 'ws';
|
|
7
7
|
|
|
8
|
-
import { parseIncomingJsonObject } from '@/shared/utils.js';
|
|
8
|
+
import { parseIncomingJsonObject, stripAnsiSequences } from '@/shared/utils.js';
|
|
9
9
|
|
|
10
10
|
type ShellIncomingMessage = {
|
|
11
11
|
type?: string;
|
|
@@ -20,8 +20,11 @@ type ShellIncomingMessage = {
|
|
|
20
20
|
isPlainShell?: boolean;
|
|
21
21
|
forceRestart?: boolean;
|
|
22
22
|
bypassPermissions?: boolean;
|
|
23
|
+
colorScheme?: string;
|
|
23
24
|
};
|
|
24
25
|
|
|
26
|
+
type ShellColorScheme = 'light' | 'dark';
|
|
27
|
+
|
|
25
28
|
type PtySessionEntry = {
|
|
26
29
|
pty: IPty;
|
|
27
30
|
ws: WebSocket | null;
|
|
@@ -29,18 +32,17 @@ type PtySessionEntry = {
|
|
|
29
32
|
timeoutId: NodeJS.Timeout | null;
|
|
30
33
|
projectPath: string;
|
|
31
34
|
sessionId: string | null;
|
|
35
|
+
// The app theme a Claude CLI in this pty was launched for; null for any other
|
|
36
|
+
// program. A running CLI keeps its launch-time colours, so a reattaching
|
|
37
|
+
// client needs this, not its own current theme, to know if they differ.
|
|
38
|
+
claudeColorScheme: ShellColorScheme | null;
|
|
32
39
|
};
|
|
33
40
|
|
|
34
41
|
const ptySessionsMap = new Map<string, PtySessionEntry>();
|
|
35
42
|
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
36
43
|
const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
|
|
37
|
-
const ANSI_ESCAPE_SEQUENCE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
|
|
38
44
|
const TRAILING_URL_PUNCTUATION_REGEX = /[)\]}>.,;:!?]+$/;
|
|
39
45
|
|
|
40
|
-
function stripAnsiSequences(value: string): string {
|
|
41
|
-
return value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, '');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
46
|
function normalizeDetectedUrl(url: string): string | null {
|
|
45
47
|
const cleanedUrl = url.trim().replace(TRAILING_URL_PUNCTUATION_REGEX, '');
|
|
46
48
|
if (!cleanedUrl) {
|
|
@@ -90,6 +92,17 @@ function extractUrlsFromText(value: string): string[] {
|
|
|
90
92
|
return Array.from(new Set([...directMatches, ...wrappedMatches]));
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Tells the client which app theme the Claude CLI in its pty was launched for,
|
|
97
|
+
* so the Shell can suggest a restart once the app theme no longer matches it.
|
|
98
|
+
* Clients that predate this frame ignore unknown frame types.
|
|
99
|
+
*/
|
|
100
|
+
function sendClaudeColorScheme(ws: WebSocket, colorScheme: ShellColorScheme | null): void {
|
|
101
|
+
if (colorScheme && ws.readyState === WebSocket.OPEN) {
|
|
102
|
+
ws.send(JSON.stringify({ type: 'claude_theme', colorScheme }));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
93
106
|
function shouldAutoOpenUrlFromOutput(value: string): boolean {
|
|
94
107
|
const normalizedOutput = value.toLowerCase();
|
|
95
108
|
return (
|
|
@@ -130,6 +143,14 @@ function readNumber(value: unknown, fallback: number): number {
|
|
|
130
143
|
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
131
144
|
}
|
|
132
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Reads the app theme the Shell tab is painted in. Older clients send none and
|
|
148
|
+
* anything unexpected is ignored, which keeps the launch exactly as before.
|
|
149
|
+
*/
|
|
150
|
+
function readColorScheme(value: unknown): ShellColorScheme | null {
|
|
151
|
+
return value === 'light' || value === 'dark' ? value : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
133
154
|
/**
|
|
134
155
|
* Parses incoming websocket shell messages and keeps processing safe when
|
|
135
156
|
* malformed payloads are received.
|
|
@@ -173,13 +194,23 @@ function resolveResumeSessionId(
|
|
|
173
194
|
return resolvedSessionId;
|
|
174
195
|
}
|
|
175
196
|
|
|
197
|
+
type ShellLaunch = {
|
|
198
|
+
command: string;
|
|
199
|
+
/**
|
|
200
|
+
* Whether the command starts a Claude CLI that paints in the app's colour
|
|
201
|
+
* scheme, so a later theme toggle can be applied by restarting it.
|
|
202
|
+
*/
|
|
203
|
+
claudeFollowsAppTheme: boolean;
|
|
204
|
+
};
|
|
205
|
+
|
|
176
206
|
/**
|
|
177
207
|
* Resolves provider command line for plain shell and agent-backed shell modes.
|
|
178
208
|
*/
|
|
179
209
|
function buildShellCommand(
|
|
180
210
|
message: ShellIncomingMessage,
|
|
181
|
-
dependencies: ShellWebSocketDependencies
|
|
182
|
-
|
|
211
|
+
dependencies: ShellWebSocketDependencies,
|
|
212
|
+
projectPath: string
|
|
213
|
+
): ShellLaunch {
|
|
183
214
|
const hasSession = readBoolean(message.hasSession);
|
|
184
215
|
const initialCommand = readString(message.initialCommand);
|
|
185
216
|
const provider = readString(message.provider, 'claude');
|
|
@@ -189,32 +220,34 @@ function buildShellCommand(
|
|
|
189
220
|
(!!initialCommand && !hasSession) ||
|
|
190
221
|
provider === 'plain-shell';
|
|
191
222
|
|
|
223
|
+
const otherProgram = (command: string): ShellLaunch => ({ command, claudeFollowsAppTheme: false });
|
|
224
|
+
|
|
192
225
|
if (isPlainShell) {
|
|
193
|
-
return initialCommand;
|
|
226
|
+
return otherProgram(initialCommand);
|
|
194
227
|
}
|
|
195
228
|
|
|
196
229
|
if (provider === 'cursor') {
|
|
197
230
|
if (resumeSessionId) {
|
|
198
|
-
return `cursor-agent --resume="${resumeSessionId}"
|
|
231
|
+
return otherProgram(`cursor-agent --resume="${resumeSessionId}"`);
|
|
199
232
|
}
|
|
200
|
-
return 'cursor-agent';
|
|
233
|
+
return otherProgram('cursor-agent');
|
|
201
234
|
}
|
|
202
235
|
|
|
203
236
|
if (provider === 'codex') {
|
|
204
237
|
if (resumeSessionId) {
|
|
205
238
|
if (os.platform() === 'win32') {
|
|
206
|
-
return `codex resume "${resumeSessionId}"; if ($LASTEXITCODE -ne 0) { codex }
|
|
239
|
+
return otherProgram(`codex resume "${resumeSessionId}"; if ($LASTEXITCODE -ne 0) { codex }`);
|
|
207
240
|
}
|
|
208
|
-
return `codex resume "${resumeSessionId}" || codex
|
|
241
|
+
return otherProgram(`codex resume "${resumeSessionId}" || codex`);
|
|
209
242
|
}
|
|
210
|
-
return 'codex';
|
|
243
|
+
return otherProgram('codex');
|
|
211
244
|
}
|
|
212
245
|
|
|
213
246
|
if (provider === 'opencode') {
|
|
214
247
|
if (resumeSessionId) {
|
|
215
|
-
return `opencode --session "${resumeSessionId}"
|
|
248
|
+
return otherProgram(`opencode --session "${resumeSessionId}"`);
|
|
216
249
|
}
|
|
217
|
-
return initialCommand || 'opencode';
|
|
250
|
+
return otherProgram(initialCommand || 'opencode');
|
|
218
251
|
}
|
|
219
252
|
|
|
220
253
|
// Launching with the flag is what unlocks "bypass permissions" in the CLI's
|
|
@@ -223,14 +256,215 @@ function buildShellCommand(
|
|
|
223
256
|
const bypassFlag = readBoolean(message.bypassPermissions)
|
|
224
257
|
? ' --dangerously-skip-permissions'
|
|
225
258
|
: '';
|
|
226
|
-
const
|
|
259
|
+
const theme = buildClaudeThemeLaunch(readColorScheme(message.colorScheme), projectPath);
|
|
260
|
+
const launchFlags = `${bypassFlag}${theme.flag}`;
|
|
227
261
|
if (resumeSessionId) {
|
|
228
|
-
|
|
229
|
-
|
|
262
|
+
const command = os.platform() === 'win32'
|
|
263
|
+
? `claude --resume "${resumeSessionId}"${launchFlags}; if ($LASTEXITCODE -ne 0) { claude${launchFlags} }`
|
|
264
|
+
: `claude --resume "${resumeSessionId}"${launchFlags} || claude${launchFlags}`;
|
|
265
|
+
return { command, claudeFollowsAppTheme: theme.followsAppTheme };
|
|
266
|
+
}
|
|
267
|
+
if (initialCommand) {
|
|
268
|
+
// A caller's own command (e.g. `claude /login`) is launched as given.
|
|
269
|
+
return otherProgram(initialCommand);
|
|
270
|
+
}
|
|
271
|
+
return { command: `claude${launchFlags}`, claudeFollowsAppTheme: theme.followsAppTheme };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Claude Code's built-in theme ids (the `/theme` picker of CLI 2.1.280). A
|
|
275
|
+
// `custom:<slug>` theme is valid too; any other value is ignored by the CLI.
|
|
276
|
+
const CLAUDE_THEME_IDS = new Set([
|
|
277
|
+
'auto',
|
|
278
|
+
'dark',
|
|
279
|
+
'light',
|
|
280
|
+
'dark-daltonized',
|
|
281
|
+
'light-daltonized',
|
|
282
|
+
'dark-ansi',
|
|
283
|
+
'light-ansi',
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
// Dark themes and their light counterparts. A light theme, `auto` (it asks the
|
|
287
|
+
// terminal for its background, which xterm answers from the live theme) or a
|
|
288
|
+
// custom theme is the user's own choice and is left alone.
|
|
289
|
+
const LIGHT_CLAUDE_THEME_BY_DARK_THEME = new Map([
|
|
290
|
+
['dark', 'light'],
|
|
291
|
+
['dark-daltonized', 'light-daltonized'],
|
|
292
|
+
['dark-ansi', 'light-ansi'],
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
// The CLI refuses settings files over 2 MiB, so a bigger one holds no theme it
|
|
296
|
+
// would use. The legacy global config (`~/.claude.json`) is not a settings file
|
|
297
|
+
// and can grow to several MB; its cap is only there so a huge file is never
|
|
298
|
+
// pulled into the server's memory.
|
|
299
|
+
const MAX_CLAUDE_SETTINGS_FILE_BYTES = 2 * 1024 * 1024;
|
|
300
|
+
const MAX_CLAUDE_GLOBAL_CONFIG_FILE_BYTES = 64 * 1024 * 1024;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Reads a config file only when it is a regular, non-empty file of sane size,
|
|
304
|
+
* else returns null. The project can be a cloned repository whose
|
|
305
|
+
* `.claude/settings.local.json` is a symlink to a FIFO or to a device such as
|
|
306
|
+
* /dev/zero: reading one would stall the server's event loop or exhaust its
|
|
307
|
+
* memory. `statSync` follows the symlink without opening the target. Empty
|
|
308
|
+
* files are skipped too: they hold no theme, and kernel pseudo-files (procfs,
|
|
309
|
+
* tracefs) report a size of 0 even when a read would block.
|
|
310
|
+
*/
|
|
311
|
+
function readRegularConfigFile(filePath: string, maxBytes = MAX_CLAUDE_SETTINGS_FILE_BYTES): string | null {
|
|
312
|
+
try {
|
|
313
|
+
const stats = fs.statSync(filePath);
|
|
314
|
+
if (!stats.isFile() || stats.size === 0 || stats.size > maxBytes) {
|
|
315
|
+
return null;
|
|
230
316
|
}
|
|
231
|
-
return
|
|
317
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
318
|
+
} catch {
|
|
319
|
+
return null;
|
|
232
320
|
}
|
|
233
|
-
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Reads the `theme` key of one Claude config file. A missing, unreadable,
|
|
325
|
+
* special (FIFO, device) or unparsable file, or a value the CLI would reject,
|
|
326
|
+
* counts as unset.
|
|
327
|
+
*/
|
|
328
|
+
function readClaudeThemeFile(filePath: string, maxBytes: number): string | null {
|
|
329
|
+
const text = readRegularConfigFile(filePath, maxBytes);
|
|
330
|
+
if (text === null) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const config: unknown = JSON.parse(text);
|
|
335
|
+
const theme = config && typeof config === 'object' ? (config as Record<string, unknown>).theme : undefined;
|
|
336
|
+
if (typeof theme !== 'string') {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
return CLAUDE_THEME_IDS.has(theme) || theme.startsWith('custom:') ? theme : null;
|
|
340
|
+
} catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Finds the theme Claude Code would start with in `projectPath`, read-only and
|
|
347
|
+
* in the CLI's own order: local, project and user settings, then the legacy
|
|
348
|
+
* global config key, then its "dark" default. Managed settings are not read:
|
|
349
|
+
* they outrank `--settings`, so no flag could change their theme anyway.
|
|
350
|
+
*/
|
|
351
|
+
function resolveClaudeTheme(projectPath: string): string {
|
|
352
|
+
const configDirOverride = process.env.CLAUDE_CONFIG_DIR;
|
|
353
|
+
const configHome = configDirOverride || path.join(os.homedir(), '.claude');
|
|
354
|
+
const legacyGlobalConfigPath = path.join(configHome, '.config.json');
|
|
355
|
+
const configFiles: Array<[string, number]> = [
|
|
356
|
+
[path.join(projectPath, '.claude', 'settings.local.json'), MAX_CLAUDE_SETTINGS_FILE_BYTES],
|
|
357
|
+
[path.join(projectPath, '.claude', 'settings.json'), MAX_CLAUDE_SETTINGS_FILE_BYTES],
|
|
358
|
+
[path.join(configHome, 'settings.json'), MAX_CLAUDE_SETTINGS_FILE_BYTES],
|
|
359
|
+
[
|
|
360
|
+
fs.existsSync(legacyGlobalConfigPath)
|
|
361
|
+
? legacyGlobalConfigPath
|
|
362
|
+
: path.join(configDirOverride || os.homedir(), '.claude.json'),
|
|
363
|
+
MAX_CLAUDE_GLOBAL_CONFIG_FILE_BYTES,
|
|
364
|
+
],
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
for (const [filePath, maxBytes] of configFiles) {
|
|
368
|
+
const theme = readClaudeThemeFile(filePath, maxBytes);
|
|
369
|
+
if (theme) {
|
|
370
|
+
return theme;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return 'dark';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let hasLoggedThemeSettingsFileError = false;
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Returns a settings file that holds only `{"theme": theme}`, writing it when
|
|
380
|
+
* it is missing or differs. It lives in the app's own data folder
|
|
381
|
+
* (`~/.cloudcli`, like the chat assets); the user's Claude config is never
|
|
382
|
+
* written. The write goes through a temp file and a rename, so a CLI starting
|
|
383
|
+
* at the same time never reads half a file. Returns null, and logs once, when
|
|
384
|
+
* the file cannot be written (a read-only home, say): the launch then simply
|
|
385
|
+
* goes ahead without a theme.
|
|
386
|
+
*/
|
|
387
|
+
function ensureClaudeThemeSettingsFile(theme: string): string | null {
|
|
388
|
+
const contents = `${JSON.stringify({ theme })}\n`;
|
|
389
|
+
let tempPath: string | null = null;
|
|
390
|
+
try {
|
|
391
|
+
const filePath = path.join(os.homedir(), '.cloudcli', `claude-shell-theme-${theme}.json`);
|
|
392
|
+
// Anything but the expected regular file is replaced; the rename never
|
|
393
|
+
// opens what it replaces.
|
|
394
|
+
if (readRegularConfigFile(filePath) !== contents) {
|
|
395
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
396
|
+
tempPath = `${filePath}.${process.pid}.tmp`;
|
|
397
|
+
fs.writeFileSync(tempPath, contents);
|
|
398
|
+
fs.renameSync(tempPath, filePath);
|
|
399
|
+
}
|
|
400
|
+
return filePath;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (tempPath) {
|
|
403
|
+
// Asynchronous and unchecked: cleaning up must not throw into the launch.
|
|
404
|
+
fs.rm(tempPath, { force: true }, () => undefined);
|
|
405
|
+
}
|
|
406
|
+
if (!hasLoggedThemeSettingsFileError) {
|
|
407
|
+
hasLoggedThemeSettingsFileError = true;
|
|
408
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
409
|
+
console.warn(`[WARN] Shell: cannot write the Claude theme settings file, launching without it: ${message}`);
|
|
410
|
+
}
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Quotes one argument for the shell that runs the launch command. bash takes
|
|
417
|
+
* everything between single quotes literally, so a `'` closes the quote, adds
|
|
418
|
+
* an escaped one and reopens it. PowerShell (win32) single quotes are literal
|
|
419
|
+
* too, backslashes included; a quote is doubled, and PowerShell also treats
|
|
420
|
+
* the typographic single quotes as quote characters.
|
|
421
|
+
*/
|
|
422
|
+
function quoteShellArgument(value: string): string {
|
|
423
|
+
if (os.platform() === 'win32') {
|
|
424
|
+
return `'${value.replace(/['\u2018\u2019\u201A\u201B]/g, '$&$&')}'`;
|
|
425
|
+
}
|
|
426
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
type ClaudeThemeLaunch = {
|
|
430
|
+
flag: string;
|
|
431
|
+
/** Whether the CLI will paint in the app's colour scheme. */
|
|
432
|
+
followsAppTheme: boolean;
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Claude Code paints its own colours in its own theme, dark by default, so in
|
|
437
|
+
* a light Shell tab its prompt rows became dark bands. Only a light tab gets a
|
|
438
|
+
* flag, and only when the user's theme is a dark one: it is swapped for the
|
|
439
|
+
* matching light variant. `--settings` applies it to this launch only. It is
|
|
440
|
+
* given a file path rather than inline JSON because PowerShell strips the
|
|
441
|
+
* JSON's double quotes when it passes an argument to a native program.
|
|
442
|
+
*
|
|
443
|
+
* The CLI follows the app theme when it gets that flag, when a dark tab runs a
|
|
444
|
+
* dark theme, and with `auto` (it asks the terminal, which xterm answers from
|
|
445
|
+
* the live theme). A light or custom theme the user chose, or a flag that
|
|
446
|
+
* could not be written, paints the same in either app theme, so restarting
|
|
447
|
+
* after a toggle would change nothing.
|
|
448
|
+
*/
|
|
449
|
+
function buildClaudeThemeLaunch(colorScheme: ShellColorScheme | null, projectPath: string): ClaudeThemeLaunch {
|
|
450
|
+
if (!colorScheme) {
|
|
451
|
+
return { flag: '', followsAppTheme: false };
|
|
452
|
+
}
|
|
453
|
+
const userTheme = resolveClaudeTheme(projectPath);
|
|
454
|
+
if (userTheme === 'auto') {
|
|
455
|
+
return { flag: '', followsAppTheme: true };
|
|
456
|
+
}
|
|
457
|
+
const lightTheme = LIGHT_CLAUDE_THEME_BY_DARK_THEME.get(userTheme);
|
|
458
|
+
if (!lightTheme) {
|
|
459
|
+
return { flag: '', followsAppTheme: false };
|
|
460
|
+
}
|
|
461
|
+
if (colorScheme === 'dark') {
|
|
462
|
+
return { flag: '', followsAppTheme: true };
|
|
463
|
+
}
|
|
464
|
+
const settingsPath = ensureClaudeThemeSettingsFile(lightTheme);
|
|
465
|
+
return settingsPath
|
|
466
|
+
? { flag: ` --settings ${quoteShellArgument(settingsPath)}`, followsAppTheme: true }
|
|
467
|
+
: { flag: '', followsAppTheme: false };
|
|
234
468
|
}
|
|
235
469
|
|
|
236
470
|
function readEnvValue(env: NodeJS.ProcessEnv, key: string): string | undefined {
|
|
@@ -318,6 +552,7 @@ export function handleShellConnection(
|
|
|
318
552
|
const provider = readString(data.provider, 'claude');
|
|
319
553
|
const initialCommand = readString(data.initialCommand);
|
|
320
554
|
const forceRestart = readBoolean(data.forceRestart);
|
|
555
|
+
const colorScheme = readColorScheme(data.colorScheme);
|
|
321
556
|
const isPlainShell =
|
|
322
557
|
readBoolean(data.isPlainShell) ||
|
|
323
558
|
(!!initialCommand && !hasSession) ||
|
|
@@ -377,6 +612,7 @@ export function handleShellConnection(
|
|
|
377
612
|
}
|
|
378
613
|
|
|
379
614
|
existingSession.ws = ws;
|
|
615
|
+
sendClaudeColorScheme(ws, existingSession.claudeColorScheme);
|
|
380
616
|
return;
|
|
381
617
|
}
|
|
382
618
|
|
|
@@ -397,8 +633,13 @@ export function handleShellConnection(
|
|
|
397
633
|
return;
|
|
398
634
|
}
|
|
399
635
|
|
|
400
|
-
const shellCommand = buildShellCommand(
|
|
636
|
+
const { command: shellCommand, claudeFollowsAppTheme } = buildShellCommand(
|
|
637
|
+
data,
|
|
638
|
+
dependencies,
|
|
639
|
+
resolvedProjectPath,
|
|
640
|
+
);
|
|
401
641
|
const resumeSessionId = resolveResumeSessionId(data, dependencies);
|
|
642
|
+
const claudeColorScheme = claudeFollowsAppTheme ? colorScheme : null;
|
|
402
643
|
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
|
403
644
|
const shellArgs =
|
|
404
645
|
os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
|
|
@@ -417,6 +658,10 @@ export function handleShellConnection(
|
|
|
417
658
|
TERM: 'xterm-256color',
|
|
418
659
|
COLORTERM: 'truecolor',
|
|
419
660
|
FORCE_COLOR: '3',
|
|
661
|
+
// rxvt's "foreground;background" palette indices (15 = white, 0 = black).
|
|
662
|
+
// Programs that cannot query the terminal (vim, Claude Code's "auto"
|
|
663
|
+
// theme before its OSC 11 reply arrives) read it to pick light or dark.
|
|
664
|
+
...(colorScheme ? { COLORFGBG: colorScheme === 'light' ? '0;15' : '15;0' } : {}),
|
|
420
665
|
},
|
|
421
666
|
});
|
|
422
667
|
|
|
@@ -427,6 +672,7 @@ export function handleShellConnection(
|
|
|
427
672
|
timeoutId: null,
|
|
428
673
|
projectPath,
|
|
429
674
|
sessionId,
|
|
675
|
+
claudeColorScheme,
|
|
430
676
|
});
|
|
431
677
|
|
|
432
678
|
shellProcess.onData((chunk) => {
|
|
@@ -555,6 +801,7 @@ export function handleShellConnection(
|
|
|
555
801
|
data: welcomeMsg,
|
|
556
802
|
})
|
|
557
803
|
);
|
|
804
|
+
sendClaudeColorScheme(ws, claudeColorScheme);
|
|
558
805
|
return;
|
|
559
806
|
}
|
|
560
807
|
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import test, { mock } from 'node:test';
|
|
7
|
+
|
|
8
|
+
import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
|
|
9
|
+
import { handleChatConnection } from '@/modules/websocket/services/chat-websocket.service.js';
|
|
10
|
+
import { chatRunRegistry } from '@/modules/websocket/services/chat-run-registry.service.js';
|
|
11
|
+
import { connectedClients } from '@/modules/websocket/services/websocket-state.service.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A session's turn ends while its agents, workflow or backgrounded command are
|
|
15
|
+
* still going. The run registry marks the run completed at the turn's
|
|
16
|
+
* `complete`, but the runtime keeps producing events through that run's
|
|
17
|
+
* writer — task progress, and the turn the CLI pushes when the task reports.
|
|
18
|
+
* A tab opened during that work has to attach to the run to see them, and the
|
|
19
|
+
* run has to still be there to attach to.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const SESSION_ID = 'held-session';
|
|
23
|
+
const FIVE_MINUTES = 5 * 60 * 1000;
|
|
24
|
+
|
|
25
|
+
function createFakeSocket() {
|
|
26
|
+
const socket = new EventEmitter() as EventEmitter & {
|
|
27
|
+
readyState: number;
|
|
28
|
+
frames: Array<Record<string, unknown>>;
|
|
29
|
+
send: (data: string) => void;
|
|
30
|
+
};
|
|
31
|
+
socket.readyState = 1;
|
|
32
|
+
socket.frames = [];
|
|
33
|
+
socket.send = (data: string) => socket.frames.push(JSON.parse(data) as Record<string, unknown>);
|
|
34
|
+
return socket;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function withIsolatedDatabase(runTest: () => Promise<void>): Promise<void> {
|
|
38
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
39
|
+
const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'chat-background-subscribe-'));
|
|
40
|
+
closeConnection();
|
|
41
|
+
process.env.DATABASE_PATH = path.join(tempDirectory, 'auth.db');
|
|
42
|
+
await initializeDatabase();
|
|
43
|
+
try {
|
|
44
|
+
sessionsDb.createAppSession(SESSION_ID, 'claude', tempDirectory);
|
|
45
|
+
await runTest();
|
|
46
|
+
} finally {
|
|
47
|
+
connectedClients.clear();
|
|
48
|
+
chatRunRegistry.clearAll();
|
|
49
|
+
chatRunRegistry.setRetentionGuard(() => false);
|
|
50
|
+
closeConnection();
|
|
51
|
+
if (previousDatabasePath === undefined) {
|
|
52
|
+
delete process.env.DATABASE_PATH;
|
|
53
|
+
} else {
|
|
54
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
55
|
+
}
|
|
56
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Starts a run for the session and ends its turn, leaving the run completed but registered. */
|
|
61
|
+
function startAndCompleteRun(connection: ReturnType<typeof createFakeSocket>) {
|
|
62
|
+
const run = chatRunRegistry.startRun({
|
|
63
|
+
appSessionId: SESSION_ID,
|
|
64
|
+
provider: 'claude',
|
|
65
|
+
providerSessionId: 'native-1',
|
|
66
|
+
connection: connection as never,
|
|
67
|
+
userId: 1,
|
|
68
|
+
});
|
|
69
|
+
assert.ok(run);
|
|
70
|
+
run.writer.send({ kind: 'complete', provider: 'claude', sessionId: 'native-1', exitCode: 0 });
|
|
71
|
+
assert.equal(chatRunRegistry.isProcessing(SESSION_ID), false);
|
|
72
|
+
return run;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const settle = () => new Promise((resolve) => { setTimeout(resolve, 30); });
|
|
76
|
+
|
|
77
|
+
test('a tab that subscribes during background work receives the run\'s later events', async () => {
|
|
78
|
+
await withIsolatedDatabase(async () => {
|
|
79
|
+
const run = startAndCompleteRun(createFakeSocket());
|
|
80
|
+
|
|
81
|
+
const newTab = createFakeSocket();
|
|
82
|
+
handleChatConnection(
|
|
83
|
+
newTab as never,
|
|
84
|
+
{ user: { id: 1 } } as never,
|
|
85
|
+
{ runtime: { hasBackgroundWork: (sessionId: string) => sessionId === SESSION_ID, getPendingApprovalsForSession: () => [] } as never },
|
|
86
|
+
);
|
|
87
|
+
newTab.emit('message', JSON.stringify({ type: 'chat.subscribe', sessions: [{ sessionId: SESSION_ID, lastSeq: 0 }] }));
|
|
88
|
+
await settle();
|
|
89
|
+
|
|
90
|
+
// Not processing — the composer stays usable — but attached.
|
|
91
|
+
assert.equal(newTab.frames[0]?.kind, 'chat_subscribed');
|
|
92
|
+
assert.equal(newTab.frames[0]?.isProcessing, false);
|
|
93
|
+
|
|
94
|
+
run.writer.send({ kind: 'task_status', provider: 'claude', sessionId: 'native-1', event: 'notification', taskId: 't1', status: 'completed' } as never);
|
|
95
|
+
assert.ok(
|
|
96
|
+
newTab.frames.some((frame) => frame.kind === 'task_status'),
|
|
97
|
+
'the task event must reach the tab that subscribed after the turn ended',
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('a tab that subscribes to an idle session is not attached to its old run', async () => {
|
|
103
|
+
await withIsolatedDatabase(async () => {
|
|
104
|
+
const run = startAndCompleteRun(createFakeSocket());
|
|
105
|
+
|
|
106
|
+
const newTab = createFakeSocket();
|
|
107
|
+
handleChatConnection(
|
|
108
|
+
newTab as never,
|
|
109
|
+
{ user: { id: 1 } } as never,
|
|
110
|
+
{ runtime: { hasBackgroundWork: () => false, getPendingApprovalsForSession: () => [] } as never },
|
|
111
|
+
);
|
|
112
|
+
newTab.emit('message', JSON.stringify({ type: 'chat.subscribe', sessions: [{ sessionId: SESSION_ID, lastSeq: 0 }] }));
|
|
113
|
+
await settle();
|
|
114
|
+
|
|
115
|
+
run.writer.send({ kind: 'task_status', provider: 'claude', sessionId: 'native-1', event: 'notification', taskId: 't1', status: 'completed' } as never);
|
|
116
|
+
assert.ok(!newTab.frames.some((frame) => frame.kind === 'task_status'));
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('a completed run stays registered while its session still has background work', async () => {
|
|
121
|
+
mock.timers.enable({ apis: ['setTimeout'] });
|
|
122
|
+
try {
|
|
123
|
+
await withIsolatedDatabase(async () => {
|
|
124
|
+
let working = true;
|
|
125
|
+
chatRunRegistry.setRetentionGuard((sessionId) => sessionId === SESSION_ID && working);
|
|
126
|
+
startAndCompleteRun(createFakeSocket());
|
|
127
|
+
|
|
128
|
+
mock.timers.tick(FIVE_MINUTES);
|
|
129
|
+
assert.ok(chatRunRegistry.getRun(SESSION_ID), 'the run must outlive the retention window while work is outstanding');
|
|
130
|
+
|
|
131
|
+
working = false;
|
|
132
|
+
mock.timers.tick(FIVE_MINUTES);
|
|
133
|
+
assert.equal(chatRunRegistry.getRun(SESSION_ID), undefined, 'once the work is gone the run is evicted as before');
|
|
134
|
+
});
|
|
135
|
+
} finally {
|
|
136
|
+
mock.timers.reset();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('a retained run\'s eviction timer cannot evict the session\'s next run early', async () => {
|
|
141
|
+
// Run one completes with work outstanding and is re-armed at five minutes;
|
|
142
|
+
// run two takes the slot and completes with two minutes of retention to
|
|
143
|
+
// go. Run one's timer firing on the slot alone would evict run two.
|
|
144
|
+
mock.timers.enable({ apis: ['setTimeout'] });
|
|
145
|
+
try {
|
|
146
|
+
await withIsolatedDatabase(async () => {
|
|
147
|
+
let working = true;
|
|
148
|
+
chatRunRegistry.setRetentionGuard((sessionId) => sessionId === SESSION_ID && working);
|
|
149
|
+
const first = startAndCompleteRun(createFakeSocket());
|
|
150
|
+
mock.timers.tick(FIVE_MINUTES);
|
|
151
|
+
assert.equal(chatRunRegistry.getRun(SESSION_ID), first);
|
|
152
|
+
|
|
153
|
+
working = false;
|
|
154
|
+
mock.timers.tick(FIVE_MINUTES - 2 * 60 * 1000);
|
|
155
|
+
const second = startAndCompleteRun(createFakeSocket());
|
|
156
|
+
assert.equal(chatRunRegistry.getRun(SESSION_ID), second);
|
|
157
|
+
|
|
158
|
+
// Run one's re-armed timer fires now; run two's has two minutes left.
|
|
159
|
+
mock.timers.tick(2 * 60 * 1000);
|
|
160
|
+
assert.equal(chatRunRegistry.getRun(SESSION_ID), second, 'run two keeps its own retention window');
|
|
161
|
+
mock.timers.tick(FIVE_MINUTES);
|
|
162
|
+
assert.equal(chatRunRegistry.getRun(SESSION_ID), undefined);
|
|
163
|
+
});
|
|
164
|
+
} finally {
|
|
165
|
+
mock.timers.reset();
|
|
166
|
+
}
|
|
167
|
+
});
|