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