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
|
@@ -0,0 +1,1790 @@
|
|
|
1
|
+
import { i as readFileInputPaths } from "./format-bytes-C7tfc3Zm.js";
|
|
2
|
+
import { rn as parseMachineKind } from "./tool-formatters-IQFDrdrs.js";
|
|
3
|
+
import { y as computeLineDiff } from "./url-D8HCl65b.js";
|
|
4
|
+
import { p as LOCAL_HOST_KEY } from "./types-CH5XpIHw.js";
|
|
5
|
+
//#region src/chat/hosts/host-index.ts
|
|
6
|
+
/**
|
|
7
|
+
* Which host an id lives on: a session (or a request a host handed out: an
|
|
8
|
+
* approval, a question, a terminal). A client learns it from what a host
|
|
9
|
+
* tells it (its lists, a create, an activation, its signals) and routes
|
|
10
|
+
* every later call on that id there.
|
|
11
|
+
*
|
|
12
|
+
* Never guessed: an id no host told about is an error
|
|
13
|
+
* ({@link UnknownHostIdError}), one several hosts list is an error unless the
|
|
14
|
+
* lookup names its host ({@link AmbiguousHostIdError}), and the ids of a host
|
|
15
|
+
* that was removed stay remembered as such ({@link HostRemovedError}) until
|
|
16
|
+
* a host claims them again, so nothing aimed at a removed server quietly
|
|
17
|
+
* lands on another host.
|
|
18
|
+
* Node-free, shared by the TUI and the GUI main process.
|
|
19
|
+
*/
|
|
20
|
+
/** No host told this client about the id: nothing to route it to. */
|
|
21
|
+
var UnknownHostIdError = class extends Error {
|
|
22
|
+
id;
|
|
23
|
+
code = "unknown-host-id";
|
|
24
|
+
constructor(id, noun = "session") {
|
|
25
|
+
super(`No host is known for ${noun} "${id}": list or open it on its host first.`);
|
|
26
|
+
this.id = id;
|
|
27
|
+
this.name = "UnknownHostIdError";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/** The id lived on a host that was removed from this window. */
|
|
31
|
+
var HostRemovedError = class extends Error {
|
|
32
|
+
hostKey;
|
|
33
|
+
hostName;
|
|
34
|
+
code = "host-removed";
|
|
35
|
+
constructor(hostKey, hostName) {
|
|
36
|
+
super(`${hostName} was removed.`);
|
|
37
|
+
this.hostKey = hostKey;
|
|
38
|
+
this.hostName = hostName;
|
|
39
|
+
this.name = "HostRemovedError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Several hosts list the id (two hosts can hold the same session id: a
|
|
44
|
+
* server sharing this machine's data dir, a copied store) and the lookup
|
|
45
|
+
* did not say which: refused, never guessed.
|
|
46
|
+
*/
|
|
47
|
+
var AmbiguousHostIdError = class extends Error {
|
|
48
|
+
id;
|
|
49
|
+
hostKeys;
|
|
50
|
+
code = "ambiguous-host-id";
|
|
51
|
+
constructor(id, hostKeys, noun = "session") {
|
|
52
|
+
super(`${noun[0].toUpperCase()}${noun.slice(1)} "${id}" exists on several hosts (${hostKeys.join(", ")}): say which one.`);
|
|
53
|
+
this.id = id;
|
|
54
|
+
this.hostKeys = hostKeys;
|
|
55
|
+
this.name = "AmbiguousHostIdError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function createHostIndex(noun = "session") {
|
|
59
|
+
/** id → live host keys, in the order they were noted. */
|
|
60
|
+
const live = /* @__PURE__ */ new Map();
|
|
61
|
+
/** id → removed host key → its name. */
|
|
62
|
+
const removed = /* @__PURE__ */ new Map();
|
|
63
|
+
const liveOf = (id) => [...live.get(id) ?? []];
|
|
64
|
+
const removedOf = (id, hostKey) => {
|
|
65
|
+
const marks = removed.get(id);
|
|
66
|
+
if (!marks) return void 0;
|
|
67
|
+
if (hostKey !== void 0) {
|
|
68
|
+
const name = marks.get(hostKey);
|
|
69
|
+
return name === void 0 ? void 0 : {
|
|
70
|
+
key: hostKey,
|
|
71
|
+
name
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const first = [...marks][0];
|
|
75
|
+
return first ? {
|
|
76
|
+
key: first[0],
|
|
77
|
+
name: first[1]
|
|
78
|
+
} : void 0;
|
|
79
|
+
};
|
|
80
|
+
const index = {
|
|
81
|
+
note: (id, hostKey) => {
|
|
82
|
+
let hosts = live.get(id);
|
|
83
|
+
if (!hosts) live.set(id, hosts = /* @__PURE__ */ new Set());
|
|
84
|
+
hosts.add(hostKey);
|
|
85
|
+
const marks = removed.get(id);
|
|
86
|
+
marks?.delete(hostKey);
|
|
87
|
+
if (marks?.size === 0) removed.delete(id);
|
|
88
|
+
},
|
|
89
|
+
forget: (id, hostKey) => {
|
|
90
|
+
if (hostKey === void 0) {
|
|
91
|
+
live.delete(id);
|
|
92
|
+
removed.delete(id);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
live.get(id)?.delete(hostKey);
|
|
96
|
+
if (live.get(id)?.size === 0) live.delete(id);
|
|
97
|
+
removed.get(id)?.delete(hostKey);
|
|
98
|
+
if (removed.get(id)?.size === 0) removed.delete(id);
|
|
99
|
+
},
|
|
100
|
+
hostOf: (id, hostKey) => {
|
|
101
|
+
const hosts = liveOf(id);
|
|
102
|
+
if (hostKey !== void 0) return hosts.includes(hostKey) ? hostKey : void 0;
|
|
103
|
+
return hosts.length === 1 ? hosts[0] : void 0;
|
|
104
|
+
},
|
|
105
|
+
hostsOf: liveOf,
|
|
106
|
+
require: (id, hostKey) => {
|
|
107
|
+
const hosts = liveOf(id);
|
|
108
|
+
if (hostKey !== void 0) {
|
|
109
|
+
if (hosts.includes(hostKey)) return hostKey;
|
|
110
|
+
const gone = removedOf(id, hostKey);
|
|
111
|
+
if (gone) throw new HostRemovedError(gone.key, gone.name);
|
|
112
|
+
throw new UnknownHostIdError(id, noun);
|
|
113
|
+
}
|
|
114
|
+
if (hosts.length === 1) return hosts[0];
|
|
115
|
+
if (hosts.length > 1) throw new AmbiguousHostIdError(id, hosts, noun);
|
|
116
|
+
const gone = removedOf(id);
|
|
117
|
+
if (gone) throw new HostRemovedError(gone.key, gone.name);
|
|
118
|
+
throw new UnknownHostIdError(id, noun);
|
|
119
|
+
},
|
|
120
|
+
removedHostOf: removedOf,
|
|
121
|
+
idsOf: (hostKey) => [...live].filter(([, hosts]) => hosts.has(hostKey)).map(([id]) => id),
|
|
122
|
+
dropHost: (hostKey, hostName) => {
|
|
123
|
+
const ids = index.idsOf(hostKey);
|
|
124
|
+
for (const id of ids) {
|
|
125
|
+
index.forget(id, hostKey);
|
|
126
|
+
let marks = removed.get(id);
|
|
127
|
+
if (!marks) removed.set(id, marks = /* @__PURE__ */ new Map());
|
|
128
|
+
marks.set(hostKey, hostName);
|
|
129
|
+
}
|
|
130
|
+
return ids;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
return index;
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/chat/hosts/session-key.ts
|
|
137
|
+
/**
|
|
138
|
+
* A session's identity across hosts: the (host, session) pair, as one
|
|
139
|
+
* string every client keys rows, maps and persisted state by. Two hosts may
|
|
140
|
+
* hold the same session id (a server sharing this computer's data dir, a
|
|
141
|
+
* copied store), so a bare id never names a session on its own once several
|
|
142
|
+
* hosts are listed.
|
|
143
|
+
*
|
|
144
|
+
* The host is named by its HOST KEY, the key of a client's host directory:
|
|
145
|
+
* {@link LOCAL_HOST_KEY} for This computer (the in-process host or the
|
|
146
|
+
* daemon), a saved server's `hostId` (for a server, hostKey === hostId),
|
|
147
|
+
* or a client's own key for a host it never saves (the TUI's `--host`
|
|
148
|
+
* server: `'ad-hoc'`).
|
|
149
|
+
*
|
|
150
|
+
* The encoding, shared by the TUI and the GUI:
|
|
151
|
+
*
|
|
152
|
+
* - This computer: the bare id (`abc`). Local-only users see exactly the
|
|
153
|
+
* ids, and state persisted before keys existed still means the same
|
|
154
|
+
* session.
|
|
155
|
+
* - Any other host: `<hostKey>␟<id>` (U+001F, never in a host key).
|
|
156
|
+
*
|
|
157
|
+
* A local id that itself contains U+001F is written `local␟<id>`, so a key
|
|
158
|
+
* always parses back to the pair it was made from. Node-free.
|
|
159
|
+
*/
|
|
160
|
+
const SESSION_KEY_SEPARATOR = "";
|
|
161
|
+
/** The key of `sessionId` on `hostKey` (`null` / absent / {@link LOCAL_HOST_KEY}: a local session). */
|
|
162
|
+
function sessionKey(hostKey, sessionId) {
|
|
163
|
+
if (!hostKey || hostKey === "local") return sessionId.includes("") ? `${LOCAL_HOST_KEY}${sessionId}` : sessionId;
|
|
164
|
+
if (hostKey.includes("")) throw new Error("A host key cannot contain U+001F.");
|
|
165
|
+
return `${hostKey}${sessionId}`;
|
|
166
|
+
}
|
|
167
|
+
/** The pair `key` names (a bare id: This computer's). */
|
|
168
|
+
function parseSessionKey(key) {
|
|
169
|
+
const at = key.indexOf("");
|
|
170
|
+
if (at < 0) return {
|
|
171
|
+
hostKey: LOCAL_HOST_KEY,
|
|
172
|
+
sessionId: key
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
hostKey: key.slice(0, at),
|
|
176
|
+
sessionId: key.slice(at + 1)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/chat/auth-rows.ts
|
|
181
|
+
/**
|
|
182
|
+
* Stored credentials, i.e. the ones a user can actually choose between.
|
|
183
|
+
*
|
|
184
|
+
* `env` is excluded: it is observed rather than stored, and is usually just the
|
|
185
|
+
* mirror `applyApiKeyEnv` wrote from the stored API key, so counting it would
|
|
186
|
+
* advertise a choice that does not exist.
|
|
187
|
+
*/
|
|
188
|
+
function selectableSources(provider) {
|
|
189
|
+
return provider.methods.filter((m) => m.source !== "env");
|
|
190
|
+
}
|
|
191
|
+
/** A provider earns a chevron only when there is a real choice to reveal. */
|
|
192
|
+
function isExpandableProvider(provider) {
|
|
193
|
+
return selectableSources(provider).length > 1;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Flatten the provider list plus the expanded set into the visible-row list.
|
|
197
|
+
*
|
|
198
|
+
* - `providers` — filtered / ordered provider list (post-search).
|
|
199
|
+
* - `expanded` — keys of providers whose sources are open. Keys for
|
|
200
|
+
* providers that are no longer expandable are ignored rather
|
|
201
|
+
* than cleaned up, so stale expansion state (a credential
|
|
202
|
+
* removed while the modal is open) can't desync the cursor.
|
|
203
|
+
*/
|
|
204
|
+
function buildVisibleAuthRows(providers, expanded) {
|
|
205
|
+
const out = [];
|
|
206
|
+
for (const provider of providers) {
|
|
207
|
+
const expandable = isExpandableProvider(provider);
|
|
208
|
+
out.push({
|
|
209
|
+
kind: "provider",
|
|
210
|
+
provider,
|
|
211
|
+
expandable
|
|
212
|
+
});
|
|
213
|
+
if (!expandable || !expanded.has(provider.key)) continue;
|
|
214
|
+
for (const method of selectableSources(provider)) out.push({
|
|
215
|
+
kind: "source",
|
|
216
|
+
providerKey: provider.key,
|
|
217
|
+
method
|
|
218
|
+
});
|
|
219
|
+
if (provider.envKey) {
|
|
220
|
+
const envMethod = provider.methods.find((m) => m.source === "env");
|
|
221
|
+
out.push({
|
|
222
|
+
kind: "source",
|
|
223
|
+
providerKey: provider.key,
|
|
224
|
+
method: envMethod ?? {
|
|
225
|
+
source: "env",
|
|
226
|
+
detail: provider.envKey,
|
|
227
|
+
active: false
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Provider key a row "belongs to" — its own for a provider header, the parent's
|
|
236
|
+
* for a source row.
|
|
237
|
+
*/
|
|
238
|
+
function parentProviderKey(row) {
|
|
239
|
+
return row.kind === "provider" ? row.provider.key : row.providerKey;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Index in `rows` of the header for the given provider key, or `-1`.
|
|
243
|
+
*
|
|
244
|
+
* Collapse handlers snap the cursor up to the parent BEFORE the row list
|
|
245
|
+
* shrinks; without that the cursor keeps its flat index and silently lands on
|
|
246
|
+
* whichever sibling row slid into that slot.
|
|
247
|
+
*/
|
|
248
|
+
function indexOfProviderRow(rows, providerKey) {
|
|
249
|
+
return rows.findIndex((r) => r.kind === "provider" && r.provider.key === providerKey);
|
|
250
|
+
}
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/chat/fold-actions.ts
|
|
253
|
+
/** Programs whose first argument is the thing they do (`git status`, `bun test`). */
|
|
254
|
+
const SUBCOMMAND_PROGRAMS = /* @__PURE__ */ new Set([
|
|
255
|
+
"git",
|
|
256
|
+
"gh",
|
|
257
|
+
"bun",
|
|
258
|
+
"npm",
|
|
259
|
+
"pnpm",
|
|
260
|
+
"yarn",
|
|
261
|
+
"npx",
|
|
262
|
+
"bunx",
|
|
263
|
+
"cargo",
|
|
264
|
+
"go",
|
|
265
|
+
"docker",
|
|
266
|
+
"kubectl",
|
|
267
|
+
"make",
|
|
268
|
+
"uv",
|
|
269
|
+
"pip",
|
|
270
|
+
"pip3",
|
|
271
|
+
"brew",
|
|
272
|
+
"deno",
|
|
273
|
+
"node",
|
|
274
|
+
"playwright",
|
|
275
|
+
"vitest",
|
|
276
|
+
"jest",
|
|
277
|
+
"tsc",
|
|
278
|
+
"eslint",
|
|
279
|
+
"prettier"
|
|
280
|
+
]);
|
|
281
|
+
/** Wrappers that only change how the next program runs. */
|
|
282
|
+
const WRAPPERS = /* @__PURE__ */ new Set([
|
|
283
|
+
"sudo",
|
|
284
|
+
"time",
|
|
285
|
+
"nice",
|
|
286
|
+
"nohup",
|
|
287
|
+
"exec",
|
|
288
|
+
"command",
|
|
289
|
+
"caffeinate"
|
|
290
|
+
]);
|
|
291
|
+
/** Setup steps that carry no meaning of their own. */
|
|
292
|
+
const SETUP = /* @__PURE__ */ new Set([
|
|
293
|
+
"cd",
|
|
294
|
+
"pushd",
|
|
295
|
+
"popd",
|
|
296
|
+
"export",
|
|
297
|
+
"set",
|
|
298
|
+
"source",
|
|
299
|
+
".",
|
|
300
|
+
"unset",
|
|
301
|
+
"true",
|
|
302
|
+
"mkdir"
|
|
303
|
+
]);
|
|
304
|
+
/** Split on `&&`, `||`, `;`, `|` and newlines outside quotes. */
|
|
305
|
+
function splitSegments(command) {
|
|
306
|
+
const out = [];
|
|
307
|
+
let cur = "";
|
|
308
|
+
let quote = null;
|
|
309
|
+
for (let i = 0; i < command.length; i++) {
|
|
310
|
+
const ch = command[i];
|
|
311
|
+
if (quote) {
|
|
312
|
+
if (ch === quote) quote = null;
|
|
313
|
+
cur += ch;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (ch === "\"" || ch === "'") {
|
|
317
|
+
quote = ch;
|
|
318
|
+
cur += ch;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const two = command.slice(i, i + 2);
|
|
322
|
+
if (two === "&&" || two === "||") {
|
|
323
|
+
out.push(cur);
|
|
324
|
+
cur = "";
|
|
325
|
+
i++;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (ch === ";" || ch === "|" || ch === "\n") {
|
|
329
|
+
out.push(cur);
|
|
330
|
+
cur = "";
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
cur += ch;
|
|
334
|
+
}
|
|
335
|
+
out.push(cur);
|
|
336
|
+
return out.map((s) => s.trim()).filter(Boolean);
|
|
337
|
+
}
|
|
338
|
+
/** Words of a segment, quotes kept as one word (unquoted). */
|
|
339
|
+
function words(segment) {
|
|
340
|
+
const out = [];
|
|
341
|
+
for (const m of segment.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)) out.push(m[1] ?? m[2] ?? m[3] ?? "");
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
/** The gist of one segment (`bun run build`, `python3`), or null for setup noise. */
|
|
345
|
+
function segmentGist(segment) {
|
|
346
|
+
let w = words(segment);
|
|
347
|
+
for (;;) {
|
|
348
|
+
if (w.length === 0) return null;
|
|
349
|
+
const head = w[0];
|
|
350
|
+
if (/^[A-Z_]\w*=/i.test(head)) w = w.slice(1);
|
|
351
|
+
else if (WRAPPERS.has(head)) w = w.slice(1);
|
|
352
|
+
else if (head === "env") {
|
|
353
|
+
w = w.slice(1);
|
|
354
|
+
while (w[0] && (w[0].startsWith("-") || /^[A-Z_]\w*=/i.test(w[0]))) {
|
|
355
|
+
const flag = w[0];
|
|
356
|
+
w = w.slice(1);
|
|
357
|
+
if (flag === "-u" && w[0]) w = w.slice(1);
|
|
358
|
+
}
|
|
359
|
+
} else if (head === "timeout" && w[1] && /^\d/.test(w[1])) w = w.slice(2);
|
|
360
|
+
else break;
|
|
361
|
+
}
|
|
362
|
+
const program = w[0].split("/").pop();
|
|
363
|
+
if (SETUP.has(program)) return null;
|
|
364
|
+
if (!SUBCOMMAND_PROGRAMS.has(program)) return program;
|
|
365
|
+
const rest = w.slice(1).filter((arg) => !arg.startsWith("-"));
|
|
366
|
+
let sub = rest[0];
|
|
367
|
+
if ((program === "npx" || program === "bunx") && sub) {
|
|
368
|
+
const tool = sub.split("/").pop();
|
|
369
|
+
const next = rest[1];
|
|
370
|
+
return SUBCOMMAND_PROGRAMS.has(tool) && next && !next.includes("/") && !next.includes(".") ? `${tool} ${next}` : tool;
|
|
371
|
+
}
|
|
372
|
+
if ((sub === "run" || sub === "exec") && rest[1] && [
|
|
373
|
+
"bun",
|
|
374
|
+
"npm",
|
|
375
|
+
"pnpm",
|
|
376
|
+
"yarn"
|
|
377
|
+
].includes(program)) return `${program} ${sub} ${rest[1]}`;
|
|
378
|
+
if (sub && (sub.includes("/") || /\.[a-z]+$/i.test(sub))) sub = void 0;
|
|
379
|
+
return sub ? `${program} ${sub}` : program;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* `Ran <gist>` for a shell command: heredoc bodies dropped, setup steps
|
|
383
|
+
* (`cd`, `export`, `set -e`) skipped, env and wrappers peeled off, program
|
|
384
|
+
* plus subcommand kept. Further meaningful steps read as `+N`.
|
|
385
|
+
*/
|
|
386
|
+
function commandGist(command) {
|
|
387
|
+
const gists = splitSegments(command.replace(/<<-?\s*(['"]?)(\w+)\1[\s\S]*?(?:\n\2\b|$)/g, "")).map(segmentGist).filter((g) => g !== null);
|
|
388
|
+
if (gists.length === 0) return {
|
|
389
|
+
gist: "a command",
|
|
390
|
+
more: 0
|
|
391
|
+
};
|
|
392
|
+
return {
|
|
393
|
+
gist: gists[0],
|
|
394
|
+
more: gists.length - 1
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function splitPath(path) {
|
|
398
|
+
const clean = path.replace(/\/+$/, "");
|
|
399
|
+
const i = clean.lastIndexOf("/");
|
|
400
|
+
return i < 0 ? {
|
|
401
|
+
name: clean,
|
|
402
|
+
dir: ""
|
|
403
|
+
} : {
|
|
404
|
+
name: clean.slice(i + 1),
|
|
405
|
+
dir: clean.slice(0, i)
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function str$1(input, key) {
|
|
409
|
+
const v = input?.[key];
|
|
410
|
+
return typeof v === "string" && v.trim() !== "" ? v : void 0;
|
|
411
|
+
}
|
|
412
|
+
/** Line counts exactly as the diff in the details shows them. */
|
|
413
|
+
function editCounts(edit) {
|
|
414
|
+
if (!edit) return null;
|
|
415
|
+
let added = 0;
|
|
416
|
+
let removed = 0;
|
|
417
|
+
for (const h of edit.hunks) for (const d of computeLineDiff(h.oldString, h.newString)) if (d.op === "add") added++;
|
|
418
|
+
else if (d.op === "remove") removed++;
|
|
419
|
+
return {
|
|
420
|
+
added,
|
|
421
|
+
removed
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function shortUrl(url) {
|
|
425
|
+
const short = url.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
426
|
+
return short.length > 48 ? `${short.slice(0, 47)}…` : short;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* The dev browser in words: where it went and what it did. Clicks and typing
|
|
430
|
+
* read their result's first line (`Clicked "Save"`), which names the element;
|
|
431
|
+
* the ref the model used (`e12`) means nothing to a person.
|
|
432
|
+
*/
|
|
433
|
+
function browserOverview(tool) {
|
|
434
|
+
const input = tool.input;
|
|
435
|
+
const url = str$1(input, "url");
|
|
436
|
+
const head = typeof tool.result === "string" && tool.ok ? tool.result.split("\n")[0].replace(/\. No navigation, dialog or error\.$|\.$/, "") : "";
|
|
437
|
+
switch (tool.name) {
|
|
438
|
+
case "browser_open": return url ? {
|
|
439
|
+
label: `Opened ${shortUrl(url)}`,
|
|
440
|
+
title: url
|
|
441
|
+
} : { label: "Opened the browser" };
|
|
442
|
+
case "browser_navigate": {
|
|
443
|
+
const action = str$1(input, "action");
|
|
444
|
+
if (action === "back") return { label: "Went back" };
|
|
445
|
+
if (action === "forward") return { label: "Went forward" };
|
|
446
|
+
if (action === "reload") return { label: "Reloaded the page" };
|
|
447
|
+
return url ? {
|
|
448
|
+
label: `Went to ${shortUrl(url)}`,
|
|
449
|
+
title: url
|
|
450
|
+
} : { label: "Navigated" };
|
|
451
|
+
}
|
|
452
|
+
case "browser_snapshot": return { label: "Read the page" };
|
|
453
|
+
case "browser_screenshot": return { label: "Took a screenshot" };
|
|
454
|
+
case "browser_click": return { label: head.startsWith("Clicked") ? head : `Clicked ${tool.target ?? "the page"}` };
|
|
455
|
+
case "browser_type": return { label: head.startsWith("Typed") ? head : "Typed in the page" };
|
|
456
|
+
case "browser_press": return { label: head.startsWith("Pressed") ? head : `Pressed ${str$1(input, "key") ?? "a key"}` };
|
|
457
|
+
case "browser_scroll": return { label: "Scrolled the page" };
|
|
458
|
+
case "browser_wait_for": {
|
|
459
|
+
const what = str$1(input, "text") ?? str$1(input, "selector") ?? str$1(input, "urlIncludes");
|
|
460
|
+
return { label: what ? `Waited for “${what.length > 40 ? `${what.slice(0, 39)}…` : what}”` : "Waited" };
|
|
461
|
+
}
|
|
462
|
+
case "browser_evaluate": {
|
|
463
|
+
const expression = str$1(input, "expression") ?? "";
|
|
464
|
+
return {
|
|
465
|
+
label: "Ran a script in the page",
|
|
466
|
+
...expression ? { title: expression.slice(0, 600) } : {}
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
case "browser_console": return { label: "Read the console" };
|
|
470
|
+
default: return null;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/** The overview line for one tool call. */
|
|
474
|
+
function toolOverview(tool) {
|
|
475
|
+
const input = tool.input;
|
|
476
|
+
if (tool.name.startsWith("browser_")) {
|
|
477
|
+
const browser = browserOverview(tool);
|
|
478
|
+
if (browser) return browser;
|
|
479
|
+
}
|
|
480
|
+
switch (tool.name) {
|
|
481
|
+
case "shell":
|
|
482
|
+
case "bash": {
|
|
483
|
+
const command = str$1(input, "command") ?? tool.target ?? "";
|
|
484
|
+
const description = str$1(input, "description")?.trim();
|
|
485
|
+
if (description) return {
|
|
486
|
+
label: description.length > 120 ? `${description.slice(0, 119)}…` : description,
|
|
487
|
+
title: command.slice(0, 600)
|
|
488
|
+
};
|
|
489
|
+
const { gist, more } = commandGist(command);
|
|
490
|
+
return {
|
|
491
|
+
label: `Ran ${gist}`,
|
|
492
|
+
...more > 0 ? { hint: `+${more}` } : {},
|
|
493
|
+
title: command.slice(0, 600)
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
case "read_file": {
|
|
497
|
+
const batch = str$1(input, "path") ? [] : readFileInputPaths(input);
|
|
498
|
+
if (batch.length > 1) return {
|
|
499
|
+
label: `Read ${splitPath(batch[0]).name} +${batch.length - 1}`,
|
|
500
|
+
title: batch.join("\n")
|
|
501
|
+
};
|
|
502
|
+
const path = str$1(input, "path") ?? batch[0] ?? tool.target ?? "";
|
|
503
|
+
const { name, dir } = splitPath(path);
|
|
504
|
+
return {
|
|
505
|
+
label: `Read ${name || "a file"}`,
|
|
506
|
+
...dir ? { hint: dir } : {},
|
|
507
|
+
title: path
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
case "edit":
|
|
511
|
+
case "multi_edit":
|
|
512
|
+
case "write_file": {
|
|
513
|
+
const path = str$1(input, "path") ?? tool.edit?.path ?? tool.target ?? "";
|
|
514
|
+
const { name, dir } = splitPath(path);
|
|
515
|
+
const counts = editCounts(tool.edit);
|
|
516
|
+
return {
|
|
517
|
+
label: `${tool.name === "write_file" ? "Wrote" : "Edited"} ${name || "a file"}`,
|
|
518
|
+
...dir ? { hint: dir } : {},
|
|
519
|
+
...counts && (counts.added > 0 || counts.removed > 0) ? counts : {},
|
|
520
|
+
title: path
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
case "grep": {
|
|
524
|
+
const pattern = str$1(input, "pattern") ?? tool.target ?? "";
|
|
525
|
+
const where = str$1(input, "path");
|
|
526
|
+
return {
|
|
527
|
+
label: pattern ? `Searched for “${pattern.length > 48 ? `${pattern.slice(0, 47)}…` : pattern}”` : "Searched",
|
|
528
|
+
...where ? { hint: where } : {},
|
|
529
|
+
title: pattern
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
case "glob": {
|
|
533
|
+
const pattern = str$1(input, "pattern") ?? tool.target ?? "";
|
|
534
|
+
return {
|
|
535
|
+
label: pattern ? `Found files ${pattern}` : "Found files",
|
|
536
|
+
title: pattern
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
case "spawn": {
|
|
540
|
+
const task = str$1(input, "task") ?? tool.target ?? "";
|
|
541
|
+
const first = task.split("\n").find((line) => line.trim()) ?? "";
|
|
542
|
+
return {
|
|
543
|
+
label: "Agent",
|
|
544
|
+
...first ? { hint: first.length > 80 ? `${first.slice(0, 79)}…` : first } : {},
|
|
545
|
+
...task ? { title: task.slice(0, 600) } : {}
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
case "present_plan": {
|
|
549
|
+
const title = str$1(input, "title");
|
|
550
|
+
return {
|
|
551
|
+
label: "Presented a plan",
|
|
552
|
+
...title ? {
|
|
553
|
+
hint: title,
|
|
554
|
+
title
|
|
555
|
+
} : {}
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
case "ask_user": {
|
|
559
|
+
const n = Array.isArray(input?.questions) ? input.questions.length : 0;
|
|
560
|
+
return { label: n > 0 ? `Asked ${n} question${n === 1 ? "" : "s"}` : "Asked a question" };
|
|
561
|
+
}
|
|
562
|
+
case "todowrite": {
|
|
563
|
+
const todos = Array.isArray(input?.todos) ? input.todos : [];
|
|
564
|
+
const done = todos.filter((t) => t.status === "completed").length;
|
|
565
|
+
return {
|
|
566
|
+
label: "Updated todos",
|
|
567
|
+
...todos.length > 0 ? { hint: `${done} of ${todos.length} done` } : {}
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
case "list_files": {
|
|
571
|
+
const path = str$1(input, "path") ?? tool.target ?? ".";
|
|
572
|
+
return {
|
|
573
|
+
label: `Listed ${path}`,
|
|
574
|
+
title: path
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
default: {
|
|
578
|
+
const target = tool.target;
|
|
579
|
+
const short = target && target.length > 60 ? `${target.slice(0, 59)}…` : target;
|
|
580
|
+
return {
|
|
581
|
+
label: short ? `${tool.displayName} ${short}` : tool.displayName,
|
|
582
|
+
...target ? { title: target } : {}
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Verb phrase per canonical tool name for a batch summary. Unknown tools
|
|
589
|
+
* derive one from their display name, so MCP and host tools read cleanly.
|
|
590
|
+
*/
|
|
591
|
+
const BATCH_VERB_PHRASE = {
|
|
592
|
+
shell: "ran a command",
|
|
593
|
+
read_file: "read a file",
|
|
594
|
+
grep: "searched",
|
|
595
|
+
glob: "searched for files",
|
|
596
|
+
list_files: "listed files",
|
|
597
|
+
edit: "edited a file",
|
|
598
|
+
multi_edit: "edited a file",
|
|
599
|
+
write_file: "created a file",
|
|
600
|
+
spawn: "spawned a subagent",
|
|
601
|
+
tool_search: "searched for tools",
|
|
602
|
+
skills_use: "activated a skill",
|
|
603
|
+
skills_read: "read a skill",
|
|
604
|
+
skills_run_script: "ran a skill script",
|
|
605
|
+
ask_user: "asked a question",
|
|
606
|
+
present_plan: "presented a plan",
|
|
607
|
+
browser_open: "opened the browser",
|
|
608
|
+
browser_navigate: "navigated",
|
|
609
|
+
browser_snapshot: "read the page",
|
|
610
|
+
browser_screenshot: "took a screenshot",
|
|
611
|
+
browser_click: "clicked",
|
|
612
|
+
browser_type: "typed",
|
|
613
|
+
browser_press: "pressed a key",
|
|
614
|
+
browser_scroll: "scrolled",
|
|
615
|
+
browser_wait_for: "waited",
|
|
616
|
+
browser_evaluate: "ran a script in the page",
|
|
617
|
+
browser_console: "read the console",
|
|
618
|
+
terminal_list: "listed terminals",
|
|
619
|
+
terminal_read: "read a terminal",
|
|
620
|
+
terminal_write: "typed in a terminal"
|
|
621
|
+
};
|
|
622
|
+
function toolPhrase(name, displayName) {
|
|
623
|
+
return BATCH_VERB_PHRASE[name] ?? `used ${displayName.toLowerCase()}`;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Cap on distinct phrases in a summary: a 40-call exchange reads "Ran a
|
|
627
|
+
* command, searched, read a file, edited a file +23", where +N counts the
|
|
628
|
+
* calls the shown phrases do not cover.
|
|
629
|
+
*/
|
|
630
|
+
const MAX_SUMMARY_PHRASES = 4;
|
|
631
|
+
/**
|
|
632
|
+
* "Ran a command, read a file +2" for a run of calls. Adjacent identical
|
|
633
|
+
* phrases merge, so three reads in a row say "read a file" once.
|
|
634
|
+
*/
|
|
635
|
+
function summarizeTools(tools) {
|
|
636
|
+
const phrases = [];
|
|
637
|
+
for (const t of tools) {
|
|
638
|
+
const phrase = toolPhrase(t.name, t.displayName);
|
|
639
|
+
const last = phrases[phrases.length - 1];
|
|
640
|
+
if (last && last.phrase === phrase) last.calls += 1;
|
|
641
|
+
else phrases.push({
|
|
642
|
+
phrase,
|
|
643
|
+
calls: 1
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
if (phrases.length === 0) return "Ran tools";
|
|
647
|
+
const shown = phrases.slice(0, MAX_SUMMARY_PHRASES);
|
|
648
|
+
const shownCalls = shown.reduce((n, p) => n + p.calls, 0);
|
|
649
|
+
const rest = tools.length - shownCalls;
|
|
650
|
+
const joined = shown.map((p) => p.phrase).join(", ");
|
|
651
|
+
const capped = rest > 0 ? `${joined} +${rest}` : joined;
|
|
652
|
+
return capped.charAt(0).toUpperCase() + capped.slice(1);
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* A message as a person skims it: its words, markdown syntax and code
|
|
656
|
+
* blocks dropped, whitespace folded. For the compact stacks of earlier
|
|
657
|
+
* messages.
|
|
658
|
+
*/
|
|
659
|
+
function messagePreview(text) {
|
|
660
|
+
return text.replace(/```[\s\S]*?(?:```|$)/g, " ").replace(/`([^`]*)`/g, "$1").replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/^\s{0,3}(?:#{1,6}\s+|>\s?|[-*+]\s+|\d+[.)]\s+)/gm, "").replace(/(\*\*|__|\*|_|~~)(\S(?:.*?\S)?)\1/g, "$2").replace(/\s+/g, " ").trim();
|
|
661
|
+
}
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/chat/instruction-rows.ts
|
|
664
|
+
/**
|
|
665
|
+
* Upper bound on preview lines rendered for one expanded segment. Mirrors the
|
|
666
|
+
* Logs tab's render cap: the scrollbox can hold arbitrary height, but pasting a
|
|
667
|
+
* runaway 64KB instruction file into the layout pass is a waste of a frame.
|
|
668
|
+
* File-backed segments have `$EDITOR` as the escape hatch for the remainder.
|
|
669
|
+
*/
|
|
670
|
+
const INSTRUCTION_PREVIEW_LINE_CAP = 400;
|
|
671
|
+
/**
|
|
672
|
+
* Filter segments by the active query and resolve each one's expanded state.
|
|
673
|
+
*
|
|
674
|
+
* Deliberately does NOT auto-expand matches the way the Extensions tab does:
|
|
675
|
+
* the search corpus includes each segment's full text, so a query typically
|
|
676
|
+
* matches deep inside a long document — auto-expanding every hit would dump
|
|
677
|
+
* thousands of lines into the viewport on a keystroke. The user expands the row
|
|
678
|
+
* they care about.
|
|
679
|
+
*/
|
|
680
|
+
function buildVisibleInstructionRows(opts) {
|
|
681
|
+
const { segments, expanded, queryActive, matchSegment } = opts;
|
|
682
|
+
const out = [];
|
|
683
|
+
for (const segment of segments) {
|
|
684
|
+
if (queryActive && !matchSegment(segment)) continue;
|
|
685
|
+
out.push({
|
|
686
|
+
segment,
|
|
687
|
+
expanded: expanded.has(segment.id)
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
return out;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Split a segment's text into renderable preview lines, clamped to
|
|
694
|
+
* {@link INSTRUCTION_PREVIEW_LINE_CAP}.
|
|
695
|
+
*
|
|
696
|
+
* `hidden` is how many lines were dropped off the tail, so the view can render
|
|
697
|
+
* an honest "+N more lines" marker instead of silently truncating. Tabs are
|
|
698
|
+
* expanded to two spaces because the terminal text renderable doesn't
|
|
699
|
+
* interpret `\t`, and `\r` is stripped so CRLF files don't leave artefacts at
|
|
700
|
+
* every line end.
|
|
701
|
+
*/
|
|
702
|
+
function instructionPreviewLines(text) {
|
|
703
|
+
const all = text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").split("\n");
|
|
704
|
+
while (all.length > 0 && all[all.length - 1].trim() === "") all.pop();
|
|
705
|
+
if (all.length <= 400) return {
|
|
706
|
+
lines: all,
|
|
707
|
+
hidden: 0
|
|
708
|
+
};
|
|
709
|
+
return {
|
|
710
|
+
lines: all.slice(0, 400),
|
|
711
|
+
hidden: all.length - 400
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Short badge for a segment's origin — the "where does this come from" column.
|
|
716
|
+
* `'file'` is intentionally the only one that reads like a location, since it's
|
|
717
|
+
* the only origin the user can act on.
|
|
718
|
+
*/
|
|
719
|
+
function instructionOriginBadge(segment) {
|
|
720
|
+
switch (segment.origin) {
|
|
721
|
+
case "file": return "file";
|
|
722
|
+
case "generated": return "generated";
|
|
723
|
+
case "mcp": return "mcp server";
|
|
724
|
+
case "built-in": return "built-in";
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
//#endregion
|
|
728
|
+
//#region src/chat/machines-view.ts
|
|
729
|
+
/** The `label` of the notices a host emits about a session's machine. */
|
|
730
|
+
const MACHINE_NOTICE_LABEL = "machine";
|
|
731
|
+
const WAITING_RE = /^waiting for (\S+?)…/;
|
|
732
|
+
const BACK_RE = /^\S+ is back\./;
|
|
733
|
+
const GAVE_UP_RE = /^\S+ stayed offline for /;
|
|
734
|
+
/**
|
|
735
|
+
* The machine a session's run is paused on, from its notices (`waiting for
|
|
736
|
+
* laptop…` until `laptop is back.` or `laptop stayed offline … run stopped`),
|
|
737
|
+
* `null` when none. `online`: machines known to be back since (a
|
|
738
|
+
* `machine:status` the window saw), which clear the wait at once.
|
|
739
|
+
*/
|
|
740
|
+
function machineWaiting(events, online = /* @__PURE__ */ new Set()) {
|
|
741
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
742
|
+
const event = events[i];
|
|
743
|
+
if (event.notice?.label !== "machine" || typeof event.text !== "string") continue;
|
|
744
|
+
if (BACK_RE.test(event.text) || GAVE_UP_RE.test(event.text)) return null;
|
|
745
|
+
const waiting = WAITING_RE.exec(event.text)?.[1];
|
|
746
|
+
if (waiting) return online.has(waiting) ? null : waiting;
|
|
747
|
+
}
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
function dirname(path) {
|
|
751
|
+
const trimmed = path.replace(/\/+$/, "");
|
|
752
|
+
const at = trimmed.lastIndexOf("/");
|
|
753
|
+
return at <= 0 ? "/" : trimmed.slice(0, at);
|
|
754
|
+
}
|
|
755
|
+
function basename(path) {
|
|
756
|
+
const trimmed = path.replace(/\/+$/, "");
|
|
757
|
+
return trimmed.slice(trimmed.lastIndexOf("/") + 1) || trimmed;
|
|
758
|
+
}
|
|
759
|
+
/** `machines.listDirs` as a folder listing: the parent stops at the root the folder is in. */
|
|
760
|
+
function machineDirListing(result) {
|
|
761
|
+
const { path } = result;
|
|
762
|
+
return {
|
|
763
|
+
roots: result.roots,
|
|
764
|
+
path,
|
|
765
|
+
parent: result.roots.includes(path) ? null : dirname(path),
|
|
766
|
+
entries: result.dirs.map((dir) => ({
|
|
767
|
+
path: dir,
|
|
768
|
+
name: basename(dir),
|
|
769
|
+
isGitRepo: false
|
|
770
|
+
}))
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Environment kinds in picker order: this host's own first, then each
|
|
775
|
+
* machine's together (`local@` before `worktree@`), machines by name.
|
|
776
|
+
*/
|
|
777
|
+
function orderByMachine(descriptors) {
|
|
778
|
+
const rank = (d) => {
|
|
779
|
+
const parsed = parseMachineKind(d.kind);
|
|
780
|
+
return parsed ? [
|
|
781
|
+
1,
|
|
782
|
+
parsed.name,
|
|
783
|
+
parsed.base === "local" ? 0 : 1
|
|
784
|
+
] : [
|
|
785
|
+
0,
|
|
786
|
+
"",
|
|
787
|
+
0
|
|
788
|
+
];
|
|
789
|
+
};
|
|
790
|
+
return descriptors.map((d, i) => ({
|
|
791
|
+
d,
|
|
792
|
+
i,
|
|
793
|
+
r: rank(d)
|
|
794
|
+
})).sort((a, b) => a.r[0] - b.r[0] || a.r[1].localeCompare(b.r[1]) || a.r[2] - b.r[2] || a.i - b.i).map((entry) => entry.d);
|
|
795
|
+
}
|
|
796
|
+
/** Why a machine kind can't be picked now (its machine is offline), else `undefined`. */
|
|
797
|
+
function machineBlockedReason(descriptor) {
|
|
798
|
+
return descriptor.machine?.status === "offline" ? `${descriptor.machine.name} is offline` : void 0;
|
|
799
|
+
}
|
|
800
|
+
/** A session's environment as its header shows it: `local@laptop`, `worktree@laptop ⎇ feat`, `⎇ feat`, or the kind. */
|
|
801
|
+
function environmentLabel(environment) {
|
|
802
|
+
if (parseMachineKind(environment.kind)) return environment.branch ? `${environment.kind} ⎇ ${environment.branch}` : environment.kind;
|
|
803
|
+
return environment.branch ? `⎇ ${environment.branch}` : environment.kind;
|
|
804
|
+
}
|
|
805
|
+
const MACHINE_LINK_STATE_TEXT = {
|
|
806
|
+
"connecting": "connecting…",
|
|
807
|
+
"connected": "connected",
|
|
808
|
+
"reconnecting": "reconnecting…",
|
|
809
|
+
"offline": "offline",
|
|
810
|
+
"auth-refused": "refused",
|
|
811
|
+
"identity": "another server answers there",
|
|
812
|
+
"refused": "refused"
|
|
813
|
+
};
|
|
814
|
+
/** "seen 5 min ago" for a machine's `lastSeen`. */
|
|
815
|
+
function lastSeenText(lastSeen, now) {
|
|
816
|
+
if (lastSeen === void 0) return "never seen";
|
|
817
|
+
const minutes = Math.max(0, Math.round((now - lastSeen) / 6e4));
|
|
818
|
+
if (minutes < 1) return "seen just now";
|
|
819
|
+
if (minutes < 60) return `seen ${minutes} min ago`;
|
|
820
|
+
const hours = Math.round(minutes / 60);
|
|
821
|
+
return hours < 48 ? `seen ${hours} h ago` : `seen ${Math.round(hours / 24)} d ago`;
|
|
822
|
+
}
|
|
823
|
+
//#endregion
|
|
824
|
+
//#region src/chat/mcp-form.ts
|
|
825
|
+
const MCP_TRANSPORTS = [
|
|
826
|
+
"stdio",
|
|
827
|
+
"streamable-http",
|
|
828
|
+
"sse"
|
|
829
|
+
];
|
|
830
|
+
/**
|
|
831
|
+
* Validate + normalize the raw field values into a config entry.
|
|
832
|
+
* Returns the first problem in row order so the dialog can surface it.
|
|
833
|
+
*/
|
|
834
|
+
function validateMcpForm(values) {
|
|
835
|
+
const name = values.name.trim();
|
|
836
|
+
if (name.length === 0) return {
|
|
837
|
+
ok: false,
|
|
838
|
+
error: "name is required"
|
|
839
|
+
};
|
|
840
|
+
if (!/^[\w.-]+$/.test(name)) return {
|
|
841
|
+
ok: false,
|
|
842
|
+
error: "name must be alphanumerics, dots, hyphens, underscores"
|
|
843
|
+
};
|
|
844
|
+
if (values.transport === "stdio") {
|
|
845
|
+
const tokens = splitCommandLine(values.commandLine);
|
|
846
|
+
if (tokens.length === 0) return {
|
|
847
|
+
ok: false,
|
|
848
|
+
error: "command is required for stdio servers"
|
|
849
|
+
};
|
|
850
|
+
const env = parseKeyValueLines(values.env, {
|
|
851
|
+
label: "env",
|
|
852
|
+
separators: ["="]
|
|
853
|
+
});
|
|
854
|
+
if (!env.ok) return {
|
|
855
|
+
ok: false,
|
|
856
|
+
error: env.error
|
|
857
|
+
};
|
|
858
|
+
return {
|
|
859
|
+
ok: true,
|
|
860
|
+
result: {
|
|
861
|
+
entry: {
|
|
862
|
+
name,
|
|
863
|
+
transport: "stdio",
|
|
864
|
+
command: tokens[0],
|
|
865
|
+
...tokens.length > 1 ? { args: tokens.slice(1) } : {},
|
|
866
|
+
...Object.keys(env.pairs).length > 0 ? { env: env.pairs } : {}
|
|
867
|
+
},
|
|
868
|
+
scope: values.scope
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
const url = values.url.trim();
|
|
873
|
+
if (url.length === 0) return {
|
|
874
|
+
ok: false,
|
|
875
|
+
error: "url is required for http/sse servers"
|
|
876
|
+
};
|
|
877
|
+
if (!/^https?:\/\//.test(url)) return {
|
|
878
|
+
ok: false,
|
|
879
|
+
error: "url must start with http:// or https://"
|
|
880
|
+
};
|
|
881
|
+
const headers = parseKeyValueLines(values.headers, {
|
|
882
|
+
label: "headers",
|
|
883
|
+
separators: [":", "="]
|
|
884
|
+
});
|
|
885
|
+
if (!headers.ok) return {
|
|
886
|
+
ok: false,
|
|
887
|
+
error: headers.error
|
|
888
|
+
};
|
|
889
|
+
return {
|
|
890
|
+
ok: true,
|
|
891
|
+
result: {
|
|
892
|
+
entry: {
|
|
893
|
+
name,
|
|
894
|
+
transport: values.transport,
|
|
895
|
+
url,
|
|
896
|
+
...Object.keys(headers.pairs).length > 0 ? { headers: headers.pairs } : {}
|
|
897
|
+
},
|
|
898
|
+
scope: values.scope
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Split a command line into tokens, honoring single/double quotes so
|
|
904
|
+
* `npx -y my-server --root "/path with spaces"` round-trips. No escape
|
|
905
|
+
* processing beyond quote grouping — this is a form field, not a shell.
|
|
906
|
+
*/
|
|
907
|
+
function splitCommandLine(line) {
|
|
908
|
+
const out = [];
|
|
909
|
+
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
910
|
+
for (let m = re.exec(line); m !== null; m = re.exec(line)) out.push(m[1] ?? m[2] ?? m[3]);
|
|
911
|
+
return out;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Parse one key/value pair PER LINE. Line-based (not whitespace-split)
|
|
915
|
+
* because values legitimately contain spaces — `Authorization: Bearer
|
|
916
|
+
* abc123` is the canonical headers case, and env values like
|
|
917
|
+
* `GREETING=hello world` are equally valid. The first separator found
|
|
918
|
+
* (in `separators` priority order, `:` before `=` for headers) splits;
|
|
919
|
+
* the rest of the line is the value, trimmed. Blank lines skipped.
|
|
920
|
+
*/
|
|
921
|
+
function parseKeyValueLines(raw, opts) {
|
|
922
|
+
const pairs = {};
|
|
923
|
+
for (const line of raw.split("\n")) {
|
|
924
|
+
const trimmed = line.trim();
|
|
925
|
+
if (trimmed.length === 0) continue;
|
|
926
|
+
let splitAt = -1;
|
|
927
|
+
for (const sep of opts.separators) {
|
|
928
|
+
const idx = trimmed.indexOf(sep);
|
|
929
|
+
if (idx > 0 && (splitAt === -1 || idx < splitAt)) splitAt = idx;
|
|
930
|
+
}
|
|
931
|
+
if (splitAt <= 0) {
|
|
932
|
+
const forms = opts.separators.map((s) => `KEY${s}VALUE`).join(" or ");
|
|
933
|
+
return {
|
|
934
|
+
ok: false,
|
|
935
|
+
error: `${opts.label} entries must be ${forms}, one per line (got "${trimmed}")`
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
const key = trimmed.slice(0, splitAt).trim();
|
|
939
|
+
const value = trimmed.slice(splitAt + 1).trim();
|
|
940
|
+
if (key.length === 0) return {
|
|
941
|
+
ok: false,
|
|
942
|
+
error: `${opts.label} entry has an empty key ("${trimmed}")`
|
|
943
|
+
};
|
|
944
|
+
pairs[key] = value;
|
|
945
|
+
}
|
|
946
|
+
return {
|
|
947
|
+
ok: true,
|
|
948
|
+
pairs
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
//#endregion
|
|
952
|
+
//#region src/chat/model-name.ts
|
|
953
|
+
/** Brand words a first-party provider's names start with. */
|
|
954
|
+
const OWN_BRAND = {
|
|
955
|
+
anthropic: /^claude[\s-]+/i,
|
|
956
|
+
openai: /^openai[\s:-]+/i
|
|
957
|
+
};
|
|
958
|
+
const ACRONYMS = /* @__PURE__ */ new Set([
|
|
959
|
+
"gpt",
|
|
960
|
+
"glm",
|
|
961
|
+
"oss",
|
|
962
|
+
"ai",
|
|
963
|
+
"r1",
|
|
964
|
+
"vl"
|
|
965
|
+
]);
|
|
966
|
+
/** `claude-opus-4-7-20250722` → `Claude Opus 4.7`; `gpt-5.5` → `GPT 5.5`. */
|
|
967
|
+
function readableId(id) {
|
|
968
|
+
const parts = (id.split("/").pop() ?? id).replace(/-\d{8}$/, "").split(/[-_]/).filter(Boolean);
|
|
969
|
+
const out = [];
|
|
970
|
+
for (const part of parts) {
|
|
971
|
+
const prev = out[out.length - 1];
|
|
972
|
+
if (/^\d+$/.test(part) && prev && /^\d+(?:\.\d+)*$/.test(prev)) {
|
|
973
|
+
out[out.length - 1] = `${prev}.${part}`;
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
out.push(ACRONYMS.has(part.toLowerCase()) ? part.toUpperCase() : part.charAt(0).toUpperCase() + part.slice(1));
|
|
977
|
+
}
|
|
978
|
+
return out.join(" ");
|
|
979
|
+
}
|
|
980
|
+
function modelDisplayName({ providerKey, modelId, modelName }) {
|
|
981
|
+
let name = (modelName && modelName !== modelId ? modelName : readableId(modelId)).trim();
|
|
982
|
+
let vendor;
|
|
983
|
+
name = name.replace(/\s*\(latest\)\s*$/i, "");
|
|
984
|
+
const prefixed = /^([^:]{2,40}):\s+(\S.*)$/.exec(name);
|
|
985
|
+
if (prefixed) {
|
|
986
|
+
vendor = prefixed[1].trim();
|
|
987
|
+
name = prefixed[2].trim();
|
|
988
|
+
}
|
|
989
|
+
const key = providerKey ?? (/^claude[-_]/i.test(modelId) ? "anthropic" : void 0);
|
|
990
|
+
const brand = key ? OWN_BRAND[key] : void 0;
|
|
991
|
+
if (brand && brand.test(name) && name.replace(brand, "").trim()) name = name.replace(brand, "").trim();
|
|
992
|
+
return vendor ? {
|
|
993
|
+
name,
|
|
994
|
+
vendor
|
|
995
|
+
} : { name };
|
|
996
|
+
}
|
|
997
|
+
//#endregion
|
|
998
|
+
//#region src/chat/notice-format.ts
|
|
999
|
+
/** Leading glyph per severity. `info` is deliberately quiet — a middle dot. */
|
|
1000
|
+
const NOTICE_GLYPH = {
|
|
1001
|
+
info: "·",
|
|
1002
|
+
warn: "!",
|
|
1003
|
+
error: "✗"
|
|
1004
|
+
};
|
|
1005
|
+
//#endregion
|
|
1006
|
+
//#region src/chat/palette.ts
|
|
1007
|
+
/** Group name the hosts use for the most-recently-used rows shown on an empty query. */
|
|
1008
|
+
const PALETTE_RECENT_GROUP = "Recent";
|
|
1009
|
+
const PALETTE_RECENTS_MAX = 12;
|
|
1010
|
+
/** Ranked rows returned for a query; the host virtualises anyway, this bounds the work. */
|
|
1011
|
+
const PALETTE_RESULT_MAX = 200;
|
|
1012
|
+
const WORD_BREAKS = /* @__PURE__ */ new Set([
|
|
1013
|
+
" ",
|
|
1014
|
+
"/",
|
|
1015
|
+
"-",
|
|
1016
|
+
"_",
|
|
1017
|
+
":",
|
|
1018
|
+
"."
|
|
1019
|
+
]);
|
|
1020
|
+
/** What a hit at `at` is worth: the very start, a word start, a camelCase hump, or anywhere. */
|
|
1021
|
+
function startBonus(text, lower, at) {
|
|
1022
|
+
if (at === 0) return 8;
|
|
1023
|
+
if (WORD_BREAKS.has(lower[at - 1])) return 6;
|
|
1024
|
+
if (text[at] !== lower[at] && text[at - 1] === lower[at - 1]) return 4;
|
|
1025
|
+
return 1;
|
|
1026
|
+
}
|
|
1027
|
+
/** Bonus per character for a term found whole: "set" in "settings" beats s·e·t across words. */
|
|
1028
|
+
const WHOLE_TERM_BONUS = 2;
|
|
1029
|
+
/**
|
|
1030
|
+
* Subsequence match of `query` in `text` with the usual bonuses: word
|
|
1031
|
+
* starts and the very beginning score higher, gaps cost, runs of adjacent
|
|
1032
|
+
* hits are rewarded, and the query found whole (as one run) beats the same
|
|
1033
|
+
* letters scattered. Returns null when not every query character appears
|
|
1034
|
+
* in order. Case-insensitive; `query` must already be lower-cased.
|
|
1035
|
+
*/
|
|
1036
|
+
function fuzzyMatch(text, query) {
|
|
1037
|
+
return matchLowered(text, text.toLowerCase(), query);
|
|
1038
|
+
}
|
|
1039
|
+
const NO_INDICES = [];
|
|
1040
|
+
/** `indices` null: the score alone (a fallback field never highlights), without the allocations. */
|
|
1041
|
+
function matchLowered(text, lower, query, indices = []) {
|
|
1042
|
+
if (query.length === 0) return {
|
|
1043
|
+
score: 0,
|
|
1044
|
+
indices: [],
|
|
1045
|
+
whole: true
|
|
1046
|
+
};
|
|
1047
|
+
let score = 0;
|
|
1048
|
+
let from = 0;
|
|
1049
|
+
let prev = -2;
|
|
1050
|
+
for (let q = 0; q < query.length; q++) {
|
|
1051
|
+
const at = lower.indexOf(query[q], from);
|
|
1052
|
+
if (at === -1) return null;
|
|
1053
|
+
score += startBonus(text, lower, at);
|
|
1054
|
+
if (at === prev + 1) score += 3;
|
|
1055
|
+
else if (prev >= 0) score -= Math.min(3, at - prev - 1) * .5;
|
|
1056
|
+
indices?.push(at);
|
|
1057
|
+
prev = at;
|
|
1058
|
+
from = at + 1;
|
|
1059
|
+
}
|
|
1060
|
+
let whole = -Infinity;
|
|
1061
|
+
let wholeAt = -1;
|
|
1062
|
+
for (let at = lower.indexOf(query); at !== -1; at = lower.indexOf(query, at + 1)) {
|
|
1063
|
+
const value = startBonus(text, lower, at) + (query.length - 1) * 4 + query.length * WHOLE_TERM_BONUS;
|
|
1064
|
+
if (value > whole) {
|
|
1065
|
+
whole = value;
|
|
1066
|
+
wholeAt = at;
|
|
1067
|
+
}
|
|
1068
|
+
if (at === 0 || WORD_BREAKS.has(lower[at - 1])) break;
|
|
1069
|
+
}
|
|
1070
|
+
const found = whole > score;
|
|
1071
|
+
if (found) {
|
|
1072
|
+
score = whole;
|
|
1073
|
+
if (indices) for (let q = 0; q < query.length; q++) indices[q] = wholeAt + q;
|
|
1074
|
+
}
|
|
1075
|
+
score -= text.length * .02;
|
|
1076
|
+
return {
|
|
1077
|
+
score,
|
|
1078
|
+
indices: indices ?? NO_INDICES,
|
|
1079
|
+
whole: found
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
const haystacks = /* @__PURE__ */ new WeakMap();
|
|
1083
|
+
function haystackOf(item) {
|
|
1084
|
+
let h = haystacks.get(item);
|
|
1085
|
+
if (!h) {
|
|
1086
|
+
const texts = [
|
|
1087
|
+
item.title,
|
|
1088
|
+
item.description,
|
|
1089
|
+
item.group,
|
|
1090
|
+
...item.keywords ?? []
|
|
1091
|
+
].filter((t) => !!t);
|
|
1092
|
+
h = {
|
|
1093
|
+
title: item.title.toLowerCase(),
|
|
1094
|
+
fields: texts.map((t) => [t, t.toLowerCase()])
|
|
1095
|
+
};
|
|
1096
|
+
haystacks.set(item, h);
|
|
1097
|
+
}
|
|
1098
|
+
return h;
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Every whitespace-separated term must match (AND); scores add. `whole`:
|
|
1102
|
+
* each term was found as one run (the words typed, not letters picked
|
|
1103
|
+
* across the text). `indices` collects the merged highlight when given;
|
|
1104
|
+
* ranking scores without it and highlights only the rows it keeps.
|
|
1105
|
+
*/
|
|
1106
|
+
function matchTerms(text, lower, terms, indices) {
|
|
1107
|
+
let score = 0;
|
|
1108
|
+
let whole = true;
|
|
1109
|
+
for (const term of terms) {
|
|
1110
|
+
const m = matchLowered(text, lower, term, indices && []);
|
|
1111
|
+
if (!m) return null;
|
|
1112
|
+
score += m.score;
|
|
1113
|
+
whole &&= m.whole;
|
|
1114
|
+
indices?.push(...m.indices);
|
|
1115
|
+
}
|
|
1116
|
+
return {
|
|
1117
|
+
score,
|
|
1118
|
+
whole
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
function titleIndices(item, terms) {
|
|
1122
|
+
const indices = [];
|
|
1123
|
+
matchTerms(item.title, haystackOf(item).title, terms, indices);
|
|
1124
|
+
return terms.length === 1 ? indices : [...new Set(indices)].sort((a, b) => a - b);
|
|
1125
|
+
}
|
|
1126
|
+
const collator = new Intl.Collator();
|
|
1127
|
+
function termsOf(query) {
|
|
1128
|
+
return query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
1129
|
+
}
|
|
1130
|
+
/** On an empty query: recents first (most recent on top), then the catalog in provider order. */
|
|
1131
|
+
function listUnfiltered(items, recents) {
|
|
1132
|
+
const recentRank = new Map(recents.map((id, i) => [id, i]));
|
|
1133
|
+
const out = [];
|
|
1134
|
+
for (const item of items) {
|
|
1135
|
+
const r = recentRank.get(item.id);
|
|
1136
|
+
if (item.searchOnly && r === void 0) continue;
|
|
1137
|
+
out.push({
|
|
1138
|
+
item,
|
|
1139
|
+
score: r === void 0 ? 0 : 12 - r,
|
|
1140
|
+
titleMatches: []
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
return out.sort((a, b) => b.score - a.score).slice(0, 200);
|
|
1144
|
+
}
|
|
1145
|
+
/** The ranked rows for `terms`, and every item that matched (the pool a longer query searches). */
|
|
1146
|
+
function rankTerms(items, terms) {
|
|
1147
|
+
const hits = [];
|
|
1148
|
+
for (const item of items) {
|
|
1149
|
+
const hay = haystackOf(item);
|
|
1150
|
+
const title = matchTerms(item.title, hay.title, terms, null);
|
|
1151
|
+
if (title?.whole) {
|
|
1152
|
+
hits.push({
|
|
1153
|
+
item,
|
|
1154
|
+
score: 100 + title.score,
|
|
1155
|
+
inTitle: true
|
|
1156
|
+
});
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
let total = 0;
|
|
1160
|
+
let ok = true;
|
|
1161
|
+
for (const term of terms) {
|
|
1162
|
+
let best = -Infinity;
|
|
1163
|
+
for (const [text, lower] of hay.fields) {
|
|
1164
|
+
const m = matchLowered(text, lower, term, null);
|
|
1165
|
+
if (m && m.score > best) best = m.score;
|
|
1166
|
+
}
|
|
1167
|
+
if (best === -Infinity) {
|
|
1168
|
+
ok = false;
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
total += best;
|
|
1172
|
+
}
|
|
1173
|
+
if (title && title.score >= total) hits.push({
|
|
1174
|
+
item,
|
|
1175
|
+
score: title.score,
|
|
1176
|
+
inTitle: true
|
|
1177
|
+
});
|
|
1178
|
+
else if (ok) hits.push({
|
|
1179
|
+
item,
|
|
1180
|
+
score: total,
|
|
1181
|
+
inTitle: false
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
const matched = hits.map((h) => h.item);
|
|
1185
|
+
hits.sort((a, b) => b.score - a.score || collator.compare(a.item.title, b.item.title));
|
|
1186
|
+
return {
|
|
1187
|
+
ranked: hits.slice(0, 200).map(({ item, score, inTitle }) => ({
|
|
1188
|
+
item,
|
|
1189
|
+
score,
|
|
1190
|
+
titleMatches: inTitle ? titleIndices(item, terms) : []
|
|
1191
|
+
})),
|
|
1192
|
+
matched
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Score every item against `query`. Terms are AND-ed: `tog sid` needs both
|
|
1197
|
+
* `tog` and `sid` in order somewhere in the text. Title matches dominate;
|
|
1198
|
+
* description, keywords and group are fallbacks that never outrank a
|
|
1199
|
+
* title hit. On an empty query recents come first (most recent on top),
|
|
1200
|
+
* then the catalog in provider order.
|
|
1201
|
+
*/
|
|
1202
|
+
function rankPaletteItems(items, query, recents = []) {
|
|
1203
|
+
const terms = termsOf(query);
|
|
1204
|
+
return terms.length === 0 ? listUnfiltered(items, recents) : rankTerms(items, terms).ranked;
|
|
1205
|
+
}
|
|
1206
|
+
/** `next` asks for at least what `prev` did: each earlier term grew or stayed, new ones came after. */
|
|
1207
|
+
function narrows(prev, next) {
|
|
1208
|
+
return next.length >= prev.length && prev.every((term, i) => next[i].startsWith(term));
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* `rankPaletteItems` for a palette being typed into: it remembers the last
|
|
1212
|
+
* query, and when the next one only adds to it (one more character, one
|
|
1213
|
+
* more word) it searches just what matched before. A longer query never
|
|
1214
|
+
* matches an item the shorter one missed, so the result is the same.
|
|
1215
|
+
*/
|
|
1216
|
+
function createPaletteRanker() {
|
|
1217
|
+
let last = null;
|
|
1218
|
+
return (items, query, recents = []) => {
|
|
1219
|
+
const terms = termsOf(query);
|
|
1220
|
+
if (terms.length === 0) {
|
|
1221
|
+
last = null;
|
|
1222
|
+
return listUnfiltered(items, recents);
|
|
1223
|
+
}
|
|
1224
|
+
const { ranked, matched } = rankTerms(last && last.items === items && narrows(last.terms, terms) ? last.matched : items, terms);
|
|
1225
|
+
last = {
|
|
1226
|
+
items,
|
|
1227
|
+
terms,
|
|
1228
|
+
matched
|
|
1229
|
+
};
|
|
1230
|
+
return ranked;
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Sections for display. With a query the ranking already interleaves
|
|
1235
|
+
* providers, so groups follow the order of their best row. Without a
|
|
1236
|
+
* query the recents (ids in `recents`, present in `ranked`) form a
|
|
1237
|
+
* leading `Recent` section and the rest keeps provider order.
|
|
1238
|
+
*/
|
|
1239
|
+
function groupPaletteRows(ranked, query, recents = []) {
|
|
1240
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1241
|
+
const push = (title, row) => {
|
|
1242
|
+
const list = groups.get(title);
|
|
1243
|
+
if (list) list.push(row);
|
|
1244
|
+
else groups.set(title, [row]);
|
|
1245
|
+
};
|
|
1246
|
+
const isRecent = new Set(query.trim() ? [] : recents);
|
|
1247
|
+
for (const row of ranked) push(isRecent.has(row.item.id) ? PALETTE_RECENT_GROUP : row.item.group, row);
|
|
1248
|
+
return [...groups.entries()].map(([title, rows]) => ({
|
|
1249
|
+
title,
|
|
1250
|
+
rows
|
|
1251
|
+
}));
|
|
1252
|
+
}
|
|
1253
|
+
/** Most-recent-first list of ids, deduplicated and capped. */
|
|
1254
|
+
function pushPaletteRecent(recents, id) {
|
|
1255
|
+
return [id, ...recents.filter((r) => r !== id)].slice(0, 12);
|
|
1256
|
+
}
|
|
1257
|
+
const PALETTE_LIMITS = {
|
|
1258
|
+
items: 500,
|
|
1259
|
+
textChars: 500,
|
|
1260
|
+
keywords: 16
|
|
1261
|
+
};
|
|
1262
|
+
function str(v, max = PALETTE_LIMITS.textChars) {
|
|
1263
|
+
return typeof v === "string" && v.length > 0 ? v.length > max ? `${v.slice(0, max - 1)}…` : v : void 0;
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Coerce whatever a provider returned into well-formed items: entries
|
|
1267
|
+
* without an id or title are dropped, strings are capped, unknown kinds
|
|
1268
|
+
* fall back to `action`, and `source` is stamped with the provider id.
|
|
1269
|
+
* Never throws.
|
|
1270
|
+
*/
|
|
1271
|
+
function normalizePaletteItems(input, source, defaultGroup) {
|
|
1272
|
+
if (!Array.isArray(input)) return [];
|
|
1273
|
+
const out = [];
|
|
1274
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1275
|
+
for (const raw of input.slice(0, PALETTE_LIMITS.items)) {
|
|
1276
|
+
if (!raw || typeof raw !== "object") continue;
|
|
1277
|
+
const r = raw;
|
|
1278
|
+
const id = str(r.id, 200);
|
|
1279
|
+
const title = str(r.title, 200);
|
|
1280
|
+
if (!id || !title || seen.has(id)) continue;
|
|
1281
|
+
seen.add(id);
|
|
1282
|
+
const item = {
|
|
1283
|
+
id,
|
|
1284
|
+
title,
|
|
1285
|
+
group: str(r.group, 80) ?? defaultGroup,
|
|
1286
|
+
source
|
|
1287
|
+
};
|
|
1288
|
+
const description = str(r.description);
|
|
1289
|
+
if (description) item.description = description;
|
|
1290
|
+
if (Array.isArray(r.keywords)) {
|
|
1291
|
+
const kw = r.keywords.filter((k) => typeof k === "string" && k.length > 0).slice(0, PALETTE_LIMITS.keywords);
|
|
1292
|
+
if (kw.length > 0) item.keywords = kw.map((k) => k.slice(0, 60));
|
|
1293
|
+
}
|
|
1294
|
+
if (r.icon && typeof r.icon === "object") {
|
|
1295
|
+
const icon = r.icon;
|
|
1296
|
+
const gui = str(icon.gui, 120);
|
|
1297
|
+
const tui = str(icon.tui, 8);
|
|
1298
|
+
if (gui || tui) item.icon = {
|
|
1299
|
+
...gui ? { gui } : {},
|
|
1300
|
+
...tui ? { tui } : {}
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
if (r.kind === "toggle" || r.kind === "picker") item.kind = r.kind;
|
|
1304
|
+
if (typeof r.checked === "boolean") item.checked = r.checked;
|
|
1305
|
+
const keybinding = str(r.keybinding, 60);
|
|
1306
|
+
if (keybinding) item.keybinding = keybinding;
|
|
1307
|
+
const disabled = str(r.disabled, 200);
|
|
1308
|
+
if (disabled) item.disabled = disabled;
|
|
1309
|
+
if (r.searchOnly === true) item.searchOnly = true;
|
|
1310
|
+
out.push(item);
|
|
1311
|
+
}
|
|
1312
|
+
return out;
|
|
1313
|
+
}
|
|
1314
|
+
/** Same guard for a `run` result: anything malformed becomes `undefined` (nothing to do). */
|
|
1315
|
+
function normalizePaletteRunResult(input, source, defaultGroup) {
|
|
1316
|
+
if (!input || typeof input !== "object") return void 0;
|
|
1317
|
+
const r = input;
|
|
1318
|
+
switch (r.kind) {
|
|
1319
|
+
case "items": {
|
|
1320
|
+
const out = {
|
|
1321
|
+
kind: "items",
|
|
1322
|
+
items: normalizePaletteItems(r.items, source, defaultGroup)
|
|
1323
|
+
};
|
|
1324
|
+
const title = str(r.title, 200);
|
|
1325
|
+
const placeholder = str(r.placeholder, 200);
|
|
1326
|
+
if (title) out.title = title;
|
|
1327
|
+
if (placeholder) out.placeholder = placeholder;
|
|
1328
|
+
return out;
|
|
1329
|
+
}
|
|
1330
|
+
case "prompt":
|
|
1331
|
+
case "insert": {
|
|
1332
|
+
const text = str(r.text, 2e4);
|
|
1333
|
+
return text ? {
|
|
1334
|
+
kind: r.kind,
|
|
1335
|
+
text
|
|
1336
|
+
} : void 0;
|
|
1337
|
+
}
|
|
1338
|
+
case "notify": {
|
|
1339
|
+
const text = str(r.text, 1e3);
|
|
1340
|
+
if (!text) return void 0;
|
|
1341
|
+
return {
|
|
1342
|
+
kind: "notify",
|
|
1343
|
+
text,
|
|
1344
|
+
tone: r.tone === "warn" || r.tone === "error" ? r.tone : "info"
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
default: return;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
/** Type guard hosts use before forwarding a payload from an untrusted provider. */
|
|
1351
|
+
function isPaletteContext(v) {
|
|
1352
|
+
if (!v || typeof v !== "object") return false;
|
|
1353
|
+
const c = v;
|
|
1354
|
+
return (c.host === "gui" || c.host === "tui") && (c.cwd === void 0 || typeof c.cwd === "string") && (c.sessionId === void 0 || typeof c.sessionId === "string") && (c.busy === void 0 || typeof c.busy === "boolean");
|
|
1355
|
+
}
|
|
1356
|
+
//#endregion
|
|
1357
|
+
//#region src/chat/panels.ts
|
|
1358
|
+
const INBOX_PANEL_ID = "inbox";
|
|
1359
|
+
const TODOS_PANEL_ID = "todos";
|
|
1360
|
+
const ARTIFACTS_PANEL_ID = "artifacts";
|
|
1361
|
+
const DIFFS_PANEL_ID = "diffs";
|
|
1362
|
+
const FILES_PANEL_ID = "files";
|
|
1363
|
+
const BROWSER_PANEL_ID = "browser";
|
|
1364
|
+
const TERMINAL_PANEL_ID = "terminal";
|
|
1365
|
+
const BUILTIN_PANEL_TABS = [
|
|
1366
|
+
{
|
|
1367
|
+
id: INBOX_PANEL_ID,
|
|
1368
|
+
label: "Inbox",
|
|
1369
|
+
icon: {
|
|
1370
|
+
gui: "i-ph-tray",
|
|
1371
|
+
tui: "✉"
|
|
1372
|
+
},
|
|
1373
|
+
builtin: true
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
id: TODOS_PANEL_ID,
|
|
1377
|
+
label: "Todos",
|
|
1378
|
+
icon: {
|
|
1379
|
+
gui: "i-ph-list-checks",
|
|
1380
|
+
tui: "▣"
|
|
1381
|
+
},
|
|
1382
|
+
builtin: true
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
id: ARTIFACTS_PANEL_ID,
|
|
1386
|
+
label: "Artifacts",
|
|
1387
|
+
icon: {
|
|
1388
|
+
gui: "i-ph-cube",
|
|
1389
|
+
tui: "◆"
|
|
1390
|
+
},
|
|
1391
|
+
builtin: true
|
|
1392
|
+
},
|
|
1393
|
+
{
|
|
1394
|
+
id: DIFFS_PANEL_ID,
|
|
1395
|
+
label: "Diffs",
|
|
1396
|
+
icon: {
|
|
1397
|
+
gui: "i-ph-git-diff",
|
|
1398
|
+
tui: "±"
|
|
1399
|
+
},
|
|
1400
|
+
builtin: true
|
|
1401
|
+
},
|
|
1402
|
+
{
|
|
1403
|
+
id: FILES_PANEL_ID,
|
|
1404
|
+
label: "Files",
|
|
1405
|
+
icon: {
|
|
1406
|
+
gui: "i-ph-tree-structure",
|
|
1407
|
+
tui: "▤"
|
|
1408
|
+
},
|
|
1409
|
+
builtin: true
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
id: BROWSER_PANEL_ID,
|
|
1413
|
+
label: "Browser",
|
|
1414
|
+
icon: {
|
|
1415
|
+
gui: "i-ph-globe-simple",
|
|
1416
|
+
tui: "◎"
|
|
1417
|
+
},
|
|
1418
|
+
builtin: true
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
id: TERMINAL_PANEL_ID,
|
|
1422
|
+
label: "Terminal",
|
|
1423
|
+
icon: {
|
|
1424
|
+
gui: "i-ph-terminal-window",
|
|
1425
|
+
tui: "❯"
|
|
1426
|
+
},
|
|
1427
|
+
builtin: true
|
|
1428
|
+
}
|
|
1429
|
+
];
|
|
1430
|
+
/** Built-ins first, then extension panels in registry (load) order. */
|
|
1431
|
+
function buildPanelCatalog(extensionPanels, options = {}) {
|
|
1432
|
+
const tabs = [];
|
|
1433
|
+
for (const tab of BUILTIN_PANEL_TABS) {
|
|
1434
|
+
if (tab.id === "artifacts" && options.showArtifacts === false) continue;
|
|
1435
|
+
if (options.omitBuiltins?.includes(tab.id)) continue;
|
|
1436
|
+
tabs.push(tab);
|
|
1437
|
+
}
|
|
1438
|
+
const seen = new Set(tabs.map((t) => t.id));
|
|
1439
|
+
for (const p of extensionPanels) {
|
|
1440
|
+
if (seen.has(p.id)) continue;
|
|
1441
|
+
seen.add(p.id);
|
|
1442
|
+
tabs.push({
|
|
1443
|
+
id: p.id,
|
|
1444
|
+
label: p.label,
|
|
1445
|
+
builtin: false,
|
|
1446
|
+
owner: p.owner,
|
|
1447
|
+
...p.icon ? { icon: p.icon } : {}
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
return tabs;
|
|
1451
|
+
}
|
|
1452
|
+
const INITIAL_PANEL_STATE = {
|
|
1453
|
+
open: false,
|
|
1454
|
+
activeId: null,
|
|
1455
|
+
userPicks: 0,
|
|
1456
|
+
proactiveAt: 0
|
|
1457
|
+
};
|
|
1458
|
+
/** Add a tab to the host's pinned strip (idempotent, keeps order). */
|
|
1459
|
+
function pinTab(pinned, id) {
|
|
1460
|
+
return pinned.includes(id) ? pinned : [...pinned, id];
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Apply a user reorder of the VISIBLE strip to the pinned list. `pinned` can
|
|
1464
|
+
* hold ids the catalog no longer shows (an extension disabled for this cwd);
|
|
1465
|
+
* they keep their slots, and the visible ids fill the remaining slots in the
|
|
1466
|
+
* new order, so re-enabling the extension brings its tab back where it was.
|
|
1467
|
+
* Ids in `visible` that aren't pinned are ignored. Returns `pinned` itself
|
|
1468
|
+
* when nothing moved.
|
|
1469
|
+
*/
|
|
1470
|
+
function reorderTabs(pinned, visible) {
|
|
1471
|
+
const shown = new Set(visible.filter((id) => pinned.includes(id)));
|
|
1472
|
+
const order = visible.filter((id) => shown.has(id));
|
|
1473
|
+
let next = 0;
|
|
1474
|
+
const out = pinned.map((id) => shown.has(id) ? order[next++] : id);
|
|
1475
|
+
return out.every((id, i) => id === pinned[i]) ? pinned : out;
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Move one visible tab `delta` slots within the visible strip (keyboard
|
|
1479
|
+
* reorder), clamped at the ends. Hidden pinned ids keep their slots.
|
|
1480
|
+
*/
|
|
1481
|
+
function moveTab(pinned, visible, id, delta) {
|
|
1482
|
+
const from = visible.indexOf(id);
|
|
1483
|
+
if (from < 0) return pinned;
|
|
1484
|
+
const to = Math.max(0, Math.min(visible.length - 1, from + delta));
|
|
1485
|
+
if (to === from) return pinned;
|
|
1486
|
+
const order = [...visible];
|
|
1487
|
+
order.splice(from, 1);
|
|
1488
|
+
order.splice(to, 0, id);
|
|
1489
|
+
return reorderTabs(pinned, order);
|
|
1490
|
+
}
|
|
1491
|
+
/** The user clicked a tab (or a keybinding cycled to it). Always honoured, always opens. */
|
|
1492
|
+
function pickTab(state, id) {
|
|
1493
|
+
return {
|
|
1494
|
+
...state,
|
|
1495
|
+
open: true,
|
|
1496
|
+
activeId: id,
|
|
1497
|
+
userPicks: state.userPicks + 1
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
function togglePanel(state) {
|
|
1501
|
+
return {
|
|
1502
|
+
...state,
|
|
1503
|
+
open: !state.open
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
function closePanel(state) {
|
|
1507
|
+
return {
|
|
1508
|
+
...state,
|
|
1509
|
+
open: false
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Agent / extension asked to show a tab. Honoured only until the user has
|
|
1514
|
+
* picked a tab themselves (a badge that flipped must never steal the tab);
|
|
1515
|
+
* otherwise the request is dropped and the state returned unchanged.
|
|
1516
|
+
*/
|
|
1517
|
+
function openProactive(state, id) {
|
|
1518
|
+
if (state.userPicks !== state.proactiveAt) return state;
|
|
1519
|
+
return {
|
|
1520
|
+
...state,
|
|
1521
|
+
open: true,
|
|
1522
|
+
activeId: id
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* The agent is blocked on the user (a plan to approve, a question to
|
|
1527
|
+
* answer): show the tab regardless of earlier picks. Costs at most a tab
|
|
1528
|
+
* switch, and nothing in that tab decides implicitly. Not a user pick, so
|
|
1529
|
+
* it does not lock out later proactive opens.
|
|
1530
|
+
*/
|
|
1531
|
+
function openForAttention(state, id) {
|
|
1532
|
+
if (state.open && state.activeId === id) return state;
|
|
1533
|
+
return {
|
|
1534
|
+
...state,
|
|
1535
|
+
open: true,
|
|
1536
|
+
activeId: id
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Resolve the tab to render: the active one when it still exists, else the
|
|
1541
|
+
* first tab, else `null` (empty catalog). Keeps state stable when an
|
|
1542
|
+
* extension is disabled and its tab vanishes.
|
|
1543
|
+
*/
|
|
1544
|
+
function resolveActiveTab(state, catalog) {
|
|
1545
|
+
if (catalog.length === 0) return null;
|
|
1546
|
+
return catalog.find((t) => t.id === state.activeId) ?? catalog[0];
|
|
1547
|
+
}
|
|
1548
|
+
/** Cycle to the next / previous tab (wraps). Counts as a user pick. */
|
|
1549
|
+
function cycleTab(state, catalog, direction) {
|
|
1550
|
+
if (catalog.length === 0) return state;
|
|
1551
|
+
const current = resolveActiveTab(state, catalog);
|
|
1552
|
+
const next = catalog[((current ? catalog.findIndex((t) => t.id === current.id) : 0) + direction + catalog.length) % catalog.length];
|
|
1553
|
+
return pickTab(state, next.id);
|
|
1554
|
+
}
|
|
1555
|
+
//#endregion
|
|
1556
|
+
//#region src/chat/prompt-text.ts
|
|
1557
|
+
function tidyPromptText(text, refs) {
|
|
1558
|
+
const trimmed = text.trimEnd();
|
|
1559
|
+
if (!/\n{3,}/.test(trimmed)) return {
|
|
1560
|
+
text: trimmed,
|
|
1561
|
+
refs: refs.filter((r) => r.end <= trimmed.length)
|
|
1562
|
+
};
|
|
1563
|
+
const map = Array.from({ length: trimmed.length + 1 });
|
|
1564
|
+
let out = "";
|
|
1565
|
+
for (let i = 0; i < trimmed.length;) {
|
|
1566
|
+
if (/^\n{3,}/.exec(trimmed.slice(i, i + 64)) && trimmed[i] === "\n") {
|
|
1567
|
+
let end = i;
|
|
1568
|
+
while (trimmed[end] === "\n") end++;
|
|
1569
|
+
for (let k = i; k < end; k++) map[k] = out.length + Math.min(k - i, 2);
|
|
1570
|
+
out += "\n\n";
|
|
1571
|
+
i = end;
|
|
1572
|
+
continue;
|
|
1573
|
+
}
|
|
1574
|
+
map[i] = out.length;
|
|
1575
|
+
out += trimmed[i];
|
|
1576
|
+
i++;
|
|
1577
|
+
}
|
|
1578
|
+
map[trimmed.length] = out.length;
|
|
1579
|
+
const remapped = refs.filter((r) => r.end <= trimmed.length).map((r) => ({
|
|
1580
|
+
...r,
|
|
1581
|
+
start: map[r.start],
|
|
1582
|
+
end: map[r.end]
|
|
1583
|
+
}));
|
|
1584
|
+
return {
|
|
1585
|
+
text: out,
|
|
1586
|
+
refs: remapped
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
//#endregion
|
|
1590
|
+
//#region src/chat/stream-failure.ts
|
|
1591
|
+
const FAILED = /(?:^|\n\n)\[✗ Streaming failed (before any output|mid-output)(?:: ([\s\S]*))?\.?\]\s*$/;
|
|
1592
|
+
const ABORTED = /(?:^|\n\n)\[⏹ Streaming was aborted\.\]\s*$/;
|
|
1593
|
+
/** Split a reply into what the model wrote and the stop marker at its end, if any. */
|
|
1594
|
+
function splitStreamFailure(text) {
|
|
1595
|
+
const aborted = ABORTED.exec(text);
|
|
1596
|
+
if (aborted) {
|
|
1597
|
+
const before = text.slice(0, aborted.index).trimEnd();
|
|
1598
|
+
return {
|
|
1599
|
+
text: before,
|
|
1600
|
+
failure: {
|
|
1601
|
+
message: "",
|
|
1602
|
+
midOutput: before !== "",
|
|
1603
|
+
aborted: true
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
const m = FAILED.exec(text);
|
|
1608
|
+
if (!m) return {
|
|
1609
|
+
text,
|
|
1610
|
+
failure: null
|
|
1611
|
+
};
|
|
1612
|
+
return {
|
|
1613
|
+
text: text.slice(0, m.index).trimEnd(),
|
|
1614
|
+
failure: {
|
|
1615
|
+
message: (m[2] ?? "").trim(),
|
|
1616
|
+
midOutput: m[1] === "mid-output",
|
|
1617
|
+
aborted: false
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
/** Cheap pre-check before splitting: the text may end in a stop marker. */
|
|
1622
|
+
function mayHaveStreamFailure(text) {
|
|
1623
|
+
return text.includes("[✗ Streaming failed") || text.includes("[⏹ Streaming was aborted");
|
|
1624
|
+
}
|
|
1625
|
+
/** What the "continue" action sends after a run that had done work. */
|
|
1626
|
+
const CONTINUE_PROMPT = "Continue from where you stopped.";
|
|
1627
|
+
//#endregion
|
|
1628
|
+
//#region src/chat/stream-rate.ts
|
|
1629
|
+
const EMPTY_RATE = {
|
|
1630
|
+
turnId: null,
|
|
1631
|
+
samples: [],
|
|
1632
|
+
estimated: null,
|
|
1633
|
+
rate: null
|
|
1634
|
+
};
|
|
1635
|
+
/** Trailing window the rate is measured over. */
|
|
1636
|
+
const WINDOW_MS = 3e3;
|
|
1637
|
+
/** Minimum span before a reading counts: shorter spans are one or two ticks of noise. */
|
|
1638
|
+
const MIN_SPAN_MS = 1200;
|
|
1639
|
+
/** Faster than any served model today (the fastest wafer-scale ones run ~3k): a reading above is a glitch. */
|
|
1640
|
+
const MAX_PLAUSIBLE_RATE = 6e3;
|
|
1641
|
+
/** Weight of a new reading in the smoothed rate. */
|
|
1642
|
+
const SMOOTHING = .35;
|
|
1643
|
+
function stepStreamRate(state, sample) {
|
|
1644
|
+
const point = {
|
|
1645
|
+
t: sample.elapsedMs,
|
|
1646
|
+
n: sample.outputTokens
|
|
1647
|
+
};
|
|
1648
|
+
if (state.turnId !== sample.turnId) return {
|
|
1649
|
+
turnId: sample.turnId,
|
|
1650
|
+
samples: [point],
|
|
1651
|
+
estimated: sample.estimated,
|
|
1652
|
+
rate: null
|
|
1653
|
+
};
|
|
1654
|
+
const last = state.samples[state.samples.length - 1];
|
|
1655
|
+
if (state.estimated === true && !sample.estimated || last !== void 0 && point.n < last.n || !last) return {
|
|
1656
|
+
...state,
|
|
1657
|
+
samples: [point],
|
|
1658
|
+
estimated: sample.estimated
|
|
1659
|
+
};
|
|
1660
|
+
if (point.t <= last.t) return state;
|
|
1661
|
+
const kept = [...state.samples, point];
|
|
1662
|
+
let start = 0;
|
|
1663
|
+
while (start < kept.length - 2 && kept[start + 1].t <= point.t - WINDOW_MS) start++;
|
|
1664
|
+
const samples = kept.slice(start);
|
|
1665
|
+
const base = samples[0];
|
|
1666
|
+
const span = point.t - base.t;
|
|
1667
|
+
let rate = state.rate;
|
|
1668
|
+
if (span >= MIN_SPAN_MS) {
|
|
1669
|
+
const reading = (point.n - base.n) / span * 1e3;
|
|
1670
|
+
if (reading >= 0 && reading <= 6e3) rate = rate === null ? reading : rate + SMOOTHING * (reading - rate);
|
|
1671
|
+
}
|
|
1672
|
+
return {
|
|
1673
|
+
turnId: state.turnId,
|
|
1674
|
+
samples,
|
|
1675
|
+
estimated: sample.estimated,
|
|
1676
|
+
rate
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
/** Whole tokens per second for display, or `null` when there is no honest reading. */
|
|
1680
|
+
function streamRateValue(state) {
|
|
1681
|
+
if (state.rate === null) return null;
|
|
1682
|
+
const rounded = Math.round(state.rate);
|
|
1683
|
+
return rounded > 0 ? rounded : null;
|
|
1684
|
+
}
|
|
1685
|
+
//#endregion
|
|
1686
|
+
//#region src/chat/todo-edits.ts
|
|
1687
|
+
/**
|
|
1688
|
+
* Prefix for ids minted here, distinguishing user-authored rows from the
|
|
1689
|
+
* model's own ids in a session file. Purely diagnostic — nothing branches on
|
|
1690
|
+
* it — but it makes "who added this" answerable after the fact.
|
|
1691
|
+
*/
|
|
1692
|
+
const USER_TODO_ID_PREFIX = "user-";
|
|
1693
|
+
/**
|
|
1694
|
+
* Mint an id that cannot collide with an existing row.
|
|
1695
|
+
*
|
|
1696
|
+
* Counter-based rather than random: a stable, readable id keeps session diffs
|
|
1697
|
+
* legible, and the collision check makes it safe against ids the model already
|
|
1698
|
+
* used (including previous `user-*` ones after a reload).
|
|
1699
|
+
*/
|
|
1700
|
+
function nextTodoId(items) {
|
|
1701
|
+
const taken = new Set(items.map((i) => i.id));
|
|
1702
|
+
for (let n = 1;; n++) {
|
|
1703
|
+
const id = `${USER_TODO_ID_PREFIX}${n}`;
|
|
1704
|
+
if (!taken.has(id)) return id;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Append a todo. Returns the list unchanged when `content` is blank — an empty
|
|
1709
|
+
* row is never what the user meant, and rejecting it here means every caller
|
|
1710
|
+
* (keyboard, click, future GUI) gets the same guard.
|
|
1711
|
+
*/
|
|
1712
|
+
function addTodo(items, content) {
|
|
1713
|
+
const trimmed = content.trim();
|
|
1714
|
+
if (!trimmed) return [...items];
|
|
1715
|
+
return [...items, {
|
|
1716
|
+
id: nextTodoId(items),
|
|
1717
|
+
content: trimmed,
|
|
1718
|
+
status: "pending"
|
|
1719
|
+
}];
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Replace one todo's text. Blank content is rejected (see {@link addTodo}); an
|
|
1723
|
+
* unknown id is a no-op rather than an error, because the list can be replaced
|
|
1724
|
+
* by a `todowrite` between render and keypress.
|
|
1725
|
+
*/
|
|
1726
|
+
function editTodo(items, id, content) {
|
|
1727
|
+
const trimmed = content.trim();
|
|
1728
|
+
if (!trimmed) return [...items];
|
|
1729
|
+
return items.map((item) => item.id === id ? {
|
|
1730
|
+
...item,
|
|
1731
|
+
content: trimmed
|
|
1732
|
+
} : item);
|
|
1733
|
+
}
|
|
1734
|
+
/** Drop one todo. Unknown id is a no-op, same rationale as {@link editTodo}. */
|
|
1735
|
+
function removeTodo(items, id) {
|
|
1736
|
+
return items.filter((item) => item.id !== id);
|
|
1737
|
+
}
|
|
1738
|
+
/** Drop everything. Separate from `removeTodo` so the caller's intent is explicit at the call site. */
|
|
1739
|
+
function clearTodos() {
|
|
1740
|
+
return [];
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Advance a todo's status one step: pending → in_progress → completed →
|
|
1744
|
+
* pending. `cancelled` also lands on `pending`, so a dropped item can be
|
|
1745
|
+
* revived without a dedicated key.
|
|
1746
|
+
*
|
|
1747
|
+
* Cycling INTO `in_progress` demotes any other in-progress row to `pending`:
|
|
1748
|
+
* the list's whole purpose is "what is being worked on right now", and two
|
|
1749
|
+
* live rows make that unanswerable — `TodoIndicator`'s successor
|
|
1750
|
+
* (`todoProgressLabel`) reads the first one and would silently ignore the rest.
|
|
1751
|
+
*/
|
|
1752
|
+
function cycleTodoStatus(items, id) {
|
|
1753
|
+
const target = items.find((item) => item.id === id);
|
|
1754
|
+
if (!target) return [...items];
|
|
1755
|
+
const next = nextStatus(target.status);
|
|
1756
|
+
return items.map((item) => {
|
|
1757
|
+
if (item.id === id) return {
|
|
1758
|
+
...item,
|
|
1759
|
+
status: next
|
|
1760
|
+
};
|
|
1761
|
+
if (next === "in_progress" && item.status === "in_progress") return {
|
|
1762
|
+
...item,
|
|
1763
|
+
status: "pending"
|
|
1764
|
+
};
|
|
1765
|
+
return item;
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
function nextStatus(status) {
|
|
1769
|
+
switch (status) {
|
|
1770
|
+
case "pending": return "in_progress";
|
|
1771
|
+
case "in_progress": return "completed";
|
|
1772
|
+
default: return "pending";
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Steering text for a "retry this todo" request.
|
|
1777
|
+
*
|
|
1778
|
+
* Quotes the content so a model reading it can match the row verbatim against
|
|
1779
|
+
* its own list, and names the status because "retry" means different things
|
|
1780
|
+
* for a row it believes is `completed` versus one it abandoned as `cancelled`.
|
|
1781
|
+
* Shared by the TUI and GUI modals so both surfaces steer with byte-identical
|
|
1782
|
+
* text; pinned by a test rather than living inside a JSX callback.
|
|
1783
|
+
*/
|
|
1784
|
+
function buildTodoRetryMessage(item) {
|
|
1785
|
+
return `Retry this todo (currently ${item.status}): "${item.content}"`;
|
|
1786
|
+
}
|
|
1787
|
+
//#endregion
|
|
1788
|
+
export { validateMcpForm as $, openProactive as A, createPaletteRanker as B, TERMINAL_PANEL_ID as C, parseSessionKey as Ct, cycleTab as D, UnknownHostIdError as Dt, closePanel as E, HostRemovedError as Et, togglePanel as F, normalizePaletteRunResult as G, groupPaletteRows as H, PALETTE_LIMITS as I, NOTICE_GLYPH as J, pushPaletteRecent as K, PALETTE_RECENTS_MAX as L, pinTab as M, reorderTabs as N, moveTab as O, createHostIndex as Ot, resolveActiveTab as P, splitCommandLine as Q, PALETTE_RECENT_GROUP as R, INITIAL_PANEL_STATE as S, SESSION_KEY_SEPARATOR as St, buildPanelCatalog as T, AmbiguousHostIdError as Tt, isPaletteContext as U, fuzzyMatch as V, normalizePaletteItems as W, MCP_TRANSPORTS as X, modelDisplayName as Y, parseKeyValueLines as Z, BROWSER_PANEL_ID as _, buildVisibleAuthRows as _t, editTodo as a, machineDirListing as at, FILES_PANEL_ID as b, parentProviderKey as bt, EMPTY_RATE as c, INSTRUCTION_PREVIEW_LINE_CAP as ct, streamRateValue as d, instructionPreviewLines as dt, MACHINE_LINK_STATE_TEXT as et, CONTINUE_PROMPT as f, commandGist as ft, ARTIFACTS_PANEL_ID as g, toolPhrase as gt, tidyPromptText as h, toolOverview as ht, cycleTodoStatus as i, machineBlockedReason as it, pickTab as j, openForAttention as k, MAX_PLAUSIBLE_RATE as l, buildVisibleInstructionRows as lt, splitStreamFailure as m, summarizeTools as mt, buildTodoRetryMessage as n, environmentLabel as nt, nextTodoId as o, machineWaiting as ot, mayHaveStreamFailure as p, messagePreview as pt, rankPaletteItems as q, clearTodos as r, lastSeenText as rt, removeTodo as s, orderByMachine as st, addTodo as t, MACHINE_NOTICE_LABEL as tt, stepStreamRate as u, instructionOriginBadge as ut, BUILTIN_PANEL_TABS as v, indexOfProviderRow as vt, TODOS_PANEL_ID as w, sessionKey as wt, INBOX_PANEL_ID as x, selectableSources as xt, DIFFS_PANEL_ID as y, isExpandableProvider as yt, PALETTE_RESULT_MAX as z };
|
|
1789
|
+
|
|
1790
|
+
//# sourceMappingURL=todo-edits-Dn251YUP.js.map
|