zidane 7.2.5 → 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 -27243
- 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-BKVIIOqH.js +0 -331
- package/dist/ipc-BKVIIOqH.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-DfdXrGg9.js +0 -1549
- package/dist/shell-path-DfdXrGg9.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,1259 @@
|
|
|
1
|
+
import { i as styleReplacementForVia, n as resolveOldString } from "./edit-utils-EGosADZq.js";
|
|
2
|
+
//#region src/chat/browser/url.ts
|
|
3
|
+
/**
|
|
4
|
+
* What the dev browser will load. It is a browser for the app you are
|
|
5
|
+
* building, not the web at large: http(s) only, bare local addresses
|
|
6
|
+
* default to http, anything that looks like a host gets https. Shared by the
|
|
7
|
+
* GUI address bar and the agent's `browser_open` / `browser_navigate`.
|
|
8
|
+
*/
|
|
9
|
+
const LOCAL_HOST = /^(?:localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|\[::1\]|[\w-]+\.localhost)(?::\d+)?(?:[/?#]|$)/i;
|
|
10
|
+
const HOST_LIKE = /^(?:[\w-]+\.)+[a-z]{2,}(?::\d+)?(?:[/?#]|$)/i;
|
|
11
|
+
const PORT_ONLY = /^:?(\d{2,5})([/?#].*)?$/;
|
|
12
|
+
/** Normalize address-bar input to a loadable URL, or `null` when it is not one. */
|
|
13
|
+
function normalizeBrowserUrl(input) {
|
|
14
|
+
const text = input.trim();
|
|
15
|
+
if (!text) return null;
|
|
16
|
+
const port = PORT_ONLY.exec(text);
|
|
17
|
+
if (port) return canonical(`http://localhost:${port[1]}${port[2] ?? ""}`);
|
|
18
|
+
if (/^[a-z][a-z\d+.-]*:\/\//i.test(text)) return isAllowedBrowserUrl(text) ? canonical(text) : null;
|
|
19
|
+
if (/\s/.test(text)) return null;
|
|
20
|
+
if (LOCAL_HOST.test(text)) return canonical(`http://${text}`);
|
|
21
|
+
if (HOST_LIKE.test(text)) return canonical(`https://${text}`);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/** Only http(s) pages (and the blank page) ever load in the dev browser. */
|
|
25
|
+
function isAllowedBrowserUrl(url) {
|
|
26
|
+
if (url === "about:blank") return true;
|
|
27
|
+
try {
|
|
28
|
+
const { protocol } = new URL(url);
|
|
29
|
+
return protocol === "http:" || protocol === "https:";
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** A loopback address: the dev servers the browser exists for. */
|
|
35
|
+
function isLocalBrowserUrl(url) {
|
|
36
|
+
try {
|
|
37
|
+
const host = new URL(url).hostname;
|
|
38
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "0.0.0.0" || host === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(host);
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function canonical(url) {
|
|
44
|
+
try {
|
|
45
|
+
return new URL(url).toString();
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/chat/command-notes.ts
|
|
52
|
+
/** The persisted invocation header block: `` `❯ /commit args` ``. */
|
|
53
|
+
function commandHeaderBlock(invocation) {
|
|
54
|
+
return `\`❯ ${invocation}\``;
|
|
55
|
+
}
|
|
56
|
+
/** `/name args` from a persisted header block, or null when the text is not one. */
|
|
57
|
+
function parseCommandHeader(text) {
|
|
58
|
+
const match = /^`❯ (\/[\s\S]+)`$/.exec(text.trim());
|
|
59
|
+
return match ? match[1] : null;
|
|
60
|
+
}
|
|
61
|
+
const PREFIX_SEPARATOR = /^\s*(?:[:·—-]\s*)?/;
|
|
62
|
+
/**
|
|
63
|
+
* The receipt text for one note. Older displays repeat the command
|
|
64
|
+
* (`/commit: 83dfea7`, `/review — 3 reviewers, fixes applied`); the prompt
|
|
65
|
+
* bubble above already names it, so the prefix goes, and a leftover ` — `
|
|
66
|
+
* reads as ` · `. A note without a display shows its first line.
|
|
67
|
+
*/
|
|
68
|
+
function commandNoteText(display, text, invocation) {
|
|
69
|
+
let line = (display ?? text.split("\n").find((l) => l.trim()) ?? "").trim();
|
|
70
|
+
const prefix = [invocation, invocation?.split(/\s/, 1)[0]].filter((p) => Boolean(p)).find((p) => line.startsWith(p)) ?? /^\/[\w.:-]+/.exec(line)?.[0];
|
|
71
|
+
if (prefix) {
|
|
72
|
+
const rest = line.slice(prefix.length).replace(PREFIX_SEPARATOR, "");
|
|
73
|
+
if (rest) line = /^[0-9a-f]{7,40}$/.test(rest) ? `committed ${rest}` : rest;
|
|
74
|
+
}
|
|
75
|
+
line = line.replace(/\s+—\s+/g, " · ");
|
|
76
|
+
return line.length > 160 ? `${line.slice(0, 159)}…` : line;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The `display` a context note persists with: the command's marker, its
|
|
80
|
+
* first line when it gave none, or `''` for a model-only note
|
|
81
|
+
* (`display: false`), which shows nothing.
|
|
82
|
+
*/
|
|
83
|
+
function noteDisplay(display, text) {
|
|
84
|
+
return display === false ? "" : display ?? commandNoteText(void 0, text);
|
|
85
|
+
}
|
|
86
|
+
/** A command step label: the display as given, minus any leftover ` — `. */
|
|
87
|
+
function commandStepLabel(display) {
|
|
88
|
+
return display.replace(/\s+—\s+/g, " · ");
|
|
89
|
+
}
|
|
90
|
+
const merged = /* @__PURE__ */ new WeakMap();
|
|
91
|
+
function isTopPrompt(event) {
|
|
92
|
+
return event.kind === "user-prompt" && !event.childId && (event.depth ?? 0) === 0;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* One receipt line per exchange: every `command-note` in an exchange moves
|
|
96
|
+
* to its end and joins into a single event (`committed 83dfea7 · pushed to
|
|
97
|
+
* origin/main`). Returns `events` itself when there is nothing to merge;
|
|
98
|
+
* the merged event keeps its identity while its notes do not change, so
|
|
99
|
+
* memoised rows stay put.
|
|
100
|
+
*/
|
|
101
|
+
function gatherCommandNotes(events) {
|
|
102
|
+
if (!events.some((e) => e.kind === "command-note")) return events;
|
|
103
|
+
const out = [];
|
|
104
|
+
let notes = [];
|
|
105
|
+
const flush = () => {
|
|
106
|
+
if (notes.length === 0) return;
|
|
107
|
+
const first = notes[0];
|
|
108
|
+
const key = notes.map((n) => n.text).join("\0");
|
|
109
|
+
let event = first;
|
|
110
|
+
if (notes.length > 1) {
|
|
111
|
+
const cached = merged.get(first);
|
|
112
|
+
event = cached?.key === key ? cached.event : {
|
|
113
|
+
...first,
|
|
114
|
+
text: notes.map((n) => n.text).join(" · ")
|
|
115
|
+
};
|
|
116
|
+
merged.set(first, {
|
|
117
|
+
key,
|
|
118
|
+
event
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
let at = out.length;
|
|
122
|
+
while (at > 0 && out[at - 1].kind === "separator") at--;
|
|
123
|
+
out.splice(at, 0, event);
|
|
124
|
+
notes = [];
|
|
125
|
+
};
|
|
126
|
+
for (const event of events) {
|
|
127
|
+
if (isTopPrompt(event)) flush();
|
|
128
|
+
if (event.kind === "command-note" && !event.childId) {
|
|
129
|
+
notes.push(event);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
out.push(event);
|
|
133
|
+
}
|
|
134
|
+
flush();
|
|
135
|
+
return out.length === events.length && out.every((e, i) => e === events[i]) ? events : out;
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/chat/edit-approval.ts
|
|
139
|
+
/**
|
|
140
|
+
* Convert a per-hunk approval mask into an `EditOutcome[]`. `true` →
|
|
141
|
+
* `applied`; `false` → `denied` with the supplied reason.
|
|
142
|
+
*
|
|
143
|
+
* Length is `Math.max(mask.length, fallbackLength)` so callers passing a
|
|
144
|
+
* shorter mask still get a fully-populated array — missing entries
|
|
145
|
+
* default to applied, matching the "no decision => keep" convention.
|
|
146
|
+
*/
|
|
147
|
+
function maskToOutcomeKinds(mask, fallbackLength, deniedReason = "denied by user") {
|
|
148
|
+
const len = Math.max(mask.length, fallbackLength);
|
|
149
|
+
const out = [];
|
|
150
|
+
for (let i = 0; i < len; i++) {
|
|
151
|
+
const keep = i < mask.length ? mask[i] : true;
|
|
152
|
+
out.push(keep ? { kind: "applied" } : {
|
|
153
|
+
kind: "denied",
|
|
154
|
+
reason: deniedReason
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
function resolveApprovalForPayload(decision, payload) {
|
|
160
|
+
const total = payload.hunks.length;
|
|
161
|
+
if (decision === "deny") {
|
|
162
|
+
const outcomes = Array.from({ length: total }, () => ({
|
|
163
|
+
kind: "denied",
|
|
164
|
+
reason: "denied by user"
|
|
165
|
+
}));
|
|
166
|
+
return {
|
|
167
|
+
outcomes,
|
|
168
|
+
shouldBlock: true,
|
|
169
|
+
syntheticEvent: {
|
|
170
|
+
...payload,
|
|
171
|
+
outcomes
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (typeof decision === "object" && decision.kind === "partial") {
|
|
176
|
+
const outcomes = maskToOutcomeKinds(decision.mask, total);
|
|
177
|
+
return {
|
|
178
|
+
outcomes,
|
|
179
|
+
shouldBlock: !outcomes.some((o) => o.kind === "applied"),
|
|
180
|
+
syntheticEvent: {
|
|
181
|
+
...payload,
|
|
182
|
+
outcomes
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
outcomes: Array.from({ length: total }, () => ({ kind: "applied" })),
|
|
188
|
+
shouldBlock: false,
|
|
189
|
+
syntheticEvent: null
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** Sentinel tags used by {@link buildEditOutcomesAnnotation} / parser. */
|
|
193
|
+
const ANNOTATION_OPEN = "<edit-outcomes>";
|
|
194
|
+
const ANNOTATION_CLOSE = "</edit-outcomes>";
|
|
195
|
+
const OUTCOME_KIND_RE = /^applied|denied|skipped|failed$/;
|
|
196
|
+
const OUTCOME_LINE_RE = /^#(\d+) (applied|denied|skipped|failed)(?:: ?(.*))?$/;
|
|
197
|
+
/**
|
|
198
|
+
* Render an `EditOutcome[]` as the wire-format annotation block. Returns
|
|
199
|
+
* the body to APPEND to a tool result; callers join with a leading
|
|
200
|
+
* `\n\n` separator. Idempotent on missing reasons — bare `applied` lines
|
|
201
|
+
* stay terse.
|
|
202
|
+
*/
|
|
203
|
+
function buildEditOutcomesAnnotation(outcomes) {
|
|
204
|
+
const lines = [ANNOTATION_OPEN];
|
|
205
|
+
for (let i = 0; i < outcomes.length; i++) {
|
|
206
|
+
const o = outcomes[i];
|
|
207
|
+
if (!OUTCOME_KIND_RE.test(o.kind)) continue;
|
|
208
|
+
const reason = o.reason ? `: ${o.reason}` : "";
|
|
209
|
+
lines.push(`#${i + 1} ${o.kind}${reason}`);
|
|
210
|
+
}
|
|
211
|
+
lines.push(ANNOTATION_CLOSE);
|
|
212
|
+
return lines.join("\n");
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Parse an `<edit-outcomes>…</edit-outcomes>` annotation block out of a
|
|
216
|
+
* tool result body. Returns the outcomes keyed by 1-based hunk index, or
|
|
217
|
+
* `null` when the block is missing / malformed.
|
|
218
|
+
*
|
|
219
|
+
* Anchored on the explicit tag pair so the parser doesn't false-positive
|
|
220
|
+
* on natural prose that happens to contain `#1 applied`.
|
|
221
|
+
*/
|
|
222
|
+
function parseEditOutcomesFromResult(result) {
|
|
223
|
+
const text = typeof result === "string" ? result : result.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
224
|
+
if (!text) return null;
|
|
225
|
+
const openIdx = text.indexOf(`\n${ANNOTATION_OPEN}\n`);
|
|
226
|
+
const startIdx = openIdx >= 0 ? openIdx + 1 : text.startsWith(`${ANNOTATION_OPEN}\n`) ? 0 : -1;
|
|
227
|
+
if (startIdx < 0) return null;
|
|
228
|
+
const closeNeedle = `\n${ANNOTATION_CLOSE}`;
|
|
229
|
+
const closeIdx = text.indexOf(closeNeedle, startIdx);
|
|
230
|
+
if (closeIdx < 0) return null;
|
|
231
|
+
const body = text.slice(startIdx + 15 + 1, closeIdx);
|
|
232
|
+
const found = [];
|
|
233
|
+
for (const line of body.split("\n")) {
|
|
234
|
+
if (line.length === 0) continue;
|
|
235
|
+
const m = OUTCOME_LINE_RE.exec(line);
|
|
236
|
+
if (!m) return null;
|
|
237
|
+
const idx = Number.parseInt(m[1], 10);
|
|
238
|
+
if (!Number.isFinite(idx) || idx < 1) return null;
|
|
239
|
+
const kind = m[2];
|
|
240
|
+
const reason = m[3]?.trim();
|
|
241
|
+
found.push({
|
|
242
|
+
idx,
|
|
243
|
+
outcome: {
|
|
244
|
+
kind,
|
|
245
|
+
...reason ? { reason } : {}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (found.length === 0) return null;
|
|
250
|
+
const maxIdx = Math.max(...found.map((f) => f.idx));
|
|
251
|
+
const outcomes = Array.from({ length: maxIdx }, () => ({ kind: "applied" }));
|
|
252
|
+
for (const { idx, outcome } of found) outcomes[idx - 1] = outcome;
|
|
253
|
+
return outcomes;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Strip the first `<edit-outcomes>…</edit-outcomes>` block out of a tool
|
|
257
|
+
* result body, returning the surrounding text. Used by the
|
|
258
|
+
* `tool:transform` hook to peel a body-emitted annotation before
|
|
259
|
+
* re-appending the merged (approval ∪ body) version — otherwise the
|
|
260
|
+
* result would carry two annotation blocks and
|
|
261
|
+
* {@link parseEditOutcomesFromResult} would only see the first.
|
|
262
|
+
*
|
|
263
|
+
* Anchored on the same `\n<edit-outcomes>\n` / start-of-string newline
|
|
264
|
+
* shape the parser uses, so prose that incidentally mentions
|
|
265
|
+
* `<edit-outcomes>` (e.g. a model summarizing its own format) isn't
|
|
266
|
+
* mistakenly stripped. Trims a single leading `\n\n` separator when
|
|
267
|
+
* present so successive strips don't leave dangling blank lines.
|
|
268
|
+
* Idempotent on inputs that don't contain a properly-anchored block.
|
|
269
|
+
*/
|
|
270
|
+
function stripEditOutcomesAnnotation(text) {
|
|
271
|
+
const newlineNeedle = `\n${ANNOTATION_OPEN}\n`;
|
|
272
|
+
const newlineIdx = text.indexOf(newlineNeedle);
|
|
273
|
+
let openIdx;
|
|
274
|
+
if (newlineIdx >= 0) openIdx = newlineIdx + 1;
|
|
275
|
+
else if (text.startsWith(`${ANNOTATION_OPEN}\n`)) openIdx = 0;
|
|
276
|
+
else return text;
|
|
277
|
+
const closeIdx = text.indexOf(ANNOTATION_CLOSE, openIdx);
|
|
278
|
+
if (closeIdx < 0) return text;
|
|
279
|
+
const blockEnd = closeIdx + 16;
|
|
280
|
+
const sepStart = openIdx >= 2 && text.slice(openIdx - 2, openIdx) === "\n\n" ? openIdx - 2 : openIdx;
|
|
281
|
+
return text.slice(0, sepStart) + text.slice(blockEnd);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Merge body-side outcomes (keyed against the approved subset the tool
|
|
285
|
+
* actually ran on, in subset-position order) into approval-side outcomes
|
|
286
|
+
* (1:1 with the model's ORIGINAL `edits` list, with `denied` entries for
|
|
287
|
+
* every hunk the user dropped).
|
|
288
|
+
*
|
|
289
|
+
* Algorithm: walk the approval array; every `applied` placeholder
|
|
290
|
+
* corresponds to one approved hunk that the body ran. Consume body's
|
|
291
|
+
* outcomes in order against those placeholders. Non-`applied` approval
|
|
292
|
+
* entries (`denied`, `skipped`) stay untouched — they describe gate-
|
|
293
|
+
* level decisions the body never saw.
|
|
294
|
+
*
|
|
295
|
+
* Pure. Returns a fresh array; never mutates either input.
|
|
296
|
+
*
|
|
297
|
+
* Edge cases:
|
|
298
|
+
* - `body` is empty / shorter than the approved count → remaining
|
|
299
|
+
* approval `applied` placeholders stay as `applied` (the body ran
|
|
300
|
+
* happily; absence of a body entry means nothing failed).
|
|
301
|
+
* - `body` longer than approved count → trailing body entries are
|
|
302
|
+
* ignored. Shouldn't happen in practice (body sees the rebound
|
|
303
|
+
* subset), but the guard keeps the merge total-pure.
|
|
304
|
+
*/
|
|
305
|
+
function mergeApprovalAndBodyOutcomes(approval, body) {
|
|
306
|
+
if (!body || body.length === 0) return approval.slice();
|
|
307
|
+
const out = [];
|
|
308
|
+
let bi = 0;
|
|
309
|
+
for (const entry of approval) if (entry.kind === "applied" && bi < body.length) {
|
|
310
|
+
out.push(body[bi]);
|
|
311
|
+
bi++;
|
|
312
|
+
} else out.push(entry);
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Rewrite a `multi_edit` body header so the totals reflect the model's
|
|
317
|
+
* ORIGINAL edit list (the merged outcomes count) instead of the subset
|
|
318
|
+
* the body actually saw after gate rebinding. Without this, a partially
|
|
319
|
+
* approved call surfaces a misleading `applied 2 of 2 edits` (subset
|
|
320
|
+
* counts) on the wire even when the original was `applied 2 of 3`.
|
|
321
|
+
*
|
|
322
|
+
* Three body-side shapes are handled (matching `multi_edit`'s emit):
|
|
323
|
+
* 1. `Edited <path>: applied N edits (R replacements).`
|
|
324
|
+
* 2. `Edited <path>: applied N of M edits (R replacements).`
|
|
325
|
+
* 3. `multi_edit error: no edits applied to <path> (M attempted).`
|
|
326
|
+
*
|
|
327
|
+
* The replacements count is preserved verbatim — it's a body-side stat
|
|
328
|
+
* the chat layer can't recompute. When the first line doesn't look like
|
|
329
|
+
* any of the three shapes (e.g. an unrelated error preamble bubbled up),
|
|
330
|
+
* the text is returned unchanged.
|
|
331
|
+
*/
|
|
332
|
+
function rewriteMultiEditHeader(text, merged, path) {
|
|
333
|
+
const newlineIdx = text.indexOf("\n");
|
|
334
|
+
const firstLine = newlineIdx < 0 ? text : text.slice(0, newlineIdx);
|
|
335
|
+
const rest = newlineIdx < 0 ? "" : text.slice(newlineIdx);
|
|
336
|
+
const successMatch = firstLine.match(/^Edited .+: applied \d+(?: of \d+)? edits? \((\d+) replacement/);
|
|
337
|
+
const isFailedShape = firstLine.startsWith("multi_edit error: no edits applied to ") && firstLine.endsWith(" attempted).");
|
|
338
|
+
if (!successMatch && !isFailedShape) return text;
|
|
339
|
+
const replacements = successMatch ? Number.parseInt(successMatch[1], 10) || 0 : 0;
|
|
340
|
+
const applied = summarizeOutcomes(merged).applied;
|
|
341
|
+
const total = merged.length;
|
|
342
|
+
let newHeader;
|
|
343
|
+
if (applied === total) newHeader = `Edited ${path}: applied ${total} edit${total === 1 ? "" : "s"} (${replacements} replacement${replacements === 1 ? "" : "s"}).`;
|
|
344
|
+
else if (applied > 0) newHeader = `Edited ${path}: applied ${applied} of ${total} edits (${replacements} replacement${replacements === 1 ? "" : "s"}).`;
|
|
345
|
+
else newHeader = `multi_edit error: no edits applied to ${path} (${total} attempted).`;
|
|
346
|
+
return newHeader + rest;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Aggregate counts for the transcript's summary badge (`3 applied · 1
|
|
350
|
+
* denied · 1 skipped`). Exported so renderers don't reimplement the
|
|
351
|
+
* tally. Pure / O(n).
|
|
352
|
+
*/
|
|
353
|
+
function summarizeOutcomes(outcomes) {
|
|
354
|
+
const counts = {
|
|
355
|
+
applied: 0,
|
|
356
|
+
denied: 0,
|
|
357
|
+
skipped: 0,
|
|
358
|
+
failed: 0,
|
|
359
|
+
pending: 0
|
|
360
|
+
};
|
|
361
|
+
if (!outcomes) return {
|
|
362
|
+
...counts,
|
|
363
|
+
total: 0
|
|
364
|
+
};
|
|
365
|
+
for (const o of outcomes) counts[o.kind] += 1;
|
|
366
|
+
return {
|
|
367
|
+
...counts,
|
|
368
|
+
total: outcomes.length
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
//#endregion
|
|
372
|
+
//#region src/chat/edit-diff.ts
|
|
373
|
+
function extractEditPayload(name, input, priorContent) {
|
|
374
|
+
const path = input.path;
|
|
375
|
+
if (typeof path !== "string" || path === "") return void 0;
|
|
376
|
+
if (name === "edit") {
|
|
377
|
+
const oldString = input.old_string;
|
|
378
|
+
const newString = input.new_string;
|
|
379
|
+
if (typeof oldString !== "string" || typeof newString !== "string") return void 0;
|
|
380
|
+
return {
|
|
381
|
+
tool: "edit",
|
|
382
|
+
path,
|
|
383
|
+
hunks: [{
|
|
384
|
+
oldString,
|
|
385
|
+
newString,
|
|
386
|
+
...input.replace_all === true ? { replaceAll: true } : {}
|
|
387
|
+
}],
|
|
388
|
+
...priorContent !== void 0 ? { priorContent } : {}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (name === "multi_edit") {
|
|
392
|
+
const steps = input.edits;
|
|
393
|
+
if (!Array.isArray(steps) || steps.length === 0) return void 0;
|
|
394
|
+
const hunks = [];
|
|
395
|
+
for (const raw of steps) {
|
|
396
|
+
if (typeof raw?.old_string !== "string" || typeof raw?.new_string !== "string") return void 0;
|
|
397
|
+
hunks.push({
|
|
398
|
+
oldString: raw.old_string,
|
|
399
|
+
newString: raw.new_string,
|
|
400
|
+
...raw.replace_all === true ? { replaceAll: true } : {}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
tool: "multi_edit",
|
|
405
|
+
path,
|
|
406
|
+
hunks,
|
|
407
|
+
...priorContent !== void 0 ? { priorContent } : {}
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
if (name === "write_file") {
|
|
411
|
+
const content = input.content;
|
|
412
|
+
if (typeof content !== "string") return void 0;
|
|
413
|
+
return {
|
|
414
|
+
tool: "write_file",
|
|
415
|
+
path,
|
|
416
|
+
hunks: [{
|
|
417
|
+
oldString: priorContent ?? "",
|
|
418
|
+
newString: content
|
|
419
|
+
}],
|
|
420
|
+
...priorContent !== void 0 ? { priorContent } : {}
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* LCS table cell budget shared by the line- and token-level diffs. 4M
|
|
426
|
+
* cells ≈ low tens of MB of `number[]` rows — imperceptible — while
|
|
427
|
+
* anything needing more (multi-thousand-line change cores) wouldn't
|
|
428
|
+
* produce a human-readable minimal diff anyway.
|
|
429
|
+
*/
|
|
430
|
+
const LCS_CELL_BUDGET = 4e6;
|
|
431
|
+
function computeLineDiff(oldString, newString) {
|
|
432
|
+
const oldLines = splitLines(oldString);
|
|
433
|
+
const newLines = splitLines(newString);
|
|
434
|
+
let lo = 0;
|
|
435
|
+
while (lo < oldLines.length && lo < newLines.length && oldLines[lo] === newLines[lo]) lo++;
|
|
436
|
+
let oldHi = oldLines.length;
|
|
437
|
+
let newHi = newLines.length;
|
|
438
|
+
while (oldHi > lo && newHi > lo && oldLines[oldHi - 1] === newLines[newHi - 1]) {
|
|
439
|
+
oldHi--;
|
|
440
|
+
newHi--;
|
|
441
|
+
}
|
|
442
|
+
const out = [];
|
|
443
|
+
for (let k = 0; k < lo; k++) out.push({
|
|
444
|
+
op: "context",
|
|
445
|
+
text: oldLines[k]
|
|
446
|
+
});
|
|
447
|
+
const n = oldHi - lo;
|
|
448
|
+
const m = newHi - lo;
|
|
449
|
+
if ((n + 1) * (m + 1) > LCS_CELL_BUDGET) {
|
|
450
|
+
for (let k = lo; k < oldHi; k++) out.push({
|
|
451
|
+
op: "remove",
|
|
452
|
+
text: oldLines[k]
|
|
453
|
+
});
|
|
454
|
+
for (let k = lo; k < newHi; k++) out.push({
|
|
455
|
+
op: "add",
|
|
456
|
+
text: newLines[k]
|
|
457
|
+
});
|
|
458
|
+
} else if (n > 0 || m > 0) appendLcsDiff(out, oldLines, newLines, lo, oldHi, newHi);
|
|
459
|
+
for (let k = oldHi; k < oldLines.length; k++) out.push({
|
|
460
|
+
op: "context",
|
|
461
|
+
text: oldLines[k]
|
|
462
|
+
});
|
|
463
|
+
return out;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* The classic LCS table + reverse walk over `old[lo..oldHi) ×
|
|
467
|
+
* new[lo..newHi)`, appending ops to `out` in forward order. Diagonals at
|
|
468
|
+
* equal lines → context; otherwise step into the higher of (up, left)
|
|
469
|
+
* and emit remove/add respectively. Ties break so adjacent remove/add
|
|
470
|
+
* pairs stay grouped (the intra-line highlighter pairs them up).
|
|
471
|
+
*/
|
|
472
|
+
function appendLcsDiff(out, oldLines, newLines, lo, oldHi, newHi) {
|
|
473
|
+
const n = oldHi - lo;
|
|
474
|
+
const m = newHi - lo;
|
|
475
|
+
const lcs = Array.from({ length: n + 1 }, () => Array.from({ length: m + 1 }).fill(0));
|
|
476
|
+
for (let i = 0; i < n; i++) for (let j = 0; j < m; j++) lcs[i + 1][j + 1] = oldLines[lo + i] === newLines[lo + j] ? lcs[i][j] + 1 : Math.max(lcs[i][j + 1], lcs[i + 1][j]);
|
|
477
|
+
const core = [];
|
|
478
|
+
let i = n;
|
|
479
|
+
let j = m;
|
|
480
|
+
while (i > 0 || j > 0) {
|
|
481
|
+
if (i > 0 && j > 0 && oldLines[lo + i - 1] === newLines[lo + j - 1]) {
|
|
482
|
+
core.push({
|
|
483
|
+
op: "context",
|
|
484
|
+
text: oldLines[lo + i - 1]
|
|
485
|
+
});
|
|
486
|
+
i--;
|
|
487
|
+
j--;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
|
|
491
|
+
core.push({
|
|
492
|
+
op: "add",
|
|
493
|
+
text: newLines[lo + j - 1]
|
|
494
|
+
});
|
|
495
|
+
j--;
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
core.push({
|
|
499
|
+
op: "remove",
|
|
500
|
+
text: oldLines[lo + i - 1]
|
|
501
|
+
});
|
|
502
|
+
i--;
|
|
503
|
+
}
|
|
504
|
+
core.reverse();
|
|
505
|
+
out.push(...core);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Split a string into lines preserving empty lines but dropping the
|
|
509
|
+
* implicit trailing `""` produced by a final `\n`. Exported only for
|
|
510
|
+
* its tests — callers should use `computeLineDiff`.
|
|
511
|
+
*/
|
|
512
|
+
function splitLines(s) {
|
|
513
|
+
if (s === "") return [];
|
|
514
|
+
const parts = s.split("\n");
|
|
515
|
+
if (parts[parts.length - 1] === "") parts.pop();
|
|
516
|
+
return parts;
|
|
517
|
+
}
|
|
518
|
+
function computeInlineDiff(oldLine, newLine) {
|
|
519
|
+
const oldTokens = tokenize(oldLine);
|
|
520
|
+
const newTokens = tokenize(newLine);
|
|
521
|
+
const n = oldTokens.length;
|
|
522
|
+
const m = newTokens.length;
|
|
523
|
+
if ((n + 1) * (m + 1) > LCS_CELL_BUDGET) return {
|
|
524
|
+
oldSegments: oldLine === "" ? [] : [{
|
|
525
|
+
text: oldLine,
|
|
526
|
+
changed: true
|
|
527
|
+
}],
|
|
528
|
+
newSegments: newLine === "" ? [] : [{
|
|
529
|
+
text: newLine,
|
|
530
|
+
changed: true
|
|
531
|
+
}]
|
|
532
|
+
};
|
|
533
|
+
const lcs = Array.from({ length: n + 1 }, () => Array.from({ length: m + 1 }).fill(0));
|
|
534
|
+
for (let i = 0; i < n; i++) for (let j = 0; j < m; j++) lcs[i + 1][j + 1] = oldTokens[i] === newTokens[j] ? lcs[i][j] + 1 : Math.max(lcs[i][j + 1], lcs[i + 1][j]);
|
|
535
|
+
const oldSegments = [];
|
|
536
|
+
const newSegments = [];
|
|
537
|
+
let i = n;
|
|
538
|
+
let j = m;
|
|
539
|
+
while (i > 0 || j > 0) {
|
|
540
|
+
if (i > 0 && j > 0 && oldTokens[i - 1] === newTokens[j - 1]) {
|
|
541
|
+
pushSegment(oldSegments, {
|
|
542
|
+
text: oldTokens[i - 1],
|
|
543
|
+
changed: false
|
|
544
|
+
});
|
|
545
|
+
pushSegment(newSegments, {
|
|
546
|
+
text: newTokens[j - 1],
|
|
547
|
+
changed: false
|
|
548
|
+
});
|
|
549
|
+
i--;
|
|
550
|
+
j--;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
|
|
554
|
+
pushSegment(newSegments, {
|
|
555
|
+
text: newTokens[j - 1],
|
|
556
|
+
changed: true
|
|
557
|
+
});
|
|
558
|
+
j--;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
pushSegment(oldSegments, {
|
|
562
|
+
text: oldTokens[i - 1],
|
|
563
|
+
changed: true
|
|
564
|
+
});
|
|
565
|
+
i--;
|
|
566
|
+
}
|
|
567
|
+
oldSegments.reverse();
|
|
568
|
+
newSegments.reverse();
|
|
569
|
+
return {
|
|
570
|
+
oldSegments,
|
|
571
|
+
newSegments
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Coalesce adjacent same-state segments so the renderer emits one
|
|
576
|
+
* `<span>` per run instead of one per token — keeps the React tree
|
|
577
|
+
* shallow on dense lines without changing the visual output.
|
|
578
|
+
*
|
|
579
|
+
* Walking direction is reverse (we push during the reverse walk, then
|
|
580
|
+
* the caller reverses the array), so we coalesce against the *tail*.
|
|
581
|
+
*/
|
|
582
|
+
function pushSegment(buf, seg) {
|
|
583
|
+
const tail = buf[buf.length - 1];
|
|
584
|
+
if (tail && tail.changed === seg.changed) tail.text = seg.text + tail.text;
|
|
585
|
+
else buf.push(seg);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Tokenize on word / non-word boundaries. Each run of `\w+` is one
|
|
589
|
+
* token; each run of `\W+` (whitespace, punctuation) is another. This
|
|
590
|
+
* gives the right granularity for renames (`oldName` → `newName`) and
|
|
591
|
+
* for symbol swaps (`+ → -`) without exploding into per-char segments.
|
|
592
|
+
*
|
|
593
|
+
* Exported only for its tests.
|
|
594
|
+
*/
|
|
595
|
+
function tokenize(s) {
|
|
596
|
+
if (s === "") return [];
|
|
597
|
+
const out = [];
|
|
598
|
+
for (const match of s.matchAll(/\w+|\W+/g)) out.push(match[0]);
|
|
599
|
+
return out;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Apply the payload's hunks against `priorContent` and return the
|
|
603
|
+
* resulting file body. Mirrors the agent's tool-side semantics:
|
|
604
|
+
* - `replaceAll === true` → `String.replaceAll`
|
|
605
|
+
* - otherwise → first-occurrence `String.replace`
|
|
606
|
+
*
|
|
607
|
+
* Hunks are applied in order — a `multi_edit` later hunk operates on
|
|
608
|
+
* the output of the earlier ones, just like the actual tool.
|
|
609
|
+
*/
|
|
610
|
+
function applyEditPayload(payload, priorContent) {
|
|
611
|
+
let out = priorContent;
|
|
612
|
+
for (const hunk of payload.hunks) out = hunk.replaceAll ? out.replaceAll(hunk.oldString, hunk.newString) : out.replace(hunk.oldString, hunk.newString);
|
|
613
|
+
return out;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Like `buildUnifiedDiff` but operating against the full file content
|
|
617
|
+
* so the diff carries *real* file line numbers and configurable
|
|
618
|
+
* surrounding context.
|
|
619
|
+
*
|
|
620
|
+
* Strategy:
|
|
621
|
+
* 1. Apply the payload to `priorContent` → `newContent`.
|
|
622
|
+
* 2. Run `computeLineDiff` over the whole file.
|
|
623
|
+
* 3. Group non-context ops into hunks, padding each with up to
|
|
624
|
+
* `contextLines` of context above and below. Adjacent hunks
|
|
625
|
+
* whose context regions touch are merged so we don't emit two
|
|
626
|
+
* `@@` headers separated by zero context lines.
|
|
627
|
+
*
|
|
628
|
+
* The output line numbers in the `@@` header are 1-based and reflect
|
|
629
|
+
* the change's position in the actual file — what the user expects
|
|
630
|
+
* when reading a diff alongside their editor.
|
|
631
|
+
*
|
|
632
|
+
* For `write_file` creating a new file (priorContent === ''), this
|
|
633
|
+
* falls back to the same `--- /dev/null` convention as
|
|
634
|
+
* `buildUnifiedDiff`.
|
|
635
|
+
*/
|
|
636
|
+
function buildContextualDiff(payload, priorContent, contextLines = 3) {
|
|
637
|
+
const newContent = applyEditPayload(payload, priorContent);
|
|
638
|
+
const isNewFile = priorContent === "";
|
|
639
|
+
const ops = computeLineDiff(priorContent, newContent);
|
|
640
|
+
const oldLineFor = [];
|
|
641
|
+
const newLineFor = [];
|
|
642
|
+
let ol = 1;
|
|
643
|
+
let nl = 1;
|
|
644
|
+
for (const op of ops) {
|
|
645
|
+
oldLineFor.push(ol);
|
|
646
|
+
newLineFor.push(nl);
|
|
647
|
+
if (op.op !== "add") ol++;
|
|
648
|
+
if (op.op !== "remove") nl++;
|
|
649
|
+
}
|
|
650
|
+
const hunks = [];
|
|
651
|
+
for (let i = 0; i < ops.length; i++) {
|
|
652
|
+
if (ops[i].op === "context") continue;
|
|
653
|
+
const start = Math.max(0, i - contextLines);
|
|
654
|
+
const end = Math.min(ops.length - 1, i + contextLines);
|
|
655
|
+
const last = hunks[hunks.length - 1];
|
|
656
|
+
if (last && start <= last[1] + 1) last[1] = Math.max(last[1], end);
|
|
657
|
+
else hunks.push([start, end]);
|
|
658
|
+
}
|
|
659
|
+
if (hunks.length === 0) return "";
|
|
660
|
+
const parts = [];
|
|
661
|
+
parts.push(isNewFile ? "--- /dev/null" : `--- a/${payload.path}`);
|
|
662
|
+
parts.push(`+++ b/${payload.path}`);
|
|
663
|
+
for (const [start, end] of hunks) {
|
|
664
|
+
const slice = ops.slice(start, end + 1);
|
|
665
|
+
const oldCount = slice.filter((l) => l.op !== "add").length;
|
|
666
|
+
const newCount = slice.filter((l) => l.op !== "remove").length;
|
|
667
|
+
const oldStart = oldCount === 0 ? Math.max(0, oldLineFor[start] - 1) : oldLineFor[start];
|
|
668
|
+
const newStart = newCount === 0 ? Math.max(0, newLineFor[start] - 1) : newLineFor[start];
|
|
669
|
+
parts.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`);
|
|
670
|
+
for (const line of slice) {
|
|
671
|
+
const prefix = line.op === "add" ? "+" : line.op === "remove" ? "-" : " ";
|
|
672
|
+
parts.push(`${prefix}${line.text}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return `${parts.join("\n")}\n`;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Build a per-hunk digest used by the compact diff view.
|
|
679
|
+
*
|
|
680
|
+
* Strategy:
|
|
681
|
+
* - When `priorContent` is present and the payload describes a real
|
|
682
|
+
* file transformation, compute the contextual diff once, then walk
|
|
683
|
+
* the LCS ops splitting at runs of `add` / `remove` to anchor each
|
|
684
|
+
* summary entry to the **real** file line. This guarantees the
|
|
685
|
+
* summary's `L<n>` matches what the user sees in their editor.
|
|
686
|
+
* - Otherwise, fall back to per-hunk LCS over the (oldString,
|
|
687
|
+
* newString) snippet pair. Line numbers are absent because the
|
|
688
|
+
* snippet has no file position.
|
|
689
|
+
*/
|
|
690
|
+
function summarizeEditPayload(payload) {
|
|
691
|
+
const prior = payload.priorContent;
|
|
692
|
+
if (prior !== void 0) return summarizeOpsByHunk(computeLineDiff(prior, applyEditPayload(payload, prior)));
|
|
693
|
+
const hunks = [];
|
|
694
|
+
let totalAdded = 0;
|
|
695
|
+
let totalRemoved = 0;
|
|
696
|
+
for (const hunk of payload.hunks) {
|
|
697
|
+
const ops = computeLineDiff(hunk.oldString, hunk.newString);
|
|
698
|
+
let added = 0;
|
|
699
|
+
let removed = 0;
|
|
700
|
+
let firstOld;
|
|
701
|
+
let firstNew;
|
|
702
|
+
for (const op of ops) if (op.op === "add") {
|
|
703
|
+
added++;
|
|
704
|
+
if (firstNew === void 0) firstNew = op.text;
|
|
705
|
+
} else if (op.op === "remove") {
|
|
706
|
+
removed++;
|
|
707
|
+
if (firstOld === void 0) firstOld = op.text;
|
|
708
|
+
}
|
|
709
|
+
totalAdded += added;
|
|
710
|
+
totalRemoved += removed;
|
|
711
|
+
hunks.push({
|
|
712
|
+
added,
|
|
713
|
+
removed,
|
|
714
|
+
...firstOld !== void 0 ? { firstOld } : {},
|
|
715
|
+
...firstNew !== void 0 ? { firstNew } : {}
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
return {
|
|
719
|
+
totalAdded,
|
|
720
|
+
totalRemoved,
|
|
721
|
+
hunks
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Walk an LCS op stream and emit one summary entry per *run* of
|
|
726
|
+
* non-context ops, with the new-file line number where each run
|
|
727
|
+
* starts. Adjacent add/remove ops collapse into the same entry —
|
|
728
|
+
* matches git's hunk grouping at zero context.
|
|
729
|
+
*/
|
|
730
|
+
function summarizeOpsByHunk(ops) {
|
|
731
|
+
const hunks = [];
|
|
732
|
+
let totalAdded = 0;
|
|
733
|
+
let totalRemoved = 0;
|
|
734
|
+
let nl = 1;
|
|
735
|
+
let i = 0;
|
|
736
|
+
while (i < ops.length) {
|
|
737
|
+
if (ops[i].op === "context") {
|
|
738
|
+
nl++;
|
|
739
|
+
i++;
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const runStartLine = nl;
|
|
743
|
+
let added = 0;
|
|
744
|
+
let removed = 0;
|
|
745
|
+
let firstOld;
|
|
746
|
+
let firstNew;
|
|
747
|
+
while (i < ops.length && ops[i].op !== "context") {
|
|
748
|
+
const cur = ops[i];
|
|
749
|
+
if (cur.op === "add") {
|
|
750
|
+
added++;
|
|
751
|
+
if (firstNew === void 0) firstNew = cur.text;
|
|
752
|
+
nl++;
|
|
753
|
+
} else {
|
|
754
|
+
removed++;
|
|
755
|
+
if (firstOld === void 0) firstOld = cur.text;
|
|
756
|
+
}
|
|
757
|
+
i++;
|
|
758
|
+
}
|
|
759
|
+
totalAdded += added;
|
|
760
|
+
totalRemoved += removed;
|
|
761
|
+
hunks.push({
|
|
762
|
+
line: runStartLine,
|
|
763
|
+
added,
|
|
764
|
+
removed,
|
|
765
|
+
...firstOld !== void 0 ? { firstOld } : {},
|
|
766
|
+
...firstNew !== void 0 ? { firstNew } : {}
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
totalAdded,
|
|
771
|
+
totalRemoved,
|
|
772
|
+
hunks
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function previewEditPayload(payload, priorContent, contextLines = 3) {
|
|
776
|
+
const resolution = [];
|
|
777
|
+
const resolvedHunks = [];
|
|
778
|
+
const perHunkDiff = [];
|
|
779
|
+
let running = priorContent;
|
|
780
|
+
for (const hunk of payload.hunks) {
|
|
781
|
+
if (hunk.oldString === "" || hunk.oldString === running) {
|
|
782
|
+
resolution.push({
|
|
783
|
+
resolved: true,
|
|
784
|
+
via: "exact",
|
|
785
|
+
occurrences: 1
|
|
786
|
+
});
|
|
787
|
+
resolvedHunks.push(hunk);
|
|
788
|
+
perHunkDiff.push(buildContextualDiff({
|
|
789
|
+
...payload,
|
|
790
|
+
hunks: [hunk]
|
|
791
|
+
}, running, contextLines));
|
|
792
|
+
running = hunk.newString;
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
const match = resolveOldString(running, hunk.oldString);
|
|
796
|
+
if (!match) {
|
|
797
|
+
resolution.push({ resolved: false });
|
|
798
|
+
resolvedHunks.push(hunk);
|
|
799
|
+
perHunkDiff.push("");
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
const ambiguous = match.occurrences > 1 && !hunk.replaceAll;
|
|
803
|
+
const styledNew = styleReplacementForVia(hunk.newString, match.via, match.actual);
|
|
804
|
+
const resolvedHunk = {
|
|
805
|
+
oldString: match.actual,
|
|
806
|
+
newString: styledNew,
|
|
807
|
+
...hunk.replaceAll ? { replaceAll: true } : {}
|
|
808
|
+
};
|
|
809
|
+
resolution.push({
|
|
810
|
+
resolved: !ambiguous,
|
|
811
|
+
via: match.via,
|
|
812
|
+
occurrences: match.occurrences,
|
|
813
|
+
...ambiguous ? { ambiguous: true } : {}
|
|
814
|
+
});
|
|
815
|
+
resolvedHunks.push(resolvedHunk);
|
|
816
|
+
perHunkDiff.push(ambiguous ? "" : buildContextualDiff({
|
|
817
|
+
...payload,
|
|
818
|
+
hunks: [resolvedHunk]
|
|
819
|
+
}, running, contextLines));
|
|
820
|
+
if (!ambiguous) running = hunk.replaceAll ? running.replaceAll(match.actual, styledNew) : running.replace(match.actual, styledNew);
|
|
821
|
+
}
|
|
822
|
+
const resolvedPayload = {
|
|
823
|
+
...payload,
|
|
824
|
+
hunks: resolvedHunks
|
|
825
|
+
};
|
|
826
|
+
const applicableHunks = resolvedHunks.filter((_, i) => resolution[i].resolved);
|
|
827
|
+
return {
|
|
828
|
+
diffText: applicableHunks.length === 0 ? "" : buildContextualDiff({
|
|
829
|
+
...payload,
|
|
830
|
+
hunks: applicableHunks
|
|
831
|
+
}, priorContent, contextLines),
|
|
832
|
+
resolution,
|
|
833
|
+
perHunkDiff,
|
|
834
|
+
resolvedPayload
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
/** Default size budget (UTF-16 code units) for transcript diff rows. */
|
|
838
|
+
const DIFF_RENDER_MAX_BYTES = 524288;
|
|
839
|
+
/** Marker appended to a line hard-truncated mid-content by the size cap. */
|
|
840
|
+
const CLIP_LINE_MARKER = " …⟨line clipped⟩";
|
|
841
|
+
/**
|
|
842
|
+
* Truncate `line` so it fits `budget` code units including the marker,
|
|
843
|
+
* without splitting a UTF-16 surrogate pair. Returns the line unchanged
|
|
844
|
+
* when it already fits.
|
|
845
|
+
*/
|
|
846
|
+
function clipLine(line, budget) {
|
|
847
|
+
if (line.length <= budget) return line;
|
|
848
|
+
let end = Math.max(0, budget - 16);
|
|
849
|
+
const code = line.charCodeAt(end - 1);
|
|
850
|
+
if (end > 0 && code >= 55296 && code <= 56319) end--;
|
|
851
|
+
return line.slice(0, end) + CLIP_LINE_MARKER;
|
|
852
|
+
}
|
|
853
|
+
function clipDiffForRender(diff, maxLines = 600, maxBytes = DIFF_RENDER_MAX_BYTES) {
|
|
854
|
+
const lines = diff.split("\n");
|
|
855
|
+
const totalLines = lines.length > 0 && lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
|
|
856
|
+
if (totalLines <= maxLines && diff.length <= maxBytes) return {
|
|
857
|
+
text: diff,
|
|
858
|
+
hiddenLines: 0,
|
|
859
|
+
totalLines
|
|
860
|
+
};
|
|
861
|
+
const kept = [];
|
|
862
|
+
let used = 0;
|
|
863
|
+
for (let i = 0; i < totalLines; i++) {
|
|
864
|
+
if (kept.length >= maxLines) break;
|
|
865
|
+
const line = lines[i];
|
|
866
|
+
const cost = line.length + 1;
|
|
867
|
+
if (used + cost > maxBytes) {
|
|
868
|
+
if (kept.length === 0) kept.push(clipLine(line, maxBytes));
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
871
|
+
kept.push(line);
|
|
872
|
+
used += cost;
|
|
873
|
+
}
|
|
874
|
+
return {
|
|
875
|
+
text: kept.join("\n"),
|
|
876
|
+
hiddenLines: totalLines - kept.length,
|
|
877
|
+
totalLines
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
function buildUnifiedDiff(payload) {
|
|
881
|
+
const parts = [];
|
|
882
|
+
const isNewFile = payload.tool === "write_file" && payload.hunks[0]?.oldString === "";
|
|
883
|
+
parts.push(isNewFile ? `--- /dev/null` : `--- a/${payload.path}`);
|
|
884
|
+
parts.push(`+++ b/${payload.path}`);
|
|
885
|
+
for (const hunk of payload.hunks) {
|
|
886
|
+
const lines = computeLineDiff(hunk.oldString, hunk.newString);
|
|
887
|
+
const oldCount = lines.filter((l) => l.op !== "add").length;
|
|
888
|
+
const newCount = lines.filter((l) => l.op !== "remove").length;
|
|
889
|
+
const oldStart = oldCount === 0 ? 0 : 1;
|
|
890
|
+
const newStart = newCount === 0 ? 0 : 1;
|
|
891
|
+
parts.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`);
|
|
892
|
+
for (const line of lines) {
|
|
893
|
+
const prefix = line.op === "add" ? "+" : line.op === "remove" ? "-" : " ";
|
|
894
|
+
parts.push(`${prefix}${line.text}`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return `${parts.join("\n")}\n`;
|
|
898
|
+
}
|
|
899
|
+
const FILETYPE_BY_EXT = {
|
|
900
|
+
ts: "typescript",
|
|
901
|
+
mts: "typescript",
|
|
902
|
+
cts: "typescript",
|
|
903
|
+
tsx: "typescriptreact",
|
|
904
|
+
js: "javascript",
|
|
905
|
+
mjs: "javascript",
|
|
906
|
+
cjs: "javascript",
|
|
907
|
+
jsx: "javascriptreact",
|
|
908
|
+
py: "python",
|
|
909
|
+
pyi: "python",
|
|
910
|
+
rs: "rust",
|
|
911
|
+
go: "go",
|
|
912
|
+
json: "json",
|
|
913
|
+
jsonc: "json",
|
|
914
|
+
sh: "bash",
|
|
915
|
+
bash: "bash",
|
|
916
|
+
zsh: "bash",
|
|
917
|
+
yaml: "yaml",
|
|
918
|
+
yml: "yaml",
|
|
919
|
+
html: "html",
|
|
920
|
+
htm: "html",
|
|
921
|
+
css: "css",
|
|
922
|
+
md: "markdown",
|
|
923
|
+
markdown: "markdown",
|
|
924
|
+
zig: "zig",
|
|
925
|
+
lua: "lua",
|
|
926
|
+
toml: "toml",
|
|
927
|
+
sql: "sql"
|
|
928
|
+
};
|
|
929
|
+
function filetypeFromPath(path) {
|
|
930
|
+
const cleaned = path.split(/[?#]/, 1)[0];
|
|
931
|
+
const lastDot = cleaned.lastIndexOf(".");
|
|
932
|
+
if (lastDot === -1 || lastDot === cleaned.length - 1) return void 0;
|
|
933
|
+
const ext = cleaned.slice(lastDot + 1).toLowerCase();
|
|
934
|
+
return FILETYPE_BY_EXT[ext];
|
|
935
|
+
}
|
|
936
|
+
//#endregion
|
|
937
|
+
//#region src/chat/prompt-segments.ts
|
|
938
|
+
/**
|
|
939
|
+
* Split a prompt buffer into word-sized atomic segments suitable for a
|
|
940
|
+
* flex-row + flex-wrap renderer (TUI) or a `display: inline` flow with
|
|
941
|
+
* inline-block chips (GUI). Each chip becomes one segment (atomic —
|
|
942
|
+
* never broken across rows); each plain run is split into "word +
|
|
943
|
+
* trailing space" units so wraps land at clean word boundaries.
|
|
944
|
+
*
|
|
945
|
+
* Robust to:
|
|
946
|
+
* - Overlapping refs — sorted by start; later refs that overlap are
|
|
947
|
+
* dropped via the first-wins rule.
|
|
948
|
+
* - Out-of-bounds refs — dropped entirely when `end > text.length` or
|
|
949
|
+
* `start >= text.length`. Partial clipping would silently truncate
|
|
950
|
+
* a chip's label; the caller is in a better position to surface the
|
|
951
|
+
* mismatch (typically a stale `refs` array referencing a previous text).
|
|
952
|
+
* - Whitespace-only plain runs — emitted as their own plain segment
|
|
953
|
+
* so chip-adjacent-to-chip cases keep the original spacing.
|
|
954
|
+
*
|
|
955
|
+
* Word splitter rationale: `\S+\s*` keeps trailing whitespace attached
|
|
956
|
+
* to its preceding word so wrap boundaries land between words (cleanly).
|
|
957
|
+
* A leading-whitespace-only segment is captured by `\s+` so we don't
|
|
958
|
+
* drop it entirely when the plain run starts with a space.
|
|
959
|
+
*/
|
|
960
|
+
function splitPromptSegments(text, refs) {
|
|
961
|
+
const sorted = [...refs].filter((r) => r.end > r.start && r.start < text.length && r.end <= text.length).sort((a, b) => a.start - b.start);
|
|
962
|
+
const out = [];
|
|
963
|
+
let cursor = 0;
|
|
964
|
+
for (const ref of sorted) {
|
|
965
|
+
if (ref.start < cursor) continue;
|
|
966
|
+
if (ref.start > cursor) {
|
|
967
|
+
const matches = text.slice(cursor, ref.start).match(/\S+\s*|\s+/g) ?? [];
|
|
968
|
+
for (const m of matches) out.push({
|
|
969
|
+
kind: "plain",
|
|
970
|
+
text: m
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
out.push({
|
|
974
|
+
kind: "chip",
|
|
975
|
+
text: text.slice(ref.start, ref.end),
|
|
976
|
+
providerId: ref.providerId
|
|
977
|
+
});
|
|
978
|
+
cursor = ref.end;
|
|
979
|
+
}
|
|
980
|
+
if (cursor < text.length) {
|
|
981
|
+
const matches = text.slice(cursor).match(/\S+\s*|\s+/g) ?? [];
|
|
982
|
+
for (const m of matches) out.push({
|
|
983
|
+
kind: "plain",
|
|
984
|
+
text: m
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
return out;
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* File-mention refs recovered from a prompt's text: `@<path>` at the start
|
|
991
|
+
* or after whitespace, where the path looks like one (has a `/` or a `.`),
|
|
992
|
+
* trailing punctuation excluded (the files completion's own rule).
|
|
993
|
+
*
|
|
994
|
+
* Refs are known only to the client that sent the prompt, which typed the
|
|
995
|
+
* mention through a completion. The persisted turn and the send echo other
|
|
996
|
+
* windows receive carry the text alone, so without this a prompt's chips
|
|
997
|
+
* existed in the sender's live transcript only: gone after a reload, never
|
|
998
|
+
* shown in another window or the TUI. Replay and the echo fill refs from
|
|
999
|
+
* this when the event has none, so every client paints the same prompt.
|
|
1000
|
+
* Skills and chats mentions are not inferred: their tokens (`/name`,
|
|
1001
|
+
* `#token`) are ordinary text too often to guess without the catalogs.
|
|
1002
|
+
*/
|
|
1003
|
+
function inferPromptRefs(text) {
|
|
1004
|
+
const refs = [];
|
|
1005
|
+
for (const m of text.matchAll(/(^|\s)@(\S+)/g)) {
|
|
1006
|
+
const path = m[2].replace(/[.,;:)\]}!?'"]+$/, "");
|
|
1007
|
+
if (!/[/.]/.test(path) || path.includes("@")) continue;
|
|
1008
|
+
const start = m.index + m[1].length;
|
|
1009
|
+
refs.push({
|
|
1010
|
+
start,
|
|
1011
|
+
end: start + 1 + path.length,
|
|
1012
|
+
providerId: "files"
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
return refs;
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* The segments cut into the lines the user typed. A flex-wrap row cannot
|
|
1019
|
+
* break on a newline inside one of its items, so each line renders as its
|
|
1020
|
+
* own row; an empty array is a blank line.
|
|
1021
|
+
*/
|
|
1022
|
+
function promptSegmentLines(segments) {
|
|
1023
|
+
const lines = [[]];
|
|
1024
|
+
for (const segment of segments) {
|
|
1025
|
+
if (segment.kind !== "plain") {
|
|
1026
|
+
lines[lines.length - 1].push(segment);
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
segment.text.split("\n").forEach((part, i) => {
|
|
1030
|
+
if (i > 0) lines.push([]);
|
|
1031
|
+
if (part) lines[lines.length - 1].push({
|
|
1032
|
+
kind: "plain",
|
|
1033
|
+
text: part
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
return lines;
|
|
1038
|
+
}
|
|
1039
|
+
//#endregion
|
|
1040
|
+
//#region src/chat/settings-defaults.ts
|
|
1041
|
+
const DEFAULT_SETTINGS = {
|
|
1042
|
+
showThinking: true,
|
|
1043
|
+
toolCallDisplay: "formatted",
|
|
1044
|
+
showToolResults: true,
|
|
1045
|
+
groupToolCalls: false,
|
|
1046
|
+
showFullShellOutput: false,
|
|
1047
|
+
showRunReceipt: true,
|
|
1048
|
+
showCacheNotices: true,
|
|
1049
|
+
safeMode: true,
|
|
1050
|
+
useDaemon: false,
|
|
1051
|
+
remoteHosts: false,
|
|
1052
|
+
orchestration: false,
|
|
1053
|
+
loadProjectExtensions: false,
|
|
1054
|
+
importForeignConfigs: false,
|
|
1055
|
+
importRulerConfigs: true,
|
|
1056
|
+
hideSubagentOutput: true,
|
|
1057
|
+
subagentModels: true,
|
|
1058
|
+
queueMessages: true,
|
|
1059
|
+
theme: "default",
|
|
1060
|
+
showAllProjects: false,
|
|
1061
|
+
resumeLastSession: true,
|
|
1062
|
+
persistToolResults: true,
|
|
1063
|
+
stableToolList: false,
|
|
1064
|
+
codemode: false,
|
|
1065
|
+
sandbox: false,
|
|
1066
|
+
sandboxNetwork: "allow",
|
|
1067
|
+
sandboxAllowWrite: "",
|
|
1068
|
+
autoCompact: true,
|
|
1069
|
+
autoCompactThreshold: .8,
|
|
1070
|
+
resumeOnMaxTokens: false,
|
|
1071
|
+
maxOutputTokens: 0,
|
|
1072
|
+
maxTurns: 0,
|
|
1073
|
+
retryMaxAttempts: 0,
|
|
1074
|
+
maxCostUsd: 0,
|
|
1075
|
+
maxTotalTokens: 0,
|
|
1076
|
+
maxWallMinutes: 0,
|
|
1077
|
+
thinkingBinding: "drop_block",
|
|
1078
|
+
showEditDiffs: true,
|
|
1079
|
+
editDiffDisplay: "full",
|
|
1080
|
+
transcriptDensity: "full",
|
|
1081
|
+
showCompactionSummary: false,
|
|
1082
|
+
compactionModel: "",
|
|
1083
|
+
targetFps: 60,
|
|
1084
|
+
mouseInteractions: true,
|
|
1085
|
+
interactionMode: "ask",
|
|
1086
|
+
smoothStreaming: true,
|
|
1087
|
+
fileLogging: false,
|
|
1088
|
+
showTodoIndicator: true,
|
|
1089
|
+
showUsageIndicator: true,
|
|
1090
|
+
showThrobber: false,
|
|
1091
|
+
artifacts: true,
|
|
1092
|
+
browser: true,
|
|
1093
|
+
browserAgent: true,
|
|
1094
|
+
browserAutoShow: true,
|
|
1095
|
+
terminal: true,
|
|
1096
|
+
terminalAgent: true,
|
|
1097
|
+
terminalAgentTasks: true,
|
|
1098
|
+
closeTerminalOnCleanExit: true,
|
|
1099
|
+
confirmGitSync: true,
|
|
1100
|
+
sidebarRunDetails: true,
|
|
1101
|
+
openIn: "editor",
|
|
1102
|
+
checkForUpdates: true,
|
|
1103
|
+
subscriptionUsage: true,
|
|
1104
|
+
uiMode: "full",
|
|
1105
|
+
userInstructionsScope: "both",
|
|
1106
|
+
ctrlCBehavior: "quit",
|
|
1107
|
+
terminalTitle: "off",
|
|
1108
|
+
sessionIcon: "",
|
|
1109
|
+
attentionIcon: "",
|
|
1110
|
+
activeIcon: "",
|
|
1111
|
+
attachmentsEnabled: true,
|
|
1112
|
+
externalEditor: true,
|
|
1113
|
+
inlineAutocomplete: false
|
|
1114
|
+
};
|
|
1115
|
+
//#endregion
|
|
1116
|
+
//#region src/chat/hosts/types.ts
|
|
1117
|
+
/** A refused `add` / `rename`; `code` says why. */
|
|
1118
|
+
var HostRegistryError = class extends Error {
|
|
1119
|
+
code;
|
|
1120
|
+
constructor(code, message) {
|
|
1121
|
+
super(message);
|
|
1122
|
+
this.code = code;
|
|
1123
|
+
this.name = "HostRegistryError";
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
//#endregion
|
|
1127
|
+
//#region src/chat/secret-shapes.ts
|
|
1128
|
+
/**
|
|
1129
|
+
* What a credential looks like, as pure predicates: secret-looking names
|
|
1130
|
+
* (`--api-key`, `DB_PASS`, `"apiKey"`), known token shapes (`sk-…`,
|
|
1131
|
+
* `ghp_…`, `zds_…`) and high-entropy values. Shared by everything that must
|
|
1132
|
+
* keep a token out of what it shows or saves: the host's MCP catalog
|
|
1133
|
+
* redaction (`src/ipc/daemon/redact.ts`) and the remote host URL check
|
|
1134
|
+
* (`./hosts/url.ts`). Node-free.
|
|
1135
|
+
*
|
|
1136
|
+
* Over-matching an ordinary word is a cosmetic loss; under-matching leaks a
|
|
1137
|
+
* credential, so the rules lean wide, but stay off file paths, package names
|
|
1138
|
+
* and plain words (`passthrough`, `bypass`, `keymap`).
|
|
1139
|
+
*/
|
|
1140
|
+
/**
|
|
1141
|
+
* A secret-looking name: a flag (`--auth-token`), a variable (`DB_PASS`), a
|
|
1142
|
+
* JSON key (`apiKey`) or a connection-string key (`Password`). `pass` and
|
|
1143
|
+
* `key` count only as a whole word or a `-` / `_` segment at the end.
|
|
1144
|
+
*/
|
|
1145
|
+
const SECRET_NAME = /token|secret|passw(?:or)?d|passphrase|pwd|api[-_]?key|access[-_]?key|private[-_]?key|auth|credential|bearer|cookie|(?:^|[-_])(?:pass|key)$/i;
|
|
1146
|
+
/** Token prefixes issued by common services, with enough body to be a credential. */
|
|
1147
|
+
const KNOWN_TOKEN = /(?<![\w-])(?:sk-[\w-]{16,}|sk_(?:live|test)_\w{16,}|gh[pousr]_\w{20,}|github_pat_\w{20,}|xox[abposr]-[\w-]{10,}|(?:AKIA|ASIA)[0-9A-Z]{16}|glpat-[\w-]{20,}|zds_[\w-]{20,}|AIza[\w-]{30,})/g;
|
|
1148
|
+
function isSecretName(name) {
|
|
1149
|
+
return SECRET_NAME.test(name);
|
|
1150
|
+
}
|
|
1151
|
+
/** A known token shape somewhere in `value`. */
|
|
1152
|
+
function containsKnownToken(value) {
|
|
1153
|
+
KNOWN_TOKEN.lastIndex = 0;
|
|
1154
|
+
return KNOWN_TOKEN.test(value);
|
|
1155
|
+
}
|
|
1156
|
+
function entropy(value) {
|
|
1157
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1158
|
+
for (const char of value) counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
1159
|
+
let bits = 0;
|
|
1160
|
+
for (const count of counts.values()) {
|
|
1161
|
+
const p = count / value.length;
|
|
1162
|
+
bits -= p * Math.log2(p);
|
|
1163
|
+
}
|
|
1164
|
+
return bits;
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* A whole value that looks like a credential: a known token shape, a long
|
|
1168
|
+
* hex / UUID-like id, or a long random mix of cases and digits. Words joined
|
|
1169
|
+
* by dashes (`server-2024-edition`) and paths stay readable.
|
|
1170
|
+
*/
|
|
1171
|
+
function looksLikeSecret(value) {
|
|
1172
|
+
KNOWN_TOKEN.lastIndex = 0;
|
|
1173
|
+
const known = KNOWN_TOKEN.exec(value);
|
|
1174
|
+
if (known && known[0] === value) return true;
|
|
1175
|
+
if (value.length < 20 || !/^[\w\-+=.~]+$/.test(value) || !/\d/.test(value) || !/[a-z]/i.test(value)) return false;
|
|
1176
|
+
const digits = value.replace(/\D/g, "").length;
|
|
1177
|
+
if (/^[\da-f-]+$/i.test(value)) return digits / value.length >= .25;
|
|
1178
|
+
return /[a-z]/.test(value) && /[A-Z]/.test(value) && entropy(value) >= 4;
|
|
1179
|
+
}
|
|
1180
|
+
function safeDecode(segment) {
|
|
1181
|
+
try {
|
|
1182
|
+
return decodeURIComponent(segment);
|
|
1183
|
+
} catch {
|
|
1184
|
+
return segment;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
/** A URL path segment that carries a credential (as is, or percent-decoded). */
|
|
1188
|
+
function isSecretPathSegment(segment) {
|
|
1189
|
+
const decoded = safeDecode(segment);
|
|
1190
|
+
return looksLikeSecret(segment) || looksLikeSecret(decoded) || containsKnownToken(decoded);
|
|
1191
|
+
}
|
|
1192
|
+
//#endregion
|
|
1193
|
+
//#region src/chat/hosts/url.ts
|
|
1194
|
+
/**
|
|
1195
|
+
* Host URLs a client accepts: `wss://`, or `ws://` to this machine only (a
|
|
1196
|
+
* token must not cross the network in the clear), with no credentials in
|
|
1197
|
+
* them: no `user:pass@`, no query string, no fragment, no token-looking
|
|
1198
|
+
* path segment (`wss://h/zds_…`, a long random id: the shapes the MCP
|
|
1199
|
+
* catalog redacts, `../secret-shapes`). Tokens go in the token field, and a
|
|
1200
|
+
* URL ends up in hosts.json, error messages and the GUI's renderer.
|
|
1201
|
+
* Messages show {@link displayHostUrl}, never the input. Node-free.
|
|
1202
|
+
*/
|
|
1203
|
+
var RemoteHostError = class extends Error {
|
|
1204
|
+
constructor(message) {
|
|
1205
|
+
super(message);
|
|
1206
|
+
this.name = "RemoteHostError";
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
/**
|
|
1210
|
+
* A loopback host: `localhost`, `127.x.x.x` or `::1` (bracketed, as in a URL,
|
|
1211
|
+
* or bare, as an interface to bind). The one check for both ends: a client
|
|
1212
|
+
* allowing `ws://`, a server refusing to listen in the clear.
|
|
1213
|
+
*/
|
|
1214
|
+
function isLoopbackHost(host) {
|
|
1215
|
+
const bare = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
1216
|
+
return bare === "localhost" || bare === "::1" || /^127\.\d+\.\d+\.\d+$/.test(bare);
|
|
1217
|
+
}
|
|
1218
|
+
/** Shown in place of a path segment that looks like a credential. */
|
|
1219
|
+
const REDACTED_SEGMENT = "<redacted>";
|
|
1220
|
+
/** Scheme, host, port and path of a URL (token-looking path segments redacted): what is safe to show or log. */
|
|
1221
|
+
function displayHostUrl(raw) {
|
|
1222
|
+
try {
|
|
1223
|
+
const url = new URL(raw.trim());
|
|
1224
|
+
const path = url.pathname.split("/").map((segment) => isSecretPathSegment(segment) ? REDACTED_SEGMENT : segment).join("/");
|
|
1225
|
+
return `${url.protocol}//${url.host}${path.replace(/\/$/, "")}`;
|
|
1226
|
+
} catch {
|
|
1227
|
+
return "<invalid URL>";
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
/** `raw` carries something {@link parseRemoteHostUrl} refuses as a credential (userinfo, query, fragment, a token-looking path segment). */
|
|
1231
|
+
function urlCarriesSecrets(raw) {
|
|
1232
|
+
if (/[@?#]/.test(raw)) return true;
|
|
1233
|
+
try {
|
|
1234
|
+
return new URL(raw.trim()).pathname.split("/").some(isSecretPathSegment);
|
|
1235
|
+
} catch {
|
|
1236
|
+
return false;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
/** Validate and normalize a host URL (no trailing slash); throws {@link RemoteHostError}. */
|
|
1240
|
+
function parseRemoteHostUrl(raw) {
|
|
1241
|
+
let url;
|
|
1242
|
+
try {
|
|
1243
|
+
url = new URL(raw.trim());
|
|
1244
|
+
} catch {
|
|
1245
|
+
throw new RemoteHostError("Invalid host URL: expected wss://host[:port].");
|
|
1246
|
+
}
|
|
1247
|
+
const shown = displayHostUrl(raw);
|
|
1248
|
+
if (url.username !== "" || url.password !== "") throw new RemoteHostError(`Host URLs can't carry a user or password (${shown}): put the token in the token field (or --token-file), not in the URL.`);
|
|
1249
|
+
if (url.search !== "" || raw.includes("?")) throw new RemoteHostError(`Host URLs can't carry a query string (${shown}): remove it; a token goes in the token field (or --token-file).`);
|
|
1250
|
+
if (url.hash !== "" || raw.includes("#")) throw new RemoteHostError(`Host URLs can't carry a fragment (${shown}): remove it.`);
|
|
1251
|
+
if (url.pathname.split("/").some(isSecretPathSegment)) throw new RemoteHostError(`Host URLs can't carry a token in their path (${url.protocol}//${url.host}): remove it; a token goes in the token field (or --token-file).`);
|
|
1252
|
+
if (url.protocol === "wss:" || url.protocol === "ws:" && isLoopbackHost(url.hostname)) return url.href.replace(/\/$/, "");
|
|
1253
|
+
if (url.protocol === "ws:") throw new RemoteHostError(`Refusing ws://${url.host}: the token would travel unencrypted. Use wss:// (ws:// is only for 127.0.0.1 / localhost).`);
|
|
1254
|
+
throw new RemoteHostError(`Invalid host URL ${shown}: expected wss://host[:port].`);
|
|
1255
|
+
}
|
|
1256
|
+
//#endregion
|
|
1257
|
+
export { resolveApprovalForPayload as A, isAllowedBrowserUrl as B, splitLines as C, maskToOutcomeKinds as D, buildEditOutcomesAnnotation as E, commandNoteText as F, normalizeBrowserUrl as H, commandStepLabel as I, gatherCommandNotes as L, stripEditOutcomesAnnotation as M, summarizeOutcomes as N, mergeApprovalAndBodyOutcomes as O, commandHeaderBlock as P, noteDisplay as R, previewEditPayload as S, tokenize as T, isLocalBrowserUrl as V, clipDiffForRender as _, KNOWN_TOKEN as a, extractEditPayload as b, looksLikeSecret as c, inferPromptRefs as d, promptSegmentLines as f, buildUnifiedDiff as g, buildContextualDiff as h, urlCarriesSecrets as i, rewriteMultiEditHeader as j, parseEditOutcomesFromResult as k, HostRegistryError as l, applyEditPayload as m, displayHostUrl as n, isSecretName as o, splitPromptSegments as p, parseRemoteHostUrl as r, isSecretPathSegment as s, RemoteHostError as t, DEFAULT_SETTINGS as u, computeInlineDiff as v, summarizeEditPayload as w, filetypeFromPath as x, computeLineDiff as y, parseCommandHeader as z };
|
|
1258
|
+
|
|
1259
|
+
//# sourceMappingURL=url-D8HCl65b.js.map
|