@hienlh/ppm 0.23.8 → 0.23.10
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/CHANGELOG.md +16 -0
- package/CLAUDE.md +4 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/accounts-settings-section-BOBJbG7i.js +1 -0
- package/dist/web/assets/accounts-settings-section-BOBJbG7i.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-BOBJbG7i.js.gz +0 -0
- package/dist/web/assets/agent-session-sheet-CjD4hBZV.js +2 -0
- package/dist/web/assets/agent-session-sheet-CjD4hBZV.js.br +0 -0
- package/dist/web/assets/agent-session-sheet-CjD4hBZV.js.gz +0 -0
- package/dist/web/assets/agent-session-sheet-store-BcQbnaV2.js +1 -0
- package/dist/web/assets/agent-session-window-content-Drm1HsSn.js +1 -0
- package/dist/web/assets/agent-session-window-content-Drm1HsSn.js.br +0 -0
- package/dist/web/assets/agent-session-window-content-Drm1HsSn.js.gz +0 -0
- package/dist/web/assets/{ai-resource-editor-DfVRMUHS.js → ai-resource-editor-M1Eax2Hc.js} +1 -1
- package/dist/web/assets/ai-resource-editor-M1Eax2Hc.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-M1Eax2Hc.js.gz +0 -0
- package/dist/web/assets/ai-settings-section-B73t9SXm.js +1 -0
- package/dist/web/assets/{ai-settings-section-Ezhw4Fef.js → ai-settings-section-H173dm1k.js} +1 -1
- package/dist/web/assets/ai-settings-section-H173dm1k.js.br +0 -0
- package/dist/web/assets/ai-settings-section-H173dm1k.js.gz +0 -0
- package/dist/web/assets/{api-settings-tYKJ5Fqk.js → api-settings-BA_n8cPt.js} +1 -1
- package/dist/web/assets/{appearance-settings-section-CPOAp0fY.js → appearance-settings-section-Dqu20pt8.js} +1 -1
- package/dist/web/assets/appearance-settings-section-Dqu20pt8.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-Dqu20pt8.js.gz +0 -0
- package/dist/web/assets/{apps-panel-Cz7_o-M4.js → apps-panel-mkrzY7K0.js} +1 -1
- package/dist/web/assets/apps-panel-mkrzY7K0.js.br +0 -0
- package/dist/web/assets/apps-panel-mkrzY7K0.js.gz +0 -0
- package/dist/web/assets/branch-review-tab-Dw3bNixU.js +1 -0
- package/dist/web/assets/branch-review-tab-Dw3bNixU.js.br +0 -0
- package/dist/web/assets/branch-review-tab-Dw3bNixU.js.gz +0 -0
- package/dist/web/assets/chat-tab-BAbcVFzD.js +15 -0
- package/dist/web/assets/chat-tab-BAbcVFzD.js.br +0 -0
- package/dist/web/assets/chat-tab-BAbcVFzD.js.gz +0 -0
- package/dist/web/assets/chat-tab-BaXGaBEC.js +1 -0
- package/dist/web/assets/chat-tab-BaXGaBEC.js.br +4 -0
- package/dist/web/assets/chat-tab-BaXGaBEC.js.gz +0 -0
- package/dist/web/assets/code-editor-CXLANXnI.js +7 -0
- package/dist/web/assets/code-editor-CXLANXnI.js.br +0 -0
- package/dist/web/assets/code-editor-CXLANXnI.js.gz +0 -0
- package/dist/web/assets/{codex-reset-credit-button-D2hal4bG.js → codex-reset-credit-button-CB8Dfhki.js} +1 -1
- package/dist/web/assets/codex-reset-credit-button-CB8Dfhki.js.br +0 -0
- package/dist/web/assets/codex-reset-credit-button-CB8Dfhki.js.gz +0 -0
- package/dist/web/assets/{conflict-editor-Cj_gUncw.js → conflict-editor-CKsxIw3v.js} +2 -2
- package/dist/web/assets/conflict-editor-CKsxIw3v.js.br +0 -0
- package/dist/web/assets/conflict-editor-CKsxIw3v.js.gz +0 -0
- package/dist/web/assets/{csv-preview-Be3ODNj0.js → csv-preview-U0v_r_-7.js} +1 -1
- package/dist/web/assets/csv-preview-U0v_r_-7.js.br +0 -0
- package/dist/web/assets/csv-preview-U0v_r_-7.js.gz +0 -0
- package/dist/web/assets/{database-viewer-B1b--4y8.js → database-viewer-xQyUcBoL.js} +1 -1
- package/dist/web/assets/database-viewer-xQyUcBoL.js.br +0 -0
- package/dist/web/assets/database-viewer-xQyUcBoL.js.gz +0 -0
- package/dist/web/assets/{design-tab-8wi0IHss.js → design-tab-aaAt2_QE.js} +4 -4
- package/dist/web/assets/design-tab-aaAt2_QE.js.br +0 -0
- package/dist/web/assets/design-tab-aaAt2_QE.js.gz +0 -0
- package/dist/web/assets/{designs-sidebar-panel-D5-1Q8zQ.js → designs-sidebar-panel-CTzO4Thu.js} +1 -1
- package/dist/web/assets/designs-sidebar-panel-CTzO4Thu.js.br +0 -0
- package/dist/web/assets/designs-sidebar-panel-CTzO4Thu.js.gz +0 -0
- package/dist/web/assets/{diff-viewer-Dcxk6UTG.js → diff-viewer-CSE2-OE7.js} +1 -1
- package/dist/web/assets/diff-viewer-CSE2-OE7.js.br +0 -0
- package/dist/web/assets/diff-viewer-CSE2-OE7.js.gz +0 -0
- package/dist/web/assets/{edit-diff-preview-DxDAtYhi.js → edit-diff-preview-gH6pRk4S.js} +1 -1
- package/dist/web/assets/edit-diff-preview-gH6pRk4S.js.br +0 -0
- package/dist/web/assets/{edit-diff-preview-DxDAtYhi.js.gz → edit-diff-preview-gH6pRk4S.js.gz} +0 -0
- package/dist/web/assets/{editor-language-service-BVJQnTSV.js → editor-language-service-CYl04pUy.js} +1 -1
- package/dist/web/assets/editor-language-service-CYl04pUy.js.br +0 -0
- package/dist/web/assets/editor-language-service-CYl04pUy.js.gz +0 -0
- package/dist/web/assets/explorer-body-BBOcvwmV.js +2 -0
- package/dist/web/assets/explorer-body-BBOcvwmV.js.br +0 -0
- package/dist/web/assets/explorer-body-BBOcvwmV.js.gz +0 -0
- package/dist/web/assets/{explorer-window-content-DH5AD3Ng.js → explorer-window-content-juCGcrkc.js} +1 -1
- package/dist/web/assets/explorer-window-content-juCGcrkc.js.br +0 -0
- package/dist/web/assets/explorer-window-content-juCGcrkc.js.gz +0 -0
- package/dist/web/assets/{extension-manager-section-BJr6ccWw.js → extension-manager-section-poMRmwvg.js} +1 -1
- package/dist/web/assets/extension-manager-section-poMRmwvg.js.br +0 -0
- package/dist/web/assets/extension-manager-section-poMRmwvg.js.gz +0 -0
- package/dist/web/assets/{extension-webview-CYNXHd8D.js → extension-webview-0QfzTI6s.js} +1 -1
- package/dist/web/assets/extension-webview-0QfzTI6s.js.br +0 -0
- package/dist/web/assets/extension-webview-0QfzTI6s.js.gz +0 -0
- package/dist/web/assets/{files-settings-section-JX7o4lex.js → files-settings-section-DJptxRgH.js} +1 -1
- package/dist/web/assets/files-settings-section-DJptxRgH.js.br +0 -0
- package/dist/web/assets/files-settings-section-DJptxRgH.js.gz +0 -0
- package/dist/web/assets/{general-settings-section-kF94Mz5q.js → general-settings-section-BFNbdDNv.js} +1 -1
- package/dist/web/assets/general-settings-section-BFNbdDNv.js.br +0 -0
- package/dist/web/assets/general-settings-section-BFNbdDNv.js.gz +0 -0
- package/dist/web/assets/{git-log-panel-Crlp9ofM.js → git-log-panel-SMMIbjSe.js} +1 -1
- package/dist/web/assets/git-log-panel-SMMIbjSe.js.br +0 -0
- package/dist/web/assets/git-log-panel-SMMIbjSe.js.gz +0 -0
- package/dist/web/assets/{glide-data-grid-O6MO5dKU.js → glide-data-grid-DZVmgu0T.js} +3 -3
- package/dist/web/assets/glide-data-grid-DZVmgu0T.js.br +0 -0
- package/dist/web/assets/glide-data-grid-DZVmgu0T.js.gz +0 -0
- package/dist/web/assets/group-chat-tab-d_aaYuK_.js +1 -0
- package/dist/web/assets/group-chat-tab-d_aaYuK_.js.br +0 -0
- package/dist/web/assets/group-chat-tab-d_aaYuK_.js.gz +0 -0
- package/dist/web/assets/{index-DQCw6d-2.js → index-CIscPw4g.js} +6 -6
- package/dist/web/assets/index-CIscPw4g.js.br +0 -0
- package/dist/web/assets/index-CIscPw4g.js.gz +0 -0
- package/dist/web/assets/{jira-watcher-section-DwM7gqHT.js → jira-watcher-section-CsbhnLLn.js} +1 -1
- package/dist/web/assets/keybindings-store-BPs0pM0r.js +1 -0
- package/dist/web/assets/{keyboard-shortcuts-section-CewWGwiF.js → keyboard-shortcuts-section-CXqc2OtM.js} +1 -1
- package/dist/web/assets/keyboard-shortcuts-section-CXqc2OtM.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-CXqc2OtM.js.gz +0 -0
- package/dist/web/assets/{language-servers-section-nricdxke.js → language-servers-section-B4FsBVvZ.js} +2 -2
- package/dist/web/assets/language-servers-section-B4FsBVvZ.js.br +0 -0
- package/dist/web/assets/language-servers-section-B4FsBVvZ.js.gz +0 -0
- package/dist/web/assets/{lsp-client-DRMEWb-z.js → lsp-client-MQ4mjDfT.js} +1 -1
- package/dist/web/assets/lsp-client-MQ4mjDfT.js.br +0 -0
- package/dist/web/assets/lsp-client-MQ4mjDfT.js.gz +0 -0
- package/dist/web/assets/markdown-renderer-BvCZZlue.js +1 -0
- package/dist/web/assets/{markdown-renderer-Dab9Us6u.js → markdown-renderer-CjPb5pns.js} +3 -3
- package/dist/web/assets/markdown-renderer-CjPb5pns.js.br +0 -0
- package/dist/web/assets/markdown-renderer-CjPb5pns.js.gz +0 -0
- package/dist/web/assets/{mobile-explorer-sheet-DtgdpvJ9.js → mobile-explorer-sheet-0aGhVQtI.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-0aGhVQtI.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-0aGhVQtI.js.gz +0 -0
- package/dist/web/assets/{new-design-dialog-Dd5KDB_p.js → new-design-dialog-2oT2oc_F.js} +1 -1
- package/dist/web/assets/new-design-dialog-2oT2oc_F.js.br +0 -0
- package/dist/web/assets/new-design-dialog-2oT2oc_F.js.gz +0 -0
- package/dist/web/assets/notification-store-Dd25PMPQ.js +1 -0
- package/dist/web/assets/{panel-store-DrSH3Nhp.js → panel-store-1X6yM3XZ.js} +1 -1
- package/dist/web/assets/panel-store-1X6yM3XZ.js.br +0 -0
- package/dist/web/assets/panel-store-1X6yM3XZ.js.gz +0 -0
- package/dist/web/assets/performance-panel-Ub96qrEH.js +3 -0
- package/dist/web/assets/performance-panel-Ub96qrEH.js.br +0 -0
- package/dist/web/assets/performance-panel-Ub96qrEH.js.gz +0 -0
- package/dist/web/assets/{ppmbot-settings-section-D67u6gPi.js → ppmbot-settings-section-CPMGP66g.js} +1 -1
- package/dist/web/assets/ppmbot-settings-section-CPMGP66g.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-CPMGP66g.js.gz +0 -0
- package/dist/web/assets/{problems-panel-DKgIw4uZ.js → problems-panel-CWwzZIP3.js} +1 -1
- package/dist/web/assets/problems-panel-CWwzZIP3.js.br +0 -0
- package/dist/web/assets/problems-panel-CWwzZIP3.js.gz +0 -0
- package/dist/web/assets/process-table-BwPws3zS.js +1 -0
- package/dist/web/assets/process-table-BwPws3zS.js.br +0 -0
- package/dist/web/assets/process-table-BwPws3zS.js.gz +0 -0
- package/dist/web/assets/{proxy-settings-section-CFiYcTBA.js → proxy-settings-section-nS8VbxI_.js} +1 -1
- package/dist/web/assets/proxy-settings-section-nS8VbxI_.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-nS8VbxI_.js.gz +0 -0
- package/dist/web/assets/{query-audit-section-BybgiPsc.js → query-audit-section-Dv5Pxn-w.js} +1 -1
- package/dist/web/assets/query-audit-section-Dv5Pxn-w.js.br +0 -0
- package/dist/web/assets/query-audit-section-Dv5Pxn-w.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-clipboard-notice-Bs3fZSRw.js → remote-desktop-clipboard-notice-DEbdu0hC.js} +1 -1
- package/dist/web/assets/remote-desktop-clipboard-notice-DEbdu0hC.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-DEbdu0hC.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-BJm-akVY.js +2 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-BJm-akVY.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-BJm-akVY.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-mobile-view-DgGOfPVY.js → remote-desktop-mobile-view-DPdGFD6C.js} +1 -1
- package/dist/web/assets/remote-desktop-mobile-view-DPdGFD6C.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-DPdGFD6C.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-readiness-gate-DOWryrQx.js → remote-desktop-readiness-gate-DV7YgSpP.js} +1 -1
- package/dist/web/assets/remote-desktop-readiness-gate-DV7YgSpP.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-DV7YgSpP.js.gz +0 -0
- package/dist/web/assets/remote-desktop-window-content-BktcWMjD.js +1 -0
- package/dist/web/assets/remote-desktop-window-content-BktcWMjD.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-BktcWMjD.js.gz +0 -0
- package/dist/web/assets/{run-in-terminal-Cdn9PaAl.js → run-in-terminal-B_PCeBqG.js} +1 -1
- package/dist/web/assets/{schedules-settings-section-DGhG-b5O.js → schedules-settings-section-B3HYKSVh.js} +1 -1
- package/dist/web/assets/schedules-settings-section-B3HYKSVh.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-B3HYKSVh.js.gz +0 -0
- package/dist/web/assets/{send-to-chat-B09VjsB5.js → send-to-chat-Bjg6c8Ld.js} +1 -1
- package/dist/web/assets/send-to-chat-Bjg6c8Ld.js.br +0 -0
- package/dist/web/assets/send-to-chat-Bjg6c8Ld.js.gz +0 -0
- package/dist/web/assets/{services-panel-0klE3O4g.js → services-panel-D6k6AVS4.js} +1 -1
- package/dist/web/assets/services-panel-D6k6AVS4.js.br +0 -0
- package/dist/web/assets/services-panel-D6k6AVS4.js.gz +0 -0
- package/dist/web/assets/{settings-body-CfqpqFfq.js → settings-body-Cu0mNA5x.js} +2 -2
- package/dist/web/assets/settings-body-Cu0mNA5x.js.br +0 -0
- package/dist/web/assets/settings-body-Cu0mNA5x.js.gz +0 -0
- package/dist/web/assets/{settings-store-3jIBB8gx.js → settings-store-DIV25PM_.js} +2 -2
- package/dist/web/assets/settings-store-DIV25PM_.js.br +0 -0
- package/dist/web/assets/settings-store-DIV25PM_.js.gz +0 -0
- package/dist/web/assets/{settings-tab-CHweH5Nh.js → settings-tab-C-A6RNQ3.js} +1 -1
- package/dist/web/assets/{settings-window-content-C6Od3qV7.js → settings-window-content-48e5FJAq.js} +1 -1
- package/dist/web/assets/{sql-query-editor-ALQ5_X0j.js → sql-query-editor-CgGWYu9K.js} +1 -1
- package/dist/web/assets/sql-query-editor-CgGWYu9K.js.br +0 -0
- package/dist/web/assets/sql-query-editor-CgGWYu9K.js.gz +0 -0
- package/dist/web/assets/{sqlite-viewer-DKTty_jR.js → sqlite-viewer-Co1Oi3dv.js} +1 -1
- package/dist/web/assets/sqlite-viewer-Co1Oi3dv.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-Co1Oi3dv.js.gz +0 -0
- package/dist/web/assets/{system-monitor-body-D6J6lq7-.js → system-monitor-body-DxPKVa-W.js} +2 -2
- package/dist/web/assets/system-monitor-body-DxPKVa-W.js.br +0 -0
- package/dist/web/assets/system-monitor-body-DxPKVa-W.js.gz +0 -0
- package/dist/web/assets/system-monitor-tab-4miOZuQQ.js +1 -0
- package/dist/web/assets/{system-monitor-window-content-D1t30f32.js → system-monitor-window-content-B-A7DdML.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-DTcx2D0O.js → tab-host-window-content-CChcpg1v.js} +1 -1
- package/dist/web/assets/tab-host-window-content-CChcpg1v.js.br +0 -0
- package/dist/web/assets/tab-host-window-content-CChcpg1v.js.gz +0 -0
- package/dist/web/assets/{tab-store-CUPgQIQz.js → tab-store-D33icgdA.js} +1 -1
- package/dist/web/assets/{terminal-tab-DgTfQKBI.js → terminal-tab-BDqlyBLK.js} +2 -2
- package/dist/web/assets/terminal-tab-BDqlyBLK.js.br +0 -0
- package/dist/web/assets/terminal-tab-BDqlyBLK.js.gz +0 -0
- package/dist/web/assets/tool-cards-iJ8tyrLq.js +9 -0
- package/dist/web/assets/tool-cards-iJ8tyrLq.js.br +0 -0
- package/dist/web/assets/tool-cards-iJ8tyrLq.js.gz +0 -0
- package/dist/web/assets/{use-inline-blame-Dv111BKY.js → use-inline-blame-D9js570a.js} +4 -4
- package/dist/web/assets/use-inline-blame-D9js570a.js.br +0 -0
- package/dist/web/assets/use-inline-blame-D9js570a.js.gz +0 -0
- package/dist/web/assets/{use-lsp-CZTFJIXX.js → use-lsp-BYtylZId.js} +1 -1
- package/dist/web/assets/use-lsp-BYtylZId.js.br +0 -0
- package/dist/web/assets/use-lsp-BYtylZId.js.gz +0 -0
- package/dist/web/assets/{use-monaco-theme-DL2dIG5L.js → use-monaco-theme-DJ8y_7IZ.js} +1 -1
- package/dist/web/assets/{use-process-kill-D7D9QvJG.js → use-process-kill-BS84S-kD.js} +1 -1
- package/dist/web/assets/use-process-kill-BS84S-kD.js.br +0 -0
- package/dist/web/assets/use-process-kill-BS84S-kD.js.gz +0 -0
- package/dist/web/assets/{use-websocket-ziY-Gz_B.js → use-websocket-Mqj27umG.js} +1 -1
- package/dist/web/assets/{video-preview-DzwY3q9W.js → video-preview-BuzUAIma.js} +1 -1
- package/dist/web/assets/video-preview-BuzUAIma.js.br +0 -0
- package/dist/web/assets/video-preview-BuzUAIma.js.gz +0 -0
- package/dist/web/assets/{voice-settings-section-B7zMHsuz.js → voice-settings-section-DnwQjBbw.js} +1 -1
- package/dist/web/assets/voice-settings-section-DnwQjBbw.js.br +0 -0
- package/dist/web/assets/voice-settings-section-DnwQjBbw.js.gz +0 -0
- package/dist/web/assets/{window-pip-registry-dxrBFfkP.js → window-pip-registry-Bl--fK8-.js} +1 -1
- package/dist/web/assets/window-store-vo1N70nK.js +1 -0
- package/dist/web/assets/window-store-vo1N70nK.js.br +0 -0
- package/dist/web/assets/window-store-vo1N70nK.js.gz +0 -0
- package/dist/web/index.html +6 -6
- package/dist/web/index.html.br +0 -0
- package/dist/web/index.html.gz +0 -0
- package/dist/web/sw.js +1 -1
- package/dist/web/sw.js.br +0 -0
- package/dist/web/sw.js.gz +0 -0
- package/docs/architecture/ai-chat-and-providers.md +66 -0
- package/docs/architecture/workspace-and-ui.md +15 -3
- package/docs/design-guidelines.md +6 -1
- package/package.json +1 -1
- package/src/providers/codex-app-server/codex-history.ts +4 -4
- package/src/providers/codex-app-server/codex-provider.ts +1 -1
- package/src/providers/codex-app-server/codex-rollout-tail-parser.ts +203 -0
- package/src/providers/codex-app-server/codex-subagent-thread.ts +7 -1
- package/src/server/index.ts +7 -2
- package/src/server/routes/chat.ts +12 -4
- package/src/server/ws/global.ts +19 -3
- package/src/services/agent-transcript/agent-transcript-activity.ts +72 -0
- package/src/services/agent-transcript/agent-transcript-codex-activity.ts +161 -0
- package/src/services/agent-transcript/agent-transcript-codex-tail-cache.ts +85 -0
- package/src/services/agent-transcript/agent-transcript-envelope.ts +86 -0
- package/src/services/agent-transcript/agent-transcript-error-map.ts +33 -0
- package/src/services/agent-transcript/agent-transcript-file-tail-feed.ts +180 -0
- package/src/services/agent-transcript/agent-transcript-file-tail-state.ts +100 -0
- package/src/services/agent-transcript/agent-transcript-fs-io.ts +79 -0
- package/src/services/agent-transcript/agent-transcript-hub-clock.ts +37 -0
- package/src/services/agent-transcript/agent-transcript-hub-registry.ts +153 -0
- package/src/services/agent-transcript/agent-transcript-hub.ts +103 -0
- package/src/services/agent-transcript/agent-transcript-index-cache.ts +85 -0
- package/src/services/agent-transcript/agent-transcript-session-hub-activity.ts +103 -0
- package/src/services/agent-transcript/agent-transcript-session-hub-read.ts +194 -0
- package/src/services/agent-transcript/agent-transcript-session-hub-types.ts +81 -0
- package/src/services/agent-transcript/agent-transcript-session-hub.ts +196 -0
- package/src/services/agent-transcript/agent-transcript-sources.ts +199 -0
- package/src/services/agent-transcript/agent-transcript-ws-like.ts +19 -0
- package/src/services/agent-transcript/claude-projects-root.ts +21 -0
- package/src/services/agent-transcript/session-ownership.ts +152 -0
- package/src/services/nested-subagent-spy.ts +3 -3
- package/src/services/subagent-transcript-merger.ts +21 -7
- package/src/shared/agent-transcript-protocol.ts +158 -0
- package/src/types/chat.ts +27 -3
- package/src/web/app.tsx +5 -5
- package/src/web/components/chat/agent-card-summary.tsx +73 -0
- package/src/web/components/chat/agent-session-context.ts +34 -0
- package/src/web/components/chat/agent-session-fallback-store.ts +60 -0
- package/src/web/components/chat/agent-session-sheet-store.ts +23 -0
- package/src/web/components/chat/{team-member-sheet.tsx → agent-session-sheet.tsx} +12 -12
- package/src/web/components/chat/agent-session-window-content.tsx +133 -0
- package/src/web/components/chat/chat-history-bar.tsx +1 -0
- package/src/web/components/chat/chat-tab.tsx +21 -4
- package/src/web/components/chat/codex-usage-panel.tsx +26 -8
- package/src/web/components/chat/running-agents-bar.tsx +93 -0
- package/src/web/components/chat/team-activity-panel.tsx +19 -5
- package/src/web/components/chat/tool-cards.tsx +73 -4
- package/src/web/components/chat/use-open-agent-session.ts +83 -0
- package/src/web/components/floating-window/window-content-registry.ts +4 -5
- package/src/web/components/floating-window/window-geometry.ts +18 -0
- package/src/web/components/floating-window/window-persistence.ts +5 -4
- package/src/web/components/floating-window/window-store-types.ts +1 -1
- package/src/web/components/settings/resolve-open-settings-action.ts +1 -1
- package/src/web/components/system/resolve-open-system-monitor-action.ts +1 -1
- package/src/web/hooks/use-agent-activity.ts +65 -0
- package/src/web/hooks/use-agent-session-stream.ts +134 -0
- package/src/web/hooks/use-chat.ts +12 -18
- package/src/web/hooks/use-global-events.ts +35 -2
- package/src/web/lib/agent-session-stream-merge.ts +48 -0
- package/src/web/lib/agent-step-summary.ts +331 -0
- package/src/web/lib/global-ws-channel.ts +41 -0
- package/src/web/lib/running-agent-rows.ts +115 -0
- package/dist/web/assets/accounts-settings-section-BNcPvEq1.js +0 -1
- package/dist/web/assets/accounts-settings-section-BNcPvEq1.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-BNcPvEq1.js.gz +0 -0
- package/dist/web/assets/ai-resource-editor-DfVRMUHS.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-DfVRMUHS.js.gz +0 -0
- package/dist/web/assets/ai-settings-section-B_0-P8jx.js +0 -1
- package/dist/web/assets/ai-settings-section-Ezhw4Fef.js.br +0 -0
- package/dist/web/assets/ai-settings-section-Ezhw4Fef.js.gz +0 -0
- package/dist/web/assets/appearance-settings-section-CPOAp0fY.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-CPOAp0fY.js.gz +0 -0
- package/dist/web/assets/apps-panel-Cz7_o-M4.js.br +0 -0
- package/dist/web/assets/apps-panel-Cz7_o-M4.js.gz +0 -0
- package/dist/web/assets/branch-review-tab-BPx3kHrM.js +0 -1
- package/dist/web/assets/branch-review-tab-BPx3kHrM.js.br +0 -0
- package/dist/web/assets/branch-review-tab-BPx3kHrM.js.gz +0 -0
- package/dist/web/assets/chat-tab-BadDM_j8.js +0 -1
- package/dist/web/assets/chat-tab-BadDM_j8.js.br +0 -0
- package/dist/web/assets/chat-tab-BadDM_j8.js.gz +0 -0
- package/dist/web/assets/chat-tab-IqAY4c7p.js +0 -15
- package/dist/web/assets/chat-tab-IqAY4c7p.js.br +0 -0
- package/dist/web/assets/chat-tab-IqAY4c7p.js.gz +0 -0
- package/dist/web/assets/code-editor-Dmitw9QS.js +0 -7
- package/dist/web/assets/code-editor-Dmitw9QS.js.br +0 -0
- package/dist/web/assets/code-editor-Dmitw9QS.js.gz +0 -0
- package/dist/web/assets/codex-reset-credit-button-D2hal4bG.js.br +0 -0
- package/dist/web/assets/codex-reset-credit-button-D2hal4bG.js.gz +0 -0
- package/dist/web/assets/conflict-editor-Cj_gUncw.js.br +0 -0
- package/dist/web/assets/conflict-editor-Cj_gUncw.js.gz +0 -0
- package/dist/web/assets/csv-preview-Be3ODNj0.js.br +0 -0
- package/dist/web/assets/csv-preview-Be3ODNj0.js.gz +0 -0
- package/dist/web/assets/database-viewer-B1b--4y8.js.br +0 -0
- package/dist/web/assets/database-viewer-B1b--4y8.js.gz +0 -0
- package/dist/web/assets/design-tab-8wi0IHss.js.br +0 -0
- package/dist/web/assets/design-tab-8wi0IHss.js.gz +0 -0
- package/dist/web/assets/designs-sidebar-panel-D5-1Q8zQ.js.br +0 -0
- package/dist/web/assets/designs-sidebar-panel-D5-1Q8zQ.js.gz +0 -0
- package/dist/web/assets/diff-viewer-Dcxk6UTG.js.br +0 -0
- package/dist/web/assets/diff-viewer-Dcxk6UTG.js.gz +0 -0
- package/dist/web/assets/edit-diff-preview-DxDAtYhi.js.br +0 -0
- package/dist/web/assets/editor-language-service-BVJQnTSV.js.br +0 -0
- package/dist/web/assets/editor-language-service-BVJQnTSV.js.gz +0 -0
- package/dist/web/assets/explorer-body-CTkrlrI3.js +0 -2
- package/dist/web/assets/explorer-body-CTkrlrI3.js.br +0 -0
- package/dist/web/assets/explorer-body-CTkrlrI3.js.gz +0 -0
- package/dist/web/assets/explorer-window-content-DH5AD3Ng.js.br +0 -0
- package/dist/web/assets/explorer-window-content-DH5AD3Ng.js.gz +0 -0
- package/dist/web/assets/extension-manager-section-BJr6ccWw.js.br +0 -0
- package/dist/web/assets/extension-manager-section-BJr6ccWw.js.gz +0 -0
- package/dist/web/assets/extension-webview-CYNXHd8D.js.br +0 -0
- package/dist/web/assets/extension-webview-CYNXHd8D.js.gz +0 -0
- package/dist/web/assets/files-settings-section-JX7o4lex.js.br +0 -0
- package/dist/web/assets/files-settings-section-JX7o4lex.js.gz +0 -0
- package/dist/web/assets/general-settings-section-kF94Mz5q.js.br +0 -0
- package/dist/web/assets/general-settings-section-kF94Mz5q.js.gz +0 -0
- package/dist/web/assets/git-log-panel-Crlp9ofM.js.br +0 -0
- package/dist/web/assets/git-log-panel-Crlp9ofM.js.gz +0 -0
- package/dist/web/assets/glide-data-grid-O6MO5dKU.js.br +0 -0
- package/dist/web/assets/glide-data-grid-O6MO5dKU.js.gz +0 -0
- package/dist/web/assets/group-chat-tab-DGLSGf37.js +0 -1
- package/dist/web/assets/group-chat-tab-DGLSGf37.js.br +0 -0
- package/dist/web/assets/group-chat-tab-DGLSGf37.js.gz +0 -0
- package/dist/web/assets/index-DQCw6d-2.js.br +0 -0
- package/dist/web/assets/index-DQCw6d-2.js.gz +0 -0
- package/dist/web/assets/keybindings-store-CJtEHFIQ.js +0 -1
- package/dist/web/assets/keyboard-shortcuts-section-CewWGwiF.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-CewWGwiF.js.gz +0 -0
- package/dist/web/assets/language-servers-section-nricdxke.js.br +0 -0
- package/dist/web/assets/language-servers-section-nricdxke.js.gz +0 -0
- package/dist/web/assets/lsp-client-DRMEWb-z.js.br +0 -0
- package/dist/web/assets/lsp-client-DRMEWb-z.js.gz +0 -0
- package/dist/web/assets/markdown-renderer-D5-xliE9.js +0 -1
- package/dist/web/assets/markdown-renderer-Dab9Us6u.js.br +0 -0
- package/dist/web/assets/markdown-renderer-Dab9Us6u.js.gz +0 -0
- package/dist/web/assets/mobile-explorer-sheet-DtgdpvJ9.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-DtgdpvJ9.js.gz +0 -0
- package/dist/web/assets/new-design-dialog-Dd5KDB_p.js.br +0 -0
- package/dist/web/assets/new-design-dialog-Dd5KDB_p.js.gz +0 -0
- package/dist/web/assets/notification-store-D2vTyp_Q.js +0 -1
- package/dist/web/assets/panel-store-DrSH3Nhp.js.br +0 -0
- package/dist/web/assets/panel-store-DrSH3Nhp.js.gz +0 -0
- package/dist/web/assets/performance-panel-LMGefOQ2.js +0 -3
- package/dist/web/assets/performance-panel-LMGefOQ2.js.br +0 -0
- package/dist/web/assets/performance-panel-LMGefOQ2.js.gz +0 -0
- package/dist/web/assets/ppmbot-settings-section-D67u6gPi.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-D67u6gPi.js.gz +0 -0
- package/dist/web/assets/problems-panel-DKgIw4uZ.js.br +0 -0
- package/dist/web/assets/problems-panel-DKgIw4uZ.js.gz +0 -0
- package/dist/web/assets/process-table-BIAgC_jG.js +0 -1
- package/dist/web/assets/process-table-BIAgC_jG.js.br +0 -0
- package/dist/web/assets/process-table-BIAgC_jG.js.gz +0 -0
- package/dist/web/assets/proxy-settings-section-CFiYcTBA.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-CFiYcTBA.js.gz +0 -0
- package/dist/web/assets/query-audit-section-BybgiPsc.js.br +0 -0
- package/dist/web/assets/query-audit-section-BybgiPsc.js.gz +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-Bs3fZSRw.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-Bs3fZSRw.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-DgxTdWMh.js +0 -2
- package/dist/web/assets/remote-desktop-mobile-sheet-DgxTdWMh.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-DgxTdWMh.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-DgGOfPVY.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-DgGOfPVY.js.gz +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-DOWryrQx.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-DOWryrQx.js.gz +0 -0
- package/dist/web/assets/remote-desktop-window-content-7t0wQsHx.js +0 -1
- package/dist/web/assets/remote-desktop-window-content-7t0wQsHx.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-7t0wQsHx.js.gz +0 -0
- package/dist/web/assets/schedules-settings-section-DGhG-b5O.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-DGhG-b5O.js.gz +0 -0
- package/dist/web/assets/send-to-chat-B09VjsB5.js.br +0 -0
- package/dist/web/assets/send-to-chat-B09VjsB5.js.gz +0 -0
- package/dist/web/assets/services-panel-0klE3O4g.js.br +0 -0
- package/dist/web/assets/services-panel-0klE3O4g.js.gz +0 -0
- package/dist/web/assets/settings-body-CfqpqFfq.js.br +0 -0
- package/dist/web/assets/settings-body-CfqpqFfq.js.gz +0 -0
- package/dist/web/assets/settings-store-3jIBB8gx.js.br +0 -0
- package/dist/web/assets/settings-store-3jIBB8gx.js.gz +0 -0
- package/dist/web/assets/sql-query-editor-ALQ5_X0j.js.br +0 -0
- package/dist/web/assets/sql-query-editor-ALQ5_X0j.js.gz +0 -0
- package/dist/web/assets/sqlite-viewer-DKTty_jR.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-DKTty_jR.js.gz +0 -0
- package/dist/web/assets/system-monitor-body-D6J6lq7-.js.br +0 -0
- package/dist/web/assets/system-monitor-body-D6J6lq7-.js.gz +0 -0
- package/dist/web/assets/system-monitor-tab-Di18zNNS.js +0 -1
- package/dist/web/assets/tab-host-window-content-DTcx2D0O.js.br +0 -0
- package/dist/web/assets/tab-host-window-content-DTcx2D0O.js.gz +0 -0
- package/dist/web/assets/team-member-sheet-CFSbPG6n.js +0 -2
- package/dist/web/assets/team-member-sheet-CFSbPG6n.js.br +0 -0
- package/dist/web/assets/team-member-sheet-CFSbPG6n.js.gz +0 -0
- package/dist/web/assets/team-member-window-content-VeI6DGeM.js +0 -1
- package/dist/web/assets/team-member-window-content-VeI6DGeM.js.br +0 -0
- package/dist/web/assets/team-member-window-content-VeI6DGeM.js.gz +0 -0
- package/dist/web/assets/terminal-tab-DgTfQKBI.js.br +0 -0
- package/dist/web/assets/terminal-tab-DgTfQKBI.js.gz +0 -0
- package/dist/web/assets/tool-cards-_fpYI1hB.js +0 -9
- package/dist/web/assets/tool-cards-_fpYI1hB.js.br +0 -0
- package/dist/web/assets/tool-cards-_fpYI1hB.js.gz +0 -0
- package/dist/web/assets/use-inline-blame-Dv111BKY.js.br +0 -0
- package/dist/web/assets/use-inline-blame-Dv111BKY.js.gz +0 -0
- package/dist/web/assets/use-lsp-CZTFJIXX.js.br +0 -0
- package/dist/web/assets/use-lsp-CZTFJIXX.js.gz +0 -0
- package/dist/web/assets/use-open-team-member-ElN3KmCI.js +0 -1
- package/dist/web/assets/use-process-kill-D7D9QvJG.js.br +0 -0
- package/dist/web/assets/use-process-kill-D7D9QvJG.js.gz +0 -0
- package/dist/web/assets/video-preview-DzwY3q9W.js.br +0 -0
- package/dist/web/assets/video-preview-DzwY3q9W.js.gz +0 -0
- package/dist/web/assets/voice-settings-section-B7zMHsuz.js.br +0 -0
- package/dist/web/assets/voice-settings-section-B7zMHsuz.js.gz +0 -0
- package/dist/web/assets/window-store-B4cbZ5rr.js +0 -1
- package/dist/web/assets/window-store-B4cbZ5rr.js.br +0 -0
- package/dist/web/assets/window-store-B4cbZ5rr.js.gz +0 -0
- package/src/web/components/chat/team-member-window-content.tsx +0 -66
- package/src/web/components/chat/team-working-bar.tsx +0 -62
- package/src/web/components/chat/use-open-team-member.ts +0 -41
- package/src/web/hooks/use-member-transcript-tail.ts +0 -95
- /package/dist/web/assets/{api-settings-BeINN5Dy.js → api-settings-BWgfXl7o.js} +0 -0
- /package/dist/web/assets/{api-settings-BeINN5Dy.js.br → api-settings-BWgfXl7o.js.br} +0 -0
- /package/dist/web/assets/{api-settings-BeINN5Dy.js.gz → api-settings-BWgfXl7o.js.gz} +0 -0
- /package/dist/web/assets/{libesm-CxeQSM3t.js → libesm-DAF5_F99.js} +0 -0
- /package/dist/web/assets/{libesm-CxeQSM3t.js.br → libesm-DAF5_F99.js.br} +0 -0
- /package/dist/web/assets/{libesm-CxeQSM3t.js.gz → libesm-DAF5_F99.js.gz} +0 -0
- /package/dist/web/assets/{onboarding-entry-DD3QSDiz.js → onboarding-entry-Bh-WE3vq.js} +0 -0
- /package/dist/web/assets/{onboarding-entry-DD3QSDiz.js.br → onboarding-entry-Bh-WE3vq.js.br} +0 -0
- /package/dist/web/assets/{onboarding-entry-DD3QSDiz.js.gz → onboarding-entry-Bh-WE3vq.js.gz} +0 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message handlers for the `/ws/global` agent-transcript protocol (see
|
|
3
|
+
* `src/shared/agent-transcript-protocol.ts`). Per-client caps, `subId`
|
|
4
|
+
* bookkeeping and cross-hub client drop live in `agent-transcript-hub-registry.ts`;
|
|
5
|
+
* this module is only the four message shapes and their validation.
|
|
6
|
+
*/
|
|
7
|
+
import { assertSessionInProject } from "./session-ownership.ts";
|
|
8
|
+
import { mapOwnershipErrorCode } from "./agent-transcript-error-map.ts";
|
|
9
|
+
import { sendWsMessage, type AgentTranscriptWsLike } from "./agent-transcript-ws-like.ts";
|
|
10
|
+
import { addTranscriptSubscription } from "./agent-transcript-session-hub.ts";
|
|
11
|
+
import { addActivitySubscription } from "./agent-transcript-session-hub-activity.ts";
|
|
12
|
+
import {
|
|
13
|
+
activitySubCountForClient, dropClientEverywhere, getOrCreateHub, maybeDropHub, projectPathFor,
|
|
14
|
+
registerActivitySub, registerTranscriptSub, totalActivitySubs, totalTranscriptSubs, transcriptSubCountForClient,
|
|
15
|
+
unregisterActivitySub, unregisterTranscriptSub,
|
|
16
|
+
} from "./agent-transcript-hub-registry.ts";
|
|
17
|
+
import {
|
|
18
|
+
MAX_TRANSCRIPT_SUBS_PER_CLIENT, MAX_TRANSCRIPT_SUBS_PER_SERVER,
|
|
19
|
+
type AgentActivitySubscribeMsg, type AgentActivityUnsubscribeMsg,
|
|
20
|
+
type AgentTranscriptErrorMsg, type AgentTranscriptSubscribeMsg, type AgentTranscriptUnsubscribeMsg,
|
|
21
|
+
} from "../../shared/agent-transcript-protocol.ts";
|
|
22
|
+
|
|
23
|
+
function sendError(ws: AgentTranscriptWsLike, subId: string, code: AgentTranscriptErrorMsg["code"]): void {
|
|
24
|
+
sendWsMessage(ws, { type: "agent-transcript:error", subId, code } satisfies AgentTranscriptErrorMsg);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function handleAgentTranscriptSubscribe(ws: AgentTranscriptWsLike, msg: AgentTranscriptSubscribeMsg): void {
|
|
28
|
+
const { subId, projectName, providerId, sessionId, source, cursor } = msg;
|
|
29
|
+
if (typeof subId !== "string" || !subId) return;
|
|
30
|
+
if (
|
|
31
|
+
typeof projectName !== "string" || (providerId !== "claude" && providerId !== "codex") ||
|
|
32
|
+
typeof sessionId !== "string" || !source || typeof source !== "object" ||
|
|
33
|
+
(source.kind !== "card" && source.kind !== "member")
|
|
34
|
+
) {
|
|
35
|
+
return sendError(ws, subId, "bad_request");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const projectPath = projectPathFor(projectName);
|
|
39
|
+
if (!projectPath) return sendError(ws, subId, "not_found");
|
|
40
|
+
|
|
41
|
+
// Replacing an existing subId costs nothing against the cap — drop it first.
|
|
42
|
+
unregisterTranscriptSub(ws, subId);
|
|
43
|
+
|
|
44
|
+
if (transcriptSubCountForClient(ws) >= MAX_TRANSCRIPT_SUBS_PER_CLIENT) return sendError(ws, subId, "limit");
|
|
45
|
+
if (totalTranscriptSubs() >= MAX_TRANSCRIPT_SUBS_PER_SERVER) return sendError(ws, subId, "limit");
|
|
46
|
+
|
|
47
|
+
const owned = assertSessionInProject({ providerId, sessionId, projectPath });
|
|
48
|
+
if (!owned.ok) return sendError(ws, subId, mapOwnershipErrorCode(owned.code));
|
|
49
|
+
|
|
50
|
+
const hub = getOrCreateHub(owned);
|
|
51
|
+
const result = addTranscriptSubscription(hub, ws, subId, source, cursor);
|
|
52
|
+
if (!result.ok) {
|
|
53
|
+
maybeDropHub(hub.key);
|
|
54
|
+
return sendError(ws, subId, result.code ?? "bad_request");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
registerTranscriptSub(ws, subId, hub.key);
|
|
58
|
+
if (result.sendFailed) dropClientEverywhere(ws);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function handleAgentTranscriptUnsubscribe(ws: AgentTranscriptWsLike, msg: AgentTranscriptUnsubscribeMsg): void {
|
|
62
|
+
if (typeof msg.subId === "string") unregisterTranscriptSub(ws, msg.subId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function handleAgentActivitySubscribe(ws: AgentTranscriptWsLike, msg: AgentActivitySubscribeMsg): void {
|
|
66
|
+
const { subId, projectName, providerId, sessionId } = msg;
|
|
67
|
+
if (typeof subId !== "string" || !subId) return;
|
|
68
|
+
if (typeof projectName !== "string" || (providerId !== "claude" && providerId !== "codex") || typeof sessionId !== "string") {
|
|
69
|
+
return sendError(ws, subId, "bad_request");
|
|
70
|
+
}
|
|
71
|
+
const projectPath = projectPathFor(projectName);
|
|
72
|
+
if (!projectPath) return sendError(ws, subId, "not_found");
|
|
73
|
+
|
|
74
|
+
// Replacing an existing subId costs nothing against the cap — drop it first.
|
|
75
|
+
unregisterActivitySub(ws, subId);
|
|
76
|
+
|
|
77
|
+
if (activitySubCountForClient(ws) >= MAX_TRANSCRIPT_SUBS_PER_CLIENT) return sendError(ws, subId, "limit");
|
|
78
|
+
if (totalActivitySubs() >= MAX_TRANSCRIPT_SUBS_PER_SERVER) return sendError(ws, subId, "limit");
|
|
79
|
+
|
|
80
|
+
const owned = assertSessionInProject({ providerId, sessionId, projectPath });
|
|
81
|
+
if (!owned.ok) return sendError(ws, subId, mapOwnershipErrorCode(owned.code));
|
|
82
|
+
|
|
83
|
+
const hub = getOrCreateHub(owned);
|
|
84
|
+
addActivitySubscription(hub, ws, subId);
|
|
85
|
+
registerActivitySub(ws, subId, hub.key);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function handleAgentActivityUnsubscribe(ws: AgentTranscriptWsLike, msg: AgentActivityUnsubscribeMsg): void {
|
|
89
|
+
if (typeof msg.subId === "string") unregisterActivitySub(ws, msg.subId);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function handleAgentTranscriptPing(ws: AgentTranscriptWsLike): void {
|
|
93
|
+
sendWsMessage(ws, { type: "pong" });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Socket closed: drop every transcript and activity subscription it held. */
|
|
97
|
+
export function handleAgentTranscriptClientClosed(ws: AgentTranscriptWsLike): void {
|
|
98
|
+
dropClientEverywhere(ws);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export {
|
|
102
|
+
_debugHubSnapshotForTest, _getSessionHubForTest, _resetAgentTranscriptHubForTest,
|
|
103
|
+
} from "./agent-transcript-hub-registry.ts";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared cache over `groupSubagentsByCard`, so the hub and
|
|
3
|
+
* `nested-subagent-spy.ts` cost one directory scan per session per refresh
|
|
4
|
+
* window between them, not one each. `groupSubagentsByCard` itself reads
|
|
5
|
+
* every `*.meta.json` in the subagents dir — cheap for one caller, not for
|
|
6
|
+
* two independent pollers doing it every tick.
|
|
7
|
+
*
|
|
8
|
+
* Refreshed at most every `INDEX_REFRESH_MS`, except a directory mtime change
|
|
9
|
+
* (a new agent spawned, one finished) forces an immediate refresh regardless
|
|
10
|
+
* of how recently the last one ran — the 2s floor bounds cost, the mtime
|
|
11
|
+
* check bounds staleness.
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
groupSubagentsByCard, indexTranscriptsByMember,
|
|
15
|
+
type SubagentTranscriptEntry,
|
|
16
|
+
} from "../team-member-activity/subagent-transcript-index.ts";
|
|
17
|
+
import { agentTranscriptClock } from "./agent-transcript-hub-clock.ts";
|
|
18
|
+
import { statDirMtimeSafe } from "./agent-transcript-fs-io.ts";
|
|
19
|
+
import { INDEX_REFRESH_MS } from "../../shared/agent-transcript-protocol.ts";
|
|
20
|
+
|
|
21
|
+
/** Bounds how many distinct subagents dirs stay cached at once — a server with
|
|
22
|
+
* many sessions ever subscribed-to should not keep every one of them forever. */
|
|
23
|
+
const MAX_CACHE_ENTRIES = 200;
|
|
24
|
+
|
|
25
|
+
interface CacheEntry {
|
|
26
|
+
groups: Map<string, SubagentTranscriptEntry[]>;
|
|
27
|
+
byMember: Map<string, SubagentTranscriptEntry>;
|
|
28
|
+
refreshedAt: number;
|
|
29
|
+
dirMtimeMs: number | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const cache = new Map<string, CacheEntry>();
|
|
33
|
+
|
|
34
|
+
function evictOldestIfOverCapacity(): void {
|
|
35
|
+
if (cache.size < MAX_CACHE_ENTRIES) return;
|
|
36
|
+
let oldestKey: string | undefined;
|
|
37
|
+
let oldestAt = Infinity;
|
|
38
|
+
for (const [key, entry] of cache) {
|
|
39
|
+
if (entry.refreshedAt < oldestAt) {
|
|
40
|
+
oldestAt = entry.refreshedAt;
|
|
41
|
+
oldestKey = key;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (oldestKey !== undefined) cache.delete(oldestKey);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function refresh(subagentsDir: string): CacheEntry {
|
|
48
|
+
evictOldestIfOverCapacity();
|
|
49
|
+
const entry: CacheEntry = {
|
|
50
|
+
groups: groupSubagentsByCard(subagentsDir),
|
|
51
|
+
byMember: indexTranscriptsByMember(subagentsDir),
|
|
52
|
+
refreshedAt: agentTranscriptClock.now(),
|
|
53
|
+
dirMtimeMs: statDirMtimeSafe(subagentsDir),
|
|
54
|
+
};
|
|
55
|
+
cache.set(subagentsDir, entry);
|
|
56
|
+
return entry;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cached(subagentsDir: string): CacheEntry {
|
|
60
|
+
const now = agentTranscriptClock.now();
|
|
61
|
+
const existing = cache.get(subagentsDir);
|
|
62
|
+
if (!existing) return refresh(subagentsDir);
|
|
63
|
+
|
|
64
|
+
const currentMtime = statDirMtimeSafe(subagentsDir);
|
|
65
|
+
const mtimeChanged = currentMtime !== null && currentMtime !== existing.dirMtimeMs;
|
|
66
|
+
if (mtimeChanged || now - existing.refreshedAt >= INDEX_REFRESH_MS) return refresh(subagentsDir);
|
|
67
|
+
return existing;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Cached `groupSubagentsByCard(subagentsDir)`, refreshed per the policy above. */
|
|
71
|
+
export function getCachedSubagentGroups(subagentsDir: string): Map<string, SubagentTranscriptEntry[]> {
|
|
72
|
+
return cached(subagentsDir).groups;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Cached `indexTranscriptsByMember(subagentsDir)`, same refresh policy as the
|
|
76
|
+
* card-group index — the activity feed's team-member pass used to re-read
|
|
77
|
+
* every `*.meta.json` itself, uncached, on top of the group scan above. */
|
|
78
|
+
export function getCachedTranscriptsByMember(subagentsDir: string): Map<string, SubagentTranscriptEntry> {
|
|
79
|
+
return cached(subagentsDir).byMember;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Test-only: forget every cached entry. */
|
|
83
|
+
export function _resetAgentTranscriptIndexCache(): void {
|
|
84
|
+
cache.clear();
|
|
85
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity-subscription half of a session hub: "who is running right now",
|
|
3
|
+
* pushed every 3s while at least one client is watching. Independent of the
|
|
4
|
+
* transcript ticker — a running bar costs nothing extra when no window is
|
|
5
|
+
* open, and a window being open costs nothing extra for the bar.
|
|
6
|
+
*/
|
|
7
|
+
import { computeRunningAgents } from "./agent-transcript-activity.ts";
|
|
8
|
+
import { agentTranscriptClock, agentTranscriptTimers } from "./agent-transcript-hub-clock.ts";
|
|
9
|
+
import { sendWsMessage, type AgentTranscriptWsLike } from "./agent-transcript-ws-like.ts";
|
|
10
|
+
import type { SessionHub } from "./agent-transcript-session-hub-types.ts";
|
|
11
|
+
import { ACTIVITY_TICK_MS, type AgentActivityMsg } from "../../shared/agent-transcript-protocol.ts";
|
|
12
|
+
|
|
13
|
+
function ensureActivityTicker(hub: SessionHub): void {
|
|
14
|
+
if (hub.activityHandle !== null) return;
|
|
15
|
+
hub.activityHandle = agentTranscriptTimers.setInterval(() => {
|
|
16
|
+
void tickActivity(hub);
|
|
17
|
+
}, ACTIVITY_TICK_MS);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stopActivityTickerIfEmpty(hub: SessionHub): void {
|
|
21
|
+
if (hub.activitySubs.size > 0 || hub.activityHandle === null) return;
|
|
22
|
+
agentTranscriptTimers.clearInterval(hub.activityHandle);
|
|
23
|
+
hub.activityHandle = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function addActivitySubscription(hub: SessionHub, ws: AgentTranscriptWsLike, subId: string): void {
|
|
27
|
+
let subIds = hub.activitySubs.get(ws);
|
|
28
|
+
if (!subIds) {
|
|
29
|
+
subIds = new Set();
|
|
30
|
+
hub.activitySubs.set(ws, subIds);
|
|
31
|
+
}
|
|
32
|
+
subIds.add(subId);
|
|
33
|
+
ensureActivityTicker(hub);
|
|
34
|
+
// Guarded the same way `tickActivity` is below: this runs un-awaited, so a
|
|
35
|
+
// throw anywhere in `computeRunningAgents` (a descendant read racing a
|
|
36
|
+
// delete, a locked rollout) would otherwise become an unhandled rejection
|
|
37
|
+
// rather than something this function can react to.
|
|
38
|
+
void (async () => {
|
|
39
|
+
try {
|
|
40
|
+
const now = agentTranscriptClock.now();
|
|
41
|
+
const running = await computeRunningAgents(hub.owned, now);
|
|
42
|
+
if (!hub.tokenStillValid(ws) || !pushActivity(ws, [subId], running)) hub.onSendFailure(ws);
|
|
43
|
+
} catch {
|
|
44
|
+
console.warn(`[agent-activity] hub=${hub.key} subId=${subId} initial compute failed`);
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function removeActivitySubscription(hub: SessionHub, ws: AgentTranscriptWsLike, subId: string): boolean {
|
|
50
|
+
const subIds = hub.activitySubs.get(ws);
|
|
51
|
+
if (!subIds?.delete(subId)) return false;
|
|
52
|
+
if (subIds.size === 0) hub.activitySubs.delete(ws);
|
|
53
|
+
stopActivityTickerIfEmpty(hub);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function dropAllActivityForWs(hub: SessionHub, ws: AgentTranscriptWsLike): number {
|
|
58
|
+
const subIds = hub.activitySubs.get(ws);
|
|
59
|
+
if (!subIds) return 0;
|
|
60
|
+
hub.activitySubs.delete(ws);
|
|
61
|
+
stopActivityTickerIfEmpty(hub);
|
|
62
|
+
return subIds.size;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function activitySubIdsForWs(hub: SessionHub, ws: AgentTranscriptWsLike): string[] {
|
|
66
|
+
return [...(hub.activitySubs.get(ws)?.values() ?? [])];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Push an already-computed running list to one client's given subscriptions. */
|
|
70
|
+
function pushActivity(ws: AgentTranscriptWsLike, subIds: string[], running: AgentActivityMsg["running"]): boolean {
|
|
71
|
+
for (const subId of subIds) {
|
|
72
|
+
const msg: AgentActivityMsg = { type: "agent-activity", subId, running };
|
|
73
|
+
if (!sendWsMessage(ws, msg)) return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* One 3s wake: compute the running list once for the whole hub and fan it
|
|
80
|
+
* out to every subscriber — a per-session scan (Codex especially) is not
|
|
81
|
+
* cheap enough to repeat once per client watching the same session.
|
|
82
|
+
*
|
|
83
|
+
* Wrapped in its own try/catch because this is invoked as `void tickActivity(hub)`
|
|
84
|
+
* (fire-and-forget, from an un-awaited `setInterval` callback): a throw
|
|
85
|
+
* anywhere in `computeRunningAgents` — a descendant rollout that disappears
|
|
86
|
+
* or locks up between the stat and the read that bounds it — would otherwise
|
|
87
|
+
* surface as an unhandled promise rejection, counted the same way an
|
|
88
|
+
* uncaught exception is, on a 3s timer instead of the 250ms one the cursor fix covered.
|
|
89
|
+
*/
|
|
90
|
+
export async function tickActivity(hub: SessionHub): Promise<void> {
|
|
91
|
+
if (hub.activitySubs.size === 0) return;
|
|
92
|
+
try {
|
|
93
|
+
const now = agentTranscriptClock.now();
|
|
94
|
+
const running = await computeRunningAgents(hub.owned, now);
|
|
95
|
+
const failed: AgentTranscriptWsLike[] = [];
|
|
96
|
+
for (const [ws, subIds] of hub.activitySubs) {
|
|
97
|
+
if (!hub.tokenStillValid(ws) || !pushActivity(ws, [...subIds], running)) failed.push(ws);
|
|
98
|
+
}
|
|
99
|
+
for (const ws of failed) hub.onSendFailure(ws);
|
|
100
|
+
} catch {
|
|
101
|
+
console.warn(`[agent-activity] hub=${hub.key} tick failed`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The read-and-push half of a transcript subscription: one tick's worth of
|
|
3
|
+
* "what's new across every file this subscription watches, including any
|
|
4
|
+
* file the set itself grew by" turned into paged WS pushes with an exact
|
|
5
|
+
* per-page cursor. Split out from `agent-transcript-session-hub.ts` (the
|
|
6
|
+
* subscribe/unsubscribe/tick-scheduling half) to keep both files within the
|
|
7
|
+
* project's file-size guideline.
|
|
8
|
+
*/
|
|
9
|
+
import { resolveCodexDescendantFile, resolveSources } from "./agent-transcript-sources.ts";
|
|
10
|
+
import { createFileTailState } from "./agent-transcript-file-tail-state.ts";
|
|
11
|
+
import { processFileTail, type ProcessFileResult } from "./agent-transcript-file-tail-feed.ts";
|
|
12
|
+
import { paginateTagged, type TaggedEnvelope } from "./agent-transcript-envelope.ts";
|
|
13
|
+
import type { SessionHub, TranscriptSubscription } from "./agent-transcript-session-hub-types.ts";
|
|
14
|
+
import { sendWsMessage, type AgentTranscriptWsLike } from "./agent-transcript-ws-like.ts";
|
|
15
|
+
import {
|
|
16
|
+
IDLE_AFTER_MS, type AgentTranscriptCursor, type AgentTranscriptEventsMsg,
|
|
17
|
+
} from "../../shared/agent-transcript-protocol.ts";
|
|
18
|
+
|
|
19
|
+
type FileEntry = { key: string; result: ProcessFileResult };
|
|
20
|
+
|
|
21
|
+
export function buildCursor(sub: TranscriptSubscription): AgentTranscriptCursor {
|
|
22
|
+
const cursor: AgentTranscriptCursor = {};
|
|
23
|
+
for (const [key, f] of sub.files) cursor[key] = f.offset;
|
|
24
|
+
return cursor;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isIdle(sub: TranscriptSubscription, now: number): boolean {
|
|
28
|
+
if (sub.files.size === 0) return false;
|
|
29
|
+
let latestGrowth = 0;
|
|
30
|
+
for (const f of sub.files.values()) latestGrowth = Math.max(latestGrowth, f.lastGrowthAt);
|
|
31
|
+
return now - latestGrowth >= IDLE_AFTER_MS;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Re-derive every file in a subscription from byte 0, for the rare case where any one of them reset. */
|
|
35
|
+
function fullResync(sub: TranscriptSubscription, now: number): FileEntry[] {
|
|
36
|
+
const entries: FileEntry[] = [];
|
|
37
|
+
for (const [key, f] of sub.files) {
|
|
38
|
+
const fresh = createFileTailState(f.ref, 0, now);
|
|
39
|
+
const result = processFileTail(fresh, now);
|
|
40
|
+
sub.files.set(key, fresh);
|
|
41
|
+
entries.push({ key, result });
|
|
42
|
+
}
|
|
43
|
+
return entries;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Bring a Claude card subscription's file set up to date with the card's
|
|
48
|
+
* current group — a grandchild whose own transcript appears after subscribe
|
|
49
|
+
* must join the stream rather than wait for the window to be reopened. Runs
|
|
50
|
+
* every tick but stays cheap: `resolveSources` caches its own result for ~2s.
|
|
51
|
+
*/
|
|
52
|
+
function refreshClaudeCardFiles(hub: SessionHub, sub: TranscriptSubscription, now: number): void {
|
|
53
|
+
if (hub.owned.providerId !== "claude" || sub.source.kind !== "card") return;
|
|
54
|
+
const resolved = resolveSources(hub.owned, sub.source);
|
|
55
|
+
if (!Array.isArray(resolved)) return;
|
|
56
|
+
for (const ref of resolved) {
|
|
57
|
+
if (sub.files.has(ref.key)) continue;
|
|
58
|
+
sub.files.set(ref.key, createFileTailState(ref, 0, now));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Fold newly-discovered Codex descendant threads — surfaced as `links` from
|
|
64
|
+
* this tick's own reads — into the subscription, each re-validated through
|
|
65
|
+
* the exact same fail-closed descendant chain a directly-requested card goes
|
|
66
|
+
* through (`resolveCodexDescendantFile`), never a weaker check invented for
|
|
67
|
+
* the live-discovery path.
|
|
68
|
+
*/
|
|
69
|
+
function addDiscoveredCodexFiles(hub: SessionHub, sub: TranscriptSubscription, entries: FileEntry[], now: number): FileEntry[] {
|
|
70
|
+
if (hub.owned.providerId !== "codex" || sub.source.kind !== "card") return [];
|
|
71
|
+
const seen = new Set<string>();
|
|
72
|
+
const added: FileEntry[] = [];
|
|
73
|
+
for (const { result } of entries) {
|
|
74
|
+
for (const threadId of result.links) {
|
|
75
|
+
if (seen.has(threadId) || sub.files.has(threadId)) continue;
|
|
76
|
+
seen.add(threadId);
|
|
77
|
+
const ref = resolveCodexDescendantFile(hub.owned, threadId);
|
|
78
|
+
if (!ref) continue;
|
|
79
|
+
const fresh = createFileTailState(ref, 0, now);
|
|
80
|
+
sub.files.set(threadId, fresh);
|
|
81
|
+
added.push({ key: threadId, result: processFileTail(fresh, now) });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return added;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function tagEntries(entries: FileEntry[]): TaggedEnvelope[] {
|
|
88
|
+
const out: TaggedEnvelope[] = [];
|
|
89
|
+
for (const { key, result } of entries) {
|
|
90
|
+
for (let i = 0; i < result.envelopes.length; i++) {
|
|
91
|
+
out.push({ envelope: result.envelopes[i]!, fileKey: key, consumedThrough: result.consumedThrough[i]! });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* One read across every file in the subscription, plus whatever the file set
|
|
99
|
+
* itself grew by this tick (new Claude group entries, newly-linked Codex
|
|
100
|
+
* descendants). If any file reports a reset, every file is re-derived from 0
|
|
101
|
+
* — a client wiping its view on `reset: true` must not lose content from a
|
|
102
|
+
* file that did not itself change.
|
|
103
|
+
*/
|
|
104
|
+
function readSubscription(hub: SessionHub, sub: TranscriptSubscription, now: number): { items: TaggedEnvelope[]; reset: boolean; hasMoreBacklog: boolean } {
|
|
105
|
+
refreshClaudeCardFiles(hub, sub, now);
|
|
106
|
+
|
|
107
|
+
let entries: FileEntry[] = [...sub.files.entries()].map(([key, f]) => ({ key, result: processFileTail(f, now) }));
|
|
108
|
+
const reset = entries.some(({ result }) => result.reset);
|
|
109
|
+
if (reset) entries = fullResync(sub, now);
|
|
110
|
+
|
|
111
|
+
entries = [...entries, ...addDiscoveredCodexFiles(hub, sub, entries, now)];
|
|
112
|
+
|
|
113
|
+
const hasMoreBacklog = entries.some(({ result }) => result.hasMoreBacklog);
|
|
114
|
+
return { items: tagEntries(entries), reset, hasMoreBacklog };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface PushOutcome {
|
|
118
|
+
sent: boolean;
|
|
119
|
+
/** Whether a per-tick read budget left bytes unread — the caller must keep
|
|
120
|
+
* polling at the live cadence, even for an otherwise-idle subscription. */
|
|
121
|
+
hasMoreBacklog: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Drain and send a subscription's current backlog, paging large ones. Every
|
|
126
|
+
* page's `cursor` reflects exactly the file offsets consumed by that page's
|
|
127
|
+
* own content — not the subscription's final offset after the whole tick's
|
|
128
|
+
* read — so a client that only received page k of a multi-page push before
|
|
129
|
+
* dropping resumes at page k+1 on reconnect, never past it.
|
|
130
|
+
*
|
|
131
|
+
* `alwaysSend` covers the subscribe-time call: a client that subscribed
|
|
132
|
+
* already caught up (cursor === current size) still gets one confirming
|
|
133
|
+
* message — silence would be indistinguishable from a subscribe that never
|
|
134
|
+
* reached the hub at all. A live tick with nothing new AND nothing left to
|
|
135
|
+
* catch up on sends nothing, or every idle subscription would cost a message
|
|
136
|
+
* every 250ms/2s forever.
|
|
137
|
+
*/
|
|
138
|
+
function pushUpdate(hub: SessionHub, ws: AgentTranscriptWsLike, sub: TranscriptSubscription, now: number, alwaysSend: boolean): PushOutcome {
|
|
139
|
+
const preTickCursor = buildCursor(sub);
|
|
140
|
+
const { items, reset, hasMoreBacklog } = readSubscription(hub, sub, now);
|
|
141
|
+
const pages = paginateTagged(items);
|
|
142
|
+
if (pages.length === 0) {
|
|
143
|
+
if (!reset && !alwaysSend && !hasMoreBacklog) return { sent: true, hasMoreBacklog: false };
|
|
144
|
+
pages.push([]);
|
|
145
|
+
}
|
|
146
|
+
const running = !isIdle(sub, now);
|
|
147
|
+
// On reset every file is being replaced from scratch, so the cursor for
|
|
148
|
+
// this push starts empty rather than carrying over pre-tick offsets that
|
|
149
|
+
// no longer describe what a client holding them has actually been sent.
|
|
150
|
+
const runningCursor: AgentTranscriptCursor = reset ? {} : { ...preTickCursor };
|
|
151
|
+
for (let i = 0; i < pages.length; i++) {
|
|
152
|
+
for (const item of pages[i]!) runningCursor[item.fileKey] = item.consumedThrough;
|
|
153
|
+
const msg: AgentTranscriptEventsMsg = {
|
|
154
|
+
type: "agent-transcript:events",
|
|
155
|
+
subId: sub.subId,
|
|
156
|
+
events: pages[i]!.map((item) => item.envelope),
|
|
157
|
+
cursor: { ...runningCursor },
|
|
158
|
+
available: true,
|
|
159
|
+
running,
|
|
160
|
+
// Not just "another page is already queued behind this one" — also
|
|
161
|
+
// true on the LAST page when the read budget left bytes unread, so the
|
|
162
|
+
// client keeps showing "still catching up" instead of "done".
|
|
163
|
+
...(i < pages.length - 1 || hasMoreBacklog ? { more: true as const } : {}),
|
|
164
|
+
...(reset && i === 0 ? { reset: true as const } : {}),
|
|
165
|
+
};
|
|
166
|
+
if (!sendWsMessage(ws, msg)) return { sent: false, hasMoreBacklog };
|
|
167
|
+
}
|
|
168
|
+
return { sent: true, hasMoreBacklog };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export type PushResult =
|
|
172
|
+
| { outcome: "ok"; hasMoreBacklog: boolean }
|
|
173
|
+
| { outcome: "send-failed"; hasMoreBacklog: boolean }
|
|
174
|
+
| { outcome: "error"; hasMoreBacklog: false };
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* `pushUpdate`, but a throw anywhere in the read/parse/send path is caught
|
|
178
|
+
* and reported as `outcome: "error"` instead of reaching the caller — a
|
|
179
|
+
* single malformed record or a raced file error must never climb out of a
|
|
180
|
+
* WS message handler or a `setInterval` tick into an uncaught exception that
|
|
181
|
+
* takes the whole server down. Distinct from `"send-failed"` (the socket
|
|
182
|
+
* itself is gone) because the caller must react very differently: a
|
|
183
|
+
* send failure means the WHOLE client is unreachable and every hub should
|
|
184
|
+
* drop it; a thrown error means only THIS subscription's own read/parse
|
|
185
|
+
* broke, and every other subscription on the same socket must keep working.
|
|
186
|
+
*/
|
|
187
|
+
export function safePushUpdate(hub: SessionHub, ws: AgentTranscriptWsLike, sub: TranscriptSubscription, now: number, alwaysSend: boolean): PushResult {
|
|
188
|
+
try {
|
|
189
|
+
const { sent, hasMoreBacklog } = pushUpdate(hub, ws, sub, now, alwaysSend);
|
|
190
|
+
return sent ? { outcome: "ok", hasMoreBacklog } : { outcome: "send-failed", hasMoreBacklog };
|
|
191
|
+
} catch {
|
|
192
|
+
return { outcome: "error", hasMoreBacklog: false };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared state for one `(providerId, sessionId)` hub: the subscriptions
|
|
3
|
+
* currently reading it, and the two lazily-started tickers (live transcript
|
|
4
|
+
* push, activity feed) that exist only while something is subscribed.
|
|
5
|
+
*/
|
|
6
|
+
import type { OwnedSession } from "./session-ownership.ts";
|
|
7
|
+
import type { FileTailState } from "./agent-transcript-file-tail-state.ts";
|
|
8
|
+
import type { AgentTranscriptWsLike } from "./agent-transcript-ws-like.ts";
|
|
9
|
+
import type { AgentTranscriptSourceKind } from "../../shared/agent-transcript-protocol.ts";
|
|
10
|
+
|
|
11
|
+
export interface TranscriptSubscription {
|
|
12
|
+
subId: string;
|
|
13
|
+
source: AgentTranscriptSourceKind;
|
|
14
|
+
files: Map<string, FileTailState>;
|
|
15
|
+
nextTickAt: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SessionHub {
|
|
19
|
+
key: string;
|
|
20
|
+
owned: OwnedSession;
|
|
21
|
+
/** By socket, then by that socket's own `subId` — a client may hold several subscriptions. */
|
|
22
|
+
subscriptions: Map<AgentTranscriptWsLike, Map<string, TranscriptSubscription>>;
|
|
23
|
+
activitySubs: Map<AgentTranscriptWsLike, Set<string>>;
|
|
24
|
+
tickHandle: unknown | null;
|
|
25
|
+
activityHandle: unknown | null;
|
|
26
|
+
/**
|
|
27
|
+
* Owned by the top-level registry (`agent-transcript-hub.ts`), which is the
|
|
28
|
+
* only layer that knows a socket's subscriptions can span several session
|
|
29
|
+
* hubs at once. A failed push or a stale auth token must drop the client
|
|
30
|
+
* everywhere, not just in the hub that happened to detect it.
|
|
31
|
+
*/
|
|
32
|
+
onSendFailure: (ws: AgentTranscriptWsLike) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Owned by the top-level registry, same reason as `onSendFailure` — drop
|
|
35
|
+
* ONE subscription's bookkeeping everywhere it is tracked (this hub's own
|
|
36
|
+
* map AND the registry's per-client `subId` index), not just here. Used
|
|
37
|
+
* when a single subscription's own tick throws: the rest of that socket's
|
|
38
|
+
* subscriptions, on this hub or any other, must keep working.
|
|
39
|
+
*/
|
|
40
|
+
onSubscriptionError: (ws: AgentTranscriptWsLike, subId: string) => void;
|
|
41
|
+
/** True while `ws`'s token snapshot still matches the live config. */
|
|
42
|
+
tokenStillValid: (ws: AgentTranscriptWsLike) => boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createSessionHub(
|
|
46
|
+
key: string,
|
|
47
|
+
owned: OwnedSession,
|
|
48
|
+
onSendFailure: (ws: AgentTranscriptWsLike) => void,
|
|
49
|
+
onSubscriptionError: (ws: AgentTranscriptWsLike, subId: string) => void,
|
|
50
|
+
tokenStillValid: (ws: AgentTranscriptWsLike) => boolean,
|
|
51
|
+
): SessionHub {
|
|
52
|
+
return {
|
|
53
|
+
key,
|
|
54
|
+
owned,
|
|
55
|
+
subscriptions: new Map(),
|
|
56
|
+
activitySubs: new Map(),
|
|
57
|
+
tickHandle: null,
|
|
58
|
+
activityHandle: null,
|
|
59
|
+
onSendFailure,
|
|
60
|
+
onSubscriptionError,
|
|
61
|
+
tokenStillValid,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Total transcript subscriptions across every client on this hub. */
|
|
66
|
+
export function transcriptSubCount(hub: SessionHub): number {
|
|
67
|
+
let n = 0;
|
|
68
|
+
for (const subs of hub.subscriptions.values()) n += subs.size;
|
|
69
|
+
return n;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Total activity subscriptions across every client on this hub. */
|
|
73
|
+
export function activitySubCount(hub: SessionHub): number {
|
|
74
|
+
let n = 0;
|
|
75
|
+
for (const subs of hub.activitySubs.values()) n += subs.size;
|
|
76
|
+
return n;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function hubIsEmpty(hub: SessionHub): boolean {
|
|
80
|
+
return hub.subscriptions.size === 0 && hub.activitySubs.size === 0;
|
|
81
|
+
}
|