@cloudcli-ai/cloudcli 1.37.2 → 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 +269 -0
- package/dist/assets/{abnfDiagram-N423BO3Z-C4UAoHJb.js → abnfDiagram-N423BO3Z-Dmkwx38H.js} +1 -1
- package/dist/assets/{arc-B8jF-ja9.js → arc-DLe9AYuO.js} +1 -1
- package/dist/assets/{architectureDiagram-T3A2C74G-C_Wur_CU.js → architectureDiagram-T3A2C74G-CBbbbNUW.js} +1 -1
- package/dist/assets/{blockDiagram-VBNYF7ZC-Cq4P1i8t.js → blockDiagram-VBNYF7ZC-CaMEaWu4.js} +1 -1
- package/dist/assets/{c4Diagram-5PPSVZJV-C1wG4eUj.js → c4Diagram-5PPSVZJV-DhjMgNnR.js} +1 -1
- package/dist/assets/channel-CWrmwsir.js +1 -0
- package/dist/assets/{chunk-2GRJ4B5K-B8fnBG6E.js → chunk-2GRJ4B5K-yjl3-F-i.js} +1 -1
- package/dist/assets/{chunk-2Q5K7J3B-WdfPooPJ.js → chunk-2Q5K7J3B-DsAYaWEP.js} +1 -1
- package/dist/assets/{chunk-5RXB4S5H-DhZJuG2E.js → chunk-5RXB4S5H-Bt5qmqOX.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-B0rjslal.js → chunk-5VM5RSS4-yWrWVvUt.js} +1 -1
- package/dist/assets/{chunk-6Q2QTUOP-CG2Oj4jL.js → chunk-6Q2QTUOP-Dt66K5xb.js} +1 -1
- package/dist/assets/{chunk-GF5L2VYU-rrl2lRxY.js → chunk-GF5L2VYU-DA0vzaj4.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CjVbrVMA.js → chunk-JWPE2WC7-CUkbx96V.js} +1 -1
- package/dist/assets/{chunk-KBJHAD2P-CRzKgU8S.js → chunk-KBJHAD2P-DWKxx1WH.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-DBz4GTZF.js → chunk-RYQCIY6F-BqluplN0.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-wHxalsXk.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-s1hygY8e.js → cose-bilkent-JH36ORCC-DHV93-Em.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-C4LGV15i.js → cynefin-VYW2F7L2-B9HZgoGX.js} +1 -1
- package/dist/assets/{cynefinDiagram-MW4NZA55-C5uhu7B3.js → cynefinDiagram-MW4NZA55-hFjjEQg8.js} +1 -1
- package/dist/assets/{dagre-VZM6K2ZE-B-OBTewm.js → dagre-VZM6K2ZE-CpeVOC7V.js} +1 -1
- package/dist/assets/{diagram-7IWD3JNH-BjGKQxih.js → diagram-7IWD3JNH-D0H1UoRA.js} +1 -1
- package/dist/assets/{diagram-B4RE2ZJO-BMKz1X2L.js → diagram-B4RE2ZJO-6Y7BQUup.js} +1 -1
- package/dist/assets/{diagram-LBJQPF4R-DNh-GhKC.js → diagram-LBJQPF4R-C0nNOwht.js} +1 -1
- package/dist/assets/{diagram-Q27KOJAE-BaTG9jPT.js → diagram-Q27KOJAE-En6QlOVX.js} +1 -1
- package/dist/assets/{diagram-UB23O5K3-BUwo2OvJ.js → diagram-UB23O5K3-CmPiNeGD.js} +1 -1
- package/dist/assets/{ebnfDiagram-BXEA7PRR-DAZgfzX2.js → ebnfDiagram-BXEA7PRR-Bygzp1Qt.js} +1 -1
- package/dist/assets/{erDiagram-JOGREHBK-1w1CcGEb.js → erDiagram-JOGREHBK-CwScRfzl.js} +1 -1
- package/dist/assets/{flowDiagram-UKHOOZJN-D4EII8Jj.js → flowDiagram-UKHOOZJN-bKX-0xP6.js} +1 -1
- package/dist/assets/{ganttDiagram-PKOTCBZU-ChvJZAgB.js → ganttDiagram-PKOTCBZU-DtZPKXGd.js} +1 -1
- package/dist/assets/{gitGraphDiagram-DS77QQ5N-DtOn41S8.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-DN_PVEJZ.js → infoDiagram-6WML65LV-ciRQTS0h.js} +1 -1
- package/dist/assets/{ishikawaDiagram-WSZJBQD7-DbkF1kXb.js → ishikawaDiagram-WSZJBQD7-CgjhQZ84.js} +1 -1
- package/dist/assets/{journeyDiagram-NVQOT4AX-pJwmhddE.js → journeyDiagram-NVQOT4AX-BsbQSeIW.js} +1 -1
- package/dist/assets/{kanban-definition-27J2QSJJ-PPAPgNp5.js → kanban-definition-27J2QSJJ-CrwzY9Jw.js} +1 -1
- package/dist/assets/{linear-iPR-J9f7.js → linear-BUaCDr6l.js} +1 -1
- package/dist/assets/{mermaid.core-BEUL86Z6.js → mermaid.core-ClgdUU0j.js} +5 -5
- package/dist/assets/{mindmap-definition-FAOFIHXS-BvZoBib7.js → mindmap-definition-FAOFIHXS-BictljY1.js} +1 -1
- package/dist/assets/{pegDiagram-VL7TDLO6-DhYreaUd.js → pegDiagram-VL7TDLO6-BXdByy_U.js} +1 -1
- package/dist/assets/{pieDiagram-7S7Q4E2Y-CrOHlyD0.js → pieDiagram-7S7Q4E2Y-uZsMfkRX.js} +1 -1
- package/dist/assets/{quadrantDiagram-CIZ2JOQS-D4g5yH3t.js → quadrantDiagram-CIZ2JOQS-7lCv4QIj.js} +1 -1
- package/dist/assets/{railroadDiagram-AXF67PYL-Dj39lHlf.js → railroadDiagram-AXF67PYL-07dxIWbi.js} +1 -1
- package/dist/assets/{requirementDiagram-LRYGKXZP-1AS4Zgm_.js → requirementDiagram-LRYGKXZP-Cb31Yv1_.js} +1 -1
- package/dist/assets/{sankeyDiagram-W5VNT64P-CKlMn19P.js → sankeyDiagram-W5VNT64P-X0YPSyF9.js} +1 -1
- package/dist/assets/{sequenceDiagram-SI44F4Z6-B_j3qyvo.js → sequenceDiagram-SI44F4Z6-BdBxSIlP.js} +1 -1
- package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
- package/dist/assets/{sizeCapture-X5ZJPWSS-Bp2Op16v.js → sizeCapture-X5ZJPWSS-DQM5xAyD.js} +1 -1
- package/dist/assets/{stateDiagram-OKZ733FA-C0cN6DoY.js → stateDiagram-OKZ733FA-DJ3xkLUF.js} +1 -1
- package/dist/assets/stateDiagram-v2-UEYNNEHI-vAaE4zNo.js +1 -0
- package/dist/assets/{swimlanes-SLNWSIFB-BEvXOV9b.js → swimlanes-SLNWSIFB-rjfYdW0w.js} +2 -2
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-TV2IF4Nq.js +8 -0
- package/dist/assets/{timeline-definition-Z64GVDOM-zdJQinvZ.js → timeline-definition-Z64GVDOM-DZMra5do.js} +1 -1
- package/dist/assets/{vendor-codemirror-OwyKSvPE.js → vendor-codemirror-CYrSt7-U.js} +18 -18
- package/dist/assets/{vennDiagram-T6HMQDX7-DuF1uZMY.js → vennDiagram-T6HMQDX7-BevD8uu0.js} +1 -1
- package/dist/assets/{wardleyDiagram-T6FBY63Y-B9AlLQup.js → wardleyDiagram-T6FBY63Y-DsIyXB7z.js} +1 -1
- package/dist/assets/{xychartDiagram-ELKLHX3M-DTJX48Qg.js → xychartDiagram-ELKLHX3M-G3PlF3ko.js} +1 -1
- package/dist/index.html +3 -3
- package/dist-server/server/index.js +11 -4
- 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 +177 -130
- 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 +48 -2
- package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
- package/dist-server/server/modules/browser-use/browser-use.routes.js +39 -33
- package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -1
- package/dist-server/server/modules/database/connection.js +1 -1
- package/dist-server/server/modules/database/connection.js.map +1 -1
- package/dist-server/server/modules/database/index.js +5 -0
- package/dist-server/server/modules/database/index.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +45 -1
- 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/scheduled-messages.db.js +71 -0
- package/dist-server/server/modules/database/repositories/scheduled-messages.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/session-drafts.db.js +105 -0
- package/dist-server/server/modules/database/repositories/session-drafts.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +144 -5
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/repositories/user-preferences.db.js +65 -0
- package/dist-server/server/modules/database/repositories/user-preferences.db.js.map +1 -0
- package/dist-server/server/modules/database/schema.js +99 -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/database/tests/sessions.db.integration.test.js +39 -0
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
- package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js +105 -0
- package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js.map +1 -0
- 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/services/project-delete.service.js +12 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-star.service.js +2 -2
- package/dist-server/server/modules/projects/services/project-star.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/index.js +3 -0
- package/dist-server/server/modules/providers/index.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +62 -9
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js +46 -0
- package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +31 -12
- 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 +472 -53
- 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 +1007 -60
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +4 -1
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js +204 -0
- package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js +32 -0
- package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +47 -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 +71 -53
- package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +8 -0
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +1621 -431
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
- package/dist-server/server/modules/providers/list/codex/codex.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 +16 -8
- package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +274 -9
- 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 +134 -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 +23 -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/provider-token-usage.service.js +139 -35
- package/dist-server/server/modules/providers/services/provider-token-usage.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 +91 -0
- package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +105 -32
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +3 -60
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +252 -12
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-auth.test.js +183 -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-models.test.js +39 -0
- package/dist-server/server/modules/providers/tests/claude-models.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 +1815 -33
- 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/claude-subagent-echo.test.js +16 -0
- package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-token-budget.test.js +85 -0
- package/dist-server/server/modules/providers/tests/claude-token-budget.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-message-editing.test.js +338 -0
- package/dist-server/server/modules/providers/tests/codex-message-editing.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 +83 -0
- package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-session-fork.test.js +54 -0
- package/dist-server/server/modules/providers/tests/codex-session-fork.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js +354 -15
- 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 +196 -4
- 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-token-usage.service.test.js +164 -1
- package/dist-server/server/modules/providers/tests/provider-token-usage.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-fork.test.js +113 -0
- package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +139 -0
- package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js +90 -0
- package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js +122 -0
- package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/sessions.service.test.js +219 -1
- package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/skills.test.js +15 -3
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
- package/dist-server/server/modules/scheduled-messages/index.js +5 -0
- package/dist-server/server/modules/scheduled-messages/index.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js +47 -0
- package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +175 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js +95 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +290 -0
- package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -0
- package/dist-server/server/modules/taskmaster/taskmaster.routes.js +23 -23
- package/dist-server/server/modules/taskmaster/taskmaster.routes.js.map +1 -1
- package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js +48 -0
- package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js.map +1 -1
- package/dist-server/server/modules/user/tests/user.service.test.js +70 -0
- package/dist-server/server/modules/user/tests/user.service.test.js.map +1 -1
- package/dist-server/server/modules/user/user.module.js +10 -1
- package/dist-server/server/modules/user/user.module.js.map +1 -1
- package/dist-server/server/modules/user/user.routes.js +44 -0
- package/dist-server/server/modules/user/user.routes.js.map +1 -1
- package/dist-server/server/modules/user/user.service.js +75 -0
- package/dist-server/server/modules/user/user.service.js.map +1 -1
- package/dist-server/server/modules/voice/tests/voice.service.test.js +3 -2
- package/dist-server/server/modules/voice/tests/voice.service.test.js.map +1 -1
- package/dist-server/server/modules/websocket/index.js +7 -0
- package/dist-server/server/modules/websocket/index.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +37 -49
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +35 -5
- package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +204 -10
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js +94 -0
- package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +236 -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-edit-send.test.js +263 -0
- package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +29 -3
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
- 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/session-upsert-broadcast.test.js +106 -0
- package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +500 -0
- package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
- package/dist-server/server/shared/claude-cli-path.js +22 -5
- package/dist-server/server/shared/claude-cli-path.js.map +1 -1
- package/dist-server/server/shared/index.js +3 -0
- package/dist-server/server/shared/index.js.map +1 -0
- package/dist-server/server/shared/message-unification.js +376 -0
- package/dist-server/server/shared/message-unification.js.map +1 -0
- package/dist-server/server/shared/tests/claude-cli-path.test.js +45 -1
- package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -1
- package/dist-server/server/shared/tests/message-unification.test.js +174 -0
- package/dist-server/server/shared/tests/message-unification.test.js.map +1 -0
- 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 +160 -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 +20 -17
- package/server/index.ts +16 -5
- package/server/modules/agent/agent.module.ts +11 -1
- package/server/modules/agent/agent.routes.ts +200 -142
- package/server/modules/agent/tests/agent-run-registration.test.ts +348 -0
- package/server/modules/agent/tests/agent.routes.test.ts +51 -2
- package/server/modules/browser-use/browser-use.routes.ts +96 -33
- package/server/modules/database/connection.ts +2 -1
- package/server/modules/database/index.ts +10 -0
- package/server/modules/database/migrations.ts +51 -0
- package/server/modules/database/repositories/provider-models.ts +79 -6
- package/server/modules/database/repositories/scheduled-messages.db.ts +127 -0
- package/server/modules/database/repositories/session-drafts.db.ts +171 -0
- package/server/modules/database/repositories/sessions.db.ts +204 -3
- package/server/modules/database/repositories/user-preferences.db.ts +80 -0
- package/server/modules/database/schema.ts +103 -0
- package/server/modules/database/tests/provider-models.db.integration.test.ts +113 -0
- package/server/modules/database/tests/sessions.db.integration.test.ts +52 -0
- package/server/modules/database/tests/user-preferences.db.integration.test.ts +135 -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/services/project-delete.service.ts +13 -1
- package/server/modules/projects/services/project-star.service.ts +2 -2
- 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/index.ts +4 -0
- package/server/modules/providers/list/claude/claude-auth.provider.ts +81 -11
- package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
- package/server/modules/providers/list/claude/claude-models.provider.ts +37 -17
- package/server/modules/providers/list/claude/claude-runtime.provider.js +510 -54
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +40 -14
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +1195 -63
- package/server/modules/providers/list/claude/claude-skills.provider.ts +4 -1
- package/server/modules/providers/list/claude/claude.provider.ts +3 -0
- package/server/modules/providers/list/codex/codex-app-server.client.ts +243 -0
- package/server/modules/providers/list/codex/codex-fork.provider.ts +40 -0
- package/server/modules/providers/list/codex/codex-models.provider.ts +47 -0
- package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +105 -70
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +1847 -455
- package/server/modules/providers/list/codex/codex.provider.ts +3 -0
- package/server/modules/providers/list/cursor/cursor-auth.provider.ts +10 -2
- package/server/modules/providers/list/cursor/cursor-runtime.provider.js +17 -8
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +297 -9
- 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 +162 -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 +40 -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/provider-token-usage.service.ts +166 -39
- package/server/modules/providers/services/session-conversations-search.service.ts +2 -5
- package/server/modules/providers/services/session-history-cache.service.ts +150 -0
- package/server/modules/providers/services/session-synchronizer.service.ts +125 -38
- package/server/modules/providers/services/sessions-watcher.service.ts +3 -64
- package/server/modules/providers/services/sessions.service.ts +309 -17
- package/server/modules/providers/tests/claude-auth.test.ts +208 -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-models.test.ts +87 -0
- package/server/modules/providers/tests/claude-runtime-hold.test.ts +309 -0
- package/server/modules/providers/tests/claude-sessions.test.ts +2167 -37
- package/server/modules/providers/tests/claude-settings-permissions.test.ts +271 -0
- package/server/modules/providers/tests/claude-subagent-echo.test.ts +25 -0
- package/server/modules/providers/tests/claude-token-budget.test.ts +104 -0
- package/server/modules/providers/tests/codex-message-editing.test.ts +419 -0
- package/server/modules/providers/tests/codex-models.test.ts +37 -0
- package/server/modules/providers/tests/codex-runtime.test.ts +94 -0
- package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
- package/server/modules/providers/tests/codex-sessions.test.ts +396 -15
- package/server/modules/providers/tests/cursor-auth.test.ts +187 -0
- package/server/modules/providers/tests/opencode-models.test.ts +266 -8
- 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-token-usage.service.test.ts +188 -1
- package/server/modules/providers/tests/provider.routes.test.ts +230 -1
- package/server/modules/providers/tests/session-fork.test.ts +173 -0
- package/server/modules/providers/tests/session-history-cache.service.test.ts +165 -0
- package/server/modules/providers/tests/session-orphan-prune.test.ts +107 -0
- package/server/modules/providers/tests/session-synchronizer-coalescing.test.ts +136 -0
- package/server/modules/providers/tests/sessions.service.test.ts +303 -1
- package/server/modules/providers/tests/skills.test.ts +21 -5
- package/server/modules/scheduled-messages/index.ts +8 -0
- package/server/modules/scheduled-messages/scheduled-messages.routes.ts +68 -0
- package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +216 -0
- package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
- package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +354 -0
- package/server/modules/taskmaster/taskmaster.routes.ts +23 -33
- package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +57 -0
- package/server/modules/user/tests/user.service.test.ts +90 -0
- package/server/modules/user/user.module.ts +10 -1
- package/server/modules/user/user.routes.ts +44 -0
- package/server/modules/user/user.service.ts +105 -0
- package/server/modules/voice/tests/voice.service.test.ts +3 -2
- package/server/modules/websocket/README.md +4 -2
- package/server/modules/websocket/index.ts +8 -0
- package/server/modules/websocket/services/chat-run-registry.service.ts +45 -54
- package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
- package/server/modules/websocket/services/chat-websocket.service.ts +302 -16
- package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
- package/server/modules/websocket/services/shell-websocket.service.ts +276 -22
- package/server/modules/websocket/tests/chat-background-subscribe.test.ts +167 -0
- package/server/modules/websocket/tests/chat-edit-send.test.ts +309 -0
- package/server/modules/websocket/tests/chat-run-registry.test.ts +34 -3
- package/server/modules/websocket/tests/chat-stop-task.test.ts +127 -0
- package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
- package/server/modules/websocket/tests/shell-websocket.service.test.ts +629 -0
- package/server/shared/claude-cli-path.ts +24 -6
- package/server/shared/index.ts +3 -0
- package/server/shared/interfaces.ts +91 -0
- package/server/shared/message-unification.ts +449 -0
- package/server/shared/tests/claude-cli-path.test.ts +58 -1
- package/server/shared/tests/message-unification.test.ts +218 -0
- 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 +382 -7
- package/server/shared/utils.ts +176 -0
- package/dist/assets/channel-_falYbU1.js +0 -1
- package/dist/assets/classDiagram-JCYQIIEL-Dn-3CfpR.js +0 -1
- package/dist/assets/classDiagram-v2-OCEON4UE-Dn-3CfpR.js +0 -1
- package/dist/assets/index-CzPLBvXT.css +0 -32
- package/dist/assets/index-DFkBBuSA.js +0 -1549
- package/dist/assets/stateDiagram-v2-UEYNNEHI-BvBxUUrZ.js +0 -1
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-CsE-x4YK.js +0 -8
|
@@ -4,17 +4,67 @@ import path from 'node:path';
|
|
|
4
4
|
import readline from 'node:readline';
|
|
5
5
|
|
|
6
6
|
import type { IProviderSessions } from '@/shared/interfaces.js';
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
AnyRecord,
|
|
9
|
+
CompactionInfo,
|
|
10
|
+
FetchHistoryOptions,
|
|
11
|
+
FetchHistoryResult,
|
|
12
|
+
NormalizedMessage,
|
|
13
|
+
SubagentActivity,
|
|
14
|
+
SubagentInfo,
|
|
15
|
+
TaskUsage,
|
|
16
|
+
WorkflowAgentActivity,
|
|
17
|
+
WorkflowAgentInfo,
|
|
18
|
+
WorkflowAgentProgress,
|
|
19
|
+
WorkflowInfo,
|
|
20
|
+
} from '@/shared/types.js';
|
|
8
21
|
import { parseFilesInputTag } from '@/shared/image-attachments.js';
|
|
9
|
-
import {
|
|
22
|
+
import { prepareTranscriptMessages, truncateNestedOutput } from '@/shared/message-unification.js';
|
|
23
|
+
import {
|
|
24
|
+
createNormalizedMessage,
|
|
25
|
+
generateMessageId,
|
|
26
|
+
isPlaceholderProviderModel,
|
|
27
|
+
readObjectRecord,
|
|
28
|
+
sliceTailPage,
|
|
29
|
+
stripAnsiSequences,
|
|
30
|
+
truncateSubagentActivity,
|
|
31
|
+
} from '@/shared/utils.js';
|
|
10
32
|
import { sessionsDb } from '@/modules/database/index.js';
|
|
33
|
+
import { getClaudeSDKSessionStartTime } from '@/modules/providers/list/claude/claude-runtime.provider.js';
|
|
34
|
+
import { summarizeClaudeTokenUsage } from '@/modules/providers/services/provider-token-usage.service.js';
|
|
11
35
|
|
|
12
36
|
const PROVIDER = 'claude';
|
|
13
37
|
|
|
38
|
+
/** Tokens the way the CLI writes them: 725k rather than 724,871. */
|
|
39
|
+
const formatTokenCount = (tokens: number): string => {
|
|
40
|
+
if (tokens >= 1_000_000) {
|
|
41
|
+
return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
42
|
+
}
|
|
43
|
+
return tokens >= 1_000 ? `${Math.round(tokens / 1_000)}k` : `${tokens}`;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** A duration the way the CLI writes one: `2m 22s`. */
|
|
47
|
+
const formatDuration = (milliseconds: number): string => {
|
|
48
|
+
const seconds = Math.round(milliseconds / 1_000);
|
|
49
|
+
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Upper bound on how much of a subagent's timeline is sent to the client. A
|
|
54
|
+
* long-running agent can record hundreds of tool calls, and the transcript only
|
|
55
|
+
* ever shows them behind a collapsed header, so shipping the whole history on
|
|
56
|
+
* every load costs far more than it shows. The newest steps are the ones
|
|
57
|
+
* kept: a running agent's timeline is polled for what it is doing now, and
|
|
58
|
+
* the card says how many earlier ones were left out.
|
|
59
|
+
*/
|
|
60
|
+
const MAX_TRANSMITTED_SUBAGENT_ACTIVITIES = 200;
|
|
61
|
+
|
|
14
62
|
type ClaudeToolResult = {
|
|
15
63
|
content: unknown;
|
|
16
64
|
isError: boolean;
|
|
17
|
-
subagentTools?:
|
|
65
|
+
subagentTools?: SubagentActivity[];
|
|
66
|
+
subagent?: SubagentInfo;
|
|
67
|
+
workflow?: WorkflowInfo;
|
|
18
68
|
toolUseResult?: unknown;
|
|
19
69
|
};
|
|
20
70
|
|
|
@@ -36,8 +86,24 @@ type ClaudeHistoryMessagesResult =
|
|
|
36
86
|
limit?: number | null;
|
|
37
87
|
};
|
|
38
88
|
|
|
39
|
-
|
|
40
|
-
|
|
89
|
+
type ClaudeSubagentTranscript = {
|
|
90
|
+
activity: SubagentActivity[];
|
|
91
|
+
model?: string;
|
|
92
|
+
/** When the agent's first row was written: the moment it was spawned. */
|
|
93
|
+
startedAt?: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Flattens one subagent transcript into the shared activity timeline.
|
|
98
|
+
*
|
|
99
|
+
* Assistant prose and reasoning are kept alongside the tool calls so the
|
|
100
|
+
* transcript can replay what the agent actually did rather than listing tool
|
|
101
|
+
* names with no narrative.
|
|
102
|
+
*/
|
|
103
|
+
async function readClaudeSubagentTranscript(filePath: string): Promise<ClaudeSubagentTranscript> {
|
|
104
|
+
const activity: SubagentActivity[] = [];
|
|
105
|
+
const transcript: ClaudeSubagentTranscript = { activity };
|
|
106
|
+
const toolsById = new Map<string, SubagentActivity>();
|
|
41
107
|
|
|
42
108
|
try {
|
|
43
109
|
const fileStream = fs.createReadStream(filePath);
|
|
@@ -53,16 +119,37 @@ async function parseAgentTools(filePath: string): Promise<AnyRecord[]> {
|
|
|
53
119
|
|
|
54
120
|
try {
|
|
55
121
|
const entry = JSON.parse(line) as AnyRecord;
|
|
122
|
+
const timestamp = typeof entry.timestamp === 'string' ? entry.timestamp : undefined;
|
|
123
|
+
if (timestamp && !transcript.startedAt) {
|
|
124
|
+
transcript.startedAt = timestamp;
|
|
125
|
+
}
|
|
56
126
|
|
|
57
127
|
if (entry.message?.role === 'assistant' && Array.isArray(entry.message?.content)) {
|
|
128
|
+
if (typeof entry.message.model === 'string') {
|
|
129
|
+
transcript.model = entry.message.model;
|
|
130
|
+
}
|
|
131
|
+
|
|
58
132
|
for (const part of entry.message.content as AnyRecord[]) {
|
|
59
133
|
if (part.type === 'tool_use') {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
134
|
+
const tool: SubagentActivity = {
|
|
135
|
+
kind: 'tool',
|
|
136
|
+
toolId: String(part.id ?? ''),
|
|
137
|
+
toolName: String(part.name ?? 'Tool'),
|
|
63
138
|
toolInput: part.input,
|
|
64
|
-
timestamp
|
|
65
|
-
}
|
|
139
|
+
timestamp,
|
|
140
|
+
};
|
|
141
|
+
activity.push(tool);
|
|
142
|
+
if (tool.toolId) {
|
|
143
|
+
toolsById.set(tool.toolId, tool);
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (part.type === 'text' && typeof part.text === 'string' && part.text.trim()) {
|
|
148
|
+
activity.push({ kind: 'text', content: part.text, timestamp });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (part.type === 'thinking' && typeof part.thinking === 'string' && part.thinking.trim()) {
|
|
152
|
+
activity.push({ kind: 'thinking', content: part.thinking, timestamp });
|
|
66
153
|
}
|
|
67
154
|
}
|
|
68
155
|
}
|
|
@@ -73,7 +160,7 @@ async function parseAgentTools(filePath: string): Promise<AnyRecord[]> {
|
|
|
73
160
|
continue;
|
|
74
161
|
}
|
|
75
162
|
|
|
76
|
-
const tool =
|
|
163
|
+
const tool = toolsById.get(String(part.tool_use_id ?? ''));
|
|
77
164
|
if (!tool) {
|
|
78
165
|
continue;
|
|
79
166
|
}
|
|
@@ -99,14 +186,514 @@ async function parseAgentTools(filePath: string): Promise<AnyRecord[]> {
|
|
|
99
186
|
console.warn(`Error parsing agent file ${filePath}:`, message);
|
|
100
187
|
}
|
|
101
188
|
|
|
102
|
-
return
|
|
189
|
+
return transcript;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
type ClaudeSubagentMeta = {
|
|
193
|
+
agentType?: string;
|
|
194
|
+
description?: string;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/** Reads the sidecar `.meta.json` Claude writes next to a subagent transcript. */
|
|
198
|
+
async function readClaudeSubagentMeta(metaPath: string): Promise<ClaudeSubagentMeta> {
|
|
199
|
+
try {
|
|
200
|
+
const parsed = JSON.parse(await fsp.readFile(metaPath, 'utf8')) as AnyRecord;
|
|
201
|
+
return {
|
|
202
|
+
agentType: typeof parsed.agentType === 'string' ? parsed.agentType : undefined,
|
|
203
|
+
description: typeof parsed.description === 'string' ? parsed.description : undefined,
|
|
204
|
+
};
|
|
205
|
+
} catch {
|
|
206
|
+
return {};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Resolves where a subagent's transcript lives.
|
|
212
|
+
*
|
|
213
|
+
* Current Claude versions write it to
|
|
214
|
+
* `<projectDir>/<providerSessionId>/subagents/agent-<agentId>.jsonl`; older
|
|
215
|
+
* ones dropped it next to the parent transcript. Both are checked, newest
|
|
216
|
+
* layout first, because a project directory usually holds sessions from
|
|
217
|
+
* several CLI versions.
|
|
218
|
+
*/
|
|
219
|
+
async function findClaudeSubagentTranscript(
|
|
220
|
+
projectDirectory: string,
|
|
221
|
+
providerSessionId: string,
|
|
222
|
+
agentId: string,
|
|
223
|
+
): Promise<{ transcriptPath: string; metaPath: string } | null> {
|
|
224
|
+
const candidates = [
|
|
225
|
+
path.join(projectDirectory, providerSessionId, 'subagents', `agent-${agentId}.jsonl`),
|
|
226
|
+
path.join(projectDirectory, `agent-${agentId}.jsonl`),
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
for (const transcriptPath of candidates) {
|
|
230
|
+
try {
|
|
231
|
+
await fsp.access(transcriptPath);
|
|
232
|
+
return { transcriptPath, metaPath: transcriptPath.replace(/\.jsonl$/, '.meta.json') };
|
|
233
|
+
} catch {
|
|
234
|
+
// Try the next layout.
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Reads which agents a workflow run spawned and how far each got.
|
|
243
|
+
*
|
|
244
|
+
* The run appends one record per event to `<transcriptDir>/journal.jsonl`:
|
|
245
|
+
* `started` names the agent (and the label and phase the script gave it, when
|
|
246
|
+
* it gave any), `result` and `failed` settle it. The journal is what the
|
|
247
|
+
* workflow itself resumes from, so it is the only record of an agent that is
|
|
248
|
+
* still running — its transcript file is written, but nothing in it says the
|
|
249
|
+
* agent is not done. A missing directory or journal means the run left no
|
|
250
|
+
* record here (a fork copies only the parent's transcript), which reads as no
|
|
251
|
+
* agents rather than an error.
|
|
252
|
+
*/
|
|
253
|
+
async function readWorkflowJournal(transcriptDir: string): Promise<WorkflowAgentInfo[]> {
|
|
254
|
+
const agentsById = new Map<string, WorkflowAgentInfo>();
|
|
255
|
+
|
|
256
|
+
let journal: string;
|
|
257
|
+
try {
|
|
258
|
+
journal = await fsp.readFile(path.join(transcriptDir, 'journal.jsonl'), 'utf8');
|
|
259
|
+
} catch {
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const line of journal.split('\n')) {
|
|
264
|
+
if (!line.trim()) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let entry: AnyRecord;
|
|
269
|
+
try {
|
|
270
|
+
entry = JSON.parse(line) as AnyRecord;
|
|
271
|
+
} catch {
|
|
272
|
+
// The workflow appends while this reads, so the last line can be partial.
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const agentId = typeof entry.agentId === 'string' ? entry.agentId : '';
|
|
277
|
+
if (!agentId) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (entry.type === 'started') {
|
|
282
|
+
agentsById.set(agentId, {
|
|
283
|
+
id: agentId,
|
|
284
|
+
label: typeof entry.label === 'string' ? entry.label : undefined,
|
|
285
|
+
phase: typeof entry.phase === 'string' ? entry.phase : undefined,
|
|
286
|
+
status: 'running',
|
|
287
|
+
});
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const agent = agentsById.get(agentId);
|
|
292
|
+
if (!agent) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (entry.type === 'result') {
|
|
296
|
+
agent.status = 'completed';
|
|
297
|
+
} else if (entry.type === 'failed') {
|
|
298
|
+
agent.status = 'failed';
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return [...agentsById.values()];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Which kind of background work a tool-result row launched, read off the
|
|
307
|
+
* structured `toolUseResult` the CLI stores beside it.
|
|
308
|
+
*
|
|
309
|
+
* All three answer the same way — a launch acknowledgement now, a
|
|
310
|
+
* `<task-notification>` later — but each is recognised by its own key: an
|
|
311
|
+
* `Agent` by the `agentId` of the transcript it writes, a `Workflow` by the
|
|
312
|
+
* `async_launched` status it shares with an agent launch (it has no agent of
|
|
313
|
+
* its own), and a backgrounded `Bash` by the `backgroundTaskId` the shell
|
|
314
|
+
* reports. A synchronous `Agent` also carries `agentId` and is included here
|
|
315
|
+
* because its card is built the same way; only `isAsync` says it launched.
|
|
316
|
+
*/
|
|
317
|
+
type ClaudeBackgroundLaunch = 'agent' | 'workflow' | 'bash';
|
|
318
|
+
|
|
319
|
+
function readBackgroundLaunch(toolUseResult: unknown): ClaudeBackgroundLaunch | null {
|
|
320
|
+
const result = readObjectRecord(toolUseResult);
|
|
321
|
+
if (!result) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
if (result.agentId) {
|
|
325
|
+
return 'agent';
|
|
326
|
+
}
|
|
327
|
+
if (result.status === 'async_launched') {
|
|
328
|
+
return 'workflow';
|
|
329
|
+
}
|
|
330
|
+
if (typeof result.backgroundTaskId === 'string' && result.backgroundTaskId) {
|
|
331
|
+
return 'bash';
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
type ClaudeTaskNotification = {
|
|
337
|
+
/**
|
|
338
|
+
* `uuid`s of every transcript row that notified for this call, so all of
|
|
339
|
+
* them can be dropped once the latest one is folded. A task that resumes or
|
|
340
|
+
* is first reported as stopped and later as completed notifies more than
|
|
341
|
+
* once, and every earlier row is superseded bookkeeping. Queue-operation
|
|
342
|
+
* records carry no `uuid` and never render on their own, so they add
|
|
343
|
+
* nothing here.
|
|
344
|
+
*/
|
|
345
|
+
sourceUuids: string[];
|
|
346
|
+
toolUseId: string;
|
|
347
|
+
status: string;
|
|
348
|
+
summary: string;
|
|
349
|
+
result: string;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
function readTaggedValue(content: string, tagName: string): string {
|
|
353
|
+
const match = new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`).exec(content);
|
|
354
|
+
return match ? match[1].trim() : '';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Reads every `<task-notification>` payload one transcript row carries.
|
|
359
|
+
*
|
|
360
|
+
* Claude writes the same completion report in two shapes, and which one you get
|
|
361
|
+
* depends only on when the agent happened to finish:
|
|
362
|
+
*
|
|
363
|
+
* - as an ordinary user-role turn, when the queued notification is delivered
|
|
364
|
+
* between turns (the harness also logs a contentless `dequeue` for it);
|
|
365
|
+
* - as a top-level `queue-operation` record whose `content` holds the payload,
|
|
366
|
+
* when the harness consumes it inline while the parent turn is still running
|
|
367
|
+
* (`remove`), or when the session ends before it is ever delivered
|
|
368
|
+
* (`enqueue`).
|
|
369
|
+
*
|
|
370
|
+
* The operation names the queue's bookkeeping, not whether the agent finished,
|
|
371
|
+
* so every queue-operation payload counts. Reading only the user-role shape
|
|
372
|
+
* loses the majority of them: across the 919 transcripts this was measured on,
|
|
373
|
+
* 31 of 56 background agent launches report *only* through a queue-operation
|
|
374
|
+
* record, and their result and status were invisible to every consumer.
|
|
375
|
+
*/
|
|
376
|
+
function readTaskNotificationTexts(message: AnyRecord): string[] {
|
|
377
|
+
if (message.type === 'queue-operation') {
|
|
378
|
+
return typeof message.content === 'string' ? [message.content] : [];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (message.message?.role !== 'user') {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const content = message.message.content;
|
|
386
|
+
if (typeof content === 'string') {
|
|
387
|
+
return [content];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return Array.isArray(content)
|
|
391
|
+
? content.filter((part: AnyRecord) => part?.type === 'text').map((part: AnyRecord) => String(part.text ?? ''))
|
|
392
|
+
: [];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Collects every `<task-notification>` keyed by the tool call that spawned the
|
|
397
|
+
* agent it reports on.
|
|
398
|
+
*
|
|
399
|
+
* Only notifications that name a tool-use id are collected: without one there
|
|
400
|
+
* is no card to fold them into, and they must keep rendering on their own.
|
|
401
|
+
*/
|
|
402
|
+
function collectTaskNotifications(messages: AnyRecord[]): Map<string, ClaudeTaskNotification> {
|
|
403
|
+
const notifications = new Map<string, ClaudeTaskNotification>();
|
|
404
|
+
|
|
405
|
+
for (const message of messages) {
|
|
406
|
+
for (const text of readTaskNotificationTexts(message)) {
|
|
407
|
+
if (!text.trimStart().startsWith('<task-notification>')) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const toolUseId = readTaggedValue(text, 'tool-use-id');
|
|
412
|
+
if (!toolUseId) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// A resumed agent notifies more than once; the last word wins, and the
|
|
417
|
+
// rows that carried the earlier words go with it.
|
|
418
|
+
const sourceUuids = notifications.get(toolUseId)?.sourceUuids ?? [];
|
|
419
|
+
if (typeof message.uuid === 'string' && message.uuid) {
|
|
420
|
+
sourceUuids.push(message.uuid);
|
|
421
|
+
}
|
|
422
|
+
notifications.set(toolUseId, {
|
|
423
|
+
sourceUuids,
|
|
424
|
+
toolUseId,
|
|
425
|
+
status: readTaggedValue(text, 'status') || 'completed',
|
|
426
|
+
summary: readTaggedValue(text, 'summary'),
|
|
427
|
+
result: readTaggedValue(text, 'result'),
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return notifications;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Reads the `tool_use_id` off the tool-result row that launched background work. */
|
|
436
|
+
function readLaunchToolUseId(message: AnyRecord): string | null {
|
|
437
|
+
const content = message.message?.content;
|
|
438
|
+
if (!Array.isArray(content)) {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
for (const part of content) {
|
|
443
|
+
if (part?.type === 'tool_result' && typeof part.tool_use_id === 'string') {
|
|
444
|
+
return part.tool_use_id;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Swaps a launch acknowledgement for the work's actual answer. */
|
|
452
|
+
function replaceLaunchToolResultContent(message: AnyRecord, replacement: string): void {
|
|
453
|
+
const content = message.message?.content;
|
|
454
|
+
if (!Array.isArray(content)) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
for (const part of content) {
|
|
459
|
+
if (part?.type === 'tool_result') {
|
|
460
|
+
part.content = replacement;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Reads a Claude transcript's rows as a graph keyed by `uuid`.
|
|
467
|
+
*
|
|
468
|
+
* The file is append-only and every row names its predecessor in `parentUuid`,
|
|
469
|
+
* so a conversation is a path through it rather than the whole file. Editing a
|
|
470
|
+
* sent message makes a second path appear alongside the first.
|
|
471
|
+
*/
|
|
472
|
+
async function readTranscriptRows(jsonlPath: string, providerSessionId: string): Promise<AnyRecord[]> {
|
|
473
|
+
const rows: AnyRecord[] = [];
|
|
474
|
+
const fileStream = fs.createReadStream(jsonlPath);
|
|
475
|
+
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
476
|
+
|
|
477
|
+
for await (const line of rl) {
|
|
478
|
+
if (!line.trim()) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
const entry = JSON.parse(line) as AnyRecord;
|
|
483
|
+
if (entry.sessionId === providerSessionId) {
|
|
484
|
+
rows.push(entry);
|
|
485
|
+
}
|
|
486
|
+
} catch {
|
|
487
|
+
// A row can be half-written while the CLI is streaming into the file.
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return rows;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** True for a row the user typed, as opposed to a tool result or an injected note. */
|
|
495
|
+
function isUserPromptRow(row: AnyRecord): boolean {
|
|
496
|
+
if (row.type !== 'user' || row.isMeta === true || row.isCompactSummary === true) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const content = row.message?.content;
|
|
501
|
+
if (Array.isArray(content)) {
|
|
502
|
+
return content.some((part: AnyRecord) => part?.type === 'text' || part?.type === 'image');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return typeof content === 'string' && content.length > 0;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Returns the uuids on the chain the next `claude --resume` replays, from the
|
|
510
|
+
* leaf it continues from back to the root.
|
|
511
|
+
*
|
|
512
|
+
* Mirrors the transcript loader of the Claude CLI, which the Agent SDK's
|
|
513
|
+
* `resume` spawns as well. Every CLI process stamps a `last-prompt` row naming
|
|
514
|
+
* the leaf it was on when it exits, so with two writers on one session it is
|
|
515
|
+
* that stamp — not the newest row — that decides which branch is replayed. The
|
|
516
|
+
* newest conversation row only takes over when it descends from the stamped
|
|
517
|
+
* leaf (the stamping process kept going), or when the file carries no stamp.
|
|
518
|
+
* A compact boundary clears the stamp, and `progress` rows are skipped over
|
|
519
|
+
* when linking a row to its parent, as the CLI does.
|
|
520
|
+
*/
|
|
521
|
+
function resolveResumePath(rows: AnyRecord[]): Set<string> {
|
|
522
|
+
const conversation = new Map<string, AnyRecord>();
|
|
523
|
+
const progressParents = new Map<string, string | null>();
|
|
524
|
+
const parentOf = (row: AnyRecord): string | undefined => {
|
|
525
|
+
const parentUuid = typeof row.parentUuid === 'string' ? row.parentUuid : undefined;
|
|
526
|
+
if (parentUuid && progressParents.has(parentUuid)) {
|
|
527
|
+
return progressParents.get(parentUuid) ?? undefined;
|
|
528
|
+
}
|
|
529
|
+
return parentUuid;
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
let newestRow: string | undefined;
|
|
533
|
+
let stampedLeaf: string | undefined;
|
|
534
|
+
let stampIsExplicit = false;
|
|
535
|
+
|
|
536
|
+
for (const row of rows) {
|
|
537
|
+
if (row.type === 'progress' && typeof row.uuid === 'string') {
|
|
538
|
+
progressParents.set(row.uuid, parentOf(row) ?? null);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (typeof row.uuid === 'string'
|
|
542
|
+
&& (row.type === 'user' || row.type === 'assistant' || row.type === 'attachment' || row.type === 'system')) {
|
|
543
|
+
conversation.set(row.uuid, row);
|
|
544
|
+
if (row.isSidechain !== true) {
|
|
545
|
+
newestRow = row.uuid;
|
|
546
|
+
stampIsExplicit = false;
|
|
547
|
+
}
|
|
548
|
+
if (row.type === 'system' && row.subtype === 'compact_boundary') {
|
|
549
|
+
stampedLeaf = undefined;
|
|
550
|
+
stampIsExplicit = false;
|
|
551
|
+
}
|
|
552
|
+
} else if (row.type === 'last-prompt' && typeof row.leafUuid === 'string' && row.leafUuid) {
|
|
553
|
+
stampIsExplicit = row.explicit === true || (stampIsExplicit && row.leafUuid === stampedLeaf);
|
|
554
|
+
stampedLeaf = row.leafUuid;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const chainFrom = (leaf: string | undefined): string[] => {
|
|
559
|
+
const chain: string[] = [];
|
|
560
|
+
const seen = new Set<string>();
|
|
561
|
+
let cursor = leaf;
|
|
562
|
+
while (cursor && conversation.has(cursor) && !seen.has(cursor)) {
|
|
563
|
+
seen.add(cursor);
|
|
564
|
+
chain.push(cursor);
|
|
565
|
+
cursor = parentOf(conversation.get(cursor) as AnyRecord);
|
|
566
|
+
}
|
|
567
|
+
return chain;
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
let leaf = stampedLeaf && conversation.has(stampedLeaf) ? stampedLeaf : undefined;
|
|
571
|
+
if (leaf && !stampIsExplicit && newestRow && newestRow !== leaf && chainFrom(newestRow).includes(leaf)) {
|
|
572
|
+
leaf = newestRow;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return new Set(chainFrom(leaf ?? newestRow));
|
|
103
576
|
}
|
|
104
577
|
|
|
578
|
+
/**
|
|
579
|
+
* Maps each row's uuid to the file position of the newest row in its subtree.
|
|
580
|
+
*
|
|
581
|
+
* The transcript is append-only and a child is always written after its
|
|
582
|
+
* parent, so one backward pass hands every row's position to its parent and
|
|
583
|
+
* each uuid ends up holding the position of the last line its branch grew.
|
|
584
|
+
* Rows after a compact boundary do not link back across it, so this only
|
|
585
|
+
* orders branches within one compaction segment.
|
|
586
|
+
*/
|
|
587
|
+
function indexNewestRowPerBranch(rows: AnyRecord[]): Map<string, number> {
|
|
588
|
+
const newestByUuid = new Map<string, number>();
|
|
589
|
+
|
|
590
|
+
for (let index = rows.length - 1; index >= 0; index -= 1) {
|
|
591
|
+
const row = rows[index];
|
|
592
|
+
if (typeof row.uuid !== 'string') {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const newest = newestByUuid.get(row.uuid) ?? index;
|
|
596
|
+
newestByUuid.set(row.uuid, newest);
|
|
597
|
+
if (typeof row.parentUuid === 'string' && (newestByUuid.get(row.parentUuid) ?? -1) < newest) {
|
|
598
|
+
newestByUuid.set(row.parentUuid, newest);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return newestByUuid;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Drops the rows belonging to prompts the next resume will not replay.
|
|
607
|
+
*
|
|
608
|
+
* When a message is edited, Claude resumes the conversation partway and appends
|
|
609
|
+
* the replacement, so two prompts end up sharing one parent and the file holds
|
|
610
|
+
* both the abandoned attempt and the live one. A flat read would show them
|
|
611
|
+
* stacked, which reads as the app having sent the message twice. The same shape
|
|
612
|
+
* appears without an edit when a session is driven from Chat and from the
|
|
613
|
+
* native `claude` CLI at once: both writers branch off the row each of them saw
|
|
614
|
+
* last.
|
|
615
|
+
*
|
|
616
|
+
* Which sibling survives is decided by the branch the runtime will actually
|
|
617
|
+
* resume: the sibling on the chain `resolveResumePath` returns. Going by the
|
|
618
|
+
* newest prompt, or by the newest row, would show a branch the model was never
|
|
619
|
+
* sent whenever the two writers exit in a different order than they wrote. A
|
|
620
|
+
* fork that is not on that chain at all (one made before a compaction) keeps
|
|
621
|
+
* the branch that was appended to last.
|
|
622
|
+
*
|
|
623
|
+
* Only sibling *prompts* are treated as a fork. Branch points made by parallel
|
|
624
|
+
* tool calls are extremely common — one assistant turn writes several chained
|
|
625
|
+
* rows and each tool result parents onto its own — and pruning those would
|
|
626
|
+
* delete tool output from every transcript in the app.
|
|
627
|
+
*/
|
|
628
|
+
function dropSupersededPromptBranches(rows: AnyRecord[]): AnyRecord[] {
|
|
629
|
+
const promptSiblings = new Map<string, AnyRecord[]>();
|
|
630
|
+
for (const row of rows) {
|
|
631
|
+
if (typeof row.parentUuid !== 'string' || !isUserPromptRow(row)) {
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
const siblings = promptSiblings.get(row.parentUuid);
|
|
635
|
+
if (siblings) {
|
|
636
|
+
siblings.push(row);
|
|
637
|
+
} else {
|
|
638
|
+
promptSiblings.set(row.parentUuid, [row]);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const forks = [...promptSiblings.values()].filter((siblings) => siblings.length > 1);
|
|
643
|
+
if (forks.length === 0) {
|
|
644
|
+
return rows;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const resumePath = resolveResumePath(rows);
|
|
648
|
+
let newestByUuid: Map<string, number> | undefined;
|
|
649
|
+
const newestRowOf = (row: AnyRecord): number => {
|
|
650
|
+
newestByUuid ??= indexNewestRowPerBranch(rows);
|
|
651
|
+
return typeof row.uuid === 'string' ? newestByUuid.get(row.uuid) ?? -1 : -1;
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const supersededRoots = new Set<string>();
|
|
655
|
+
for (const siblings of forks) {
|
|
656
|
+
let live = siblings.find((row) => typeof row.uuid === 'string' && resumePath.has(row.uuid));
|
|
657
|
+
if (!live) {
|
|
658
|
+
live = siblings[0];
|
|
659
|
+
for (const row of siblings) {
|
|
660
|
+
if (newestRowOf(row) > newestRowOf(live)) {
|
|
661
|
+
live = row;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
for (const row of siblings) {
|
|
666
|
+
if (row !== live && typeof row.uuid === 'string') {
|
|
667
|
+
supersededRoots.add(row.uuid);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (supersededRoots.size === 0) {
|
|
673
|
+
return rows;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const abandoned = new Set(supersededRoots);
|
|
677
|
+
// Rows are appended in order, so one forward pass propagates each superseded
|
|
678
|
+
// root to its whole subtree.
|
|
679
|
+
for (const row of rows) {
|
|
680
|
+
if (typeof row.parentUuid === 'string' && abandoned.has(row.parentUuid) && typeof row.uuid === 'string') {
|
|
681
|
+
abandoned.add(row.uuid);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return rows.filter((row) => typeof row.uuid !== 'string' || !abandoned.has(row.uuid));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** When the CLI run behind a session started (epoch ms), or null when none is up. */
|
|
689
|
+
type LiveRunStartTimeProbe = (sessionId: string) => number | null;
|
|
690
|
+
|
|
105
691
|
async function getSessionMessages(
|
|
106
692
|
sessionId: string,
|
|
107
693
|
providerSessionId: string,
|
|
108
694
|
limit: number | null,
|
|
109
695
|
offset: number,
|
|
696
|
+
getLiveRunStartTime: LiveRunStartTimeProbe,
|
|
110
697
|
): Promise<ClaudeHistoryMessagesResult> {
|
|
111
698
|
try {
|
|
112
699
|
// The DB row is keyed by the app-facing session id, while the JSONL rows
|
|
@@ -118,32 +705,10 @@ async function getSessionMessages(
|
|
|
118
705
|
}
|
|
119
706
|
|
|
120
707
|
const projectDir = path.dirname(jsonLPath);
|
|
121
|
-
const files = await fsp.readdir(projectDir);
|
|
122
|
-
const agentFiles = files.filter((file) => file.endsWith('.jsonl') && file.startsWith('agent-'));
|
|
123
708
|
|
|
124
|
-
const messages
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const fileStream = fs.createReadStream(jsonLPath);
|
|
128
|
-
const rl = readline.createInterface({
|
|
129
|
-
input: fileStream,
|
|
130
|
-
crlfDelay: Infinity,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
for await (const line of rl) {
|
|
134
|
-
if (!line.trim()) {
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
const entry = JSON.parse(line) as AnyRecord;
|
|
140
|
-
if (entry.sessionId === providerSessionId) {
|
|
141
|
-
messages.push(entry);
|
|
142
|
-
}
|
|
143
|
-
} catch {
|
|
144
|
-
// Skip malformed JSONL lines that can happen during concurrent writes.
|
|
145
|
-
}
|
|
146
|
-
}
|
|
709
|
+
const messages = dropSupersededPromptBranches(
|
|
710
|
+
await readTranscriptRows(jsonLPath, providerSessionId),
|
|
711
|
+
);
|
|
147
712
|
|
|
148
713
|
const agentIds = new Set<string>();
|
|
149
714
|
for (const message of messages) {
|
|
@@ -153,30 +718,192 @@ async function getSessionMessages(
|
|
|
153
718
|
}
|
|
154
719
|
}
|
|
155
720
|
|
|
721
|
+
// Read each spawned agent's own transcript once, then hang it off every
|
|
722
|
+
// row that references it. Status is not part of this: the transcript
|
|
723
|
+
// cannot say whether the agent finished (see the loop below).
|
|
724
|
+
const subagentsById = new Map<string, {
|
|
725
|
+
activity: SubagentActivity[];
|
|
726
|
+
info: Omit<SubagentInfo, 'status'>;
|
|
727
|
+
}>();
|
|
156
728
|
for (const agentId of agentIds) {
|
|
157
|
-
const
|
|
158
|
-
if (!
|
|
729
|
+
const located = await findClaudeSubagentTranscript(projectDir, providerSessionId, agentId);
|
|
730
|
+
if (!located) {
|
|
159
731
|
continue;
|
|
160
732
|
}
|
|
161
733
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
734
|
+
const [transcript, meta] = await Promise.all([
|
|
735
|
+
readClaudeSubagentTranscript(located.transcriptPath),
|
|
736
|
+
readClaudeSubagentMeta(located.metaPath),
|
|
737
|
+
]);
|
|
738
|
+
|
|
739
|
+
subagentsById.set(agentId, {
|
|
740
|
+
activity: transcript.activity
|
|
741
|
+
.slice(-MAX_TRANSMITTED_SUBAGENT_ACTIVITIES)
|
|
742
|
+
.map(truncateSubagentActivity),
|
|
743
|
+
info: {
|
|
744
|
+
id: agentId,
|
|
745
|
+
name: meta.agentType,
|
|
746
|
+
type: meta.agentType,
|
|
747
|
+
description: meta.description,
|
|
748
|
+
model: transcript.model,
|
|
749
|
+
activityCount: transcript.activity.length,
|
|
750
|
+
},
|
|
751
|
+
});
|
|
165
752
|
}
|
|
166
753
|
|
|
754
|
+
// Read each workflow run's journal once, keyed by the directory the launch
|
|
755
|
+
// row names. The journal is the only thing that says which of a run's
|
|
756
|
+
// agents are still going — see `readWorkflowJournal`.
|
|
757
|
+
const workflowAgentsByDir = new Map<string, WorkflowAgentInfo[]>();
|
|
167
758
|
for (const message of messages) {
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
759
|
+
const transcriptDir = message.toolUseResult?.transcriptDir;
|
|
760
|
+
if (
|
|
761
|
+
readBackgroundLaunch(message.toolUseResult) === 'workflow'
|
|
762
|
+
&& typeof transcriptDir === 'string'
|
|
763
|
+
&& transcriptDir
|
|
764
|
+
&& !workflowAgentsByDir.has(transcriptDir)
|
|
765
|
+
) {
|
|
766
|
+
workflowAgentsByDir.set(transcriptDir, await readWorkflowJournal(transcriptDir));
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// A background launch's tool result is internal bookkeeping ("Async agent
|
|
771
|
+
// launched successfully…", "Workflow launched in background. Task ID: …",
|
|
772
|
+
// "Command running in background with ID: …"); its real answer arrives
|
|
773
|
+
// later as a separate `<task-notification>` turn. Folding the notification
|
|
774
|
+
// back onto the tool call that started the work keeps one card per launch
|
|
775
|
+
// instead of a card, an unrelated status line, and a stray markdown reply.
|
|
776
|
+
// Reading only rows with an `agentId` left a workflow's card on its launch
|
|
777
|
+
// acknowledgement forever, with its report rendering as a raw user bubble
|
|
778
|
+
// further down or, for a queue-operation record, nowhere at all.
|
|
779
|
+
const notificationsByToolUseId = collectTaskNotifications(messages);
|
|
780
|
+
const foldedNotificationUuids = new Set<string>();
|
|
781
|
+
|
|
782
|
+
// A background agent runs inside the CLI process that launched it, so once
|
|
783
|
+
// that process is gone an agent that has not reported back never will. A
|
|
784
|
+
// later run of the same session is a new process, so a launch row older
|
|
785
|
+
// than the live run belongs to a process that has already exited. The
|
|
786
|
+
// runtime keys its process map by the app session id when the chat gateway
|
|
787
|
+
// starts a run and by the provider-native id when the agent API does, so
|
|
788
|
+
// both are asked.
|
|
789
|
+
const liveRunStartedAt = getLiveRunStartTime(sessionId) ?? getLiveRunStartTime(providerSessionId);
|
|
790
|
+
const launchedByLiveRun = (message: AnyRecord): boolean =>
|
|
791
|
+
liveRunStartedAt !== null && Date.parse(String(message.timestamp ?? '')) >= liveRunStartedAt;
|
|
792
|
+
|
|
793
|
+
for (const message of messages) {
|
|
794
|
+
const launch = readBackgroundLaunch(message.toolUseResult);
|
|
795
|
+
if (!launch) {
|
|
170
796
|
continue;
|
|
171
797
|
}
|
|
172
798
|
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
799
|
+
const toolUseId = readLaunchToolUseId(message);
|
|
800
|
+
const notification = toolUseId ? notificationsByToolUseId.get(toolUseId) : undefined;
|
|
801
|
+
// A workflow and a backgrounded shell command have no foreground form;
|
|
802
|
+
// only an agent can hand its answer back inline.
|
|
803
|
+
const isAsyncLaunch = launch !== 'agent' || message.toolUseResult?.isAsync === true;
|
|
804
|
+
|
|
805
|
+
// An async launch row never tells you it finished — only the later
|
|
806
|
+
// notification does, so a missing notification means the outcome is
|
|
807
|
+
// still unknown.
|
|
808
|
+
//
|
|
809
|
+
// The agent's own transcript cannot stand in for that. A background agent
|
|
810
|
+
// routinely ends its turn cleanly while its work is still outstanding: it
|
|
811
|
+
// replies "I've started that, waiting for it to finish" and stops, or it
|
|
812
|
+
// has live background children of its own, which is precisely when the
|
|
813
|
+
// harness holds the notification back. Treating a cleanly-ended timeline
|
|
814
|
+
// as proof of completion therefore marked every background agent
|
|
815
|
+
// `completed` the moment it launched, collapsing its card and dropping
|
|
816
|
+
// its spinner while it was still working.
|
|
817
|
+
//
|
|
818
|
+
// "Unknown" only means "still outstanding" while the process the agent
|
|
819
|
+
// runs in is alive. Once it is gone — the run was stopped or crashed, or
|
|
820
|
+
// this transcript is a fork, which copies the parent's conversation rows
|
|
821
|
+
// but not its agent files or queue-operation records — nothing can still
|
|
822
|
+
// report, and reading that as `running` pinned a spinner on the card
|
|
823
|
+
// forever.
|
|
824
|
+
//
|
|
825
|
+
// A synchronous agent hands its answer back inline on its own tool
|
|
826
|
+
// result, so its status never depends on a notification or the process.
|
|
827
|
+
const unreportedStatus = isAsyncLaunch
|
|
828
|
+
? launchedByLiveRun(message) ? 'running' : 'stopped'
|
|
829
|
+
: 'completed';
|
|
830
|
+
|
|
831
|
+
if (launch === 'agent') {
|
|
832
|
+
const agentId = String(message.toolUseResult.agentId);
|
|
833
|
+
const subagent = subagentsById.get(agentId);
|
|
834
|
+
const status: SubagentInfo['status'] = notification
|
|
835
|
+
? notification.status === 'completed' ? 'completed' : 'failed'
|
|
836
|
+
: unreportedStatus;
|
|
837
|
+
|
|
838
|
+
if (subagent && subagent.activity.length > 0) {
|
|
839
|
+
message.subagentTools = subagent.activity;
|
|
840
|
+
}
|
|
841
|
+
// The agent's own transcript names its type, model and timeline, but
|
|
842
|
+
// it is not what says whether the agent finished, and it can be
|
|
843
|
+
// missing — a fork copies only the parent's `.jsonl`. The card reads
|
|
844
|
+
// its status off `subagent`, so the launch row publishes one with or
|
|
845
|
+
// without it; otherwise a notification saying "completed" left the
|
|
846
|
+
// card `running`.
|
|
847
|
+
message.subagent = {
|
|
848
|
+
id: agentId,
|
|
849
|
+
...subagent?.info,
|
|
850
|
+
description: subagent?.info.description
|
|
851
|
+
?? (typeof message.toolUseResult?.description === 'string' ? message.toolUseResult.description : undefined),
|
|
852
|
+
model: subagent?.info.model
|
|
853
|
+
?? (typeof message.toolUseResult?.resolvedModel === 'string' ? message.toolUseResult.resolvedModel : undefined),
|
|
854
|
+
status,
|
|
855
|
+
};
|
|
856
|
+
} else if (launch === 'workflow') {
|
|
857
|
+
// The notification's own word is kept here, `stopped` included: a
|
|
858
|
+
// workflow the user stopped is not one that failed.
|
|
859
|
+
const status: WorkflowInfo['status'] = notification
|
|
860
|
+
? notification.status === 'completed' || notification.status === 'stopped' ? notification.status : 'failed'
|
|
861
|
+
: unreportedStatus;
|
|
862
|
+
// The journal marks an agent `started` and later `result`/`failed`.
|
|
863
|
+
// Once the run itself has settled, a `started` with no second record
|
|
864
|
+
// is not still going: the run was stopped under it, or a resume re-ran
|
|
865
|
+
// the step under a new id. Drawing those as running put a pulsing dot
|
|
866
|
+
// on a finished card.
|
|
867
|
+
const agents = (workflowAgentsByDir.get(String(message.toolUseResult.transcriptDir ?? '')) ?? [])
|
|
868
|
+
.map((agent) => (status !== 'running' && agent.status === 'running' ? { ...agent, status: 'stopped' as const } : agent));
|
|
869
|
+
const countWith = (agentStatus: WorkflowAgentInfo['status']) =>
|
|
870
|
+
agents.filter((agent) => agent.status === agentStatus).length;
|
|
871
|
+
|
|
872
|
+
message.workflow = {
|
|
873
|
+
runId: String(message.toolUseResult.runId ?? ''),
|
|
874
|
+
name: String(message.toolUseResult.workflowName ?? ''),
|
|
875
|
+
description: typeof message.toolUseResult.summary === 'string' ? message.toolUseResult.summary : undefined,
|
|
876
|
+
status,
|
|
877
|
+
agents,
|
|
878
|
+
agentCounts: {
|
|
879
|
+
total: agents.length,
|
|
880
|
+
completed: countWith('completed'),
|
|
881
|
+
failed: countWith('failed'),
|
|
882
|
+
running: countWith('running'),
|
|
883
|
+
stopped: countWith('stopped'),
|
|
884
|
+
},
|
|
885
|
+
scriptPath: typeof message.toolUseResult.scriptPath === 'string' ? message.toolUseResult.scriptPath : undefined,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (notification) {
|
|
890
|
+
replaceLaunchToolResultContent(message, notification.result || notification.summary);
|
|
891
|
+
for (const sourceUuid of notification.sourceUuids) {
|
|
892
|
+
foldedNotificationUuids.add(sourceUuid);
|
|
893
|
+
}
|
|
894
|
+
} else if (isAsyncLaunch && launch !== 'bash') {
|
|
895
|
+
// Without a notification there is no answer to show, and an agent's
|
|
896
|
+
// or workflow's launch acknowledgement is internal bookkeeping the
|
|
897
|
+
// user must never read. A backgrounded command's acknowledgement is
|
|
898
|
+
// different: it names the output file, which is the only handle on
|
|
899
|
+
// the command's output until it reports, so it stays.
|
|
900
|
+
replaceLaunchToolResultContent(message, '');
|
|
176
901
|
}
|
|
177
902
|
}
|
|
178
903
|
|
|
179
|
-
const sortedMessages = messages
|
|
904
|
+
const sortedMessages = messages
|
|
905
|
+
.filter((message) => !foldedNotificationUuids.has(String(message.uuid ?? '')))
|
|
906
|
+
.sort(
|
|
180
907
|
(a, b) => new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime(),
|
|
181
908
|
);
|
|
182
909
|
const total = sortedMessages.length;
|
|
@@ -290,20 +1017,213 @@ function buildLocalCommandDisplayText(payload: ClaudeLocalCommandPayload): strin
|
|
|
290
1017
|
return commandArgs ? `${baseCommand} ${commandArgs}` : baseCommand;
|
|
291
1018
|
}
|
|
292
1019
|
|
|
1020
|
+
/** The `task_status` fields of a normalized message, minus the envelope. */
|
|
1021
|
+
type ClaudeTaskStatusEvent = Pick<
|
|
1022
|
+
NormalizedMessage,
|
|
1023
|
+
'event' | 'taskId' | 'toolUseId' | 'taskType' | 'workflowName' | 'description' | 'status' | 'summary' | 'usage' | 'outputFile' | 'agents'
|
|
1024
|
+
>;
|
|
1025
|
+
|
|
1026
|
+
const readOptionalString = (value: unknown): string | undefined =>
|
|
1027
|
+
typeof value === 'string' && value ? value : undefined;
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* The model an assistant row reports, or undefined when there is nothing
|
|
1031
|
+
* honest to report.
|
|
1032
|
+
*
|
|
1033
|
+
* Rows Claude Code fabricated locally — a usage-limit notice, an API-error
|
|
1034
|
+
* placeholder — are stamped `<synthetic>`; labelling one with a model would
|
|
1035
|
+
* claim a request that never ran, so those are dropped here rather than
|
|
1036
|
+
* guessed at further down.
|
|
1037
|
+
*/
|
|
1038
|
+
const readAssistantModel = (value: unknown): string | undefined => {
|
|
1039
|
+
const model = readOptionalString(value)?.trim();
|
|
1040
|
+
return model && !isPlaceholderProviderModel(model) ? model : undefined;
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
/** The CLI's `{total_tokens, tool_uses, duration_ms}` in the app's spelling. */
|
|
1044
|
+
function readTaskUsage(value: unknown): TaskUsage | undefined {
|
|
1045
|
+
const usage = readObjectRecord(value);
|
|
1046
|
+
if (!usage) {
|
|
1047
|
+
return undefined;
|
|
1048
|
+
}
|
|
1049
|
+
return {
|
|
1050
|
+
totalTokens: Number(usage.total_tokens) || 0,
|
|
1051
|
+
toolUses: Number(usage.tool_uses) || 0,
|
|
1052
|
+
durationMs: Number(usage.duration_ms) || 0,
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const readOptionalNumber = (value: unknown): number | undefined =>
|
|
1057
|
+
typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
1058
|
+
|
|
293
1059
|
/**
|
|
294
|
-
*
|
|
295
|
-
*
|
|
1060
|
+
* The SDK's word on one workflow agent, in the app's spelling.
|
|
1061
|
+
*
|
|
1062
|
+
* The SDK reports `start` for an agent the script has queued as well as one
|
|
1063
|
+
* that is running; only an `agentId` — the transcript the agent writes — tells
|
|
1064
|
+
* the two apart. `progress` is a running agent that has reported, `done` one
|
|
1065
|
+
* that finished, and anything else (`error`, `failed`) one that did not.
|
|
296
1066
|
*/
|
|
297
|
-
function
|
|
298
|
-
|
|
1067
|
+
function readWorkflowAgentState(state: unknown, agentId: string | undefined): WorkflowAgentProgress['state'] {
|
|
1068
|
+
switch (state) {
|
|
1069
|
+
case 'start':
|
|
1070
|
+
return agentId ? 'running' : 'queued';
|
|
1071
|
+
case 'progress':
|
|
1072
|
+
return 'running';
|
|
1073
|
+
case 'done':
|
|
1074
|
+
return 'done';
|
|
1075
|
+
default:
|
|
1076
|
+
return 'failed';
|
|
1077
|
+
}
|
|
299
1078
|
}
|
|
300
1079
|
|
|
1080
|
+
/**
|
|
1081
|
+
* The per-agent progress a workflow's `task_progress` carries in its
|
|
1082
|
+
* undocumented `workflow_progress`, or undefined when the event has none (an
|
|
1083
|
+
* agent's or shell command's progress, or a workflow that has not spawned).
|
|
1084
|
+
*/
|
|
1085
|
+
function readWorkflowAgentsProgress(value: unknown): WorkflowAgentProgress[] | undefined {
|
|
1086
|
+
if (!Array.isArray(value)) {
|
|
1087
|
+
return undefined;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const agents: WorkflowAgentProgress[] = [];
|
|
1091
|
+
for (const raw of value) {
|
|
1092
|
+
const entry = readObjectRecord(raw);
|
|
1093
|
+
const index = readOptionalNumber(entry?.index);
|
|
1094
|
+
if (!entry || entry.type !== 'workflow_agent' || index === undefined) {
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
const agentId = readOptionalString(entry.agentId);
|
|
1098
|
+
agents.push({
|
|
1099
|
+
index,
|
|
1100
|
+
label: readOptionalString(entry.label),
|
|
1101
|
+
phase: readOptionalString(entry.phaseTitle),
|
|
1102
|
+
agentId,
|
|
1103
|
+
model: readOptionalString(entry.model),
|
|
1104
|
+
state: readWorkflowAgentState(entry.state, agentId),
|
|
1105
|
+
startedAt: readOptionalNumber(entry.startedAt),
|
|
1106
|
+
lastToolName: readOptionalString(entry.lastToolName),
|
|
1107
|
+
lastToolSummary: readOptionalString(entry.lastToolSummary),
|
|
1108
|
+
promptPreview: readOptionalString(entry.promptPreview),
|
|
1109
|
+
tokens: readOptionalNumber(entry.tokens),
|
|
1110
|
+
toolCalls: readOptionalNumber(entry.toolCalls),
|
|
1111
|
+
durationMs: readOptionalNumber(entry.durationMs),
|
|
1112
|
+
resultPreview: readOptionalString(entry.resultPreview),
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
return agents;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Maps one of the SDK's four background-task `system` events onto the
|
|
1120
|
+
* `task_status` fields, or returns null for any other `system` subtype.
|
|
1121
|
+
*
|
|
1122
|
+
* `task_started` and `task_progress` name the tool call that launched the
|
|
1123
|
+
* task; `task_updated` carries only the task id and a patch of what changed,
|
|
1124
|
+
* so the client has to remember the id from the start event. The patch's
|
|
1125
|
+
* `killed` is spelled `stopped` here, the word the task notification and the
|
|
1126
|
+
* history reader already use for a task that ended without an outcome.
|
|
1127
|
+
*/
|
|
1128
|
+
function readTaskStatusEvent(raw: AnyRecord): ClaudeTaskStatusEvent | null {
|
|
1129
|
+
const taskId = readOptionalString(raw.task_id);
|
|
1130
|
+
if (!taskId) {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const shared = {
|
|
1135
|
+
taskId,
|
|
1136
|
+
toolUseId: readOptionalString(raw.tool_use_id),
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
switch (raw.subtype) {
|
|
1140
|
+
case 'task_started':
|
|
1141
|
+
return {
|
|
1142
|
+
...shared,
|
|
1143
|
+
event: 'started',
|
|
1144
|
+
taskType: readOptionalString(raw.task_type),
|
|
1145
|
+
workflowName: readOptionalString(raw.workflow_name),
|
|
1146
|
+
description: readOptionalString(raw.description),
|
|
1147
|
+
};
|
|
1148
|
+
case 'task_progress':
|
|
1149
|
+
// A workflow's progress reports on each agent it spawned. Its
|
|
1150
|
+
// task-level `last_tool_name` is the current agent's label rather than
|
|
1151
|
+
// a tool — and nothing draws a task's, so it is not forwarded; the
|
|
1152
|
+
// client reads the current agent from `agents`.
|
|
1153
|
+
return {
|
|
1154
|
+
...shared,
|
|
1155
|
+
event: 'progress',
|
|
1156
|
+
description: readOptionalString(raw.description),
|
|
1157
|
+
summary: readOptionalString(raw.summary),
|
|
1158
|
+
usage: readTaskUsage(raw.usage),
|
|
1159
|
+
agents: readWorkflowAgentsProgress(raw.workflow_progress),
|
|
1160
|
+
};
|
|
1161
|
+
case 'task_updated': {
|
|
1162
|
+
const patched = readOptionalString(readObjectRecord(raw.patch)?.status);
|
|
1163
|
+
return {
|
|
1164
|
+
...shared,
|
|
1165
|
+
event: 'updated',
|
|
1166
|
+
status: patched === 'killed' ? 'stopped' : patched,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
case 'task_notification':
|
|
1170
|
+
return {
|
|
1171
|
+
...shared,
|
|
1172
|
+
event: 'notification',
|
|
1173
|
+
status: readOptionalString(raw.status),
|
|
1174
|
+
summary: readOptionalString(raw.summary),
|
|
1175
|
+
usage: readTaskUsage(raw.usage),
|
|
1176
|
+
outputFile: readOptionalString(raw.output_file),
|
|
1177
|
+
};
|
|
1178
|
+
default:
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
type ClaudeSessionsProviderOptions = {
|
|
1184
|
+
/**
|
|
1185
|
+
* When the CLI run behind a session started, or null when none is up.
|
|
1186
|
+
* Injected so history tests can pin what a stopped run reads as without
|
|
1187
|
+
* driving a real SDK run.
|
|
1188
|
+
*/
|
|
1189
|
+
getLiveRunStartTime?: LiveRunStartTimeProbe;
|
|
1190
|
+
};
|
|
1191
|
+
|
|
301
1192
|
export class ClaudeSessionsProvider implements IProviderSessions {
|
|
1193
|
+
private readonly getLiveRunStartTime: LiveRunStartTimeProbe;
|
|
1194
|
+
|
|
1195
|
+
constructor({ getLiveRunStartTime = getClaudeSDKSessionStartTime }: ClaudeSessionsProviderOptions = {}) {
|
|
1196
|
+
this.getLiveRunStartTime = getLiveRunStartTime;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
302
1199
|
/**
|
|
303
1200
|
* Normalizes one Claude JSONL entry or live SDK stream event into the shared
|
|
304
1201
|
* message shape consumed by REST and WebSocket clients.
|
|
1202
|
+
*
|
|
1203
|
+
* Every user turn it produces is stamped with the transcript row's `uuid`,
|
|
1204
|
+
* which is the anchor "edit this message" and "fork from here" address. It is
|
|
1205
|
+
* applied here rather than at each `createNormalizedMessage` call because the
|
|
1206
|
+
* row-shape branches below have several exits, and an anchor missing from one
|
|
1207
|
+
* of them would show up as a message the user silently cannot edit.
|
|
305
1208
|
*/
|
|
306
1209
|
normalizeMessage(rawMessage: unknown, sessionId: string | null): NormalizedMessage[] {
|
|
1210
|
+
const messages = this.normalizeMessageRows(rawMessage, sessionId);
|
|
1211
|
+
// A synthesized id is useless as an anchor — it changes on every read — so
|
|
1212
|
+
// a row without its own uuid produces messages with no anchor at all.
|
|
1213
|
+
const raw = readObjectRecord(rawMessage);
|
|
1214
|
+
const anchorId = typeof raw?.uuid === 'string' && raw.uuid ? raw.uuid : null;
|
|
1215
|
+
if (anchorId) {
|
|
1216
|
+
for (const message of messages) {
|
|
1217
|
+
if (message.role === 'user') {
|
|
1218
|
+
message.transcriptAnchorId = anchorId;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
return messages;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
private normalizeMessageRows(rawMessage: unknown, sessionId: string | null): NormalizedMessage[] {
|
|
307
1227
|
const raw = readObjectRecord(rawMessage);
|
|
308
1228
|
if (!raw) {
|
|
309
1229
|
return [];
|
|
@@ -320,6 +1240,86 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
|
|
320
1240
|
const ts = raw.timestamp || new Date().toISOString();
|
|
321
1241
|
const baseId = raw.uuid || generateMessageId('claude');
|
|
322
1242
|
|
|
1243
|
+
// The live stream reports on background work — a launched workflow, agent
|
|
1244
|
+
// or shell command — through four `system` subtypes that the transcript
|
|
1245
|
+
// never records. They normalize to one `task_status` kind so the client
|
|
1246
|
+
// can keep a workflow's card current while it runs; before this they fell
|
|
1247
|
+
// through every branch below and were dropped.
|
|
1248
|
+
if (raw.type === 'system') {
|
|
1249
|
+
const taskStatus = readTaskStatusEvent(raw);
|
|
1250
|
+
if (taskStatus) {
|
|
1251
|
+
return [createNormalizedMessage({
|
|
1252
|
+
id: baseId,
|
|
1253
|
+
sessionId,
|
|
1254
|
+
timestamp: ts,
|
|
1255
|
+
provider: PROVIDER,
|
|
1256
|
+
kind: 'task_status',
|
|
1257
|
+
...taskStatus,
|
|
1258
|
+
})];
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// A compaction is the most expensive thing a long session does without
|
|
1263
|
+
// being asked, and neither record that describes it survives the branches
|
|
1264
|
+
// below: `system` events normalize to nothing. Both become one ordinary
|
|
1265
|
+
// assistant row, so a client that knows nothing of `compact` still reads
|
|
1266
|
+
// the sentence, while one that does can draw it as a row of its own.
|
|
1267
|
+
if (raw.type === 'system' && (raw.subtype === 'compact_boundary' || raw.subtype === 'status')) {
|
|
1268
|
+
const compactionRow = (content: string, compact: CompactionInfo): NormalizedMessage[] => [
|
|
1269
|
+
createNormalizedMessage({
|
|
1270
|
+
id: `${baseId}_compact`,
|
|
1271
|
+
sessionId,
|
|
1272
|
+
timestamp: ts,
|
|
1273
|
+
provider: PROVIDER,
|
|
1274
|
+
kind: 'text',
|
|
1275
|
+
role: 'assistant',
|
|
1276
|
+
content,
|
|
1277
|
+
compact,
|
|
1278
|
+
}),
|
|
1279
|
+
];
|
|
1280
|
+
|
|
1281
|
+
if (raw.subtype === 'compact_boundary') {
|
|
1282
|
+
// `compact_metadata` live, `compactMetadata` on disk.
|
|
1283
|
+
const metadata = readObjectRecord(raw.compact_metadata ?? raw.compactMetadata) ?? {};
|
|
1284
|
+
const trigger = metadata.trigger === 'manual' ? 'manual' : 'auto';
|
|
1285
|
+
const preTokens = Number(metadata.pre_tokens ?? metadata.preTokens) || 0;
|
|
1286
|
+
const postTokens = Number(metadata.post_tokens ?? metadata.postTokens) || 0;
|
|
1287
|
+
const durationMs = Number(metadata.duration_ms ?? metadata.durationMs) || 0;
|
|
1288
|
+
const said = ['Compacted', trigger];
|
|
1289
|
+
if (preTokens) {
|
|
1290
|
+
said.push(postTokens
|
|
1291
|
+
? `${formatTokenCount(preTokens)} → ${formatTokenCount(postTokens)} tokens`
|
|
1292
|
+
: `${formatTokenCount(preTokens)} tokens`);
|
|
1293
|
+
}
|
|
1294
|
+
if (durationMs) {
|
|
1295
|
+
said.push(formatDuration(durationMs));
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
return compactionRow(said.join(' · '), {
|
|
1299
|
+
phase: 'done',
|
|
1300
|
+
trigger,
|
|
1301
|
+
preTokens,
|
|
1302
|
+
postTokens,
|
|
1303
|
+
durationMs,
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// A status message reports several things, and compaction is the only one
|
|
1308
|
+
// with anything to say: every other turn sends `requesting` or nothing.
|
|
1309
|
+
if (raw.status === 'compacting') {
|
|
1310
|
+
return compactionRow('Compacting conversation…', { phase: 'running' });
|
|
1311
|
+
}
|
|
1312
|
+
if (raw.compact_result === 'failed') {
|
|
1313
|
+
const error = typeof raw.compact_error === 'string' ? raw.compact_error : null;
|
|
1314
|
+
return compactionRow(`Compaction failed${error ? `: ${error}` : ''}`, {
|
|
1315
|
+
phase: 'failed',
|
|
1316
|
+
error,
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
return [];
|
|
1321
|
+
}
|
|
1322
|
+
|
|
323
1323
|
if (raw.message?.role === 'user' && raw.message?.content && raw.isMeta !== true) {
|
|
324
1324
|
if (Array.isArray(raw.message.content)) {
|
|
325
1325
|
// Image attachments sent through the SDK are persisted as base64
|
|
@@ -347,8 +1347,14 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
|
|
347
1347
|
toolId: part.tool_use_id,
|
|
348
1348
|
content: typeof part.content === 'string' ? part.content : JSON.stringify(part.content),
|
|
349
1349
|
isError: Boolean(part.is_error),
|
|
350
|
-
|
|
351
|
-
|
|
1350
|
+
// `toolUseResult` on disk, `tool_use_result` on the live SDK
|
|
1351
|
+
// stream. Reading only the transcript key meant a live agent
|
|
1352
|
+
// launch reached the client with no `isAsync`, so the card could
|
|
1353
|
+
// not tell the launch acknowledgement from an answer. The stream
|
|
1354
|
+
// value is the tool's whole structured output — an Edit carries
|
|
1355
|
+
// the file it edited — so it gets the same per-string cap the
|
|
1356
|
+
// transcript path applies before anything is sent or buffered.
|
|
1357
|
+
toolUseResult: truncateNestedOutput(raw.toolUseResult ?? raw.tool_use_result),
|
|
352
1358
|
}));
|
|
353
1359
|
} else if (part.type === 'text') {
|
|
354
1360
|
const text = part.text || '';
|
|
@@ -470,7 +1476,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
|
|
470
1476
|
*/
|
|
471
1477
|
const localCommandStdout = extractTaggedContent(text, 'local-command-stdout');
|
|
472
1478
|
if (localCommandStdout !== null) {
|
|
473
|
-
const stdoutText =
|
|
1479
|
+
const stdoutText = stripAnsiSequences(localCommandStdout).trim();
|
|
474
1480
|
if (stdoutText) {
|
|
475
1481
|
messages.push(createNormalizedMessage({
|
|
476
1482
|
id: baseId,
|
|
@@ -547,6 +1553,13 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
|
|
547
1553
|
}
|
|
548
1554
|
|
|
549
1555
|
if (raw.message?.role === 'assistant' && raw.message?.content) {
|
|
1556
|
+
// Which model actually answered. It rides on the assistant row itself in
|
|
1557
|
+
// both the transcript and the live SDK stream, so history renders it
|
|
1558
|
+
// retroactively with no migration. Only prose carries it: a tool call has
|
|
1559
|
+
// no footer to show it in, and a locally-fabricated row (`<synthetic>`,
|
|
1560
|
+
// e.g. the usage-limit notice) gets no label rather than a guessed one.
|
|
1561
|
+
const answeredByModel = readAssistantModel(raw.message.model);
|
|
1562
|
+
|
|
550
1563
|
if (Array.isArray(raw.message.content)) {
|
|
551
1564
|
let partIndex = 0;
|
|
552
1565
|
for (const part of raw.message.content) {
|
|
@@ -559,6 +1572,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
|
|
559
1572
|
kind: 'text',
|
|
560
1573
|
role: 'assistant',
|
|
561
1574
|
content: part.text,
|
|
1575
|
+
model: answeredByModel,
|
|
562
1576
|
}));
|
|
563
1577
|
} else if (part.type === 'tool_use') {
|
|
564
1578
|
messages.push(createNormalizedMessage({
|
|
@@ -592,6 +1606,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
|
|
592
1606
|
kind: 'text',
|
|
593
1607
|
role: 'assistant',
|
|
594
1608
|
content: raw.message.content,
|
|
1609
|
+
model: answeredByModel,
|
|
595
1610
|
}));
|
|
596
1611
|
}
|
|
597
1612
|
return messages;
|
|
@@ -600,6 +1615,117 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
|
|
600
1615
|
return messages;
|
|
601
1616
|
}
|
|
602
1617
|
|
|
1618
|
+
/**
|
|
1619
|
+
* Finds the row to resume *through* so that `anchorId`'s turn is replaced.
|
|
1620
|
+
*
|
|
1621
|
+
* Walks up the `parentUuid` chain to the nearest assistant row, because the
|
|
1622
|
+
* SDK's `resumeSessionAt` is documented against assistant message ids — the
|
|
1623
|
+
* user row's immediate parent can be an attachment or an injected note.
|
|
1624
|
+
* Returns `null` when nothing precedes the edited prompt, which means the
|
|
1625
|
+
* conversation should start over rather than resume.
|
|
1626
|
+
*/
|
|
1627
|
+
async resolveEditAnchor(
|
|
1628
|
+
sessionId: string,
|
|
1629
|
+
anchorId: string,
|
|
1630
|
+
): Promise<{ found: boolean; resumeThroughId: string | null }> {
|
|
1631
|
+
const session = sessionsDb.getSessionById(sessionId);
|
|
1632
|
+
const jsonlPath = session?.jsonl_path;
|
|
1633
|
+
const providerSessionId = session?.provider_session_id;
|
|
1634
|
+
if (!jsonlPath || !providerSessionId) {
|
|
1635
|
+
return { found: false, resumeThroughId: null };
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
const rows = await readTranscriptRows(jsonlPath, providerSessionId);
|
|
1639
|
+
const byUuid = new Map<string, AnyRecord>();
|
|
1640
|
+
for (const row of rows) {
|
|
1641
|
+
if (typeof row.uuid === 'string') {
|
|
1642
|
+
byUuid.set(row.uuid, row);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
const target = byUuid.get(anchorId);
|
|
1647
|
+
if (!target) {
|
|
1648
|
+
return { found: false, resumeThroughId: null };
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
const visited = new Set<string>([anchorId]);
|
|
1652
|
+
let parentUuid: unknown = target.parentUuid;
|
|
1653
|
+
while (typeof parentUuid === 'string' && !visited.has(parentUuid)) {
|
|
1654
|
+
visited.add(parentUuid);
|
|
1655
|
+
const parent = byUuid.get(parentUuid);
|
|
1656
|
+
if (!parent) {
|
|
1657
|
+
break;
|
|
1658
|
+
}
|
|
1659
|
+
if (parent.type === 'assistant') {
|
|
1660
|
+
return { found: true, resumeThroughId: parentUuid };
|
|
1661
|
+
}
|
|
1662
|
+
parentUuid = parent.parentUuid;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
return { found: true, resumeThroughId: null };
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
/**
|
|
1669
|
+
* Reads one workflow agent's timeline from the transcript the run wrote for
|
|
1670
|
+
* it, under `<projectDir>/<providerSessionId>/subagents/workflows/<runId>/`
|
|
1671
|
+
* — the only place it exists, since the SDK never streams a workflow agent's
|
|
1672
|
+
* rows to the parent session.
|
|
1673
|
+
*
|
|
1674
|
+
* The journal beside it settles the agent when it has; short of that the
|
|
1675
|
+
* agent is running only while the process that spawned it is — the same
|
|
1676
|
+
* rule history applies to a launch row, keyed here on the agent's first row
|
|
1677
|
+
* rather than the launch's.
|
|
1678
|
+
*/
|
|
1679
|
+
async readWorkflowAgentActivity(
|
|
1680
|
+
sessionId: string,
|
|
1681
|
+
runId: string,
|
|
1682
|
+
agentId: string,
|
|
1683
|
+
): Promise<WorkflowAgentActivity | null> {
|
|
1684
|
+
const session = sessionsDb.getSessionById(sessionId);
|
|
1685
|
+
const jsonlPath = session?.jsonl_path;
|
|
1686
|
+
const providerSessionId = session?.provider_session_id;
|
|
1687
|
+
if (!jsonlPath || !providerSessionId) {
|
|
1688
|
+
return null;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
const transcriptDir = path.join(path.dirname(jsonlPath), providerSessionId, 'subagents', 'workflows', runId);
|
|
1692
|
+
const transcriptPath = path.join(transcriptDir, `agent-${agentId}.jsonl`);
|
|
1693
|
+
try {
|
|
1694
|
+
await fsp.access(transcriptPath);
|
|
1695
|
+
} catch {
|
|
1696
|
+
return null;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
const [transcript, journalAgents] = await Promise.all([
|
|
1700
|
+
readClaudeSubagentTranscript(transcriptPath),
|
|
1701
|
+
readWorkflowJournal(transcriptDir),
|
|
1702
|
+
]);
|
|
1703
|
+
const journalAgent = journalAgents.find((agent) => agent.id === agentId);
|
|
1704
|
+
|
|
1705
|
+
let status: WorkflowAgentActivity['agent']['status'];
|
|
1706
|
+
if (journalAgent && journalAgent.status !== 'running') {
|
|
1707
|
+
status = journalAgent.status;
|
|
1708
|
+
} else {
|
|
1709
|
+
const liveRunStartedAt = this.getLiveRunStartTime(sessionId) ?? this.getLiveRunStartTime(providerSessionId);
|
|
1710
|
+
status = liveRunStartedAt !== null && Date.parse(transcript.startedAt ?? '') >= liveRunStartedAt
|
|
1711
|
+
? 'running'
|
|
1712
|
+
: 'stopped';
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
return {
|
|
1716
|
+
agent: {
|
|
1717
|
+
id: agentId,
|
|
1718
|
+
label: journalAgent?.label,
|
|
1719
|
+
model: transcript.model,
|
|
1720
|
+
status,
|
|
1721
|
+
},
|
|
1722
|
+
activity: transcript.activity
|
|
1723
|
+
.slice(-MAX_TRANSMITTED_SUBAGENT_ACTIVITIES)
|
|
1724
|
+
.map(truncateSubagentActivity),
|
|
1725
|
+
activityCount: transcript.activity.length,
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
|
|
603
1729
|
/**
|
|
604
1730
|
* Loads Claude JSONL history for a project/session and returns normalized
|
|
605
1731
|
* messages, preserving the existing pagination behavior from projects.js.
|
|
@@ -615,7 +1741,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
|
|
615
1741
|
try {
|
|
616
1742
|
// Load full history first so `total` reflects frontend-normalized messages,
|
|
617
1743
|
// not raw JSONL records.
|
|
618
|
-
result = await getSessionMessages(sessionId, providerSessionId, null, 0);
|
|
1744
|
+
result = await getSessionMessages(sessionId, providerSessionId, null, 0, this.getLiveRunStartTime);
|
|
619
1745
|
} catch (error) {
|
|
620
1746
|
const message = error instanceof Error ? error.message : String(error);
|
|
621
1747
|
console.warn(`[ClaudeProvider] Failed to load session ${sessionId}:`, message);
|
|
@@ -633,6 +1759,8 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
|
|
633
1759
|
content: part.content,
|
|
634
1760
|
isError: Boolean(part.is_error),
|
|
635
1761
|
subagentTools: raw.subagentTools,
|
|
1762
|
+
subagent: raw.subagent,
|
|
1763
|
+
workflow: raw.workflow,
|
|
636
1764
|
toolUseResult: raw.toolUseResult,
|
|
637
1765
|
});
|
|
638
1766
|
}
|
|
@@ -660,18 +1788,18 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
|
|
660
1788
|
toolUseResult: toolResult.toolUseResult,
|
|
661
1789
|
};
|
|
662
1790
|
msg.subagentTools = toolResult.subagentTools;
|
|
1791
|
+
msg.subagent = toolResult.subagent;
|
|
1792
|
+
msg.workflow = toolResult.workflow;
|
|
663
1793
|
}
|
|
664
1794
|
}
|
|
665
1795
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
}
|
|
1796
|
+
// Everything the transcript draws, and nothing else — so a page of N rows
|
|
1797
|
+
// is N rows the user sees, and `total` counts the same thing.
|
|
1798
|
+
const transcript = prepareTranscriptMessages(normalized);
|
|
1799
|
+
const total = transcript.length;
|
|
672
1800
|
const normalizedOffset = Math.max(0, offset);
|
|
673
1801
|
const normalizedLimit = limit === null ? null : Math.max(0, limit);
|
|
674
|
-
const { page, hasMore } = sliceTailPage(
|
|
1802
|
+
const { page, hasMore } = sliceTailPage(transcript, normalizedLimit, normalizedOffset);
|
|
675
1803
|
|
|
676
1804
|
return {
|
|
677
1805
|
messages: page,
|
|
@@ -679,6 +1807,10 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
|
|
679
1807
|
hasMore,
|
|
680
1808
|
offset: normalizedOffset,
|
|
681
1809
|
limit: normalizedLimit,
|
|
1810
|
+
// Carried on every page, like the Codex and OpenCode readers do, so the
|
|
1811
|
+
// composer's counter tracks the conversation instead of being frozen at
|
|
1812
|
+
// whatever it was when the session was opened.
|
|
1813
|
+
tokenUsage: summarizeClaudeTokenUsage(rawMessages),
|
|
682
1814
|
};
|
|
683
1815
|
}
|
|
684
1816
|
}
|