zidane 7.2.4 → 8.0.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/LICENSE +21 -0
- package/README.md +34 -6
- package/dist/{acp-D-tVyUjw.js → acp-DQDhi2dj.js} +230 -77
- package/dist/acp-DQDhi2dj.js.map +1 -0
- package/dist/acp-cli.d.ts +1 -1
- package/dist/acp-cli.js +9 -66
- package/dist/acp-cli.js.map +1 -1
- package/dist/acp.d.ts +10 -590
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +3 -3
- package/dist/{agent-D0zZnGbt.js → agent-DSLNmBp0.js} +14401 -13214
- package/dist/agent-DSLNmBp0.js.map +1 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +3 -2
- package/dist/aliasing-lVFbm81n.js +217 -0
- package/dist/aliasing-lVFbm81n.js.map +1 -0
- package/dist/{anthropic-C2aole0G.js → anthropic-CD00LhUP.js} +1442 -1264
- package/dist/anthropic-CD00LhUP.js.map +1 -0
- package/dist/artifacts-CV2hUKnL.js.map +1 -1
- package/dist/{atomic-write-B7zr1ddF.js → atomic-write-Ijgf2Mhc.js} +13 -3
- package/dist/atomic-write-Ijgf2Mhc.js.map +1 -0
- package/dist/call-tool-dHlbGOIY.js +70 -0
- package/dist/call-tool-dHlbGOIY.js.map +1 -0
- package/dist/chat/pure.d.ts +372 -6
- package/dist/chat/pure.d.ts.map +1 -1
- package/dist/chat/pure.js +35 -244
- package/dist/chat/pure.js.map +1 -1
- package/dist/{approvals-DKaJZYlm.js → chat-JPJJ8uu5.js} +15879 -14631
- package/dist/chat-JPJJ8uu5.js.map +1 -0
- package/dist/chat.d.ts +4 -6
- package/dist/chat.js +23 -12
- package/dist/cli-shared-C-YCaglL.js +159 -0
- package/dist/cli-shared-C-YCaglL.js.map +1 -0
- package/dist/client-core-BQSyDALT.d.ts +2115 -0
- package/dist/client-core-BQSyDALT.d.ts.map +1 -0
- package/dist/codemode-worker.d.ts +1 -0
- package/dist/codemode-worker.js +155 -0
- package/dist/codemode-worker.js.map +1 -0
- package/dist/codemode.d.ts +352 -0
- package/dist/codemode.d.ts.map +1 -0
- package/dist/codemode.js +5 -0
- package/dist/connection-Dr-Js4yZ.d.ts +28 -0
- package/dist/connection-Dr-Js4yZ.d.ts.map +1 -0
- package/dist/connection-loop-gdh22LTI.js +112 -0
- package/dist/connection-loop-gdh22LTI.js.map +1 -0
- package/dist/context-segments-BHKlagL6.js +1016 -0
- package/dist/context-segments-BHKlagL6.js.map +1 -0
- package/dist/{context-transport-T_6AYIvy.js → context-transport-DSMTUsK1.js} +3 -3
- package/dist/{context-transport-T_6AYIvy.js.map → context-transport-DSMTUsK1.js.map} +1 -1
- package/dist/contexts/daytona.d.ts +19 -8
- package/dist/contexts/daytona.d.ts.map +1 -1
- package/dist/contexts/daytona.js +1 -440
- package/dist/contexts/docker.d.ts +36 -7
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js +1 -456
- package/dist/contexts/e2b.d.ts +68 -13
- package/dist/contexts/e2b.d.ts.map +1 -1
- package/dist/contexts/e2b.js +1 -382
- package/dist/contexts/sandbox.d.ts +1 -1
- package/dist/contexts/sandbox.js +5 -0
- package/dist/contexts/sandbox.js.map +1 -1
- package/dist/{contexts-CRE9pw07.js → contexts-d7UolYot.js} +1059 -524
- package/dist/contexts-d7UolYot.js.map +1 -0
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/cred-keys-GpBg71cP.js +105 -0
- package/dist/cred-keys-GpBg71cP.js.map +1 -0
- package/dist/{auth-BG2rVKSt.js → credentials-JttEuRON.js} +55 -206
- package/dist/credentials-JttEuRON.js.map +1 -0
- package/dist/{cursor-iojRXHF9.js → cursor-CdjaQTBW.js} +3 -3
- package/dist/{cursor-iojRXHF9.js.map → cursor-CdjaQTBW.js.map} +1 -1
- package/dist/daemon-cli.d.ts +1 -1
- package/dist/daemon-cli.js +3 -2
- package/dist/daemon-cli.js.map +1 -1
- package/dist/daytona-DG3omipZ.js +513 -0
- package/dist/daytona-DG3omipZ.js.map +1 -0
- package/dist/defaults-Cx3zvDOV.js +23 -0
- package/dist/defaults-Cx3zvDOV.js.map +1 -0
- package/dist/dispatcher-BMlu2BmZ.js +4941 -0
- package/dist/dispatcher-BMlu2BmZ.js.map +1 -0
- package/dist/docker-BZhMMJUp.js +727 -0
- package/dist/docker-BZhMMJUp.js.map +1 -0
- package/dist/e2b-CEVOdRib.js +480 -0
- package/dist/e2b-CEVOdRib.js.map +1 -0
- package/dist/edit-utils-EGosADZq.js +234 -0
- package/dist/edit-utils-EGosADZq.js.map +1 -0
- package/dist/environment-8wsKE2Ln.js +214 -0
- package/dist/environment-8wsKE2Ln.js.map +1 -0
- package/dist/eval-BNcvle_J.js +1595 -0
- package/dist/eval-BNcvle_J.js.map +1 -0
- package/dist/eval.d.ts +2 -2
- package/dist/eval.js +1 -1489
- package/dist/executor-Cx5-8yqi.js +621 -0
- package/dist/executor-Cx5-8yqi.js.map +1 -0
- package/dist/extension-authoring-DyGGmq9V.js +385 -0
- package/dist/extension-authoring-DyGGmq9V.js.map +1 -0
- package/dist/extensions.d.ts +2 -2
- package/dist/extensions.js +3 -3
- package/dist/{fetch-url-DX75gpL0.js → fetch-url-Ct2TjDWt.js} +4 -4
- package/dist/{fetch-url-DX75gpL0.js.map → fetch-url-Ct2TjDWt.js.map} +1 -1
- package/dist/{format-BF745pLL.js → format-C2NQEVUH.js} +39 -2
- package/dist/format-C2NQEVUH.js.map +1 -0
- package/dist/format-bytes-C7tfc3Zm.js +55 -0
- package/dist/format-bytes-C7tfc3Zm.js.map +1 -0
- package/dist/headless-C_rNUuYC.js +960 -0
- package/dist/headless-C_rNUuYC.js.map +1 -0
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +1 -413
- package/dist/{history-search-C4bk1Wz_.js → history-search-CcYzF8-0.js} +7 -3
- package/dist/history-search-CcYzF8-0.js.map +1 -0
- package/dist/history-search-worker.d.ts +1 -1
- package/dist/history-search-worker.js +1 -1
- package/dist/host-D7NrI03V.js +378 -0
- package/dist/host-D7NrI03V.js.map +1 -0
- package/dist/host-signals-BcP5QKUD.js +118 -0
- package/dist/host-signals-BcP5QKUD.js.map +1 -0
- package/dist/identity-CwfO8K-t.js +26 -0
- package/dist/identity-CwfO8K-t.js.map +1 -0
- package/dist/{index-BgN1T3lk.d.ts → index-BGe1fQT7.d.ts} +10987 -9320
- package/dist/index-BGe1fQT7.d.ts.map +1 -0
- package/dist/index-BVQsIKGn.d.ts +448 -0
- package/dist/index-BVQsIKGn.d.ts.map +1 -0
- package/dist/{index-C6NEd5y_.d.ts → index-CHABJd-L.d.ts} +3386 -502
- package/dist/index-CHABJd-L.d.ts.map +1 -0
- package/dist/{index-C37_ppWv.d.ts → index-D6a7qXpg.d.ts} +2 -3
- package/dist/index-D6a7qXpg.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +27 -23
- package/dist/{completion-core-BRo86R_L.js → inline-ghost-BVRQeGoL.js} +50 -2
- package/dist/inline-ghost-BVRQeGoL.js.map +1 -0
- package/dist/ipc/remote.d.ts +313 -0
- package/dist/ipc/remote.d.ts.map +1 -0
- package/dist/ipc/remote.js +4 -0
- package/dist/ipc.d.ts +6 -964
- package/dist/ipc.js +403 -4
- package/dist/ipc.js.map +1 -0
- package/dist/{isolation-Kfoof0MM.d.ts → isolation-D9L650W5.d.ts} +100 -15
- package/dist/isolation-D9L650W5.d.ts.map +1 -0
- package/dist/json-rpc--HXQti1E.js +350 -0
- package/dist/json-rpc--HXQti1E.js.map +1 -0
- package/dist/links-j8rED1sR.js +603 -0
- package/dist/links-j8rED1sR.js.map +1 -0
- package/dist/local-terminal-D8QThwo_.js +252 -0
- package/dist/local-terminal-D8QThwo_.js.map +1 -0
- package/dist/{logger--C1VQjwJ.js → logger-Gtqlayx8.js} +199 -173
- package/dist/logger-Gtqlayx8.js.map +1 -0
- package/dist/logs-view-CWG8aUAG.js.map +1 -1
- package/dist/{mcp-CgNd-f3F.js → mcp-_wecH9Qe.js} +608 -562
- package/dist/mcp-_wecH9Qe.js.map +1 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/media-sniff-UKijEaiQ.js.map +1 -1
- package/dist/{messages-2jcOA_5R.js → messages-BqiwcD2Q.js} +1017 -1014
- package/dist/messages-BqiwcD2Q.js.map +1 -0
- package/dist/{oauth-DxHO17JE.js → oauth-Px_pBB9W.js} +5 -5
- package/dist/{oauth-DxHO17JE.js.map → oauth-Px_pBB9W.js.map} +1 -1
- package/dist/{openai-compat-Dy56weK8.js → openai-compat-CFrpH7uq.js} +432 -589
- package/dist/openai-compat-CFrpH7uq.js.map +1 -0
- package/dist/orchestration-BCGmB8Ps.js +511 -0
- package/dist/orchestration-BCGmB8Ps.js.map +1 -0
- package/dist/output/stream-json.d.ts +1 -2
- package/dist/output/stream-json.d.ts.map +1 -1
- package/dist/output/stream-json.js +1 -1
- package/dist/output/terminal.d.ts +1 -2
- package/dist/output/terminal.d.ts.map +1 -1
- package/dist/output/terminal.js +13 -7
- package/dist/output/terminal.js.map +1 -1
- package/dist/{plugins-gbI7SBA6.js → plugins-DS7mahzZ.js} +20 -101
- package/dist/plugins-DS7mahzZ.js.map +1 -0
- package/dist/plugins.d.ts +1 -1
- package/dist/plugins.js +1 -2
- package/dist/prelude-Cxo4t74B.js +517 -0
- package/dist/prelude-Cxo4t74B.js.map +1 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -2
- package/dist/project-root-B8zADr4o.js +167 -0
- package/dist/project-root-B8zADr4o.js.map +1 -0
- package/dist/project-user-paths-BSgtrRxl.js +58 -0
- package/dist/project-user-paths-BSgtrRxl.js.map +1 -0
- package/dist/{prompts-Pqc3JdW_.d.ts → prompts-DYcvqShf.d.ts} +14 -11
- package/dist/prompts-DYcvqShf.d.ts.map +1 -0
- package/dist/protocol-DnhqqSmE.js +13 -0
- package/dist/protocol-DnhqqSmE.js.map +1 -0
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.js +1 -2
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/arcee.js +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/baseten.js +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/cerebras.js +3 -2
- package/dist/providers/cerebras.js.map +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/local.js +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +1 -2
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +15 -12
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +3 -2
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +3 -3
- package/dist/{providers-BVhnFyiY.js → providers-XUR7F4Hi.js} +4 -4
- package/dist/{providers-BVhnFyiY.js.map → providers-XUR7F4Hi.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +4 -4
- package/dist/registry-D6ShBWJU.js +5234 -0
- package/dist/registry-D6ShBWJU.js.map +1 -0
- package/dist/remote-hosts-BtA_9ZGp.js +97 -0
- package/dist/remote-hosts-BtA_9ZGp.js.map +1 -0
- package/dist/restate.d.ts +2 -3
- package/dist/restate.d.ts.map +1 -1
- package/dist/restate.js +631 -572
- package/dist/restate.js.map +1 -1
- package/dist/run-summary-CklLiZ4y.js +288 -0
- package/dist/run-summary-CklLiZ4y.js.map +1 -0
- package/dist/{sandbox-BH0WQkXn.d.ts → sandbox-C5dR5DwQ.d.ts} +6 -3
- package/dist/sandbox-C5dR5DwQ.d.ts.map +1 -0
- package/dist/session/sqlite.d.ts +1 -2
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +1 -616
- package/dist/session-factory-BUGY5P40.js +1576 -0
- package/dist/session-factory-BUGY5P40.js.map +1 -0
- package/dist/{session-BqRP1hS7.js → session-xh9JXu6i.js} +94 -52
- package/dist/session-xh9JXu6i.js.map +1 -0
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/settings-keys-Ckf29NeM.js +677 -0
- package/dist/settings-keys-Ckf29NeM.js.map +1 -0
- package/dist/shell-path-Dqaoow2J.js +412 -0
- package/dist/shell-path-Dqaoow2J.js.map +1 -0
- package/dist/{skills-BC3dodWp.js → skills-DKqZJm4X.js} +430 -470
- package/dist/skills-DKqZJm4X.js.map +1 -0
- package/dist/skills.d.ts +2 -2
- package/dist/skills.js +1 -2
- package/dist/source-mPExaUm8.js +381 -0
- package/dist/source-mPExaUm8.js.map +1 -0
- package/dist/sqlite-DV1DQzN2.js +698 -0
- package/dist/sqlite-DV1DQzN2.js.map +1 -0
- package/dist/{src-CkNQKFKr.js → src-BRMYRJib.js} +903 -1121
- package/dist/src-BRMYRJib.js.map +1 -0
- package/dist/{stdio-DPpBDPx5.js → stdio-C6YyoW3I.js} +4 -4
- package/dist/{stdio-DPpBDPx5.js.map → stdio-C6YyoW3I.js.map} +1 -1
- package/dist/{stdio-loader-Bbx19dMZ.js → stdio-loader-C830Vg-Z.js} +4 -4
- package/dist/{stdio-loader-Bbx19dMZ.js.map → stdio-loader-C830Vg-Z.js.map} +1 -1
- package/dist/system-prompt-MNVHatSD.d.ts.map +1 -1
- package/dist/timeout-DEaqslLO.d.ts.map +1 -1
- package/dist/todo-edits-Dn251YUP.js +1790 -0
- package/dist/todo-edits-Dn251YUP.js.map +1 -0
- package/dist/{turn-operations-DrbJlpq9.js → tool-formatters-IQFDrdrs.js} +1456 -2120
- package/dist/tool-formatters-IQFDrdrs.js.map +1 -0
- package/dist/tool-state-Dku6YqAB.js +989 -0
- package/dist/tool-state-Dku6YqAB.js.map +1 -0
- package/dist/tools/fetch-url.d.ts +1 -2
- package/dist/tools/fetch-url.d.ts.map +1 -1
- package/dist/tools/fetch-url.js +1 -2
- package/dist/tools/prompts.d.ts +1 -1
- package/dist/tools/prompts.js +7 -7
- package/dist/tools/prompts.js.map +1 -1
- package/dist/tools/web-search.d.ts +1 -2
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +2 -2
- package/dist/tools/web-search.js.map +1 -1
- package/dist/tools-_DnT3AGM.js +578 -0
- package/dist/tools-_DnT3AGM.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +4 -2
- package/dist/tui.d.ts +534 -517
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +34933 -27242
- package/dist/tui.js.map +1 -1
- package/dist/turn-operations-B8sokwi5.js +333 -0
- package/dist/turn-operations-B8sokwi5.js.map +1 -0
- package/dist/turn-operations-DQX5ZZni.d.ts +3089 -0
- package/dist/turn-operations-DQX5ZZni.d.ts.map +1 -0
- package/dist/types-C7ZFOztE.d.ts +132 -0
- package/dist/types-C7ZFOztE.d.ts.map +1 -0
- package/dist/types-CH5XpIHw.js +114 -0
- package/dist/types-CH5XpIHw.js.map +1 -0
- package/dist/{types-CWgENVLY.js → types-Cu0ZGDu9.js} +10 -6
- package/dist/types-Cu0ZGDu9.js.map +1 -0
- package/dist/types-IqegbJEE.d.ts +624 -0
- package/dist/types-IqegbJEE.d.ts.map +1 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.js +1 -1
- package/dist/url-D8HCl65b.js +1259 -0
- package/dist/url-D8HCl65b.js.map +1 -0
- package/dist/wasm-BVT-j5cl.js +33 -0
- package/dist/wasm-BVT-j5cl.js.map +1 -0
- package/dist/workspace-exec-stream-SV2R9d5S.js +355 -0
- package/dist/workspace-exec-stream-SV2R9d5S.js.map +1 -0
- package/dist/ws-CIHMvlSh.js +952 -0
- package/dist/ws-CIHMvlSh.js.map +1 -0
- package/dist/ws-client-N7NxzvQp.js +230 -0
- package/dist/ws-client-N7NxzvQp.js.map +1 -0
- package/dist/{xai-CazjStOK.js → xai-CvJTeLrI.js} +165 -5
- package/dist/xai-CvJTeLrI.js.map +1 -0
- package/dist/xdg-zweed0-t.js +117 -0
- package/dist/xdg-zweed0-t.js.map +1 -0
- package/docs/ACP.md +52 -0
- package/docs/ARCHITECTURE.md +103 -6
- package/docs/ARTIFACTS.md +8 -4
- package/docs/CHAT.md +165 -14
- package/docs/EXECUTION_CONTEXT.md +8 -0
- package/docs/EXTENSIONS.md +123 -2
- package/docs/HEADLESS.md +2 -0
- package/docs/IPC.md +775 -64
- package/docs/MACHINES.md +456 -0
- package/docs/PANELS.md +3 -0
- package/docs/README.md +4 -0
- package/docs/RESTATE.md +3 -2
- package/docs/RUN_IN_BACKGROUND.md +10 -2
- package/docs/SERVER.md +1085 -0
- package/docs/SKILL.md +6 -1
- package/docs/TERMINAL.md +75 -0
- package/docs/TUI.md +65 -14
- package/package.json +58 -26
- package/scripts/eval.ts +4 -0
- package/dist/acp-D-tVyUjw.js.map +0 -1
- package/dist/agent-D0zZnGbt.js.map +0 -1
- package/dist/anthropic-C2aole0G.js.map +0 -1
- package/dist/approvals-DKaJZYlm.js.map +0 -1
- package/dist/atomic-write-B7zr1ddF.js.map +0 -1
- package/dist/auth-BG2rVKSt.js.map +0 -1
- package/dist/completion-core-BRo86R_L.js.map +0 -1
- package/dist/contexts/daytona.js.map +0 -1
- package/dist/contexts/docker.js.map +0 -1
- package/dist/contexts/e2b.js.map +0 -1
- package/dist/contexts-CRE9pw07.js.map +0 -1
- package/dist/eval.js.map +0 -1
- package/dist/format-BF745pLL.js.map +0 -1
- package/dist/glob-DluQFSw1.js +0 -270
- package/dist/glob-DluQFSw1.js.map +0 -1
- package/dist/headless.js.map +0 -1
- package/dist/history-search-C4bk1Wz_.js.map +0 -1
- package/dist/index-BgN1T3lk.d.ts.map +0 -1
- package/dist/index-C37_ppWv.d.ts.map +0 -1
- package/dist/index-C6NEd5y_.d.ts.map +0 -1
- package/dist/ipc-zVSyo6K1.js +0 -331
- package/dist/ipc-zVSyo6K1.js.map +0 -1
- package/dist/ipc.d.ts.map +0 -1
- package/dist/isolation-Kfoof0MM.d.ts.map +0 -1
- package/dist/json-rpc-rSWMUh9b.js +0 -192
- package/dist/json-rpc-rSWMUh9b.js.map +0 -1
- package/dist/logger--C1VQjwJ.js.map +0 -1
- package/dist/mcp-CgNd-f3F.js.map +0 -1
- package/dist/messages-2jcOA_5R.js.map +0 -1
- package/dist/openai-compat-Dy56weK8.js.map +0 -1
- package/dist/output-cap-C9zukcCj.js +0 -100
- package/dist/output-cap-C9zukcCj.js.map +0 -1
- package/dist/panels-B6gRk4bf.js +0 -789
- package/dist/panels-B6gRk4bf.js.map +0 -1
- package/dist/plugins-gbI7SBA6.js.map +0 -1
- package/dist/prompts-Pqc3JdW_.d.ts.map +0 -1
- package/dist/run-summary-BU_NgwUT.js +0 -269
- package/dist/run-summary-BU_NgwUT.js.map +0 -1
- package/dist/sandbox-BH0WQkXn.d.ts.map +0 -1
- package/dist/session/sqlite.js.map +0 -1
- package/dist/session-BqRP1hS7.js.map +0 -1
- package/dist/shell-path-h5czzllR.js +0 -1549
- package/dist/shell-path-h5czzllR.js.map +0 -1
- package/dist/skills-BC3dodWp.js.map +0 -1
- package/dist/src-CkNQKFKr.js.map +0 -1
- package/dist/todo-edits-UOwgxGKu.js +0 -528
- package/dist/todo-edits-UOwgxGKu.js.map +0 -1
- package/dist/tool-formatters-DoRLnfuD.d.ts +0 -759
- package/dist/tool-formatters-DoRLnfuD.d.ts.map +0 -1
- package/dist/transcript-anchors-BeudMbXx.d.ts +0 -1762
- package/dist/transcript-anchors-BeudMbXx.d.ts.map +0 -1
- package/dist/turn-operations-C8bMBDl_.d.ts +0 -1258
- package/dist/turn-operations-C8bMBDl_.d.ts.map +0 -1
- package/dist/turn-operations-DrbJlpq9.js.map +0 -1
- package/dist/types-CWgENVLY.js.map +0 -1
- package/dist/xai-CazjStOK.js.map +0 -1
|
@@ -1,1555 +1,67 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as readFileInputPaths, t as formatBytes } from "./format-bytes-C7tfc3Zm.js";
|
|
2
|
+
import { a as fmtTokens, c as formatElapsed, d as previewLine, f as shortId, s as formatDuration, t as ageString } from "./format-C2NQEVUH.js";
|
|
2
3
|
import { r as utf8ByteLength } from "./utils-ngQzYzZD.js";
|
|
3
|
-
import { a as formatDuration, i as fmtTokens, l as shortId, t as ageString } from "./format-BF745pLL.js";
|
|
4
|
-
import { t as escapeXml } from "./xml-D5HBhJle.js";
|
|
5
4
|
import { t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
|
|
6
|
-
import {
|
|
5
|
+
import { _ as shortSessionId } from "./orchestration-BCGmB8Ps.js";
|
|
6
|
+
import { a as searchHighlightRanges } from "./history-search-CcYzF8-0.js";
|
|
7
7
|
import { Fzf } from "fzf";
|
|
8
|
-
//#region src/chat/
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const parts = projectRoot.split(/[\\/]/).filter(Boolean);
|
|
18
|
-
return parts[parts.length - 1] ?? projectRoot;
|
|
19
|
-
}
|
|
8
|
+
//#region src/chat/todo-glyphs.ts
|
|
9
|
+
const TODO_STATUS_GLYPHS = {
|
|
10
|
+
pending: "☐",
|
|
11
|
+
in_progress: "◐",
|
|
12
|
+
completed: "☑",
|
|
13
|
+
cancelled: "☒"
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/chat/agent-accent.ts
|
|
20
17
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* a refresh swaps it, so a WeakMap invalidates exactly when it should.
|
|
18
|
+
* Resolve a profile's `accent` token to a concrete hex color via the
|
|
19
|
+
* caller's color palette. Renderer-agnostic — accepts the four-role
|
|
20
|
+
* subset of `ThemeColors` that's relevant to accent surfaces, so this
|
|
21
|
+
* helper composes against either `useColors()` (TUI) or a CSS-variable
|
|
22
|
+
* lookup (GUI). Omitted / unknown tokens fall back to `accent`.
|
|
27
23
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
tokens: /* @__PURE__ */ new Map()
|
|
35
|
-
};
|
|
36
|
-
tokensByCatalog.set(catalog, cached);
|
|
37
|
-
}
|
|
38
|
-
return cached;
|
|
39
|
-
}
|
|
40
|
-
function tokenFor(id, catalog) {
|
|
41
|
-
const { normalizedIds, tokens } = catalogTokens(catalog);
|
|
42
|
-
const known = tokens.get(id);
|
|
43
|
-
if (known !== void 0) return known;
|
|
44
|
-
const normalized = normalizeId(id);
|
|
45
|
-
let result;
|
|
46
|
-
for (let len = Math.min(MIN_TOKEN_LENGTH, normalized.length); len <= normalized.length; len++) {
|
|
47
|
-
const token = normalized.slice(0, len);
|
|
48
|
-
if (normalizedIds.filter((candidate) => candidate.startsWith(token)).length === 1) {
|
|
49
|
-
result = token;
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
result ??= normalized || shortId(id);
|
|
54
|
-
tokens.set(id, result);
|
|
55
|
-
return result;
|
|
56
|
-
}
|
|
57
|
-
function stripTrailingPunctuation(candidate) {
|
|
58
|
-
return candidate.replace(/[.,;:)\]}!?]+$/, "");
|
|
59
|
-
}
|
|
60
|
-
function descriptionFor(entry, token, scope) {
|
|
61
|
-
const bits = [
|
|
62
|
-
`#${token}`,
|
|
63
|
-
ageString(entry.updatedAt),
|
|
64
|
-
`${entry.userMessageCount} msg${entry.userMessageCount === 1 ? "" : "s"}`
|
|
65
|
-
];
|
|
66
|
-
if ((entry.artifactCount ?? 0) > 0) bits.push(`${entry.artifactCount} artifact${entry.artifactCount === 1 ? "" : "s"}`);
|
|
67
|
-
const project = projectLabel(entry.projectRoot);
|
|
68
|
-
if (scope === "all" && project) bits.push(project);
|
|
69
|
-
return bits.join(" · ");
|
|
70
|
-
}
|
|
71
|
-
function scoreChats(catalog, tokenCatalog, query, limit, scope) {
|
|
72
|
-
const q = query.trim().toLowerCase();
|
|
73
|
-
const rows = catalog.map((entry) => ({
|
|
74
|
-
entry,
|
|
75
|
-
token: tokenFor(entry.id, tokenCatalog),
|
|
76
|
-
project: projectLabel(entry.projectRoot)
|
|
77
|
-
}));
|
|
78
|
-
const fuzzyHits = q.length > 0 ? new Set(new Fzf(rows, {
|
|
79
|
-
selector: (r) => `${r.entry.title} ${r.token} ${r.project}`,
|
|
80
|
-
casing: "case-insensitive"
|
|
81
|
-
}).find(q).map((r) => r.item)) : null;
|
|
82
|
-
const scored = [];
|
|
83
|
-
for (const row of rows) {
|
|
84
|
-
const title = row.entry.title.toLowerCase();
|
|
85
|
-
const token = row.token.toLowerCase();
|
|
86
|
-
const project = row.project.toLowerCase();
|
|
87
|
-
if (q.length === 0) {
|
|
88
|
-
scored.push({
|
|
89
|
-
entry: row.entry,
|
|
90
|
-
token: row.token,
|
|
91
|
-
rank: 5
|
|
92
|
-
});
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (token.startsWith(q)) {
|
|
96
|
-
scored.push({
|
|
97
|
-
entry: row.entry,
|
|
98
|
-
token: row.token,
|
|
99
|
-
rank: 0
|
|
100
|
-
});
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (title.startsWith(q)) {
|
|
104
|
-
scored.push({
|
|
105
|
-
entry: row.entry,
|
|
106
|
-
token: row.token,
|
|
107
|
-
rank: 1
|
|
108
|
-
});
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (title.includes(q)) {
|
|
112
|
-
scored.push({
|
|
113
|
-
entry: row.entry,
|
|
114
|
-
token: row.token,
|
|
115
|
-
rank: 2
|
|
116
|
-
});
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (project.includes(q)) {
|
|
120
|
-
scored.push({
|
|
121
|
-
entry: row.entry,
|
|
122
|
-
token: row.token,
|
|
123
|
-
rank: 3
|
|
124
|
-
});
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if (fuzzyHits?.has(row)) scored.push({
|
|
128
|
-
entry: row.entry,
|
|
129
|
-
token: row.token,
|
|
130
|
-
rank: 4
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
scored.sort((a, b) => {
|
|
134
|
-
if (a.rank !== b.rank) return a.rank - b.rank;
|
|
135
|
-
return b.entry.updatedAt - a.entry.updatedAt;
|
|
136
|
-
});
|
|
137
|
-
return scored.slice(0, limit).map(({ entry, token }) => ({
|
|
138
|
-
id: token,
|
|
139
|
-
label: entry.title,
|
|
140
|
-
description: descriptionFor(entry, token, scope),
|
|
141
|
-
insertText: `#${token} `,
|
|
142
|
-
data: {
|
|
143
|
-
entry,
|
|
144
|
-
token
|
|
145
|
-
}
|
|
146
|
-
}));
|
|
147
|
-
}
|
|
148
|
-
function createChatsCompletionProvider(opts) {
|
|
149
|
-
const limit = opts.limit ?? DEFAULT_RESULT_LIMIT$1;
|
|
150
|
-
const scope = () => opts.getScope?.() ?? "project";
|
|
151
|
-
return {
|
|
152
|
-
id: "chats",
|
|
153
|
-
trigger: "#",
|
|
154
|
-
label: "Chats",
|
|
155
|
-
suggest(query) {
|
|
156
|
-
const current = opts.getCatalog();
|
|
157
|
-
const tokenCatalog = opts.getReferenceCatalog?.() ?? current;
|
|
158
|
-
const effectiveTokenCatalog = tokenCatalog.length > 0 ? tokenCatalog : current;
|
|
159
|
-
if (current.length > 0) return scoreChats(current, effectiveTokenCatalog, query, limit, scope());
|
|
160
|
-
if (opts.ensureCatalog) return opts.ensureCatalog().then((loaded) => {
|
|
161
|
-
const refreshed = opts.getReferenceCatalog?.() ?? loaded;
|
|
162
|
-
return scoreChats(loaded, refreshed.length > 0 ? refreshed : loaded, query, limit, scope());
|
|
163
|
-
});
|
|
164
|
-
return [];
|
|
165
|
-
},
|
|
166
|
-
parseReferences(text, _ctx) {
|
|
167
|
-
const catalog = opts.getReferenceCatalog?.() ?? opts.getCatalog();
|
|
168
|
-
if (catalog.length === 0) return [];
|
|
169
|
-
const byToken = /* @__PURE__ */ new Map();
|
|
170
|
-
for (const entry of catalog) {
|
|
171
|
-
const token = tokenFor(entry.id, catalog);
|
|
172
|
-
byToken.set(token, {
|
|
173
|
-
entry,
|
|
174
|
-
token
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
const refs = [];
|
|
178
|
-
for (const m of text.matchAll(/(^|\s)#(\S+)/g)) {
|
|
179
|
-
const candidate = stripTrailingPunctuation(m[2]);
|
|
180
|
-
const data = byToken.get(candidate.toLowerCase());
|
|
181
|
-
if (!data) continue;
|
|
182
|
-
const start = m.index + m[1].length;
|
|
183
|
-
refs.push({
|
|
184
|
-
providerId: "chats",
|
|
185
|
-
start,
|
|
186
|
-
end: start + 1 + candidate.length,
|
|
187
|
-
itemId: data.entry.id,
|
|
188
|
-
data
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
return refs;
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
function uniqueChatIdsFromReferences(references) {
|
|
196
|
-
const out = [];
|
|
197
|
-
const seen = /* @__PURE__ */ new Set();
|
|
198
|
-
for (const ref of references) {
|
|
199
|
-
if (ref.providerId !== "chats") continue;
|
|
200
|
-
if (seen.has(ref.itemId)) continue;
|
|
201
|
-
seen.add(ref.itemId);
|
|
202
|
-
out.push(ref.itemId);
|
|
203
|
-
}
|
|
204
|
-
return out;
|
|
205
|
-
}
|
|
206
|
-
function chatReferenceTokensFromText(text) {
|
|
207
|
-
const out = [];
|
|
208
|
-
const seen = /* @__PURE__ */ new Set();
|
|
209
|
-
for (const m of text.matchAll(/(^|\s)#(\S+)/g)) {
|
|
210
|
-
const token = stripTrailingPunctuation(m[2]).toLowerCase();
|
|
211
|
-
if (!token || seen.has(token)) continue;
|
|
212
|
-
seen.add(token);
|
|
213
|
-
out.push(token);
|
|
24
|
+
function accentColor(accent, COLOR) {
|
|
25
|
+
switch (accent) {
|
|
26
|
+
case "brand": return COLOR.brand;
|
|
27
|
+
case "warn": return COLOR.warn;
|
|
28
|
+
case "model": return COLOR.model;
|
|
29
|
+
default: return COLOR.accent;
|
|
214
30
|
}
|
|
215
|
-
return out;
|
|
216
31
|
}
|
|
217
32
|
//#endregion
|
|
218
|
-
//#region src/chat/
|
|
33
|
+
//#region src/chat/turn-selection.ts
|
|
34
|
+
/** Tools whose `tool-result` event is suppressed when `showEditDiffs` is on. */
|
|
35
|
+
const EDIT_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
36
|
+
"edit",
|
|
37
|
+
"multi_edit",
|
|
38
|
+
"write_file"
|
|
39
|
+
]);
|
|
219
40
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* Length is `Math.max(mask.length, fallbackLength)` so callers passing a
|
|
224
|
-
* shorter mask still get a fully-populated array — missing entries
|
|
225
|
-
* default to applied, matching the "no decision => keep" convention.
|
|
41
|
+
* Keep turn-selection mode alive after deleting its active row. Prefer the
|
|
42
|
+
* row that slides into the deleted index; at the tail, select the new last
|
|
43
|
+
* row. Returns null only when no selectable rows survive.
|
|
226
44
|
*/
|
|
227
|
-
function
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
out.push(keep ? { kind: "applied" } : {
|
|
233
|
-
kind: "denied",
|
|
234
|
-
reason: deniedReason
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
return out;
|
|
238
|
-
}
|
|
239
|
-
function resolveApprovalForPayload(decision, payload) {
|
|
240
|
-
const total = payload.hunks.length;
|
|
241
|
-
if (decision === "deny") {
|
|
242
|
-
const outcomes = Array.from({ length: total }, () => ({
|
|
243
|
-
kind: "denied",
|
|
244
|
-
reason: "denied by user"
|
|
245
|
-
}));
|
|
246
|
-
return {
|
|
247
|
-
outcomes,
|
|
248
|
-
shouldBlock: true,
|
|
249
|
-
syntheticEvent: {
|
|
250
|
-
...payload,
|
|
251
|
-
outcomes
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
if (typeof decision === "object" && decision.kind === "partial") {
|
|
256
|
-
const outcomes = maskToOutcomeKinds(decision.mask, total);
|
|
257
|
-
return {
|
|
258
|
-
outcomes,
|
|
259
|
-
shouldBlock: !outcomes.some((o) => o.kind === "applied"),
|
|
260
|
-
syntheticEvent: {
|
|
261
|
-
...payload,
|
|
262
|
-
outcomes
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
return {
|
|
267
|
-
outcomes: Array.from({ length: total }, () => ({ kind: "applied" })),
|
|
268
|
-
shouldBlock: false,
|
|
269
|
-
syntheticEvent: null
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
/** Sentinel tags used by {@link buildEditOutcomesAnnotation} / parser. */
|
|
273
|
-
const ANNOTATION_OPEN = "<edit-outcomes>";
|
|
274
|
-
const ANNOTATION_CLOSE = "</edit-outcomes>";
|
|
275
|
-
const OUTCOME_KIND_RE = /^applied|denied|skipped|failed$/;
|
|
276
|
-
const OUTCOME_LINE_RE = /^#(\d+) (applied|denied|skipped|failed)(?:: ?(.*))?$/;
|
|
277
|
-
/**
|
|
278
|
-
* Render an `EditOutcome[]` as the wire-format annotation block. Returns
|
|
279
|
-
* the body to APPEND to a tool result; callers join with a leading
|
|
280
|
-
* `\n\n` separator. Idempotent on missing reasons — bare `applied` lines
|
|
281
|
-
* stay terse.
|
|
282
|
-
*/
|
|
283
|
-
function buildEditOutcomesAnnotation(outcomes) {
|
|
284
|
-
const lines = [ANNOTATION_OPEN];
|
|
285
|
-
for (let i = 0; i < outcomes.length; i++) {
|
|
286
|
-
const o = outcomes[i];
|
|
287
|
-
if (!OUTCOME_KIND_RE.test(o.kind)) continue;
|
|
288
|
-
const reason = o.reason ? `: ${o.reason}` : "";
|
|
289
|
-
lines.push(`#${i + 1} ${o.kind}${reason}`);
|
|
290
|
-
}
|
|
291
|
-
lines.push(ANNOTATION_CLOSE);
|
|
292
|
-
return lines.join("\n");
|
|
45
|
+
function selectionAfterTurnRemoval(before, removedId, after) {
|
|
46
|
+
if (after.includes(removedId)) return removedId;
|
|
47
|
+
if (after.length === 0) return null;
|
|
48
|
+
const index = before.indexOf(removedId);
|
|
49
|
+
return after[Math.min(Math.max(0, index), after.length - 1)] ?? null;
|
|
293
50
|
}
|
|
294
51
|
/**
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
* `null` when the block is missing / malformed.
|
|
298
|
-
*
|
|
299
|
-
* Anchored on the explicit tag pair so the parser doesn't false-positive
|
|
300
|
-
* on natural prose that happens to contain `#1 applied`.
|
|
301
|
-
*/
|
|
302
|
-
function parseEditOutcomesFromResult(result) {
|
|
303
|
-
const text = typeof result === "string" ? result : result.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
304
|
-
if (!text) return null;
|
|
305
|
-
const openIdx = text.indexOf(`\n${ANNOTATION_OPEN}\n`);
|
|
306
|
-
const startIdx = openIdx >= 0 ? openIdx + 1 : text.startsWith(`${ANNOTATION_OPEN}\n`) ? 0 : -1;
|
|
307
|
-
if (startIdx < 0) return null;
|
|
308
|
-
const closeNeedle = `\n${ANNOTATION_CLOSE}`;
|
|
309
|
-
const closeIdx = text.indexOf(closeNeedle, startIdx);
|
|
310
|
-
if (closeIdx < 0) return null;
|
|
311
|
-
const body = text.slice(startIdx + 15 + 1, closeIdx);
|
|
312
|
-
const found = [];
|
|
313
|
-
for (const line of body.split("\n")) {
|
|
314
|
-
if (line.length === 0) continue;
|
|
315
|
-
const m = OUTCOME_LINE_RE.exec(line);
|
|
316
|
-
if (!m) return null;
|
|
317
|
-
const idx = Number.parseInt(m[1], 10);
|
|
318
|
-
if (!Number.isFinite(idx) || idx < 1) return null;
|
|
319
|
-
const kind = m[2];
|
|
320
|
-
const reason = m[3]?.trim();
|
|
321
|
-
found.push({
|
|
322
|
-
idx,
|
|
323
|
-
outcome: {
|
|
324
|
-
kind,
|
|
325
|
-
...reason ? { reason } : {}
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
if (found.length === 0) return null;
|
|
330
|
-
const maxIdx = Math.max(...found.map((f) => f.idx));
|
|
331
|
-
const outcomes = Array.from({ length: maxIdx }, () => ({ kind: "applied" }));
|
|
332
|
-
for (const { idx, outcome } of found) outcomes[idx - 1] = outcome;
|
|
333
|
-
return outcomes;
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Strip the first `<edit-outcomes>…</edit-outcomes>` block out of a tool
|
|
337
|
-
* result body, returning the surrounding text. Used by the
|
|
338
|
-
* `tool:transform` hook to peel a body-emitted annotation before
|
|
339
|
-
* re-appending the merged (approval ∪ body) version — otherwise the
|
|
340
|
-
* result would carry two annotation blocks and
|
|
341
|
-
* {@link parseEditOutcomesFromResult} would only see the first.
|
|
52
|
+
* Recognize a tool-result body as carrying NON-success information so the
|
|
53
|
+
* renderer doesn't suppress it under `showEditDiffs`. Three categories:
|
|
342
54
|
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
let openIdx;
|
|
354
|
-
if (newlineIdx >= 0) openIdx = newlineIdx + 1;
|
|
355
|
-
else if (text.startsWith(`${ANNOTATION_OPEN}\n`)) openIdx = 0;
|
|
356
|
-
else return text;
|
|
357
|
-
const closeIdx = text.indexOf(ANNOTATION_CLOSE, openIdx);
|
|
358
|
-
if (closeIdx < 0) return text;
|
|
359
|
-
const blockEnd = closeIdx + 16;
|
|
360
|
-
const sepStart = openIdx >= 2 && text.slice(openIdx - 2, openIdx) === "\n\n" ? openIdx - 2 : openIdx;
|
|
361
|
-
return text.slice(0, sepStart) + text.slice(blockEnd);
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Merge body-side outcomes (keyed against the approved subset the tool
|
|
365
|
-
* actually ran on, in subset-position order) into approval-side outcomes
|
|
366
|
-
* (1:1 with the model's ORIGINAL `edits` list, with `denied` entries for
|
|
367
|
-
* every hunk the user dropped).
|
|
368
|
-
*
|
|
369
|
-
* Algorithm: walk the approval array; every `applied` placeholder
|
|
370
|
-
* corresponds to one approved hunk that the body ran. Consume body's
|
|
371
|
-
* outcomes in order against those placeholders. Non-`applied` approval
|
|
372
|
-
* entries (`denied`, `skipped`) stay untouched — they describe gate-
|
|
373
|
-
* level decisions the body never saw.
|
|
374
|
-
*
|
|
375
|
-
* Pure. Returns a fresh array; never mutates either input.
|
|
376
|
-
*
|
|
377
|
-
* Edge cases:
|
|
378
|
-
* - `body` is empty / shorter than the approved count → remaining
|
|
379
|
-
* approval `applied` placeholders stay as `applied` (the body ran
|
|
380
|
-
* happily; absence of a body entry means nothing failed).
|
|
381
|
-
* - `body` longer than approved count → trailing body entries are
|
|
382
|
-
* ignored. Shouldn't happen in practice (body sees the rebound
|
|
383
|
-
* subset), but the guard keeps the merge total-pure.
|
|
384
|
-
*/
|
|
385
|
-
function mergeApprovalAndBodyOutcomes(approval, body) {
|
|
386
|
-
if (!body || body.length === 0) return approval.slice();
|
|
387
|
-
const out = [];
|
|
388
|
-
let bi = 0;
|
|
389
|
-
for (const entry of approval) if (entry.kind === "applied" && bi < body.length) {
|
|
390
|
-
out.push(body[bi]);
|
|
391
|
-
bi++;
|
|
392
|
-
} else out.push(entry);
|
|
393
|
-
return out;
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* Rewrite a `multi_edit` body header so the totals reflect the model's
|
|
397
|
-
* ORIGINAL edit list (the merged outcomes count) instead of the subset
|
|
398
|
-
* the body actually saw after gate rebinding. Without this, a partially
|
|
399
|
-
* approved call surfaces a misleading `applied 2 of 2 edits` (subset
|
|
400
|
-
* counts) on the wire even when the original was `applied 2 of 3`.
|
|
401
|
-
*
|
|
402
|
-
* Three body-side shapes are handled (matching `multi_edit`'s emit):
|
|
403
|
-
* 1. `Edited <path>: applied N edits (R replacements).`
|
|
404
|
-
* 2. `Edited <path>: applied N of M edits (R replacements).`
|
|
405
|
-
* 3. `multi_edit error: no edits applied to <path> (M attempted).`
|
|
406
|
-
*
|
|
407
|
-
* The replacements count is preserved verbatim — it's a body-side stat
|
|
408
|
-
* the chat layer can't recompute. When the first line doesn't look like
|
|
409
|
-
* any of the three shapes (e.g. an unrelated error preamble bubbled up),
|
|
410
|
-
* the text is returned unchanged.
|
|
411
|
-
*/
|
|
412
|
-
function rewriteMultiEditHeader(text, merged, path) {
|
|
413
|
-
const newlineIdx = text.indexOf("\n");
|
|
414
|
-
const firstLine = newlineIdx < 0 ? text : text.slice(0, newlineIdx);
|
|
415
|
-
const rest = newlineIdx < 0 ? "" : text.slice(newlineIdx);
|
|
416
|
-
const successMatch = firstLine.match(/^Edited .+: applied \d+(?: of \d+)? edits? \((\d+) replacement/);
|
|
417
|
-
const isFailedShape = firstLine.startsWith("multi_edit error: no edits applied to ") && firstLine.endsWith(" attempted).");
|
|
418
|
-
if (!successMatch && !isFailedShape) return text;
|
|
419
|
-
const replacements = successMatch ? Number.parseInt(successMatch[1], 10) || 0 : 0;
|
|
420
|
-
const applied = summarizeOutcomes(merged).applied;
|
|
421
|
-
const total = merged.length;
|
|
422
|
-
let newHeader;
|
|
423
|
-
if (applied === total) newHeader = `Edited ${path}: applied ${total} edit${total === 1 ? "" : "s"} (${replacements} replacement${replacements === 1 ? "" : "s"}).`;
|
|
424
|
-
else if (applied > 0) newHeader = `Edited ${path}: applied ${applied} of ${total} edits (${replacements} replacement${replacements === 1 ? "" : "s"}).`;
|
|
425
|
-
else newHeader = `multi_edit error: no edits applied to ${path} (${total} attempted).`;
|
|
426
|
-
return newHeader + rest;
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Aggregate counts for the transcript's summary badge (`3 applied · 1
|
|
430
|
-
* denied · 1 skipped`). Exported so renderers don't reimplement the
|
|
431
|
-
* tally. Pure / O(n).
|
|
432
|
-
*/
|
|
433
|
-
function summarizeOutcomes(outcomes) {
|
|
434
|
-
const counts = {
|
|
435
|
-
applied: 0,
|
|
436
|
-
denied: 0,
|
|
437
|
-
skipped: 0,
|
|
438
|
-
failed: 0,
|
|
439
|
-
pending: 0
|
|
440
|
-
};
|
|
441
|
-
if (!outcomes) return {
|
|
442
|
-
...counts,
|
|
443
|
-
total: 0
|
|
444
|
-
};
|
|
445
|
-
for (const o of outcomes) counts[o.kind] += 1;
|
|
446
|
-
return {
|
|
447
|
-
...counts,
|
|
448
|
-
total: outcomes.length
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
//#endregion
|
|
452
|
-
//#region src/chat/edit-diff.ts
|
|
453
|
-
function extractEditPayload(name, input, priorContent) {
|
|
454
|
-
const path = input.path;
|
|
455
|
-
if (typeof path !== "string" || path === "") return void 0;
|
|
456
|
-
if (name === "edit") {
|
|
457
|
-
const oldString = input.old_string;
|
|
458
|
-
const newString = input.new_string;
|
|
459
|
-
if (typeof oldString !== "string" || typeof newString !== "string") return void 0;
|
|
460
|
-
return {
|
|
461
|
-
tool: "edit",
|
|
462
|
-
path,
|
|
463
|
-
hunks: [{
|
|
464
|
-
oldString,
|
|
465
|
-
newString,
|
|
466
|
-
...input.replace_all === true ? { replaceAll: true } : {}
|
|
467
|
-
}],
|
|
468
|
-
...priorContent !== void 0 ? { priorContent } : {}
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
if (name === "multi_edit") {
|
|
472
|
-
const steps = input.edits;
|
|
473
|
-
if (!Array.isArray(steps) || steps.length === 0) return void 0;
|
|
474
|
-
const hunks = [];
|
|
475
|
-
for (const raw of steps) {
|
|
476
|
-
if (typeof raw?.old_string !== "string" || typeof raw?.new_string !== "string") return void 0;
|
|
477
|
-
hunks.push({
|
|
478
|
-
oldString: raw.old_string,
|
|
479
|
-
newString: raw.new_string,
|
|
480
|
-
...raw.replace_all === true ? { replaceAll: true } : {}
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
return {
|
|
484
|
-
tool: "multi_edit",
|
|
485
|
-
path,
|
|
486
|
-
hunks,
|
|
487
|
-
...priorContent !== void 0 ? { priorContent } : {}
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
if (name === "write_file") {
|
|
491
|
-
const content = input.content;
|
|
492
|
-
if (typeof content !== "string") return void 0;
|
|
493
|
-
return {
|
|
494
|
-
tool: "write_file",
|
|
495
|
-
path,
|
|
496
|
-
hunks: [{
|
|
497
|
-
oldString: priorContent ?? "",
|
|
498
|
-
newString: content
|
|
499
|
-
}],
|
|
500
|
-
...priorContent !== void 0 ? { priorContent } : {}
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* LCS table cell budget shared by the line- and token-level diffs. 4M
|
|
506
|
-
* cells ≈ low tens of MB of `number[]` rows — imperceptible — while
|
|
507
|
-
* anything needing more (multi-thousand-line change cores) wouldn't
|
|
508
|
-
* produce a human-readable minimal diff anyway.
|
|
509
|
-
*/
|
|
510
|
-
const LCS_CELL_BUDGET = 4e6;
|
|
511
|
-
function computeLineDiff(oldString, newString) {
|
|
512
|
-
const oldLines = splitLines(oldString);
|
|
513
|
-
const newLines = splitLines(newString);
|
|
514
|
-
let lo = 0;
|
|
515
|
-
while (lo < oldLines.length && lo < newLines.length && oldLines[lo] === newLines[lo]) lo++;
|
|
516
|
-
let oldHi = oldLines.length;
|
|
517
|
-
let newHi = newLines.length;
|
|
518
|
-
while (oldHi > lo && newHi > lo && oldLines[oldHi - 1] === newLines[newHi - 1]) {
|
|
519
|
-
oldHi--;
|
|
520
|
-
newHi--;
|
|
521
|
-
}
|
|
522
|
-
const out = [];
|
|
523
|
-
for (let k = 0; k < lo; k++) out.push({
|
|
524
|
-
op: "context",
|
|
525
|
-
text: oldLines[k]
|
|
526
|
-
});
|
|
527
|
-
const n = oldHi - lo;
|
|
528
|
-
const m = newHi - lo;
|
|
529
|
-
if ((n + 1) * (m + 1) > LCS_CELL_BUDGET) {
|
|
530
|
-
for (let k = lo; k < oldHi; k++) out.push({
|
|
531
|
-
op: "remove",
|
|
532
|
-
text: oldLines[k]
|
|
533
|
-
});
|
|
534
|
-
for (let k = lo; k < newHi; k++) out.push({
|
|
535
|
-
op: "add",
|
|
536
|
-
text: newLines[k]
|
|
537
|
-
});
|
|
538
|
-
} else if (n > 0 || m > 0) appendLcsDiff(out, oldLines, newLines, lo, oldHi, newHi);
|
|
539
|
-
for (let k = oldHi; k < oldLines.length; k++) out.push({
|
|
540
|
-
op: "context",
|
|
541
|
-
text: oldLines[k]
|
|
542
|
-
});
|
|
543
|
-
return out;
|
|
544
|
-
}
|
|
545
|
-
/**
|
|
546
|
-
* The classic LCS table + reverse walk over `old[lo..oldHi) ×
|
|
547
|
-
* new[lo..newHi)`, appending ops to `out` in forward order. Diagonals at
|
|
548
|
-
* equal lines → context; otherwise step into the higher of (up, left)
|
|
549
|
-
* and emit remove/add respectively. Ties break so adjacent remove/add
|
|
550
|
-
* pairs stay grouped (the intra-line highlighter pairs them up).
|
|
551
|
-
*/
|
|
552
|
-
function appendLcsDiff(out, oldLines, newLines, lo, oldHi, newHi) {
|
|
553
|
-
const n = oldHi - lo;
|
|
554
|
-
const m = newHi - lo;
|
|
555
|
-
const lcs = Array.from({ length: n + 1 }, () => Array.from({ length: m + 1 }).fill(0));
|
|
556
|
-
for (let i = 0; i < n; i++) for (let j = 0; j < m; j++) lcs[i + 1][j + 1] = oldLines[lo + i] === newLines[lo + j] ? lcs[i][j] + 1 : Math.max(lcs[i][j + 1], lcs[i + 1][j]);
|
|
557
|
-
const core = [];
|
|
558
|
-
let i = n;
|
|
559
|
-
let j = m;
|
|
560
|
-
while (i > 0 || j > 0) {
|
|
561
|
-
if (i > 0 && j > 0 && oldLines[lo + i - 1] === newLines[lo + j - 1]) {
|
|
562
|
-
core.push({
|
|
563
|
-
op: "context",
|
|
564
|
-
text: oldLines[lo + i - 1]
|
|
565
|
-
});
|
|
566
|
-
i--;
|
|
567
|
-
j--;
|
|
568
|
-
continue;
|
|
569
|
-
}
|
|
570
|
-
if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
|
|
571
|
-
core.push({
|
|
572
|
-
op: "add",
|
|
573
|
-
text: newLines[lo + j - 1]
|
|
574
|
-
});
|
|
575
|
-
j--;
|
|
576
|
-
continue;
|
|
577
|
-
}
|
|
578
|
-
core.push({
|
|
579
|
-
op: "remove",
|
|
580
|
-
text: oldLines[lo + i - 1]
|
|
581
|
-
});
|
|
582
|
-
i--;
|
|
583
|
-
}
|
|
584
|
-
core.reverse();
|
|
585
|
-
out.push(...core);
|
|
586
|
-
}
|
|
587
|
-
/**
|
|
588
|
-
* Split a string into lines preserving empty lines but dropping the
|
|
589
|
-
* implicit trailing `""` produced by a final `\n`. Exported only for
|
|
590
|
-
* its tests — callers should use `computeLineDiff`.
|
|
591
|
-
*/
|
|
592
|
-
function splitLines(s) {
|
|
593
|
-
if (s === "") return [];
|
|
594
|
-
const parts = s.split("\n");
|
|
595
|
-
if (parts[parts.length - 1] === "") parts.pop();
|
|
596
|
-
return parts;
|
|
597
|
-
}
|
|
598
|
-
function computeInlineDiff(oldLine, newLine) {
|
|
599
|
-
const oldTokens = tokenize(oldLine);
|
|
600
|
-
const newTokens = tokenize(newLine);
|
|
601
|
-
const n = oldTokens.length;
|
|
602
|
-
const m = newTokens.length;
|
|
603
|
-
if ((n + 1) * (m + 1) > LCS_CELL_BUDGET) return {
|
|
604
|
-
oldSegments: oldLine === "" ? [] : [{
|
|
605
|
-
text: oldLine,
|
|
606
|
-
changed: true
|
|
607
|
-
}],
|
|
608
|
-
newSegments: newLine === "" ? [] : [{
|
|
609
|
-
text: newLine,
|
|
610
|
-
changed: true
|
|
611
|
-
}]
|
|
612
|
-
};
|
|
613
|
-
const lcs = Array.from({ length: n + 1 }, () => Array.from({ length: m + 1 }).fill(0));
|
|
614
|
-
for (let i = 0; i < n; i++) for (let j = 0; j < m; j++) lcs[i + 1][j + 1] = oldTokens[i] === newTokens[j] ? lcs[i][j] + 1 : Math.max(lcs[i][j + 1], lcs[i + 1][j]);
|
|
615
|
-
const oldSegments = [];
|
|
616
|
-
const newSegments = [];
|
|
617
|
-
let i = n;
|
|
618
|
-
let j = m;
|
|
619
|
-
while (i > 0 || j > 0) {
|
|
620
|
-
if (i > 0 && j > 0 && oldTokens[i - 1] === newTokens[j - 1]) {
|
|
621
|
-
pushSegment(oldSegments, {
|
|
622
|
-
text: oldTokens[i - 1],
|
|
623
|
-
changed: false
|
|
624
|
-
});
|
|
625
|
-
pushSegment(newSegments, {
|
|
626
|
-
text: newTokens[j - 1],
|
|
627
|
-
changed: false
|
|
628
|
-
});
|
|
629
|
-
i--;
|
|
630
|
-
j--;
|
|
631
|
-
continue;
|
|
632
|
-
}
|
|
633
|
-
if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
|
|
634
|
-
pushSegment(newSegments, {
|
|
635
|
-
text: newTokens[j - 1],
|
|
636
|
-
changed: true
|
|
637
|
-
});
|
|
638
|
-
j--;
|
|
639
|
-
continue;
|
|
640
|
-
}
|
|
641
|
-
pushSegment(oldSegments, {
|
|
642
|
-
text: oldTokens[i - 1],
|
|
643
|
-
changed: true
|
|
644
|
-
});
|
|
645
|
-
i--;
|
|
646
|
-
}
|
|
647
|
-
oldSegments.reverse();
|
|
648
|
-
newSegments.reverse();
|
|
649
|
-
return {
|
|
650
|
-
oldSegments,
|
|
651
|
-
newSegments
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
/**
|
|
655
|
-
* Coalesce adjacent same-state segments so the renderer emits one
|
|
656
|
-
* `<span>` per run instead of one per token — keeps the React tree
|
|
657
|
-
* shallow on dense lines without changing the visual output.
|
|
658
|
-
*
|
|
659
|
-
* Walking direction is reverse (we push during the reverse walk, then
|
|
660
|
-
* the caller reverses the array), so we coalesce against the *tail*.
|
|
661
|
-
*/
|
|
662
|
-
function pushSegment(buf, seg) {
|
|
663
|
-
const tail = buf[buf.length - 1];
|
|
664
|
-
if (tail && tail.changed === seg.changed) tail.text = seg.text + tail.text;
|
|
665
|
-
else buf.push(seg);
|
|
666
|
-
}
|
|
667
|
-
/**
|
|
668
|
-
* Tokenize on word / non-word boundaries. Each run of `\w+` is one
|
|
669
|
-
* token; each run of `\W+` (whitespace, punctuation) is another. This
|
|
670
|
-
* gives the right granularity for renames (`oldName` → `newName`) and
|
|
671
|
-
* for symbol swaps (`+ → -`) without exploding into per-char segments.
|
|
672
|
-
*
|
|
673
|
-
* Exported only for its tests.
|
|
674
|
-
*/
|
|
675
|
-
function tokenize(s) {
|
|
676
|
-
if (s === "") return [];
|
|
677
|
-
const out = [];
|
|
678
|
-
for (const match of s.matchAll(/\w+|\W+/g)) out.push(match[0]);
|
|
679
|
-
return out;
|
|
680
|
-
}
|
|
681
|
-
/**
|
|
682
|
-
* Apply the payload's hunks against `priorContent` and return the
|
|
683
|
-
* resulting file body. Mirrors the agent's tool-side semantics:
|
|
684
|
-
* - `replaceAll === true` → `String.replaceAll`
|
|
685
|
-
* - otherwise → first-occurrence `String.replace`
|
|
686
|
-
*
|
|
687
|
-
* Hunks are applied in order — a `multi_edit` later hunk operates on
|
|
688
|
-
* the output of the earlier ones, just like the actual tool.
|
|
689
|
-
*/
|
|
690
|
-
function applyEditPayload(payload, priorContent) {
|
|
691
|
-
let out = priorContent;
|
|
692
|
-
for (const hunk of payload.hunks) out = hunk.replaceAll ? out.replaceAll(hunk.oldString, hunk.newString) : out.replace(hunk.oldString, hunk.newString);
|
|
693
|
-
return out;
|
|
694
|
-
}
|
|
695
|
-
/**
|
|
696
|
-
* Like `buildUnifiedDiff` but operating against the full file content
|
|
697
|
-
* so the diff carries *real* file line numbers and configurable
|
|
698
|
-
* surrounding context.
|
|
699
|
-
*
|
|
700
|
-
* Strategy:
|
|
701
|
-
* 1. Apply the payload to `priorContent` → `newContent`.
|
|
702
|
-
* 2. Run `computeLineDiff` over the whole file.
|
|
703
|
-
* 3. Group non-context ops into hunks, padding each with up to
|
|
704
|
-
* `contextLines` of context above and below. Adjacent hunks
|
|
705
|
-
* whose context regions touch are merged so we don't emit two
|
|
706
|
-
* `@@` headers separated by zero context lines.
|
|
707
|
-
*
|
|
708
|
-
* The output line numbers in the `@@` header are 1-based and reflect
|
|
709
|
-
* the change's position in the actual file — what the user expects
|
|
710
|
-
* when reading a diff alongside their editor.
|
|
711
|
-
*
|
|
712
|
-
* For `write_file` creating a new file (priorContent === ''), this
|
|
713
|
-
* falls back to the same `--- /dev/null` convention as
|
|
714
|
-
* `buildUnifiedDiff`.
|
|
715
|
-
*/
|
|
716
|
-
function buildContextualDiff(payload, priorContent, contextLines = 3) {
|
|
717
|
-
const newContent = applyEditPayload(payload, priorContent);
|
|
718
|
-
const isNewFile = priorContent === "";
|
|
719
|
-
const ops = computeLineDiff(priorContent, newContent);
|
|
720
|
-
const oldLineFor = [];
|
|
721
|
-
const newLineFor = [];
|
|
722
|
-
let ol = 1;
|
|
723
|
-
let nl = 1;
|
|
724
|
-
for (const op of ops) {
|
|
725
|
-
oldLineFor.push(ol);
|
|
726
|
-
newLineFor.push(nl);
|
|
727
|
-
if (op.op !== "add") ol++;
|
|
728
|
-
if (op.op !== "remove") nl++;
|
|
729
|
-
}
|
|
730
|
-
const hunks = [];
|
|
731
|
-
for (let i = 0; i < ops.length; i++) {
|
|
732
|
-
if (ops[i].op === "context") continue;
|
|
733
|
-
const start = Math.max(0, i - contextLines);
|
|
734
|
-
const end = Math.min(ops.length - 1, i + contextLines);
|
|
735
|
-
const last = hunks[hunks.length - 1];
|
|
736
|
-
if (last && start <= last[1] + 1) last[1] = Math.max(last[1], end);
|
|
737
|
-
else hunks.push([start, end]);
|
|
738
|
-
}
|
|
739
|
-
if (hunks.length === 0) return "";
|
|
740
|
-
const parts = [];
|
|
741
|
-
parts.push(isNewFile ? "--- /dev/null" : `--- a/${payload.path}`);
|
|
742
|
-
parts.push(`+++ b/${payload.path}`);
|
|
743
|
-
for (const [start, end] of hunks) {
|
|
744
|
-
const slice = ops.slice(start, end + 1);
|
|
745
|
-
const oldCount = slice.filter((l) => l.op !== "add").length;
|
|
746
|
-
const newCount = slice.filter((l) => l.op !== "remove").length;
|
|
747
|
-
const oldStart = oldCount === 0 ? Math.max(0, oldLineFor[start] - 1) : oldLineFor[start];
|
|
748
|
-
const newStart = newCount === 0 ? Math.max(0, newLineFor[start] - 1) : newLineFor[start];
|
|
749
|
-
parts.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`);
|
|
750
|
-
for (const line of slice) {
|
|
751
|
-
const prefix = line.op === "add" ? "+" : line.op === "remove" ? "-" : " ";
|
|
752
|
-
parts.push(`${prefix}${line.text}`);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
return `${parts.join("\n")}\n`;
|
|
756
|
-
}
|
|
757
|
-
/**
|
|
758
|
-
* Build a per-hunk digest used by the compact diff view.
|
|
759
|
-
*
|
|
760
|
-
* Strategy:
|
|
761
|
-
* - When `priorContent` is present and the payload describes a real
|
|
762
|
-
* file transformation, compute the contextual diff once, then walk
|
|
763
|
-
* the LCS ops splitting at runs of `add` / `remove` to anchor each
|
|
764
|
-
* summary entry to the **real** file line. This guarantees the
|
|
765
|
-
* summary's `L<n>` matches what the user sees in their editor.
|
|
766
|
-
* - Otherwise, fall back to per-hunk LCS over the (oldString,
|
|
767
|
-
* newString) snippet pair. Line numbers are absent because the
|
|
768
|
-
* snippet has no file position.
|
|
769
|
-
*/
|
|
770
|
-
function summarizeEditPayload(payload) {
|
|
771
|
-
const prior = payload.priorContent;
|
|
772
|
-
if (prior !== void 0) return summarizeOpsByHunk(computeLineDiff(prior, applyEditPayload(payload, prior)));
|
|
773
|
-
const hunks = [];
|
|
774
|
-
let totalAdded = 0;
|
|
775
|
-
let totalRemoved = 0;
|
|
776
|
-
for (const hunk of payload.hunks) {
|
|
777
|
-
const ops = computeLineDiff(hunk.oldString, hunk.newString);
|
|
778
|
-
let added = 0;
|
|
779
|
-
let removed = 0;
|
|
780
|
-
let firstOld;
|
|
781
|
-
let firstNew;
|
|
782
|
-
for (const op of ops) if (op.op === "add") {
|
|
783
|
-
added++;
|
|
784
|
-
if (firstNew === void 0) firstNew = op.text;
|
|
785
|
-
} else if (op.op === "remove") {
|
|
786
|
-
removed++;
|
|
787
|
-
if (firstOld === void 0) firstOld = op.text;
|
|
788
|
-
}
|
|
789
|
-
totalAdded += added;
|
|
790
|
-
totalRemoved += removed;
|
|
791
|
-
hunks.push({
|
|
792
|
-
added,
|
|
793
|
-
removed,
|
|
794
|
-
...firstOld !== void 0 ? { firstOld } : {},
|
|
795
|
-
...firstNew !== void 0 ? { firstNew } : {}
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
return {
|
|
799
|
-
totalAdded,
|
|
800
|
-
totalRemoved,
|
|
801
|
-
hunks
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
/**
|
|
805
|
-
* Walk an LCS op stream and emit one summary entry per *run* of
|
|
806
|
-
* non-context ops, with the new-file line number where each run
|
|
807
|
-
* starts. Adjacent add/remove ops collapse into the same entry —
|
|
808
|
-
* matches git's hunk grouping at zero context.
|
|
809
|
-
*/
|
|
810
|
-
function summarizeOpsByHunk(ops) {
|
|
811
|
-
const hunks = [];
|
|
812
|
-
let totalAdded = 0;
|
|
813
|
-
let totalRemoved = 0;
|
|
814
|
-
let nl = 1;
|
|
815
|
-
let i = 0;
|
|
816
|
-
while (i < ops.length) {
|
|
817
|
-
if (ops[i].op === "context") {
|
|
818
|
-
nl++;
|
|
819
|
-
i++;
|
|
820
|
-
continue;
|
|
821
|
-
}
|
|
822
|
-
const runStartLine = nl;
|
|
823
|
-
let added = 0;
|
|
824
|
-
let removed = 0;
|
|
825
|
-
let firstOld;
|
|
826
|
-
let firstNew;
|
|
827
|
-
while (i < ops.length && ops[i].op !== "context") {
|
|
828
|
-
const cur = ops[i];
|
|
829
|
-
if (cur.op === "add") {
|
|
830
|
-
added++;
|
|
831
|
-
if (firstNew === void 0) firstNew = cur.text;
|
|
832
|
-
nl++;
|
|
833
|
-
} else {
|
|
834
|
-
removed++;
|
|
835
|
-
if (firstOld === void 0) firstOld = cur.text;
|
|
836
|
-
}
|
|
837
|
-
i++;
|
|
838
|
-
}
|
|
839
|
-
totalAdded += added;
|
|
840
|
-
totalRemoved += removed;
|
|
841
|
-
hunks.push({
|
|
842
|
-
line: runStartLine,
|
|
843
|
-
added,
|
|
844
|
-
removed,
|
|
845
|
-
...firstOld !== void 0 ? { firstOld } : {},
|
|
846
|
-
...firstNew !== void 0 ? { firstNew } : {}
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
return {
|
|
850
|
-
totalAdded,
|
|
851
|
-
totalRemoved,
|
|
852
|
-
hunks
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
function previewEditPayload(payload, priorContent, contextLines = 3) {
|
|
856
|
-
const resolution = [];
|
|
857
|
-
const resolvedHunks = [];
|
|
858
|
-
const perHunkDiff = [];
|
|
859
|
-
let running = priorContent;
|
|
860
|
-
for (const hunk of payload.hunks) {
|
|
861
|
-
if (hunk.oldString === "" || hunk.oldString === running) {
|
|
862
|
-
resolution.push({
|
|
863
|
-
resolved: true,
|
|
864
|
-
via: "exact",
|
|
865
|
-
occurrences: 1
|
|
866
|
-
});
|
|
867
|
-
resolvedHunks.push(hunk);
|
|
868
|
-
perHunkDiff.push(buildContextualDiff({
|
|
869
|
-
...payload,
|
|
870
|
-
hunks: [hunk]
|
|
871
|
-
}, running, contextLines));
|
|
872
|
-
running = hunk.newString;
|
|
873
|
-
continue;
|
|
874
|
-
}
|
|
875
|
-
const match = resolveOldString(running, hunk.oldString);
|
|
876
|
-
if (!match) {
|
|
877
|
-
resolution.push({ resolved: false });
|
|
878
|
-
resolvedHunks.push(hunk);
|
|
879
|
-
perHunkDiff.push("");
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
|
-
const ambiguous = match.occurrences > 1 && !hunk.replaceAll;
|
|
883
|
-
const styledNew = styleReplacementForVia(hunk.newString, match.via, match.actual);
|
|
884
|
-
const resolvedHunk = {
|
|
885
|
-
oldString: match.actual,
|
|
886
|
-
newString: styledNew,
|
|
887
|
-
...hunk.replaceAll ? { replaceAll: true } : {}
|
|
888
|
-
};
|
|
889
|
-
resolution.push({
|
|
890
|
-
resolved: !ambiguous,
|
|
891
|
-
via: match.via,
|
|
892
|
-
occurrences: match.occurrences,
|
|
893
|
-
...ambiguous ? { ambiguous: true } : {}
|
|
894
|
-
});
|
|
895
|
-
resolvedHunks.push(resolvedHunk);
|
|
896
|
-
perHunkDiff.push(ambiguous ? "" : buildContextualDiff({
|
|
897
|
-
...payload,
|
|
898
|
-
hunks: [resolvedHunk]
|
|
899
|
-
}, running, contextLines));
|
|
900
|
-
if (!ambiguous) running = hunk.replaceAll ? running.replaceAll(match.actual, styledNew) : running.replace(match.actual, styledNew);
|
|
901
|
-
}
|
|
902
|
-
const resolvedPayload = {
|
|
903
|
-
...payload,
|
|
904
|
-
hunks: resolvedHunks
|
|
905
|
-
};
|
|
906
|
-
const applicableHunks = resolvedHunks.filter((_, i) => resolution[i].resolved);
|
|
907
|
-
return {
|
|
908
|
-
diffText: applicableHunks.length === 0 ? "" : buildContextualDiff({
|
|
909
|
-
...payload,
|
|
910
|
-
hunks: applicableHunks
|
|
911
|
-
}, priorContent, contextLines),
|
|
912
|
-
resolution,
|
|
913
|
-
perHunkDiff,
|
|
914
|
-
resolvedPayload
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
/** Default size budget (UTF-16 code units) for transcript diff rows. */
|
|
918
|
-
const DIFF_RENDER_MAX_BYTES = 512 * 1024;
|
|
919
|
-
/** Marker appended to a line hard-truncated mid-content by the size cap. */
|
|
920
|
-
const CLIP_LINE_MARKER = " …⟨line clipped⟩";
|
|
921
|
-
/**
|
|
922
|
-
* Truncate `line` so it fits `budget` code units including the marker,
|
|
923
|
-
* without splitting a UTF-16 surrogate pair. Returns the line unchanged
|
|
924
|
-
* when it already fits.
|
|
925
|
-
*/
|
|
926
|
-
function clipLine(line, budget) {
|
|
927
|
-
if (line.length <= budget) return line;
|
|
928
|
-
let end = Math.max(0, budget - 16);
|
|
929
|
-
const code = line.charCodeAt(end - 1);
|
|
930
|
-
if (end > 0 && code >= 55296 && code <= 56319) end--;
|
|
931
|
-
return line.slice(0, end) + CLIP_LINE_MARKER;
|
|
932
|
-
}
|
|
933
|
-
function clipDiffForRender(diff, maxLines = 600, maxBytes = DIFF_RENDER_MAX_BYTES) {
|
|
934
|
-
const lines = diff.split("\n");
|
|
935
|
-
const totalLines = lines.length > 0 && lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
|
|
936
|
-
if (totalLines <= maxLines && diff.length <= maxBytes) return {
|
|
937
|
-
text: diff,
|
|
938
|
-
hiddenLines: 0,
|
|
939
|
-
totalLines
|
|
940
|
-
};
|
|
941
|
-
const kept = [];
|
|
942
|
-
let used = 0;
|
|
943
|
-
for (let i = 0; i < totalLines; i++) {
|
|
944
|
-
if (kept.length >= maxLines) break;
|
|
945
|
-
const line = lines[i];
|
|
946
|
-
const cost = line.length + 1;
|
|
947
|
-
if (used + cost > maxBytes) {
|
|
948
|
-
if (kept.length === 0) kept.push(clipLine(line, maxBytes));
|
|
949
|
-
break;
|
|
950
|
-
}
|
|
951
|
-
kept.push(line);
|
|
952
|
-
used += cost;
|
|
953
|
-
}
|
|
954
|
-
return {
|
|
955
|
-
text: kept.join("\n"),
|
|
956
|
-
hiddenLines: totalLines - kept.length,
|
|
957
|
-
totalLines
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
function buildUnifiedDiff(payload) {
|
|
961
|
-
const parts = [];
|
|
962
|
-
const isNewFile = payload.tool === "write_file" && payload.hunks[0]?.oldString === "";
|
|
963
|
-
parts.push(isNewFile ? `--- /dev/null` : `--- a/${payload.path}`);
|
|
964
|
-
parts.push(`+++ b/${payload.path}`);
|
|
965
|
-
for (const hunk of payload.hunks) {
|
|
966
|
-
const lines = computeLineDiff(hunk.oldString, hunk.newString);
|
|
967
|
-
const oldCount = lines.filter((l) => l.op !== "add").length;
|
|
968
|
-
const newCount = lines.filter((l) => l.op !== "remove").length;
|
|
969
|
-
const oldStart = oldCount === 0 ? 0 : 1;
|
|
970
|
-
const newStart = newCount === 0 ? 0 : 1;
|
|
971
|
-
parts.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`);
|
|
972
|
-
for (const line of lines) {
|
|
973
|
-
const prefix = line.op === "add" ? "+" : line.op === "remove" ? "-" : " ";
|
|
974
|
-
parts.push(`${prefix}${line.text}`);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
return `${parts.join("\n")}\n`;
|
|
978
|
-
}
|
|
979
|
-
const FILETYPE_BY_EXT = {
|
|
980
|
-
ts: "typescript",
|
|
981
|
-
mts: "typescript",
|
|
982
|
-
cts: "typescript",
|
|
983
|
-
tsx: "typescriptreact",
|
|
984
|
-
js: "javascript",
|
|
985
|
-
mjs: "javascript",
|
|
986
|
-
cjs: "javascript",
|
|
987
|
-
jsx: "javascriptreact",
|
|
988
|
-
py: "python",
|
|
989
|
-
pyi: "python",
|
|
990
|
-
rs: "rust",
|
|
991
|
-
go: "go",
|
|
992
|
-
json: "json",
|
|
993
|
-
jsonc: "json",
|
|
994
|
-
sh: "bash",
|
|
995
|
-
bash: "bash",
|
|
996
|
-
zsh: "bash",
|
|
997
|
-
yaml: "yaml",
|
|
998
|
-
yml: "yaml",
|
|
999
|
-
html: "html",
|
|
1000
|
-
htm: "html",
|
|
1001
|
-
css: "css",
|
|
1002
|
-
md: "markdown",
|
|
1003
|
-
markdown: "markdown",
|
|
1004
|
-
zig: "zig",
|
|
1005
|
-
lua: "lua",
|
|
1006
|
-
toml: "toml",
|
|
1007
|
-
sql: "sql"
|
|
1008
|
-
};
|
|
1009
|
-
function filetypeFromPath(path) {
|
|
1010
|
-
const cleaned = path.split(/[?#]/, 1)[0];
|
|
1011
|
-
const lastDot = cleaned.lastIndexOf(".");
|
|
1012
|
-
if (lastDot === -1 || lastDot === cleaned.length - 1) return void 0;
|
|
1013
|
-
const ext = cleaned.slice(lastDot + 1).toLowerCase();
|
|
1014
|
-
return FILETYPE_BY_EXT[ext];
|
|
1015
|
-
}
|
|
1016
|
-
/** `1.2k` / `48k` / `1.3M` — token counts are read at a glance, not counted. */
|
|
1017
|
-
function formatTokenCount(tokens) {
|
|
1018
|
-
if (tokens >= 1e6) return `${(tokens / 1e6).toFixed(1)}M`;
|
|
1019
|
-
if (tokens >= 1e3) return `${(tokens / 1e3).toFixed(tokens >= 1e4 ? 0 : 1)}k`;
|
|
1020
|
-
return String(tokens);
|
|
1021
|
-
}
|
|
1022
|
-
/** `2m18s` / `48s` / `<1s` / `1h05m`. Compact — this is a receipt, not a profiler. */
|
|
1023
|
-
function formatRunDuration(ms) {
|
|
1024
|
-
if (ms < 1e3) return "<1s";
|
|
1025
|
-
const s = Math.round(ms / 1e3);
|
|
1026
|
-
if (s < 60) return `${s}s`;
|
|
1027
|
-
const m = Math.floor(s / 60);
|
|
1028
|
-
const rest = s % 60;
|
|
1029
|
-
if (m < 60) return rest ? `${m}m${String(rest).padStart(2, "0")}s` : `${m}m`;
|
|
1030
|
-
return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
|
|
1031
|
-
}
|
|
1032
|
-
/** `$0.04`, or four decimals under a cent so a cheap run is not shown as `$0.00`. */
|
|
1033
|
-
function formatCostUsd(cost) {
|
|
1034
|
-
return `$${cost.toFixed(cost < .01 ? 4 : 2)}`;
|
|
1035
|
-
}
|
|
1036
|
-
/**
|
|
1037
|
-
* Build the receipt for a run record. `now` stands in for `endedAt` when the
|
|
1038
|
-
* record is not finalized yet (`run:end` is broadcast a beat before it is).
|
|
1039
|
-
*/
|
|
1040
|
-
function runReceiptFromRun(run, now = Date.now()) {
|
|
1041
|
-
const sumIn = (u) => u.input + (u.cacheRead ?? 0) + (u.cacheCreation ?? 0);
|
|
1042
|
-
const usage = run.totalUsage ? {
|
|
1043
|
-
input: sumIn(run.totalUsage),
|
|
1044
|
-
output: run.totalUsage.output
|
|
1045
|
-
} : run.turnUsage?.reduce((acc, u) => ({
|
|
1046
|
-
input: acc.input + sumIn(u),
|
|
1047
|
-
output: acc.output + u.output
|
|
1048
|
-
}), {
|
|
1049
|
-
input: 0,
|
|
1050
|
-
output: 0
|
|
1051
|
-
});
|
|
1052
|
-
const models = (run.turnUsage ?? []).map((u) => u.modelId).filter((m) => typeof m === "string" && m.length > 0);
|
|
1053
|
-
const modelId = models.at(-1);
|
|
1054
|
-
const otherModels = modelId ? new Set(models).size - 1 : 0;
|
|
1055
|
-
const durationMs = (run.endedAt ?? now) - run.startedAt;
|
|
1056
|
-
const cost = run.cost ?? run.turnUsage?.reduce((n, u) => n + (u.cost ?? 0), 0) ?? 0;
|
|
1057
|
-
const turns = run.turns ?? run.turnUsage?.length ?? 0;
|
|
1058
|
-
let genMs = 0;
|
|
1059
|
-
let genOut = 0;
|
|
1060
|
-
for (const u of run.turnUsage ?? []) {
|
|
1061
|
-
if (u.streamDurationMs === void 0 || u.output <= 0) continue;
|
|
1062
|
-
const ms = u.streamDurationMs - (u.timeToFirstTokenMs ?? 0);
|
|
1063
|
-
if (ms <= 0) continue;
|
|
1064
|
-
genMs += ms;
|
|
1065
|
-
genOut += u.output;
|
|
1066
|
-
}
|
|
1067
|
-
const tokensPerSecond = genMs > 0 && genOut > 0 ? genOut * 1e3 / genMs : void 0;
|
|
1068
|
-
return {
|
|
1069
|
-
...modelId ? { modelId } : {},
|
|
1070
|
-
...otherModels > 0 ? { otherModels } : {},
|
|
1071
|
-
...durationMs > 0 ? { durationMs } : {},
|
|
1072
|
-
...turns > 0 ? { turns } : {},
|
|
1073
|
-
...cost > 0 ? { costUsd: cost } : {},
|
|
1074
|
-
...usage && usage.input > 0 ? { tokensIn: usage.input } : {},
|
|
1075
|
-
...usage && usage.output > 0 ? { tokensOut: usage.output } : {},
|
|
1076
|
-
...tokensPerSecond !== void 0 ? { tokensPerSecond } : {}
|
|
1077
|
-
};
|
|
1078
|
-
}
|
|
1079
|
-
/** `42 tk/s`; one decimal under 10 so a slow model is not shown as `0 tk/s`. */
|
|
1080
|
-
function formatTokensPerSecond(tps) {
|
|
1081
|
-
return `${tps < 10 ? tps.toFixed(1) : Math.round(tps).toString()} tk/s`;
|
|
1082
|
-
}
|
|
1083
|
-
/**
|
|
1084
|
-
* The receipt's cells, in display order. Renderers join them with their own
|
|
1085
|
-
* separator, tint them by `kind`, and truncate to their own width; the CONTENT
|
|
1086
|
-
* is fixed here.
|
|
1087
|
-
*/
|
|
1088
|
-
function formatRunReceiptCells(r) {
|
|
1089
|
-
const cells = [];
|
|
1090
|
-
if (r.modelId) cells.push({
|
|
1091
|
-
kind: "model",
|
|
1092
|
-
text: r.otherModels ? `${r.modelId} +${r.otherModels}` : r.modelId
|
|
1093
|
-
});
|
|
1094
|
-
if (r.durationMs !== void 0) cells.push({
|
|
1095
|
-
kind: "duration",
|
|
1096
|
-
text: formatRunDuration(r.durationMs)
|
|
1097
|
-
});
|
|
1098
|
-
if (r.turns !== void 0) cells.push({
|
|
1099
|
-
kind: "turns",
|
|
1100
|
-
text: `↻${r.turns}`
|
|
1101
|
-
});
|
|
1102
|
-
if (r.costUsd !== void 0) cells.push({
|
|
1103
|
-
kind: "cost",
|
|
1104
|
-
text: formatCostUsd(r.costUsd)
|
|
1105
|
-
});
|
|
1106
|
-
if (r.tokensIn !== void 0 || r.tokensOut !== void 0) {
|
|
1107
|
-
const segments = [{
|
|
1108
|
-
kind: "tokens-in",
|
|
1109
|
-
text: `↑${formatTokenCount(r.tokensIn ?? 0)}`
|
|
1110
|
-
}, {
|
|
1111
|
-
kind: "tokens-out",
|
|
1112
|
-
text: `↓${formatTokenCount(r.tokensOut ?? 0)}`
|
|
1113
|
-
}];
|
|
1114
|
-
cells.push({
|
|
1115
|
-
kind: "tokens",
|
|
1116
|
-
text: segments.map((seg) => seg.text).join(" "),
|
|
1117
|
-
segments
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
if (r.tokensPerSecond !== void 0) cells.push({
|
|
1121
|
-
kind: "speed",
|
|
1122
|
-
text: formatTokensPerSecond(r.tokensPerSecond)
|
|
1123
|
-
});
|
|
1124
|
-
return cells;
|
|
1125
|
-
}
|
|
1126
|
-
/** The glyph that leads the line on every surface. */
|
|
1127
|
-
const RUN_RECEIPT_GLYPH = "≡";
|
|
1128
|
-
/**
|
|
1129
|
-
* Fit cells to `width` columns for a tinted renderer: returns the cells that
|
|
1130
|
-
* fit whole, plus whether the last one was cut. A cut cell is truncated with
|
|
1131
|
-
* `…`; anything after it is dropped. Never wraps. `prefix` is the columns the
|
|
1132
|
-
* renderer already spent (glyph + space).
|
|
1133
|
-
*/
|
|
1134
|
-
function fitRunReceiptCells(cells, width, prefix = 0) {
|
|
1135
|
-
let used = prefix;
|
|
1136
|
-
const out = [];
|
|
1137
|
-
for (const cell of cells) {
|
|
1138
|
-
const need = (out.length ? 3 : 0) + cell.text.length;
|
|
1139
|
-
if (used + need <= width) {
|
|
1140
|
-
out.push(cell);
|
|
1141
|
-
used += need;
|
|
1142
|
-
continue;
|
|
1143
|
-
}
|
|
1144
|
-
const room = width - used - (out.length ? 3 : 0);
|
|
1145
|
-
if (room >= 2) out.push({
|
|
1146
|
-
kind: cell.kind,
|
|
1147
|
-
text: `${cell.text.slice(0, room - 1).trimEnd()}…`
|
|
1148
|
-
});
|
|
1149
|
-
break;
|
|
1150
|
-
}
|
|
1151
|
-
return out;
|
|
1152
|
-
}
|
|
1153
|
-
/** One line, ` · `-joined, ellipsized to `width` columns. Never wraps. */
|
|
1154
|
-
function formatRunReceiptLine(r, width = Number.POSITIVE_INFINITY) {
|
|
1155
|
-
const line = formatRunReceiptCells(r).map((c) => c.text).join(" · ");
|
|
1156
|
-
if (line.length <= width) return line;
|
|
1157
|
-
if (width <= 1) return width === 1 ? "…" : "";
|
|
1158
|
-
return `${line.slice(0, width - 1).trimEnd()}…`;
|
|
1159
|
-
}
|
|
1160
|
-
/** Anything to show? A run with no record data renders no line at all. */
|
|
1161
|
-
function hasRunReceipt(r) {
|
|
1162
|
-
return formatRunReceiptCells(r).length > 0;
|
|
1163
|
-
}
|
|
1164
|
-
//#endregion
|
|
1165
|
-
//#region src/chat/orchestration.ts
|
|
1166
|
-
/**
|
|
1167
|
-
* Orchestration — the pure model for a "main" session that drives others.
|
|
1168
|
-
*
|
|
1169
|
-
* A main session is an ordinary session with a role, and a worker is an
|
|
1170
|
-
* ordinary session with a parent pointer. Both are `metadata` keys, so every
|
|
1171
|
-
* existing surface (sidebar, history, attach, search, replay) works on them
|
|
1172
|
-
* with no special cases; only the surfaces that WANT to treat them specially
|
|
1173
|
-
* (pinning, the event row, the jump shortcut) read these keys.
|
|
1174
|
-
*
|
|
1175
|
-
* This is deliberately not `spawn`. A spawned child is ephemeral, inline, and
|
|
1176
|
-
* dies with the parent's run; a worker is a persistent, resumable session with
|
|
1177
|
-
* its own cwd, model chain, todos and history, visible in every window, and it
|
|
1178
|
-
* outlives the main's run. The parent pointer is what makes them a tree.
|
|
1179
|
-
*
|
|
1180
|
-
* Node-free: re-exported from `zidane/chat/pure` for the Electron renderer,
|
|
1181
|
-
* which needs `isMainSession` / `orchestratedBy` for pinning and the event row.
|
|
1182
|
-
*/
|
|
1183
|
-
/** Title the main session is created with. Users can rename it; the ROLE is what matters. */
|
|
1184
|
-
const MAIN_SESSION_TITLE = "Main";
|
|
1185
|
-
/** `session.metadata.role` — present and `'main'` on the one orchestrating session. */
|
|
1186
|
-
const SESSION_ROLE_KEY = "role";
|
|
1187
|
-
/** `session.metadata.orchestratedBy` — the main session's id, on a worker. */
|
|
1188
|
-
const ORCHESTRATED_BY_KEY = "orchestratedBy";
|
|
1189
|
-
/** Type guard over untrusted metadata. */
|
|
1190
|
-
function isMainSession(metadata) {
|
|
1191
|
-
return metadata?.[SESSION_ROLE_KEY] === "main";
|
|
1192
|
-
}
|
|
1193
|
-
/** The main session id a worker reports to, or `null` for an ordinary session. */
|
|
1194
|
-
function orchestratedBy(metadata) {
|
|
1195
|
-
const value = metadata?.[ORCHESTRATED_BY_KEY];
|
|
1196
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1197
|
-
}
|
|
1198
|
-
/** Tools whose `path` input means "this file changed". */
|
|
1199
|
-
const CHANGE_TOOLS = /* @__PURE__ */ new Set([
|
|
1200
|
-
"edit",
|
|
1201
|
-
"multi_edit",
|
|
1202
|
-
"write_file"
|
|
1203
|
-
]);
|
|
1204
|
-
const ARTIFACT_PATH_CAP = 12;
|
|
1205
|
-
/**
|
|
1206
|
-
* What a run touched, read off its assistant turns' tool calls. Only the
|
|
1207
|
-
* tools that MUTATE count as "changed" — reads are not artifacts. Capped so a
|
|
1208
|
-
* refactor across a hundred files does not turn the event into a manifest;
|
|
1209
|
-
* the count still tells the truth.
|
|
1210
|
-
*/
|
|
1211
|
-
function artifactsFromTurns(turns) {
|
|
1212
|
-
const changed = [];
|
|
1213
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1214
|
-
let commands = 0;
|
|
1215
|
-
let toolCalls = 0;
|
|
1216
|
-
for (const turn of turns) {
|
|
1217
|
-
if (turn.role !== "assistant") continue;
|
|
1218
|
-
for (const block of turn.content) {
|
|
1219
|
-
if (block.type !== "tool_call" || !block.name) continue;
|
|
1220
|
-
toolCalls++;
|
|
1221
|
-
if (block.name === "shell") commands++;
|
|
1222
|
-
if (CHANGE_TOOLS.has(block.name)) {
|
|
1223
|
-
const path = block.input?.path;
|
|
1224
|
-
if (typeof path === "string" && path && !seen.has(path)) {
|
|
1225
|
-
seen.add(path);
|
|
1226
|
-
changed.push(path);
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
if (toolCalls === 0) return void 0;
|
|
1232
|
-
return {
|
|
1233
|
-
changed: changed.slice(0, ARTIFACT_PATH_CAP),
|
|
1234
|
-
commands,
|
|
1235
|
-
toolCalls
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
|
-
const STATUSES = [
|
|
1239
|
-
"done",
|
|
1240
|
-
"error",
|
|
1241
|
-
"cancelled",
|
|
1242
|
-
"needs-attention"
|
|
1243
|
-
];
|
|
1244
|
-
/**
|
|
1245
|
-
* Wire format between the harness and the model — and, because it is persisted
|
|
1246
|
-
* inside the user turn's text block, between a live session and its replay.
|
|
1247
|
-
*
|
|
1248
|
-
* Follows `<task-notification>` exactly: child-tag XML, one block per event,
|
|
1249
|
-
* escaped with `escapeXml`. The model already knows this shape as "the harness
|
|
1250
|
-
* telling me something", which is the point — a main that mistook a worker
|
|
1251
|
-
* report for a human instruction would act on it as one. `eventsFromTurns`
|
|
1252
|
-
* detects the block on replay and re-synthesizes the structured row instead of
|
|
1253
|
-
* painting the raw XML as a user prompt.
|
|
1254
|
-
*/
|
|
1255
|
-
function renderSessionEventXml(event) {
|
|
1256
|
-
const tag = (name, value) => value === void 0 ? [] : [` <${name}>${escapeXml(String(value))}</${name}>`];
|
|
1257
|
-
return [
|
|
1258
|
-
"<session-event>",
|
|
1259
|
-
...tag("session-id", event.sessionId),
|
|
1260
|
-
...tag("title", event.title),
|
|
1261
|
-
...tag("status", event.status),
|
|
1262
|
-
...tag("at", event.at),
|
|
1263
|
-
...tag("cwd", event.cwd ?? ""),
|
|
1264
|
-
...tag("cost-usd", event.costUsd),
|
|
1265
|
-
...tag("duration-ms", event.durationMs),
|
|
1266
|
-
...tag("tokens", event.tokens),
|
|
1267
|
-
...tag("turns", event.turns),
|
|
1268
|
-
...tag("worker", event.worker === void 0 ? void 0 : String(event.worker)),
|
|
1269
|
-
...tag("seq", event.seq),
|
|
1270
|
-
...tag("finish-reason", event.finishReason),
|
|
1271
|
-
...tag("error", event.error),
|
|
1272
|
-
...tag("detail", event.detail),
|
|
1273
|
-
...tag("goal", event.goal),
|
|
1274
|
-
...event.artifacts ? [
|
|
1275
|
-
` <artifacts tool-calls="${event.artifacts.toolCalls}" commands="${event.artifacts.commands}">`,
|
|
1276
|
-
...event.artifacts.changed.map((p) => ` <changed>${escapeXml(p)}</changed>`),
|
|
1277
|
-
" </artifacts>"
|
|
1278
|
-
] : [],
|
|
1279
|
-
...tag("summary", event.summary),
|
|
1280
|
-
"</session-event>"
|
|
1281
|
-
].join("\n");
|
|
1282
|
-
}
|
|
1283
|
-
const SESSION_EVENT_RE = /^\s*<session-event>([\s\S]*?)<\/session-event>\s*$/;
|
|
1284
|
-
const FIELD = {
|
|
1285
|
-
sessionId: /<session-id>([\s\S]*?)<\/session-id>/,
|
|
1286
|
-
title: /<title>([\s\S]*?)<\/title>/,
|
|
1287
|
-
status: /<status>([\s\S]*?)<\/status>/,
|
|
1288
|
-
at: /<at>([\s\S]*?)<\/at>/,
|
|
1289
|
-
cwd: /<cwd>([\s\S]*?)<\/cwd>/,
|
|
1290
|
-
costUsd: /<cost-usd>([\s\S]*?)<\/cost-usd>/,
|
|
1291
|
-
durationMs: /<duration-ms>([\s\S]*?)<\/duration-ms>/,
|
|
1292
|
-
tokens: /<tokens>([\s\S]*?)<\/tokens>/,
|
|
1293
|
-
turns: /<turns>([\s\S]*?)<\/turns>/,
|
|
1294
|
-
worker: /<worker>([\s\S]*?)<\/worker>/,
|
|
1295
|
-
error: /<error>([\s\S]*?)<\/error>/,
|
|
1296
|
-
detail: /<detail>([\s\S]*?)<\/detail>/,
|
|
1297
|
-
seq: /<seq>([\s\S]*?)<\/seq>/,
|
|
1298
|
-
goal: /<goal>([\s\S]*?)<\/goal>/,
|
|
1299
|
-
finishReason: /<finish-reason>([\s\S]*?)<\/finish-reason>/,
|
|
1300
|
-
artifacts: /<artifacts tool-calls="(\d+)" commands="(\d+)">([\s\S]*?)<\/artifacts>/,
|
|
1301
|
-
summary: /<summary>([\s\S]*?)<\/summary>/
|
|
1302
|
-
};
|
|
1303
|
-
const CHANGED_RE = /<changed>([\s\S]*?)<\/changed>/g;
|
|
1304
|
-
const FINISH_REASONS = /* @__PURE__ */ new Set([
|
|
1305
|
-
"length",
|
|
1306
|
-
"pause",
|
|
1307
|
-
"content-filter"
|
|
1308
|
-
]);
|
|
1309
|
-
/**
|
|
1310
|
-
* Inverse of {@link renderSessionEventXml}. Untrusted input — it comes off disk
|
|
1311
|
-
* and outlives this module's versions — so it validates each field and returns
|
|
1312
|
-
* `null` for anything malformed, in which case the turn replays as the plain
|
|
1313
|
-
* user text it structurally is.
|
|
1314
|
-
*/
|
|
1315
|
-
function parseSessionEventXml(text) {
|
|
1316
|
-
const m = text.match(SESSION_EVENT_RE);
|
|
1317
|
-
if (!m) return null;
|
|
1318
|
-
const body = m[1];
|
|
1319
|
-
const pick = (re) => {
|
|
1320
|
-
const inner = body.match(re);
|
|
1321
|
-
return inner ? unescapeXml(inner[1].trim()) : void 0;
|
|
1322
|
-
};
|
|
1323
|
-
const sessionId = pick(FIELD.sessionId);
|
|
1324
|
-
const title = pick(FIELD.title);
|
|
1325
|
-
const status = pick(FIELD.status);
|
|
1326
|
-
const at = Number(pick(FIELD.at));
|
|
1327
|
-
const summary = pick(FIELD.summary);
|
|
1328
|
-
if (!sessionId || title === void 0 || summary === void 0) return null;
|
|
1329
|
-
if (!status || !STATUSES.includes(status)) return null;
|
|
1330
|
-
if (!Number.isFinite(at)) return null;
|
|
1331
|
-
const cost = Number(pick(FIELD.costUsd));
|
|
1332
|
-
const cwd = pick(FIELD.cwd);
|
|
1333
|
-
const positive = (raw) => {
|
|
1334
|
-
const n = Number(raw);
|
|
1335
|
-
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
1336
|
-
};
|
|
1337
|
-
const durationMs = positive(pick(FIELD.durationMs));
|
|
1338
|
-
const tokens = positive(pick(FIELD.tokens));
|
|
1339
|
-
const turns = positive(pick(FIELD.turns));
|
|
1340
|
-
const worker = pick(FIELD.worker);
|
|
1341
|
-
const error = pick(FIELD.error);
|
|
1342
|
-
const detail = pick(FIELD.detail);
|
|
1343
|
-
const seq = positive(pick(FIELD.seq));
|
|
1344
|
-
const goal = pick(FIELD.goal);
|
|
1345
|
-
const finishReason = pick(FIELD.finishReason);
|
|
1346
|
-
const art = body.match(FIELD.artifacts);
|
|
1347
|
-
const artifacts = art ? {
|
|
1348
|
-
toolCalls: Number(art[1]),
|
|
1349
|
-
commands: Number(art[2]),
|
|
1350
|
-
changed: [...art[3].matchAll(CHANGED_RE)].map((m) => unescapeXml(m[1].trim()))
|
|
1351
|
-
} : void 0;
|
|
1352
|
-
return {
|
|
1353
|
-
sessionId,
|
|
1354
|
-
title,
|
|
1355
|
-
status,
|
|
1356
|
-
summary,
|
|
1357
|
-
at,
|
|
1358
|
-
...Number.isFinite(cost) && cost > 0 ? { costUsd: cost } : {},
|
|
1359
|
-
...cwd !== void 0 ? { cwd } : {},
|
|
1360
|
-
...durationMs !== void 0 ? { durationMs } : {},
|
|
1361
|
-
...tokens !== void 0 ? { tokens } : {},
|
|
1362
|
-
...turns !== void 0 ? { turns } : {},
|
|
1363
|
-
...worker === "true" ? { worker: true } : worker === "false" ? { worker: false } : {},
|
|
1364
|
-
...error ? { error } : {},
|
|
1365
|
-
...detail ? { detail } : {},
|
|
1366
|
-
...seq !== void 0 ? { seq } : {},
|
|
1367
|
-
...goal ? { goal } : {},
|
|
1368
|
-
...finishReason && FINISH_REASONS.has(finishReason) ? { finishReason } : {},
|
|
1369
|
-
...artifacts ? { artifacts } : {}
|
|
1370
|
-
};
|
|
1371
|
-
}
|
|
1372
|
-
function unescapeXml(s) {
|
|
1373
|
-
return s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/&/g, "&");
|
|
1374
|
-
}
|
|
1375
|
-
/**
|
|
1376
|
-
* The short label a transcript row shows — `api-refactor · done`. Status words
|
|
1377
|
-
* are chosen to read as a verb phrase after the title.
|
|
1378
|
-
*/
|
|
1379
|
-
function sessionEventLabel(event) {
|
|
1380
|
-
return `${event.title} ${{
|
|
1381
|
-
"done": "finished",
|
|
1382
|
-
"error": "failed",
|
|
1383
|
-
"cancelled": "was cancelled",
|
|
1384
|
-
"needs-attention": "needs attention"
|
|
1385
|
-
}[event.status]}`;
|
|
1386
|
-
}
|
|
1387
|
-
/**
|
|
1388
|
-
* Status → how it reads. `needs-attention` is deliberately the loudest: it is
|
|
1389
|
-
* the one status where the human, not the main, has to act.
|
|
1390
|
-
*/
|
|
1391
|
-
const SESSION_EVENT_STATUS_STYLE = {
|
|
1392
|
-
"done": {
|
|
1393
|
-
glyph: "✓",
|
|
1394
|
-
tone: "accent",
|
|
1395
|
-
verb: "finished"
|
|
1396
|
-
},
|
|
1397
|
-
"error": {
|
|
1398
|
-
glyph: "✗",
|
|
1399
|
-
tone: "error",
|
|
1400
|
-
verb: "failed"
|
|
1401
|
-
},
|
|
1402
|
-
"cancelled": {
|
|
1403
|
-
glyph: "⊘",
|
|
1404
|
-
tone: "dim",
|
|
1405
|
-
verb: "was cancelled"
|
|
1406
|
-
},
|
|
1407
|
-
"needs-attention": {
|
|
1408
|
-
glyph: "!",
|
|
1409
|
-
tone: "warn",
|
|
1410
|
-
verb: "needs your input"
|
|
1411
|
-
}
|
|
1412
|
-
};
|
|
1413
|
-
/**
|
|
1414
|
-
* The directory name a session lives in — the same basename label the sidebar
|
|
1415
|
-
* groups by. `undefined` for a directory-less session (the main) or no cwd.
|
|
1416
|
-
*/
|
|
1417
|
-
function sessionDirLabel(cwd) {
|
|
1418
|
-
if (!cwd) return void 0;
|
|
1419
|
-
const parts = cwd.split(/[\\/]+/).filter(Boolean);
|
|
1420
|
-
return parts[parts.length - 1];
|
|
1421
|
-
}
|
|
1422
|
-
/** Short, stable id fragment for the header — enough to disambiguate, not to read. */
|
|
1423
|
-
function shortSessionId(id) {
|
|
1424
|
-
return id.length <= 8 ? id : id.slice(0, 8);
|
|
1425
|
-
}
|
|
1426
|
-
/**
|
|
1427
|
-
* The header's metadata cells, in display order, omitting anything unknown.
|
|
1428
|
-
* Renderers join them with their own separator; the CONTENT is fixed here so
|
|
1429
|
-
* both surfaces show the same receipt for the same run.
|
|
1430
|
-
*
|
|
1431
|
-
* The session id is deliberately NOT a cell: it is the least legible datum and
|
|
1432
|
-
* exists only to disambiguate. Renderers append `shortSessionId` LAST and
|
|
1433
|
-
* dimmer than the rest.
|
|
1434
|
-
*/
|
|
1435
|
-
function sessionEventMeta(event) {
|
|
1436
|
-
const cells = [];
|
|
1437
|
-
const dir = sessionDirLabel(event.cwd);
|
|
1438
|
-
if (dir) cells.push(dir);
|
|
1439
|
-
if (event.durationMs !== void 0) cells.push(formatRunDuration(event.durationMs));
|
|
1440
|
-
if (event.tokens !== void 0) cells.push(`${formatTokenCount(event.tokens)} tok`);
|
|
1441
|
-
if (event.turns !== void 0) cells.push(`${event.turns} turn${event.turns === 1 ? "" : "s"}`);
|
|
1442
|
-
if (event.costUsd !== void 0) cells.push(formatCostUsd(event.costUsd));
|
|
1443
|
-
return cells;
|
|
1444
|
-
}
|
|
1445
|
-
/**
|
|
1446
|
-
* The dimmed second line: the last thing the session said, collapsed to one
|
|
1447
|
-
* paragraph and capped. Renderers may clamp further (line-clamp in the GUI,
|
|
1448
|
-
* width in the TUI); this is the upper bound and the shared ellipsis rule.
|
|
1449
|
-
*/
|
|
1450
|
-
function sessionEventExcerpt(event, max = 240) {
|
|
1451
|
-
const flat = (event.status === "needs-attention" && event.detail ? event.detail : event.status === "error" && event.error ? event.error : event.summary).replace(/\s+/g, " ").trim();
|
|
1452
|
-
return flat.length <= max ? flat : `${flat.slice(0, max - 1).trimEnd()}…`;
|
|
1453
|
-
}
|
|
1454
|
-
/** Ellipsize a title to `max` characters; titles are user text and can be a paragraph. */
|
|
1455
|
-
function truncateTitle(title, max = 48) {
|
|
1456
|
-
const flat = title.replace(/\s+/g, " ").trim();
|
|
1457
|
-
return flat.length <= max ? flat : `${flat.slice(0, max - 1).trimEnd()}…`;
|
|
1458
|
-
}
|
|
1459
|
-
/**
|
|
1460
|
-
* System-prompt fragment appended to every WORKER's agent. A worker's last
|
|
1461
|
-
* message becomes the main's `summary`, so it must be a report, not a sign-off
|
|
1462
|
-
* — the main should be able to act on it without reading the transcript.
|
|
1463
|
-
*/
|
|
1464
|
-
const WORKER_REPORT_FRAGMENT = `You are a worker session created by an orchestrating main session. Your final message of a run is delivered to it verbatim as your report, so end every run with a short structured report:
|
|
1465
|
-
|
|
1466
|
-
## Report
|
|
1467
|
-
- **Goal**: what you were asked, in one line
|
|
1468
|
-
- **Outcome**: done / partial / blocked — one line, and why if not done
|
|
1469
|
-
- **Changed**: files or artifacts touched (paths), or "none"
|
|
1470
|
-
- **Checks**: what you ran and the result (tests, typecheck, lint), or "none"
|
|
1471
|
-
- **Open questions**: anything the main or a human must decide, or "none"
|
|
1472
|
-
- **Next**: the concrete next step you recommend, or "none"
|
|
1473
|
-
|
|
1474
|
-
Keep it factual and under ~15 lines. This block is what the main decides on; prose before it is fine, prose instead of it is not.`;
|
|
1475
|
-
/**
|
|
1476
|
-
* System-prompt fragment appended to the MAIN's agent: how to delegate without
|
|
1477
|
-
* wasting calls. The `sessions` tool description says what each verb does;
|
|
1478
|
-
* this says which one to reach for, and when not to call at all.
|
|
1479
|
-
*/
|
|
1480
|
-
const MAIN_PLAYBOOK_FRAGMENT = `You are the main session: an orchestrator. Work happens in worker sessions; you brief them, integrate their reports, and keep the human informed. Do not do the workers' work here.
|
|
1481
|
-
|
|
1482
|
-
Work efficiently with the \`sessions\` tool:
|
|
1483
|
-
- Never poll. After create or send you WILL receive a <session-event> when the worker finishes, fails, or blocks. Do not read or list a worker that has not reported yet.
|
|
1484
|
-
- Reuse before creating. To continue work in a project, send to its existing idle session (listed under Projects below; its context is already warm). Create a worker only for a new, independent unit of work, or when the project has no suitable session.
|
|
1485
|
-
- You already know the ids. Take them from Projects below, from a <session-event>, or from an earlier list. Do not list just to find an id you have.
|
|
1486
|
-
- send vs steer: send when the worker is idle or the new prompt should queue after its current run; steer only to change course mid-run.
|
|
1487
|
-
- Fire-and-forget when you can. For work you will not act on when it lands (docs, cleanup, low-stakes checks), pass wake: false on create or send; the report still reaches your history, you just are not woken for it.
|
|
1488
|
-
- Brief completely. A worker prompt states the goal, the constraints, what to check (tests, typecheck, lint), and that it must end with its report. Follow-up questions cost a full round trip.
|
|
1489
|
-
- read is cheap (last message); transcript is not. Use transcript only when a report is unclear.
|
|
1490
|
-
- Report to the human briefly: what went where, what came back, what needs a decision. Do not re-narrate worker reports.`;
|
|
1491
|
-
/**
|
|
1492
|
-
* The main's "Projects" briefing: which directories have ongoing work and
|
|
1493
|
-
* the session to address in each, so the main does not rediscover them with
|
|
1494
|
-
* a list call every run. Snapshot at agent start — live state is one `list`
|
|
1495
|
-
* away and the text says so.
|
|
1496
|
-
*/
|
|
1497
|
-
function renderProjectsBriefing(projects, now) {
|
|
1498
|
-
const lines = ["## Projects", "Known as of this session's start; live state comes from `sessions list`. Directory-less sessions are omitted."];
|
|
1499
|
-
if (projects.length === 0) {
|
|
1500
|
-
lines.push("- none yet");
|
|
1501
|
-
return lines.join("\n");
|
|
1502
|
-
}
|
|
1503
|
-
for (const p of projects) {
|
|
1504
|
-
const cells = [`${p.sessions} session${p.sessions === 1 ? "" : "s"}`, `last active ${agoLabel(now - p.lastActiveAt)}`];
|
|
1505
|
-
if (p.active > 0) cells.push(`${p.active} active`);
|
|
1506
|
-
lines.push(`- ${sessionDirLabel(p.root) ?? p.root} (${p.root}) · ${cells.join(" · ")}`);
|
|
1507
|
-
lines.push(` latest: ${p.latest.sessionId} "${truncateTitle(p.latest.title, 60)}" (${p.latest.state})`);
|
|
1508
|
-
}
|
|
1509
|
-
return lines.join("\n");
|
|
1510
|
-
}
|
|
1511
|
-
/** Coarse relative time for the briefing: `just now`, `12m ago`, `3h ago`, `5d ago`. */
|
|
1512
|
-
function agoLabel(elapsedMs) {
|
|
1513
|
-
const m = Math.floor(Math.max(0, elapsedMs) / 6e4);
|
|
1514
|
-
if (m < 1) return "just now";
|
|
1515
|
-
if (m < 60) return `${m}m ago`;
|
|
1516
|
-
const h = Math.floor(m / 60);
|
|
1517
|
-
if (h < 48) return `${h}h ago`;
|
|
1518
|
-
return `${Math.floor(h / 24)}d ago`;
|
|
1519
|
-
}
|
|
1520
|
-
//#endregion
|
|
1521
|
-
//#region src/chat/turn-selection.ts
|
|
1522
|
-
/** Tools whose `tool-result` event is suppressed when `showEditDiffs` is on. */
|
|
1523
|
-
const EDIT_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
1524
|
-
"edit",
|
|
1525
|
-
"multi_edit",
|
|
1526
|
-
"write_file"
|
|
1527
|
-
]);
|
|
1528
|
-
/**
|
|
1529
|
-
* Keep turn-selection mode alive after deleting its active row. Prefer the
|
|
1530
|
-
* row that slides into the deleted index; at the tail, select the new last
|
|
1531
|
-
* row. Returns null only when no selectable rows survive.
|
|
1532
|
-
*/
|
|
1533
|
-
function selectionAfterTurnRemoval(before, removedId, after) {
|
|
1534
|
-
if (after.includes(removedId)) return removedId;
|
|
1535
|
-
if (after.length === 0) return null;
|
|
1536
|
-
const index = before.indexOf(removedId);
|
|
1537
|
-
return after[Math.min(Math.max(0, index), after.length - 1)] ?? null;
|
|
1538
|
-
}
|
|
1539
|
-
/**
|
|
1540
|
-
* Recognize a tool-result body as carrying NON-success information so the
|
|
1541
|
-
* renderer doesn't suppress it under `showEditDiffs`. Three categories:
|
|
1542
|
-
*
|
|
1543
|
-
* - `edit` → "Edit error: …"
|
|
1544
|
-
* - `write_file` permission errors wrapped by the loop → "Tool failed: …"
|
|
1545
|
-
* - `multi_edit` → legacy single-line error `multi_edit error: …`, OR
|
|
1546
|
-
* a result carrying an `<edit-outcomes>…</edit-outcomes>` annotation
|
|
1547
|
-
* block. The TUI only appends the annotation when at least one hunk
|
|
1548
|
-
* was NOT applied, so its mere presence is the signal — the result
|
|
1549
|
-
* body needs to stay visible next to the diff so the user can read
|
|
1550
|
-
* denial / skip / failure reasons longer than the per-hunk badge.
|
|
1551
|
-
* - Fully-denied gate emit (`[fully denied] <edit-outcomes>…`) likewise
|
|
1552
|
-
* stays visible.
|
|
55
|
+
* - `edit` → "Edit error: …"
|
|
56
|
+
* - `write_file` permission errors wrapped by the loop → "Tool failed: …"
|
|
57
|
+
* - `multi_edit` → legacy single-line error `multi_edit error: …`, OR
|
|
58
|
+
* a result carrying an `<edit-outcomes>…</edit-outcomes>` annotation
|
|
59
|
+
* block. The TUI only appends the annotation when at least one hunk
|
|
60
|
+
* was NOT applied, so its mere presence is the signal — the result
|
|
61
|
+
* body needs to stay visible next to the diff so the user can read
|
|
62
|
+
* denial / skip / failure reasons longer than the per-hunk badge.
|
|
63
|
+
* - Fully-denied gate emit (`[fully denied] <edit-outcomes>…`) likewise
|
|
64
|
+
* stays visible.
|
|
1553
65
|
*
|
|
1554
66
|
* Exported for unit-testability of the visibility matrix.
|
|
1555
67
|
*/
|
|
@@ -1562,8 +74,8 @@ function isEditErrorResult(text) {
|
|
|
1562
74
|
return false;
|
|
1563
75
|
}
|
|
1564
76
|
/**
|
|
1565
|
-
* `callId` prefix minted by the TUI's `!` bash mode (see `
|
|
1566
|
-
* in `src/tui/
|
|
77
|
+
* `callId` prefix minted by the TUI's `!` bash mode (see `runBashMode`
|
|
78
|
+
* in `src/tui/commands/run-bash-mode.ts`). The prefix is the durable signal — it survives
|
|
1567
79
|
* persistence and replay — that a `shell` tool exchange was USER-initiated,
|
|
1568
80
|
* so renderers can keep it visible even when the tool-display settings
|
|
1569
81
|
* hide model-driven plumbing: the user typed the command and expects to
|
|
@@ -1692,7 +204,7 @@ function isVisibleDuringSearch(event, settings, query) {
|
|
|
1692
204
|
* Renderer-agnostic — returns plain `boolean` so TUI / GUI consumers
|
|
1693
205
|
* can filter events identically.
|
|
1694
206
|
*/
|
|
1695
|
-
/** `notice.label` the host stamps on prompt-cache break/expiry rows (`host/
|
|
207
|
+
/** `notice.label` the host stamps on prompt-cache break/expiry rows (`host/notices/index.ts`). */
|
|
1696
208
|
const CACHE_NOTICE_LABEL = "cache";
|
|
1697
209
|
function isVisible(event, settings) {
|
|
1698
210
|
if (settings.hideSubagentOutput) {
|
|
@@ -1706,6 +218,7 @@ function isVisible(event, settings) {
|
|
|
1706
218
|
case "tool-result": return settings.showToolResults || isUserBashEvent(event);
|
|
1707
219
|
case "run-summary": return settings.showRunReceipt;
|
|
1708
220
|
case "info": return settings.showCacheNotices || event.notice?.label !== "cache";
|
|
221
|
+
case "spawn-queued": return settings.transcriptDensity === "compact";
|
|
1709
222
|
default: return true;
|
|
1710
223
|
}
|
|
1711
224
|
}
|
|
@@ -1768,68 +281,621 @@ function turnSelectionOwnership(events) {
|
|
|
1768
281
|
else if (lastToolEmitterTurnId) ownership.set(tid, lastToolEmitterTurnId);
|
|
1769
282
|
continue;
|
|
1770
283
|
}
|
|
1771
|
-
if (kinds.includes("tool")) lastToolEmitterTurnId = tid;
|
|
1772
|
-
}
|
|
1773
|
-
return ownership;
|
|
1774
|
-
}
|
|
1775
|
-
/**
|
|
1776
|
-
* Render-time check: should `event` paint with the selection accent?
|
|
1777
|
-
*
|
|
1778
|
-
* `true` when the event's own turn is selected, OR when the selected turn
|
|
1779
|
-
* `owns` the event's turn via {@link turnSelectionOwnership} (the call and
|
|
1780
|
-
* its tool-result rows highlight together). `false` when nothing is
|
|
1781
|
-
* selected or the relationship doesn't apply.
|
|
1782
|
-
*
|
|
1783
|
-
* Pure. Renderer-agnostic — the TUI's `<Transcript>` uses it; a GUI's
|
|
1784
|
-
* equivalent walks the same rule.
|
|
1785
|
-
*/
|
|
1786
|
-
function isTurnHighlighted(event, selectedTurnId, ownership) {
|
|
1787
|
-
if (selectedTurnId === null || !event.turnId) return false;
|
|
1788
|
-
if (event.turnId === selectedTurnId) return true;
|
|
1789
|
-
return ownership.get(event.turnId) === selectedTurnId || ownership.get(selectedTurnId) === event.turnId;
|
|
284
|
+
if (kinds.includes("tool")) lastToolEmitterTurnId = tid;
|
|
285
|
+
}
|
|
286
|
+
return ownership;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Render-time check: should `event` paint with the selection accent?
|
|
290
|
+
*
|
|
291
|
+
* `true` when the event's own turn is selected, OR when the selected turn
|
|
292
|
+
* `owns` the event's turn via {@link turnSelectionOwnership} (the call and
|
|
293
|
+
* its tool-result rows highlight together). `false` when nothing is
|
|
294
|
+
* selected or the relationship doesn't apply.
|
|
295
|
+
*
|
|
296
|
+
* Pure. Renderer-agnostic — the TUI's `<Transcript>` uses it; a GUI's
|
|
297
|
+
* equivalent walks the same rule.
|
|
298
|
+
*/
|
|
299
|
+
function isTurnHighlighted(event, selectedTurnId, ownership) {
|
|
300
|
+
if (selectedTurnId === null || !event.turnId) return false;
|
|
301
|
+
if (event.turnId === selectedTurnId) return true;
|
|
302
|
+
return ownership.get(event.turnId) === selectedTurnId || ownership.get(selectedTurnId) === event.turnId;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Ownership for "one selection stop per message": result-only turns fold
|
|
306
|
+
* into their assistant turn (`turnSelectionOwnership`), and turns that only
|
|
307
|
+
* did work (thinking, tool calls, worker markers) fold into the answer that
|
|
308
|
+
* follows them in the same exchange. "Read three files, then answered" is
|
|
309
|
+
* one stop, so ↑ moves one visible message at a time. An exchange that ends
|
|
310
|
+
* in work alone stops on its first work turn. Same rule as the GUI's
|
|
311
|
+
* `computeSelectStops`.
|
|
312
|
+
*/
|
|
313
|
+
function messageSelectionOwnership(events) {
|
|
314
|
+
const base = turnSelectionOwnership(events);
|
|
315
|
+
const resolve = (id) => base.get(id) ?? id;
|
|
316
|
+
const stops = /* @__PURE__ */ new Set();
|
|
317
|
+
const merged = /* @__PURE__ */ new Map();
|
|
318
|
+
let pending = [];
|
|
319
|
+
const settle = (into) => {
|
|
320
|
+
const target = into ?? pending[0];
|
|
321
|
+
for (const id of pending) if (target !== void 0 && id !== target) merged.set(id, target);
|
|
322
|
+
if (target !== void 0) stops.add(target);
|
|
323
|
+
pending = [];
|
|
324
|
+
};
|
|
325
|
+
for (const event of events) {
|
|
326
|
+
if (event.childId !== void 0 || (event.depth ?? 0) > 0) continue;
|
|
327
|
+
if (event.kind === "separator" || event.kind === "user-prompt" || event.kind === "compact-summary") settle(null);
|
|
328
|
+
if (!event.turnId) continue;
|
|
329
|
+
const owner = resolve(event.turnId);
|
|
330
|
+
const work = event.kind === "tool" || event.kind === "tool-result" || event.kind === "thinking" || event.kind === "spawn-start" || event.kind === "spawn-end" || event.kind === "command-detail" || event.kind === "info" && event.notice === void 0;
|
|
331
|
+
if (event.kind === "markdown" && event.text.trim()) {
|
|
332
|
+
stops.add(owner);
|
|
333
|
+
settle(owner);
|
|
334
|
+
} else if (work) {
|
|
335
|
+
if (!stops.has(owner) && !pending.includes(owner)) pending.push(owner);
|
|
336
|
+
} else if (event.kind !== "user-prompt" && event.kind !== "compact-summary") {
|
|
337
|
+
settle(null);
|
|
338
|
+
stops.add(owner);
|
|
339
|
+
} else stops.add(owner);
|
|
340
|
+
}
|
|
341
|
+
settle(null);
|
|
342
|
+
const out = /* @__PURE__ */ new Map();
|
|
343
|
+
const final = (id) => merged.get(id) ?? id;
|
|
344
|
+
for (const [raw, owner] of base) {
|
|
345
|
+
const stop = final(owner);
|
|
346
|
+
if (stop !== raw) out.set(raw, stop);
|
|
347
|
+
}
|
|
348
|
+
for (const [id, stop] of merged) out.set(id, stop);
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Deduplicated, in-order list of **parent-conversation** turn ids that appear
|
|
353
|
+
* in a rendered transcript — the navigation index for the TUI's select-turn
|
|
354
|
+
* mode. Three classes of turns are deliberately skipped:
|
|
355
|
+
*
|
|
356
|
+
* - **Subagent turns** (`childId` set). Nested execution detail; the
|
|
357
|
+
* user's mental model of a "message" is the conversational exchange,
|
|
358
|
+
* not each spawn turn. Also filtered out by `isVisible` under
|
|
359
|
+
* `hideSubagentOutput: true` — selecting them would highlight nothing.
|
|
360
|
+
* - **Result-only turns** — see {@link turnSelectionOwnership}. These get
|
|
361
|
+
* coalesced into the assistant turn that emitted their tool_calls.
|
|
362
|
+
* - **Settings-hidden turns** (when `settings` is supplied). A turn whose
|
|
363
|
+
* every event fails {@link isVisible} would render no rows — landing
|
|
364
|
+
* the cursor there hides it from the user entirely. The check is opt-
|
|
365
|
+
* in so SDK callers without a Settings object keep the legacy
|
|
366
|
+
* "everything visible" behavior.
|
|
367
|
+
*
|
|
368
|
+
* Synthetic events (separator, spawn-start, spawn-end) have no `turnId` and
|
|
369
|
+
* are skipped naturally.
|
|
370
|
+
*/
|
|
371
|
+
function selectableTurnIds(events, settings, visible, opts) {
|
|
372
|
+
const ownership = opts?.byMessage ? messageSelectionOwnership(events) : turnSelectionOwnership(events);
|
|
373
|
+
const visibleCount = settings ? /* @__PURE__ */ new Map() : null;
|
|
374
|
+
if (settings && visibleCount) for (const e of events) {
|
|
375
|
+
if (!e.turnId || e.childId) continue;
|
|
376
|
+
if (!(visible ? visible(e) : isVisible(e, settings))) continue;
|
|
377
|
+
visibleCount.set(e.turnId, (visibleCount.get(e.turnId) ?? 0) + 1);
|
|
378
|
+
}
|
|
379
|
+
const seen = /* @__PURE__ */ new Set();
|
|
380
|
+
const ordered = [];
|
|
381
|
+
for (const e of events) {
|
|
382
|
+
if (!e.turnId) continue;
|
|
383
|
+
if (e.childId) continue;
|
|
384
|
+
if (seen.has(e.turnId)) continue;
|
|
385
|
+
if (ownership.has(e.turnId)) continue;
|
|
386
|
+
if (visibleCount && (visibleCount.get(e.turnId) ?? 0) === 0) continue;
|
|
387
|
+
seen.add(e.turnId);
|
|
388
|
+
ordered.push(e.turnId);
|
|
389
|
+
}
|
|
390
|
+
return ordered;
|
|
391
|
+
}
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region src/chat/machine-protocol.ts
|
|
394
|
+
/** Bumped on an incompatible change; a host refuses another major. */
|
|
395
|
+
const MACHINE_PROTOCOL_VERSION = 1;
|
|
396
|
+
/** Largest `data` chunk (decoded bytes) of one `exec.stream.data` notification or `exec.stream.write`. */
|
|
397
|
+
const MACHINE_STREAM_CHUNK_BYTES = 262144;
|
|
398
|
+
/**
|
|
399
|
+
* Calls whose re-send after a lost reply cannot change anything (reads, and
|
|
400
|
+
* the idempotent handle calls: the machine keys handles by the host's id).
|
|
401
|
+
* Everything else that was sent is reported as a lost result, never re-sent.
|
|
402
|
+
*/
|
|
403
|
+
const MACHINE_IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
|
|
404
|
+
"machine.ping",
|
|
405
|
+
"exec.spawn",
|
|
406
|
+
"exec.cancel",
|
|
407
|
+
"exec.destroy",
|
|
408
|
+
"fs.read",
|
|
409
|
+
"fs.list",
|
|
410
|
+
"fs.size",
|
|
411
|
+
"bg.list",
|
|
412
|
+
"bg.wait",
|
|
413
|
+
"env.checkFolder",
|
|
414
|
+
"terminal.attach"
|
|
415
|
+
]);
|
|
416
|
+
/** Error codes of the machine protocol (JSON-RPC server-error range, apart from `IpcErrorCode`). */
|
|
417
|
+
const MachineErrorCode = {
|
|
418
|
+
/** A machine connection called a client method, or a client a machine one. */
|
|
419
|
+
MethodNotAllowed: -32040,
|
|
420
|
+
/** `machine.register` from another major `MACHINE_PROTOCOL_VERSION`. */
|
|
421
|
+
VersionSkew: -32041,
|
|
422
|
+
/** The name is held by another machine on this host. */
|
|
423
|
+
NameTaken: -32042,
|
|
424
|
+
/** A path or cwd outside the machine's roots, or in its data dir. */
|
|
425
|
+
OutsideRoots: -32043,
|
|
426
|
+
/** The machine id is bound to another (live) token, or this token was used by the machine before the host forgot it. */
|
|
427
|
+
IdBound: -32044,
|
|
428
|
+
/** No room now (a capacity limit): try again later, unlike a refusal. */
|
|
429
|
+
Busy: -32045
|
|
430
|
+
};
|
|
431
|
+
/** Where a local host keeps the machine token a saved server gave it (credentials store). */
|
|
432
|
+
function machineCredentialKey(hostId) {
|
|
433
|
+
return `machine:${hostId}`;
|
|
434
|
+
}
|
|
435
|
+
/** A machine name from a free-form one (a server's display name): the valid characters kept, the rest dashed. */
|
|
436
|
+
function toMachineName(raw) {
|
|
437
|
+
return raw.trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "machine";
|
|
438
|
+
}
|
|
439
|
+
/** A machine-qualified environment kind: `<base>@<machine name>`. */
|
|
440
|
+
function machineKind(base, name) {
|
|
441
|
+
return `${base}@${name}`;
|
|
442
|
+
}
|
|
443
|
+
/** `{ base, name }` of a machine-qualified kind, `null` for any other. */
|
|
444
|
+
function parseMachineKind(kind) {
|
|
445
|
+
const match = /^(local|worktree)@(.+)$/.exec(kind);
|
|
446
|
+
return match ? {
|
|
447
|
+
base: match[1],
|
|
448
|
+
name: match[2]
|
|
449
|
+
} : null;
|
|
450
|
+
}
|
|
451
|
+
/** A machine name: one path-safe segment, so `local@<name>` is unambiguous. */
|
|
452
|
+
function isValidMachineName(name) {
|
|
453
|
+
return /^[\w.-]{1,64}$/.test(name);
|
|
454
|
+
}
|
|
455
|
+
//#endregion
|
|
456
|
+
//#region src/chat/feature-gate.ts
|
|
457
|
+
/** `process.env` when there is one; a renderer has no `process`. */
|
|
458
|
+
function ambientEnv() {
|
|
459
|
+
return globalThis.process?.env ?? {};
|
|
460
|
+
}
|
|
461
|
+
function defineFeatureGate({ setting, killSwitchEnv, flag }) {
|
|
462
|
+
const isKilled = (env = ambientEnv()) => env[killSwitchEnv] === "1";
|
|
463
|
+
return {
|
|
464
|
+
setting,
|
|
465
|
+
killSwitchEnv,
|
|
466
|
+
flag,
|
|
467
|
+
isKilled,
|
|
468
|
+
isEnabled: (settings, env) => settings[setting] === true && !isKilled(env)
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* The shared daemon runtime. `ZIDANE_NO_DAEMON=1` is set by
|
|
473
|
+
* `zidane --no-daemon`.
|
|
474
|
+
*/
|
|
475
|
+
const daemonGate = defineFeatureGate({
|
|
476
|
+
setting: "useDaemon",
|
|
477
|
+
killSwitchEnv: "ZIDANE_NO_DAEMON",
|
|
478
|
+
flag: "--no-daemon"
|
|
479
|
+
});
|
|
480
|
+
/**
|
|
481
|
+
* Remote hosts (zidane-server). The setting ("Remote hosts")
|
|
482
|
+
* gates the saved servers: the Hosts settings, the per-host session groups
|
|
483
|
+
* and picking a host for a new session. Connecting through an explicit `--host` /
|
|
484
|
+
* `ZIDANE_HOST_URL` is its own opt-in, but `ZIDANE_NO_REMOTE=1` still wins
|
|
485
|
+
* over it (`src/tui/host-target.ts`). `ZIDANE_NO_REMOTE=1` is set by
|
|
486
|
+
* `zidane --no-remote`.
|
|
487
|
+
*/
|
|
488
|
+
const remoteHostsGate = defineFeatureGate({
|
|
489
|
+
setting: "remoteHosts",
|
|
490
|
+
killSwitchEnv: "ZIDANE_NO_REMOTE",
|
|
491
|
+
flag: "--no-remote"
|
|
492
|
+
});
|
|
493
|
+
/** Every gate, so a client can report which kill switches are engaged. */
|
|
494
|
+
const FEATURE_GATES = [daemonGate, remoteHostsGate];
|
|
495
|
+
/**
|
|
496
|
+
* The kill-switch env vars engaged in `env`, sorted. The GUI main process
|
|
497
|
+
* hands this to its renderer (which has no `process.env`) so settings rows
|
|
498
|
+
* can say why they are inert.
|
|
499
|
+
*/
|
|
500
|
+
function engagedKillSwitches(env = ambientEnv()) {
|
|
501
|
+
return FEATURE_GATES.filter((g) => g.isKilled(env)).map((g) => g.killSwitchEnv).sort();
|
|
502
|
+
}
|
|
503
|
+
/** `name` is set to `'1'` in `env`: the kill-switch test, for a row that only knows the variable. */
|
|
504
|
+
function isKillSwitchEngaged(name, env = ambientEnv()) {
|
|
505
|
+
return env[name] === "1";
|
|
506
|
+
}
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region src/chat/footer-chips.ts
|
|
509
|
+
/**
|
|
510
|
+
* Abbreviate a re-billed token count: `1200` → `1.2k`, `800` → `800`.
|
|
511
|
+
* Thousands only — a cache prefix large enough to need `M` would be past any
|
|
512
|
+
* provider's window.
|
|
513
|
+
*/
|
|
514
|
+
function formatTokens(tokens) {
|
|
515
|
+
return tokens >= 1e3 ? `${(tokens / 1e3).toFixed(1)}k` : String(tokens);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Build the chip for a cache break.
|
|
519
|
+
*
|
|
520
|
+
* `costUsd` is OPTIONAL and omitted entirely on a rate miss — never rendered
|
|
521
|
+
* as `$0.00`. A fabricated zero would read as "this was free", which is the
|
|
522
|
+
* opposite of what the chip exists to say. When present it is prefixed `~`
|
|
523
|
+
* because it is an estimate (registry rates × re-billed prefix), and clamped
|
|
524
|
+
* to `<0.01` rather than rounding a real charge down to nothing.
|
|
525
|
+
*/
|
|
526
|
+
function cacheBreakChip(info) {
|
|
527
|
+
const rebilled = formatTokens(info.rebilledTokens);
|
|
528
|
+
const cost = info.costUsd !== void 0 ? ` · ~$${info.costUsd < .01 ? "<0.01" : info.costUsd.toFixed(2)}` : "";
|
|
529
|
+
if (info.changed.length === 0) return {
|
|
530
|
+
tone: "dim",
|
|
531
|
+
glyph: "⚡",
|
|
532
|
+
label: `cache expired · +${rebilled} tok${cost}`
|
|
533
|
+
};
|
|
534
|
+
return {
|
|
535
|
+
tone: "warn",
|
|
536
|
+
glyph: "⚡",
|
|
537
|
+
label: `cache break · ${info.changed.join(",")} · +${rebilled} tok${cost}`
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Transcript-row text for a persisted cache-break notice. Multi-line, one
|
|
542
|
+
* fact per line, so both hosts can show a short headline and fold or list
|
|
543
|
+
* the rest:
|
|
544
|
+
*
|
|
545
|
+
* cache break ↑116.9k tokens ~$0.48
|
|
546
|
+
* messagePrefix: message 16 of 41→43 rewritten
|
|
547
|
+
* likely compaction or history trimming
|
|
548
|
+
*
|
|
549
|
+
* The footer chip stays terse; the row carries the culprit so a break is
|
|
550
|
+
* diagnosable from the transcript. Only the headline on a TTL / eviction
|
|
551
|
+
* break (nothing was done wrong).
|
|
552
|
+
*/
|
|
553
|
+
function cacheBreakNoticeText(info) {
|
|
554
|
+
const rebilled = formatTokens(info.rebilledTokens);
|
|
555
|
+
const cost = info.costUsd !== void 0 ? ` ~$${info.costUsd < .01 ? "<0.01" : info.costUsd.toFixed(2)}` : "";
|
|
556
|
+
return [`${info.changed.length === 0 ? "cache expired" : "cache break"} ↑${rebilled} tokens${cost}`, ...info.changed.flatMap((dim) => {
|
|
557
|
+
const detail = info.details[dim];
|
|
558
|
+
if (typeof detail !== "string" || detail.length === 0) return [`${dim} changed`];
|
|
559
|
+
const [first, ...rest] = detail.split("\n");
|
|
560
|
+
return [`${dim}: ${first}`, ...rest];
|
|
561
|
+
})].join("\n");
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Provider-retry label — `retrying 2/5`, or `retrying 2/5 · child-3` when the
|
|
565
|
+
* stalled stream belongs to a subagent rather than the parent.
|
|
566
|
+
*
|
|
567
|
+
* Naming the source matters: with subagents in flight, an unattributed
|
|
568
|
+
* "retrying" leaves the user unable to tell whether their own turn is stuck or
|
|
569
|
+
* a delegated worker is.
|
|
570
|
+
*/
|
|
571
|
+
function retryLabel(info) {
|
|
572
|
+
const suffix = info.source === "parent" ? "" : ` · ${info.source}`;
|
|
573
|
+
return `retrying ${info.attempt}/${info.max}${suffix}`;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Footer label while a compaction summary is being generated:
|
|
577
|
+
* `Compacting...`, then `~12k` once the loop reports a token estimate, then
|
|
578
|
+
* ` · 8s` once a second has passed. Same rule on every surface so a
|
|
579
|
+
* minutes-long compaction of a large transcript reads as progress — not a
|
|
580
|
+
* hang — identically in the TUI and the GUI.
|
|
581
|
+
*
|
|
582
|
+
* `now` is injectable so callers driving a 1 Hz tick don't have to re-read
|
|
583
|
+
* the clock, and so tests are deterministic.
|
|
584
|
+
*/
|
|
585
|
+
/**
|
|
586
|
+
* The live run readout: `1m 15s · ↓1.2k · 41 tok/s`. Time runs from the
|
|
587
|
+
* run's start, the count covers the whole run (settled turns plus the one
|
|
588
|
+
* streaming), and the rate is the honest windowed figure from
|
|
589
|
+
* `stepStreamRate` (`null` until it has one). `↓~` marks a count that is
|
|
590
|
+
* still the character heuristic. A tool-only stretch shows just the time.
|
|
591
|
+
*
|
|
592
|
+
* `detail` drops the least useful part first when space runs out:
|
|
593
|
+
* `'full'` → `'count'` (no rate) → `'time'`.
|
|
594
|
+
*/
|
|
595
|
+
function runStatusLabel(run, detail = "full") {
|
|
596
|
+
const parts = [formatElapsed(Math.max(0, Math.floor(run.elapsedSec)))];
|
|
597
|
+
if (detail !== "time" && run.outputTokens > 0) parts.push(`↓${run.estimated ? "~" : ""}${fmtTokens(run.outputTokens)}`);
|
|
598
|
+
if (detail === "full" && run.outputTokens > 0 && run.rate !== null && run.rate > 0) parts.push(`${run.rate} tok/s`);
|
|
599
|
+
return parts.join(" · ");
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Live counter for one streaming turn: `↓1.2k`, `↓~1.2k` for the character
|
|
603
|
+
* heuristic, `↓1.2k · 41 tok/s` when an honest rate is passed in (see
|
|
604
|
+
* `stepStreamRate`; dividing tokens by time since the request counted the
|
|
605
|
+
* wait for the first token and the estimate correction). Empty until the
|
|
606
|
+
* first token.
|
|
607
|
+
*/
|
|
608
|
+
function streamingLabel(progress, opts = {}) {
|
|
609
|
+
if (!progress || progress.outputTokens <= 0) return "";
|
|
610
|
+
const count = `↓${progress.estimated ? "~" : ""}${fmtTokens(progress.outputTokens)}`;
|
|
611
|
+
return opts.rate ? `${count} · ${opts.rate} tok/s` : count;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Session cost while a turn streams: settled figure plus the running estimate
|
|
615
|
+
* for the in-flight turn. `estimated` tells the surface to render it dimmed
|
|
616
|
+
* (the number will move, then snap to the settled value at turn end). `null`
|
|
617
|
+
* when there is nothing to show — never `$0.00`.
|
|
618
|
+
*/
|
|
619
|
+
function liveSessionCost(settledUsd, progress) {
|
|
620
|
+
const live = progress?.estCostUsd ?? 0;
|
|
621
|
+
const total = settledUsd + live;
|
|
622
|
+
if (total <= 0) return null;
|
|
623
|
+
return {
|
|
624
|
+
usd: total,
|
|
625
|
+
estimated: live > 0
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function compactingLabel(estTokens, startedAt, now = Date.now()) {
|
|
629
|
+
const base = "Compacting...";
|
|
630
|
+
const elapsedMs = typeof startedAt === "number" ? now - startedAt : 0;
|
|
631
|
+
const elapsed = elapsedMs >= 1e3 ? ` · ${formatDuration(elapsedMs)}` : "";
|
|
632
|
+
return typeof estTokens === "number" && estTokens > 0 ? `${base} ~${fmtTokens(estTokens)}${elapsed}` : `${base}${elapsed}`;
|
|
633
|
+
}
|
|
634
|
+
//#endregion
|
|
635
|
+
//#region src/chat/completion-chats.ts
|
|
636
|
+
const CHATS_TRIGGER = "#";
|
|
637
|
+
const DEFAULT_RESULT_LIMIT$1 = 50;
|
|
638
|
+
const MIN_TOKEN_LENGTH = 6;
|
|
639
|
+
function normalizeId(id) {
|
|
640
|
+
return id.replace(/-/g, "").toLowerCase();
|
|
641
|
+
}
|
|
642
|
+
function projectLabel(projectRoot) {
|
|
643
|
+
if (!projectRoot) return "";
|
|
644
|
+
const parts = projectRoot.split(/[\\/]/).filter(Boolean);
|
|
645
|
+
return parts[parts.length - 1] ?? projectRoot;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Per-catalog memo for `tokenFor`, keyed on catalog reference identity.
|
|
649
|
+
* Token derivation is O(catalog) per id, and both `suggest()` (every
|
|
650
|
+
* keystroke) and `parseReferences()` (every submit/highlight pass) need
|
|
651
|
+
* a token for EVERY entry — quadratic over catalogs that can reach
|
|
652
|
+
* thousands of sessions. The catalog getters return a stable array until
|
|
653
|
+
* a refresh swaps it, so a WeakMap invalidates exactly when it should.
|
|
654
|
+
*/
|
|
655
|
+
const tokensByCatalog = /* @__PURE__ */ new WeakMap();
|
|
656
|
+
function catalogTokens(catalog) {
|
|
657
|
+
let cached = tokensByCatalog.get(catalog);
|
|
658
|
+
if (!cached) {
|
|
659
|
+
cached = {
|
|
660
|
+
normalizedIds: catalog.map((entry) => normalizeId(entry.id)),
|
|
661
|
+
tokens: /* @__PURE__ */ new Map()
|
|
662
|
+
};
|
|
663
|
+
tokensByCatalog.set(catalog, cached);
|
|
664
|
+
}
|
|
665
|
+
return cached;
|
|
666
|
+
}
|
|
667
|
+
function tokenFor(id, catalog) {
|
|
668
|
+
const { normalizedIds, tokens } = catalogTokens(catalog);
|
|
669
|
+
const known = tokens.get(id);
|
|
670
|
+
if (known !== void 0) return known;
|
|
671
|
+
const normalized = normalizeId(id);
|
|
672
|
+
let result;
|
|
673
|
+
for (let len = Math.min(MIN_TOKEN_LENGTH, normalized.length); len <= normalized.length; len++) {
|
|
674
|
+
const token = normalized.slice(0, len);
|
|
675
|
+
if (normalizedIds.filter((candidate) => candidate.startsWith(token)).length === 1) {
|
|
676
|
+
result = token;
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
result ??= normalized || shortId(id);
|
|
681
|
+
tokens.set(id, result);
|
|
682
|
+
return result;
|
|
683
|
+
}
|
|
684
|
+
function stripTrailingPunctuation(candidate) {
|
|
685
|
+
return candidate.replace(/[.,;:)\]}!?]+$/, "");
|
|
686
|
+
}
|
|
687
|
+
function descriptionFor(entry, token, scope) {
|
|
688
|
+
const bits = [
|
|
689
|
+
`#${token}`,
|
|
690
|
+
ageString(entry.updatedAt),
|
|
691
|
+
`${entry.userMessageCount} msg${entry.userMessageCount === 1 ? "" : "s"}`
|
|
692
|
+
];
|
|
693
|
+
if ((entry.artifactCount ?? 0) > 0) bits.push(`${entry.artifactCount} artifact${entry.artifactCount === 1 ? "" : "s"}`);
|
|
694
|
+
const project = projectLabel(entry.projectRoot);
|
|
695
|
+
if (scope === "all" && project) bits.push(project);
|
|
696
|
+
return bits.join(" · ");
|
|
697
|
+
}
|
|
698
|
+
function scoreChats(catalog, tokenCatalog, query, limit, scope) {
|
|
699
|
+
const q = query.trim().toLowerCase();
|
|
700
|
+
const rows = catalog.map((entry) => ({
|
|
701
|
+
entry,
|
|
702
|
+
token: tokenFor(entry.id, tokenCatalog),
|
|
703
|
+
project: projectLabel(entry.projectRoot)
|
|
704
|
+
}));
|
|
705
|
+
const fuzzyHits = q.length > 0 ? new Set(new Fzf(rows, {
|
|
706
|
+
selector: (r) => `${r.entry.title} ${r.token} ${r.project}`,
|
|
707
|
+
casing: "case-insensitive"
|
|
708
|
+
}).find(q).map((r) => r.item)) : null;
|
|
709
|
+
const scored = [];
|
|
710
|
+
for (const row of rows) {
|
|
711
|
+
const title = row.entry.title.toLowerCase();
|
|
712
|
+
const token = row.token.toLowerCase();
|
|
713
|
+
const project = row.project.toLowerCase();
|
|
714
|
+
if (q.length === 0) {
|
|
715
|
+
scored.push({
|
|
716
|
+
entry: row.entry,
|
|
717
|
+
token: row.token,
|
|
718
|
+
rank: 5
|
|
719
|
+
});
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
if (token.startsWith(q)) {
|
|
723
|
+
scored.push({
|
|
724
|
+
entry: row.entry,
|
|
725
|
+
token: row.token,
|
|
726
|
+
rank: 0
|
|
727
|
+
});
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (title.startsWith(q)) {
|
|
731
|
+
scored.push({
|
|
732
|
+
entry: row.entry,
|
|
733
|
+
token: row.token,
|
|
734
|
+
rank: 1
|
|
735
|
+
});
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (title.includes(q)) {
|
|
739
|
+
scored.push({
|
|
740
|
+
entry: row.entry,
|
|
741
|
+
token: row.token,
|
|
742
|
+
rank: 2
|
|
743
|
+
});
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (project.includes(q)) {
|
|
747
|
+
scored.push({
|
|
748
|
+
entry: row.entry,
|
|
749
|
+
token: row.token,
|
|
750
|
+
rank: 3
|
|
751
|
+
});
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
if (fuzzyHits?.has(row)) scored.push({
|
|
755
|
+
entry: row.entry,
|
|
756
|
+
token: row.token,
|
|
757
|
+
rank: 4
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
scored.sort((a, b) => {
|
|
761
|
+
if (a.rank !== b.rank) return a.rank - b.rank;
|
|
762
|
+
return b.entry.updatedAt - a.entry.updatedAt;
|
|
763
|
+
});
|
|
764
|
+
return scored.slice(0, limit).map(({ entry, token }) => ({
|
|
765
|
+
id: token,
|
|
766
|
+
label: entry.title,
|
|
767
|
+
description: descriptionFor(entry, token, scope),
|
|
768
|
+
insertText: `#${token} `,
|
|
769
|
+
data: {
|
|
770
|
+
entry,
|
|
771
|
+
token
|
|
772
|
+
}
|
|
773
|
+
}));
|
|
774
|
+
}
|
|
775
|
+
function createChatsCompletionProvider(opts) {
|
|
776
|
+
const limit = opts.limit ?? DEFAULT_RESULT_LIMIT$1;
|
|
777
|
+
const scope = () => opts.getScope?.() ?? "project";
|
|
778
|
+
return {
|
|
779
|
+
id: "chats",
|
|
780
|
+
trigger: "#",
|
|
781
|
+
label: "Chats",
|
|
782
|
+
suggest(query) {
|
|
783
|
+
const current = opts.getCatalog();
|
|
784
|
+
const tokenCatalog = opts.getReferenceCatalog?.() ?? current;
|
|
785
|
+
const effectiveTokenCatalog = tokenCatalog.length > 0 ? tokenCatalog : current;
|
|
786
|
+
if (current.length > 0) return scoreChats(current, effectiveTokenCatalog, query, limit, scope());
|
|
787
|
+
if (opts.ensureCatalog) return opts.ensureCatalog().then((loaded) => {
|
|
788
|
+
const refreshed = opts.getReferenceCatalog?.() ?? loaded;
|
|
789
|
+
return scoreChats(loaded, refreshed.length > 0 ? refreshed : loaded, query, limit, scope());
|
|
790
|
+
});
|
|
791
|
+
return [];
|
|
792
|
+
},
|
|
793
|
+
parseReferences(text, _ctx) {
|
|
794
|
+
const catalog = opts.getReferenceCatalog?.() ?? opts.getCatalog();
|
|
795
|
+
if (catalog.length === 0) return [];
|
|
796
|
+
const byToken = /* @__PURE__ */ new Map();
|
|
797
|
+
for (const entry of catalog) {
|
|
798
|
+
const token = tokenFor(entry.id, catalog);
|
|
799
|
+
byToken.set(token, {
|
|
800
|
+
entry,
|
|
801
|
+
token
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
const refs = [];
|
|
805
|
+
for (const m of text.matchAll(/(^|\s)#(\S+)/g)) {
|
|
806
|
+
const candidate = stripTrailingPunctuation(m[2]);
|
|
807
|
+
const data = byToken.get(candidate.toLowerCase());
|
|
808
|
+
if (!data) continue;
|
|
809
|
+
const start = m.index + m[1].length;
|
|
810
|
+
refs.push({
|
|
811
|
+
providerId: "chats",
|
|
812
|
+
start,
|
|
813
|
+
end: start + 1 + candidate.length,
|
|
814
|
+
itemId: data.entry.id,
|
|
815
|
+
data
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
return refs;
|
|
819
|
+
}
|
|
820
|
+
};
|
|
1790
821
|
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
* `hideSubagentOutput: true` — selecting them would highlight nothing.
|
|
1800
|
-
* - **Result-only turns** — see {@link turnSelectionOwnership}. These get
|
|
1801
|
-
* coalesced into the assistant turn that emitted their tool_calls.
|
|
1802
|
-
* - **Settings-hidden turns** (when `settings` is supplied). A turn whose
|
|
1803
|
-
* every event fails {@link isVisible} would render no rows — landing
|
|
1804
|
-
* the cursor there hides it from the user entirely. The check is opt-
|
|
1805
|
-
* in so SDK callers without a Settings object keep the legacy
|
|
1806
|
-
* "everything visible" behavior.
|
|
1807
|
-
*
|
|
1808
|
-
* Synthetic events (separator, spawn-start, spawn-end) have no `turnId` and
|
|
1809
|
-
* are skipped naturally.
|
|
1810
|
-
*/
|
|
1811
|
-
function selectableTurnIds(events, settings, visible) {
|
|
1812
|
-
const ownership = turnSelectionOwnership(events);
|
|
1813
|
-
const visibleCount = settings ? /* @__PURE__ */ new Map() : null;
|
|
1814
|
-
if (settings && visibleCount) for (const e of events) {
|
|
1815
|
-
if (!e.turnId || e.childId) continue;
|
|
1816
|
-
if (!(visible ? visible(e) : isVisible(e, settings))) continue;
|
|
1817
|
-
visibleCount.set(e.turnId, (visibleCount.get(e.turnId) ?? 0) + 1);
|
|
822
|
+
function uniqueChatIdsFromReferences(references) {
|
|
823
|
+
const out = [];
|
|
824
|
+
const seen = /* @__PURE__ */ new Set();
|
|
825
|
+
for (const ref of references) {
|
|
826
|
+
if (ref.providerId !== "chats") continue;
|
|
827
|
+
if (seen.has(ref.itemId)) continue;
|
|
828
|
+
seen.add(ref.itemId);
|
|
829
|
+
out.push(ref.itemId);
|
|
1818
830
|
}
|
|
831
|
+
return out;
|
|
832
|
+
}
|
|
833
|
+
function chatReferenceTokensFromText(text) {
|
|
834
|
+
const out = [];
|
|
1819
835
|
const seen = /* @__PURE__ */ new Set();
|
|
1820
|
-
const
|
|
1821
|
-
|
|
1822
|
-
if (!
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
if (ownership.has(e.turnId)) continue;
|
|
1826
|
-
if (visibleCount && (visibleCount.get(e.turnId) ?? 0) === 0) continue;
|
|
1827
|
-
seen.add(e.turnId);
|
|
1828
|
-
ordered.push(e.turnId);
|
|
836
|
+
for (const m of text.matchAll(/(^|\s)#(\S+)/g)) {
|
|
837
|
+
const token = stripTrailingPunctuation(m[2]).toLowerCase();
|
|
838
|
+
if (!token || seen.has(token)) continue;
|
|
839
|
+
seen.add(token);
|
|
840
|
+
out.push(token);
|
|
1829
841
|
}
|
|
1830
|
-
return
|
|
842
|
+
return out;
|
|
1831
843
|
}
|
|
1832
844
|
//#endregion
|
|
845
|
+
//#region src/chat/browser/protocol.ts
|
|
846
|
+
const BROWSER_OPS = [
|
|
847
|
+
"open",
|
|
848
|
+
"navigate",
|
|
849
|
+
"snapshot",
|
|
850
|
+
"screenshot",
|
|
851
|
+
"click",
|
|
852
|
+
"type",
|
|
853
|
+
"press",
|
|
854
|
+
"scroll",
|
|
855
|
+
"wait_for",
|
|
856
|
+
"evaluate",
|
|
857
|
+
"console"
|
|
858
|
+
];
|
|
859
|
+
var BrowserOpError = class extends Error {
|
|
860
|
+
code;
|
|
861
|
+
constructor(code, message) {
|
|
862
|
+
super(message);
|
|
863
|
+
this.code = code;
|
|
864
|
+
this.name = "BrowserOpError";
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
/** Default and ceiling for an action's own wait (element to become actionable, text to appear). */
|
|
868
|
+
const BROWSER_ACTION_TIMEOUT_MS = 5e3;
|
|
869
|
+
const BROWSER_WAIT_TIMEOUT_MS = 1e4;
|
|
870
|
+
const BROWSER_MAX_WAIT_MS = 3e4;
|
|
871
|
+
/** How long the host waits for a page to mount and register before `open` gives up. */
|
|
872
|
+
const BROWSER_OPEN_MOUNT_MS = 15e3;
|
|
873
|
+
/** Transport budget on top of the action's own wait. */
|
|
874
|
+
const BROWSER_REQUEST_SLACK_MS = 5e3;
|
|
875
|
+
const BROWSER_EVALUATE_MAX_BYTES = 65536;
|
|
876
|
+
const BROWSER_OUTLINE_MAX_ELEMENTS = 250;
|
|
877
|
+
const BROWSER_TEXT_MAX_CHARS = 8e3;
|
|
878
|
+
//#endregion
|
|
879
|
+
//#region src/chat/terminal/protocol.ts
|
|
880
|
+
/** Characters a client may be behind before the host stops sending to it. */
|
|
881
|
+
const TERMINAL_OUTPUT_WINDOW = 262144;
|
|
882
|
+
/** History kept per terminal for new views (characters). */
|
|
883
|
+
const TERMINAL_HISTORY_MAX_CHARS = 4194304;
|
|
884
|
+
/** Plain-text lines kept per terminal for the agent. */
|
|
885
|
+
const TERMINAL_TEXT_MAX_LINES = 5e3;
|
|
886
|
+
/** Most the agent gets from one read (characters). */
|
|
887
|
+
const TERMINAL_READ_MAX_CHARS = 16384;
|
|
888
|
+
/** Terminals a session may have open at once. */
|
|
889
|
+
const TERMINAL_MAX_PER_SESSION = 12;
|
|
890
|
+
var TerminalError = class extends Error {
|
|
891
|
+
code;
|
|
892
|
+
constructor(code, message) {
|
|
893
|
+
super(message);
|
|
894
|
+
this.code = code;
|
|
895
|
+
this.name = "TerminalError";
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
//#endregion
|
|
1833
899
|
//#region src/chat/color-gradient.ts
|
|
1834
900
|
/** Parse `#rrggbb` (case-insensitive) into `[r, g, b]` 0–255 integers. */
|
|
1835
901
|
function parseHex(hex) {
|
|
@@ -2349,326 +1415,43 @@ function createSkillsCompletionProvider(opts) {
|
|
|
2349
1415
|
});
|
|
2350
1416
|
}
|
|
2351
1417
|
return refs;
|
|
2352
|
-
}
|
|
2353
|
-
};
|
|
2354
|
-
}
|
|
2355
|
-
/**
|
|
2356
|
-
* Walk a parsed prompt for skill references and return the deduplicated
|
|
2357
|
-
* list of skill names — input to `agent.activateSkill(name)` calls on
|
|
2358
|
-
* submit.
|
|
2359
|
-
*/
|
|
2360
|
-
function uniqueSkillNamesFromReferences(references) {
|
|
2361
|
-
const out = [];
|
|
2362
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2363
|
-
for (const ref of references) {
|
|
2364
|
-
if (ref.providerId !== "skills") continue;
|
|
2365
|
-
if (seen.has(ref.itemId)) continue;
|
|
2366
|
-
seen.add(ref.itemId);
|
|
2367
|
-
out.push(ref.itemId);
|
|
2368
|
-
}
|
|
2369
|
-
return out;
|
|
2370
|
-
}
|
|
2371
|
-
//#endregion
|
|
2372
|
-
//#region src/chat/model-chain.ts
|
|
2373
|
-
/** Identity of a chain entry — the pair, not object identity. */
|
|
2374
|
-
function chainEntryKey(entry) {
|
|
2375
|
-
return `${entry.provider}\u0000${entry.model}`;
|
|
2376
|
-
}
|
|
2377
|
-
/** Toggle `entry`: absent → appended, present → removed. Pure. */
|
|
2378
|
-
function toggleKeyed(chain, entry, keyOf) {
|
|
2379
|
-
const key = keyOf(entry);
|
|
2380
|
-
const idx = chain.findIndex((e) => keyOf(e) === key);
|
|
2381
|
-
if (idx === -1) return [...chain, entry];
|
|
2382
|
-
return [...chain.slice(0, idx), ...chain.slice(idx + 1)];
|
|
2383
|
-
}
|
|
2384
|
-
/**
|
|
2385
|
-
* Make `entry` the primary (position 0), keeping the rest in order. Inserts
|
|
2386
|
-
* when absent, promotes when present; no-op when it already leads.
|
|
2387
|
-
*
|
|
2388
|
-
* This is `↵` in the picker. It exists because toggling APPENDS: with a chain
|
|
2389
|
-
* of `[current]`, toggling a new model produced `[current, new]` — the pick
|
|
2390
|
-
* became a fallback and the active model never changed. "Highlight and ↵"
|
|
2391
|
-
* means "use this one", so that gesture promotes; building a fallback list is
|
|
2392
|
-
* the secondary gesture (`⇥`) and keeps toggle semantics.
|
|
2393
|
-
*/
|
|
2394
|
-
function pickPrimaryKeyed(chain, entry, keyOf) {
|
|
2395
|
-
const key = keyOf(entry);
|
|
2396
|
-
return [entry, ...chain.filter((e) => keyOf(e) !== key)];
|
|
2397
|
-
}
|
|
2398
|
-
/** 1-based position of `key` in the chain, or `null` when absent. */
|
|
2399
|
-
function positionKeyed(chain, key, keyOf) {
|
|
2400
|
-
const idx = chain.findIndex((e) => keyOf(e) === key);
|
|
2401
|
-
return idx === -1 ? null : idx + 1;
|
|
2402
|
-
}
|
|
2403
|
-
/** Same entries in the same order. */
|
|
2404
|
-
function chainsEqualKeyed(a, b, keyOf) {
|
|
2405
|
-
return a.length === b.length && a.every((e, i) => keyOf(e) === keyOf(b[i]));
|
|
2406
|
-
}
|
|
2407
|
-
/** Move the entry with `key` by one priority slot; boundary or missing = identity. */
|
|
2408
|
-
function reorderKeyed(chain, key, delta, keyOf) {
|
|
2409
|
-
const from = chain.findIndex((e) => keyOf(e) === key);
|
|
2410
|
-
const to = from + delta;
|
|
2411
|
-
if (from < 0 || to < 0 || to >= chain.length) return chain;
|
|
2412
|
-
const next = [...chain];
|
|
2413
|
-
const [entry] = next.splice(from, 1);
|
|
2414
|
-
next.splice(to, 0, entry);
|
|
2415
|
-
return next;
|
|
2416
|
-
}
|
|
2417
|
-
function toggleChainEntry(chain, entry) {
|
|
2418
|
-
return toggleKeyed(chain, entry, chainEntryKey);
|
|
2419
|
-
}
|
|
2420
|
-
function pickChainPrimary(chain, entry) {
|
|
2421
|
-
return pickPrimaryKeyed(chain, entry, chainEntryKey);
|
|
2422
|
-
}
|
|
2423
|
-
/** 1-based chain position of a provider+model pair, or `null` when not in the chain. */
|
|
2424
|
-
function chainPositionOf(chain, provider, model) {
|
|
2425
|
-
return positionKeyed(chain, chainEntryKey({
|
|
2426
|
-
provider,
|
|
2427
|
-
model
|
|
2428
|
-
}), chainEntryKey);
|
|
2429
|
-
}
|
|
2430
|
-
function chainsEqual(a, b) {
|
|
2431
|
-
return chainsEqualKeyed(a, b, chainEntryKey);
|
|
2432
|
-
}
|
|
2433
|
-
/**
|
|
2434
|
-
* Did the SET of provider keys change between two chains? The agent is
|
|
2435
|
-
* built with one provider instance per distinct chain key, so a set
|
|
2436
|
-
* change requires a session re-activation (rebuild); a pure model /
|
|
2437
|
-
* order change within the same providers does not.
|
|
2438
|
-
*/
|
|
2439
|
-
function providerKeySetChanged(prev, next) {
|
|
2440
|
-
const prevKeys = new Set(prev.map((e) => e.provider));
|
|
2441
|
-
const nextKeys = new Set(next.map((e) => e.provider));
|
|
2442
|
-
if (prevKeys.size !== nextKeys.size) return true;
|
|
2443
|
-
for (const k of nextKeys) if (!prevKeys.has(k)) return true;
|
|
2444
|
-
return false;
|
|
2445
|
-
}
|
|
2446
|
-
/**
|
|
2447
|
-
* Assemble the `agent.run` `model` option from a chain.
|
|
2448
|
-
*
|
|
2449
|
-
* - Single-entry chain → the bare model id string (byte-identical to the
|
|
2450
|
-
* pre-chain behavior).
|
|
2451
|
-
* - Multi-entry chain → pinned `{ provider, model }` targets, where
|
|
2452
|
-
* `provider` is the runtime `Provider.name` resolved via
|
|
2453
|
-
* `providerNameFor` (chain entries carry chat-level `ProviderKey`s,
|
|
2454
|
-
* which need not equal the instance names the agent loop matches on).
|
|
2455
|
-
*
|
|
2456
|
-
* Throws only on an empty chain — a programmer error, since chains
|
|
2457
|
-
* always carry a primary. Stale or unresolvable entries degrade
|
|
2458
|
-
* defensively instead (never throw, never produce an entry the agent
|
|
2459
|
-
* would reject):
|
|
2460
|
-
* - Entries whose key has no resolvable name (provider not constructed
|
|
2461
|
-
* into the current agent) are dropped.
|
|
2462
|
-
* - If the PRIMARY itself doesn't resolve, or fewer than two entries
|
|
2463
|
-
* survive, fall back to the bare primary model string.
|
|
2464
|
-
*
|
|
2465
|
-
* `contextWindowFor` (optional) stamps each target with its model's
|
|
2466
|
-
* context window from the host catalog, so the loop re-bases the
|
|
2467
|
-
* auto-compact trigger when a fallback advances onto it (the host's
|
|
2468
|
-
* explicit window is sized for the PRIMARY otherwise).
|
|
2469
|
-
*/
|
|
2470
|
-
function buildRunModel(chain, providerNameFor, contextWindowFor) {
|
|
2471
|
-
const primary = chain[0];
|
|
2472
|
-
if (!primary) throw new Error("buildRunModel: chain must not be empty");
|
|
2473
|
-
if (chain.length === 1 || providerNameFor(primary.provider) === void 0) return primary.model;
|
|
2474
|
-
const resolved = [];
|
|
2475
|
-
for (const entry of chain) {
|
|
2476
|
-
const name = providerNameFor(entry.provider);
|
|
2477
|
-
if (name === void 0) continue;
|
|
2478
|
-
const contextWindow = contextWindowFor?.(entry);
|
|
2479
|
-
resolved.push({
|
|
2480
|
-
provider: name,
|
|
2481
|
-
model: entry.model,
|
|
2482
|
-
...typeof contextWindow === "number" && contextWindow > 0 ? { contextWindow } : {}
|
|
2483
|
-
});
|
|
2484
|
-
}
|
|
2485
|
-
return resolved.length > 1 ? resolved : primary.model;
|
|
2486
|
-
}
|
|
2487
|
-
/**
|
|
2488
|
-
* Validate a persisted chain read back from `state.json`. The state file
|
|
2489
|
-
* is user-editable JSON, so the value can be arbitrarily malformed —
|
|
2490
|
-
* return only the well-shaped `{ provider, model }` entries (both
|
|
2491
|
-
* non-empty strings), or `[]` for anything else. Never throws.
|
|
2492
|
-
*/
|
|
2493
|
-
function sanitizeModelChain(value) {
|
|
2494
|
-
if (!Array.isArray(value)) return [];
|
|
2495
|
-
const entries = [];
|
|
2496
|
-
for (const item of value) {
|
|
2497
|
-
if (typeof item !== "object" || item === null || !("provider" in item) || !("model" in item)) continue;
|
|
2498
|
-
const { provider, model } = item;
|
|
2499
|
-
if (typeof provider === "string" && provider.length > 0 && typeof model === "string" && model.length > 0) entries.push({
|
|
2500
|
-
provider,
|
|
2501
|
-
model
|
|
2502
|
-
});
|
|
2503
|
-
}
|
|
2504
|
-
return entries;
|
|
2505
|
-
}
|
|
2506
|
-
/**
|
|
2507
|
-
* Restore the fallback tail of a persisted chain for a resolved primary
|
|
2508
|
-
* pair. Returns entries 1+ only when the persisted chain's entry 0
|
|
2509
|
-
* matches `primary` — a divergent chain belongs to a different selection
|
|
2510
|
-
* (e.g. a `--model` launch override) and silently attaching it would
|
|
2511
|
-
* surprise. Entries whose provider isn't registered, entries equal to
|
|
2512
|
-
* the primary, and exact duplicates drop. Accepts the raw persisted
|
|
2513
|
-
* value (`state.json` is user-editable) — shape validation included.
|
|
2514
|
-
*/
|
|
2515
|
-
function restoreChainFallbacks(saved, primary, isRegistered) {
|
|
2516
|
-
const chain = sanitizeModelChain(saved);
|
|
2517
|
-
const head = chain[0];
|
|
2518
|
-
if (chain.length < 2 || !head || head.provider !== primary.provider || head.model !== primary.model) return [];
|
|
2519
|
-
return filterChainFallbacks(chain.slice(1), primary, isRegistered);
|
|
2520
|
-
}
|
|
2521
|
-
/**
|
|
2522
|
-
* Filter candidate fallback entries against a primary pair, preserving
|
|
2523
|
-
* picked order: entries whose provider `isAvailable` rejects, entries
|
|
2524
|
-
* equal to the primary, and exact duplicates drop. Shared by the
|
|
2525
|
-
* picker's apply path (live-auth availability) and persisted-chain
|
|
2526
|
-
* restore (registry availability).
|
|
2527
|
-
*/
|
|
2528
|
-
function filterChainFallbacks(entries, primary, isAvailable) {
|
|
2529
|
-
const fallbacks = [];
|
|
2530
|
-
for (const entry of entries) {
|
|
2531
|
-
if (!isAvailable(entry.provider)) continue;
|
|
2532
|
-
if (entry.provider === primary.provider && entry.model === primary.model) continue;
|
|
2533
|
-
if (fallbacks.some((f) => f.provider === entry.provider && f.model === entry.model)) continue;
|
|
2534
|
-
fallbacks.push({
|
|
2535
|
-
provider: entry.provider,
|
|
2536
|
-
model: entry.model
|
|
2537
|
-
});
|
|
2538
|
-
}
|
|
2539
|
-
return fallbacks;
|
|
2540
|
-
}
|
|
2541
|
-
//#endregion
|
|
2542
|
-
//#region src/chat/daemon-health.ts
|
|
2543
|
-
const DAEMON_HEALTH_GLYPH = "●";
|
|
2544
|
-
const DAEMON_HEALTH_STYLE = {
|
|
2545
|
-
connected: {
|
|
2546
|
-
tone: "accent",
|
|
2547
|
-
label: "daemon connected"
|
|
2548
|
-
},
|
|
2549
|
-
connecting: {
|
|
2550
|
-
tone: "warn",
|
|
2551
|
-
label: "connecting to daemon…"
|
|
2552
|
-
},
|
|
2553
|
-
disconnected: {
|
|
2554
|
-
tone: "error",
|
|
2555
|
-
label: "daemon unreachable"
|
|
2556
|
-
}
|
|
2557
|
-
};
|
|
2558
|
-
//#endregion
|
|
2559
|
-
//#region src/chat/footer-chips.ts
|
|
2560
|
-
/**
|
|
2561
|
-
* Abbreviate a re-billed token count: `1200` → `1.2k`, `800` → `800`.
|
|
2562
|
-
* Thousands only — a cache prefix large enough to need `M` would be past any
|
|
2563
|
-
* provider's window.
|
|
2564
|
-
*/
|
|
2565
|
-
function formatTokens(tokens) {
|
|
2566
|
-
return tokens >= 1e3 ? `${(tokens / 1e3).toFixed(1)}k` : String(tokens);
|
|
2567
|
-
}
|
|
2568
|
-
/**
|
|
2569
|
-
* Build the chip for a cache break.
|
|
2570
|
-
*
|
|
2571
|
-
* `costUsd` is OPTIONAL and omitted entirely on a rate miss — never rendered
|
|
2572
|
-
* as `$0.00`. A fabricated zero would read as "this was free", which is the
|
|
2573
|
-
* opposite of what the chip exists to say. When present it is prefixed `~`
|
|
2574
|
-
* because it is an estimate (registry rates × re-billed prefix), and clamped
|
|
2575
|
-
* to `<0.01` rather than rounding a real charge down to nothing.
|
|
2576
|
-
*/
|
|
2577
|
-
function cacheBreakChip(info) {
|
|
2578
|
-
const rebilled = formatTokens(info.rebilledTokens);
|
|
2579
|
-
const cost = info.costUsd !== void 0 ? ` · ~$${info.costUsd < .01 ? "<0.01" : info.costUsd.toFixed(2)}` : "";
|
|
2580
|
-
if (info.changed.length === 0) return {
|
|
2581
|
-
tone: "dim",
|
|
2582
|
-
glyph: "⚡",
|
|
2583
|
-
label: `cache expired · +${rebilled} tok${cost}`
|
|
2584
|
-
};
|
|
2585
|
-
return {
|
|
2586
|
-
tone: "warn",
|
|
2587
|
-
glyph: "⚡",
|
|
2588
|
-
label: `cache break · ${info.changed.join(",")} · +${rebilled} tok${cost}`
|
|
2589
|
-
};
|
|
2590
|
-
}
|
|
2591
|
-
/**
|
|
2592
|
-
* Transcript-row text for a persisted cache-break notice. Multi-line, one
|
|
2593
|
-
* fact per line, so both hosts can show a short headline and fold or list
|
|
2594
|
-
* the rest:
|
|
2595
|
-
*
|
|
2596
|
-
* cache break ↑116.9k tokens ~$0.48
|
|
2597
|
-
* messagePrefix: message 16 of 41→43 rewritten
|
|
2598
|
-
* likely compaction or history trimming
|
|
2599
|
-
*
|
|
2600
|
-
* The footer chip stays terse; the row carries the culprit so a break is
|
|
2601
|
-
* diagnosable from the transcript. Only the headline on a TTL / eviction
|
|
2602
|
-
* break (nothing was done wrong).
|
|
2603
|
-
*/
|
|
2604
|
-
function cacheBreakNoticeText(info) {
|
|
2605
|
-
const rebilled = formatTokens(info.rebilledTokens);
|
|
2606
|
-
const cost = info.costUsd !== void 0 ? ` ~$${info.costUsd < .01 ? "<0.01" : info.costUsd.toFixed(2)}` : "";
|
|
2607
|
-
return [`${info.changed.length === 0 ? "cache expired" : "cache break"} ↑${rebilled} tokens${cost}`, ...info.changed.flatMap((dim) => {
|
|
2608
|
-
const detail = info.details[dim];
|
|
2609
|
-
if (typeof detail !== "string" || detail.length === 0) return [`${dim} changed`];
|
|
2610
|
-
const [first, ...rest] = detail.split("\n");
|
|
2611
|
-
return [`${dim}: ${first}`, ...rest];
|
|
2612
|
-
})].join("\n");
|
|
2613
|
-
}
|
|
2614
|
-
/**
|
|
2615
|
-
* Provider-retry label — `retrying 2/5`, or `retrying 2/5 · child-3` when the
|
|
2616
|
-
* stalled stream belongs to a subagent rather than the parent.
|
|
2617
|
-
*
|
|
2618
|
-
* Naming the source matters: with subagents in flight, an unattributed
|
|
2619
|
-
* "retrying" leaves the user unable to tell whether their own turn is stuck or
|
|
2620
|
-
* a delegated worker is.
|
|
2621
|
-
*/
|
|
2622
|
-
function retryLabel(info) {
|
|
2623
|
-
const suffix = info.source === "parent" ? "" : ` · ${info.source}`;
|
|
2624
|
-
return `retrying ${info.attempt}/${info.max}${suffix}`;
|
|
2625
|
-
}
|
|
2626
|
-
/**
|
|
2627
|
-
* Footer label while a compaction summary is being generated:
|
|
2628
|
-
* `Compacting...`, then `~12k` once the loop reports a token estimate, then
|
|
2629
|
-
* ` · 8s` once a second has passed. Same rule on every surface so a
|
|
2630
|
-
* minutes-long compaction of a large transcript reads as progress — not a
|
|
2631
|
-
* hang — identically in the TUI and the GUI.
|
|
2632
|
-
*
|
|
2633
|
-
* `now` is injectable so callers driving a 1 Hz tick don't have to re-read
|
|
2634
|
-
* the clock, and so tests are deterministic.
|
|
2635
|
-
*/
|
|
2636
|
-
/**
|
|
2637
|
-
* Live counter for the parent turn currently streaming: `↓1.2k` when the
|
|
2638
|
-
* provider counted the tokens, `↓~1.2k` when it is the character heuristic.
|
|
2639
|
-
* Empty until the first token so a tool-only turn shows just the spinner.
|
|
2640
|
-
* Tokens-per-second appears after two seconds (`↓1.2k · 41 tok/s`) — earlier
|
|
2641
|
-
* the rate is dominated by time-to-first-token and would swing wildly.
|
|
2642
|
-
*/
|
|
2643
|
-
function streamingLabel(progress, opts = {}) {
|
|
2644
|
-
if (!progress || progress.outputTokens <= 0) return "";
|
|
2645
|
-
const count = `↓${progress.estimated ? "~" : ""}${fmtTokens(progress.outputTokens)}`;
|
|
2646
|
-
if (opts.rate === false || progress.elapsedMs < 2e3) return count;
|
|
2647
|
-
const perSecond = Math.round(progress.outputTokens / (progress.elapsedMs / 1e3));
|
|
2648
|
-
return perSecond > 0 ? `${count} · ${perSecond} tok/s` : count;
|
|
2649
|
-
}
|
|
2650
|
-
/**
|
|
2651
|
-
* Session cost while a turn streams: settled figure plus the running estimate
|
|
2652
|
-
* for the in-flight turn. `estimated` tells the surface to render it dimmed
|
|
2653
|
-
* (the number will move, then snap to the settled value at turn end). `null`
|
|
2654
|
-
* when there is nothing to show — never `$0.00`.
|
|
2655
|
-
*/
|
|
2656
|
-
function liveSessionCost(settledUsd, progress) {
|
|
2657
|
-
const live = progress?.estCostUsd ?? 0;
|
|
2658
|
-
const total = settledUsd + live;
|
|
2659
|
-
if (total <= 0) return null;
|
|
2660
|
-
return {
|
|
2661
|
-
usd: total,
|
|
2662
|
-
estimated: live > 0
|
|
2663
|
-
};
|
|
2664
|
-
}
|
|
2665
|
-
function compactingLabel(estTokens, startedAt, now = Date.now()) {
|
|
2666
|
-
const base = "Compacting...";
|
|
2667
|
-
const elapsedMs = typeof startedAt === "number" ? now - startedAt : 0;
|
|
2668
|
-
const elapsed = elapsedMs >= 1e3 ? ` · ${formatDuration(elapsedMs)}` : "";
|
|
2669
|
-
return typeof estTokens === "number" && estTokens > 0 ? `${base} ~${fmtTokens(estTokens)}${elapsed}` : `${base}${elapsed}`;
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Walk a parsed prompt for skill references and return the deduplicated
|
|
1423
|
+
* list of skill names — input to `agent.activateSkill(name)` calls on
|
|
1424
|
+
* submit.
|
|
1425
|
+
*/
|
|
1426
|
+
function uniqueSkillNamesFromReferences(references) {
|
|
1427
|
+
const out = [];
|
|
1428
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1429
|
+
for (const ref of references) {
|
|
1430
|
+
if (ref.providerId !== "skills") continue;
|
|
1431
|
+
if (seen.has(ref.itemId)) continue;
|
|
1432
|
+
seen.add(ref.itemId);
|
|
1433
|
+
out.push(ref.itemId);
|
|
1434
|
+
}
|
|
1435
|
+
return out;
|
|
2670
1436
|
}
|
|
2671
1437
|
//#endregion
|
|
1438
|
+
//#region src/chat/daemon-health.ts
|
|
1439
|
+
const DAEMON_HEALTH_GLYPH = "●";
|
|
1440
|
+
const DAEMON_HEALTH_STYLE = {
|
|
1441
|
+
connected: {
|
|
1442
|
+
tone: "accent",
|
|
1443
|
+
label: "daemon connected"
|
|
1444
|
+
},
|
|
1445
|
+
connecting: {
|
|
1446
|
+
tone: "warn",
|
|
1447
|
+
label: "connecting to daemon…"
|
|
1448
|
+
},
|
|
1449
|
+
disconnected: {
|
|
1450
|
+
tone: "error",
|
|
1451
|
+
label: "daemon unreachable"
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
//#endregion
|
|
2672
1455
|
//#region src/chat/markdown-segments.ts
|
|
2673
1456
|
/**
|
|
2674
1457
|
* Split markdown text into alternating prose / fenced-code segments.
|
|
@@ -3058,6 +1841,160 @@ function isSpaceOrBreak(ch) {
|
|
|
3058
1841
|
return ch === "\n" || ch === "\r" || ch === " " || ch === " ";
|
|
3059
1842
|
}
|
|
3060
1843
|
//#endregion
|
|
1844
|
+
//#region src/chat/mcp-allowlist.ts
|
|
1845
|
+
/**
|
|
1846
|
+
* Edits to the `enabledMcps` allowlist. Without an explicit list every
|
|
1847
|
+
* server is on, so there is nothing to change. Servers that merely appear
|
|
1848
|
+
* in a config file stay off once a list exists (they don't opt in
|
|
1849
|
+
* silently); a server the user adds in Settings is turned on with it.
|
|
1850
|
+
*/
|
|
1851
|
+
/** The list with `name` turned on, or `undefined` when it already is. */
|
|
1852
|
+
function enabledMcpsWith(list, name) {
|
|
1853
|
+
return list === void 0 || list.includes(name) ? void 0 : [...list, name];
|
|
1854
|
+
}
|
|
1855
|
+
/** The list without `name`, or `undefined` when it isn't listed. */
|
|
1856
|
+
function enabledMcpsWithout(list, name) {
|
|
1857
|
+
return list?.includes(name) ? list.filter((n) => n !== name) : void 0;
|
|
1858
|
+
}
|
|
1859
|
+
//#endregion
|
|
1860
|
+
//#region src/chat/mcp-auth-state.ts
|
|
1861
|
+
/**
|
|
1862
|
+
* The status words both apps show for a server, so the desktop app's badge
|
|
1863
|
+
* and the TUI's read the same. `idle` says nothing.
|
|
1864
|
+
*/
|
|
1865
|
+
const MCP_STATUS_LABEL = {
|
|
1866
|
+
"authed": "Signed in",
|
|
1867
|
+
"needs-auth": "Needs sign-in",
|
|
1868
|
+
"authorizing": "Signing in…",
|
|
1869
|
+
"error": "Sign-in failed",
|
|
1870
|
+
"bootstrap-error": "Couldn't connect",
|
|
1871
|
+
"key-refused": "Key refused"
|
|
1872
|
+
};
|
|
1873
|
+
/**
|
|
1874
|
+
* Apply one event to the state map. Pure, immutable — returns a new map
|
|
1875
|
+
* with only the affected key reassigned.
|
|
1876
|
+
*
|
|
1877
|
+
* State transitions are simple "last write wins"; we don't gate (e.g.) a
|
|
1878
|
+
* `login:start` from overriding `authorizing` because the loop layer
|
|
1879
|
+
* never doubles those up — interactive login is serialized in the TUI.
|
|
1880
|
+
*/
|
|
1881
|
+
function reduceMcpAuth(state, event) {
|
|
1882
|
+
const next = (status) => ({
|
|
1883
|
+
...state,
|
|
1884
|
+
[event.name]: status
|
|
1885
|
+
});
|
|
1886
|
+
switch (event.type) {
|
|
1887
|
+
case "auth-required": return next({
|
|
1888
|
+
kind: "needs-auth",
|
|
1889
|
+
reason: event.reason
|
|
1890
|
+
});
|
|
1891
|
+
case "auth-url": return next({
|
|
1892
|
+
kind: "authorizing",
|
|
1893
|
+
url: event.url
|
|
1894
|
+
});
|
|
1895
|
+
case "auth-success": return next({ kind: "authed" });
|
|
1896
|
+
case "auth-error": return next({
|
|
1897
|
+
kind: "error",
|
|
1898
|
+
error: event.error
|
|
1899
|
+
});
|
|
1900
|
+
case "bootstrap-error": return next({
|
|
1901
|
+
kind: "bootstrap-error",
|
|
1902
|
+
error: event.error
|
|
1903
|
+
});
|
|
1904
|
+
case "key-refused": return next({
|
|
1905
|
+
kind: "key-refused",
|
|
1906
|
+
error: event.error
|
|
1907
|
+
});
|
|
1908
|
+
case "connected":
|
|
1909
|
+
if (state[event.name]?.kind === "needs-auth" && state[event.name].reason === "refused") {
|
|
1910
|
+
const { [event.name]: _, ...rest } = state;
|
|
1911
|
+
return rest;
|
|
1912
|
+
}
|
|
1913
|
+
if (state[event.name]?.kind === "authorizing" || state[event.name]?.kind === "needs-auth" || state[event.name]?.kind === "authed") return next({ kind: "authed" });
|
|
1914
|
+
if (state[event.name]?.kind === "bootstrap-error" || state[event.name]?.kind === "key-refused") {
|
|
1915
|
+
const { [event.name]: _, ...rest } = state;
|
|
1916
|
+
return rest;
|
|
1917
|
+
}
|
|
1918
|
+
return state;
|
|
1919
|
+
case "login:start": return next({ kind: "authorizing" });
|
|
1920
|
+
case "login:cancel": return next({
|
|
1921
|
+
kind: "needs-auth",
|
|
1922
|
+
reason: "no-tokens"
|
|
1923
|
+
});
|
|
1924
|
+
case "logout":
|
|
1925
|
+
case "reset": {
|
|
1926
|
+
const { [event.name]: _, ...rest } = state;
|
|
1927
|
+
return rest;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* The status a failed connection leaves, from `classifyMcpTest`'s answer
|
|
1933
|
+
* (never called with `ok`: a connection that worked leaves no failure).
|
|
1934
|
+
* A server that asked for OAuth keeps "sign-in failed" for an unexplained
|
|
1935
|
+
* failure (its tokens are the likely cause, and sign-in stays offered).
|
|
1936
|
+
*/
|
|
1937
|
+
function mcpFailureEvent(name, outcome, oauthTagged) {
|
|
1938
|
+
if (outcome.kind === "needs-auth") return {
|
|
1939
|
+
type: "auth-required",
|
|
1940
|
+
name,
|
|
1941
|
+
reason: "refused"
|
|
1942
|
+
};
|
|
1943
|
+
if (outcome.kind === "token-rejected") return {
|
|
1944
|
+
type: "key-refused",
|
|
1945
|
+
name,
|
|
1946
|
+
error: outcome.message ?? ""
|
|
1947
|
+
};
|
|
1948
|
+
return oauthTagged ? {
|
|
1949
|
+
type: "auth-error",
|
|
1950
|
+
name,
|
|
1951
|
+
error: outcome.message ?? ""
|
|
1952
|
+
} : {
|
|
1953
|
+
type: "bootstrap-error",
|
|
1954
|
+
name,
|
|
1955
|
+
error: outcome.message ?? ""
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
/** Convenience getter — treats `undefined` as `idle`. */
|
|
1959
|
+
function getMcpAuthStatus(state, name) {
|
|
1960
|
+
return state[name] ?? { kind: "idle" };
|
|
1961
|
+
}
|
|
1962
|
+
//#endregion
|
|
1963
|
+
//#region src/chat/mcp-test-outcome.ts
|
|
1964
|
+
/** What to do about a refused key, under the "Key refused" headline. */
|
|
1965
|
+
const MCP_KEY_REFUSED_HINT = "Check the headers in mcps.json.";
|
|
1966
|
+
/** A refusal for want of credentials: HTTP 401 / 403, however the SDK words it. */
|
|
1967
|
+
function isMcpAuthRefusal(error) {
|
|
1968
|
+
const code = error?.code;
|
|
1969
|
+
if (code === 401 || code === 403) return true;
|
|
1970
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
1971
|
+
return /\b(?:401|403)\b|unauthori[sz]ed|forbidden|invalid_token/i.test(message);
|
|
1972
|
+
}
|
|
1973
|
+
/** The first line of a failure, never empty. */
|
|
1974
|
+
function mcpFailureLine(error) {
|
|
1975
|
+
const code = error?.code;
|
|
1976
|
+
return (error instanceof Error && error.message ? error.message : code !== void 0 ? `The server answered ${String(code)}.` : typeof error === "string" ? error : "").split("\n")[0]?.trim() || "Couldn't connect.";
|
|
1977
|
+
}
|
|
1978
|
+
function classifyMcpTest(config, result) {
|
|
1979
|
+
const http = config.transport !== "stdio";
|
|
1980
|
+
const ownKey = Object.keys(config.headers ?? {}).length > 0;
|
|
1981
|
+
if (http && (result.authRequired || result.error !== void 0 && isMcpAuthRefusal(result.error))) return ownKey ? {
|
|
1982
|
+
kind: "token-rejected",
|
|
1983
|
+
message: MCP_KEY_REFUSED_HINT
|
|
1984
|
+
} : {
|
|
1985
|
+
kind: "needs-auth",
|
|
1986
|
+
message: "Sign in to connect."
|
|
1987
|
+
};
|
|
1988
|
+
if (result.error !== void 0) return {
|
|
1989
|
+
kind: "error",
|
|
1990
|
+
message: mcpFailureLine(result.error)
|
|
1991
|
+
};
|
|
1992
|
+
return {
|
|
1993
|
+
kind: "ok",
|
|
1994
|
+
tools: result.tools ?? 0
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
//#endregion
|
|
3061
1998
|
//#region src/chat/model-catalog.ts
|
|
3062
1999
|
/**
|
|
3063
2000
|
* Build the unified catalog from a list of available providers.
|
|
@@ -3163,24 +2100,60 @@ function indexOfEntry(catalog, target) {
|
|
|
3163
2100
|
* provider name.
|
|
3164
2101
|
*/
|
|
3165
2102
|
function parseCustomModelQuery(query, providers, fallbackProviderKey) {
|
|
2103
|
+
const { modelId, named } = splitProviderPrefix(query, providers);
|
|
2104
|
+
const provider = named ?? providers.find((p) => p.key === fallbackProviderKey);
|
|
2105
|
+
if (!provider || !modelId) return null;
|
|
2106
|
+
return {
|
|
2107
|
+
providerKey: provider.key,
|
|
2108
|
+
providerLabel: provider.label,
|
|
2109
|
+
modelId
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* Every route for a typed model id, one per provider: the provider the query
|
|
2114
|
+
* names first (`xai grok-9`), then `fallbackProviderKey` (the current pick),
|
|
2115
|
+
* then the rest in the given order. Lets a picker offer "use this id via…"
|
|
2116
|
+
* for each signed-in provider instead of guessing one. Empty when the query
|
|
2117
|
+
* names no model id.
|
|
2118
|
+
*/
|
|
2119
|
+
function customModelTargets(query, providers, fallbackProviderKey) {
|
|
2120
|
+
const { modelId, named } = splitProviderPrefix(query, providers);
|
|
2121
|
+
if (!modelId) return [];
|
|
2122
|
+
const rank = (p) => p.key === named?.key ? 0 : p.key === fallbackProviderKey ? 1 : 2;
|
|
2123
|
+
return providers.map((p, i) => ({
|
|
2124
|
+
p,
|
|
2125
|
+
i
|
|
2126
|
+
})).sort((a, b) => rank(a.p) - rank(b.p) || a.i - b.i).map(({ p }) => ({
|
|
2127
|
+
providerKey: p.key,
|
|
2128
|
+
providerLabel: p.label,
|
|
2129
|
+
modelId,
|
|
2130
|
+
...p.key === named?.key ? { named: true } : {}
|
|
2131
|
+
}));
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Split an optional leading provider name off a typed query. Longest prefix
|
|
2135
|
+
* wins, so a multi-word label ("OpenAI Codex gpt-9") beats the bare key it
|
|
2136
|
+
* starts with. The trailing space is required: a query that is only a
|
|
2137
|
+
* provider name has no id in it and reads as an id itself. Only the prefix
|
|
2138
|
+
* is dropped; the rest is kept verbatim, so an id with spaces round-trips.
|
|
2139
|
+
*/
|
|
2140
|
+
function splitProviderPrefix(query, providers) {
|
|
3166
2141
|
const trimmed = query.trim().replace(/\s+/g, " ");
|
|
3167
|
-
if (!trimmed) return
|
|
2142
|
+
if (!trimmed) return {
|
|
2143
|
+
modelId: "",
|
|
2144
|
+
named: void 0
|
|
2145
|
+
};
|
|
3168
2146
|
const lower = trimmed.toLowerCase();
|
|
3169
|
-
const
|
|
2147
|
+
const match = providers.flatMap((p) => [{
|
|
3170
2148
|
provider: p,
|
|
3171
2149
|
token: p.label.toLowerCase()
|
|
3172
2150
|
}, {
|
|
3173
2151
|
provider: p,
|
|
3174
2152
|
token: p.key.toLowerCase()
|
|
3175
2153
|
}]).filter((candidate) => lower.startsWith(`${candidate.token} `)).sort((a, b) => b.token.length - a.token.length)[0];
|
|
3176
|
-
const provider = named?.provider ?? providers.find((p) => p.key === fallbackProviderKey);
|
|
3177
|
-
if (!provider) return null;
|
|
3178
|
-
const modelId = named ? trimmed.slice(named.token.length).trim() : trimmed;
|
|
3179
|
-
if (!modelId) return null;
|
|
3180
2154
|
return {
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
modelId
|
|
2155
|
+
modelId: match ? trimmed.slice(match.token.length).trim() : trimmed,
|
|
2156
|
+
named: match?.provider
|
|
3184
2157
|
};
|
|
3185
2158
|
}
|
|
3186
2159
|
function buildSearchCorpus(provider, model) {
|
|
@@ -3193,57 +2166,159 @@ function buildSearchCorpus(provider, model) {
|
|
|
3193
2166
|
].join(" ").toLowerCase();
|
|
3194
2167
|
}
|
|
3195
2168
|
//#endregion
|
|
3196
|
-
//#region src/chat/
|
|
2169
|
+
//#region src/chat/model-lineup.ts
|
|
2170
|
+
const DATE_DASHED = /-(\d{4})-(\d{2})-(\d{2})(?=$|[-:])/;
|
|
2171
|
+
const SIZE = /^(?:\d+(?:\.\d+)?[bkm]|a\d+(?:\.\d+)?b|e\d+b|\d+x\d+b)$/;
|
|
2172
|
+
const SNAPSHOT = /^\d{4}$|^\d{6,8}$/;
|
|
2173
|
+
const NUMBER = /^\d+(?:\.\d+)*$/;
|
|
2174
|
+
/** Zero-padded short number (`001`, `05`): a revision or a date part, never a version. */
|
|
2175
|
+
const PADDED = /^0\d{1,2}$/;
|
|
2176
|
+
/** Bedrock's `-v1` / `-v2:0` revision suffix. Only a revision once a version was read. */
|
|
2177
|
+
const REVISION = /^v\d+$/;
|
|
2178
|
+
/** `v4.1`, `k2.5`, `r1`, `o3`, `qwen3.5`: letters then a version. */
|
|
2179
|
+
const LETTERS_VERSION = /^([a-z]+)(\d+(?:\.\d+)*)$/;
|
|
2180
|
+
/** `4o`: a version then letters. */
|
|
2181
|
+
const VERSION_LETTERS = /^(\d+(?:\.\d+)*)([a-z]+)$/;
|
|
2182
|
+
/** Words that mark a pre-release or an alias of the same model, not a different one. */
|
|
2183
|
+
const NOISE = /* @__PURE__ */ new Set([
|
|
2184
|
+
"preview",
|
|
2185
|
+
"latest",
|
|
2186
|
+
"exp",
|
|
2187
|
+
"experimental"
|
|
2188
|
+
]);
|
|
2189
|
+
function parseModelId(id) {
|
|
2190
|
+
let rest = id.toLowerCase();
|
|
2191
|
+
let variant = "";
|
|
2192
|
+
const colon = rest.indexOf(":");
|
|
2193
|
+
if (colon >= 0) {
|
|
2194
|
+
variant = rest.slice(colon + 1);
|
|
2195
|
+
rest = rest.slice(0, colon);
|
|
2196
|
+
}
|
|
2197
|
+
let vendor = "";
|
|
2198
|
+
const slash = rest.lastIndexOf("/");
|
|
2199
|
+
if (slash >= 0) {
|
|
2200
|
+
vendor = rest.slice(0, slash);
|
|
2201
|
+
rest = rest.slice(slash + 1);
|
|
2202
|
+
}
|
|
2203
|
+
let snapshot = "";
|
|
2204
|
+
const dated = DATE_DASHED.exec(rest);
|
|
2205
|
+
if (dated) {
|
|
2206
|
+
snapshot = `${dated[1]}${dated[2]}${dated[3]}`;
|
|
2207
|
+
rest = rest.slice(0, dated.index) + rest.slice(dated.index + dated[0].length);
|
|
2208
|
+
}
|
|
2209
|
+
const words = [];
|
|
2210
|
+
const version = [];
|
|
2211
|
+
const sizes = [];
|
|
2212
|
+
const snapParts = [];
|
|
2213
|
+
let afterNoise = false;
|
|
2214
|
+
const pushVersion = (v) => {
|
|
2215
|
+
for (const n of v.split(".")) version.push(Number(n));
|
|
2216
|
+
};
|
|
2217
|
+
for (const token of rest.split(/[-_\s]+/).filter(Boolean)) {
|
|
2218
|
+
if (NOISE.has(token)) {
|
|
2219
|
+
afterNoise = true;
|
|
2220
|
+
continue;
|
|
2221
|
+
}
|
|
2222
|
+
if (SIZE.test(token)) {
|
|
2223
|
+
sizes.push(token);
|
|
2224
|
+
continue;
|
|
2225
|
+
}
|
|
2226
|
+
if ((words.length > 0 || version.length > 0) && (SNAPSHOT.test(token) || afterNoise && NUMBER.test(token) || version.length > 0 && (PADDED.test(token) || REVISION.test(token)))) {
|
|
2227
|
+
snapParts.push(token);
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
if (NUMBER.test(token)) {
|
|
2231
|
+
pushVersion(token);
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
const lv = LETTERS_VERSION.exec(token);
|
|
2235
|
+
if (lv) {
|
|
2236
|
+
words.push(lv[1]);
|
|
2237
|
+
pushVersion(lv[2]);
|
|
2238
|
+
continue;
|
|
2239
|
+
}
|
|
2240
|
+
const vl = VERSION_LETTERS.exec(token);
|
|
2241
|
+
if (vl) {
|
|
2242
|
+
pushVersion(vl[1]);
|
|
2243
|
+
words.push(vl[2]);
|
|
2244
|
+
continue;
|
|
2245
|
+
}
|
|
2246
|
+
words.push(token);
|
|
2247
|
+
}
|
|
2248
|
+
if (!snapshot && snapParts.length > 0) snapshot = snapParts.length === 2 && /^\d{2}$/.test(snapParts[0]) && /^\d{4}$/.test(snapParts[1]) ? `${snapParts[1]}${snapParts[0]}` : snapParts.join("");
|
|
2249
|
+
return {
|
|
2250
|
+
vendor,
|
|
2251
|
+
family: words.join("-"),
|
|
2252
|
+
version,
|
|
2253
|
+
size: sizes.join("-"),
|
|
2254
|
+
snapshot,
|
|
2255
|
+
variant
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
/** Numeric, segment by segment; a longer version wins a tie on the shared prefix (`5.5` > `5`). */
|
|
2259
|
+
function compareModelVersion(a, b) {
|
|
2260
|
+
const n = Math.max(a.length, b.length);
|
|
2261
|
+
for (let i = 0; i < n; i++) {
|
|
2262
|
+
const x = a[i];
|
|
2263
|
+
const y = b[i];
|
|
2264
|
+
if (x === void 0) return -1;
|
|
2265
|
+
if (y === void 0) return 1;
|
|
2266
|
+
if (x !== y) return x - y;
|
|
2267
|
+
}
|
|
2268
|
+
return 0;
|
|
2269
|
+
}
|
|
3197
2270
|
/**
|
|
3198
|
-
*
|
|
3199
|
-
*
|
|
3200
|
-
*
|
|
3201
|
-
*
|
|
3202
|
-
* trailing space" units so wraps land at clean word boundaries.
|
|
3203
|
-
*
|
|
3204
|
-
* Robust to:
|
|
3205
|
-
* - Overlapping refs — sorted by start; later refs that overlap are
|
|
3206
|
-
* dropped via the first-wins rule.
|
|
3207
|
-
* - Out-of-bounds refs — dropped entirely when `end > text.length` or
|
|
3208
|
-
* `start >= text.length`. Partial clipping would silently truncate
|
|
3209
|
-
* a chip's label; the caller is in a better position to surface the
|
|
3210
|
-
* mismatch (typically a stale `refs` array referencing a previous text).
|
|
3211
|
-
* - Whitespace-only plain runs — emitted as their own plain segment
|
|
3212
|
-
* so chip-adjacent-to-chip cases keep the original spacing.
|
|
3213
|
-
*
|
|
3214
|
-
* Word splitter rationale: `\S+\s*` keeps trailing whitespace attached
|
|
3215
|
-
* to its preceding word so wrap boundaries land between words (cleanly).
|
|
3216
|
-
* A leading-whitespace-only segment is captured by `\s+` so we don't
|
|
3217
|
-
* drop it entirely when the plain run starts with a space.
|
|
2271
|
+
* Among ids naming the same model (family, version and size equal), the one
|
|
2272
|
+
* a picker should show: the plain alias over a dated snapshot, the newest
|
|
2273
|
+
* snapshot among snapshots, no routing variant over a variant, then the
|
|
2274
|
+
* shorter id. Negative when `a` is preferred.
|
|
3218
2275
|
*/
|
|
3219
|
-
function
|
|
3220
|
-
const
|
|
3221
|
-
const
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
2276
|
+
function preferAlias(a, b) {
|
|
2277
|
+
const sa = a.parts.snapshot;
|
|
2278
|
+
const sb = b.parts.snapshot;
|
|
2279
|
+
if (!sa !== !sb) return sa ? 1 : -1;
|
|
2280
|
+
if (sa !== sb) return sa < sb ? 1 : -1;
|
|
2281
|
+
if (!a.parts.variant !== !b.parts.variant) return a.parts.variant ? 1 : -1;
|
|
2282
|
+
return a.id.length - b.id.length || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
2283
|
+
}
|
|
2284
|
+
const lineupKey = (e) => `${e.providerKey}\u0000${e.modelId}`;
|
|
2285
|
+
/**
|
|
2286
|
+
* Keys (`provider\0model`) of the current models: per provider and family,
|
|
2287
|
+
* the newest version, one entry per size. `keep` entries (the active model,
|
|
2288
|
+
* fallback chain, off-catalog ids) are always included.
|
|
2289
|
+
*/
|
|
2290
|
+
function currentModelKeys(entries, keep = []) {
|
|
2291
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2292
|
+
for (const entry of entries) {
|
|
2293
|
+
const parts = parseModelId(entry.modelId);
|
|
2294
|
+
const group = `${entry.providerKey}\u0000${parts.vendor}\u0000${parts.family}`;
|
|
2295
|
+
const list = groups.get(group);
|
|
2296
|
+
const item = {
|
|
2297
|
+
id: entry.modelId,
|
|
2298
|
+
key: lineupKey(entry),
|
|
2299
|
+
parts
|
|
2300
|
+
};
|
|
2301
|
+
if (list) list.push(item);
|
|
2302
|
+
else groups.set(group, [item]);
|
|
3238
2303
|
}
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
2304
|
+
const kept = new Set(keep.map(lineupKey));
|
|
2305
|
+
for (const list of groups.values()) {
|
|
2306
|
+
let newest = [];
|
|
2307
|
+
for (const item of list) if (compareModelVersion(item.parts.version, newest) > 0) newest = item.parts.version;
|
|
2308
|
+
const bySize = /* @__PURE__ */ new Map();
|
|
2309
|
+
for (const item of list) {
|
|
2310
|
+
if (compareModelVersion(item.parts.version, newest) !== 0) continue;
|
|
2311
|
+
const best = bySize.get(item.parts.size);
|
|
2312
|
+
if (!best || preferAlias(item, best) < 0) bySize.set(item.parts.size, item);
|
|
2313
|
+
}
|
|
2314
|
+
for (const item of bySize.values()) kept.add(item.key);
|
|
3245
2315
|
}
|
|
3246
|
-
return
|
|
2316
|
+
return kept;
|
|
2317
|
+
}
|
|
2318
|
+
/** `entries` in their original order, current models only (see {@link currentModelKeys}). */
|
|
2319
|
+
function currentModels(entries, keep = []) {
|
|
2320
|
+
const kept = currentModelKeys(entries, keep);
|
|
2321
|
+
return entries.filter((entry) => kept.has(lineupKey(entry)));
|
|
3247
2322
|
}
|
|
3248
2323
|
//#endregion
|
|
3249
2324
|
//#region src/chat/provider-usage-format.ts
|
|
@@ -3328,11 +2403,192 @@ function formatBalance(credits) {
|
|
|
3328
2403
|
return credits.total === null ? remaining : `${remaining} / ${formatCredits(credits.total)}`;
|
|
3329
2404
|
}
|
|
3330
2405
|
//#endregion
|
|
2406
|
+
//#region src/chat/spawn-tree.ts
|
|
2407
|
+
/** Worker id → the worker that spawned it, from nested spawn markers. */
|
|
2408
|
+
function spawnParents(rows) {
|
|
2409
|
+
const parents = /* @__PURE__ */ new Map();
|
|
2410
|
+
for (const row of rows) {
|
|
2411
|
+
const parent = row.spawn?.parentChildId;
|
|
2412
|
+
if (row.childId !== void 0 && parent !== void 0 && (row.kind === "spawn-start" || row.kind === "spawn-end")) parents.set(row.childId, parent);
|
|
2413
|
+
}
|
|
2414
|
+
return parents;
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* The worker `childId`'s rows belong under: its outermost ancestor that is
|
|
2418
|
+
* itself one of the `present` workers (the ones owning rows in this view).
|
|
2419
|
+
* A view that already re-based its owner's rows (a worker opened in a fold)
|
|
2420
|
+
* leaves that owner out of `present`, so the owner's direct children become
|
|
2421
|
+
* the lanes there. `childId` itself when no ancestor is present.
|
|
2422
|
+
*/
|
|
2423
|
+
function spawnLane(childId, parents, present) {
|
|
2424
|
+
let lane = childId;
|
|
2425
|
+
const seen = /* @__PURE__ */ new Set([lane]);
|
|
2426
|
+
for (let parent = parents.get(lane); parent !== void 0 && present.has(parent) && !seen.has(parent); parent = parents.get(lane)) {
|
|
2427
|
+
lane = parent;
|
|
2428
|
+
seen.add(lane);
|
|
2429
|
+
}
|
|
2430
|
+
return lane;
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Lane resolver over one view's rows: precomputes the parent map and the
|
|
2434
|
+
* present-worker set, then maps a row's `childId` to its lane.
|
|
2435
|
+
*/
|
|
2436
|
+
function spawnLaneResolver(rows) {
|
|
2437
|
+
const parents = spawnParents(rows);
|
|
2438
|
+
if (parents.size === 0) return (childId) => childId;
|
|
2439
|
+
const present = /* @__PURE__ */ new Set();
|
|
2440
|
+
for (const row of rows) if (row.childId !== void 0) present.add(row.childId);
|
|
2441
|
+
return (childId) => spawnLane(childId, parents, present);
|
|
2442
|
+
}
|
|
2443
|
+
//#endregion
|
|
3331
2444
|
//#region src/chat/streaming-pure.ts
|
|
3332
2445
|
const PARENT_OWNER = "parent";
|
|
3333
2446
|
function ownerOf(event) {
|
|
3334
2447
|
return event.childId ?? PARENT_OWNER;
|
|
3335
2448
|
}
|
|
2449
|
+
/** Is this event owned by a subagent (rather than the top-level agent)? */
|
|
2450
|
+
function isChildOwned(event) {
|
|
2451
|
+
return event.childId !== void 0 || (event.depth ?? 0) > 0;
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* Index of the event `owner`'s next delta may coalesce into, or `-1` when the
|
|
2455
|
+
* owner has no open lane at the tail.
|
|
2456
|
+
*
|
|
2457
|
+
* Walks backwards over events owned by OTHER subagents — concurrent siblings
|
|
2458
|
+
* whose rows carry no ordering relation to this owner's output, and which a
|
|
2459
|
+
* per-owner drain interleaves at ~60Hz — and stops at the first event that is
|
|
2460
|
+
* either this owner's or a real ordering boundary (parent output, user
|
|
2461
|
+
* prompts, banners). The caller decides whether the event found is mergeable
|
|
2462
|
+
* for its kind. A tail-only check splits every concurrent child's message
|
|
2463
|
+
* into fragments braided with its siblings'.
|
|
2464
|
+
*/
|
|
2465
|
+
function mergeTargetIndex(events, owner) {
|
|
2466
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
2467
|
+
const event = events[i];
|
|
2468
|
+
if (ownerOf(event) === owner) return i;
|
|
2469
|
+
if (!isChildOwned(event)) return -1;
|
|
2470
|
+
}
|
|
2471
|
+
return -1;
|
|
2472
|
+
}
|
|
2473
|
+
/**
|
|
2474
|
+
* Default segment bound for a growing streaming markdown event. Coalescing
|
|
2475
|
+
* copies the ENTIRE accumulated string into fresh state on every drain tick,
|
|
2476
|
+
* so one long streamed message costs O(L²) aggregate — past this threshold
|
|
2477
|
+
* the event is split and the copy re-bounded. The split reuses
|
|
2478
|
+
* {@link chunkMarkdownForRender} (blank-line boundaries outside fences,
|
|
2479
|
+
* unsafe-continuation guard), so the finalized segments are byte-identical
|
|
2480
|
+
* to the chunks the renderer would have produced inside a single event, and
|
|
2481
|
+
* the one-row markdown/markdown margin stands in for the consumed blank line
|
|
2482
|
+
* exactly like the renderer's inter-chunk gap. When no safe boundary exists
|
|
2483
|
+
* (one giant fence/list) the event keeps growing until one arrives.
|
|
2484
|
+
*/
|
|
2485
|
+
const STREAMING_MARKDOWN_SEGMENT_CHARS = 65536;
|
|
2486
|
+
/**
|
|
2487
|
+
* Backoff for over-threshold events `chunkMarkdownForRender` could not
|
|
2488
|
+
* split (one giant fence/list): records the text length at the last failed
|
|
2489
|
+
* attempt, keyed on the streaming event object (re-keyed onto each tick's
|
|
2490
|
+
* replacement). Without it a single unsplittable segment re-runs the full
|
|
2491
|
+
* O(L) chunk walk on every drain tick. Growth is geometric (retry at +25%),
|
|
2492
|
+
* so total re-chunk work stays O(L) amortized.
|
|
2493
|
+
*/
|
|
2494
|
+
const unsplittableAtLength = /* @__PURE__ */ new WeakMap();
|
|
2495
|
+
const SEGMENT_SPLIT_RETRY_GROWTH = 1.25;
|
|
2496
|
+
/**
|
|
2497
|
+
* Stamp owner (parent vs subagent) + depth + optional `turnId` onto a
|
|
2498
|
+
* freshly-minted event so consumers can identify the producer and the
|
|
2499
|
+
* source turn. Parent-owned events leave `childId` / `depth` off; `depth` is
|
|
2500
|
+
* also left off when the source never named one.
|
|
2501
|
+
*/
|
|
2502
|
+
function tagEvent(evt, owner, depth, turnId) {
|
|
2503
|
+
const withTurn = turnId ? {
|
|
2504
|
+
...evt,
|
|
2505
|
+
turnId
|
|
2506
|
+
} : evt;
|
|
2507
|
+
if (owner === PARENT_OWNER) return withTurn;
|
|
2508
|
+
return {
|
|
2509
|
+
...withTurn,
|
|
2510
|
+
childId: owner,
|
|
2511
|
+
...depth !== void 0 ? { depth } : {}
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
/** Coalesce a markdown delta into `owner`'s open streaming block, or open one. */
|
|
2515
|
+
function appendMarkdownDelta(prev, delta, owner, depth, turnId, options = {}) {
|
|
2516
|
+
const segmentChars = options.segmentChars ?? 65536;
|
|
2517
|
+
const at = mergeTargetIndex(prev, owner);
|
|
2518
|
+
const target = at >= 0 ? prev[at] : void 0;
|
|
2519
|
+
if (target && target.kind === "markdown" && target.streaming) {
|
|
2520
|
+
const merged = target.text + delta;
|
|
2521
|
+
const next = prev.slice();
|
|
2522
|
+
if (merged.length > segmentChars) {
|
|
2523
|
+
const lastFailed = unsplittableAtLength.get(target);
|
|
2524
|
+
if (lastFailed === void 0 || merged.length > lastFailed * SEGMENT_SPLIT_RETRY_GROWTH) {
|
|
2525
|
+
const chunks = chunkMarkdownForRender(merged);
|
|
2526
|
+
if (chunks.length > 1) {
|
|
2527
|
+
const segments = chunks.map((chunk, i) => i === chunks.length - 1 ? {
|
|
2528
|
+
...target,
|
|
2529
|
+
text: chunk
|
|
2530
|
+
} : {
|
|
2531
|
+
...target,
|
|
2532
|
+
text: chunk,
|
|
2533
|
+
streaming: false
|
|
2534
|
+
});
|
|
2535
|
+
next.splice(at, 1, ...segments);
|
|
2536
|
+
return next;
|
|
2537
|
+
}
|
|
2538
|
+
const grown = {
|
|
2539
|
+
...target,
|
|
2540
|
+
text: merged
|
|
2541
|
+
};
|
|
2542
|
+
unsplittableAtLength.set(grown, merged.length);
|
|
2543
|
+
next[at] = grown;
|
|
2544
|
+
return next;
|
|
2545
|
+
}
|
|
2546
|
+
const grown = {
|
|
2547
|
+
...target,
|
|
2548
|
+
text: merged
|
|
2549
|
+
};
|
|
2550
|
+
unsplittableAtLength.set(grown, lastFailed);
|
|
2551
|
+
next[at] = grown;
|
|
2552
|
+
return next;
|
|
2553
|
+
}
|
|
2554
|
+
next[at] = {
|
|
2555
|
+
...target,
|
|
2556
|
+
text: merged
|
|
2557
|
+
};
|
|
2558
|
+
return next;
|
|
2559
|
+
}
|
|
2560
|
+
return [...prev, tagEvent({
|
|
2561
|
+
kind: "markdown",
|
|
2562
|
+
text: delta,
|
|
2563
|
+
streaming: true
|
|
2564
|
+
}, owner, depth, turnId)];
|
|
2565
|
+
}
|
|
2566
|
+
/** Coalesce a thinking delta into `owner`'s open lane, one row per line. */
|
|
2567
|
+
function appendThinkingLines(prev, delta, owner, depth, turnId, options = {}) {
|
|
2568
|
+
const lines = delta.split("\n");
|
|
2569
|
+
const at = mergeTargetIndex(prev, owner);
|
|
2570
|
+
const target = at >= 0 ? prev[at] : void 0;
|
|
2571
|
+
const row = (text) => tagEvent({
|
|
2572
|
+
kind: "thinking",
|
|
2573
|
+
text
|
|
2574
|
+
}, owner, depth, turnId);
|
|
2575
|
+
if (target && target.kind === "thinking") {
|
|
2576
|
+
const next = prev.slice();
|
|
2577
|
+
next[at] = {
|
|
2578
|
+
...target,
|
|
2579
|
+
text: target.text + lines[0]
|
|
2580
|
+
};
|
|
2581
|
+
if (lines.length > 1) next.splice(at + 1, 0, ...lines.slice(1).map(row));
|
|
2582
|
+
return next;
|
|
2583
|
+
}
|
|
2584
|
+
const result = [...prev];
|
|
2585
|
+
if (lines[0] || lines.length > 1) {
|
|
2586
|
+
const first = row(lines[0]);
|
|
2587
|
+
result.push(options.onLaneOpen ? options.onLaneOpen(first) : first);
|
|
2588
|
+
}
|
|
2589
|
+
for (let i = 1; i < lines.length; i++) result.push(row(lines[i]));
|
|
2590
|
+
return result;
|
|
2591
|
+
}
|
|
3336
2592
|
/** Flip any trailing streaming markdown blocks (any owner) to finalized. */
|
|
3337
2593
|
function finalizeStreamingMarkdown(events) {
|
|
3338
2594
|
let changed = false;
|
|
@@ -3417,10 +2673,8 @@ function hiddenMarkerEvent(count) {
|
|
|
3417
2673
|
};
|
|
3418
2674
|
}
|
|
3419
2675
|
/**
|
|
3420
|
-
*
|
|
3421
|
-
*
|
|
3422
|
-
* deliberately scrolls into the marker. The cut expands to a turn boundary
|
|
3423
|
-
* so tool-call/result groups are never split.
|
|
2676
|
+
* Swap one turn's rows for its rebuilt ones, in place; `null` when the turn
|
|
2677
|
+
* is not on show.
|
|
3424
2678
|
*/
|
|
3425
2679
|
function replaceTranscriptTurnEvents(displayed, rebuilt, turnId) {
|
|
3426
2680
|
const replacements = rebuilt.filter((event) => event.turnId === turnId);
|
|
@@ -3433,6 +2687,26 @@ function replaceTranscriptTurnEvents(displayed, rebuilt, turnId) {
|
|
|
3433
2687
|
});
|
|
3434
2688
|
return inserted ? patched : null;
|
|
3435
2689
|
}
|
|
2690
|
+
/**
|
|
2691
|
+
* A run in the transcript: everything from one of the user's own prompts
|
|
2692
|
+
* up to the next one. Revealing and capping move in whole runs, so the top
|
|
2693
|
+
* of the transcript always opens on a prompt, not halfway through what the
|
|
2694
|
+
* agent did for it.
|
|
2695
|
+
*/
|
|
2696
|
+
function isRunStart(e) {
|
|
2697
|
+
return e !== void 0 && isTopLevelPrompt(e);
|
|
2698
|
+
}
|
|
2699
|
+
/** A run longer than this many reveal pages on its own is revealed in pieces of that size. */
|
|
2700
|
+
const WHOLE_RUN_REVEAL_PAGES = 5;
|
|
2701
|
+
/**
|
|
2702
|
+
* Reveal the earlier history while keeping everything on show: the whole
|
|
2703
|
+
* run right before it, and the runs before that while they fit `pageSize`
|
|
2704
|
+
* events together (several small runs at once, never part of one). A run
|
|
2705
|
+
* longer than {@link WHOLE_RUN_REVEAL_PAGES} pages on its own comes in
|
|
2706
|
+
* pieces that size, cut at a turn boundary so a tool call keeps its result.
|
|
2707
|
+
* The default transcript is memory-bounded; memory grows only when the user
|
|
2708
|
+
* deliberately scrolls into the marker.
|
|
2709
|
+
*/
|
|
3436
2710
|
function revealEarlierTranscriptEvents(all, displayed, pageSize = 600) {
|
|
3437
2711
|
const hidden = hiddenEarlierTranscriptCount(displayed);
|
|
3438
2712
|
if (hidden <= 0) return displayed;
|
|
@@ -3440,12 +2714,63 @@ function revealEarlierTranscriptEvents(all, displayed, pageSize = 600) {
|
|
|
3440
2714
|
const anchorTurnId = tail.find((e) => e.turnId !== void 0)?.turnId;
|
|
3441
2715
|
const anchorIndex = anchorTurnId !== void 0 ? all.findIndex((e) => e.turnId === anchorTurnId) : -1;
|
|
3442
2716
|
const seam = anchorIndex >= 0 ? anchorIndex : Math.min(hidden, all.length);
|
|
3443
|
-
|
|
3444
|
-
|
|
2717
|
+
if (seam <= 0) return displayed;
|
|
2718
|
+
const page = Math.max(1, pageSize);
|
|
2719
|
+
/** Where the run holding `all[end - 1]` starts (0 when no prompt precedes it). */
|
|
2720
|
+
const runStart = (end) => {
|
|
2721
|
+
let i = end - 1;
|
|
2722
|
+
while (i > 0 && !isRunStart(all[i])) i--;
|
|
2723
|
+
return i;
|
|
2724
|
+
};
|
|
2725
|
+
let from = runStart(seam);
|
|
2726
|
+
if (seam - from > page * WHOLE_RUN_REVEAL_PAGES) {
|
|
2727
|
+
from = seam - page * WHOLE_RUN_REVEAL_PAGES;
|
|
2728
|
+
while (from < seam - 1 && all[from]?.turnId !== void 0 && all[from]?.turnId === all[from - 1]?.turnId) from++;
|
|
2729
|
+
} else while (from > 0) {
|
|
2730
|
+
const previous = runStart(from);
|
|
2731
|
+
if (seam - previous > page) break;
|
|
2732
|
+
from = previous;
|
|
2733
|
+
}
|
|
3445
2734
|
const out = [...all.slice(from, seam), ...tail];
|
|
3446
2735
|
if (from > 0) out.unshift(hiddenMarkerEvent(from));
|
|
3447
2736
|
return out;
|
|
3448
2737
|
}
|
|
2738
|
+
/** The user's own prompt, not a subagent's. */
|
|
2739
|
+
function isTopLevelPrompt(e) {
|
|
2740
|
+
return e.kind === "user-prompt" && !e.depth && (!e.childId || e.childId === "parent");
|
|
2741
|
+
}
|
|
2742
|
+
/** Index of the `n`-th last top-level user prompt at or after `from` (the earliest one when fewer); -1 with none. */
|
|
2743
|
+
function nthLastPrompt(events, n, from) {
|
|
2744
|
+
let seen = 0;
|
|
2745
|
+
let at = -1;
|
|
2746
|
+
for (let i = events.length - 1; i >= from; i--) {
|
|
2747
|
+
const e = events[i];
|
|
2748
|
+
if (isTopLevelPrompt(e)) {
|
|
2749
|
+
at = i;
|
|
2750
|
+
if (++seen >= n) break;
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
return at;
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* A transcript's end, with `earlier` events of the whole replay before it:
|
|
2757
|
+
* the hidden-count marker in front, as a cap would have left it.
|
|
2758
|
+
*/
|
|
2759
|
+
function withEarlierHidden(events, earlier) {
|
|
2760
|
+
return earlier > 0 ? [hiddenMarkerEvent(earlier), ...events] : events;
|
|
2761
|
+
}
|
|
2762
|
+
/**
|
|
2763
|
+
* Whether the end of a transcript (from a turn boundary) is enough for
|
|
2764
|
+
* {@link capTranscriptEvents} at the default bounds: capping it behind its
|
|
2765
|
+
* hidden-count marker cuts exactly where capping the whole transcript
|
|
2766
|
+
* would. Over the cap, and holding the prompts the cut reaches back to.
|
|
2767
|
+
*/
|
|
2768
|
+
function coversTranscriptCap(events) {
|
|
2769
|
+
if (events.length <= 1500) return false;
|
|
2770
|
+
let prompts = 0;
|
|
2771
|
+
for (const e of events) if (isTopLevelPrompt(e) && ++prompts >= 10) return true;
|
|
2772
|
+
return false;
|
|
2773
|
+
}
|
|
3449
2774
|
/**
|
|
3450
2775
|
* Bound a transcript event array for display. Identity below the cap
|
|
3451
2776
|
* (no re-render churn on the hot append path); above it, drops the
|
|
@@ -3469,7 +2794,14 @@ function capTranscriptEvents(events, max = MAX_TRANSCRIPT_EVENTS, trimTo = TRANS
|
|
|
3469
2794
|
}
|
|
3470
2795
|
let cut = events.length - keep;
|
|
3471
2796
|
if (cut <= start) return events;
|
|
2797
|
+
const promptAt = nthLastPrompt(events, 10, start);
|
|
2798
|
+
if (promptAt >= 0 && promptAt < cut) cut = Math.max(promptAt, events.length - Math.floor((max + keep) / 2), start + 1);
|
|
3472
2799
|
const turnAtCut = (i) => events[i]?.turnId;
|
|
2800
|
+
if (!isRunStart(events[cut])) {
|
|
2801
|
+
let next = cut + 1;
|
|
2802
|
+
while (next < events.length && !isRunStart(events[next])) next++;
|
|
2803
|
+
if (next < events.length) cut = next;
|
|
2804
|
+
}
|
|
3473
2805
|
while (cut < events.length) {
|
|
3474
2806
|
const turn = turnAtCut(cut);
|
|
3475
2807
|
if (turn === void 0 || turn !== turnAtCut(cut - 1)) break;
|
|
@@ -3512,7 +2844,14 @@ const TOOL_DISPLAY = {
|
|
|
3512
2844
|
displayName: "Read",
|
|
3513
2845
|
format: (input) => {
|
|
3514
2846
|
const path = stringField(input, "path");
|
|
3515
|
-
if (!path)
|
|
2847
|
+
if (!path) {
|
|
2848
|
+
const paths = readFileInputPaths(input);
|
|
2849
|
+
if (paths.length === 0) return null;
|
|
2850
|
+
return {
|
|
2851
|
+
target: paths[0],
|
|
2852
|
+
meta: paths.length > 1 ? [`+${paths.length - 1} more`] : []
|
|
2853
|
+
};
|
|
2854
|
+
}
|
|
3516
2855
|
const meta = [];
|
|
3517
2856
|
const offset = numberField(input, "offset");
|
|
3518
2857
|
const limit = numberField(input, "limit");
|
|
@@ -3568,14 +2907,16 @@ const TOOL_DISPLAY = {
|
|
|
3568
2907
|
}
|
|
3569
2908
|
},
|
|
3570
2909
|
shell: {
|
|
3571
|
-
displayName: (input) =>
|
|
2910
|
+
displayName: (input) => {
|
|
2911
|
+
const description = input ? stringField(input, "description")?.trim() : void 0;
|
|
2912
|
+
if (description) return previewLine(description, 120);
|
|
2913
|
+
return input?.run_in_background === true ? "Shell (background)" : "Shell";
|
|
2914
|
+
},
|
|
3572
2915
|
format: (input) => {
|
|
3573
2916
|
const command = stringField(input, "command");
|
|
3574
2917
|
if (!command) return null;
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
if (description && description.trim() !== "") line.meta = [truncate(description, 100)];
|
|
3578
|
-
return line;
|
|
2918
|
+
if (stringField(input, "description")?.trim()) return input.run_in_background === true ? { meta: ["background"] } : {};
|
|
2919
|
+
return { target: previewLine(command, 200) };
|
|
3579
2920
|
}
|
|
3580
2921
|
},
|
|
3581
2922
|
shell_kill: {
|
|
@@ -3645,11 +2986,103 @@ const TOOL_DISPLAY = {
|
|
|
3645
2986
|
if (!task) return null;
|
|
3646
2987
|
const resume = stringField(input, "resume");
|
|
3647
2988
|
return {
|
|
3648
|
-
target:
|
|
2989
|
+
target: previewLine(task, 200),
|
|
3649
2990
|
...resume ? { meta: [`resumed ${resume}`] } : {}
|
|
3650
2991
|
};
|
|
3651
2992
|
}
|
|
3652
2993
|
},
|
|
2994
|
+
codemode: {
|
|
2995
|
+
displayName: (input) => {
|
|
2996
|
+
const description = input ? stringField(input, "description")?.trim() : void 0;
|
|
2997
|
+
return description ? previewLine(description, 120) : "Codemode";
|
|
2998
|
+
},
|
|
2999
|
+
format: (input) => {
|
|
3000
|
+
const description = stringField(input, "description")?.trim();
|
|
3001
|
+
const code = stringField(input, "code");
|
|
3002
|
+
const lines = code ? code.split("\n").filter((line) => line.trim() !== "" && !line.trimStart().startsWith("// @options:")) : [];
|
|
3003
|
+
const meta = lines.length > 0 ? [`${lines.length} line${lines.length === 1 ? "" : "s"}`] : [];
|
|
3004
|
+
if (description) return { meta };
|
|
3005
|
+
if (lines.length === 0) return null;
|
|
3006
|
+
return {
|
|
3007
|
+
target: previewLine(lines[0].trim(), 120),
|
|
3008
|
+
meta
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
},
|
|
3012
|
+
browser_open: {
|
|
3013
|
+
displayName: "Browser",
|
|
3014
|
+
format: (input) => ({ target: stringField(input, "url") ?? "open" })
|
|
3015
|
+
},
|
|
3016
|
+
browser_navigate: {
|
|
3017
|
+
displayName: "Navigate",
|
|
3018
|
+
format: (input) => ({ target: stringField(input, "url") ?? stringField(input, "action") ?? "" })
|
|
3019
|
+
},
|
|
3020
|
+
browser_snapshot: {
|
|
3021
|
+
displayName: "Snapshot",
|
|
3022
|
+
format: () => ({ target: "page" })
|
|
3023
|
+
},
|
|
3024
|
+
browser_screenshot: {
|
|
3025
|
+
displayName: "Screenshot",
|
|
3026
|
+
format: (input) => ({ target: stringField(input, "ref") ?? "viewport" })
|
|
3027
|
+
},
|
|
3028
|
+
browser_click: {
|
|
3029
|
+
displayName: "Click",
|
|
3030
|
+
format: (input) => ({ target: browserTarget(input) })
|
|
3031
|
+
},
|
|
3032
|
+
browser_type: {
|
|
3033
|
+
displayName: "Type",
|
|
3034
|
+
format: (input) => {
|
|
3035
|
+
const text = stringField(input, "text") ?? "";
|
|
3036
|
+
const where = browserTarget(input);
|
|
3037
|
+
return {
|
|
3038
|
+
target: previewLine(JSON.stringify(text), 80),
|
|
3039
|
+
...where ? { meta: [`into ${where}`] } : {}
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
},
|
|
3043
|
+
browser_press: {
|
|
3044
|
+
displayName: "Press",
|
|
3045
|
+
format: (input) => {
|
|
3046
|
+
return { target: [...Array.isArray(input.modifiers) ? input.modifiers.map(String) : [], stringField(input, "key") ?? ""].join("+") };
|
|
3047
|
+
}
|
|
3048
|
+
},
|
|
3049
|
+
browser_scroll: {
|
|
3050
|
+
displayName: "Scroll",
|
|
3051
|
+
format: (input) => ({ target: `${numberField(input, "dx") ?? 0}, ${numberField(input, "dy") ?? 0}` })
|
|
3052
|
+
},
|
|
3053
|
+
browser_wait_for: {
|
|
3054
|
+
displayName: "Wait for",
|
|
3055
|
+
format: (input) => ({ target: stringField(input, "text") ?? stringField(input, "selector") ?? stringField(input, "urlIncludes") ?? "" })
|
|
3056
|
+
},
|
|
3057
|
+
browser_evaluate: {
|
|
3058
|
+
displayName: "Evaluate",
|
|
3059
|
+
format: (input) => ({ target: previewLine(stringField(input, "expression") ?? "", 120) })
|
|
3060
|
+
},
|
|
3061
|
+
browser_console: {
|
|
3062
|
+
displayName: "Console",
|
|
3063
|
+
format: (input) => ({ target: stringField(input, "level") ?? "all" })
|
|
3064
|
+
},
|
|
3065
|
+
terminal_list: {
|
|
3066
|
+
displayName: "Terminals",
|
|
3067
|
+
format: () => ({ target: "list" })
|
|
3068
|
+
},
|
|
3069
|
+
terminal_read: {
|
|
3070
|
+
displayName: "Read terminal",
|
|
3071
|
+
format: (input) => ({ target: stringField(input, "id") ?? "" })
|
|
3072
|
+
},
|
|
3073
|
+
terminal_write: {
|
|
3074
|
+
displayName: "Type in terminal",
|
|
3075
|
+
format: (input) => {
|
|
3076
|
+
const text = stringField(input, "text");
|
|
3077
|
+
const keys = Array.isArray(input.keys) ? input.keys.map(String).filter(Boolean) : [];
|
|
3078
|
+
const typed = [text ? previewLine(JSON.stringify(text), 80) : "", ...keys].filter(Boolean).join(" + ");
|
|
3079
|
+
const id = stringField(input, "id");
|
|
3080
|
+
return {
|
|
3081
|
+
target: typed || (id ?? ""),
|
|
3082
|
+
...id && typed ? { meta: [id] } : {}
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
},
|
|
3653
3086
|
tool_search: {
|
|
3654
3087
|
displayName: "Search tools",
|
|
3655
3088
|
format: (input) => {
|
|
@@ -3697,7 +3130,7 @@ const TOOL_DISPLAY = {
|
|
|
3697
3130
|
if (!name || !script) return null;
|
|
3698
3131
|
const meta = [`skill ${name}`];
|
|
3699
3132
|
const args = Array.isArray(input.args) ? input.args : null;
|
|
3700
|
-
if (args && args.length > 0) meta.push(
|
|
3133
|
+
if (args && args.length > 0) meta.push(previewLine(args.map(String).join(" "), 80));
|
|
3701
3134
|
return {
|
|
3702
3135
|
target: script,
|
|
3703
3136
|
meta
|
|
@@ -3817,6 +3250,35 @@ function displayNameFor(name, input) {
|
|
|
3817
3250
|
return sentenceCase(name.startsWith("mcp_") ? name.slice(4) : name);
|
|
3818
3251
|
}
|
|
3819
3252
|
/**
|
|
3253
|
+
* The source code a tool call runs, for renderers that show it highlighted
|
|
3254
|
+
* instead of the raw JSON input: Codemode's script as JavaScript, a `shell`
|
|
3255
|
+
* / `bash` command as bash (the command alone: the label carries the
|
|
3256
|
+
* description, the rest is plumbing). `null` for
|
|
3257
|
+
* every other tool and for calls without the code field. `language` is a
|
|
3258
|
+
* fence-style name both highlighters accept (Shiki in the GUI, OpenTUI's
|
|
3259
|
+
* tree-sitter in the TUI).
|
|
3260
|
+
*/
|
|
3261
|
+
function toolInputCode(name, input) {
|
|
3262
|
+
if (!input) return null;
|
|
3263
|
+
if (name === "codemode") {
|
|
3264
|
+
const code = stringField(input, "code");
|
|
3265
|
+
if (!code || code.trim() === "") return null;
|
|
3266
|
+
return {
|
|
3267
|
+
code: code.replace(/^\n+|\s+$/g, ""),
|
|
3268
|
+
language: "javascript"
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
if (name === "shell" || name === "bash") {
|
|
3272
|
+
const command = stringField(input, "command");
|
|
3273
|
+
if (!command || command.trim() === "") return null;
|
|
3274
|
+
return {
|
|
3275
|
+
code: command.replace(/^\n+|\s+$/g, ""),
|
|
3276
|
+
language: "bash"
|
|
3277
|
+
};
|
|
3278
|
+
}
|
|
3279
|
+
return null;
|
|
3280
|
+
}
|
|
3281
|
+
/**
|
|
3820
3282
|
* Run a tool's curated formatter and return the result, or `null` when
|
|
3821
3283
|
* no formatter is registered / the input shape doesn't match. Renderer
|
|
3822
3284
|
* decides what to do with `null` — typically: show `↳ <displayName>`
|
|
@@ -3831,6 +3293,12 @@ function formatToolCall(name, input) {
|
|
|
3831
3293
|
return null;
|
|
3832
3294
|
}
|
|
3833
3295
|
}
|
|
3296
|
+
/** A browser tool's target as written: ref, selector, or point. */
|
|
3297
|
+
function browserTarget(input) {
|
|
3298
|
+
const x = numberField(input, "x");
|
|
3299
|
+
const y = numberField(input, "y");
|
|
3300
|
+
return stringField(input, "ref") ?? stringField(input, "selector") ?? (x !== void 0 && y !== void 0 ? `${x}, ${y}` : "");
|
|
3301
|
+
}
|
|
3834
3302
|
function stringField(input, key) {
|
|
3835
3303
|
const v = input[key];
|
|
3836
3304
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
@@ -3846,139 +3314,7 @@ function sentenceCase(s) {
|
|
|
3846
3314
|
words[0] = (words[0][0]?.toUpperCase() ?? "") + words[0].slice(1);
|
|
3847
3315
|
return words.join(" ");
|
|
3848
3316
|
}
|
|
3849
|
-
/**
|
|
3850
|
-
* Collapse internal whitespace (including newlines) to single spaces
|
|
3851
|
-
* and clip to `max` columns with a trailing `…`. The whitespace
|
|
3852
|
-
* normalisation is the load-bearing bit — tool input strings like a
|
|
3853
|
-
* shell heredoc or a multi-line Python `-c` script otherwise render
|
|
3854
|
-
* across several rows in the transcript even though the `↳ Tool …`
|
|
3855
|
-
* line is meant to be a single-line scannable summary.
|
|
3856
|
-
*/
|
|
3857
|
-
function truncate(s, max) {
|
|
3858
|
-
const clean = s.replace(/\s+/g, " ").trim();
|
|
3859
|
-
return clean.length <= max ? clean : `${clean.slice(0, max - 1)}…`;
|
|
3860
|
-
}
|
|
3861
|
-
function formatBytes(bytes) {
|
|
3862
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
3863
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
3864
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
3865
|
-
}
|
|
3866
|
-
//#endregion
|
|
3867
|
-
//#region src/chat/turn-operations.ts
|
|
3868
|
-
/**
|
|
3869
|
-
* Fork — keep every turn up to and including `turnId`, then strip any
|
|
3870
|
-
* `tool_call` blocks left without a matching `tool_result` in the slice.
|
|
3871
|
-
*
|
|
3872
|
-
* Semantics:
|
|
3873
|
-
* - Include the selected turn ("branch from HERE" mental model — the
|
|
3874
|
-
* user wants the selected message to be the latest in the fork).
|
|
3875
|
-
* - If the selected turn is an assistant turn with unresolved
|
|
3876
|
-
* `tool_call` blocks (their `tool_result`s live in turns AFTER the
|
|
3877
|
-
* slice), strip those calls. Otherwise the fork would post an
|
|
3878
|
-
* assistant turn with no matching tool results, breaking the next
|
|
3879
|
-
* provider call.
|
|
3880
|
-
* - Drop turns that become empty (all blocks stripped).
|
|
3881
|
-
*
|
|
3882
|
-
* Returns `null` when `turnId` doesn't exist in `turns` — caller should
|
|
3883
|
-
* surface a "turn not found" error rather than silently no-op.
|
|
3884
|
-
*/
|
|
3885
|
-
function truncateTurnsAt(turns, turnId) {
|
|
3886
|
-
const idx = turns.findIndex((t) => t.id === turnId);
|
|
3887
|
-
if (idx === -1) return null;
|
|
3888
|
-
return stripOrphanToolBlocks(turns.slice(0, idx + 1));
|
|
3889
|
-
}
|
|
3890
|
-
/**
|
|
3891
|
-
* Delete — remove the turn with `turnId` and any tool blocks left
|
|
3892
|
-
* orphaned by the removal. Returns `null` when `turnId` doesn't exist.
|
|
3893
|
-
*
|
|
3894
|
-
* Strategy:
|
|
3895
|
-
* 1. Drop the target turn.
|
|
3896
|
-
* 2. Scan the remaining turns for `tool_call`s without a matching
|
|
3897
|
-
* `tool_result` (orphaned by removing the user turn that carried
|
|
3898
|
-
* the result), and `tool_result`s without a matching `tool_call`
|
|
3899
|
-
* (orphaned by removing the assistant turn that issued the call).
|
|
3900
|
-
* Strip both sides.
|
|
3901
|
-
* 3. Drop turns whose content is now empty.
|
|
3902
|
-
*
|
|
3903
|
-
* This guarantees the resulting history is protocol-clean — a follow-up
|
|
3904
|
-
* `agent.run()` against the modified session can post turns without the
|
|
3905
|
-
* provider rejecting the history.
|
|
3906
|
-
*/
|
|
3907
|
-
function deleteTurnSafely(turns, turnId) {
|
|
3908
|
-
const idx = turns.findIndex((t) => t.id === turnId);
|
|
3909
|
-
if (idx === -1) return null;
|
|
3910
|
-
return stripOrphanToolBlocks([...turns.slice(0, idx), ...turns.slice(idx + 1)]);
|
|
3911
|
-
}
|
|
3912
|
-
/**
|
|
3913
|
-
* Walk a turn list and remove any tool blocks whose counterpart is
|
|
3914
|
-
* missing. Drops turns left empty. Used by `truncateTurnsAt` (which can
|
|
3915
|
-
* leave `tool_call`s orphaned when their results are past the cut) and
|
|
3916
|
-
* `deleteTurnSafely` (which can orphan either side of a pair).
|
|
3917
|
-
*
|
|
3918
|
-
* Pure / total: returns a new array; never throws.
|
|
3919
|
-
*/
|
|
3920
|
-
function stripOrphanToolBlocks(turns) {
|
|
3921
|
-
const callIds = /* @__PURE__ */ new Set();
|
|
3922
|
-
const resultIds = /* @__PURE__ */ new Set();
|
|
3923
|
-
for (const turn of turns) for (const block of turn.content) if (block.type === "tool_call") callIds.add(block.id);
|
|
3924
|
-
else if (block.type === "tool_result") resultIds.add(block.callId);
|
|
3925
|
-
const result = [];
|
|
3926
|
-
for (const turn of turns) {
|
|
3927
|
-
const filtered = [];
|
|
3928
|
-
for (const block of turn.content) {
|
|
3929
|
-
if (block.type === "tool_call") {
|
|
3930
|
-
if (!resultIds.has(block.id)) continue;
|
|
3931
|
-
} else if (block.type === "tool_result") {
|
|
3932
|
-
if (!callIds.has(block.callId)) continue;
|
|
3933
|
-
}
|
|
3934
|
-
filtered.push(block);
|
|
3935
|
-
}
|
|
3936
|
-
if (filtered.length === 0) continue;
|
|
3937
|
-
result.push(filtered.length === turn.content.length ? turn : {
|
|
3938
|
-
...turn,
|
|
3939
|
-
content: filtered
|
|
3940
|
-
});
|
|
3941
|
-
}
|
|
3942
|
-
return result;
|
|
3943
|
-
}
|
|
3944
|
-
/**
|
|
3945
|
-
* Serialize a turn's content to a clean text representation suited for
|
|
3946
|
-
* the clipboard. Joins text + thinking blocks verbatim; tool calls and
|
|
3947
|
-
* tool results get bracketed labels so the user can paste a readable
|
|
3948
|
-
* record of what happened without losing structure.
|
|
3949
|
-
*
|
|
3950
|
-
* Empty turns return `''`.
|
|
3951
|
-
*/
|
|
3952
|
-
function turnAsText(turn) {
|
|
3953
|
-
const parts = [];
|
|
3954
|
-
for (const block of turn.content) if (block.type === "text" && block.text.trim()) parts.push(block.text);
|
|
3955
|
-
else if (block.type === "thinking" && block.text.trim()) parts.push(`[thinking]\n${block.text}`);
|
|
3956
|
-
else if (block.type === "tool_call") parts.push(`[tool call · ${block.name}]\n${stringifyArgs(block.input)}`);
|
|
3957
|
-
else if (block.type === "tool_result") parts.push(`[tool result]\n${typeof block.output === "string" ? block.output : JSON.stringify(block.output, null, 2)}`);
|
|
3958
|
-
else if (block.type === "compact-summary") parts.push(`[compaction summary · ${block.replacesTurnIds.length} turn${block.replacesTurnIds.length === 1 ? "" : "s"}]\n${block.summary}`);
|
|
3959
|
-
return parts.join("\n\n");
|
|
3960
|
-
}
|
|
3961
|
-
function stringifyArgs(input) {
|
|
3962
|
-
try {
|
|
3963
|
-
return JSON.stringify(input, null, 2);
|
|
3964
|
-
} catch {
|
|
3965
|
-
return String(input);
|
|
3966
|
-
}
|
|
3967
|
-
}
|
|
3968
|
-
/**
|
|
3969
|
-
* Count turns before / after the one identified by `turnId` in the
|
|
3970
|
-
* given list. Returns `null` when the id is missing. Used to label the
|
|
3971
|
-
* turn-details modal with `N before · M after`.
|
|
3972
|
-
*/
|
|
3973
|
-
function countNeighbors(turnIds, turnId) {
|
|
3974
|
-
const idx = turnIds.indexOf(turnId);
|
|
3975
|
-
if (idx === -1) return null;
|
|
3976
|
-
return {
|
|
3977
|
-
before: idx,
|
|
3978
|
-
after: turnIds.length - 1 - idx
|
|
3979
|
-
};
|
|
3980
|
-
}
|
|
3981
3317
|
//#endregion
|
|
3982
|
-
export {
|
|
3318
|
+
export { chunkMarkdownForRender as $, MachineErrorCode as $t, showsAccount as A, BROWSER_TEXT_MAX_CHARS as At, indexOfEntry as B, liveSessionCost as Bt, spawnParents as C, TODO_STATUS_GLYPHS as Cn, BROWSER_ACTION_TIMEOUT_MS as Ct, formatUsagePercent as D, BROWSER_OPS as Dt, formatCredits as E, BROWSER_OPEN_MOUNT_MS as Et, currentModels as F, createChatsCompletionProvider as Ft, mcpFailureLine as G, daemonGate as Gt, MCP_KEY_REFUSED_HINT as H, runStatusLabel as Ht, parseModelId as I, uniqueChatIdsFromReferences as It, mcpFailureEvent as J, isKillSwitchEngaged as Jt, MCP_STATUS_LABEL as K, defineFeatureGate as Kt, buildModelCatalog as L, cacheBreakChip as Lt, usageTone as M, BrowserOpError as Mt, compareModelVersion as N, CHATS_TRIGGER as Nt, mostConstrainedWindow as O, BROWSER_OUTLINE_MAX_ELEMENTS as Ot, currentModelKeys as P, chatReferenceTokensFromText as Pt, MARKDOWN_RENDER_CHUNK_TARGET_BYTES as Q, MACHINE_STREAM_CHUNK_BYTES as Qt, customModelTargets as R, cacheBreakNoticeText as Rt, spawnLaneResolver as S, accentColor as Sn, TerminalError as St, formatBalance as T, BROWSER_MAX_WAIT_MS as Tt, classifyMcpTest as U, streamingLabel as Ut, parseCustomModelQuery as V, retryLabel as Vt, isMcpAuthRefusal as W, FEATURE_GATES as Wt, enabledMcpsWith as X, MACHINE_IDEMPOTENT_METHODS as Xt, reduceMcpAuth as Y, remoteHostsGate as Yt, enabledMcpsWithout as Z, MACHINE_PROTOCOL_VERSION as Zt, replaceTranscriptTurnEvents as _, retagUserPromptEventsFromRefs as _n, TERMINAL_HISTORY_MAX_CHARS as _t, MAX_TRANSCRIPT_EVENTS as a, BASH_CALL_ID_PREFIX as an, DAEMON_HEALTH_GLYPH as at, withEarlierHidden as b, streamEventSearchText as bn, TERMINAL_READ_MAX_CHARS as bt, appendMarkdownDelta as c, isEditErrorResult as cn, createSkillsCompletionProvider as ct, coversTranscriptCap as d, isVisible as dn, createFilesCompletionProvider as dt, isValidMachineName as en, closeDanglingCodeFence as et, finalizeStreamingMarkdown as f, isVisibleDuringSearch as fn, uniqueFilesFromReferences as ft, ownerOf as g, retagUserPromptEvents as gn, buildLinearRamp as gt, mergeTargetIndex as h, promptTurnRefs as hn, blendHsl as ht, toolInputCode as i, toMachineName as in, stripHtmlCommentsOutsideCode as it, updatedLabel as j, BROWSER_WAIT_TIMEOUT_MS as jt, resetLabel as k, BROWSER_REQUEST_SLACK_MS as kt, appendThinkingLines as l, isTurnHighlighted as ln, uniqueSkillNamesFromReferences as lt, hiddenEarlierTranscriptCount as m, promptHistoryFromEvents as mn, createCommandsCompletionProvider as mt, displayNameFor as n, machineKind as nn, normalizeMarkdownCodeFences as nt, STREAMING_MARKDOWN_SEGMENT_CHARS as o, CACHE_NOTICE_LABEL as on, DAEMON_HEALTH_STYLE as ot, finalizeStreamingMarkdownForOwner as p, messageSelectionOwnership as pn, COMMANDS_TRIGGER as pt, getMcpAuthStatus as q, engagedKillSwitches as qt, formatToolCall as r, parseMachineKind as rn, splitMarkdownCodeBlocks as rt, TRANSCRIPT_TRIM_TO_EVENTS as s, EDIT_TOOL_NAMES as sn, SKILLS_TRIGGER as st, TOOL_DISPLAY as t, machineCredentialKey as tn, codeTokenHasClosingFence as tt, capTranscriptEvents as u, isUserBashEvent as un, FILES_TRIGGER as ut, revealEarlierTranscriptEvents as v, selectableTurnIds as vn, TERMINAL_MAX_PER_SESSION as vt, activeSnapshotFor as w, BROWSER_EVALUATE_MAX_BYTES as wt, spawnLane as x, turnSelectionOwnership as xn, TERMINAL_TEXT_MAX_LINES as xt, turnContextSize as y, selectionAfterTurnRemoval as yn, TERMINAL_OUTPUT_WINDOW as yt, filterModelCatalog as z, compactingLabel as zt };
|
|
3983
3319
|
|
|
3984
|
-
//# sourceMappingURL=
|
|
3320
|
+
//# sourceMappingURL=tool-formatters-IQFDrdrs.js.map
|