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
package/docs/IPC.md
CHANGED
|
@@ -7,8 +7,8 @@ prompts. The engine (`createChatHost`) is transport-agnostic; the daemon
|
|
|
7
7
|
is one embedding of it, reached over a unix socket with JSON-RPC.
|
|
8
8
|
|
|
9
9
|
Entry points: `src/ipc/daemon/server.ts` (`startIpcDaemon`), `src/ipc/client.ts`
|
|
10
|
-
(`connectIpcClient` / `connectOrSpawnDaemon`), `src/tui/runtime/
|
|
11
|
-
(`
|
|
10
|
+
(`connectIpcClient` / `connectOrSpawnDaemon`), `src/tui/runtime/host-session.ts`
|
|
11
|
+
(`connectHostSession`, the TUI's session controller, in-process or daemon).
|
|
12
12
|
|
|
13
13
|
```
|
|
14
14
|
┌────────────┐ ┌────────────┐ ┌─────────────┐
|
|
@@ -30,9 +30,10 @@ Entry points: `src/ipc/daemon/server.ts` (`startIpcDaemon`), `src/ipc/client.ts`
|
|
|
30
30
|
Contents: [Pieces](#the-pieces) · [Host environment](#host-environment) ·
|
|
31
31
|
[RPC methods](#rpc-methods) · [Signals](#signals-and-convergence) ·
|
|
32
32
|
[Concurrent sends](#concurrent-sends) · [Extension bus](#extension-bus-ctxbus) ·
|
|
33
|
-
[Daemon lifecycle](#daemon-lifecycle) · [
|
|
33
|
+
[Daemon lifecycle](#daemon-lifecycle) · [Remote server](#remote-server-zidane-server) ·
|
|
34
|
+
[Reconnect and resume](#reconnect-and-resume) · [Turning it off](#turning-it-off-containment-guarantees) ·
|
|
34
35
|
[Execution semantics](#execution-semantics-read-this) · [Testing](#testing) ·
|
|
35
|
-
[Surface matrix](#surface-cabling-matrix-daemon-runtime) · [Removability](#removability) ·
|
|
36
|
+
[Surface matrix](#surface-cabling-matrix-daemon-runtime) · [TUI remote mode](#tui-remote-mode) · [Removability](#removability) ·
|
|
36
37
|
[Status](#status--follow-ups)
|
|
37
38
|
|
|
38
39
|
## The pieces
|
|
@@ -41,8 +42,15 @@ Contents: [Pieces](#the-pieces) · [Host environment](#host-environment) ·
|
|
|
41
42
|
| --- | --- |
|
|
42
43
|
| `src/ipc/protocol.ts` | Transport contract: typed `IpcMethods` map, `IPC_PROTOCOL_VERSION`, handshake shapes. Re-exports `HostSignal` & co. from `src/chat/host/signals.ts`. |
|
|
43
44
|
| `src/chat/host/` | `createChatHost(env)` — the transport-agnostic engine. Owns live `Agent`s, forwards loop hooks as signals, runs the approval + interaction hubs. |
|
|
44
|
-
| `src/ipc/daemon/` |
|
|
45
|
+
| `src/ipc/daemon/` | `dispatcher.ts`: the transport-free method table, per-peer state, signal fan-out and `ipc.hello` handling (`createHostDispatcher`); a connection is a `HostPeer` with a `send`. `handshake.ts`: the pure version / build / release fences. `server.ts`: the unix-socket transport around it (`startIpcDaemon`: framing, socket file, idle exit, retiring, drain; write backpressure is `../transports/send-budget.ts`). Plus the disk-backed environment and lifecycle helpers (socket path, liveness probe, spawn-on-demand). |
|
|
45
46
|
| `src/ipc/client.ts` | `connectIpcClient` / `connectOrSpawnDaemon` — typed `request()`, `onSignal()`, version handshake with stale-daemon drain. |
|
|
47
|
+
| `src/ipc/transports/` | Wire pieces shared across transports: `peer-rpc.ts` (the host side of one JSON-RPC connection: `ipc.signal` fan-out, requests routed to the dispatcher), `send-budget.ts` (one connection's outgoing budget, socket and WebSocket alike: a peer with more than 8 MiB of pushed signals unsent is dropped, reply bytes set aside; a reply over the message size (WebSocket 32 MiB, socket 256 MiB) goes out as an `IpcErrorCode.ResponseTooLarge` error and the connection stays), `client-core.ts` (the client side: hello with its fences, request deadlines, signals). The WebSocket transport: `ws-server.ts` (`startWsServer`, `Bun.serve`, one JSON-RPC message per text frame; Bun-only, so exported nowhere), `ws-client.ts` (`connectWsClient`, the runtime's global `WebSocket`; never drains a host on mismatch; exported from `zidane/ipc/remote` with the saved-hosts registry (`src/ipc/remote.ts`), not from the `zidane/ipc` barrel, so a client that keeps remote hosts opt-in loads it only on demand), `ws-auth.ts` (token carriers: `Authorization: Bearer`, or a `zidane.bearer.<token>` subprotocol entry next to `zidane` for clients that can't set headers; not enforced unless the server is given `authorize`). |
|
|
48
|
+
| `src/ipc/daemon/signal-log.ts` | Per-session signal numbering and the bounded replay log `sessions.resume` reads; see [Reconnect and resume](#reconnect-and-resume). |
|
|
49
|
+
| `src/server/` | zidane-server: `startZidaneServer` (`server.ts`: one host behind the WebSocket transport with device tokens and a unix socket), the token store (`tokens.ts`), configuration (`config.ts`), upgrade policy (`security.ts`), the checks before the token and the failure accounting (`upgrade-guard.ts`), the audit log (`audit.ts`), and `zidane server …` (`cli.ts`, standalone `main.ts`). May import `src/ipc` and `src/chat`; nothing in `src/` imports it (the `zidane` binary routes `server` to `cli.ts`). See [Remote server](#remote-server-zidane-server). |
|
|
50
|
+
| `src/ipc/daemon/identity.ts` | Host identity: a UUID written once to `<userDataDir>/host-id`. The daemon and a window's in-process host share the user dir, so they share the id (one computer, one store); a remote server has its own. |
|
|
51
|
+
| `src/ipc/memory.ts` | `startInProcessHost` — the same dispatcher in the caller's process, with `IpcClient`s that call it directly (no socket, no spawn). Wire-faithful: JSON copies (a top-level `undefined` result → `null`, nested `undefined` fields dropped), `JsonRpcRemoteError` codes, `JsonRpcConnectionClosedError` for closed connections (in-flight requests included), responses and signals through one queue in production order, `close()` detaches quietly, host shutdown drains runs and fires `onClose`. No request deadlines (nothing to reconnect to) and `daemon.shutdown` does not stop it: its lifetime belongs to the embedding window. |
|
|
52
|
+
| `src/tui/runtime/transport.ts` | `HostTransport`: how a TUI window reaches its host. `socketTransport` (connect-or-spawn for session controllers, a passive connect for the presence watcher) or `inProcessTransport(host)`. `connectHostSession({ transport })` and the presence watcher speak only `IpcClient`. |
|
|
53
|
+
| `src/chat/hosts/` | The client's saved hosts (`createHostRegistry`): entries in `<userDir>/hosts.json` (0600, atomic), tokens in `credentials.json` under `hosts:<id>`. `add` verifies a host before saving it (the entry id is the server's `hostId`); `test` says who answers, or why not (`auth`, `unreachable`, `tls`, `protocol`, `identity`, …). Connecting is injected: `src/ipc/hosts/probe.ts` (`createWsHostRegistry`, `probeWsHost`) wires the WebSocket client. The host index (`host-index.ts`: which host an id lives on, never guessed) and the presence merge (`presence.ts`) are here too. Types and URL rules are in `zidane/chat/pure`. |
|
|
46
54
|
| `src/ipc/attach.ts` | `zidane attach` — a thin readline client proving the multi-window contract end-to-end. |
|
|
47
55
|
|
|
48
56
|
## Host environment
|
|
@@ -57,6 +65,15 @@ process supplies a `ChatHostEnvironment`:
|
|
|
57
65
|
same `state.json` settings slot; pending approvals in
|
|
58
66
|
`pending-approvals.json`; per-cwd extension discovery with the same
|
|
59
67
|
allowlist + trust contract as every other surface.
|
|
68
|
+
- **A host in the window's own process**: `createHostEnvironment({
|
|
69
|
+
broadcast, shared: { config, runtimeConfig, hostRuntime?, extensionBus?,
|
|
70
|
+
multiProcess? } })`, the daemon's environment reusing the window's boot
|
|
71
|
+
config (one store and one writer to `state.json` in the process), its
|
|
72
|
+
per-cwd runtime config (no second extension discovery, so no extension
|
|
73
|
+
`setup` runs twice in the process; `invalidate` / `forget` keep the
|
|
74
|
+
daemon's reload contract) and its extension bus. Single-process unless
|
|
75
|
+
`multiProcess`. `createDaemonEnvironment` is the same function without
|
|
76
|
+
`shared`: own config, discovery, multi-process.
|
|
60
77
|
|
|
61
78
|
Both run the same engine — the gate flow, edit-approval pipeline,
|
|
62
79
|
compaction, and hook forwarding cannot drift between surfaces.
|
|
@@ -71,28 +88,53 @@ path through the same `ChatHost` method. Source of truth: `IpcMethods` in
|
|
|
71
88
|
| --- | --- |
|
|
72
89
|
| `chat.send` | Send a prompt; starts a run (errors if already running elsewhere). |
|
|
73
90
|
| `chat.interrupt` | Stop the session's active run. |
|
|
74
|
-
| `chat.steer` | Inject a message into the live run. |
|
|
75
|
-
| `chat.followUp` | Queue a message on the agent to run after the current turn (no steer). |
|
|
91
|
+
| `chat.steer` | Inject a message into the live run. Resolves `false` when no run took it. |
|
|
92
|
+
| `chat.followUp` | Queue a message on the agent to run after the current turn (no steer). Resolves `false` when no run took it. |
|
|
76
93
|
| `chat.pushQueued` | Race-free send: steers a live run, sends otherwise. |
|
|
77
94
|
| `chat.isRunning` | Whether the session has a run in flight. |
|
|
78
95
|
| `chat.rebuildAgent` | Retire the cached agent so the next run rebuilds (fresh MCP/creds/extensions). |
|
|
79
96
|
| `chat.listWork` | List cancellable tool calls / background tasks for the session. |
|
|
80
97
|
| `chat.cancelWork` | Cancel one work item, or `'all'`. |
|
|
81
98
|
| `sessions.list` | List sessions, filtered by project root / archive state. |
|
|
82
|
-
| `sessions.
|
|
83
|
-
| `sessions.
|
|
99
|
+
| `sessions.listMeta` | The sessions list as a window renders it (`SessionMeta`: title, turn / message / run counts, project, cwd, environment, artifact count, main flag), by project root (`null` = untagged) or all. |
|
|
100
|
+
| `sessions.getMeta` | One `SessionMeta` by id, or `null`. |
|
|
101
|
+
| `sessions.get` | The whole session document: the live copy when an agent holds it, else the stored one (stale `running` marker demoted), or `null`. `turns: false` leaves the turns out (read them with `sessions.getTurns`) and names the snapshot's `version`. |
|
|
102
|
+
| `sessions.getTurns` | `{ sessionId, from?, limit? }` → `{ turns, total, version }`: a slice of the same copy's turns (default 200, at most 1000), or `null`. Pages and the `turns: false` head come from one cached snapshot per content version (the session is read once per version, not per page), named by `version`. `loadSessionDocument(request, sessionId)` (`zidane/ipc`) reads a whole document a page at a time with it (halving a page refused as too large), starts over when the version changes mid-read (a few times, then the whole document in one reply), and reads in one reply from an older host. What the GUI exports a server session with. |
|
|
103
|
+
| `sessions.getToolResult` | `{ sessionId, callId, turnId? }` → the full text of one tool result, or `null`: what a clipped tool row shows on expand, without the session crossing the wire. |
|
|
104
|
+
| `sessions.getHeader` | One session's header: its `SessionMeta` row plus `metadata`, `agentId`, `status`, `createdAt`, without turns or runs (the live copy when an agent holds it; from the store's header index when it has one). What a window re-reads on `sessions:changed`. `usage: true` adds `contextTokens` (the last parent turn's context size: the footer gauge) and `costUsd` (summed run costs: the cost pill), computed host-side from the turns and runs, so a client needs no document for them. |
|
|
105
|
+
| `sessions.setExtensionState` | Write (or, `value` omitted, delete) one key of an extension's per-session state in the host's copy (renderer `useSessionState`). |
|
|
106
|
+
| `sessions.create` | Create a session through the shared lifecycle factory (`src/chat/session-factory.ts`): cwd, derived project root, agent, title, model + fallbacks + `effort` / `modelOptions`, `extensionEdit` / `skillEdit` markers, `pinnedSkills`, `environment` (`{ kind, branch?, base? }`; omitted = local), and an `id` (refused when taken or not a plain token). A kind that provisions returns at once with the row's `environment.state` = `provisioning`. |
|
|
107
|
+
| `sessions.delete` | Delete a session: retire its live agent + pending HITL state, tear down the environment it owns, then remove the row and its on-disk artifacts (`ChatHost.deleteSession`). Resolves with `{ environment? }` — `removed: false` + `reason` when the provider kept it. Clients in daemon mode must delete through this, never behind the daemon's back. |
|
|
108
|
+
| `sessions.environmentActions` | The per-session commands the session's environment offers right now (push, open PR). |
|
|
109
|
+
| `sessions.runEnvironmentAction` | Run one in the session's environment (resumed when paused); `{ message, url? }`. |
|
|
110
|
+
| `sessions.workspaceExec` | Run a shell command in the session's workspace: its execution context, rooted inside the environment when the files live there (resumed when paused), else at the session's cwd. Optional `cwd` (same path rules), `env`, `timeoutSeconds` (default 30): `ExecResult`. Backs `!` shell mode for sandboxed sessions and window-side `ctx.workspace` in the TUI's daemon mode. |
|
|
111
|
+
| `sessions.workspaceReadFile` | Read a file from the session's workspace. Paths resolve against the workspace root the way the execution context resolves them (absolute paths as given); containment is the context's own: a sandbox confines to its workspace, the local process context doesn't, except on a confined host (a server), where a path on the host's filesystem must resolve (real path) inside the workspace roots or the session's own environment folder (a worktree), and never into the data dir (`ChatHost.workspace.checkPath`); the same holds for `workspaceWriteFile` / `ListFiles` / `FileSize` and `workspaceExec`'s `cwd`. Backs the files panel and diffs for sandboxed sessions. |
|
|
112
|
+
| `sessions.workspaceWriteFile` | Write a file in the session's workspace, same path rules. |
|
|
113
|
+
| `sessions.workspaceExecStream` | The same command streamed (`!` on a remote host): `{ execId }`, then `workspace:exec:data { execId, stream, chunk }` signals in arrival order and one `workspace:exec:exit { execId, code, signal, truncated?, cancelled? }`, to the requesting connection only (never fanned out or replayed; signals may beat the reply). Runs the session context's `execStream` (a machine's too: `exec.stream*`); a context without one runs the buffered exec and reports it the same way. Bounded per connection: 4 running commands, 64 KiB chunks, 1 MiB of output (then the process group is stopped and the exit says `truncated`). Same `cwd` confinement as `workspaceExec`. A dropped connection kills what it started. |
|
|
114
|
+
| `sessions.workspaceExecCancel` | Kill a streamed command's process group (`SIGTERM`, `SIGKILL` after the grace): `{ cancelled }`, `false` for another connection's exec or one that ended. |
|
|
115
|
+
| `sessions.workspaceListFiles` | List a directory (or, with `options.glob`, match a pattern) in the session's workspace: `ListFilesOptions` semantics, same path rules. |
|
|
116
|
+
| `sessions.workspaceFileSize` | A file's byte size in the session's workspace, or `null`. |
|
|
117
|
+
| `sessions.setLaunchOverrides` | A window's overrides for the session agent: `zidane run --system / --mcp / --context` (`system` replaces the base prompt, extension fragments still append; `mcp` adds servers after the discovered ones; `execution` runs the session on the launch context) and `cwd`, a folder picked mid-session (moves the live agent without a rebuild). In memory and per client: the session runs the most recent connected setter's, and a client clearing (a call with no field) or disconnecting drops only its own, so the previous setter's apply again. |
|
|
118
|
+
| `sessions.retryEnvironment` | Provision a failed (or restart-stranded) environment again, in the background; progress arrives as `sessions:changed`. |
|
|
84
119
|
| `sessions.setTitle` | Rename a session. |
|
|
120
|
+
| `sessions.generateTitle` | Title a session on the host, with the host's providers and credentials (`{ providerKey?, model? }`: the window's pick, used when the host has that provider; else the session's own). Written as a rename; refuses Main. |
|
|
85
121
|
| `sessions.setArchived` | Archive / unarchive a session. |
|
|
86
122
|
| `sessions.setAgentId` | Change a session's agent profile. |
|
|
87
123
|
| `sessions.setEffort` | Change a session's thinking-effort level. |
|
|
88
124
|
| `sessions.setModel` | Set provider/model; `fallbacks` patches the chain (omit = untouched, `[]` = clear). |
|
|
89
125
|
| `sessions.setModelOptions` | Set per-model boolean options. |
|
|
90
|
-
| `sessions.loadEvents` | Canonical transcript replay — the re-convergence source of truth. |
|
|
126
|
+
| `sessions.loadEvents` | Canonical transcript replay — the re-convergence source of truth. Optional paging: `{ before?, limit? }` (turn positions; `limit` default 200, at most 1000) answers `{ events, cursor?, exact: true, start }`, whole runs before `before` (a run: a user message to the turn before the next one): the run the page ends in, and the runs before it while the page stays within `limit` turns, so scroll-back never brings part of a run. A run longer than `5 × limit` turns on its own comes in pieces of that size, each starting on a turn that keeps a tool call with its result; `cursor` is the next page's `before`, absent at the first turn. A page's events are its turns' slice of the WHOLE replay (`exact`), so pages concatenate to exactly one whole load: exchange separators, run receipts, subagent markers and command state as the whole replay has them. `start` is how many events of the whole replay come before the page (absent from older hosts). `loadAllEvents` joins them as they are, and puts the exchange separator back at the joins of an older host's (unflagged) pages; `loadEventsTail` reads from the end only until a predicate is met, returning how many events it left out. Without them, the whole transcript as a plain array (the old shape). Typed callers page through `loadEventsPage(client.request, params)` (`zidane/ipc`), which also reads an older host's whole array as one page. |
|
|
127
|
+
| `sessions.resume` | Reattach without a reload: `{ sessionId, epoch, afterSeq }` → the session's numbered signals after `afterSeq`, or `{ ok: false, reason: 'epoch' \| 'gap' }` (then `loadEvents`). See [Reconnect and resume](#reconnect-and-resume). |
|
|
91
128
|
| `sessions.getTurn` | Fetch one turn. |
|
|
92
129
|
| `sessions.contextBreakdown` | Context-usage breakdown for the context panel. |
|
|
93
130
|
| `sessions.artifacts` | One session's artifacts — see [ARTIFACTS.md](./ARTIFACTS.md). |
|
|
94
131
|
| `sessions.touch` | Announce an out-of-band local `/`/`!` append; fans out `sessions:changed`. |
|
|
95
|
-
| `sessions.
|
|
132
|
+
| `sessions.fork` | Fork at a turn into a new session (`ChatHost.forkSession`); announced as a `sessions:changed` create. |
|
|
133
|
+
| `sessions.deleteTurn` / `sessions.editTurnText` | History edits in the host: refused while the session runs (here or by another process's fresh run marker), one atomic rewrite when the store has `mutateTurns`, cached agent retired, `sessions:changed`. |
|
|
134
|
+
| `sessions.setFolder` | Bind a session to a folder (`cwd` + derived project root), or back to a plain chat with `cwd: ''`. Refused while running and for the Main session. |
|
|
135
|
+
| `sessions.appendTurns` | Turns written outside a run (`!` shell output, a window's `/` built-in output and notices), through the live session when one exists; fans out `sessions:changed`. |
|
|
136
|
+
| `sessions.appendNote` | `ctx.appendNote` outside a run: a display-only note persisted as a `command` turn and broadcast under the same turn id. `false` for an unknown session or no blocks. |
|
|
137
|
+
| `sessions.compact` | Compact a session now (loads its agent when no window has run it). Returns a `CompactionReport` (replaced/dropped turn counts, restored files and skills, summarizer model and usage, post-compact `effectiveTokens`), or `null` when there was nothing to compact. |
|
|
96
138
|
| `sessions.systemMd` | Attach-time backfill of the resolved `SYSTEM.md` override. |
|
|
97
139
|
| `sessions.contextSegments` | Every prompt segment with provenance (Instructions tab). |
|
|
98
140
|
| `sessions.createSystemOverride` | Seed a user-scope `SYSTEM.md`; throws when no override could apply. |
|
|
@@ -102,7 +144,7 @@ path through the same `ChatHost` method. Source of truth: `IpcMethods` in
|
|
|
102
144
|
| `history.searchSession` | Transcript search over one session, against authoritative mid-run-fresh turns. |
|
|
103
145
|
| `history.searchAll` | Cross-session content search; one shared index built in the daemon. |
|
|
104
146
|
| `todos.get` | Current todos snapshot for a session. |
|
|
105
|
-
| `todos.set` | Apply a user todo edit to the live agent's `Session` (what `todowrite` reads). |
|
|
147
|
+
| `todos.set` | Apply a user todo edit to the live agent's `Session` (what `todowrite` reads); with `persistIdle`, to the stored session when no agent is live. |
|
|
106
148
|
| `orchestration.main` | Resolve/create the pinned "main" session id. Gated on the `orchestration` setting. |
|
|
107
149
|
| `orchestration.createWorker` | Create a worker session under a main. |
|
|
108
150
|
| `orchestration.list` | List a main's workers. |
|
|
@@ -111,12 +153,18 @@ path through the same `ChatHost` method. Source of truth: `IpcMethods` in
|
|
|
111
153
|
| `orchestration.steer` | Steer a worker's live run. |
|
|
112
154
|
| `orchestration.cancel` | Cancel a worker's run. |
|
|
113
155
|
| `orchestration.transcript` | Fetch a worker's transcript text. |
|
|
114
|
-
| `commands.list` |
|
|
156
|
+
| `commands.list` | The `/` commands the host runs for a cwd: its extensions' (the registry for that folder there) then its built-ins (`/help`). A TUI session on a server completes and sends these (`useHostCommands`). |
|
|
115
157
|
| `commands.completeArgs` | Argument suggestions for `/name <prefix>`. |
|
|
116
158
|
| `interactions.list` | Pending `present_plan` / `ask_user` requests. |
|
|
117
159
|
| `interactions.respond` | Answer an interaction. |
|
|
118
160
|
| `interactions.cancel` | Cancel an interaction. |
|
|
119
161
|
| `interactions.rehydrate` | Re-queue orphaned interactions from a reopened session. |
|
|
162
|
+
| `terminal.state` | A session's terminals, whether this host can run them, and its environment kind. See [TERMINAL.md](./TERMINAL.md). |
|
|
163
|
+
| `terminal.open` | Open a terminal in the session's environment (or `target: 'local'`). |
|
|
164
|
+
| `terminal.attach` / `terminal.detach` | Start / stop showing a terminal in one view (`clientId`): history plus the offset live output resumes at. Views are detached when their connection drops. |
|
|
165
|
+
| `terminal.write` / `terminal.resize` / `terminal.ack` | Input; size (`claim: true` when the user focuses or types); acknowledge written output to keep it flowing. |
|
|
166
|
+
| `terminal.restart` / `terminal.close` / `terminal.setHidden` | Restart a user terminal; close one (an agent terminal's task is killed); hide one from the agent's tools. |
|
|
167
|
+
| `terminal.registerViewer` / `terminal.unregisterViewer` | This connection can show terminals (the desktop app). Agents get the terminal tools only while a viewer is attached; the registration ends with the connection. |
|
|
120
168
|
| `approvals.resolve` | Resolve a safe-mode approval request. |
|
|
121
169
|
| `approvals.denyAll` | Deny every pending approval (optionally scoped to a session). |
|
|
122
170
|
| `approvals.listPending` | Pending approvals — backfill for a late attacher. |
|
|
@@ -133,15 +181,55 @@ path through the same `ChatHost` method. Source of truth: `IpcMethods` in
|
|
|
133
181
|
| `suggest.resolve` | Resolve inline ghost-text autocomplete daemon-side; latest `seq` per session wins. |
|
|
134
182
|
| `settings.get` | Read host settings. |
|
|
135
183
|
| `settings.set` | Patch host settings; fans out `settings:changed`. |
|
|
184
|
+
| `environments.list` | Every registered environment kind described for a `cwd`: enabled, default, availability + reason, branch suggestion, option fields (secrets as `configured` / `fromEnv` only), and for a machine's kinds `machine: { id, name, status }`. |
|
|
185
|
+
| `environments.verify` | "Test connection" for a kind with its current options: `{ ok, detail }` or `{ ok: false, reason }`. |
|
|
186
|
+
| `environments.update` | One Settings → Environments edit (enable, default, option values, secrets), validated against the kind's schema; settings go through `settings.set`, secrets to the daemon's environment credential store. |
|
|
136
187
|
| `presence.list` | Current presence snapshot — backfill for a freshly opened session list. |
|
|
137
188
|
| `presence.watch` | Tag this connection as attached to a session; detach is implicit on close. |
|
|
189
|
+
| `fs.roots` | Where session folders may be: a server's workspace roots (`confined: true`), or the user's home on a local host (unconfined). |
|
|
190
|
+
| `fs.listDirs` | The folders in `path` (default: the first root) for a picker on the host's machine: real paths inside the roots only (`..` and symlinks out refused), no dotfiles, `isGitRepo` per entry, capped at 2000 (`truncated`). |
|
|
191
|
+
| `auth.status` | The host's provider credentials: per provider, `configured`, `oauth`, `envKey`, and its sources (stored API key, OAuth accounts with labels, env var), which is active. Never a secret. |
|
|
192
|
+
| `auth.setApiKey` | Store an API key as the provider's active credential in the host's `credentials.json`, re-apply the host's auth env, rebuild agents (idle now, busy at run end). OAuth sign-in: `auth.oauth.*`. |
|
|
193
|
+
| `auth.remove` | Remove one stored credential (`account`, a key from `auth.status`) or all of a provider's; a removed key also leaves the host's environment (one it only has from its own environment stays). |
|
|
194
|
+
| `auth.oauth.begin` | OAuth sign-in with the browser on the client and the tokens on the host: `{ provider }` or `{ mcpServer, cwd?, kind?, redirectUri }` (an MCP server of `cwd`'s catalog; `redirectUri` a `http://127.0.0.1:<port>/…` loopback the client listens on). The host runs the login (PKCE verifier, `state`, discovery and dynamic registration stay there) and answers `{ flowId, url, mode, redirectUri?, userCode?, instructions?, expiresAt }`: `loopback` (the client listens at `redirectUri`: a provider's fixed port, e.g. Anthropic's `http://localhost:53692/callback`; a busy port falls back to pasting), `browser` (open `url`, maybe paste a code), `device` (show `userCode`). zidane's providers bind no listener on the host (`callbackServer: false`); a provider that would (it ignores the flag) is refused. A question the login asks before its URL comes back as `{ mode: 'step', url: '', step }` (`HostOAuthStep`: a `select` with options, or a `prompt`): answer it with `auth.oauth.respond`. The flow is bound to this connection, at most 4 open, expires after 10 minutes (`oauthFlowTtlMs`). |
|
|
195
|
+
| `auth.oauth.complete` | `{ flowId, code?, state?, input? }`: the code from the loopback (its `state` must be the flow's, else the flow ends refused) or what the user pasted (a callback URL, `code=…&state=…`, Anthropic's `code#state`; a `state` in it is checked too), delivered once; then waits for the token exchange. Without a code it only waits (device and polling flows). Another connection's flow is refused. Result: `{ provider?: HostProviderAuth }` or `{ mcpServer? }`, never a token, or `{ step }` when the login asks something after its URL (answer with `auth.oauth.respond`, then call `complete` again); tokens go to the host's `credentials.json` / `mcp-credentials.json` and agents rebuild. |
|
|
196
|
+
| `auth.oauth.respond` | `{ flowId, stepId, value? }`: answer the flow's pending step (a `select`'s option id, none to cancel; a `prompt`'s text). Answers with where the flow stands (`HostOAuthBegin`: its URL, or the next step). Additive. |
|
|
197
|
+
| `auth.oauth.cancel` | Abandon a flow of this connection: `{ cancelled }`. |
|
|
198
|
+
| `catalog.install` / `catalog.remove` / `catalog.update` | Install a skill or an extension in the host user's scope from a GitHub (`owner/repo[#ref][:dir]`, GitHub / skills.sh URLs) or npm spec (paths refused; `scope` only `'user'`), remove a user-scope one by name, update an extension from its source. Gated by `remoteInstalls` (a zidane-server's `allowRemoteInstalls`; refused on any other host), every attempt audited with the token, bounded to 5 minutes; progress lines come back with the result. Extensions join an explicit `enabledExtensions` list; removals leave the enabled lists. Then agents rebuild and every window gets `catalog:changed`. |
|
|
199
|
+
| `mcp.upsert` / `mcp.remove` | Add or replace / remove an MCP server in the host user's `mcps.json` (`{ name, created }` / `{ removed }`); validated (`command` or http(s) `url`, string `env` / `headers`), its secrets stored there and never echoed (`catalog.mcps` redacts them). Same gate, audit and signal as `catalog.install`. |
|
|
200
|
+
| `catalog.skills` / `catalog.mcps` / `catalog.extensions` / `catalog.plugins` | What the host discovered for the settings tabs, with the same scans and gates its agents use, each entry with `enabled` (its settings' allowlist; unset = all) and `toggleable` (its own allowlist governs it: not a plugin's or extension's contribution, not broken). `cwd` adds that folder's project scope (inside the workspace roots on a server), else the host user's scope. Skills carry origin (`disk`, `plugin`, `extension`); MCP servers their transport, command, args, URL, file, with `<redacted>` in place of: every `env` / `headers` value; in the URL and in every URL-shaped argument, the userinfo, query values, fragment values (`#k=v`, or a token-shaped fragment) and token-shaped path segments; the value of a secret-looking flag (`--token x`, `--api-key=x`), of a header flag (`-H` / `--header` / `--header=`: `Name: <redacted>`) and of a secret-looking header anywhere (`Authorization: …`); the value of a secret-looking `KEY=VALUE` (`-e GITHUB_TOKEN=…`, `env API_KEY=…`, `--env=…`); known token shapes anywhere in an argument (`sk-`, `sk_live_`, `ghp_` / `gho_` / `ghu_` / `ghs_` / `ghr_`, `github_pat_`, `xox?-`, `AKIA` / `ASIA`, `glpat-`, `zds_`, `AIza`); and a whole argument or path segment that looks random (20+ characters: hex / UUID-like, or mixed case and digits with high entropy). File paths, package names and dash-joined words are kept. The rules are `src/ipc/daemon/redact.ts`; they are a best effort over free-form arguments, not a guarantee. Plus the config files that failed to parse; extensions their source, version, error, capabilities; plugins their origin, contributions, rejection. Toggles write the enabled lists with `settings.set`; installs, removal and MCP sign-in stay on the host's machine (`src/ipc/daemon/handlers/catalogs.ts`, `catalog-entries.ts`). |
|
|
201
|
+
| `models.list` | The host's credentialed providers (`detectAuth` on its data dir) and their models (`ModelInfo`), for the model picker of a session on that host; `cwd` adds what its extensions contribute there. |
|
|
202
|
+
| `machines.list` / `machines.forget` | The machines lending this host their execution (online or not), and forgetting one: it disconnects, its `local@` / `worktree@<name>` kinds stop resolving, and the token it registered with can't register it again (a new token can). `machine:status` signals each change. See `docs/MACHINES.md`. |
|
|
203
|
+
| `machines.listDirs` | A machine's folders for a picker (`path`, default its first root), listed on the machine (`fs.list`, confined there); never waits for an offline machine. |
|
|
204
|
+
| `machines.share.start` / `machines.share.stop` / `machines.useServer` / `machines.links.status` | This (local) host's machine links, behind remote hosts (refused while off; always refused on a zidane-server): share this computer with a saved server, use a saved server as a machine; `machine:links` signals each change. See `docs/MACHINES.md` § Client UI. |
|
|
205
|
+
| `machine.attach` / `machine.call` / `machine.detach` | The reverse direction, on a server with `allowAsMachine`: a client's host asks the server to lend itself over this connection, then sends each machine request; notifications come back as `machine:notification` signals to that connection only. The server keys its executor by this connection's token and the `hostId` (another token never reaches it; 4 per token); a full one answers `MachineErrorCode.Busy` (retry). Refused while off. See `docs/MACHINES.md` § Reverse direction. |
|
|
206
|
+
| `tokens.createMachineToken` | A zidane-server mints a `machine` token for the asking client (one-step "share this computer"), at most 16 live per client token, audited with it; other hosts refuse. |
|
|
207
|
+
| `sync.status` / `sync.claim` / `sync.push` / `sync.reset` / `sync.release` | Settings sync from a client's computer to a zidane-server ([SERVER.md § Settings sync](./SERVER.md#settings-sync)); other hosts refuse, and `ipc.hello` says `sync: true` on a server that takes it. `status`: the record (source device, categories, opted-in keys, synced fields, overrides; no values). `claim { deviceId, deviceName, takeover?, categories?, keys? }`: `{ claimed: false, status }` when another device is the source and `takeover` is not set. `push { deviceId, category, payload }`: one category's snapshot (`SyncPayloads`), source only, applied through the host's own setters (`settings.set` for syncable host-scoped keys, never window-only ones; the user-scope `SYSTEM.md` and skills as files; `mcp.upsert` / `remove`; `catalog.install`, behind `allowRemoteInstalls`; `auth.setApiKey` for opted-in providers), skipping overrides: `{ applied, unchanged, skipped: [{ field, reason }], warnings }`. `reset { field }`: drop an override. `release { deviceId }`: the source stops (values stay). A window's `settings.set` / `mcp.*` / `catalog.remove` / `update` / `auth.setApiKey` / `remove` on a synced field marks it an override. Each changes the record → `sync:changed` to every window; each call is audited. |
|
|
138
208
|
| `daemon.info` | Handshake / diagnostics info. |
|
|
139
209
|
| `daemon.shutdown` | Graceful drain: deny pending approvals, abort live runs, close the socket. |
|
|
140
210
|
|
|
141
|
-
All methods above `IPC_PROTOCOL_VERSION` 9 are additive: an older daemon
|
|
211
|
+
Without a version bump (additive; an older daemon answers "method not found", which clients treat as unsupported): `catalog.*` and `models.list` (the TUI keeps its enabled-list panels and this window's providers), `sessions.loadEvents` paging and `exact`, `sessions.getHeader` `usage` (an older host ignores the fields), `sessions.getTurns`, `sessions.getToolResult` and `sessions.get` `turns: false` (a client reads the whole document from an older host), `sessions.getHeader` (the TUI falls back to `sessions.get`), `fs.roots`, `fs.listDirs`, `auth.status`, `auth.setApiKey`, `auth.remove`, `sessions.resume`, `signalEpoch` on `ipc.hello` and the `$seq` signal envelope field, the `$/ping` heartbeat request (answered outside the method table, before the handshake too), host identity (`hostId` / `hostKind` / `hostName` on `ipc.hello` and `daemon.info`, `hostId` on `SessionRow`; `hostRefOf(hello)` is `null` against an older host), `sessions.setLaunchOverrides`, `sessions.workspaceListFiles`, `sessions.workspaceFileSize`, `cwd` / `env` on `sessions.workspaceExec`, workspace calls for local sessions, the `sessions.compact` report, `sessions.appendNote`, `sessions.workspaceExecStream` / `workspaceExecCancel` and the `workspace:exec:*` signals (a client falls back to the buffered `workspaceExec`), `auth.oauth.*`, `catalog.install` / `remove` / `update` and `mcp.upsert` / `remove` (offered when `ipc.hello` says `remoteInstalls`), the `catalog:changed` signal, `cwd` / `execution` on `sessions.setLaunchOverrides`, and the `mcp:status` and `mcp:settled` signals. Version 13 added the `terminal.*` methods and the `terminal:*` signals. Version 12 added `sessions.workspaceExec`, `sessions.workspaceReadFile` and `sessions.workspaceWriteFile`. Version 11 added `environments.verify`, `sessions.environmentActions` and `sessions.runEnvironmentAction`, and `suspended` on row environments. Version 10 added `sessions.retryEnvironment`, `environments.list`, `environments.update`, `environment` on `sessions.create` / `SessionRow`, and the `sessions.delete` result. All methods above `IPC_PROTOCOL_VERSION` 9 are additive: an older daemon
|
|
142
212
|
answers "method not found" and clients degrade (local compute, or a
|
|
143
213
|
store-level fallback) instead of failing. `optional()` in
|
|
144
|
-
`src/tui/runtime/
|
|
214
|
+
`src/tui/runtime/host-session.ts` is the TUI's wrapper for that pattern.
|
|
215
|
+
|
|
216
|
+
### Errors
|
|
217
|
+
|
|
218
|
+
A failed request rejects with a `JsonRpcRemoteError` carrying one of these
|
|
219
|
+
codes (`AcpErrorCode` in `src/acp/types.ts`, `IpcErrorCode` in
|
|
220
|
+
`src/ipc/protocol.ts`). Clients branch only on the codes that call for a
|
|
221
|
+
different action (`VersionMismatch`, `ResponseTooLarge`, method not found);
|
|
222
|
+
the rest are a plain remote error with a message.
|
|
223
|
+
|
|
224
|
+
| Code | Name | When |
|
|
225
|
+
| --- | --- | --- |
|
|
226
|
+
| -32601 | method not found | The host lacks the method (an older host: degrade). |
|
|
227
|
+
| -32602 | invalid params | The params are malformed (`InvalidParamsError`: not an object, a bad session id, a wrong type); `message` names the field. |
|
|
228
|
+
| -32010 | `VersionMismatch` | `ipc.hello` across protocol versions or builds. |
|
|
229
|
+
| -32011 | `NotHandshaken` | A method before `ipc.hello` (`daemon.info` / `daemon.shutdown` excepted). |
|
|
230
|
+
| -32012 | `RateLimited` | A network transport's per-connection request budget is spent; retry later. |
|
|
231
|
+
| -32013 | `ResponseTooLarge` | The reply would exceed the transport's message size; ask for less. |
|
|
232
|
+
| -32014 | `HandlerFailed` | The handler threw: the host refused or failed the call (`message` says why). Hosts before it answered -32000, ACP's `AuthenticationRequired`; clients never branched on either. |
|
|
145
233
|
|
|
146
234
|
## Signals and convergence
|
|
147
235
|
|
|
@@ -153,7 +241,7 @@ Host → every connected client, one push channel. Source of truth:
|
|
|
153
241
|
| `run:start` | A run began. |
|
|
154
242
|
| `run:end` | A run ended (`ok`, optional `error`). |
|
|
155
243
|
| `turn:end` | A turn finished; carries `TurnUsage` (footer context gauge + live cost). |
|
|
156
|
-
| `event` | Canonical `StreamEvent`; `echo: true` marks the sender's own send/steer echo. |
|
|
244
|
+
| `event` | Canonical `StreamEvent`; `echo: true` marks the sender's own send/steer echo. A failed call's `tool-result` carries `isError: true` (live and on replay); there is no separate `error` row for it. |
|
|
157
245
|
| `compacting:start` | Manual or auto-compaction began. |
|
|
158
246
|
| `compacting:progress` | Live summary-token estimate during an in-flight compaction. |
|
|
159
247
|
| `stream:progress` | Live counters for the streaming parent turn (`outputTokens`, `estimated`, `elapsedMs`, optional `estCostUsd`), throttled ~4/s plus one final tick before the turn settles. Clients fold `estCostUsd` into the session cost until the matching `turn:end`. |
|
|
@@ -166,6 +254,10 @@ Host → every connected client, one push channel. Source of truth:
|
|
|
166
254
|
| `artifact:changed` | An artifact was deleted / refreshed — see [ARTIFACTS.md](./ARTIFACTS.md). |
|
|
167
255
|
| `interaction:request` | A `present_plan` / `ask_user` call is pending. |
|
|
168
256
|
| `interaction:resolved` | An interaction left the pending queue. |
|
|
257
|
+
| `terminal:changed` | A session's terminal list changed (full list). |
|
|
258
|
+
| `terminal:output` / `terminal:reset` | Output batch starting at `offset`, or a view reset from history; sent only to connections that attached that terminal. |
|
|
259
|
+
| `workspace:exec:data` / `workspace:exec:exit` | A streamed workspace command's output and its end (`sessions.workspaceExecStream`); sent only to the connection that started it. |
|
|
260
|
+
| `catalog:changed` | `{ catalog }`: a client installed, updated or removed something on the host (`catalog.install`, `mcp.upsert`, …); windows re-read that catalog. |
|
|
169
261
|
| `approval:request` | A safe-mode approval is pending. |
|
|
170
262
|
| `approval:resolved` | An approval left the pending queue. |
|
|
171
263
|
| `sessions:changed` | Session created / updated / deleted. |
|
|
@@ -176,8 +268,10 @@ Host → every connected client, one push channel. Source of truth:
|
|
|
176
268
|
| `suggest:changed` | Another window's ghost-text autocomplete resolved (origin excluded). |
|
|
177
269
|
| `skills:snapshot` | The daemon-side agent's active-skill set changed. |
|
|
178
270
|
| `mcp:auth` | A daemon-side MCP server needs (re-)authentication. |
|
|
271
|
+
| `mcp:status` | A daemon-side MCP server connected (`status: 'connected'`) or failed to bootstrap (`status: 'failed'`, with `transport`, `error`, optional `code`). |
|
|
272
|
+
| `mcp:settled` | The session agent's MCP bootstrap finished (after every server's `mcp:status` / `mcp:auth`); a window's deadline for its one-shot auto-login. |
|
|
179
273
|
| `stream:retry` | Provider retry in progress; `source` is `'parent'` or a retrying child's id. |
|
|
180
|
-
| `stream:retry:cleared` | A retried stream reached a terminal state
|
|
274
|
+
| `stream:retry:cleared` | A retried stream for `source` resumed streaming or reached a terminal state. |
|
|
181
275
|
| `system-md` | Which `SYSTEM.md` override (if any) composed this session's prompt. |
|
|
182
276
|
| `cache:break` | A cache-prefix break was detected for the session's last request. |
|
|
183
277
|
| `presence:changed` | Full presence snapshot (attached windows, running, attention). |
|
|
@@ -186,16 +280,19 @@ Host → every connected client, one push channel. Source of truth:
|
|
|
186
280
|
| `queue:changed` | The shared type-ahead queue changed — broadcast to every peer, origin included. |
|
|
187
281
|
| `extension:signal` | Extension-bus message (`ctx.bus.emit`), private per `extensionId`, 64 KiB cap. |
|
|
188
282
|
|
|
283
|
+
**Numbering.** Every signal with a `sessionId` that goes to every peer (not the origin-excluded drafts / search / suggestions, not terminal output) carries its number in its session as an extra `$seq` field on the `ipc.signal` params: from 1, gap-free within one host process (`HelloResult.signalEpoch`). Clients predating it ignore the field; `IpcClient` strips it and hands it to listeners as `onSignal((signal, meta) => ...)`, `meta.seq`.
|
|
284
|
+
|
|
189
285
|
`event { kind: 'session-event' }` is not a separate signal: a worker's
|
|
190
286
|
outcome to its main session rides the existing `event` kind as a
|
|
191
287
|
`StreamEvent` variant, clickable in both clients.
|
|
192
288
|
|
|
193
289
|
Cross-cutting contracts worth knowing before you touch a consumer:
|
|
194
290
|
|
|
195
|
-
- **Re-convergence guarantee**: at every run end and on reconnect
|
|
196
|
-
replace their transcript via `sessions.loadEvents`
|
|
197
|
-
a disconnect, attach gap, or daemon respawn
|
|
198
|
-
boundary.
|
|
291
|
+
- **Re-convergence guarantee**: at every run end, and on a reconnect that
|
|
292
|
+
cannot resume, windows replace their transcript via `sessions.loadEvents`
|
|
293
|
+
— a signal missed to a disconnect, attach gap, or daemon respawn
|
|
294
|
+
reconciles at the next run boundary. A reconnect to the same host process
|
|
295
|
+
replays exactly what was missed instead ([Reconnect and resume](#reconnect-and-resume)).
|
|
199
296
|
- **User messages are part of the stream**: the host broadcasts
|
|
200
297
|
`user-prompt` (inside `event`) for every send/steer, before `run:start`,
|
|
201
298
|
tagging the sender's own copy `echo: true`. A client that appends its
|
|
@@ -214,12 +311,12 @@ Cross-cutting contracts worth knowing before you touch a consumer:
|
|
|
214
311
|
(any window) and sends otherwise; the host drains FIFO at run end, so a
|
|
215
312
|
queued message still runs after the queuing window closes.
|
|
216
313
|
|
|
217
|
-
> **Adding a signal.** Both `src/tui/runtime/
|
|
314
|
+
> **Adding a signal.** Both `src/tui/runtime/host-session.ts` and
|
|
218
315
|
> `gui/src/main/chat/host.ts` end their dispatch with
|
|
219
316
|
> `signal satisfies never`, so a new `HostSignal` kind fails to compile
|
|
220
317
|
> until routed explicitly. That catches an unrouted *kind*, not an
|
|
221
318
|
> unconsumed *callback* — `onModelFallback`/`onRetry`/`onRetryCleared` are
|
|
222
|
-
> therefore required (not optional) on `
|
|
319
|
+
> therefore required (not optional) on `HostSessionCallbacks`: they were
|
|
223
320
|
> once dispatched while the TUI silently never supplied them, quietly
|
|
224
321
|
> dropping the fallback notice and the retry indicator.
|
|
225
322
|
|
|
@@ -230,9 +327,8 @@ loser gets an explicit error. `chat.pushQueued` is the race-free
|
|
|
230
327
|
spelling — it steers the live run when one is active and sends otherwise.
|
|
231
328
|
|
|
232
329
|
> **Cross-process run guard.** The canonical loop persists session-level
|
|
233
|
-
> `status: 'running'`, so the daemon refuses a session a local
|
|
234
|
-
>
|
|
235
|
-
> running — each with an explicit error. A 5-minute freshness bound
|
|
330
|
+
> `status: 'running'`, so the daemon refuses a session a local-mode window's
|
|
331
|
+
> in-process host is running, and the reverse — each with an explicit error. A 5-minute freshness bound
|
|
236
332
|
> against `updatedAt` means a crashed process's leftover marker never
|
|
237
333
|
> blocks new runs; a live run refreshes the marker every 60s.
|
|
238
334
|
|
|
@@ -262,11 +358,12 @@ is `undefined`.
|
|
|
262
358
|
(honors `ZIDANE_STORAGE_DIR` / `ZIDANE_PREFIX`), override with
|
|
263
359
|
`ZIDANE_DAEMON_SOCKET`. Dev builds (`ZIDANE_DEV=1`) use `dev.sock` in the
|
|
264
360
|
same dir so a checkout never attaches to or drains the installed daemon.
|
|
265
|
-
- **`useDaemon` setting** (Settings → agent, experimental):
|
|
266
|
-
(default) vs. the shared daemon. On, two terminals on
|
|
267
|
-
the live stream — either can steer/interrupt/answer
|
|
268
|
-
closing a window leaves the run executing.
|
|
269
|
-
`
|
|
361
|
+
- **`useDaemon` setting** (Settings → agent, experimental): the window's
|
|
362
|
+
own in-process host (default) vs. the shared daemon. On, two terminals on
|
|
363
|
+
one session share the live stream — either can steer/interrupt/answer
|
|
364
|
+
approvals, and closing a window leaves the run executing. Both go through
|
|
365
|
+
`connectHostSession` in `src/tui/runtime/host-session.ts`; only the transport
|
|
366
|
+
differs (`src/tui/runtime/transport.ts`).
|
|
270
367
|
- Start explicitly: `zidane daemon [--socket <path>] [--verbose]` or the
|
|
271
368
|
published `zidane-daemon` bin (`--idle-exit <ms>`, default 30 min, `0`
|
|
272
369
|
disables). `connectOrSpawnDaemon()` / `zidane attach` spawn one
|
|
@@ -290,28 +387,200 @@ is `undefined`.
|
|
|
290
387
|
may ignore `codeVersion`, so a recognizable `appVersion` is the fallback.
|
|
291
388
|
A successful legacy hello reporting an older release is refused without
|
|
292
389
|
shutting down its active runs. Unknown versions cannot establish release age.
|
|
390
|
+
- **A network server** (`hostKind: 'server'`) keeps only the protocol fence:
|
|
391
|
+
a newer-release or newer-build client attaches and degrades (`optional()`)
|
|
392
|
+
like an older one, the server never retires for it, and no client ever
|
|
393
|
+
drains it (`client-core` refuses to, whatever its `drain` option). A
|
|
394
|
+
protocol mismatch is refused both ways with both versions and the side to
|
|
395
|
+
upgrade (`Protocol version mismatch: the server speaks v13, the client v14.
|
|
396
|
+
Upgrade the server.`; the client words it from its side).
|
|
397
|
+
|
|
398
|
+
## Remote server (zidane-server)
|
|
399
|
+
|
|
400
|
+
Running one (Docker, configuration, proxies, tokens, operations):
|
|
401
|
+
[SERVER.md](./SERVER.md). This section is the internals.
|
|
402
|
+
|
|
403
|
+
`startZidaneServer` (`src/server/server.ts`) serves ONE host over two
|
|
404
|
+
front-ends: the WebSocket transport (`src/ipc/transports/ws-server.ts`,
|
|
405
|
+
Bun only) for remote windows, and a unix socket (`startIpcDaemon`) for
|
|
406
|
+
local tools on the server's machine. Both attach to the same dispatcher,
|
|
407
|
+
built by `createServerHostDispatcher` (`ws-server.ts`, around
|
|
408
|
+
`createHostDispatcher`) and handed to each as the `dispatcher` option of
|
|
409
|
+
`startWsServer` / `startIpcDaemon` (a transport given a dispatcher serves
|
|
410
|
+
it and leaves draining it to its owner), so a session created over one is
|
|
411
|
+
live on the other. The host is a `server` (`hostKind`): multi-process (`env.multiProcess`
|
|
412
|
+
forced on: the run guard engages, runs outlive their windows), no idle
|
|
413
|
+
exit, never drained or retired by a client. The socket is unauthenticated
|
|
414
|
+
like the daemon's (0600 socket in the data dir) and not audited.
|
|
415
|
+
|
|
416
|
+
- **Run it**: `zidane server start` (`src/server/cli.ts`; standalone
|
|
417
|
+
`bun src/server/main.ts start`, compiled into the `zidane-server`
|
|
418
|
+
binary by `scripts/build-server.ts`). `zidane server health` asks
|
|
419
|
+
`daemon.info` over the unix socket; `health --http` GETs `/healthz` on
|
|
420
|
+
the configured port (`healthUrl`: loopback for a wildcard host; the
|
|
421
|
+
image's healthcheck). Tokens: `zidane server token create
|
|
422
|
+
--name <device>` (prints the token once, on stdout), `token list` (id,
|
|
423
|
+
name, scope, created, last used, revoked; never secrets), `token revoke
|
|
424
|
+
<id|name>`, `token generate` (a fresh token, stored nowhere). The token
|
|
425
|
+
commands edit the data dir's files directly, so they work while the
|
|
426
|
+
server runs. At start, `ZIDANE_SERVER_BOOTSTRAP_TOKEN` (or
|
|
427
|
+
`ZIDANE_SERVER_BOOTSTRAP_TOKEN_FILE`, a file holding it; both set is a
|
|
428
|
+
boot error; well-formed or a boot error) is adopted as the live client
|
|
429
|
+
token `bootstrap` (`TokenStore.adopt`: created, unchanged, rotated with
|
|
430
|
+
the old one revoked, or ignored with a warning when that value was
|
|
431
|
+
revoked); both variables are removed from the process environment
|
|
432
|
+
(children spawned later don't inherit them; the container's own
|
|
433
|
+
configuration and `/proc/1/environ` still hold an inline value). In a
|
|
434
|
+
Linux container, a data dir on no mount, an anonymous volume or a tmpfs
|
|
435
|
+
gets a start-up warning (`src/server/storage-check.ts`, from
|
|
436
|
+
`/proc/self/mountinfo`).
|
|
437
|
+
- **Configuration** (`src/server/config.ts`): flags > `ZIDANE_SERVER_*` env
|
|
438
|
+
(empty values are unset; the generic `PORT` is never read)
|
|
439
|
+
> `<data dir>/server.json` > defaults, validated at boot (errors name the
|
|
440
|
+
flag, variable or file key; unknown file keys and wrongly typed JSON
|
|
441
|
+
values are errors). Keys: `host` (127.0.0.1), `port` (4280), `tlsCert` /
|
|
442
|
+
`tlsKey` (relative to the data dir in the file; with `behindProxy` a
|
|
443
|
+
boot error), `behindProxy`, `drainTimeoutMs` (8000; env
|
|
444
|
+
`ZIDANE_SERVER_DRAIN_TIMEOUT_MS`), `name`
|
|
445
|
+
(the `hostName` clients see), `heartbeatMs`, `maxMessageBytes` (32 MiB),
|
|
446
|
+
`rateLimit` `{ perSecond: 50, burst: 200 }` (env `_RATE_LIMIT` /
|
|
447
|
+
`_RATE_BURST`; 0 disables), `allowedOrigins` (none), `socket` (default
|
|
448
|
+
`<data dir>/server.sock`, `off` for none), `workspaceRoots` (default
|
|
449
|
+
`[<data dir>/workspaces]`; flag `--workspace-root`, repeatable; env
|
|
450
|
+
`ZIDANE_SERVER_WORKSPACE_ROOTS`, path-delimited, absolute; relative
|
|
451
|
+
entries in the file resolve against the data dir; missing ones are
|
|
452
|
+
created at start and handed to the host as `DaemonEnvironment.workspaceRoots`). The data dir is the zidane
|
|
453
|
+
user dir (sessions, settings, `host-id`, and the server's files);
|
|
454
|
+
`--data-dir` / `ZIDANE_SERVER_DATA_DIR` set the storage root as
|
|
455
|
+
`ZIDANE_STORAGE_DIR` does, so `--data-dir /data` keeps everything in
|
|
456
|
+
`/data/.zidane`.
|
|
457
|
+
- **Exposure**: a non-loopback `host` without TLS is refused at boot
|
|
458
|
+
unless `behindProxy` (the error lists the three ways out). With
|
|
459
|
+
`tlsCert` / `tlsKey` the server speaks `wss://`. Behind a proxy, every
|
|
460
|
+
upgrade must carry `X-Forwarded-Proto` with only `https` entries, from
|
|
461
|
+
any peer, loopback included (local tools use the unix socket); with
|
|
462
|
+
`trustedProxies` (IPs / CIDRs, or `private` for every private range:
|
|
463
|
+
`PRIVATE_NETWORKS`) any other peer is a 403; without them every
|
|
464
|
+
peer's forwarded headers are believed, and the server warns at start
|
|
465
|
+
(`startupWarnings`: stderr, and `warnings` on the audit's `server.start`).
|
|
466
|
+
A 403 carries its reason in `X-Zidane-Refusal` (`origin`,
|
|
467
|
+
`insecure-transport`, `untrusted-proxy`, `scope`). An upgrade
|
|
468
|
+
carrying an `Origin` not in `allowedOrigins` is a 403 (browsers send
|
|
469
|
+
one, CLI clients don't; the default list is empty). These checks
|
|
470
|
+
(`src/server/security.ts`, `src/server/upgrade-guard.ts`) run before the
|
|
471
|
+
token. Behind a proxy the client address is the last `X-Forwarded-For`
|
|
472
|
+
entry (skipping trusted proxies from the right), never the first.
|
|
473
|
+
`GET` / `HEAD /healthz` (`SERVER_HEALTH_PATH`, the transport's
|
|
474
|
+
`healthPath` option) is answered before all of these: `200 ok`, `503`
|
|
475
|
+
while closing, no token, no rate limit, no audit, no host details.
|
|
476
|
+
- **Limits**: a frame over `maxMessageBytes` closes the connection with
|
|
477
|
+
1009; a reply over it goes out as an `IpcErrorCode.ResponseTooLarge`
|
|
478
|
+
error and the connection stays (clients page what can be big:
|
|
479
|
+
`loadAllEvents`, `loadSessionDocument`). Each connection has a token bucket over its incoming messages;
|
|
480
|
+
past it a request gets JSON-RPC error `IpcErrorCode.RateLimited`
|
|
481
|
+
(-32012) and the connection lives on (notifications over budget are
|
|
482
|
+
dropped; a message over 4 KiB is not parsed, its id is read from its
|
|
483
|
+
head). Upgrades are rate-limited per client address
|
|
484
|
+
(`upgradeRateLimit`, 429), and open connections capped per token
|
|
485
|
+
(`maxConnectionsPerToken`) and per client address
|
|
486
|
+
(`maxConnectionsPerAddress`), 429. A token revoked between its upgrade
|
|
487
|
+
and the socket opening is closed with 1008.
|
|
488
|
+
- **Audit log** (`src/server/audit.ts`): append-only JSONL, 0600, rotated
|
|
489
|
+
to `.1` past 5 MB, written asynchronously in order and flushed at
|
|
490
|
+
shutdown. `<data dir>/server-audit.jsonl`: `connect` (token id and
|
|
491
|
+
name, remote address, client `appVersion` from its hello), `disconnect`
|
|
492
|
+
(and why: client close, heartbeat, slow consumer, oversized frame, token
|
|
493
|
+
revoked, shutdown), `token.create` / `token.revoke` (written by the
|
|
494
|
+
CLI, and at start for the bootstrap token), `server.start` /
|
|
495
|
+
`server.stop`.
|
|
496
|
+
`server-audit.failures.jsonl`: `auth.failure` (address, `missing` |
|
|
497
|
+
`invalid` | `scope` | `origin` | `insecure-transport` |
|
|
498
|
+
`untrusted-proxy` | `rate-limited` | `too-many-connections`, the
|
|
499
|
+
forwarded client behind a proxy), the first per address and minute;
|
|
500
|
+
the rest as one `auth.failure.summary` (count by reason). Never token
|
|
501
|
+
material.
|
|
502
|
+
- **Shutdown** (SIGINT / SIGTERM, or `close()`): stop accepting (upgrades
|
|
503
|
+
get 503, the socket stops listening), close every connection on both
|
|
504
|
+
front-ends (clients' `onClose` fires), drain the host within
|
|
505
|
+
`drainTimeoutMs` (8 s by default, under Docker's 10 s stop timeout),
|
|
506
|
+
exit 0. A second signal at least a second
|
|
507
|
+
after the first exits at once (one Ctrl-C delivered twice does not).
|
|
508
|
+
|
|
509
|
+
- **Device tokens** (`src/server/tokens.ts`): `zds_` + 32 random bytes
|
|
510
|
+
base64url, handed out once by `create(name, scope)`. The store keeps only
|
|
511
|
+
a SHA-256 (constant-time compare) in `<userDir>/server-tokens.json`, mode
|
|
512
|
+
0600, written atomically (`writeJsonFileAtomic`): id, name, scope,
|
|
513
|
+
`createdAt`, `revokedAt`. `lastUsedAt` lives apart in
|
|
514
|
+
`server-tokens.usage.json` (written at most once a minute per token,
|
|
515
|
+
merged with what is on disk), so the server never rewrites the token
|
|
516
|
+
file and can't undo another process's revocation. `list()` never
|
|
517
|
+
returns hashes; `revoke(idOrName)`; `verify(token)` refuses revoked ones;
|
|
518
|
+
`adopt(name, token, scope)` stores a value chosen elsewhere (the
|
|
519
|
+
bootstrap token). Another process writing the file (a CLI) is picked up
|
|
520
|
+
on the next verify. An unreadable or corrupt file keeps the last good
|
|
521
|
+
tokens in force (never "no tokens").
|
|
522
|
+
- **Auth on every upgrade**: no token, an unknown or a revoked one is the
|
|
523
|
+
same plain HTTP 401, before any JSON-RPC (no echo, no telling them apart).
|
|
524
|
+
`client` tokens open client connections; `machine` tokens open a machine
|
|
525
|
+
connection ([MACHINES.md](./MACHINES.md)): never a dispatcher peer (no
|
|
526
|
+
signals), only `ipc.hello`, `$/ping` and `machine.register`, then the
|
|
527
|
+
host sends it execution requests (`src/ipc/daemon/machine-gateway.ts`).
|
|
528
|
+
A client calling `machine.*` gets `MachineErrorCode.MethodNotAllowed`. Revoking a token (here, or by another process: the server watches the
|
|
529
|
+
file and re-reads it every 2 s) drops the connections it opened (close
|
|
530
|
+
1008); the server tracks connection → token id. Logs name token id and
|
|
531
|
+
name, never the token. `auth: { disabled: true }` is refused off loopback.
|
|
532
|
+
- **Heartbeat** (both ends, default 20 s, `heartbeatMs`): the server sends
|
|
533
|
+
WS ping frames, which every WebSocket client answers on its own; the
|
|
534
|
+
client sends a `$/ping` JSON-RPC request (a browser's WebSocket cannot send
|
|
535
|
+
ping frames), which every host answers. Two ticks in a row without an
|
|
536
|
+
answer (a pong, or for the server any message) and the peer is dead: the
|
|
537
|
+
client aborts (its `onClose` fires, its owner reconnects), the server
|
|
538
|
+
terminates the connection and removes the peer (presence follows).
|
|
539
|
+
- **Slow consumers** (`src/ipc/transports/send-budget.ts`, the socket's
|
|
540
|
+
too): a peer with more than 8 MiB (`maxBufferedBytes`) of pushed signals
|
|
541
|
+
unsent, or one Bun drops a frame for, is terminated; it reconnects and
|
|
542
|
+
resumes. Reply bytes it asked for don't count toward the bound (a big
|
|
543
|
+
page followed by a signal is not a slow consumer); the whole buffer has
|
|
544
|
+
a hard cap of `maxBufferedBytes + 2 × maxMessageBytes`.
|
|
545
|
+
|
|
546
|
+
## Reconnect and resume
|
|
547
|
+
|
|
548
|
+
The host numbers session signals (see [Signals](#signals-and-convergence)) and
|
|
549
|
+
keeps the last ones per session (`src/ipc/daemon/signal-log.ts`: 2000
|
|
550
|
+
signals or 4 MiB per session, 32 MiB across sessions, least recently written
|
|
551
|
+
session evicted first; numbering survives eviction). The TUI's session
|
|
552
|
+
controller (`connectHostSession`, `src/tui/runtime/host-session-resume.ts`)
|
|
553
|
+
keeps a resume cursor per activation (session, epoch, last applied number).
|
|
554
|
+
On reattach to the same host process it calls `sessions.resume`, holding the
|
|
555
|
+
session's live signals meanwhile; it applies the replay, then the held ones
|
|
556
|
+
it did not carry: no duplicate, no gap, and `onInitialTranscript` does not
|
|
557
|
+
fire. Another process (`epoch`), a position the log dropped (`gap`), or an
|
|
558
|
+
older host fall back to the full `sessions.loadEvents` reload.
|
|
559
|
+
Transport-agnostic: it runs through the dispatcher on every transport, and
|
|
560
|
+
matters for the network one.
|
|
293
561
|
|
|
294
562
|
## Turning it off (containment guarantees)
|
|
295
563
|
|
|
296
564
|
The daemon runtime is opt-in and inert when disabled:
|
|
297
565
|
|
|
298
|
-
- **Default off**: `useDaemon` defaults to `false`;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
detached daemons idle-exit on their own.
|
|
566
|
+
- **Default off**: `useDaemon` defaults to `false`; off, no socket, no
|
|
567
|
+
spawn, no other process: the TUI runs its sessions on a host inside its
|
|
568
|
+
own process (`startInProcessHost`, see [TUI local mode](#tui-local-mode))
|
|
569
|
+
and the GUI on its embedded `ChatHost`. Toggling re-activates the open
|
|
570
|
+
session on the other host; detached daemons idle-exit on their own.
|
|
303
571
|
- **Run guard + heartbeat gated too**: the pre-send running-marker guard
|
|
304
572
|
and its 60s heartbeat only engage when `useDaemon` is on, since with it
|
|
305
573
|
off there is exactly one writer per session. `loadSession` heals stale
|
|
306
574
|
(>5 min) on-disk `running` headers with a bounded targeted write.
|
|
307
575
|
- **Emergency kill-switch**: `zidane --no-daemon` (or `ZIDANE_NO_DAEMON=1`)
|
|
308
|
-
forces the in-process
|
|
576
|
+
forces the in-process host for both TUI and GUI regardless of the
|
|
309
577
|
setting.
|
|
310
|
-
- **Fail-
|
|
311
|
-
handshake bug
|
|
312
|
-
notice; the
|
|
313
|
-
`
|
|
314
|
-
|
|
578
|
+
- **Fail-visible + zero footprint**: in daemon mode a spawn failure, socket
|
|
579
|
+
problem, or handshake bug leaves the window reconnecting with a visible
|
|
580
|
+
notice (sends disabled until reattached); turning the setting off or
|
|
581
|
+
`--no-daemon` runs sessions in-process. The daemon's extra state
|
|
582
|
+
(socket, pidfile, `pending-approvals.json`, in-memory drafts/queues)
|
|
583
|
+
exists only while a daemon is running.
|
|
315
584
|
|
|
316
585
|
## Execution semantics (read this)
|
|
317
586
|
|
|
@@ -329,6 +598,13 @@ Moving the host out of the window process changes two things users notice:
|
|
|
329
598
|
broadcast: run lifecycle, gate decisions, interaction flow, dispose/shutdown.
|
|
330
599
|
- `test/ipc-host-commands.test.ts` — host-side `/` command execution and
|
|
331
600
|
parity with the local runtime.
|
|
601
|
+
- `test/server-sync.test.ts` (a server's `sync.*`: claim and takeover,
|
|
602
|
+
each category, window-only keys, overrides and reset, path and size
|
|
603
|
+
checks, `allowRemoteInstalls`, opted-in keys, audit),
|
|
604
|
+
`test/chat-sync-engine.test.ts` (the client engine: watchers and the
|
|
605
|
+
settings stream, offline queue across restarts, import once, takeover,
|
|
606
|
+
off), `test/tui/app-settings-sync.test.tsx` (the real `<App>` with a
|
|
607
|
+
saved server, end to end).
|
|
332
608
|
- `test/ipc-daemon.test.ts` — real unix-socket round-trips: handshake,
|
|
333
609
|
version mismatch, stale socket, multi-peer fan-out, late-attach replay,
|
|
334
610
|
cross-window approval convergence, the concurrent-send guard, and a
|
|
@@ -359,19 +635,29 @@ store at the next run-end resync; **gap** = known missing cabling.
|
|
|
359
635
|
| Running work + ctrl+k cancel picker | synced | `work:snapshot` mirror; cancel via `chat.cancelWork` |
|
|
360
636
|
| Context panel (ctrl+?) | synced | `sessions.contextBreakdown` RPC |
|
|
361
637
|
| Artifacts (rail, badges, `#pr` completion) | synced | see [ARTIFACTS.md § IPC](./ARTIFACTS.md#ipc) |
|
|
362
|
-
| History edits (selection modal) | synced |
|
|
638
|
+
| History edits (selection modal), fork, folder attach/detach | synced | host-side (`sessions.deleteTurn` / `editTurnText` / `fork` / `setFolder`); refused mid-run on every surface |
|
|
363
639
|
| History search (transcript + cross-session) | synced + accelerated | `history.searchSession`/`searchAll`, daemon LRU cache |
|
|
364
|
-
| Provider-retry indicator | synced | `stream:retry`;
|
|
640
|
+
| Provider-retry indicator | synced | `stream:retry`; `stream:retry:cleared` when the stream resumes, errors, or the run ends |
|
|
365
641
|
| Run ceilings (turns/cost/tokens/wall clock/retry) | synced | host reads the same settings-driven caps as local |
|
|
366
642
|
| Native server tools (e.g. web_search) | synced | host forwards `stream:server_tool_use/result` |
|
|
367
643
|
| Footer context gauge + live session cost | synced | `turn:end` carries `TurnUsage` |
|
|
368
644
|
| Subagent spawn banners | synced | host forwards `spawn:before/complete/error` |
|
|
369
645
|
| Active-skill chips | synced | `skills:snapshot` on activation change |
|
|
370
646
|
| MCP "needs login" badge | synced | `mcp:auth`; login flow itself runs window-local |
|
|
371
|
-
|
|
|
647
|
+
| MCP connect / failure status | synced | `mcp:status` |
|
|
648
|
+
| Manual compaction (trigger, indicator, "compacted N turns") | synced | `sessions.compact` runs host-side and returns the report; `compacting:*` signals |
|
|
649
|
+
| Repeat-guard notice | synced | host broadcasts and persists the notice row |
|
|
650
|
+
| Safe-mode "allow for this session" rules | synced | host-owned per session; survive agent rebuilds, dropped on delete |
|
|
651
|
+
| Auto-title after the first exchange | synced | generated host-side at run end (`sessions.generateTitle`: the host's providers and credentials, so a server session is titled with the server's); with several TUI windows on one new session each may generate one (last write wins) |
|
|
652
|
+
| `zidane run --context` (non-`process`) | local | forces the in-process host (`ZIDANE_NO_DAEMON=1`), which runs the session on the launch context (`sessions.setLaunchOverrides`) |
|
|
653
|
+
| `zidane run --system` / `--mcp` | synced | `sessions.setLaunchOverrides` on every activation, per window; the latest connected setter's apply |
|
|
654
|
+
| Cwd picked mid-session | synced | `sessions.setLaunchOverrides({ cwd })`: the live agent moves (tools at once, prompt next turn), not persisted |
|
|
655
|
+
| Extension `ctx.appendNote` outside a run | synced | `sessions.appendNote`: host persists and broadcasts the note |
|
|
656
|
+
| Auto-login after adding an MCP server | synced | window arms it; `mcp:auth` starts the login, `mcp:settled` disarms |
|
|
657
|
+
| Window-side extension `ctx.workspace` | synced | daemon session's `workspace` ops over `sessions.workspace*` |
|
|
372
658
|
| Transcript convergence after any miss | synced | `loadEvents` replace at run end + reconnect |
|
|
373
659
|
| `/` commands (incl. extension) | synced | host-side (`commands.ts`); TUI's own `/` stays local, converges via `sessions.touch` |
|
|
374
|
-
| `!` shell mode | synced output | runs in the issuing window's cwd/env; `sessions.touch` on persist |
|
|
660
|
+
| `!` shell mode | synced output | runs in the issuing window's cwd/env (on a remote server: streamed from the session's folder there, `sessions.workspaceExecStream`, esc kills it there; an older server: the buffered `sessions.workspaceExec`); `sessions.touch` on persist |
|
|
375
661
|
| Extension tools / hooks / MCPs | synced | daemon loads the same registry; tools run daemon-side |
|
|
376
662
|
| Agent Plugins (`enabledPlugins`) | synced | host applies both plugin gates before composing the preset |
|
|
377
663
|
| `SYSTEM.md` prompt override | synced | `system-md` carries the winning file + shadowed candidates |
|
|
@@ -384,36 +670,461 @@ store at the next run-end resync; **gap** = known missing cabling.
|
|
|
384
670
|
| Steer / follow-up transcript order | synced | `user-prompt` fires at delivery via the `steer:inject` hook |
|
|
385
671
|
| MCP login → daemon credential pickup | synced | `chat.rebuildAgent` after a window-local OAuth login |
|
|
386
672
|
| Cross-process run guard | synced | store-status guard both ways (see [Concurrent sends](#concurrent-sends)) |
|
|
387
|
-
| Agent-spawning `/` commands |
|
|
673
|
+
| Agent-spawning `/` commands | synced | host-side spawn seam; children may pin any credentialed provider's model |
|
|
388
674
|
| GUI windows on the shared daemon | synced | GUI main is a daemon guest under the same opt-in gate |
|
|
389
675
|
| GUI composer draft/attachments/search/queue/presence | synced | renderer bound to daemon-mode channels; inert in-process |
|
|
390
676
|
|
|
677
|
+
## TUI local mode
|
|
678
|
+
|
|
679
|
+
A single-process TUI window (the default) runs its sessions on a host in
|
|
680
|
+
its own process: `startInProcessHost` (`src/ipc/memory.ts`) around the same
|
|
681
|
+
dispatcher a daemon serves, with `createHostEnvironment({ shared })` built
|
|
682
|
+
from the window's setup (its config and state store, its live extension
|
|
683
|
+
registry and agent profiles, its HUD / bus / host-runtime bridges, its Logs
|
|
684
|
+
tab), so nothing is discovered or set up twice. The window reaches it
|
|
685
|
+
through `lazyInProcessTransport` (started by the first connect) with the
|
|
686
|
+
same `connectHostSession` code daemon mode uses, and drains it on quit
|
|
687
|
+
(`closeLocalHost`, bounded). Local semantics are kept: a run lives with its
|
|
688
|
+
window (opening another session mid-run is refused, and leaving a session
|
|
689
|
+
disposes it in the host, background tasks included); per-session artifacts
|
|
690
|
+
go to the window's cache dir. `isMultiProcess()` (another process shares
|
|
691
|
+
the sessions) gates what only exists across processes: presence, the run
|
|
692
|
+
guard and heartbeat, the daemon health dot.
|
|
693
|
+
|
|
694
|
+
## TUI remote mode
|
|
695
|
+
|
|
696
|
+
`zidane --host <wss-url> --token-file <path|->` (or `--token <t|->`, or
|
|
697
|
+
`ZIDANE_HOST_URL` / `ZIDANE_HOST_TOKEN`) runs every session of the window on a zidane-server
|
|
698
|
+
through `wsTransport`: a whole-window choice for its lifetime. Which host a
|
|
699
|
+
window uses is one decision, `resolveHostTarget` (`src/tui/host-target.ts`):
|
|
700
|
+
`remote` (a launch's or the environment's remote host), else `daemon`
|
|
701
|
+
(`daemonGate`), else `local`. `isMultiProcess()` is `target !== local`, so
|
|
702
|
+
remote behaves like daemon mode everywhere it is consulted (runs outlive the
|
|
703
|
+
window, switching sessions mid-run is allowed, presence, the run guard, the
|
|
704
|
+
health dot); `useLocalHost` picks the transport from the same target; the
|
|
705
|
+
footer adds `host: <name>` (the server's `hostName` from `ipc.hello`), only
|
|
706
|
+
in remote mode.
|
|
707
|
+
|
|
708
|
+
- **Opt-in**: the flag (or the variable) is the explicit opt-in, as `zidane
|
|
709
|
+
attach` is for the daemon. The `remoteHosts` setting
|
|
710
|
+
(`remoteHostsGate`, default off; Settings → Sessions, "Remote hosts
|
|
711
|
+
(experimental)", inert under the kill switch) gates the several-hosts
|
|
712
|
+
window ([TUI.md § Several hosts](./TUI.md#several-hosts-remote-hosts)):
|
|
713
|
+
it connects only to the servers saved on Settings → Hosts. With several
|
|
714
|
+
hosts, `isMultiProcess()` and the footer label follow the active
|
|
715
|
+
session's host once it is not the window's default one
|
|
716
|
+
(`setActiveHostElsewhereSource` in `src/tui/live-settings.ts`). It stays in the window's `state.json`
|
|
717
|
+
even while a host owns the settings file (`WINDOW_LOCAL_SETTINGS`). `ZIDANE_NO_REMOTE=1`
|
|
718
|
+
wins over everything: the CLI refuses `--host` / `ZIDANE_HOST_URL` with a
|
|
719
|
+
message naming the switch, and `resolveHostTarget` never answers `remote`.
|
|
720
|
+
- **URLs**: `wss://`, or `ws://` to loopback only (a token never crosses
|
|
721
|
+
the network in the clear). The CLI keeps the URL and token in memory
|
|
722
|
+
(`runTui({ remoteHost })`) and removes `ZIDANE_HOST_URL` and
|
|
723
|
+
`ZIDANE_HOST_TOKEN` from its environment so child processes inherit
|
|
724
|
+
neither (`remoteHostFromCli`). A literal `--token` warns on stderr (ps,
|
|
725
|
+
shell history).
|
|
726
|
+
- **Connecting** (`connectWsClient`): bounded by `openTimeoutMs` (15 s),
|
|
727
|
+
abortable through `signal` up to the end of the handshake. A refused
|
|
728
|
+
upgrade is asked again over plain HTTP for its status: 401 / 403 reject
|
|
729
|
+
with `HostAuthError` (`token refused`, `forbidden`), with fixed messages:
|
|
730
|
+
the body is never read (a proxy may echo the token), and a 403's reason
|
|
731
|
+
comes only from the server's `X-Zidane-Refusal` code (`WS_REFUSAL_REASONS`
|
|
732
|
+
in `src/ipc/transports/ws-auth.ts`; unknown codes are ignored). That
|
|
733
|
+
request is `redirect: 'manual'`: a 3xx rejects with `HostRedirectError`
|
|
734
|
+
and the token never leaves the URL's origin (the TLS diagnosis probe in
|
|
735
|
+
`src/ipc/hosts/probe.ts` sends no token and follows no redirect either).
|
|
736
|
+
`expectHostId` (saved hosts: the entry id) closes a connection whose
|
|
737
|
+
hello names another host and rejects with `HostIdentityError`; the shared
|
|
738
|
+
connection loop (`src/ipc/hosts/connection-loop.ts`, `saved-hosts.ts`,
|
|
739
|
+
which checks every connection itself too) reports it as status
|
|
740
|
+
`identity` and does not retry. Messages name URLs by scheme, host and
|
|
741
|
+
path (`displayHostUrl`); `parseRemoteHostUrl` refuses userinfo, query
|
|
742
|
+
strings and fragments.
|
|
743
|
+
- **`zidane attach --host <wss-url> --token-file <path|->`**: the thin client on a
|
|
744
|
+
server (`runAttach({ host })`); nothing is spawned or drained.
|
|
745
|
+
- **Session data comes from the host**, in every mode
|
|
746
|
+
(`src/tui/runtime/session-data.ts`): the sessions list and the `#` chat
|
|
747
|
+
catalog (`sessions.listMeta` / `sessions.getMeta`), resume and activation
|
|
748
|
+
(`sessions.get`; header refreshes on `sessions:changed` and the model /
|
|
749
|
+
agent reflectors read `sessions.getHeader`, so opening a session moves
|
|
750
|
+
its turns once as a document and once as `sessions.loadEvents`, and later
|
|
751
|
+
header refreshes move none), creation (`sessions.create`, stamped host-side with the
|
|
752
|
+
cwd, profile, the window's whole model pick and an authoring session's
|
|
753
|
+
pinned skill), header / model / agent refreshes, titles, export, the
|
|
754
|
+
details modal, cross-session search, environment actions, artifacts,
|
|
755
|
+
out-of-band turns (`sessions.appendTurns`: command output, `!` shell,
|
|
756
|
+
notices), idle todo edits (`todos.set` with `persistIdle`) and renderer
|
|
757
|
+
state (`sessions.setExtensionState`). Requests ride the window's
|
|
758
|
+
app-lifetime host client, or a connection of their own while it is down.
|
|
759
|
+
The window keeps a `SessionSnapshot` of the open session
|
|
760
|
+
(`src/tui/runtime/session-snapshot.ts`): the host's document wrapped as a
|
|
761
|
+
`Session` with no store (mutators are memory-only mirrors, `save()`
|
|
762
|
+
throws), refreshed from the host on activation, history edits and a todos
|
|
763
|
+
signal for a run it doesn't know.
|
|
764
|
+
- **Older hosts**: a host on this machine (the in-process host, the local
|
|
765
|
+
daemon) shares the window's store, so when it predates a method (or can't
|
|
766
|
+
be reached) the window falls back to that store, as before. A remote
|
|
767
|
+
server has no such fallback: one too old to serve sessions is refused
|
|
768
|
+
with a message naming it ("runs an older zidane that can't serve this
|
|
769
|
+
window's sessions").
|
|
770
|
+
- **`!` shell on a server** runs on the server, in the session's folder
|
|
771
|
+
there, streamed like a local `!` (`sessions.workspaceExecStream`; esc
|
|
772
|
+
kills the process group on the server; 1 MiB of output, then it is
|
|
773
|
+
stopped). A server without the method runs it buffered
|
|
774
|
+
(`sessions.workspaceExec`, 10 min bound; esc stops waiting). On a host on
|
|
775
|
+
this machine it is the window's local shell, as before. Either way the
|
|
776
|
+
turns go to the host, in the same format.
|
|
777
|
+
- **Session folders on a server** are inside its workspace roots
|
|
778
|
+
(`--workspace-root`, default `<data dir>/workspaces`): `sessions.create`
|
|
779
|
+
without a folder takes the first root, one outside is refused, as is
|
|
780
|
+
`sessions.setFolder`; a launch-override `cwd` outside them is dropped (a
|
|
781
|
+
window's own path is on its machine). The chat host enforces this itself
|
|
782
|
+
(`ChatHostEnvironment.workspaceRoots` → `ChatHost.workspace`,
|
|
783
|
+
`src/chat/workspace-confinement.ts`) at every entry point taking a
|
|
784
|
+
folder: session creation (orchestration workers included),
|
|
785
|
+
`setSessionFolder`, `setLaunchOverrides`, `listCommands` /
|
|
786
|
+
`completeCommandArgs`, `listEnvironments`, and the workspace file / exec
|
|
787
|
+
methods; `catalog.*` / `models.list` and `fs.*` use the same instance. A
|
|
788
|
+
folderless session (the orchestration main) works in the first root. `fs.roots` / `fs.listDirs` browse
|
|
789
|
+
them. Local hosts are unconfined (`fs.*` rooted at the home; clients may
|
|
790
|
+
keep their local picker there).
|
|
791
|
+
- **Window-local, by design**: `state.json` (UI settings in local mode, the
|
|
792
|
+
last provider and session), keybindings, the window's credentials and MCP
|
|
793
|
+
tokens, `/` built-ins (they act on this window; their transcript turns go
|
|
794
|
+
to the host), session export files (written on
|
|
795
|
+
this machine), and in local / daemon mode the cleanup of this machine's
|
|
796
|
+
artifact blobs on delete. Still on the window's store: the Claude Code
|
|
797
|
+
import (`Settings → import`), which imports this machine's Claude data
|
|
798
|
+
into this machine's store, whatever the window's host
|
|
799
|
+
(`importFromClaudeCode` returns `target: 'this-computer'`; given the
|
|
800
|
+
window's `activeHost`, a remote one adds `claudeImportHostNote`'s "imported
|
|
801
|
+
to this computer" note). Against a remote server the imported sessions
|
|
802
|
+
don't show in its list.
|
|
803
|
+
- **The host's provider keys** are managed over `auth.*`: API keys, and
|
|
804
|
+
OAuth sign-ins with the browser on this computer and the tokens on the
|
|
805
|
+
server (`auth.oauth.*`; Settings › Authentication, `o`). MCP servers sign
|
|
806
|
+
in the same way (MCP servers tab, `o`), and with the server's
|
|
807
|
+
`remoteInstalls` skills / extensions install (`i`), user-scope ones are
|
|
808
|
+
removed (ctrl+x) or updated (ctrl+u), MCP servers added (`a`). The
|
|
809
|
+
window's own credentials stay its own.
|
|
810
|
+
- `test/tui/app-modes.test.tsx` remote mode gives the server its own store
|
|
811
|
+
and data dir (daemon mode keeps sharing the window's: same machine), so a
|
|
812
|
+
direct read of the window's store fails its scenarios; "sessions come from
|
|
813
|
+
the host" opens a session only the host has and checks that one only in
|
|
814
|
+
the window's store is not listed.
|
|
815
|
+
|
|
816
|
+
## GUI several hosts
|
|
817
|
+
|
|
818
|
+
The desktop app's counterpart of [TUI.md § Several hosts](./TUI.md#several-hosts-remote-hosts),
|
|
819
|
+
behind the same gate (`remoteHosts`, Settings › General › Experimental;
|
|
820
|
+
`ZIDANE_NO_REMOTE=1` wins) and the same saved hosts (`hosts.json` + tokens
|
|
821
|
+
in the shared data dir).
|
|
822
|
+
|
|
823
|
+
- **Off** (default, or the kill switch): the app is the single-host app.
|
|
824
|
+
`gui/src/main/chat/hosts/gate.ts` never imports `./remote-hosts` (the only
|
|
825
|
+
module importing `zidane/ipc/remote`), so `hosts.json` is not read, no
|
|
826
|
+
registry is built, no WebSocket opens; the host directory holds This
|
|
827
|
+
computer only and every request goes where it went before.
|
|
828
|
+
`hosts/gate.containment.test.ts` boots `registerChatIpc` with the setting
|
|
829
|
+
off and a saved server on disk (also with the setting on under the kill
|
|
830
|
+
switch), and checks the static graph: no main module but
|
|
831
|
+
`remote-hosts.ts` imports `zidane/ipc/remote`, none imports
|
|
832
|
+
`remote-hosts` statically, and the `zidane` entries main imports
|
|
833
|
+
statically don't reach the WebSocket chunk.
|
|
834
|
+
- **Host directory** (`gui/src/main/chat/host-directory.ts`): This computer
|
|
835
|
+
(the embedded host or the daemon, through `hostRequest`, its in-process
|
|
836
|
+
fallback kept) plus one `ServerHost` per saved server (the WebSocket
|
|
837
|
+
client, reconnecting with backoff, its status: `connecting`,
|
|
838
|
+
`connected`, `reconnecting`, `offline`, `auth-refused`, `identity`: a
|
|
839
|
+
different server answers at its URL, not retried). A session is a
|
|
840
|
+
(host, id) pair: two hosts can hold the same id (a server sharing this
|
|
841
|
+
machine's data dir, a copied store). The app names a session by its
|
|
842
|
+
`SessionKey` (`gui/src/shared/session-key.ts`): the bare id for This
|
|
843
|
+
computer's (so local-only state is what it always was), `<hostId>␟<id>`
|
|
844
|
+
for a server's. Rows, the open chat, selection, the sidebar, persisted
|
|
845
|
+
view state and every signal main pushes carry keys (main keys a host's
|
|
846
|
+
signals by the host they came from); every session-scoped channel takes
|
|
847
|
+
one (branded type: a bare id doesn't type-check), and main routes with
|
|
848
|
+
`hostFor({ key })` or `hostFor({ hostId })` (list, create, environments,
|
|
849
|
+
settings, folders). A key of a server never known, or a local key This
|
|
850
|
+
computer has no session for, throws `UnknownHostIdError`: never a
|
|
851
|
+
guess. Saving a saved server again (a new token) reconnects it in place,
|
|
852
|
+
its sessions still routed to it. Removing one (Settings › Hosts, another
|
|
853
|
+
window, the gate turned off) marks its sessions and request ids removed
|
|
854
|
+
(`HostRemovedError` from then on), retracts its pending approvals and
|
|
855
|
+
questions on the renderer and sends `hosts:removed` with their keys
|
|
856
|
+
(`hosts/server-removal.ts`): a chat open on one of its sessions closes
|
|
857
|
+
with "<server> was removed", a twin on This computer stays open.
|
|
858
|
+
`sessionRequest(method, key, params)` / `sessionNotify` are the shorthands every
|
|
859
|
+
chat channel uses (send, steer, queue, approvals, interactions, todos,
|
|
860
|
+
context, model picks, history edits, terminals, workspace exec). Ids a
|
|
861
|
+
server hands out (approvals, questions, terminals) are noted and
|
|
862
|
+
answered there, and forgotten once settled (answered, withdrawn,
|
|
863
|
+
closed: `forgetOwner`); a late use of a settled id is refused
|
|
864
|
+
(`RequestSettledError`), never sent to This computer. A server that is down rejects with `HostOfflineError`:
|
|
865
|
+
never a fallback to this computer's store.
|
|
866
|
+
- **Signals**: every connected server's signals go through the same
|
|
867
|
+
`broadcast()` demux (`hosts/server-signals.ts`), minus what is this
|
|
868
|
+
computer's (its settings, extension bus, the dev browser, the
|
|
869
|
+
transcript-search mirror, the shared ghost text). Drafts another client
|
|
870
|
+
of the server types on a session (`draft:changed`, `draft:attachments`)
|
|
871
|
+
and the session's active skills (`skills:snapshot`) reach the window as
|
|
872
|
+
the local daemon's do, whether or not the local daemon is connected,
|
|
873
|
+
and the server's
|
|
874
|
+
`presence:changed` is merged with every other host's
|
|
875
|
+
(`presence-merge.ts`: the last snapshot per host, a server's entries
|
|
876
|
+
dropped when it goes away); `presence:list` asks every connected host.
|
|
877
|
+
`drafts.*` go to the session's host. `sessions:changed` from any host
|
|
878
|
+
re-reads every group of the sidebar.
|
|
879
|
+
- **Session data**: a server session's rows, transcript
|
|
880
|
+
(`sessions.loadEvents` pages of 200 turns, `loadEventsPage`: the last
|
|
881
|
+
page first, scrolling back passes the page's `cursor` as `before`),
|
|
882
|
+
footer numbers (`sessions.getHeader` `usage`: `contextTokens` for the
|
|
883
|
+
context gauge, `costUsd` for the cost pill; only a server too old for
|
|
884
|
+
`usage` has its document read), a clipped tool result's full text
|
|
885
|
+
(`sessions.getToolResult`), header, title, archive, turns and export
|
|
886
|
+
(`loadSessionDocument`: the document a page of turns at a time, written
|
|
887
|
+
as a copy on this computer) come from the server
|
|
888
|
+
(`hosts/server-sessions.ts`, `channels/history.ts`). No read asks a
|
|
889
|
+
current server for a whole session in one reply. Its last good list
|
|
890
|
+
stays in memory while it is offline (shown read-only, opening says the
|
|
891
|
+
server is offline). Rows carry `hostId` and a `zidane-env://<key>`
|
|
892
|
+
workspace root (the key URI-encoded), so the
|
|
893
|
+
Files / Diffs panels and `@` mentions read the server's folder through
|
|
894
|
+
`sessions.workspace*`, and `!` runs there, streamed
|
|
895
|
+
(`sessions.workspaceExecStream`, `server-shell.ts`; `shell:cancel` kills
|
|
896
|
+
it there; an older server: buffered), like the TUI.
|
|
897
|
+
- **Channels**: `hosts:status` (snapshot, also pushed on `hosts:status`),
|
|
898
|
+
`hosts:list` (with the registry's dates), `hosts:add` / `hosts:test`
|
|
899
|
+
(a saved id, or an unsaved URL + token; refusals come back as
|
|
900
|
+
`{ ok: false, error, message }` with the registry's codes),
|
|
901
|
+
`hosts:rename`, `hosts:remove`, `hosts:retry`, `hosts:listDirs`
|
|
902
|
+
(`fs.listDirs`), `hosts:authStatus` / `hosts:authSetApiKey` /
|
|
903
|
+
`hosts:authRemove` (`auth.*`), `hosts:catalog` (`catalog.*`, `null`
|
|
904
|
+
from a server too old for them). `sessions:list`, `sessions:create`,
|
|
905
|
+
`environments:list`, `settings:get` / `settings:set`,
|
|
906
|
+
`sessions:listModels`, `commands:list` / `commands:completeArgs`,
|
|
907
|
+
`extensions:completionProviders` / `extensions:completionSubmit` take
|
|
908
|
+
an optional host id.
|
|
909
|
+
- **Renderer**: Settings › Hosts (This computer + servers, status, server
|
|
910
|
+
name and version; add with the token masked, test before saving, a
|
|
911
|
+
message per refusal code; test, rename, remove with a confirm); the
|
|
912
|
+
sidebar grouped by host (no headers with This computer alone); a new
|
|
913
|
+
chat picks its host first, then that host's environments, then for a
|
|
914
|
+
server a folder on it; the chat header shows `on <server>` with its
|
|
915
|
+
status dot. On a server session, actions that need this machine's files
|
|
916
|
+
are not offered (reveal in Finder, open in editor, the local folder
|
|
917
|
+
picker, the dev browser tab, extension panels); there is no "download a
|
|
918
|
+
copy" of a single file yet, the session export writes a copy here. A
|
|
919
|
+
server terminal's loopback links (`localhost:5173`) are the server's:
|
|
920
|
+
they are not opened here, a tooltip says why (`serverOnlyLink`,
|
|
921
|
+
`terminal/links.ts`). A new chat that can't be created (its server went
|
|
922
|
+
offline) puts the typed prompt back with the reason above it
|
|
923
|
+
(`unsentDraft`, `ComposerNotice`).
|
|
924
|
+
- **Settings sync** ([SERVER.md § Settings sync](./SERVER.md#settings-sync)):
|
|
925
|
+
a server card's *Sync* (`ServerSync.tsx`): mode (Off · Import once ·
|
|
926
|
+
Keep in sync), the categories, each API key opted in after a
|
|
927
|
+
confirmation, *Take over* when another device syncs there (confirmed),
|
|
928
|
+
the server's overrides with *Sync again*, the connection / last push /
|
|
929
|
+
what waits / errors and what can't sync. Main: `hosts:syncState`,
|
|
930
|
+
`hosts:syncConfigure` (modes, categories and provider names checked),
|
|
931
|
+
`hosts:syncPush`, `hosts:syncTakeOver`, `hosts:syncReset`, and the
|
|
932
|
+
`hosts:syncChanged` push (the engine changed, or a server's
|
|
933
|
+
`sync:changed`). A server's Settings show `synced from <device>` under a
|
|
934
|
+
synced setting, or `edited here, not synced from <device>` with *Sync
|
|
935
|
+
again* (`SyncMarker.tsx`, `hosts:syncStatus`, any window). A local
|
|
936
|
+
`settings:set` tells the engine; its watchers cover the TUI's and other
|
|
937
|
+
windows' writes. Off with remote hosts: `hosts:syncState` answers
|
|
938
|
+
`enabled: false` and nothing loads (`gate.containment.test.ts`).
|
|
939
|
+
- **Settings on a server session**: `HostSettingsProvider` (renderer)
|
|
940
|
+
reads `settings:get(hostId)` (this window's settings overlaid with the
|
|
941
|
+
server's, `overlayHostSettings`) and writes `settings:set(patch,
|
|
942
|
+
hostId)` (main splits it with `splitHostSettingsPatch`: window-only keys
|
|
943
|
+
to this computer, the rest to the server, the server's part first: a
|
|
944
|
+
refused write applies nothing here and rejects with the server's error);
|
|
945
|
+
title `Settings · <server>`.
|
|
946
|
+
Authentication lists the server's providers: an API key is stored there,
|
|
947
|
+
an OAuth provider signs in (`hosts:oauth`, `ServerOAuthSignIn`: the
|
|
948
|
+
browser opens here through a loopback in the main process, a code can be
|
|
949
|
+
pasted, `hosts:oauthPaste` / `hosts:oauthCancel`, `hosts:oauthStart`
|
|
950
|
+
pushes the URL / device code), a stored credential is removed. Skills / MCPs / Extensions /
|
|
951
|
+
Plugins show the server's catalog for the session's folder there
|
|
952
|
+
(`HostCatalogPanel`, `hosts:catalog`): each entry with the server's
|
|
953
|
+
enabled state, a toggle writing the server's enabled list (the first
|
|
954
|
+
one seeds it from what the list governs; a plugin's or extension's
|
|
955
|
+
contribution or a broken entry shows its state and can't be toggled);
|
|
956
|
+
an HTTP MCP server signs in from here (`hosts:oauth`). When the server
|
|
957
|
+
takes installs (`hosts:installsAllowed`, its hello's `remoteInstalls`):
|
|
958
|
+
skills / extensions install from a spec (`hosts:install`), user-scope
|
|
959
|
+
entries are removed (two presses, `hosts:uninstall`), extensions updated
|
|
960
|
+
(`hosts:update`), MCP servers added / removed (`hosts:mcpUpsert` /
|
|
961
|
+
`hosts:mcpRemove`; an Authorization header stays on the server); a
|
|
962
|
+
`catalog:changed` from the server (`hosts:catalogChanged`) re-reads the
|
|
963
|
+
open tab. A server too old for `catalog.*` gets its enabled list. The summarizer picker
|
|
964
|
+
lists the server's models. Environments and Instructions are hidden.
|
|
965
|
+
The Claude Code import still targets this computer and says so when the
|
|
966
|
+
active chat is on a server.
|
|
967
|
+
- **Models, commands, search on a server session**: the model pickers
|
|
968
|
+
list the server's credentialed providers and models (`models.list`;
|
|
969
|
+
an older server: this computer's, as before). A pick there becomes this
|
|
970
|
+
window's remembered default (`lastProvider`, `lastModelChain`) only when
|
|
971
|
+
this computer can run its provider, without the fallbacks it can't
|
|
972
|
+
(`rememberableChain` in `zidane/chat/pure`, the TUI applies the same one); a server draft that picks a
|
|
973
|
+
provider only the server has launches under it without remembering it.
|
|
974
|
+
`/` completion, the palette's commands and the submit path's command
|
|
975
|
+
check use the server's `commands.list` / `commands.completeArgs` for the
|
|
976
|
+
session's folder there. Ghost text is the server's (`suggest.resolve`:
|
|
977
|
+
its path provider over its files, its extensions' providers). This
|
|
978
|
+
window's extension popup providers (`extensions:completionProviders`)
|
|
979
|
+
are not offered on a server session: they are this window's extensions,
|
|
980
|
+
run against this computer's files, and no RPC runs a server's. `@`
|
|
981
|
+
mentions list the server's files (`sessions.workspaceListFiles`). The
|
|
982
|
+
sidebar's content search also searches each connected server's group
|
|
983
|
+
there (`history.searchAll` over that group's rows), the hits merged with
|
|
984
|
+
this computer's. The `/` popover's skills are the ones the server
|
|
985
|
+
enables (`hosts:catalog` skills for the folder there).
|
|
986
|
+
- Tests: `host-directory.test.ts` (routing by key, twins),
|
|
987
|
+
`hosts/remote-hosts.server.test.ts` (the channels against a real
|
|
988
|
+
zidane-server spawned with Bun: per-code refusals, add, folders,
|
|
989
|
+
environments, create in a server folder, rename, `!` on the server,
|
|
990
|
+
transcript pages and scroll-back, the same id on this computer and the
|
|
991
|
+
server (both listed, each opened and edited on its host), a tool
|
|
992
|
+
result's text and an export without the whole document in one reply,
|
|
993
|
+
skills signals without the local daemon,
|
|
994
|
+
footer numbers from the header, the server's models, `/` commands,
|
|
995
|
+
ghost text and files, content search, drafts and presence from another
|
|
996
|
+
client, catalogs and a toggle, per-host settings and provider keys,
|
|
997
|
+
offline rows, remove; the server runs with its own HOME and no provider
|
|
998
|
+
keys of the developer's), `hosts/gate.containment.test.ts`, renderer
|
|
999
|
+
`HostsPanel.test.tsx`, `HostCatalogPanel.test.tsx`,
|
|
1000
|
+
`remember-model-pick.test.ts`, `sidebar-host-rows.test.ts`,
|
|
1001
|
+
`sidebar/hosts-sidebar.test.tsx`, `SessionsContext.twins.test.tsx` (two
|
|
1002
|
+
rows with one id, each opened and acted on with its key).
|
|
1003
|
+
|
|
1004
|
+
### Machines in the desktop app
|
|
1005
|
+
|
|
1006
|
+
Behind the same gate (`docs/MACHINES.md` § Client UI). Main
|
|
1007
|
+
(`gui/src/main/chat/channels/machines.ts`) answers `machines:links`,
|
|
1008
|
+
`machines:shareStart` (minting the machine token on the server when This
|
|
1009
|
+
computer's host has none; `needsToken` asks the renderer for a pasted
|
|
1010
|
+
one, never echoed back in an error), `machines:shareStop`,
|
|
1011
|
+
`machines:useServer`, `machines:list`, `machines:forget`,
|
|
1012
|
+
`machines:listDirs`, all through This computer's host (`hostRequest`:
|
|
1013
|
+
the daemon, or the embedded host through the dispatcher's own handlers,
|
|
1014
|
+
`gui/src/main/chat/machine-links.ts`) or a saved server. It relays
|
|
1015
|
+
`machine:status` (any host, with its `hostId`) as `machines:status` and
|
|
1016
|
+
This computer's `machine:links` as `machines:links`; a server's
|
|
1017
|
+
`machine:notification` never reaches a window. `HostSummary.machineServe`
|
|
1018
|
+
says a server lends itself. Remote hosts coming on resume the embedded
|
|
1019
|
+
host's saved links. The renderer: Settings › Hosts › a server's
|
|
1020
|
+
*Machines* (`ServerMachines.tsx`), machine kinds grouped in the
|
|
1021
|
+
environment picker with a machine folder picker in the composer, and
|
|
1022
|
+
`MachineWaitingBanner` over the composer.
|
|
1023
|
+
|
|
1024
|
+
## Several hosts: what the TUI and the GUI share
|
|
1025
|
+
|
|
1026
|
+
One implementation of the client side, each app keeping only its glue:
|
|
1027
|
+
|
|
1028
|
+
- `src/chat/hosts` (pure parts re-exported from `zidane/chat/pure`):
|
|
1029
|
+
`LOCAL_HOST_KEY` / `LOCAL_HOST_NAME`, `HostConnectionStatus`,
|
|
1030
|
+
`isMethodNotFound` (JSON-RPC -32601), the presence merge
|
|
1031
|
+
(`createPresenceMerge`, `mergePresenceMaps`), the id → host index
|
|
1032
|
+
(`createHostIndex`: unknown ids refused, a removed host's ids remembered
|
|
1033
|
+
as removed), and the registry with `checkHostInput` /
|
|
1034
|
+
`probeUnsavedHost` (the add form's unsaved test runs `add`'s checks).
|
|
1035
|
+
- `src/ipc/hosts` (exported from `zidane/ipc/remote`): `keepHostConnected`
|
|
1036
|
+
(the transport-agnostic connection loop: backoff, statuses, auth refusal
|
|
1037
|
+
not retried), `startSavedHosts` (registry ↔ directory sync through a
|
|
1038
|
+
`SavedHostsDirectory` the app supplies; a re-add reconfigures the entry
|
|
1039
|
+
in place), and `startWsSavedHosts` (the WebSocket registry, dial, unsaved
|
|
1040
|
+
probe and refusal classification). `connection-loop.ts` imports no
|
|
1041
|
+
WebSocket code, so the TUI loads it statically for This computer; the
|
|
1042
|
+
rest stays behind each app's gate.
|
|
1043
|
+
- TUI glue: `src/tui/host/host-directory.ts` (entries with transports,
|
|
1044
|
+
default/active pointers, session routing, `onRemoved`),
|
|
1045
|
+
`remote-hosts.ts`, `host-signals.ts`, `use-host-removal.ts`,
|
|
1046
|
+
`runtime/routed-session-data.ts`. GUI glue:
|
|
1047
|
+
`gui/src/main/chat/host-directory.ts`, `hosts/remote-hosts.ts`,
|
|
1048
|
+
`hosts/server-removal.ts`, `hosts/server-signals.ts`,
|
|
1049
|
+
`hosts/server-sessions.ts`, `presence-merge.ts`, and the renderer's
|
|
1050
|
+
`gui/src/shared/session-key.ts`.
|
|
1051
|
+
- Paged reads (`zidane/ipc`): `loadAllEvents` / `loadEventsPage` (the
|
|
1052
|
+
transcript, both apps) and `loadSessionDocument` (the whole document a
|
|
1053
|
+
page of turns at a time: the GUI's export of a server session; the TUI's
|
|
1054
|
+
own reads of a document still use `sessions.get`).
|
|
1055
|
+
- Settings sync to a server (`src/chat/sync`, loaded with the saved
|
|
1056
|
+
servers, behind the same gate): `types.ts` (pure: categories, modes,
|
|
1057
|
+
field names, `syncMarkerOf`; re-exported from `zidane/chat/pure`),
|
|
1058
|
+
`local.ts` (the device id in `device.json`, the per-server config and
|
|
1059
|
+
offline queue in `hosts-sync.json`, both 0600), `collect.ts` (each
|
|
1060
|
+
category's snapshot from this computer's files), `watchers.ts`,
|
|
1061
|
+
`pusher.ts` and `engine.ts` (`createSyncEngine`, exported from
|
|
1062
|
+
`zidane/ipc/remote`). Each app hands it its servers' connections and
|
|
1063
|
+
`sync:changed` signals: the TUI in `src/tui/host/remote-hosts.ts`, the
|
|
1064
|
+
GUI in `gui/src/main/chat/hosts/remote-hosts.ts`. UIs: the TUI's
|
|
1065
|
+
`src/tui/settings/server-sync-page.tsx` (Hosts → `ctrl+s`) and markers
|
|
1066
|
+
in `host-settings-scope.tsx`; the GUI's `ServerSync.tsx` (a server card's
|
|
1067
|
+
Sync) and `SyncMarker.tsx` (its Settings), over the `hosts:sync*`
|
|
1068
|
+
channels.
|
|
1069
|
+
- Settings on a server, both apps: the split is keyed on "the host is a
|
|
1070
|
+
server, not This computer" (`isWindowOnlySetting`). Window-only keys
|
|
1071
|
+
never go to a server and a server's never come in; the GUI's main splits
|
|
1072
|
+
`settings:set` and drops a server's `settings:changed`, the TUI applies a
|
|
1073
|
+
server's host-scoped keys only and persists only window-only ones while
|
|
1074
|
+
on a server (`--host` included).
|
|
1075
|
+
|
|
391
1076
|
## Removability
|
|
392
1077
|
|
|
393
1078
|
The dependency direction is strictly `src/ipc` → `src/chat`: the host and
|
|
394
1079
|
its wire shapes (`src/chat/host/**`, incl. `signals.ts`) import nothing
|
|
395
|
-
from `src/ipc`.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
`
|
|
399
|
-
|
|
400
|
-
|
|
1080
|
+
from `src/ipc`. The TUI's local mode runs on `src/ipc`'s dispatcher, host
|
|
1081
|
+
environment and in-process transport, so those stay. Deleting the socket
|
|
1082
|
+
side — the daemon server and socket client, `src/daemon-cli.ts`, the
|
|
1083
|
+
socket transport, and the GUI's daemon glue (`daemon-client.ts`,
|
|
1084
|
+
`daemonHostClient` in `host-client.ts`, and the few remaining daemon
|
|
1085
|
+
branches in `ipc.ts`: the `!` guard, inline autocomplete, the settings
|
|
1086
|
+
write) — removes multi-window sync and nothing else: `createChatHost`
|
|
1087
|
+
stays, since it's the chat engine the GUI embeds unconditionally.
|
|
401
1088
|
|
|
402
1089
|
## Status / follow-ups
|
|
403
1090
|
|
|
404
|
-
-
|
|
405
|
-
|
|
406
|
-
|
|
1091
|
+
- Every TUI mode is host-backed ([TUI local mode](#tui-local-mode)).
|
|
1092
|
+
`test/tui/app-modes.test.tsx` drives the real `<App>` through every
|
|
1093
|
+
scenario on the in-process host and against a daemon built with the
|
|
1094
|
+
production environment around the window's setup, and pins the intended
|
|
1095
|
+
difference (local refuses to open another session mid-run; the daemon
|
|
1096
|
+
lets the run continue). `test/tui-runtime-parity.test.ts` runs every
|
|
1097
|
+
runtime scenario over the in-process and socket transports, which must
|
|
1098
|
+
match.
|
|
1099
|
+
- The daemon keeps per-session artifacts under the config dir
|
|
1100
|
+
(`paths.userDir`), the in-process host under the cache dir
|
|
1101
|
+
(`paths.cacheDir`, where the TUI always kept them). They coincide in the
|
|
1102
|
+
default layout and differ under an XDG layout.
|
|
1103
|
+
- `state.json` across processes: the daemon and a local-mode window on the
|
|
1104
|
+
same file both read-modify-write it without a lock (in daemon mode the
|
|
1105
|
+
window leaves settings to the daemon). A host in the window's process
|
|
1106
|
+
shares the window's state store, so that pair is one writer. Field-scoped
|
|
1107
|
+
merges or a lock would close the cross-process case.
|
|
407
1108
|
- The GUI embeds `ChatHost` in Electron main and also speaks daemon: when
|
|
408
1109
|
one is listening and `useDaemon` is on, `daemon-client.ts` connects as a
|
|
409
|
-
guest (`drainOnMismatch: false`)
|
|
410
|
-
|
|
411
|
-
|
|
1110
|
+
guest (`drainOnMismatch: false`). Every host operation goes through one
|
|
1111
|
+
`HostClient` (`gui/src/main/chat/host-client.ts`), named and typed by
|
|
1112
|
+
`IpcMethods`: `daemonHostClient` forwards to the socket,
|
|
1113
|
+
`embeddedHostClient` is a typed table of the in-process behavior
|
|
1114
|
+
(including where it differs: inert drafts / presence / search, artifacts
|
|
1115
|
+
in the local store). `hostRequest(method, params, { fallback })` picks
|
|
1116
|
+
the live one; with `fallback`, a dead socket (never a daemon answer)
|
|
1117
|
+
retries on the in-process host. A remote host would be a third
|
|
1118
|
+
implementation.
|
|
412
1119
|
- `settings.set` in GUI daemon mode is wire-only (a local pre-write would
|
|
413
1120
|
skip the daemon's agent-teardown ladder on extension/MCP toggles), with
|
|
414
1121
|
a local-write fallback if the socket dies mid-write.
|
|
415
|
-
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
1122
|
+
- Session edits (fork / deleteTurn / editTurnText / attach and detach a
|
|
1123
|
+
folder / `!` output) run in the host on every surface: the GUI calls
|
|
1124
|
+
the daemon's RPC or its in-process host, the TUI calls the RPC on its
|
|
1125
|
+
host (daemon or in-process), and no client writes the store for them. History and folder edits
|
|
1126
|
+
are refused while the session runs; the GUI greys the actions out, the
|
|
1127
|
+
TUI explains. Remaining optional wire addition: a SQL-narrowed
|
|
1128
|
+
cross-session search fast path (perf only). `sessions.loadEvents` pages
|
|
1129
|
+
from the host's loaded document (the store has no turn-range read), so
|
|
1130
|
+
paging saves the wire, not the host's disk read.
|