@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
|
@@ -3,16 +3,31 @@ import fsp from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import readline from 'node:readline';
|
|
5
5
|
import { parseFilesInputTag } from '../../../../shared/image-attachments.js';
|
|
6
|
-
import { prepareTranscriptMessages } from '../../../../shared/message-unification.js';
|
|
7
|
-
import { createNormalizedMessage, generateMessageId, readObjectRecord, sliceTailPage, truncateSubagentActivity, } from '../../../../shared/utils.js';
|
|
6
|
+
import { prepareTranscriptMessages, truncateNestedOutput } from '../../../../shared/message-unification.js';
|
|
7
|
+
import { createNormalizedMessage, generateMessageId, isPlaceholderProviderModel, readObjectRecord, sliceTailPage, stripAnsiSequences, truncateSubagentActivity, } from '../../../../shared/utils.js';
|
|
8
8
|
import { sessionsDb } from '../../../../modules/database/index.js';
|
|
9
|
+
import { getClaudeSDKSessionStartTime } from '../../../../modules/providers/list/claude/claude-runtime.provider.js';
|
|
9
10
|
import { summarizeClaudeTokenUsage } from '../../../../modules/providers/services/provider-token-usage.service.js';
|
|
10
11
|
const PROVIDER = 'claude';
|
|
12
|
+
/** Tokens the way the CLI writes them: 725k rather than 724,871. */
|
|
13
|
+
const formatTokenCount = (tokens) => {
|
|
14
|
+
if (tokens >= 1_000_000) {
|
|
15
|
+
return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
16
|
+
}
|
|
17
|
+
return tokens >= 1_000 ? `${Math.round(tokens / 1_000)}k` : `${tokens}`;
|
|
18
|
+
};
|
|
19
|
+
/** A duration the way the CLI writes one: `2m 22s`. */
|
|
20
|
+
const formatDuration = (milliseconds) => {
|
|
21
|
+
const seconds = Math.round(milliseconds / 1_000);
|
|
22
|
+
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
23
|
+
};
|
|
11
24
|
/**
|
|
12
25
|
* Upper bound on how much of a subagent's timeline is sent to the client. A
|
|
13
26
|
* long-running agent can record hundreds of tool calls, and the transcript only
|
|
14
27
|
* ever shows them behind a collapsed header, so shipping the whole history on
|
|
15
|
-
* every load costs far more than it shows.
|
|
28
|
+
* every load costs far more than it shows. The newest steps are the ones
|
|
29
|
+
* kept: a running agent's timeline is polled for what it is doing now, and
|
|
30
|
+
* the card says how many earlier ones were left out.
|
|
16
31
|
*/
|
|
17
32
|
const MAX_TRANSMITTED_SUBAGENT_ACTIVITIES = 200;
|
|
18
33
|
/**
|
|
@@ -24,7 +39,7 @@ const MAX_TRANSMITTED_SUBAGENT_ACTIVITIES = 200;
|
|
|
24
39
|
*/
|
|
25
40
|
async function readClaudeSubagentTranscript(filePath) {
|
|
26
41
|
const activity = [];
|
|
27
|
-
const transcript = { activity
|
|
42
|
+
const transcript = { activity };
|
|
28
43
|
const toolsById = new Map();
|
|
29
44
|
try {
|
|
30
45
|
const fileStream = fs.createReadStream(filePath);
|
|
@@ -39,6 +54,9 @@ async function readClaudeSubagentTranscript(filePath) {
|
|
|
39
54
|
try {
|
|
40
55
|
const entry = JSON.parse(line);
|
|
41
56
|
const timestamp = typeof entry.timestamp === 'string' ? entry.timestamp : undefined;
|
|
57
|
+
if (timestamp && !transcript.startedAt) {
|
|
58
|
+
transcript.startedAt = timestamp;
|
|
59
|
+
}
|
|
42
60
|
if (entry.message?.role === 'assistant' && Array.isArray(entry.message?.content)) {
|
|
43
61
|
if (typeof entry.message.model === 'string') {
|
|
44
62
|
transcript.model = entry.message.model;
|
|
@@ -98,8 +116,6 @@ async function readClaudeSubagentTranscript(filePath) {
|
|
|
98
116
|
const message = error instanceof Error ? error.message : String(error);
|
|
99
117
|
console.warn(`Error parsing agent file ${filePath}:`, message);
|
|
100
118
|
}
|
|
101
|
-
const lastActivity = activity[activity.length - 1];
|
|
102
|
-
transcript.endedMidToolCall = lastActivity?.kind === 'tool' && !lastActivity.toolResult;
|
|
103
119
|
return transcript;
|
|
104
120
|
}
|
|
105
121
|
/** Reads the sidecar `.meta.json` Claude writes next to a subagent transcript. */
|
|
@@ -140,13 +156,122 @@ async function findClaudeSubagentTranscript(projectDirectory, providerSessionId,
|
|
|
140
156
|
}
|
|
141
157
|
return null;
|
|
142
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Reads which agents a workflow run spawned and how far each got.
|
|
161
|
+
*
|
|
162
|
+
* The run appends one record per event to `<transcriptDir>/journal.jsonl`:
|
|
163
|
+
* `started` names the agent (and the label and phase the script gave it, when
|
|
164
|
+
* it gave any), `result` and `failed` settle it. The journal is what the
|
|
165
|
+
* workflow itself resumes from, so it is the only record of an agent that is
|
|
166
|
+
* still running — its transcript file is written, but nothing in it says the
|
|
167
|
+
* agent is not done. A missing directory or journal means the run left no
|
|
168
|
+
* record here (a fork copies only the parent's transcript), which reads as no
|
|
169
|
+
* agents rather than an error.
|
|
170
|
+
*/
|
|
171
|
+
async function readWorkflowJournal(transcriptDir) {
|
|
172
|
+
const agentsById = new Map();
|
|
173
|
+
let journal;
|
|
174
|
+
try {
|
|
175
|
+
journal = await fsp.readFile(path.join(transcriptDir, 'journal.jsonl'), 'utf8');
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
for (const line of journal.split('\n')) {
|
|
181
|
+
if (!line.trim()) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
let entry;
|
|
185
|
+
try {
|
|
186
|
+
entry = JSON.parse(line);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// The workflow appends while this reads, so the last line can be partial.
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const agentId = typeof entry.agentId === 'string' ? entry.agentId : '';
|
|
193
|
+
if (!agentId) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (entry.type === 'started') {
|
|
197
|
+
agentsById.set(agentId, {
|
|
198
|
+
id: agentId,
|
|
199
|
+
label: typeof entry.label === 'string' ? entry.label : undefined,
|
|
200
|
+
phase: typeof entry.phase === 'string' ? entry.phase : undefined,
|
|
201
|
+
status: 'running',
|
|
202
|
+
});
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const agent = agentsById.get(agentId);
|
|
206
|
+
if (!agent) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (entry.type === 'result') {
|
|
210
|
+
agent.status = 'completed';
|
|
211
|
+
}
|
|
212
|
+
else if (entry.type === 'failed') {
|
|
213
|
+
agent.status = 'failed';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return [...agentsById.values()];
|
|
217
|
+
}
|
|
218
|
+
function readBackgroundLaunch(toolUseResult) {
|
|
219
|
+
const result = readObjectRecord(toolUseResult);
|
|
220
|
+
if (!result) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
if (result.agentId) {
|
|
224
|
+
return 'agent';
|
|
225
|
+
}
|
|
226
|
+
if (result.status === 'async_launched') {
|
|
227
|
+
return 'workflow';
|
|
228
|
+
}
|
|
229
|
+
if (typeof result.backgroundTaskId === 'string' && result.backgroundTaskId) {
|
|
230
|
+
return 'bash';
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
143
234
|
function readTaggedValue(content, tagName) {
|
|
144
235
|
const match = new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`).exec(content);
|
|
145
236
|
return match ? match[1].trim() : '';
|
|
146
237
|
}
|
|
147
238
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
239
|
+
* Reads every `<task-notification>` payload one transcript row carries.
|
|
240
|
+
*
|
|
241
|
+
* Claude writes the same completion report in two shapes, and which one you get
|
|
242
|
+
* depends only on when the agent happened to finish:
|
|
243
|
+
*
|
|
244
|
+
* - as an ordinary user-role turn, when the queued notification is delivered
|
|
245
|
+
* between turns (the harness also logs a contentless `dequeue` for it);
|
|
246
|
+
* - as a top-level `queue-operation` record whose `content` holds the payload,
|
|
247
|
+
* when the harness consumes it inline while the parent turn is still running
|
|
248
|
+
* (`remove`), or when the session ends before it is ever delivered
|
|
249
|
+
* (`enqueue`).
|
|
250
|
+
*
|
|
251
|
+
* The operation names the queue's bookkeeping, not whether the agent finished,
|
|
252
|
+
* so every queue-operation payload counts. Reading only the user-role shape
|
|
253
|
+
* loses the majority of them: across the 919 transcripts this was measured on,
|
|
254
|
+
* 31 of 56 background agent launches report *only* through a queue-operation
|
|
255
|
+
* record, and their result and status were invisible to every consumer.
|
|
256
|
+
*/
|
|
257
|
+
function readTaskNotificationTexts(message) {
|
|
258
|
+
if (message.type === 'queue-operation') {
|
|
259
|
+
return typeof message.content === 'string' ? [message.content] : [];
|
|
260
|
+
}
|
|
261
|
+
if (message.message?.role !== 'user') {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
const content = message.message.content;
|
|
265
|
+
if (typeof content === 'string') {
|
|
266
|
+
return [content];
|
|
267
|
+
}
|
|
268
|
+
return Array.isArray(content)
|
|
269
|
+
? content.filter((part) => part?.type === 'text').map((part) => String(part.text ?? ''))
|
|
270
|
+
: [];
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Collects every `<task-notification>` keyed by the tool call that spawned the
|
|
274
|
+
* agent it reports on.
|
|
150
275
|
*
|
|
151
276
|
* Only notifications that name a tool-use id are collected: without one there
|
|
152
277
|
* is no card to fold them into, and they must keep rendering on their own.
|
|
@@ -154,16 +279,7 @@ function readTaggedValue(content, tagName) {
|
|
|
154
279
|
function collectTaskNotifications(messages) {
|
|
155
280
|
const notifications = new Map();
|
|
156
281
|
for (const message of messages) {
|
|
157
|
-
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
const content = message.message.content;
|
|
161
|
-
const texts = typeof content === 'string'
|
|
162
|
-
? [content]
|
|
163
|
-
: Array.isArray(content)
|
|
164
|
-
? content.filter((part) => part?.type === 'text').map((part) => String(part.text ?? ''))
|
|
165
|
-
: [];
|
|
166
|
-
for (const text of texts) {
|
|
282
|
+
for (const text of readTaskNotificationTexts(message)) {
|
|
167
283
|
if (!text.trimStart().startsWith('<task-notification>')) {
|
|
168
284
|
continue;
|
|
169
285
|
}
|
|
@@ -171,9 +287,14 @@ function collectTaskNotifications(messages) {
|
|
|
171
287
|
if (!toolUseId) {
|
|
172
288
|
continue;
|
|
173
289
|
}
|
|
174
|
-
// A resumed agent notifies more than once; the last word wins
|
|
290
|
+
// A resumed agent notifies more than once; the last word wins, and the
|
|
291
|
+
// rows that carried the earlier words go with it.
|
|
292
|
+
const sourceUuids = notifications.get(toolUseId)?.sourceUuids ?? [];
|
|
293
|
+
if (typeof message.uuid === 'string' && message.uuid) {
|
|
294
|
+
sourceUuids.push(message.uuid);
|
|
295
|
+
}
|
|
175
296
|
notifications.set(toolUseId, {
|
|
176
|
-
|
|
297
|
+
sourceUuids,
|
|
177
298
|
toolUseId,
|
|
178
299
|
status: readTaggedValue(text, 'status') || 'completed',
|
|
179
300
|
summary: readTaggedValue(text, 'summary'),
|
|
@@ -183,8 +304,8 @@ function collectTaskNotifications(messages) {
|
|
|
183
304
|
}
|
|
184
305
|
return notifications;
|
|
185
306
|
}
|
|
186
|
-
/** Reads the `tool_use_id` off the tool-result row that launched
|
|
187
|
-
function
|
|
307
|
+
/** Reads the `tool_use_id` off the tool-result row that launched background work. */
|
|
308
|
+
function readLaunchToolUseId(message) {
|
|
188
309
|
const content = message.message?.content;
|
|
189
310
|
if (!Array.isArray(content)) {
|
|
190
311
|
return null;
|
|
@@ -196,8 +317,8 @@ function readAgentToolUseId(message) {
|
|
|
196
317
|
}
|
|
197
318
|
return null;
|
|
198
319
|
}
|
|
199
|
-
/** Swaps
|
|
200
|
-
function
|
|
320
|
+
/** Swaps a launch acknowledgement for the work's actual answer. */
|
|
321
|
+
function replaceLaunchToolResultContent(message, replacement) {
|
|
201
322
|
const content = message.message?.content;
|
|
202
323
|
if (!Array.isArray(content)) {
|
|
203
324
|
return;
|
|
@@ -247,12 +368,111 @@ function isUserPromptRow(row) {
|
|
|
247
368
|
return typeof content === 'string' && content.length > 0;
|
|
248
369
|
}
|
|
249
370
|
/**
|
|
250
|
-
*
|
|
371
|
+
* Returns the uuids on the chain the next `claude --resume` replays, from the
|
|
372
|
+
* leaf it continues from back to the root.
|
|
373
|
+
*
|
|
374
|
+
* Mirrors the transcript loader of the Claude CLI, which the Agent SDK's
|
|
375
|
+
* `resume` spawns as well. Every CLI process stamps a `last-prompt` row naming
|
|
376
|
+
* the leaf it was on when it exits, so with two writers on one session it is
|
|
377
|
+
* that stamp — not the newest row — that decides which branch is replayed. The
|
|
378
|
+
* newest conversation row only takes over when it descends from the stamped
|
|
379
|
+
* leaf (the stamping process kept going), or when the file carries no stamp.
|
|
380
|
+
* A compact boundary clears the stamp, and `progress` rows are skipped over
|
|
381
|
+
* when linking a row to its parent, as the CLI does.
|
|
382
|
+
*/
|
|
383
|
+
function resolveResumePath(rows) {
|
|
384
|
+
const conversation = new Map();
|
|
385
|
+
const progressParents = new Map();
|
|
386
|
+
const parentOf = (row) => {
|
|
387
|
+
const parentUuid = typeof row.parentUuid === 'string' ? row.parentUuid : undefined;
|
|
388
|
+
if (parentUuid && progressParents.has(parentUuid)) {
|
|
389
|
+
return progressParents.get(parentUuid) ?? undefined;
|
|
390
|
+
}
|
|
391
|
+
return parentUuid;
|
|
392
|
+
};
|
|
393
|
+
let newestRow;
|
|
394
|
+
let stampedLeaf;
|
|
395
|
+
let stampIsExplicit = false;
|
|
396
|
+
for (const row of rows) {
|
|
397
|
+
if (row.type === 'progress' && typeof row.uuid === 'string') {
|
|
398
|
+
progressParents.set(row.uuid, parentOf(row) ?? null);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (typeof row.uuid === 'string'
|
|
402
|
+
&& (row.type === 'user' || row.type === 'assistant' || row.type === 'attachment' || row.type === 'system')) {
|
|
403
|
+
conversation.set(row.uuid, row);
|
|
404
|
+
if (row.isSidechain !== true) {
|
|
405
|
+
newestRow = row.uuid;
|
|
406
|
+
stampIsExplicit = false;
|
|
407
|
+
}
|
|
408
|
+
if (row.type === 'system' && row.subtype === 'compact_boundary') {
|
|
409
|
+
stampedLeaf = undefined;
|
|
410
|
+
stampIsExplicit = false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else if (row.type === 'last-prompt' && typeof row.leafUuid === 'string' && row.leafUuid) {
|
|
414
|
+
stampIsExplicit = row.explicit === true || (stampIsExplicit && row.leafUuid === stampedLeaf);
|
|
415
|
+
stampedLeaf = row.leafUuid;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const chainFrom = (leaf) => {
|
|
419
|
+
const chain = [];
|
|
420
|
+
const seen = new Set();
|
|
421
|
+
let cursor = leaf;
|
|
422
|
+
while (cursor && conversation.has(cursor) && !seen.has(cursor)) {
|
|
423
|
+
seen.add(cursor);
|
|
424
|
+
chain.push(cursor);
|
|
425
|
+
cursor = parentOf(conversation.get(cursor));
|
|
426
|
+
}
|
|
427
|
+
return chain;
|
|
428
|
+
};
|
|
429
|
+
let leaf = stampedLeaf && conversation.has(stampedLeaf) ? stampedLeaf : undefined;
|
|
430
|
+
if (leaf && !stampIsExplicit && newestRow && newestRow !== leaf && chainFrom(newestRow).includes(leaf)) {
|
|
431
|
+
leaf = newestRow;
|
|
432
|
+
}
|
|
433
|
+
return new Set(chainFrom(leaf ?? newestRow));
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Maps each row's uuid to the file position of the newest row in its subtree.
|
|
437
|
+
*
|
|
438
|
+
* The transcript is append-only and a child is always written after its
|
|
439
|
+
* parent, so one backward pass hands every row's position to its parent and
|
|
440
|
+
* each uuid ends up holding the position of the last line its branch grew.
|
|
441
|
+
* Rows after a compact boundary do not link back across it, so this only
|
|
442
|
+
* orders branches within one compaction segment.
|
|
443
|
+
*/
|
|
444
|
+
function indexNewestRowPerBranch(rows) {
|
|
445
|
+
const newestByUuid = new Map();
|
|
446
|
+
for (let index = rows.length - 1; index >= 0; index -= 1) {
|
|
447
|
+
const row = rows[index];
|
|
448
|
+
if (typeof row.uuid !== 'string') {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const newest = newestByUuid.get(row.uuid) ?? index;
|
|
452
|
+
newestByUuid.set(row.uuid, newest);
|
|
453
|
+
if (typeof row.parentUuid === 'string' && (newestByUuid.get(row.parentUuid) ?? -1) < newest) {
|
|
454
|
+
newestByUuid.set(row.parentUuid, newest);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return newestByUuid;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Drops the rows belonging to prompts the next resume will not replay.
|
|
251
461
|
*
|
|
252
462
|
* When a message is edited, Claude resumes the conversation partway and appends
|
|
253
463
|
* the replacement, so two prompts end up sharing one parent and the file holds
|
|
254
464
|
* both the abandoned attempt and the live one. A flat read would show them
|
|
255
|
-
* stacked, which reads as the app having sent the message twice.
|
|
465
|
+
* stacked, which reads as the app having sent the message twice. The same shape
|
|
466
|
+
* appears without an edit when a session is driven from Chat and from the
|
|
467
|
+
* native `claude` CLI at once: both writers branch off the row each of them saw
|
|
468
|
+
* last.
|
|
469
|
+
*
|
|
470
|
+
* Which sibling survives is decided by the branch the runtime will actually
|
|
471
|
+
* resume: the sibling on the chain `resolveResumePath` returns. Going by the
|
|
472
|
+
* newest prompt, or by the newest row, would show a branch the model was never
|
|
473
|
+
* sent whenever the two writers exit in a different order than they wrote. A
|
|
474
|
+
* fork that is not on that chain at all (one made before a compaction) keeps
|
|
475
|
+
* the branch that was appended to last.
|
|
256
476
|
*
|
|
257
477
|
* Only sibling *prompts* are treated as a fork. Branch points made by parallel
|
|
258
478
|
* tool calls are extremely common — one assistant turn writes several chained
|
|
@@ -273,15 +493,29 @@ function dropSupersededPromptBranches(rows) {
|
|
|
273
493
|
promptSiblings.set(row.parentUuid, [row]);
|
|
274
494
|
}
|
|
275
495
|
}
|
|
496
|
+
const forks = [...promptSiblings.values()].filter((siblings) => siblings.length > 1);
|
|
497
|
+
if (forks.length === 0) {
|
|
498
|
+
return rows;
|
|
499
|
+
}
|
|
500
|
+
const resumePath = resolveResumePath(rows);
|
|
501
|
+
let newestByUuid;
|
|
502
|
+
const newestRowOf = (row) => {
|
|
503
|
+
newestByUuid ??= indexNewestRowPerBranch(rows);
|
|
504
|
+
return typeof row.uuid === 'string' ? newestByUuid.get(row.uuid) ?? -1 : -1;
|
|
505
|
+
};
|
|
276
506
|
const supersededRoots = new Set();
|
|
277
|
-
for (const siblings of
|
|
278
|
-
|
|
279
|
-
|
|
507
|
+
for (const siblings of forks) {
|
|
508
|
+
let live = siblings.find((row) => typeof row.uuid === 'string' && resumePath.has(row.uuid));
|
|
509
|
+
if (!live) {
|
|
510
|
+
live = siblings[0];
|
|
511
|
+
for (const row of siblings) {
|
|
512
|
+
if (newestRowOf(row) > newestRowOf(live)) {
|
|
513
|
+
live = row;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
280
516
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
for (const row of siblings.slice(0, -1)) {
|
|
284
|
-
if (typeof row.uuid === 'string') {
|
|
517
|
+
for (const row of siblings) {
|
|
518
|
+
if (row !== live && typeof row.uuid === 'string') {
|
|
285
519
|
supersededRoots.add(row.uuid);
|
|
286
520
|
}
|
|
287
521
|
}
|
|
@@ -299,7 +533,7 @@ function dropSupersededPromptBranches(rows) {
|
|
|
299
533
|
}
|
|
300
534
|
return rows.filter((row) => typeof row.uuid !== 'string' || !abandoned.has(row.uuid));
|
|
301
535
|
}
|
|
302
|
-
async function getSessionMessages(sessionId, providerSessionId, limit, offset) {
|
|
536
|
+
async function getSessionMessages(sessionId, providerSessionId, limit, offset, getLiveRunStartTime) {
|
|
303
537
|
try {
|
|
304
538
|
// The DB row is keyed by the app-facing session id, while the JSONL rows
|
|
305
539
|
// on disk carry the provider-native id — both ids are needed here.
|
|
@@ -317,7 +551,8 @@ async function getSessionMessages(sessionId, providerSessionId, limit, offset) {
|
|
|
317
551
|
}
|
|
318
552
|
}
|
|
319
553
|
// Read each spawned agent's own transcript once, then hang it off every
|
|
320
|
-
// row that references it.
|
|
554
|
+
// row that references it. Status is not part of this: the transcript
|
|
555
|
+
// cannot say whether the agent finished (see the loop below).
|
|
321
556
|
const subagentsById = new Map();
|
|
322
557
|
for (const agentId of agentIds) {
|
|
323
558
|
const located = await findClaudeSubagentTranscript(projectDir, providerSessionId, agentId);
|
|
@@ -329,9 +564,8 @@ async function getSessionMessages(sessionId, providerSessionId, limit, offset) {
|
|
|
329
564
|
readClaudeSubagentMeta(located.metaPath),
|
|
330
565
|
]);
|
|
331
566
|
subagentsById.set(agentId, {
|
|
332
|
-
endedMidToolCall: transcript.endedMidToolCall,
|
|
333
567
|
activity: transcript.activity
|
|
334
|
-
.slice(
|
|
568
|
+
.slice(-MAX_TRANSMITTED_SUBAGENT_ACTIVITIES)
|
|
335
569
|
.map(truncateSubagentActivity),
|
|
336
570
|
info: {
|
|
337
571
|
id: agentId,
|
|
@@ -339,58 +573,146 @@ async function getSessionMessages(sessionId, providerSessionId, limit, offset) {
|
|
|
339
573
|
type: meta.agentType,
|
|
340
574
|
description: meta.description,
|
|
341
575
|
model: transcript.model,
|
|
342
|
-
status: 'completed',
|
|
343
576
|
activityCount: transcript.activity.length,
|
|
344
577
|
},
|
|
345
578
|
});
|
|
346
579
|
}
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
580
|
+
// Read each workflow run's journal once, keyed by the directory the launch
|
|
581
|
+
// row names. The journal is the only thing that says which of a run's
|
|
582
|
+
// agents are still going — see `readWorkflowJournal`.
|
|
583
|
+
const workflowAgentsByDir = new Map();
|
|
584
|
+
for (const message of messages) {
|
|
585
|
+
const transcriptDir = message.toolUseResult?.transcriptDir;
|
|
586
|
+
if (readBackgroundLaunch(message.toolUseResult) === 'workflow'
|
|
587
|
+
&& typeof transcriptDir === 'string'
|
|
588
|
+
&& transcriptDir
|
|
589
|
+
&& !workflowAgentsByDir.has(transcriptDir)) {
|
|
590
|
+
workflowAgentsByDir.set(transcriptDir, await readWorkflowJournal(transcriptDir));
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// A background launch's tool result is internal bookkeeping ("Async agent
|
|
594
|
+
// launched successfully…", "Workflow launched in background. Task ID: …",
|
|
595
|
+
// "Command running in background with ID: …"); its real answer arrives
|
|
596
|
+
// later as a separate `<task-notification>` turn. Folding the notification
|
|
597
|
+
// back onto the tool call that started the work keeps one card per launch
|
|
598
|
+
// instead of a card, an unrelated status line, and a stray markdown reply.
|
|
599
|
+
// Reading only rows with an `agentId` left a workflow's card on its launch
|
|
600
|
+
// acknowledgement forever, with its report rendering as a raw user bubble
|
|
601
|
+
// further down or, for a queue-operation record, nowhere at all.
|
|
352
602
|
const notificationsByToolUseId = collectTaskNotifications(messages);
|
|
353
603
|
const foldedNotificationUuids = new Set();
|
|
604
|
+
// A background agent runs inside the CLI process that launched it, so once
|
|
605
|
+
// that process is gone an agent that has not reported back never will. A
|
|
606
|
+
// later run of the same session is a new process, so a launch row older
|
|
607
|
+
// than the live run belongs to a process that has already exited. The
|
|
608
|
+
// runtime keys its process map by the app session id when the chat gateway
|
|
609
|
+
// starts a run and by the provider-native id when the agent API does, so
|
|
610
|
+
// both are asked.
|
|
611
|
+
const liveRunStartedAt = getLiveRunStartTime(sessionId) ?? getLiveRunStartTime(providerSessionId);
|
|
612
|
+
const launchedByLiveRun = (message) => liveRunStartedAt !== null && Date.parse(String(message.timestamp ?? '')) >= liveRunStartedAt;
|
|
354
613
|
for (const message of messages) {
|
|
355
|
-
const
|
|
356
|
-
if (!
|
|
614
|
+
const launch = readBackgroundLaunch(message.toolUseResult);
|
|
615
|
+
if (!launch) {
|
|
357
616
|
continue;
|
|
358
617
|
}
|
|
359
|
-
const
|
|
360
|
-
const toolUseId = readAgentToolUseId(message);
|
|
618
|
+
const toolUseId = readLaunchToolUseId(message);
|
|
361
619
|
const notification = toolUseId ? notificationsByToolUseId.get(toolUseId) : undefined;
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
620
|
+
// A workflow and a backgrounded shell command have no foreground form;
|
|
621
|
+
// only an agent can hand its answer back inline.
|
|
622
|
+
const isAsyncLaunch = launch !== 'agent' || message.toolUseResult?.isAsync === true;
|
|
623
|
+
// An async launch row never tells you it finished — only the later
|
|
624
|
+
// notification does, so a missing notification means the outcome is
|
|
625
|
+
// still unknown.
|
|
626
|
+
//
|
|
627
|
+
// The agent's own transcript cannot stand in for that. A background agent
|
|
628
|
+
// routinely ends its turn cleanly while its work is still outstanding: it
|
|
629
|
+
// replies "I've started that, waiting for it to finish" and stops, or it
|
|
630
|
+
// has live background children of its own, which is precisely when the
|
|
631
|
+
// harness holds the notification back. Treating a cleanly-ended timeline
|
|
632
|
+
// as proof of completion therefore marked every background agent
|
|
633
|
+
// `completed` the moment it launched, collapsing its card and dropping
|
|
634
|
+
// its spinner while it was still working.
|
|
635
|
+
//
|
|
636
|
+
// "Unknown" only means "still outstanding" while the process the agent
|
|
637
|
+
// runs in is alive. Once it is gone — the run was stopped or crashed, or
|
|
638
|
+
// this transcript is a fork, which copies the parent's conversation rows
|
|
639
|
+
// but not its agent files or queue-operation records — nothing can still
|
|
640
|
+
// report, and reading that as `running` pinned a spinner on the card
|
|
641
|
+
// forever.
|
|
642
|
+
//
|
|
643
|
+
// A synchronous agent hands its answer back inline on its own tool
|
|
644
|
+
// result, so its status never depends on a notification or the process.
|
|
645
|
+
const unreportedStatus = isAsyncLaunch
|
|
646
|
+
? launchedByLiveRun(message) ? 'running' : 'stopped'
|
|
647
|
+
: 'completed';
|
|
648
|
+
if (launch === 'agent') {
|
|
649
|
+
const agentId = String(message.toolUseResult.agentId);
|
|
650
|
+
const subagent = subagentsById.get(agentId);
|
|
651
|
+
const status = notification
|
|
652
|
+
? notification.status === 'completed' ? 'completed' : 'failed'
|
|
653
|
+
: unreportedStatus;
|
|
654
|
+
if (subagent && subagent.activity.length > 0) {
|
|
371
655
|
message.subagentTools = subagent.activity;
|
|
372
656
|
}
|
|
657
|
+
// The agent's own transcript names its type, model and timeline, but
|
|
658
|
+
// it is not what says whether the agent finished, and it can be
|
|
659
|
+
// missing — a fork copies only the parent's `.jsonl`. The card reads
|
|
660
|
+
// its status off `subagent`, so the launch row publishes one with or
|
|
661
|
+
// without it; otherwise a notification saying "completed" left the
|
|
662
|
+
// card `running`.
|
|
373
663
|
message.subagent = {
|
|
374
|
-
|
|
375
|
-
|
|
664
|
+
id: agentId,
|
|
665
|
+
...subagent?.info,
|
|
666
|
+
description: subagent?.info.description
|
|
376
667
|
?? (typeof message.toolUseResult?.description === 'string' ? message.toolUseResult.description : undefined),
|
|
377
|
-
model: subagent
|
|
668
|
+
model: subagent?.info.model
|
|
378
669
|
?? (typeof message.toolUseResult?.resolvedModel === 'string' ? message.toolUseResult.resolvedModel : undefined),
|
|
379
|
-
status
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
670
|
+
status,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
else if (launch === 'workflow') {
|
|
674
|
+
// The notification's own word is kept here, `stopped` included: a
|
|
675
|
+
// workflow the user stopped is not one that failed.
|
|
676
|
+
const status = notification
|
|
677
|
+
? notification.status === 'completed' || notification.status === 'stopped' ? notification.status : 'failed'
|
|
678
|
+
: unreportedStatus;
|
|
679
|
+
// The journal marks an agent `started` and later `result`/`failed`.
|
|
680
|
+
// Once the run itself has settled, a `started` with no second record
|
|
681
|
+
// is not still going: the run was stopped under it, or a resume re-ran
|
|
682
|
+
// the step under a new id. Drawing those as running put a pulsing dot
|
|
683
|
+
// on a finished card.
|
|
684
|
+
const agents = (workflowAgentsByDir.get(String(message.toolUseResult.transcriptDir ?? '')) ?? [])
|
|
685
|
+
.map((agent) => (status !== 'running' && agent.status === 'running' ? { ...agent, status: 'stopped' } : agent));
|
|
686
|
+
const countWith = (agentStatus) => agents.filter((agent) => agent.status === agentStatus).length;
|
|
687
|
+
message.workflow = {
|
|
688
|
+
runId: String(message.toolUseResult.runId ?? ''),
|
|
689
|
+
name: String(message.toolUseResult.workflowName ?? ''),
|
|
690
|
+
description: typeof message.toolUseResult.summary === 'string' ? message.toolUseResult.summary : undefined,
|
|
691
|
+
status,
|
|
692
|
+
agents,
|
|
693
|
+
agentCounts: {
|
|
694
|
+
total: agents.length,
|
|
695
|
+
completed: countWith('completed'),
|
|
696
|
+
failed: countWith('failed'),
|
|
697
|
+
running: countWith('running'),
|
|
698
|
+
stopped: countWith('stopped'),
|
|
699
|
+
},
|
|
700
|
+
scriptPath: typeof message.toolUseResult.scriptPath === 'string' ? message.toolUseResult.scriptPath : undefined,
|
|
384
701
|
};
|
|
385
702
|
}
|
|
386
703
|
if (notification) {
|
|
387
|
-
|
|
388
|
-
|
|
704
|
+
replaceLaunchToolResultContent(message, notification.result || notification.summary);
|
|
705
|
+
for (const sourceUuid of notification.sourceUuids) {
|
|
706
|
+
foldedNotificationUuids.add(sourceUuid);
|
|
707
|
+
}
|
|
389
708
|
}
|
|
390
|
-
else if (
|
|
391
|
-
// Without a notification there is no answer to show, and
|
|
392
|
-
// acknowledgement is internal bookkeeping the
|
|
393
|
-
|
|
709
|
+
else if (isAsyncLaunch && launch !== 'bash') {
|
|
710
|
+
// Without a notification there is no answer to show, and an agent's
|
|
711
|
+
// or workflow's launch acknowledgement is internal bookkeeping the
|
|
712
|
+
// user must never read. A backgrounded command's acknowledgement is
|
|
713
|
+
// different: it names the output file, which is the only handle on
|
|
714
|
+
// the command's output until it reports, so it stays.
|
|
715
|
+
replaceLaunchToolResultContent(message, '');
|
|
394
716
|
}
|
|
395
717
|
}
|
|
396
718
|
const sortedMessages = messages
|
|
@@ -489,14 +811,156 @@ function buildLocalCommandDisplayText(payload) {
|
|
|
489
811
|
}
|
|
490
812
|
return commandArgs ? `${baseCommand} ${commandArgs}` : baseCommand;
|
|
491
813
|
}
|
|
814
|
+
const readOptionalString = (value) => typeof value === 'string' && value ? value : undefined;
|
|
815
|
+
/**
|
|
816
|
+
* The model an assistant row reports, or undefined when there is nothing
|
|
817
|
+
* honest to report.
|
|
818
|
+
*
|
|
819
|
+
* Rows Claude Code fabricated locally — a usage-limit notice, an API-error
|
|
820
|
+
* placeholder — are stamped `<synthetic>`; labelling one with a model would
|
|
821
|
+
* claim a request that never ran, so those are dropped here rather than
|
|
822
|
+
* guessed at further down.
|
|
823
|
+
*/
|
|
824
|
+
const readAssistantModel = (value) => {
|
|
825
|
+
const model = readOptionalString(value)?.trim();
|
|
826
|
+
return model && !isPlaceholderProviderModel(model) ? model : undefined;
|
|
827
|
+
};
|
|
828
|
+
/** The CLI's `{total_tokens, tool_uses, duration_ms}` in the app's spelling. */
|
|
829
|
+
function readTaskUsage(value) {
|
|
830
|
+
const usage = readObjectRecord(value);
|
|
831
|
+
if (!usage) {
|
|
832
|
+
return undefined;
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
totalTokens: Number(usage.total_tokens) || 0,
|
|
836
|
+
toolUses: Number(usage.tool_uses) || 0,
|
|
837
|
+
durationMs: Number(usage.duration_ms) || 0,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
const readOptionalNumber = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
841
|
+
/**
|
|
842
|
+
* The SDK's word on one workflow agent, in the app's spelling.
|
|
843
|
+
*
|
|
844
|
+
* The SDK reports `start` for an agent the script has queued as well as one
|
|
845
|
+
* that is running; only an `agentId` — the transcript the agent writes — tells
|
|
846
|
+
* the two apart. `progress` is a running agent that has reported, `done` one
|
|
847
|
+
* that finished, and anything else (`error`, `failed`) one that did not.
|
|
848
|
+
*/
|
|
849
|
+
function readWorkflowAgentState(state, agentId) {
|
|
850
|
+
switch (state) {
|
|
851
|
+
case 'start':
|
|
852
|
+
return agentId ? 'running' : 'queued';
|
|
853
|
+
case 'progress':
|
|
854
|
+
return 'running';
|
|
855
|
+
case 'done':
|
|
856
|
+
return 'done';
|
|
857
|
+
default:
|
|
858
|
+
return 'failed';
|
|
859
|
+
}
|
|
860
|
+
}
|
|
492
861
|
/**
|
|
493
|
-
*
|
|
494
|
-
*
|
|
862
|
+
* The per-agent progress a workflow's `task_progress` carries in its
|
|
863
|
+
* undocumented `workflow_progress`, or undefined when the event has none (an
|
|
864
|
+
* agent's or shell command's progress, or a workflow that has not spawned).
|
|
495
865
|
*/
|
|
496
|
-
function
|
|
497
|
-
|
|
866
|
+
function readWorkflowAgentsProgress(value) {
|
|
867
|
+
if (!Array.isArray(value)) {
|
|
868
|
+
return undefined;
|
|
869
|
+
}
|
|
870
|
+
const agents = [];
|
|
871
|
+
for (const raw of value) {
|
|
872
|
+
const entry = readObjectRecord(raw);
|
|
873
|
+
const index = readOptionalNumber(entry?.index);
|
|
874
|
+
if (!entry || entry.type !== 'workflow_agent' || index === undefined) {
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const agentId = readOptionalString(entry.agentId);
|
|
878
|
+
agents.push({
|
|
879
|
+
index,
|
|
880
|
+
label: readOptionalString(entry.label),
|
|
881
|
+
phase: readOptionalString(entry.phaseTitle),
|
|
882
|
+
agentId,
|
|
883
|
+
model: readOptionalString(entry.model),
|
|
884
|
+
state: readWorkflowAgentState(entry.state, agentId),
|
|
885
|
+
startedAt: readOptionalNumber(entry.startedAt),
|
|
886
|
+
lastToolName: readOptionalString(entry.lastToolName),
|
|
887
|
+
lastToolSummary: readOptionalString(entry.lastToolSummary),
|
|
888
|
+
promptPreview: readOptionalString(entry.promptPreview),
|
|
889
|
+
tokens: readOptionalNumber(entry.tokens),
|
|
890
|
+
toolCalls: readOptionalNumber(entry.toolCalls),
|
|
891
|
+
durationMs: readOptionalNumber(entry.durationMs),
|
|
892
|
+
resultPreview: readOptionalString(entry.resultPreview),
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
return agents;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Maps one of the SDK's four background-task `system` events onto the
|
|
899
|
+
* `task_status` fields, or returns null for any other `system` subtype.
|
|
900
|
+
*
|
|
901
|
+
* `task_started` and `task_progress` name the tool call that launched the
|
|
902
|
+
* task; `task_updated` carries only the task id and a patch of what changed,
|
|
903
|
+
* so the client has to remember the id from the start event. The patch's
|
|
904
|
+
* `killed` is spelled `stopped` here, the word the task notification and the
|
|
905
|
+
* history reader already use for a task that ended without an outcome.
|
|
906
|
+
*/
|
|
907
|
+
function readTaskStatusEvent(raw) {
|
|
908
|
+
const taskId = readOptionalString(raw.task_id);
|
|
909
|
+
if (!taskId) {
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
const shared = {
|
|
913
|
+
taskId,
|
|
914
|
+
toolUseId: readOptionalString(raw.tool_use_id),
|
|
915
|
+
};
|
|
916
|
+
switch (raw.subtype) {
|
|
917
|
+
case 'task_started':
|
|
918
|
+
return {
|
|
919
|
+
...shared,
|
|
920
|
+
event: 'started',
|
|
921
|
+
taskType: readOptionalString(raw.task_type),
|
|
922
|
+
workflowName: readOptionalString(raw.workflow_name),
|
|
923
|
+
description: readOptionalString(raw.description),
|
|
924
|
+
};
|
|
925
|
+
case 'task_progress':
|
|
926
|
+
// A workflow's progress reports on each agent it spawned. Its
|
|
927
|
+
// task-level `last_tool_name` is the current agent's label rather than
|
|
928
|
+
// a tool — and nothing draws a task's, so it is not forwarded; the
|
|
929
|
+
// client reads the current agent from `agents`.
|
|
930
|
+
return {
|
|
931
|
+
...shared,
|
|
932
|
+
event: 'progress',
|
|
933
|
+
description: readOptionalString(raw.description),
|
|
934
|
+
summary: readOptionalString(raw.summary),
|
|
935
|
+
usage: readTaskUsage(raw.usage),
|
|
936
|
+
agents: readWorkflowAgentsProgress(raw.workflow_progress),
|
|
937
|
+
};
|
|
938
|
+
case 'task_updated': {
|
|
939
|
+
const patched = readOptionalString(readObjectRecord(raw.patch)?.status);
|
|
940
|
+
return {
|
|
941
|
+
...shared,
|
|
942
|
+
event: 'updated',
|
|
943
|
+
status: patched === 'killed' ? 'stopped' : patched,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
case 'task_notification':
|
|
947
|
+
return {
|
|
948
|
+
...shared,
|
|
949
|
+
event: 'notification',
|
|
950
|
+
status: readOptionalString(raw.status),
|
|
951
|
+
summary: readOptionalString(raw.summary),
|
|
952
|
+
usage: readTaskUsage(raw.usage),
|
|
953
|
+
outputFile: readOptionalString(raw.output_file),
|
|
954
|
+
};
|
|
955
|
+
default:
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
498
958
|
}
|
|
499
959
|
export class ClaudeSessionsProvider {
|
|
960
|
+
getLiveRunStartTime;
|
|
961
|
+
constructor({ getLiveRunStartTime = getClaudeSDKSessionStartTime } = {}) {
|
|
962
|
+
this.getLiveRunStartTime = getLiveRunStartTime;
|
|
963
|
+
}
|
|
500
964
|
/**
|
|
501
965
|
* Normalizes one Claude JSONL entry or live SDK stream event into the shared
|
|
502
966
|
* message shape consumed by REST and WebSocket clients.
|
|
@@ -536,6 +1000,80 @@ export class ClaudeSessionsProvider {
|
|
|
536
1000
|
const messages = [];
|
|
537
1001
|
const ts = raw.timestamp || new Date().toISOString();
|
|
538
1002
|
const baseId = raw.uuid || generateMessageId('claude');
|
|
1003
|
+
// The live stream reports on background work — a launched workflow, agent
|
|
1004
|
+
// or shell command — through four `system` subtypes that the transcript
|
|
1005
|
+
// never records. They normalize to one `task_status` kind so the client
|
|
1006
|
+
// can keep a workflow's card current while it runs; before this they fell
|
|
1007
|
+
// through every branch below and were dropped.
|
|
1008
|
+
if (raw.type === 'system') {
|
|
1009
|
+
const taskStatus = readTaskStatusEvent(raw);
|
|
1010
|
+
if (taskStatus) {
|
|
1011
|
+
return [createNormalizedMessage({
|
|
1012
|
+
id: baseId,
|
|
1013
|
+
sessionId,
|
|
1014
|
+
timestamp: ts,
|
|
1015
|
+
provider: PROVIDER,
|
|
1016
|
+
kind: 'task_status',
|
|
1017
|
+
...taskStatus,
|
|
1018
|
+
})];
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
// A compaction is the most expensive thing a long session does without
|
|
1022
|
+
// being asked, and neither record that describes it survives the branches
|
|
1023
|
+
// below: `system` events normalize to nothing. Both become one ordinary
|
|
1024
|
+
// assistant row, so a client that knows nothing of `compact` still reads
|
|
1025
|
+
// the sentence, while one that does can draw it as a row of its own.
|
|
1026
|
+
if (raw.type === 'system' && (raw.subtype === 'compact_boundary' || raw.subtype === 'status')) {
|
|
1027
|
+
const compactionRow = (content, compact) => [
|
|
1028
|
+
createNormalizedMessage({
|
|
1029
|
+
id: `${baseId}_compact`,
|
|
1030
|
+
sessionId,
|
|
1031
|
+
timestamp: ts,
|
|
1032
|
+
provider: PROVIDER,
|
|
1033
|
+
kind: 'text',
|
|
1034
|
+
role: 'assistant',
|
|
1035
|
+
content,
|
|
1036
|
+
compact,
|
|
1037
|
+
}),
|
|
1038
|
+
];
|
|
1039
|
+
if (raw.subtype === 'compact_boundary') {
|
|
1040
|
+
// `compact_metadata` live, `compactMetadata` on disk.
|
|
1041
|
+
const metadata = readObjectRecord(raw.compact_metadata ?? raw.compactMetadata) ?? {};
|
|
1042
|
+
const trigger = metadata.trigger === 'manual' ? 'manual' : 'auto';
|
|
1043
|
+
const preTokens = Number(metadata.pre_tokens ?? metadata.preTokens) || 0;
|
|
1044
|
+
const postTokens = Number(metadata.post_tokens ?? metadata.postTokens) || 0;
|
|
1045
|
+
const durationMs = Number(metadata.duration_ms ?? metadata.durationMs) || 0;
|
|
1046
|
+
const said = ['Compacted', trigger];
|
|
1047
|
+
if (preTokens) {
|
|
1048
|
+
said.push(postTokens
|
|
1049
|
+
? `${formatTokenCount(preTokens)} → ${formatTokenCount(postTokens)} tokens`
|
|
1050
|
+
: `${formatTokenCount(preTokens)} tokens`);
|
|
1051
|
+
}
|
|
1052
|
+
if (durationMs) {
|
|
1053
|
+
said.push(formatDuration(durationMs));
|
|
1054
|
+
}
|
|
1055
|
+
return compactionRow(said.join(' · '), {
|
|
1056
|
+
phase: 'done',
|
|
1057
|
+
trigger,
|
|
1058
|
+
preTokens,
|
|
1059
|
+
postTokens,
|
|
1060
|
+
durationMs,
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
// A status message reports several things, and compaction is the only one
|
|
1064
|
+
// with anything to say: every other turn sends `requesting` or nothing.
|
|
1065
|
+
if (raw.status === 'compacting') {
|
|
1066
|
+
return compactionRow('Compacting conversation…', { phase: 'running' });
|
|
1067
|
+
}
|
|
1068
|
+
if (raw.compact_result === 'failed') {
|
|
1069
|
+
const error = typeof raw.compact_error === 'string' ? raw.compact_error : null;
|
|
1070
|
+
return compactionRow(`Compaction failed${error ? `: ${error}` : ''}`, {
|
|
1071
|
+
phase: 'failed',
|
|
1072
|
+
error,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
return [];
|
|
1076
|
+
}
|
|
539
1077
|
if (raw.message?.role === 'user' && raw.message?.content && raw.isMeta !== true) {
|
|
540
1078
|
if (Array.isArray(raw.message.content)) {
|
|
541
1079
|
// Image attachments sent through the SDK are persisted as base64
|
|
@@ -562,7 +1100,14 @@ export class ClaudeSessionsProvider {
|
|
|
562
1100
|
toolId: part.tool_use_id,
|
|
563
1101
|
content: typeof part.content === 'string' ? part.content : JSON.stringify(part.content),
|
|
564
1102
|
isError: Boolean(part.is_error),
|
|
565
|
-
toolUseResult
|
|
1103
|
+
// `toolUseResult` on disk, `tool_use_result` on the live SDK
|
|
1104
|
+
// stream. Reading only the transcript key meant a live agent
|
|
1105
|
+
// launch reached the client with no `isAsync`, so the card could
|
|
1106
|
+
// not tell the launch acknowledgement from an answer. The stream
|
|
1107
|
+
// value is the tool's whole structured output — an Edit carries
|
|
1108
|
+
// the file it edited — so it gets the same per-string cap the
|
|
1109
|
+
// transcript path applies before anything is sent or buffered.
|
|
1110
|
+
toolUseResult: truncateNestedOutput(raw.toolUseResult ?? raw.tool_use_result),
|
|
566
1111
|
}));
|
|
567
1112
|
}
|
|
568
1113
|
else if (part.type === 'text') {
|
|
@@ -679,7 +1224,7 @@ export class ClaudeSessionsProvider {
|
|
|
679
1224
|
*/
|
|
680
1225
|
const localCommandStdout = extractTaggedContent(text, 'local-command-stdout');
|
|
681
1226
|
if (localCommandStdout !== null) {
|
|
682
|
-
const stdoutText =
|
|
1227
|
+
const stdoutText = stripAnsiSequences(localCommandStdout).trim();
|
|
683
1228
|
if (stdoutText) {
|
|
684
1229
|
messages.push(createNormalizedMessage({
|
|
685
1230
|
id: baseId,
|
|
@@ -749,6 +1294,12 @@ export class ClaudeSessionsProvider {
|
|
|
749
1294
|
return messages;
|
|
750
1295
|
}
|
|
751
1296
|
if (raw.message?.role === 'assistant' && raw.message?.content) {
|
|
1297
|
+
// Which model actually answered. It rides on the assistant row itself in
|
|
1298
|
+
// both the transcript and the live SDK stream, so history renders it
|
|
1299
|
+
// retroactively with no migration. Only prose carries it: a tool call has
|
|
1300
|
+
// no footer to show it in, and a locally-fabricated row (`<synthetic>`,
|
|
1301
|
+
// e.g. the usage-limit notice) gets no label rather than a guessed one.
|
|
1302
|
+
const answeredByModel = readAssistantModel(raw.message.model);
|
|
752
1303
|
if (Array.isArray(raw.message.content)) {
|
|
753
1304
|
let partIndex = 0;
|
|
754
1305
|
for (const part of raw.message.content) {
|
|
@@ -761,6 +1312,7 @@ export class ClaudeSessionsProvider {
|
|
|
761
1312
|
kind: 'text',
|
|
762
1313
|
role: 'assistant',
|
|
763
1314
|
content: part.text,
|
|
1315
|
+
model: answeredByModel,
|
|
764
1316
|
}));
|
|
765
1317
|
}
|
|
766
1318
|
else if (part.type === 'tool_use') {
|
|
@@ -797,6 +1349,7 @@ export class ClaudeSessionsProvider {
|
|
|
797
1349
|
kind: 'text',
|
|
798
1350
|
role: 'assistant',
|
|
799
1351
|
content: raw.message.content,
|
|
1352
|
+
model: answeredByModel,
|
|
800
1353
|
}));
|
|
801
1354
|
}
|
|
802
1355
|
return messages;
|
|
@@ -845,6 +1398,60 @@ export class ClaudeSessionsProvider {
|
|
|
845
1398
|
}
|
|
846
1399
|
return { found: true, resumeThroughId: null };
|
|
847
1400
|
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Reads one workflow agent's timeline from the transcript the run wrote for
|
|
1403
|
+
* it, under `<projectDir>/<providerSessionId>/subagents/workflows/<runId>/`
|
|
1404
|
+
* — the only place it exists, since the SDK never streams a workflow agent's
|
|
1405
|
+
* rows to the parent session.
|
|
1406
|
+
*
|
|
1407
|
+
* The journal beside it settles the agent when it has; short of that the
|
|
1408
|
+
* agent is running only while the process that spawned it is — the same
|
|
1409
|
+
* rule history applies to a launch row, keyed here on the agent's first row
|
|
1410
|
+
* rather than the launch's.
|
|
1411
|
+
*/
|
|
1412
|
+
async readWorkflowAgentActivity(sessionId, runId, agentId) {
|
|
1413
|
+
const session = sessionsDb.getSessionById(sessionId);
|
|
1414
|
+
const jsonlPath = session?.jsonl_path;
|
|
1415
|
+
const providerSessionId = session?.provider_session_id;
|
|
1416
|
+
if (!jsonlPath || !providerSessionId) {
|
|
1417
|
+
return null;
|
|
1418
|
+
}
|
|
1419
|
+
const transcriptDir = path.join(path.dirname(jsonlPath), providerSessionId, 'subagents', 'workflows', runId);
|
|
1420
|
+
const transcriptPath = path.join(transcriptDir, `agent-${agentId}.jsonl`);
|
|
1421
|
+
try {
|
|
1422
|
+
await fsp.access(transcriptPath);
|
|
1423
|
+
}
|
|
1424
|
+
catch {
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
const [transcript, journalAgents] = await Promise.all([
|
|
1428
|
+
readClaudeSubagentTranscript(transcriptPath),
|
|
1429
|
+
readWorkflowJournal(transcriptDir),
|
|
1430
|
+
]);
|
|
1431
|
+
const journalAgent = journalAgents.find((agent) => agent.id === agentId);
|
|
1432
|
+
let status;
|
|
1433
|
+
if (journalAgent && journalAgent.status !== 'running') {
|
|
1434
|
+
status = journalAgent.status;
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
const liveRunStartedAt = this.getLiveRunStartTime(sessionId) ?? this.getLiveRunStartTime(providerSessionId);
|
|
1438
|
+
status = liveRunStartedAt !== null && Date.parse(transcript.startedAt ?? '') >= liveRunStartedAt
|
|
1439
|
+
? 'running'
|
|
1440
|
+
: 'stopped';
|
|
1441
|
+
}
|
|
1442
|
+
return {
|
|
1443
|
+
agent: {
|
|
1444
|
+
id: agentId,
|
|
1445
|
+
label: journalAgent?.label,
|
|
1446
|
+
model: transcript.model,
|
|
1447
|
+
status,
|
|
1448
|
+
},
|
|
1449
|
+
activity: transcript.activity
|
|
1450
|
+
.slice(-MAX_TRANSMITTED_SUBAGENT_ACTIVITIES)
|
|
1451
|
+
.map(truncateSubagentActivity),
|
|
1452
|
+
activityCount: transcript.activity.length,
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
848
1455
|
/**
|
|
849
1456
|
* Loads Claude JSONL history for a project/session and returns normalized
|
|
850
1457
|
* messages, preserving the existing pagination behavior from projects.js.
|
|
@@ -856,7 +1463,7 @@ export class ClaudeSessionsProvider {
|
|
|
856
1463
|
try {
|
|
857
1464
|
// Load full history first so `total` reflects frontend-normalized messages,
|
|
858
1465
|
// not raw JSONL records.
|
|
859
|
-
result = await getSessionMessages(sessionId, providerSessionId, null, 0);
|
|
1466
|
+
result = await getSessionMessages(sessionId, providerSessionId, null, 0, this.getLiveRunStartTime);
|
|
860
1467
|
}
|
|
861
1468
|
catch (error) {
|
|
862
1469
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -874,6 +1481,7 @@ export class ClaudeSessionsProvider {
|
|
|
874
1481
|
isError: Boolean(part.is_error),
|
|
875
1482
|
subagentTools: raw.subagentTools,
|
|
876
1483
|
subagent: raw.subagent,
|
|
1484
|
+
workflow: raw.workflow,
|
|
877
1485
|
toolUseResult: raw.toolUseResult,
|
|
878
1486
|
});
|
|
879
1487
|
}
|
|
@@ -899,6 +1507,7 @@ export class ClaudeSessionsProvider {
|
|
|
899
1507
|
};
|
|
900
1508
|
msg.subagentTools = toolResult.subagentTools;
|
|
901
1509
|
msg.subagent = toolResult.subagent;
|
|
1510
|
+
msg.workflow = toolResult.workflow;
|
|
902
1511
|
}
|
|
903
1512
|
}
|
|
904
1513
|
// Everything the transcript draws, and nothing else — so a page of N rows
|