@asm-agent/coding-agent 0.8.8 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +2 -7
- package/dist/asm-agent-runtime/src/__pycache__/asm_cm_driver.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/__pycache__/phase76_address_head.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__init__.py +1 -1
- package/dist/asm-agent-runtime/src/rlm/__pycache__/__init__.cpython-312.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/bash.cpython-312.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/bash.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp.cpython-312.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp_base.cpython-312.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/repl.cpython-312.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/repl.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/skill.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/bash.py +94 -31
- package/dist/asm-agent-runtime/src/rlm/repl.md +3 -3
- package/dist/asm-agent-runtime/src/rlm/repl.py +46 -45
- package/dist/asm-agent-runtime/test/__pycache__/test_bash.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/test/__pycache__/test_repl.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/test/test_bash.py +74 -19
- package/dist/asm-agent-runtime/test/test_repl.py +67 -3
- package/dist/asm-agent-runtime/uv.lock +446 -155
- package/dist/bundle/{anthropic-OFECORE7.js → anthropic-ZOZNPFXS.js} +2 -2
- package/dist/bundle/{azure-openai-responses-AOVTLR6N.js → azure-openai-responses-TDHCU647.js} +2 -2
- package/dist/bundle/{bundled-modules-JQCJDEIY.js → bundled-modules-Q2YODC4B.js} +3 -3
- package/dist/bundle/{chunk-M67RGWDQ.js → chunk-AMKLNEAK.js} +25 -44
- package/dist/bundle/{chunk-7TB7CGPV.js → chunk-FPY7FFJS.js} +978 -1821
- package/dist/bundle/{chunk-JC2TT4MZ.js → chunk-G6SC4YYW.js} +4942 -1916
- package/dist/bundle/{chunk-A65OM3TZ.js → chunk-QLHAP3FT.js} +1 -1
- package/dist/bundle/{cli-main-FNHDQGQD.js → cli-main-277AG2BR.js} +3 -3
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/{google-RVJL6RIQ.js → google-Q3AJWUOY.js} +1 -1
- package/dist/bundle/{google-vertex-EZTRRRH6.js → google-vertex-2CSNOUBS.js} +1 -1
- package/dist/bundle/{main-IYXIIAJN.js → main-TNXWJXIU.js} +3 -3
- package/dist/bundle/{mistral-FSGKSMA7.js → mistral-XRYJGLLG.js} +1 -1
- package/dist/bundle/{openai-codex-responses-VM5Y564D.js → openai-codex-responses-C6BRO2BN.js} +2 -2
- package/dist/bundle/{openai-completions-X46FOQDN.js → openai-completions-DNXCK2JW.js} +3 -3
- package/dist/bundle/{openai-responses-SOQNEPBE.js → openai-responses-BEMWVIXE.js} +2 -2
- package/dist/config.d.ts +0 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -3
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-config.d.ts +1 -2
- package/dist/core/agent-session-config.d.ts.map +1 -1
- package/dist/core/agent-session-config.js +0 -3
- package/dist/core/agent-session-config.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +0 -8
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +0 -2
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +0 -22
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +8 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +100 -22
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/kernel/bootstrap.d.ts.map +1 -1
- package/dist/core/kernel/bootstrap.js +1 -1
- package/dist/core/kernel/bootstrap.js.map +1 -1
- package/dist/core/kernel/repl-manager.d.ts +32 -8
- package/dist/core/kernel/repl-manager.d.ts.map +1 -1
- package/dist/core/kernel/repl-manager.js +401 -102
- package/dist/core/kernel/repl-manager.js.map +1 -1
- package/dist/core/kernel/shared.d.ts +11 -7
- package/dist/core/kernel/shared.d.ts.map +1 -1
- package/dist/core/kernel/shared.js +2 -5
- package/dist/core/kernel/shared.js.map +1 -1
- package/dist/core/mcp/mcp-manager.d.ts +4 -2
- package/dist/core/mcp/mcp-manager.d.ts.map +1 -1
- package/dist/core/mcp/mcp-manager.js +9 -7
- package/dist/core/mcp/mcp-manager.js.map +1 -1
- package/dist/core/prime-inference-auth.d.ts +0 -9
- package/dist/core/prime-inference-auth.d.ts.map +1 -1
- package/dist/core/prime-inference-auth.js +0 -50
- package/dist/core/prime-inference-auth.js.map +1 -1
- package/dist/core/provider-display-names.d.ts.map +1 -1
- package/dist/core/provider-display-names.js +0 -1
- package/dist/core/provider-display-names.js.map +1 -1
- package/dist/core/session-manager.d.ts +0 -4
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +0 -22
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -16
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +13 -43
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +0 -5
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/acp-mcp.d.ts +6 -0
- package/dist/core/tools/acp-mcp.d.ts.map +1 -0
- package/dist/core/tools/acp-mcp.js +86 -0
- package/dist/core/tools/acp-mcp.js.map +1 -0
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ipython.d.ts.map +1 -1
- package/dist/core/tools/ipython.js +9 -4
- package/dist/core/tools/ipython.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -11
- package/dist/main.js.map +1 -1
- package/dist/modes/acp/acp-mode.d.ts.map +1 -1
- package/dist/modes/acp/acp-mode.js +1 -1
- package/dist/modes/acp/acp-mode.js.map +1 -1
- package/dist/modes/agent-connection/daemon-agent-connection.d.ts +0 -2
- package/dist/modes/agent-connection/daemon-agent-connection.d.ts.map +1 -1
- package/dist/modes/agent-connection/daemon-agent-connection.js +0 -2
- package/dist/modes/agent-connection/daemon-agent-connection.js.map +1 -1
- package/dist/modes/agents-view/agents-view-mode.d.ts.map +1 -1
- package/dist/modes/agents-view/agents-view-mode.js +0 -19
- package/dist/modes/agents-view/agents-view-mode.js.map +1 -1
- package/dist/modes/daemon/daemon-mode.d.ts +1 -10
- package/dist/modes/daemon/daemon-mode.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-mode.js +134 -274
- package/dist/modes/daemon/daemon-mode.js.map +1 -1
- package/dist/modes/daemon/daemon-protocol.d.ts +11 -5
- package/dist/modes/daemon/daemon-protocol.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-protocol.js +6 -8
- package/dist/modes/daemon/daemon-protocol.js.map +1 -1
- package/dist/modes/daemon/daemon-supervisor.d.ts +0 -3
- package/dist/modes/daemon/daemon-supervisor.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-supervisor.js +18 -52
- package/dist/modes/daemon/daemon-supervisor.js.map +1 -1
- package/dist/modes/daemon/daemon-worker-protocol.d.ts +1 -6
- package/dist/modes/daemon/daemon-worker-protocol.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-worker-protocol.js +0 -2
- package/dist/modes/daemon/daemon-worker-protocol.js.map +1 -1
- package/dist/modes/interactive/auth-flows.d.ts +0 -2
- package/dist/modes/interactive/auth-flows.d.ts.map +1 -1
- package/dist/modes/interactive/auth-flows.js +1 -101
- package/dist/modes/interactive/auth-flows.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +6 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +13 -3
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +2 -2
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/mermaid.d.ts +12 -0
- package/dist/modes/interactive/components/mermaid.d.ts.map +1 -0
- package/dist/modes/interactive/components/mermaid.js +63 -0
- package/dist/modes/interactive/components/mermaid.js.map +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/feature-hints.d.ts.map +1 -1
- package/dist/modes/interactive/feature-hints.js +0 -4
- package/dist/modes/interactive/feature-hints.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -7
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +23 -279
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +1 -1
- package/dist/utils/version-check.js.map +1 -1
- package/docs/settings.md +1 -31
- package/docs/usage.md +1 -4
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +6 -5
- package/dist/core/agent-traces.d.ts +0 -130
- package/dist/core/agent-traces.d.ts.map +0 -1
- package/dist/core/agent-traces.js +0 -746
- package/dist/core/agent-traces.js.map +0 -1
- package/dist/core/telemetry.d.ts +0 -92
- package/dist/core/telemetry.d.ts.map +0 -1
- package/dist/core/telemetry.js +0 -633
- package/dist/core/telemetry.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interactive-mode.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/interactive-mode.ts"],"names":[],"mappings":"AAKA,OAAO,EAGN,KAAK,YAAY,EAMjB,MAAM,eAAe,CAAC;AAavB,OAAO,EAEN,KAAK,SAAS,EAcd,MAAM,gBAAgB,CAAC;AAsGxB,OAAO,KAAK,EACX,eAAe,EAkBf,oBAAoB,EAEpB,MAAM,8BAA8B,CAAC;AA4EtC,OAAO,KAAK,EACX,+BAA+B,EAE/B,yBAAyB,EACzB,MAAM,gCAAgC,CAAC;AAOxC,OAAO,KAAK,EAAE,sBAAsB,EAAiC,MAAM,yBAAyB,CAAC;AAoCrG,eAAO,MAAM,WAAW,0MAMd,CAAC;AAEX,wBAAgB,kBAAkB,CAAC,MAAM,eAAc,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAErF;AAUD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,WAAW,GAAG,MAAM,CAEnG;AAED,wBAAgB,yBAAyB,CACxC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,UAAU,GAAG,WAAW,EAC/B,wBAAwB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GACjD,MAAM,CAKR;AAgDD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAWnD;AAqBD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAsBvE;AAED,MAAM,WAAW,uBAAuB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,wBAAwB;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,SAAS,uBAAuB,EAAE,CAAC;IAC5D,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED,qBAAa,iBAAkB,YAAW,SAAS;IAOjD,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAVzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAW;IACnC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAK;IAC5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAK;IAEhC,YACkB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,MAAM,GAAG,SAAS,EACpC,MAAM,EAAE,MAAM,MAAM,EACpB,mBAAmB,CAAC,oBAAQ,EAC5B,OAAO,GAAE,wBAA6B,EAIvD;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAmD9B;CACD;AA6MD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA8BtE;AAWD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAM1G;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,gBAAgB,EAAE,MAAM,GAAG,MAAM,EAAE,CAEhG;AAED,wBAAgB,wCAAwC,CACvD,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,sBAAsB,EAAE,MAAM,GAC5B,MAAM,CAGR;AAED,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACtC,gEAAgE;IAChE,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,4DAA4D;IAC5D,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wCAAwC;IACxC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,aAAa,CAAC,EAAE,YAAY,EAAE,CAAC;IAC/B,uEAAuE;IACvE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,2EAA2E;IAC3E,cAAc,CAAC,EAAE,wBAAwB,EAAE,CAAC;IAC5C,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yGAAyG;IACzG,eAAe,EAAE,eAAe,CAAC;IACjC,iFAAiF;IACjF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,+BAA+B,CAAC;IACnD,qGAAqG;IACrG,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,uHAAuH;IACvH,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,0HAA0H;IAC1H,UAAU,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,sGAAsG;IACtG,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,0EAA0E;IAC1E,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;IAC1C,kEAAkE;IAClE,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,aAAa,GAAG,oBAAoB,CAAC;IAC3C,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,iBAAiB,GAAG,aAAa,GAAG,WAAW,GAAG,aAAa,GAAG,KAAK,CAAC,CAAC;CAC5G;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAGzG;AAED,qBAAa,eAAe;IA2Nf,OAAO,CAAC,OAAO;IA1N3B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAQ;IACrD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,uBAAuB,CAAO;IAEtD,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,gBAAgB,CAA8C;IACtE,OAAO,CAAC,0BAA0B,CAAU;IAC5C,OAAO,CAAC,EAAE,CAAM;IAChB,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,sBAAsB,CAAY;IAC1C,OAAO,CAAC,wBAAwB,CAAY;IAC5C,OAAO,CAAC,eAAe,CAAY;IACnC,OAAO,CAAC,uBAAuB,CAAY;IAC3C,OAAO,CAAC,qBAAqB,CAAY;IACzC,OAAO,CAAC,oBAAoB,CAAY;IACxC,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqC;IACtE,OAAO,CAAC,oBAAoB,CAAqB;IACjD,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,sBAAsB,CAA4B;IAC1D,OAAO,CAAC,oBAAoB,CAAmC;IAC/D,OAAO,CAAC,4BAA4B,CAAqC;IACzE,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,iBAAiB,CAAY;IAErC,OAAO,CAAC,UAAU,CAAY;IAE9B,OAAO,CAAC,UAAU,CAAY;IAC9B,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,eAAe,CAAY;IACnC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,kBAAkB,CAAqB;IAE/C,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAClD,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAAC,CAAqC;IAC7D,OAAO,CAAC,sBAAsB,CAAiC;IAC/D,OAAO,CAAC,uBAAuB,CAA0B;IACzD,OAAO,CAAC,2BAA2B,CAA0B;IAC7D,OAAO,CAAC,yBAAyB,CAAK;IACtC,OAAO,CAAC,uBAAuB,CAAK;IACpC,OAAO,CAAC,0BAA0B,CAAwD;IAC1F,OAAO,CAAC,QAAQ,CAAC,6BAA6B,CAAsC;IACpF,OAAO,CAAC,0BAA0B,CAA2D;IAC7F,OAAO,CAAC,iBAAiB,CAA+C;IACxE,OAAO,CAAC,gBAAgB,CAAiC;IACzD,OAAO,CAAC,cAAc,CAAiC;IACvD,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,uBAAuB,CAAiD;IAChF,OAAO,CAAC,gBAAgB,CAAiC;IAEzD,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,YAAY,CAAyC;IAC7D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyB;IACzD,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,gBAAgB,CAA6B;IACrD,OAAO,CAAC,yBAAyB,CAA6B;IAC9D,OAAO,CAAC,oBAAoB,CAAmC;IAC/D,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,4BAA4B,CAAS;IAC7C,OAAO,CAAC,UAAU,CAAyC;IAC3D,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA8B;IAG9D,OAAO,CAAC,yBAAyB,CAAK;IAEtC,OAAO,CAAC,mBAAmB,CAA+C;IAC1E,OAAO,CAAC,QAAQ,CAAC,0BAA0B,CAAiB;IAC5D,OAAO,CAAC,mBAAmB,CAAmC;IAE9D,OAAO,CAAC,sBAAsB,CAAK;IACnC,OAAO,CAAC,kBAAkB,CAAwD;IAClF,OAAO,CAAC,kBAAkB,CAA+B;IACzD,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,iBAAiB,CAAwD;IACjF,OAAO,CAAC,iCAAiC,CAAS;IAElD,OAAO,CAAC,gBAAgB,CAAiC;IACzD,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,oBAAoB,CAAmD;IAC/E,OAAO,CAAC,aAAa,CAAyC;IAE9D,OAAO,CAAC,kBAAkB,CAAoD;IAC9E,OAAO,CAAC,gBAAgB,CAA2C;IACnE,OAAO,CAAC,qBAAqB,CAAoC;IACjE,OAAO,CAAC,iBAAiB,CAA+C;IACxE,OAAO,CAAC,iBAAiB,CAA0C;IACnE,OAAO,CAAC,oBAAoB,CAAqB;IAKjD,OAAO,CAAC,gBAAgB,CAAwE;IAGhG,OAAO,CAAC,yBAAyB,CAAqC;IAItE,OAAO,CAAC,yBAAyB,CAAqB;IAGtD,OAAO,CAAC,mBAAmB,CAAiD;IAC5E,OAAO,CAAC,qBAAqB,CAAgC;IAG7D,OAAO,CAAC,iBAAiB,CAAoC;IAC7D,OAAO,CAAC,sBAAsB,CAAK;IACnC,OAAO,CAAC,mBAAmB,CAAoC;IAE/D,OAAO,CAAC,YAAY,CAA6C;IACjE,OAAO,CAAC,qBAAqB,CAA6C;IAC1E,OAAO,CAAC,4BAA4B,CAA+C;IACnF,OAAO,CAAC,oBAAoB,CAAqB;IACjD,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,mBAAmB,CAA0D;IACrF,OAAO,CAAC,mBAAmB,CAAwC;IAGnE,OAAO,CAAC,mBAAmB,CAAsB;IACjD,OAAO,CAAC,iBAAiB,CAA2D;IACpF,OAAO,CAAC,SAAS,CAAqB;IAEtC,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,iBAAiB,CAAS;IAElC,OAAO,CAAC,iBAAiB,CAAS;IAElC,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,kBAAkB,CAAqC;IAC/D,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,sBAAsB,CAA8B;IAC5D,OAAO,CAAC,6BAA6B,CAAqB;IAC1D,OAAO,CAAC,yBAAyB,CAAK;IACtC,OAAO,CAAC,8BAA8B,CAAK;IAC3C,OAAO,CAAC,+BAA+B,CAA4E;IACnH,OAAO,CAAC,eAAe,CAAmC;IAC1D,OAAO,CAAC,0BAA0B,CAA8C;IAChF,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,UAAU,CAAkC;IACpD,OAAO,CAAC,uBAAuB,CAA4B;IAC3D,OAAO,CAAC,yBAAyB,CAAS;IAC1C,OAAO,CAAC,gBAAgB,CAAwC;IAChE,OAAO,CAAC,sBAAsB,CAA4B;IAC1D,OAAO,CAAC,4BAA4B,CAA4C;IAOhF,OAAO,CAAC,YAAY,CAAmC;IACvD,OAAO,CAAC,iBAAiB,CAAK;IAE9B,OAAO,CAAC,WAAW,CAAC,CAAa;IACjC,OAAO,CAAC,qBAAqB,CAAyB;IAEtD,OAAO,CAAC,oBAAoB,CAAiC;IAC7D,OAAO,CAAC,YAAY,CAAiC;IAErD,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,cAAc,CAAyC;IAC/D,OAAO,CAAC,6BAA6B,CAA0C;IAG/E,OAAO,CAAC,eAAe,CAA6D;IACpF,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwB;IACvD,OAAO,CAAC,4BAA4B,CAAS;IAC7C,OAAO,CAAC,kBAAkB,CAAoC;IAC9D,OAAO,CAAC,gBAAgB,CAAqB;IAE7C,OAAO,CAAC,iBAAiB,CAAS;IAElC,OAAO,CAAC,iBAAiB,CAAqD;IAC9E,OAAO,CAAC,cAAc,CAAkD;IACxE,OAAO,CAAC,eAAe,CAAmD;IAC1E,OAAO,CAAC,mCAAmC,CAAyB;IACpE,OAAO,CAAC,mCAAmC,CAAkD;IAE7F,OAAO,CAAC,qBAAqB,CAAuD;IACpF,OAAO,CAAC,qBAAqB,CAAuD;IACpF,OAAO,CAAC,oBAAoB,CAAa;IACzC,OAAO,CAAC,oBAAoB,CAAa;IAGzC,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,YAAY,CAAqB;IAEzC,OAAO,CAAC,YAAY,CAA6D;IAEjF,OAAO,CAAC,eAAe,CAAY;IAEnC,OAAO,CAAC,aAAa,CAAoC;IAEzD,OAAO,CAAC,YAAY,CAA6D;IAEjF,OAAO,CAAC,mBAAmB;IAM3B,OAAO,KAAK,eAAe,GAE1B;IACD,OAAO,KAAK,aAAa,GAExB;IAED,YAAoB,OAAO,EAAE,sBAAsB,EAgFlD;IAED,OAAO,KAAK,WAAW,GAEtB;IAED,OAAO,KAAK,WAAW,QAEtB;IAED,OAAO,CAAC,kBAAkB;IAa1B,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,yBAAyB;IA0BjC,OAAO,CAAC,kCAAkC;IAU1C,OAAO,CAAC,wBAAwB;IA8BhC,OAAO,CAAC,0BAA0B;IAKlC,OAAO,CAAC,oCAAoC;IAgB5C,OAAO,CAAC,wBAAwB;IAIhC,OAAO,CAAC,8BAA8B;IA2EtC,OAAO,CAAC,yBAAyB;IAa3B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA8G1B;IAED,OAAO,CAAC,mBAAmB;IAUrB,GAAG,IAAI,OAAO,CAAC,wBAAwB,CAAC,CA8M7C;IAED,OAAO,CAAC,6BAA6B;IAarC,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,iCAAiC;IAIzC,OAAO,CAAC,mBAAmB;YAMb,oBAAoB;YAiCpB,4BAA4B;YAQ5B,iBAAiB;IA8B/B,OAAO,CAAC,4BAA4B;IAOpC,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,0BAA0B;IAMlC,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,wBAAwB;IAIhC,OAAO,CAAC,YAAY;IA6BpB,OAAO,CAAC,mBAAmB;IAU3B,OAAO,CAAC,4BAA4B;IAcpC,OAAO,CAAC,wBAAwB;IAqBhC,OAAO,CAAC,6BAA6B;IAOrC,OAAO,CAAC,kCAAkC;IA2B1C,OAAO,CAAC,yBAAyB;IAgCjC,OAAO,CAAC,oBAAoB;IA6B5B,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,eAAe;IAKvB,OAAO,CAAC,gBAAgB;IAqCxB,OAAO,CAAC,iBAAiB;IAkCzB,OAAO,CAAC,qBAAqB;IAiB7B,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,iBAAiB;IA8DzB,OAAO,CAAC,mBAAmB;YAyOb,4BAA4B;IAiF1C,OAAO,CAAC,oBAAoB;YAkBd,sBAAsB;IAIpC,OAAO,CAAC,sBAAsB;YAiBhB,wBAAwB;IAetC,OAAO,CAAC,uBAAuB;IAsB/B,OAAO,CAAC,qBAAqB;IAK7B,OAAO,CAAC,sBAAsB;IAmB9B,OAAO,CAAC,4BAA4B;IAcpC,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,2BAA2B;YAgBrB,6BAA6B;IAoB3C,OAAO,CAAC,8BAA8B;IA+CtC,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,mBAAmB;IAY3B,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,oBAAoB;IAY5B,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,yBAAyB;IAsBjC,OAAO,CAAC,mBAAmB;YAIb,oBAAoB;YA0BpB,uBAAuB;IAQrC,OAAO,CAAC,8BAA8B;IAmDtC,OAAO,CAAC,qBAAqB;YAOf,yBAAyB;YAazB,sCAAsC;YAStC,qBAAqB;IAsCnC,OAAO,CAAC,uBAAuB;YAIjB,kBAAkB;IAahC,OAAO,CAAC,0BAA0B;IAMlC,OAAO,CAAC,4BAA4B;YAUtB,+BAA+B;IAsD7C,OAAO,CAAC,6BAA6B;YA6BvB,sBAAsB;IAmBpC,OAAO,CAAC,uBAAuB;IA+C/B,OAAO,CAAC,kBAAkB;IAK1B,OAAO,CAAC,uBAAuB;IAyB/B,OAAO,CAAC,mBAAmB;IAU3B,OAAO,CAAC,oBAAoB;IAoB5B,OAAO,CAAC,0BAA0B;IAIlC,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,4BAA4B;IAiBpC,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,4BAA4B;IAoBpC,OAAO,CAAC,eAAe;IAiCvB,OAAO,CAAC,4BAA4B;IAgBpC,OAAO,CAAC,6BAA6B;IAWrC,OAAO,CAAC,yBAAyB;IAKjC,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,qBAAqB;IAc7B,OAAO,CAAC,kBAAkB;IAY1B,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,uBAAuB;IAS/B,OAAO,CAAC,qBAAqB;IA6B7B,8EAA8E;IAC9E,OAAO,CAAC,iBAAiB;IAazB,OAAO,CAAC,gBAAgB;IAOxB,yEAAyE;IACzE,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,iBAAiB;IAgCzB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,mBAAmB;IAM3B,OAAO,CAAC,sBAAsB;IAa9B,OAAO,CAAC,kBAAkB;IAwC1B,OAAO,CAAC,qBAAqB;IAY7B,OAAO,CAAC,gBAAgB;IAiCxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAM;IAE9C,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,WAAW;IAmBnB,OAAO,CAAC,qBAAqB;IAuB7B,OAAO,CAAC,kBAAkB;IA0B1B,OAAO,CAAC,kBAAkB;IAsC1B,OAAO,CAAC,iCAAiC;IAWzC,OAAO,CAAC,oCAAoC;IAO5C,OAAO,CAAC,wBAAwB;IAyDhC,OAAO,CAAC,qBAAqB;IAwC7B,OAAO,CAAC,qBAAqB;YASf,oBAAoB;YASpB,0BAA0B;IAQxC,OAAO,CAAC,kBAAkB;IAwC1B,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,mBAAmB;IAwB3B,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,wBAAwB;IAgGhC,OAAO,CAAC,mBAAmB;YAWb,mBAAmB;IA8EjC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAkB1B,OAAO,CAAC,gBAAgB;IAgExB,OAAO,CAAC,uBAAuB;IAW/B,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,wBAAwB;IAShC,OAAO,CAAC,uBAAuB;IAY/B,OAAO,CAAC,iBAAiB;IAiBzB,OAAO,CAAC,+BAA+B;IAoBvC,OAAO,CAAC,oBAAoB;IAkB5B,OAAO,CAAC,yBAAyB;IA0DjC,OAAO,CAAC,oBAAoB;YAWd,yBAAyB;IAqCvC;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAO3B;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAoB1B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,kBAAkB;YAIZ,kBAAkB;IA+ChC,OAAO,CAAC,uBAAuB;IAgB/B,OAAO,CAAC,sBAAsB;IAgC9B,OAAO,CAAC,iBAAiB;IA2BzB,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,iBAAiB;YAeX,oBAAoB;IAUlC,OAAO,CAAC,wBAAwB;IA2dhC,OAAO,CAAC,gBAAgB;YAiEV,kCAAkC;IAsChD,OAAO,CAAC,oCAAoC;IAS5C,OAAO,CAAC,yCAAyC;YAenC,mCAAmC;YAiHnC,WAAW;IA8azB,OAAO,CAAC,8BAA8B;IAkBtC,OAAO,CAAC,iCAAiC;IAYzC,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,mBAAmB;IAc3B,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,2BAA2B;IAInC,OAAO,CAAC,wBAAwB;IAUhC,OAAO,CAAC,wBAAwB;IA+BhC,OAAO,CAAC,gBAAgB;IAmCxB,OAAO,CAAC,sBAAsB;IAY9B,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,sBAAsB;IAW9B,OAAO,CAAC,qBAAqB;IAU7B,OAAO,CAAC,sBAAsB;IAS9B,OAAO,CAAC,yBAAyB;IAYjC,OAAO,CAAC,sBAAsB;IAO9B,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,oBAAoB;YAOd,oBAAoB;IASlC,OAAO,CAAC,+BAA+B;IAsBvC,OAAO,CAAC,oBAAoB;IAY5B,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,qBAAqB;IAS7B,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,qBAAqB;IAO7B,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,gBAAgB;IAoBxB,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,kCAAkC;IAkB1C,OAAO,CAAC,qBAAqB;IAU7B,OAAO,CAAC,gCAAgC;IAWxC,OAAO,CAAC,gCAAgC;IAWxC;;;;;OAKG;IACH,OAAO,CAAC,UAAU;YAoBJ,uBAAuB;IAarC,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,yBAAyB;IAUjC,OAAO,CAAC,qCAAqC;IAkC7C,OAAO,CAAC,gBAAgB;IA2HxB;;;;;;;OAOG;IACH,OAAO,CAAC,0BAA0B;YAoBpB,oBAAoB;IA4H5B,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAuB3C;YAEa,mCAAmC;IAYjD,OAAO,CAAC,uCAAuC;IAczC,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAUhD;YAEa,uBAAuB;IAKrC,OAAO,CAAC,YAAY;IAqBpB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,qBAAqB;IAwB7B,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,kBAAkB;IAe1B,OAAO,CAAC,sBAAsB;IAI9B,OAAO,CAAC,WAAW;IAInB;;;;OAIG;IACH,OAAO,CAAC,cAAc,CAAS;YAEjB,QAAQ;IA0BtB;;;;;OAKG;IACG,iBAAiB,CAAC,OAAO,GAAE;QAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAKpF;IAED,OAAO,CAAC,gBAAgB;YAYV,iBAAiB;YAQjB,kBAAkB;IAyBhC,OAAO,CAAC,qBAAqB;YAYf,sBAAsB;IAKpC,OAAO,CAAC,sBAAsB;IAgC9B,OAAO,CAAC,wBAAwB;IAOhC,OAAO,CAAC,WAAW;YAyCL,cAAc;IAgC5B,OAAO,CAAC,oBAAoB;IAQ5B,iGAAiG;IACjG,OAAO,CAAC,oBAAoB;IAS5B,OAAO,CAAC,kBAAkB;IAkD1B;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAkH3B;;;OAGG;IACH,OAAO,CAAC,yBAAyB;IASjC,OAAO,CAAC,+BAA+B;IASvC,OAAO,CAAC,uBAAuB;IAe/B,OAAO,CAAC,uBAAuB;IAO/B,OAAO,CAAC,0BAA0B;IAIlC,OAAO,CAAC,uBAAuB;IAI/B,kFAAkF;IAClF,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,iBAAiB;IAiBzB,OAAO,CAAC,yBAAyB;IAIjC,OAAO,CAAC,2BAA2B;IAKnC,OAAO,CAAC,uBAAuB;IAK/B,OAAO,CAAC,gBAAgB;IAKxB,yFAAyF;IACzF,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,kBAAkB;IA2B1B,OAAO,CAAC,6BAA6B;IAqBrC,OAAO,CAAC,kBAAkB;IAoD1B,OAAO,CAAC,aAAa;IASrB,SAAS,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAIpC;IAED,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAIxC;IAED,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAGnD;IAED,6BAA6B,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAGtD;IAED,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,4BAA4B;IAoCpC,OAAO,CAAC,0BAA0B;IAQlC;;;OAGG;IACH,OAAO,CAAC,YAAY;IAapB,OAAO,CAAC,mBAAmB;YAIb,oBAAoB;YA6KpB,kBAAkB;YAsBlB,mBAAmB;YAkBnB,kBAAkB;YAyBlB,sBAAsB;YAQtB,6BAA6B;IA0B3C,OAAO,CAAC,yBAAyB;IAIjC,OAAO,CAAC,2BAA2B;YAMrB,4BAA4B;YA0B5B,yBAAyB;IAKvC,OAAO,CAAC,wBAAwB;IAWhC,OAAO,CAAC,8BAA8B;IAgBtC,OAAO,CAAC,gCAAgC;IAKxC,OAAO,CAAC,0BAA0B;YAOpB,sCAAsC;YAKtC,kBAAkB;IAahC,OAAO,CAAC,2BAA2B;YAyBrB,4BAA4B;YAM5B,uCAAuC;IAiBrD,OAAO,CAAC,0BAA0B;IAMlC,OAAO,CAAC,2BAA2B;IAenC,OAAO,CAAC,+BAA+B;IAUvC,OAAO,CAAC,4BAA4B;IAKpC,OAAO,CAAC,iBAAiB;IA0CzB,OAAO,CAAC,mBAAmB;IAkB3B,OAAO,CAAC,oBAAoB;IAuB5B,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,qBAAqB;YAmIf,kBAAkB;YA8ElB,uBAAuB;YA+CvB,kBAAkB;YAsBlB,gBAAgB;YAkIhB,mBAAmB;YAwBnB,mBAAmB;IAyCjC,OAAO,CAAC,oBAAoB;IAiD5B,OAAO,CAAC,eAAe;YAmBT,kCAAkC;YAuClC,gBAAgB;YA8DhB,oBAAoB;YAapB,kBAAkB;YASlB,mBAAmB;YAiHnB,mBAAmB;YAsFnB,mBAAmB;IAgBjC,OAAO,CAAC,sBAAsB;YA6BhB,mBAAmB;YA8CnB,kBAAkB;YA8FlB,iBAAiB;YAejB,iBAAiB;YAoBjB,wBAAwB;YAmDxB,oBAAoB;IAuBlC,OAAO,CAAC,iBAAiB;YAiCX,yBAAyB;IAWvC,OAAO,CAAC,uBAAuB;YAwBjB,sBAAsB;YAWtB,mBAAmB;IAwBjC,OAAO,CAAC,kBAAkB;YAiCZ,eAAe;YAkBf,mBAAmB;YAwInB,oBAAoB;YAgBpB,sBAAsB;YA+DtB,oBAAoB;IAyBlC,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,+BAA+B;YA4BzB,eAAe;IAqB7B,OAAO,CAAC,aAAa;IA0BrB,OAAO,CAAC,sBAAsB;IAqB9B,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,gBAAgB;IA8BxB,OAAO,CAAC,eAAe;IA0HvB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,oBAAoB;IAQ5B,OAAO,CAAC,kBAAkB;YAQZ,kBAAkB;YAqClB,kBAAkB;IAqChC,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,qBAAqB;IAM7B,IAAI,CAAC,OAAO,GAAE;QAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,IAAI,CA0BxD;CACD","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { AgentMessage, ThinkingLevel } from \"@asm-agent/agent\";\nimport {\n\ttype Api,\n\ttype AssistantMessage,\n\ttype ImageContent,\n\ttype Message,\n\ttype Model,\n\ttype ServiceTier,\n\tsupportsFastMode,\n\ttype ToolCall,\n} from \"@asm-agent/ai\";\nimport { BUILTIN_MCP_CATALOG } from \"@asm-agent/ai/mcp\";\nimport type {\n\tAutocompleteItem,\n\tAutocompleteProvider,\n\tEditorComponent,\n\tKeybinding,\n\tKeyId,\n\tMarkdownTheme,\n\tOverlayHandle,\n\tOverlayOptions,\n\tSlashCommand,\n} from \"@asm-agent/tui\";\nimport {\n\tCombinedAutocompleteProvider,\n\ttype Component,\n\tContainer,\n\tLoader,\n\ttype LoaderIndicatorOptions,\n\tMarkdown,\n\tmatchesKey,\n\tProcessTerminal,\n\tSpacer,\n\tsetKeybindings,\n\tText,\n\tTruncatedText,\n\tTUI,\n\ttruncateToWidth,\n\tvisibleWidth,\n} from \"@asm-agent/tui\";\nimport { spawn, spawnSync } from \"child_process\";\nimport {\n\tbuildDaemonUpdateRestartReport,\n\tlaunchDaemonUpdateRestartCoordinator,\n\tresolveDaemonUpdateRestartSocketPath,\n} from \"../../cli/daemon-update-restart.js\";\nimport {\n\tAPP_NAME,\n\tAPP_TITLE,\n\tgetAgentDir,\n\tgetAgentTracesLogPath,\n\tgetDebugLogPath,\n\tgetLogsDir,\n\tgetShareViewerUrl,\n\tSELF_UPDATE_INTERACTIVE_CHILD_ENV,\n\tSELF_UPDATE_NOT_ATTEMPTED_EXIT_CODE,\n\tVERSION,\n} from \"../../config.js\";\nimport {\n\tAGENT_MESSAGE_RECEIVED_PREVIEW_LABEL,\n\tisAgentSessionMessage,\n\tstartsAgentRun,\n} from \"../../core/agent-messages.js\";\nimport {\n\ttype AgentTracePreviewResult,\n\ttype AgentTraceUploadAllResult,\n\ttype AgentTraceUploadResult,\n\tgetAsmAgentTraceCredential,\n\tpreviewAgentTraceFile,\n\tuploadAgentTraceFile,\n\tuploadAllAgentTraces,\n} from \"../../core/agent-traces.js\";\nimport { isNoModelsAvailableMessage } from \"../../core/auth-guidance.js\";\nimport {\n\ttype AgentCronJob,\n\ttype AgentHeartbeatManagementAction,\n\tDEFAULT_HEARTBEAT_DELIVERY_MODE,\n\tparseHeartbeatCommand,\n} from \"../../core/cron-jobs.js\";\nimport type {\n\tAutocompleteProviderFactory,\n\tContextUsage,\n\tEditorFactory,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tExtensionRunner,\n\tExtensionUIContext,\n\tExtensionUIDialogOptions,\n\tExtensionWidgetOptions,\n} from \"../../core/extensions/index.js\";\nimport { FooterDataProvider, type ReadonlyFooterDataProvider } from \"../../core/footer-data-provider.js\";\nimport { emptyGoalState, formatGoalUsage, GOAL_CONTEXT_PREVIEW_LABEL, type GoalState } from \"../../core/goals.js\";\nimport type { KernelSentAgentMessage } from \"../../core/kernel/index.js\";\nimport { type AppKeybinding, KeybindingsManager } from \"../../core/keybindings.js\";\nimport { runMcpManagementCommand } from \"../../core/mcp/mcp-command.js\";\nimport {\n\tbashOutputToText,\n\tCOMPACTION_OUTCOME_CUSTOM_TYPE,\n\ttype CustomMessage,\n\tcreateHeartbeatPromptMessage,\n\tHEARTBEAT_PROMPT_PREVIEW_LABEL,\n\tisCompactionOutcomeMessage,\n\tisRefinementOutcomeMessage,\n\tisSessionSlashCommandMessage,\n\tisSessionSlashCommandResultMessage,\n\tREFINEMENT_OUTCOME_CUSTOM_TYPE,\n\tSESSION_SLASH_COMMAND_CUSTOM_TYPE,\n\tSESSION_SLASH_COMMAND_RESULT_CUSTOM_TYPE,\n} from \"../../core/messages.js\";\nimport { findExactModelReferenceMatch, resolveModelScopeFromModels } from \"../../core/model-resolver.js\";\nimport { parseNewSessionCommand } from \"../../core/new-session-command.js\";\nimport { resolveAsmAgentTracesBaseUrl } from \"../../core/prime-inference-auth.js\";\nimport { resolvePrimeInferencePostLoginModelAction } from \"../../core/prime-inference-model-selection.js\";\nimport { parseCommandArgs } from \"../../core/prompt-templates.js\";\nimport { formatMissingSessionCwdPrompt, MissingSessionCwdError } from \"../../core/session-cwd.js\";\nimport { SessionImportFileNotFoundError } from \"../../core/session-import-errors.js\";\nimport { resolveSessionPath, SessionSelectorError, SessionSelectorNotFoundError } from \"../../core/session-resolver.js\";\nimport { parseSkillBlock } from \"../../core/skill-blocks.js\";\nimport {\n\tBUILTIN_SLASH_COMMANDS,\n\tbuiltinSlashCommandTakesArgument,\n\tisBuiltinSlashCommandName,\n\tparseSlashCommand,\n\tresolveBuiltinSlashCommandName,\n} from \"../../core/slash-commands.js\";\nimport {\n\tcaptureAgentCommandUsed,\n\tcaptureOnboardingCompleted,\n\ttype TelemetryOnboardingOutcome,\n} from \"../../core/telemetry.js\";\nimport { type TruncationResult, truncateTail } from \"../../core/tools/truncate.js\";\nimport { ASM_LOGO } from \"../../themes/asm-logo.js\";\nimport { getChangelogPath, parseChangelog } from \"../../utils/changelog.js\";\nimport { copyToClipboard } from \"../../utils/clipboard.js\";\nimport { readClipboardImage } from \"../../utils/clipboard-image.js\";\nimport { parseGitUrl } from \"../../utils/git.js\";\nimport { resizeImage } from \"../../utils/image-resize.js\";\nimport { getCwdRelativePath } from \"../../utils/paths.js\";\nimport { killTrackedDetachedChildren } from \"../../utils/shell.js\";\nimport { ensureTool, ensureToolWithStatus, formatMissingRipgrepMessage } from \"../../utils/tools-manager.js\";\nimport { checkForNewPiVersion } from \"../../utils/version-check.js\";\nimport type {\n\tAgentConnection,\n\tAgentConnectionExtensionUiRequest,\n\tAgentConnectionExtensionUiResponse,\n\tAgentConnectionHeartbeat,\n\tAgentConnectionModel,\n\tAgentConnectionModelCatalog,\n\tAgentConnectionQueuedMessageMutationStatus,\n\tAgentConnectionQueueState,\n\tAgentConnectionResourceDiagnostic,\n\tAgentConnectionResourceSnapshot,\n\tAgentConnectionRlmChildAgentSnapshot,\n\tAgentConnectionSessionContext,\n\tAgentConnectionSessionEvent,\n\tAgentConnectionSessionTreeNode,\n\tAgentConnectionSideQuestionEvent,\n\tAgentConnectionSlashCommand,\n\tAgentConnectionSnapshot,\n\tAgentConnectionSourceInfo,\n\tAgentConnectionState,\n\tAgentConnectionToolDefinition,\n} from \"../agent-connection/index.js\";\nimport { AgentConnectionPromptAdmissionError } from \"../agent-connection/index.js\";\nimport { getModelArgumentCompletions } from \"../model-autocomplete.js\";\nimport {\n\tcheckForPackageUpdates,\n\tcheckTmuxKeyboardSetup,\n\tformatPackageUpdateNotice,\n\tformatUpdateAvailableNotice,\n} from \"../shared/startup-notices.js\";\nimport { AGENT_ACTIVITY_LABELS, AgentActivityTracker, formatTokenCount } from \"./agent-activity.js\";\nimport { type AuthenticationResult, getAnthropicSubscriptionAuthWarning, ProviderAuthFlows } from \"./auth-flows.js\";\nimport { AgentMessageComponent } from \"./components/agent-message.js\";\nimport { ArminComponent } from \"./components/armin.js\";\nimport { AssistantMessageComponent } from \"./components/assistant-message.js\";\nimport { BashExecutionComponent } from \"./components/bash-execution.js\";\nimport { BorderedLoader } from \"./components/bordered-loader.js\";\nimport { BranchSummaryMessageComponent } from \"./components/branch-summary-message.js\";\nimport { type FullPaneOverlayOptions, showFullPaneOverlay } from \"./components/centered-overlay.js\";\nimport {\n\tCompactionOutcomeMessageComponent,\n\tMalformedCompactionOutcomeMessageComponent,\n} from \"./components/compaction-outcome-message.js\";\nimport { CompactionSummaryMessageComponent } from \"./components/compaction-summary-message.js\";\nimport { ConfigurationMenuComponent, type ConfigurationMenuTab } from \"./components/configuration-menu.js\";\nimport { formatContextTree } from \"./components/context-tree-format.js\";\nimport { isCompactAgentMessageNeighbor } from \"./components/conversation-components.js\";\nimport { CountdownTimer } from \"./components/countdown-timer.js\";\nimport { CustomEditor } from \"./components/custom-editor.js\";\nimport { CustomMessageComponent } from \"./components/custom-message.js\";\nimport { DaxnutsComponent } from \"./components/daxnuts.js\";\nimport { DynamicBorder } from \"./components/dynamic-border.js\";\nimport { EarendilAnnouncementComponent } from \"./components/earendil-announcement.js\";\nimport { type FileChangeSummary, formatTotalChangeSummary, mergeTurnFileChanges } from \"./components/edit-summary.js\";\nimport { ExtensionEditorComponent } from \"./components/extension-editor.js\";\nimport { ExtensionInputComponent } from \"./components/extension-input.js\";\nimport { ExtensionSelectorComponent } from \"./components/extension-selector.js\";\nimport { FEATURE_HINT_ANIMATION_INTERVAL_MS, FeatureHintComponent } from \"./components/feature-hint.js\";\nimport { FooterComponent } from \"./components/footer.js\";\nimport { HeartbeatManagerComponent } from \"./components/heartbeat-manager.js\";\nimport { InjectedPromptMessageComponent, isInjectedPromptMessage } from \"./components/injected-prompt-message.js\";\nimport { formatKeyText, keyHint, keyText, rawKeyHint } from \"./components/keybinding-hints.js\";\nimport type { AuthSelectorProvider } from \"./components/oauth-selector.js\";\nimport { OnboardingSplashComponent } from \"./components/onboarding-splash.js\";\nimport {\n\tMalformedRefinementOutcomeMessageComponent,\n\tRefinementOutcomeMessageComponent,\n} from \"./components/refinement-outcome-message.js\";\nimport { ScopedModelsSelectorComponent } from \"./components/scoped-models-selector.js\";\nimport { SettingsSelectorComponent } from \"./components/settings-selector.js\";\nimport { SideQuestionComponent } from \"./components/side-question.js\";\nimport { SkillInvocationMessageComponent } from \"./components/skill-invocation-message.js\";\nimport {\n\tisLeadingSlashCommand,\n\tSlashCommandMessageComponent,\n\tstyleSlashCommandText,\n} from \"./components/slash-command-message.js\";\nimport { SlashCommandResultMessageComponent } from \"./components/slash-command-result-message.js\";\nimport { countDirectSubagentStatuses, SubagentSummaryLine } from \"./components/subagent-summary-line.js\";\nimport { ThinkingSelectorComponent } from \"./components/thinking-selector.js\";\nimport {\n\tselectLatestToolExpandHint,\n\tToolExecutionComponent,\n\ttype ToolExecutionDefinition,\n} from \"./components/tool-execution.js\";\nimport { TreeSelectorComponent } from \"./components/tree-selector.js\";\nimport { UserMessageComponent } from \"./components/user-message.js\";\nimport { UserMessageSelectorComponent } from \"./components/user-message-selector.js\";\nimport { FeatureHintDeck } from \"./feature-hints.js\";\nimport { scopeHeartbeatsToSession } from \"./heartbeat-scope.js\";\nimport {\n\tcollectMarkedImages,\n\tevictImagesToBudget,\n\tformatImageMarker,\n\timageMarkerIds,\n\tremapImageMarkers,\n} from \"./image-markers.js\";\nimport type {\n\tInteractiveModeLocalSessionHost,\n\tInteractiveModeLocalToolRendererDefinition,\n\tInteractiveModeUiServices,\n} from \"./interactive-mode-services.js\";\nimport {\n\tisOnboardingModelReady,\n\ttype OnboardingStartupState,\n\tshouldRunOnboarding,\n\tshouldRunPrimeCliOnboardingSplash,\n} from \"./onboarding.js\";\nimport type { ClientPromptStashStore, PromptStash, PromptStashState } from \"./prompt-stash-state.js\";\nimport { QueueSelection } from \"./queue-selection.js\";\nimport { formatResumeHint } from \"./resume-hint.js\";\nimport {\n\tgetAvailableThemes,\n\tgetAvailableThemesWithPaths,\n\tgetEditorTheme,\n\tgetMarkdownTheme,\n\tgetThemeByName,\n\tinitTheme,\n\tonThemeChange,\n\tsetRegisteredThemes,\n\tsetTheme,\n\tsetThemeInstance,\n\tstopThemeWatcher,\n\tTheme,\n\ttype ThemeColor,\n\ttheme,\n} from \"./theme/theme.js\";\nimport { setWorkingPulseFrame, WORKING_ICON_INTERVAL_MS } from \"./theme/working-icon.js\";\n\ninterface Expandable {\n\tsetExpanded(expanded: boolean): void;\n}\n\ninterface PendingToolCallRenderInput {\n\tid: string;\n\tname: string;\n\targuments: ToolCall[\"arguments\"];\n}\n\nconst HEARTBEAT_LEGACY_PROMPT_MIN_TOLERANCE_MS = 15_000;\nconst HEARTBEAT_LEGACY_PROMPT_MAX_TOLERANCE_MS = 120_000;\nconst MODEL_CATALOG_REFRESH_TTL_MS = 60_000;\nconst FEATURE_HINT_DELAY_MS = 5_000;\n\nexport const START_HINTS = [\n\t'Try \"refactor @<filepath>\"',\n\t'Try \"fix bugs in @<filepath>\"',\n\t'Try \"add tests for @<filepath>\"',\n\t'Try \"explain how @<filepath> works\"',\n\t'Try \"improve performance in @<filepath>\"',\n] as const;\n\nexport function getRandomStartHint(random = Math.random): (typeof START_HINTS)[number] {\n\treturn START_HINTS[Math.floor(random() * START_HINTS.length)] ?? START_HINTS[0];\n}\n\nfunction isLabeledQueuedPreview(message: string): boolean {\n\treturn (\n\t\tmessage.startsWith(`${HEARTBEAT_PROMPT_PREVIEW_LABEL}: `) ||\n\t\tmessage.startsWith(`${GOAL_CONTEXT_PREVIEW_LABEL}: `) ||\n\t\tmessage.startsWith(`${AGENT_MESSAGE_RECEIVED_PREVIEW_LABEL}: `)\n\t);\n}\n\nexport function formatQueuedMessagePreview(message: string, label: \"Steering\" | \"Follow-up\"): string {\n\treturn isLabeledQueuedPreview(message) ? message : `${label}: ${message}`;\n}\n\nexport function styleQueuedMessagePreview(\n\tmessage: string,\n\tlabel: \"Steering\" | \"Follow-up\",\n\tisRecognizedSlashCommand: (name: string) => boolean,\n): string {\n\tconst preview = formatQueuedMessagePreview(message, label);\n\tif (!isLeadingSlashCommand(message, isRecognizedSlashCommand)) return theme.fg(\"dim\", preview);\n\tconst prefix = preview.slice(0, preview.length - message.length);\n\treturn `${theme.fg(\"dim\", prefix)}${styleSlashCommandText(message, (rest) => theme.fg(\"dim\", rest))}`;\n}\n\nfunction isExpandable(obj: unknown): obj is Expandable {\n\treturn typeof obj === \"object\" && obj !== null && \"setExpanded\" in obj && typeof obj.setExpanded === \"function\";\n}\n\ninterface AgentMessagesExpandable {\n\tsetAgentMessagesExpanded(expanded: boolean): void;\n}\n\nfunction hasAgentMessagesExpansion(obj: unknown): obj is AgentMessagesExpandable {\n\treturn (\n\t\ttypeof obj === \"object\" &&\n\t\tobj !== null &&\n\t\t\"setAgentMessagesExpanded\" in obj &&\n\t\ttypeof (obj as AgentMessagesExpandable).setAgentMessagesExpanded === \"function\"\n\t);\n}\n\ninterface EditDiffsExpandable {\n\tsetEditDiffsExpanded(expanded: boolean): void;\n}\n\nfunction hasEditDiffsExpansion(obj: unknown): obj is EditDiffsExpandable {\n\treturn (\n\t\ttypeof obj === \"object\" &&\n\t\tobj !== null &&\n\t\t\"setEditDiffsExpanded\" in obj &&\n\t\ttypeof (obj as EditDiffsExpandable).setEditDiffsExpanded === \"function\"\n\t);\n}\n\nclass ExpandableText extends Text implements Expandable {\n\tconstructor(\n\t\tprivate readonly getCollapsedText: () => string,\n\t\tprivate readonly getExpandedText: () => string,\n\t\texpanded = false,\n\t\tpaddingX = 0,\n\t\tpaddingY = 0,\n\t) {\n\t\tsuper(expanded ? getExpandedText() : getCollapsedText(), paddingX, paddingY);\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.setText(expanded ? this.getExpandedText() : this.getCollapsedText());\n\t}\n}\n\nexport function formatSplashCwd(cwd: string): string {\n\tconst normalized = cwd.replace(/\\\\/g, \"/\");\n\tconst home = os.homedir().replace(/\\\\/g, \"/\");\n\tif (home && normalized === home) {\n\t\treturn \"~\";\n\t}\n\tif (home && normalized.startsWith(`${home}/`)) {\n\t\treturn `~${normalized.slice(home.length)}`;\n\t}\n\n\treturn normalized;\n}\n\nfunction mergeSubagentSnapshot(\n\tprevious: AgentConnectionRlmChildAgentSnapshot,\n\tincoming: AgentConnectionRlmChildAgentSnapshot,\n): AgentConnectionRlmChildAgentSnapshot {\n\tconst active = incoming.status === \"running\" || incoming.status === \"queued\";\n\treturn {\n\t\t...previous,\n\t\t...incoming,\n\t\tparentId: incoming.parentId ?? previous.parentId,\n\t\t// Active updates may omit a previously known daemon session id, but a\n\t\t// terminal update without one means the child is no longer resident.\n\t\tactiveSessionId: active ? (incoming.activeSessionId ?? previous.activeSessionId) : incoming.activeSessionId,\n\t\t// A completed retained child can become active again when it receives a\n\t\t// follow-up. Its RLM run status stays terminal, so activity must remain an\n\t\t// independent projection of the live session state.\n\t\tactivity: active ? (incoming.activity ?? previous.activity) : incoming.activity,\n\t};\n}\n\nexport function truncatePathMiddle(value: string, width: number): string {\n\tif (visibleWidth(value) <= width) {\n\t\treturn value;\n\t}\n\tif (width <= 1) {\n\t\treturn truncateToWidth(value, width, \"\");\n\t}\n\n\tconst ellipsis = \"…\";\n\tconst normalized = value.replace(/\\\\/g, \"/\");\n\tconst prefix = normalized.startsWith(\"~/\") ? \"~/\" : normalized.startsWith(\"/\") ? \"/\" : \"\";\n\tconst body = prefix ? normalized.slice(prefix.length) : normalized;\n\tconst parts = body.split(\"/\").filter((part) => part.length > 0);\n\tconst last = parts.pop() ?? \"\";\n\tconst previous = parts.pop();\n\tconst suffix = previous ? `${previous}/${last}` : last;\n\tconst candidate = `${prefix}${ellipsis}/${suffix}`;\n\tif (visibleWidth(candidate) <= width) {\n\t\treturn candidate;\n\t}\n\n\treturn truncateToWidth(candidate, width);\n}\n\nexport interface BrandSplashMetadataLine {\n\tlabel: string;\n\tvalue: string;\n}\n\nexport interface BrandSplashHeaderOptions {\n\tlogo?: string;\n\ttopPadding?: boolean;\n\tgetExtraMetadata?: () => readonly BrandSplashMetadataLine[];\n\tgetHideStartHint?: () => boolean;\n\tgetStartHint?: () => string;\n}\n\nexport class BrandSplashHeader implements Component {\n\tprivate readonly logoRaw: string[];\n\tprivate readonly logoCanvasWidth: number;\n\tprivate readonly gutter = 4;\n\tprivate readonly labelWidth = 9;\n\n\tconstructor(\n\t\tprivate readonly version: string,\n\t\tprivate readonly getModelId: () => string | undefined,\n\t\tprivate readonly getCwd: () => string,\n\t\tprivate readonly verboseInstructions?: string,\n\t\tprivate readonly options: BrandSplashHeaderOptions = {},\n\t) {\n\t\tthis.logoRaw = (options.logo ?? ASM_LOGO).split(\"\\n\");\n\t\tthis.logoCanvasWidth = this.logoRaw.reduce((max, line) => Math.max(max, visibleWidth(line)), 0);\n\t}\n\n\tinvalidate(): void {\n\t\t// Render output is derived from current theme/session state.\n\t}\n\n\trender(width: number): string[] {\n\t\tconst safeWidth = Math.max(1, width);\n\t\tconst paddingX = safeWidth > 1 ? 1 : 0;\n\t\tconst contentWidth = Math.max(1, safeWidth - paddingX * 2);\n\t\tconst metaWidth = contentWidth - this.logoCanvasWidth - this.gutter;\n\t\tconst showMeta = metaWidth >= this.labelWidth + 8;\n\t\tconst valueWidth = Math.max(1, metaWidth - this.labelWidth);\n\t\tconst labelled = (label: string, value: string) => {\n\t\t\tconst displayValue =\n\t\t\t\tlabel === \"cwd\" ? truncatePathMiddle(value, valueWidth) : truncateToWidth(value, valueWidth);\n\t\t\treturn theme.fg(\"dim\", label.padEnd(this.labelWidth)) + theme.fg(\"muted\", displayValue);\n\t\t};\n\t\tconst extraMetadata = this.options.getExtraMetadata?.() ?? [];\n\t\tconst hideStartHint = this.options.getHideStartHint?.() ?? false;\n\t\tconst startHint = this.options.getStartHint?.() ?? \"type to search sessions\";\n\t\tconst metaLines = showMeta\n\t\t\t? [\n\t\t\t\t\tlabelled(\"version\", `v${this.version}`),\n\t\t\t\t\tlabelled(\"model\", this.getModelId() ?? \"—\"),\n\t\t\t\t\tlabelled(\"cwd\", formatSplashCwd(this.getCwd())),\n\t\t\t\t\t...extraMetadata.map((line) => labelled(line.label, line.value)),\n\t\t\t\t\t...(hideStartHint ? [] : [\"\", theme.fg(\"dim\", startHint)]),\n\t\t\t\t]\n\t\t\t: [];\n\t\tconst metaStart = Math.max(0, Math.floor((this.logoRaw.length - metaLines.length) / 2));\n\t\tconst lines = this.options.topPadding ? [\"\"] : [];\n\t\tlines.push(\n\t\t\t...this.logoRaw.map((line, index) => {\n\t\t\t\tconst colored = theme.fg(\"text\", line);\n\t\t\t\tconst meta = index >= metaStart && index < metaStart + metaLines.length ? metaLines[index - metaStart] : \"\";\n\t\t\t\tconst padding = showMeta\n\t\t\t\t\t? \" \".repeat(Math.max(0, this.logoCanvasWidth - visibleWidth(line) + this.gutter))\n\t\t\t\t\t: \"\";\n\t\t\t\tconst content = truncateToWidth(colored + padding + meta, contentWidth, \"\");\n\t\t\t\treturn (\n\t\t\t\t\t\" \".repeat(paddingX) + content + \" \".repeat(Math.max(0, safeWidth - paddingX - visibleWidth(content)))\n\t\t\t\t);\n\t\t\t}),\n\t\t);\n\n\t\tif (this.verboseInstructions) {\n\t\t\tlines.push(\" \".repeat(safeWidth));\n\t\t\tfor (const instruction of this.verboseInstructions.split(\"\\n\")) {\n\t\t\t\tconst content = truncateToWidth(instruction, contentWidth);\n\t\t\t\tlines.push(\n\t\t\t\t\t\" \".repeat(paddingX) + content + \" \".repeat(Math.max(0, safeWidth - paddingX - visibleWidth(content))),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n\ntype StartupPromptBarrierOutcome = \"admitted\" | \"retained\" | \"lifecycle-cancelled\";\n\ntype GoalAnnouncementSnapshot = {\n\tgoalId?: string;\n\tstatus: GoalState[\"status\"];\n\tobjective?: string;\n\tlastReason?: string;\n\tlastError?: string;\n};\n\ntype ModelFallbackWarningAction = \"show\" | \"suppress\";\n\ninterface OnboardingSplashHandle {\n\tshowProgress(message: string): void;\n\tdismiss(): void;\n}\n\nconst THINKING_LEVEL_DESCRIPTIONS: Record<ThinkingLevel, string> = {\n\toff: \"No reasoning\",\n\tminimal: \"Very brief reasoning\",\n\tlow: \"Light reasoning\",\n\tmedium: \"Moderate reasoning\",\n\thigh: \"Deep reasoning\",\n\txhigh: \"Very deep reasoning\",\n\tmax: \"Maximum reasoning\",\n};\n\nconst HEARTBEAT_ARGUMENT_COMPLETIONS: AutocompleteItem[] = [\n\t{\n\t\tvalue: \"every \",\n\t\tlabel: \"every <duration> <instruction>\",\n\t\tdescription: \"Set an interval, then add an instruction: /heartbeat every 10s Scan the logs\",\n\t},\n\t{\n\t\tvalue: \"--steer \",\n\t\tlabel: \"--steer <instruction>\",\n\t\tdescription: \"Deliver by interrupting the current turn (default)\",\n\t},\n\t{\n\t\tvalue: \"--follow-up \",\n\t\tlabel: \"--follow-up <instruction>\",\n\t\tdescription: \"Deliver as a follow-up after the current turn finishes\",\n\t},\n];\n\nconst DEAD_TERMINAL_ERROR_CODES = new Set([\"EIO\", \"EPIPE\", \"ENOTCONN\"]);\n\n// Cap on retained pasted-image bytes (base64). Images are resized below the\n// inline limit before storing, so this holds many recent pastes; the oldest are\n// evicted past the cap to keep a long session bounded.\nconst MAX_PASTED_IMAGE_BYTES = 64 * 1024 * 1024;\nconst INITIAL_TRANSCRIPT_RENDER_MESSAGE_LIMIT = 400;\n\nfunction initialRenderMessages(messages: AgentMessage[]): AgentMessage[] {\n\tif (messages.length <= INITIAL_TRANSCRIPT_RENDER_MESSAGE_LIMIT) {\n\t\treturn messages;\n\t}\n\tconst toolCallMessages = new Map<string, { index: number; message: Extract<AgentMessage, { role: \"assistant\" }> }>();\n\tfor (const [index, message] of messages.entries()) {\n\t\tif (message.role !== \"assistant\") {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const content of message.content) {\n\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\ttoolCallMessages.set(content.id, { index, message });\n\t\t\t}\n\t\t}\n\t}\n\n\tconst initialStartIndex = messages.length - INITIAL_TRANSCRIPT_RENDER_MESSAGE_LIMIT;\n\tfor (let startIndex = initialStartIndex; startIndex < messages.length; startIndex++) {\n\t\tconst visibleMessages = messages.slice(startIndex);\n\t\tconst visibleToolCallIds = new Set<string>();\n\t\tfor (const message of visibleMessages) {\n\t\t\tif (message.role !== \"assistant\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const content of message.content) {\n\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\tvisibleToolCallIds.add(content.id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst requiredToolCallIdsByMessage = new Map<\n\t\t\tnumber,\n\t\t\t{ message: Extract<AgentMessage, { role: \"assistant\" }>; toolCallIds: Set<string> }\n\t\t>();\n\t\tfor (const message of visibleMessages) {\n\t\t\tif (message.role !== \"toolResult\" || visibleToolCallIds.has(message.toolCallId)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst toolCallMessage = toolCallMessages.get(message.toolCallId);\n\t\t\tif (!toolCallMessage || toolCallMessage.index >= startIndex) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst requiredMessage = requiredToolCallIdsByMessage.get(toolCallMessage.index) ?? {\n\t\t\t\tmessage: toolCallMessage.message,\n\t\t\t\ttoolCallIds: new Set<string>(),\n\t\t\t};\n\t\t\trequiredMessage.toolCallIds.add(message.toolCallId);\n\t\t\trequiredToolCallIdsByMessage.set(toolCallMessage.index, requiredMessage);\n\t\t}\n\n\t\tif (visibleMessages.length + requiredToolCallIdsByMessage.size > INITIAL_TRANSCRIPT_RENDER_MESSAGE_LIMIT) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst requiredToolCallMessages = [...requiredToolCallIdsByMessage.entries()]\n\t\t\t.sort(([leftIndex], [rightIndex]) => leftIndex - rightIndex)\n\t\t\t.map(([, { message, toolCallIds }]) => ({\n\t\t\t\t...message,\n\t\t\t\tcontent: message.content.filter((content) => content.type !== \"toolCall\" || toolCallIds.has(content.id)),\n\t\t\t}));\n\t\treturn omitOrphanToolResults([...requiredToolCallMessages, ...visibleMessages]);\n\t}\n\n\treturn [];\n}\n\nfunction omitOrphanToolResults(messages: AgentMessage[]): AgentMessage[] {\n\tconst renderedToolCallIds = new Set<string>();\n\tconst renderableMessages: AgentMessage[] = [];\n\tfor (const message of messages) {\n\t\tif (message.role === \"assistant\") {\n\t\t\tfor (const content of message.content) {\n\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\trenderedToolCallIds.add(content.id);\n\t\t\t\t}\n\t\t\t}\n\t\t\trenderableMessages.push(message);\n\t\t} else if (message.role === \"toolResult\") {\n\t\t\tif (renderedToolCallIds.has(message.toolCallId)) {\n\t\t\t\trenderableMessages.push(message);\n\t\t\t}\n\t\t} else {\n\t\t\trenderableMessages.push(message);\n\t\t}\n\t}\n\treturn renderableMessages;\n}\n\nfunction isDeadTerminalError(error: unknown): boolean {\n\tif (!error || typeof error !== \"object\" || !(\"code\" in error)) {\n\t\treturn false;\n\t}\n\tconst code = (error as NodeJS.ErrnoException).code;\n\treturn code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);\n}\n\nfunction getPayloadString(payload: Record<string, unknown>, key: string): string | undefined {\n\tconst value = payload[key];\n\treturn typeof value === \"string\" ? value : undefined;\n}\n\nfunction getPayloadNumber(payload: Record<string, unknown>, key: string): number | undefined {\n\tconst value = payload[key];\n\treturn typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n}\n\nfunction getPayloadBoolean(payload: Record<string, unknown>, key: string): boolean | undefined {\n\tconst value = payload[key];\n\treturn typeof value === \"boolean\" ? value : undefined;\n}\n\nfunction getPayloadStringArray(payload: Record<string, unknown>, key: string): string[] | undefined {\n\tconst value = payload[key];\n\tif (value === undefined) {\n\t\treturn undefined;\n\t}\n\treturn Array.isArray(value) && value.every((item): item is string => typeof item === \"string\") ? value : undefined;\n}\n\nfunction getPayloadNotifyType(payload: Record<string, unknown>, key: string): \"info\" | \"warning\" | \"error\" | undefined {\n\tconst value = payload[key];\n\treturn value === \"info\" || value === \"warning\" || value === \"error\" ? value : undefined;\n}\n\nfunction getPayloadWidgetPlacement(\n\tpayload: Record<string, unknown>,\n\tkey: string,\n): \"aboveEditor\" | \"belowEditor\" | undefined {\n\tconst value = payload[key];\n\treturn value === \"aboveEditor\" || value === \"belowEditor\" ? value : undefined;\n}\n\nfunction getPayloadWorkingIndicatorOptions(\n\tpayload: Record<string, unknown>,\n\tkey: string,\n): LoaderIndicatorOptions | undefined {\n\tconst value = payload[key];\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) {\n\t\treturn undefined;\n\t}\n\tconst optionsPayload = value as Record<string, unknown>;\n\tconst frames = getPayloadStringArray(optionsPayload, \"frames\");\n\tconst intervalMs = getPayloadNumber(optionsPayload, \"intervalMs\");\n\treturn {\n\t\t...(frames === undefined ? {} : { frames }),\n\t\t...(intervalMs === undefined ? {} : { intervalMs }),\n\t};\n}\n\nexport function updateArgsIncludeSelf(args: readonly string[]): boolean {\n\tlet selfFlag = false;\n\tlet extensionsOnlyFlag = false;\n\tlet positional: string | undefined;\n\tfor (let index = 0; index < args.length; index++) {\n\t\tconst arg = args[index];\n\t\tif (arg === \"--self\") {\n\t\t\tselfFlag = true;\n\t\t} else if (arg === \"--extensions\") {\n\t\t\textensionsOnlyFlag = true;\n\t\t} else if (arg === \"--extension\") {\n\t\t\textensionsOnlyFlag = true;\n\t\t\tindex++;\n\t\t} else if (arg === \"--daemon-socket\") {\n\t\t\tindex++;\n\t\t} else if (arg && !arg.startsWith(\"-\") && positional === undefined) {\n\t\t\tpositional = arg;\n\t\t}\n\t}\n\tif (selfFlag) {\n\t\treturn true;\n\t}\n\tif (extensionsOnlyFlag) {\n\t\treturn false;\n\t}\n\tif (!positional) {\n\t\treturn true;\n\t}\n\tconst normalized = positional.toLowerCase();\n\treturn normalized === \"self\" || normalized === \"pi\" || normalized === APP_NAME.toLowerCase();\n}\n\nfunction argsIncludeSessionSelection(args: readonly string[]): boolean {\n\tfor (const arg of args) {\n\t\tif (arg === \"--resume\" || arg === \"-r\" || arg === \"--continue\" || arg === \"-c\" || arg === \"--fork\") {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nexport function buildUpdateRelaunchArgs(args: readonly string[], sessionFile: string | undefined): string[] {\n\tconst relaunchArgs = [...args];\n\tif (sessionFile && !argsIncludeSessionSelection(relaunchArgs)) {\n\t\trelaunchArgs.push(\"--resume\", sessionFile);\n\t}\n\treturn relaunchArgs;\n}\n\nexport function buildUpdateChildArgs(args: readonly string[], daemonSocketPath: string): string[] {\n\treturn args.includes(\"--daemon-socket\") ? [...args] : [...args, \"--daemon-socket\", daemonSocketPath];\n}\n\nexport function resolveInteractiveUpdateDaemonSocketPath(\n\targs: readonly string[],\n\tactiveDaemonSocketPath: string,\n): string {\n\tconst socketFlagIndex = args.indexOf(\"--daemon-socket\");\n\treturn socketFlagIndex === -1 ? activeDaemonSocketPath : (args[socketFlagIndex + 1] ?? activeDaemonSocketPath);\n}\n\nexport interface InteractiveInitialPrompt {\n\ttext: string;\n\timages?: ImageContent[];\n}\n\nexport interface InteractiveModeOptions {\n\t/** Providers that were migrated to auth.json (shows warning) */\n\tmigratedProviders?: string[];\n\t/** Warning message if session model couldn't be restored */\n\tmodelFallbackMessage?: string;\n\t/** One-off warning shown on startup. */\n\tstartupNotice?: string;\n\t/** Initial message to send on startup (can include @file content) */\n\tinitialMessage?: string;\n\t/** Images to attach to the initial message */\n\tinitialImages?: ImageContent[];\n\t/** Additional text-only messages to send after the initial message. */\n\tinitialMessages?: string[];\n\t/** Additional image-bearing prompts to send after the initial messages. */\n\tinitialPrompts?: InteractiveInitialPrompt[];\n\t/** Force verbose startup (overrides quietStartup setting) */\n\tverbose?: boolean;\n\t/** Agent execution boundary. InteractiveMode never talks directly to AgentSession for core execution. */\n\tagentConnection: AgentConnection;\n\t/** Exact daemon socket to preserve across an interactive self-update restart. */\n\tdaemonSocketPath?: string;\n\t/**\n\t * Local-only host for in-process extension binding and callback-bearing session operations.\n\t * This must remain optional adapter glue, not a generic execution dependency.\n\t */\n\tlocalSessionHost?: InteractiveModeLocalSessionHost;\n\t/** Bind extension handlers in the local session host. Disabled for daemon/gateway-backed clients. */\n\tbindLocalSessionExtensions?: boolean;\n\t/** UI-local services used for settings, auth, resources, and rendering. Defaults to services from localSessionHost. */\n\tuiServices?: InteractiveModeUiServices;\n\t/** Extra cleanup for externally-owned UI service hosts. Runs after the connection is disposed and before process exit. */\n\tonShutdown?: () => void | Promise<void>;\n\t/** Allow returning from a full session to the agents view without stopping the daemon-owned agent. */\n\treturnToAgentsView?: boolean;\n\t/** Enter fullscreen regardless of the persisted fullscreen preference. */\n\tforceFullscreen?: boolean;\n\t/**\n\t * The agents view already surfaced global startup notices (app/extension updates, tmux setup),\n\t * so this session must not repeat them in its chat stream. Distinct from `returnToAgentsView`,\n\t * which also covers direct daemon attaches where the agents view was never shown.\n\t */\n\tagentsViewOwnsStartupNotices?: boolean;\n\t/** Persisted RLM depth supplied by the daemon SessionSummary. */\n\tsessionDepth?: number;\n\t/** Whether the unified daemon/catalog projection had any direct children. */\n\tsessionHasChildren?: boolean;\n\t/** Client-owned stash store shared across chat views in this TUI process. */\n\tpromptStashStore?: ClientPromptStashStore;\n\t/** Initial stable session id used to scope prompt stash state. */\n\tpromptStashSessionId?: string;\n}\n\nexport interface InteractiveModeRunResult {\n\ttype: \"agents_view\" | \"scoped_agents_view\";\n\tsource: Pick<AgentConnectionState, \"activeSessionId\" | \"sessionFile\" | \"sessionId\" | \"sessionName\" | \"cwd\">;\n}\n\nexport function formatAgentDepthLabel(depth: number | undefined, hasChildren: boolean): string | undefined {\n\tif (depth === undefined || (depth === 0 && !hasChildren)) return undefined;\n\treturn `depth ${depth}`;\n}\n\nexport class InteractiveMode {\n\tprivate static readonly EXIT_HINT_DURATION_MS = 2000;\n\tprivate static readonly ESCAPE_REPEAT_WINDOW_MS = 500;\n\n\tprivate uiServices: InteractiveModeUiServices;\n\tprivate agentConnection: AgentConnection;\n\tprivate localSessionHost: InteractiveModeLocalSessionHost | undefined;\n\tprivate bindLocalSessionExtensions: boolean;\n\tprivate ui: TUI;\n\tprivate chatContainer: Container;\n\tprivate shortcutGuideContainer: Container;\n\tprivate pendingMessagesContainer: Container;\n\tprivate statusContainer: Container;\n\tprivate queuedMessagesContainer: Container;\n\tprivate sideQuestionContainer: Container;\n\tprivate featureHintContainer: Container;\n\tprivate defaultEditor: CustomEditor;\n\tprivate editor: EditorComponent;\n\tprivate readonly promptStashStore: ClientPromptStashStore | undefined;\n\tprivate promptStashSessionId: string | undefined;\n\tprivate promptStashState: PromptStashState;\n\tprivate editorComponentFactory: EditorFactory | undefined;\n\tprivate autocompleteProvider: AutocompleteProvider | undefined;\n\tprivate autocompleteProviderWrappers: AutocompleteProviderFactory[] = [];\n\tprivate fdPath: string | undefined;\n\tprivate mainContainer: Container;\n\tprivate mainViewContainer: Container;\n\t// prompt bar (editor + footer slot) — the only thing pinned to the bottom in fullscreen\n\tprivate promptDock: Container;\n\t// wraps the active footer so custom-footer swaps reflect in both layouts\n\tprivate footerSlot: Container;\n\tprivate fullscreenEnabled = false;\n\tprivate editorContainer: Container;\n\tprivate footer: FooterComponent;\n\tprivate footerDataProvider: FooterDataProvider;\n\t// Stored so the same manager can be injected into custom editors, selectors, and extension UI.\n\tprivate keybindings: KeybindingsManager;\n\tprivate version: string;\n\tprivate readonly startHint = getRandomStartHint();\n\tprivate isInitialized = false;\n\tprivate onInputCallback?: (text: string | undefined) => void;\n\tprivate submittedInputBehavior: \"steer\" | \"followUp\" = \"steer\";\n\tprivate latestEditorPromptStash: PromptStash | undefined;\n\tprivate pendingSubmittedPromptStash: PromptStash | undefined;\n\tprivate inputSubmissionGeneration = 0;\n\tprivate inputSubmissionsPending = 0;\n\tprivate pendingPromptStashReleases: { sessionId: string; state: PromptStashState }[] = [];\n\tprivate readonly retainedSubmissionGenerations = new WeakMap<PromptStash, number>();\n\tprivate admitPendingStartupPrompts: (() => Promise<StartupPromptBarrierOutcome>) | undefined;\n\tprivate agentsViewRequest: InteractiveModeRunResult[\"type\"] | undefined;\n\tprivate loadingAnimation: Loader | undefined = undefined;\n\tprivate workingMessage: string | undefined = undefined;\n\tprivate workingVisible = true;\n\tprivate workingIndicatorOptions: LoaderIndicatorOptions | undefined = undefined;\n\tprivate workingStartedAt: number | undefined = undefined;\n\t// Start of the in-flight run; survives loader teardown so the elapsed display doesn't reset on re-entry.\n\tprivate turnStartedAt: number | undefined = undefined;\n\tprivate workingTimer: NodeJS.Timeout | undefined = undefined;\n\tprivate readonly featureHintDeck = new FeatureHintDeck();\n\tprivate currentFeatureHint: string | undefined;\n\tprivate featureHintEligibleAt = 0;\n\tprivate featureHintTimer: NodeJS.Timeout | undefined;\n\tprivate featureHintAnimationTimer: NodeJS.Timeout | undefined;\n\tprivate featureHintComponent: FeatureHintComponent | undefined;\n\tprivate featureHintRunPending = false;\n\tprivate featureHintSuppressedByQueue = false;\n\tprivate pulseTimer: NodeJS.Timeout | undefined = undefined;\n\tprivate pulseFrame = 0;\n\tprivate readonly activityTracker = new AgentActivityTracker();\n\t// activityTracker token count already folded into the context snapshot; only output beyond\n\t// this counts as live in-flight (keeps auto-retries from re-adding a failed attempt).\n\tprivate contextUsageTokenBaseline = 0;\n\t// Refresh ordering: a stale failure must never clobber a newer success.\n\tprivate contextUsageRefresh = { generation: 0, lastSuccessGeneration: 0 };\n\tprivate readonly defaultHiddenThinkingLabel = \"Thinking...\";\n\tprivate hiddenThinkingLabel = this.defaultHiddenThinkingLabel;\n\n\tprivate ctrlCExitHintExpiresAt = 0;\n\tprivate ctrlCExitHintTimer: ReturnType<typeof setTimeout> | undefined = undefined;\n\tprivate escapeRepeatAction: \"tree\" | \"clear\" | undefined;\n\tprivate escapeRepeatExpiresAt = 0;\n\tprivate escapeRepeatTimer: ReturnType<typeof setTimeout> | undefined = undefined;\n\tprivate anthropicSubscriptionWarningShown = false;\n\n\tprivate lastStatusSpacer: Spacer | undefined = undefined;\n\tprivate lastStatusText: Text | undefined = undefined;\n\tprivate lastGoalAnnouncement: GoalAnnouncementSnapshot | undefined = undefined;\n\tprivate goalTrayTimer: NodeJS.Timeout | undefined = undefined;\n\n\tprivate streamingComponent: AssistantMessageComponent | undefined = undefined;\n\tprivate streamingMessage: AssistantMessage | undefined = undefined;\n\tprivate sideQuestionComponent: SideQuestionComponent | undefined;\n\tprivate sideQuestionEvent: AgentConnectionSideQuestionEvent | undefined;\n\tprivate sideQuestionTurns: AgentConnectionSideQuestionEvent[] = [];\n\tprivate activeSideQuestionId: string | undefined;\n\t// Set while a ! bash command runs inside the side conversation: its\n\t// BashExecutionComponent renders inside the pane instead of the main chat.\n\t// bash_* events broadcast to every attached client, so runs correlate by\n\t// runId — the runId we generate here is echoed on our run's events.\n\tprivate sideQuestionBash: { runId: string; input: string; seedTranscript: boolean } | undefined;\n\t// The pane-mounted component of our own side run; bash_end seeds the side\n\t// transcript only when it ends this exact component.\n\tprivate sideQuestionBashComponent: BashExecutionComponent | undefined;\n\t// Holds the runId of a side bash abandoned at pane close: that run's\n\t// remaining bash_* events are swallowed (until its bash_end) instead of\n\t// leaking into the main transcript.\n\tprivate sideQuestionBashDiscarded: string | undefined;\n\n\t// User bash execution tracking (! / !! prefix), driven by bash_* session events\n\tprivate activeBashComponent: BashExecutionComponent | undefined = undefined;\n\tprivate pendingBashComponents: BashExecutionComponent[] = [];\n\n\t// Serializes session event handling; see subscribeToAgent\n\tprivate sessionEventQueue: Promise<void> = Promise.resolve();\n\tprivate sessionEventGeneration = 0;\n\tprivate fastModeToggleQueue: Promise<void> = Promise.resolve();\n\n\tprivate pendingTools = new Map<string, ToolExecutionComponent>();\n\tprivate ipythonToolComponents = new Map<string, ToolExecutionComponent>();\n\tprivate lateIpythonSentAgentMessages = new Map<string, KernelSentAgentMessage[]>();\n\tprivate pendingToolCreations = new Set<string>();\n\tprivate startedToolCalls = new Set<string>();\n\tprivate pendingToolGeneration = 0;\n\tprivate toolDefinitionCache = new Map<string, ToolExecutionDefinition | undefined>();\n\tprivate agentRunFileChanges = new Map<string, FileChangeSummary>();\n\n\t// One summary line below the editor, backed by the existing child-status stream.\n\tprivate subagentSummaryLine: SubagentSummaryLine;\n\tprivate subagentSnapshots = new Map<string, AgentConnectionRlmChildAgentSnapshot>();\n\tprivate rlmNodeId: string | undefined;\n\n\tprivate toolOutputExpanded = false;\n\tprivate agentMessagesExpanded = false;\n\tprivate editDiffsExpanded = false;\n\n\tprivate hideThinkingBlock = false;\n\n\tprivate skillCommands = new Map<string, string>();\n\tprivate connectionCommands: AgentConnectionSlashCommand[] = [];\n\tprivate connectionModels: AgentConnectionModel[] = [];\n\tprivate connectionModelCatalog: AgentConnectionModel[] = [];\n\tprivate connectionConfiguredProviders = new Set<string>();\n\tprivate connectionModelsFetchedAt = 0;\n\tprivate connectionModelsRefreshVersion = 0;\n\tprivate connectionModelsRefreshInFlight: { version: number; promise: Promise<AgentConnectionModel[]> } | undefined;\n\tprivate connectionState: AgentConnectionState | undefined;\n\tprivate connectionResourceSnapshot: AgentConnectionResourceSnapshot | undefined;\n\tprivate sessionHasMessages = false;\n\tprivate heartbeatCatalog: AgentConnectionHeartbeat[] = [];\n\tprivate heartbeats: AgentConnectionHeartbeat[] = [];\n\tprivate heartbeatRefreshPromise: Promise<void> | undefined;\n\tprivate heartbeatRefreshRequested = false;\n\tprivate heartbeatManager: HeartbeatManagerComponent | undefined;\n\tprivate heartbeatManagerHandle: OverlayHandle | undefined;\n\tprivate heartbeatManagerRefreshTimer: ReturnType<typeof setTimeout> | undefined;\n\n\t// Registry of images pasted this session, keyed by the `[image #N]` marker\n\t// shown to the user. Insertion-ordered; the bytes persist (bounded by\n\t// MAX_PASTED_IMAGE_BYTES) so a marker resolves to its image whenever the text\n\t// reappears — on submit, undo, history recall, retry, or dequeue. A submission\n\t// attaches only the images whose markers are present in the sent text.\n\tprivate pastedImages = new Map<number, ImageContent>();\n\tprivate nextImageMarkerId = 1;\n\n\tprivate unsubscribe?: () => void;\n\tprivate signalCleanupHandlers: Array<() => void> = [];\n\n\tprivate autoCompactionLoader: Loader | undefined = undefined;\n\tprivate refineLoader: Loader | undefined = undefined;\n\n\tprivate retryLoader: Loader | undefined = undefined;\n\tprivate retryCountdown: CountdownTimer | undefined = undefined;\n\tprivate traceUploadAllAbortController: AbortController | undefined = undefined;\n\n\t// Session-owned queued messages mirrored from connection events.\n\tprivate connectionQueue: AgentConnectionQueueState = { steering: [], followUp: [] };\n\tprivate readonly queueSelection = new QueueSelection();\n\tprivate isApplyingQueueSelectionText = false;\n\tprivate queueMutationChain: Promise<void> = Promise.resolve();\n\tprivate pendingQueueEdit: symbol | undefined;\n\n\tprivate shutdownRequested = false;\n\n\tprivate extensionSelector: ExtensionSelectorComponent | undefined = undefined;\n\tprivate extensionInput: ExtensionInputComponent | undefined = undefined;\n\tprivate extensionEditor: ExtensionEditorComponent | undefined = undefined;\n\tprivate extensionTerminalInputUnsubscribers = new Set<() => void>();\n\tprivate activeConnectionExtensionUiRequests = new Map<string, { cancelLocal: () => void }>();\n\n\tprivate extensionWidgetsAbove = new Map<string, Component & { dispose?(): void }>();\n\tprivate extensionWidgetsBelow = new Map<string, Component & { dispose?(): void }>();\n\tprivate widgetContainerAbove!: Container;\n\tprivate widgetContainerBelow!: Container;\n\n\t// One-line recap of the agent's recent work, rendered just above the editor.\n\tprivate recapContainer!: Container;\n\tprivate sessionRecap: string | undefined;\n\n\tprivate customFooter: (Component & { dispose?(): void }) | undefined = undefined;\n\n\tprivate headerContainer: Container;\n\n\tprivate builtInHeader: Component | undefined = undefined;\n\n\tprivate customHeader: (Component & { dispose?(): void }) | undefined = undefined;\n\n\tprivate getLocalSessionHost(): InteractiveModeLocalSessionHost {\n\t\tif (!this.localSessionHost) {\n\t\t\tthrow new Error(\"Local session host is not available in connection-backed interactive mode\");\n\t\t}\n\t\treturn this.localSessionHost;\n\t}\n\tprivate get settingsManager() {\n\t\treturn this.uiServices.settingsManager;\n\t}\n\tprivate get modelRegistry() {\n\t\treturn this.uiServices.modelRegistry;\n\t}\n\n\tconstructor(private options: InteractiveModeOptions) {\n\t\tconst uiServices = options.uiServices ?? options.localSessionHost?.createUiServices();\n\t\tif (!uiServices) {\n\t\t\tthrow new Error(\"InteractiveMode requires uiServices when no localSessionHost is supplied\");\n\t\t}\n\t\tthis.uiServices = uiServices;\n\t\tthis.agentConnection = options.agentConnection;\n\t\tthis.promptStashStore = options.promptStashStore;\n\t\tthis.promptStashSessionId = options.promptStashSessionId;\n\t\tthis.promptStashState =\n\t\t\tthis.promptStashStore && this.promptStashSessionId\n\t\t\t\t? this.promptStashStore.forSession(this.promptStashSessionId)\n\t\t\t\t: {};\n\t\tthis.hydratePromptStash();\n\t\tthis.localSessionHost = options.localSessionHost;\n\t\tthis.bindLocalSessionExtensions = options.bindLocalSessionExtensions ?? options.localSessionHost !== undefined;\n\t\tif (this.bindLocalSessionExtensions && !options.localSessionHost) {\n\t\t\tthrow new Error(\"Local extension binding requires localSessionHost\");\n\t\t}\n\t\tthis.agentConnection.onBeforeSessionInvalidate(() => {\n\t\t\tthis.resetExtensionUI();\n\t\t\tthis.resetSideQuestion();\n\t\t});\n\t\tthis.version = VERSION;\n\t\tthis.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());\n\t\tthis.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());\n\t\tthis.ui.onCopy = (text) => {\n\t\t\tvoid this.copyFullscreenSelection(text);\n\t\t};\n\t\tthis.headerContainer = new Container();\n\t\tthis.chatContainer = new Container();\n\t\tthis.shortcutGuideContainer = new Container();\n\t\tthis.pendingMessagesContainer = new Container();\n\t\tthis.statusContainer = new Container();\n\t\tthis.queuedMessagesContainer = new Container();\n\t\tthis.sideQuestionContainer = new Container();\n\t\tthis.featureHintContainer = new Container();\n\t\tthis.widgetContainerAbove = new Container();\n\t\tthis.widgetContainerBelow = new Container();\n\t\tthis.recapContainer = new Container();\n\t\tthis.keybindings = KeybindingsManager.create();\n\t\tsetKeybindings(this.keybindings);\n\t\tconst editorPaddingX = this.settingsManager.getEditorPaddingX();\n\t\tconst autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();\n\t\tthis.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, {\n\t\t\tpaddingX: editorPaddingX,\n\t\t\tautocompleteMaxVisible,\n\t\t\tisArgumentCommand: builtinSlashCommandTakesArgument,\n\t\t\tplaceholder: this.startHint,\n\t\t\tplaceholderColor: (text) => theme.fg(\"dim\", text),\n\t\t});\n\t\tthis.editor = this.defaultEditor;\n\t\tthis.mainContainer = new Container();\n\t\tthis.mainViewContainer = new Container();\n\t\tthis.promptDock = new Container();\n\t\tthis.footerSlot = new Container();\n\t\tthis.mainViewContainer.addChild(this.chatContainer);\n\t\tthis.mainViewContainer.addChild(this.shortcutGuideContainer);\n\t\tthis.mainViewContainer.addChild(this.pendingMessagesContainer);\n\t\tthis.mainViewContainer.addChild(this.statusContainer);\n\t\tthis.editorContainer = new Container();\n\t\tthis.editorContainer.addChild(this.editor as Component);\n\t\tthis.subagentSummaryLine = new SubagentSummaryLine(\n\t\t\t() => this.getTrayLocationLabel(),\n\t\t\t() => this.getTrayContextLabel(),\n\t\t\t() => this.getTrayOverrideLabel(),\n\t\t);\n\t\tthis.subagentSummaryLine.setOpenable(this.options.returnToAgentsView === true);\n\t\tthis.subagentSummaryLine.onOpen = () => void this.openScopedAgentsView();\n\t\tthis.subagentSummaryLine.onCancel = () => this.focusEditor();\n\t\tthis.subagentSummaryLine.onChatAction = (data) => this.handleSubagentSummaryChatAction(data);\n\t\tthis.footerDataProvider = new FooterDataProvider(this.uiServices.getInitialCwd());\n\t\tthis.footer = new FooterComponent(this.footerDataProvider);\n\t\tthis.footer.setAutoCompactEnabled(this.settingsManager.getCompactionEnabled());\n\t\tthis.setGoalAnnouncementBaseline(emptyGoalState());\n\n\t\tthis.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();\n\n\t\tsetRegisteredThemes(this.uiServices.getThemes());\n\t\tinitTheme(this.settingsManager.getTheme(), true);\n\t}\n\n\tprivate get promptStash(): PromptStash | undefined {\n\t\treturn this.promptStashState.stash;\n\t}\n\n\tprivate set promptStash(stash: PromptStash | undefined) {\n\t\tthis.promptStashState.stash = stash;\n\t}\n\n\tprivate hydratePromptStash(): void {\n\t\tfor (const stash of [this.promptStash, ...(this.promptStashState?.queuedStashes ?? [])]) {\n\t\t\tif (!stash) continue;\n\t\t\tfor (const [markerId, image] of stash.images ?? []) {\n\t\t\t\tthis.pastedImages.set(markerId, image);\n\t\t\t\tthis.nextImageMarkerId = Math.max(this.nextImageMarkerId, markerId + 1);\n\t\t\t}\n\t\t\tfor (const markerId of imageMarkerIds(stash.text)) {\n\t\t\t\tthis.nextImageMarkerId = Math.max(this.nextImageMarkerId, markerId + 1);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate bindPromptStashSession(sessionId: string): void {\n\t\tif (!this.promptStashStore || this.promptStashSessionId === sessionId) {\n\t\t\treturn;\n\t\t}\n\t\tthis.releasePromptStashSession();\n\t\tthis.promptStashSessionId = sessionId;\n\t\tthis.promptStashState = this.promptStashStore.forSession(sessionId);\n\t\tthis.hydratePromptStash();\n\t}\n\n\tprivate releasePromptStashSession(): void {\n\t\tif (this.inputSubmissionsPending > 0) {\n\t\t\t// Capture the pair: a rebind may repoint the fields before the deferred\n\t\t\t// release fires, and repeated rebinds/teardowns each defer their own pair.\n\t\t\tif (\n\t\t\t\tthis.promptStashSessionId &&\n\t\t\t\t!this.pendingPromptStashReleases.some((pending) => pending.sessionId === this.promptStashSessionId)\n\t\t\t) {\n\t\t\t\tthis.pendingPromptStashReleases.push({\n\t\t\t\t\tsessionId: this.promptStashSessionId,\n\t\t\t\t\tstate: this.promptStashState,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst pending = this.pendingPromptStashReleases;\n\t\tthis.pendingPromptStashReleases = [];\n\t\tif (!this.promptStashStore) return;\n\t\tfor (const release of pending) {\n\t\t\tthis.promptStashStore.release(release.sessionId, release.state);\n\t\t}\n\t\tif (this.promptStashSessionId) {\n\t\t\tthis.promptStashStore.release(this.promptStashSessionId, this.promptStashState);\n\t\t}\n\t}\n\n\tprivate completeDeferredPromptStashRelease(): void {\n\t\tconst pending = this.pendingPromptStashReleases;\n\t\tif (pending.length === 0) return;\n\t\tthis.pendingPromptStashReleases = [];\n\t\tif (!this.promptStashStore) return;\n\t\tfor (const release of pending) {\n\t\t\tthis.promptStashStore.release(release.sessionId, release.state);\n\t\t}\n\t}\n\n\tprivate getAutocompleteSourceTag(sourceInfo?: AgentConnectionSourceInfo): string | undefined {\n\t\tif (!sourceInfo) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst scopePrefix =\n\t\t\tsourceInfo.scope === \"user\" ? \"user\" : sourceInfo.scope === \"project\" ? \"project\" : \"temporary\";\n\t\tconst source = sourceInfo.source.trim();\n\n\t\tif (source === \"builtin\") {\n\t\t\treturn \"builtin\";\n\t\t}\n\n\t\tif (source === \"auto\" || source === \"local\" || source === \"cli\") {\n\t\t\treturn scopePrefix;\n\t\t}\n\n\t\tif (source.startsWith(\"npm:\")) {\n\t\t\treturn `${scopePrefix}:${source}`;\n\t\t}\n\n\t\tconst gitSource = parseGitUrl(source);\n\t\tif (gitSource) {\n\t\t\tconst ref = gitSource.ref ? `@${gitSource.ref}` : \"\";\n\t\t\treturn `${scopePrefix}:git:${gitSource.host}/${gitSource.path}${ref}`;\n\t\t}\n\n\t\treturn scopePrefix;\n\t}\n\n\tprivate getAutocompleteSourceLabel(sourceInfo?: AgentConnectionSourceInfo): string | undefined {\n\t\tconst sourceTag = this.getAutocompleteSourceTag(sourceInfo);\n\t\treturn sourceTag ? `#${sourceTag}` : undefined;\n\t}\n\n\tprivate getBuiltInCommandConflictDiagnostics(\n\t\tcommands: readonly AgentConnectionSlashCommand[],\n\t): AgentConnectionResourceDiagnostic[] {\n\t\treturn commands\n\t\t\t.filter((command) => command.source === \"extension\")\n\t\t\t.filter((command) => isBuiltinSlashCommandName(command.registeredName ?? command.name))\n\t\t\t.map((command) => ({\n\t\t\t\ttype: \"warning\" as const,\n\t\t\t\tmessage:\n\t\t\t\t\tcommand.name === (command.registeredName ?? command.name)\n\t\t\t\t\t\t? `Extension command '/${command.name}' conflicts with built-in interactive command. Skipping in autocomplete.`\n\t\t\t\t\t\t: `Extension command '/${command.registeredName}' conflicts with built-in interactive command. Available as '/${command.name}'.`,\n\t\t\t\tpath: command.sourceInfo.path,\n\t\t\t}));\n\t}\n\n\tprivate isRecognizedSlashCommand(name: string): boolean {\n\t\treturn isBuiltinSlashCommandName(name) || this.connectionCommands.some((command) => command.name === name);\n\t}\n\n\tprivate createBaseAutocompleteProvider(): AutocompleteProvider {\n\t\tconst slashCommands: SlashCommand[] = BUILTIN_SLASH_COMMANDS.filter(\n\t\t\t(command) => command.name !== \"fast\" || this.currentModelSupportsFastMode(),\n\t\t).map((command) => ({\n\t\t\tname: command.name,\n\t\t\taliases: command.aliases,\n\t\t\tdescription: command.description,\n\t\t\targumentHint: command.argumentHint,\n\t\t\ttakesArgument: command.takesArgument,\n\t\t}));\n\n\t\tconst modelCommand = slashCommands.find((command) => command.name === \"model\");\n\t\tif (modelCommand) {\n\t\t\tmodelCommand.getArgumentCompletions = (prefix: string): AutocompleteItem[] | null =>\n\t\t\t\tgetModelArgumentCompletions(prefix, this.getCachedModelCandidates());\n\t\t}\n\n\t\tconst effortCommand = slashCommands.find((command) => command.name === \"effort\");\n\t\tif (effortCommand) {\n\t\t\teffortCommand.getArgumentCompletions = (prefix: string): AutocompleteItem[] | null =>\n\t\t\t\tthis.getThinkingLevelCompletions(prefix);\n\t\t\tconst levels = this.getAvailableThinkingLevels();\n\t\t\tif (levels.length > 0) {\n\t\t\t\teffortCommand.argumentHint = `[${levels.join(\"/\")}]`;\n\t\t\t}\n\t\t}\n\n\t\tconst heartbeatCommand = slashCommands.find((command) => command.name === \"heartbeat\");\n\t\tif (heartbeatCommand) {\n\t\t\theartbeatCommand.getArgumentCompletions = (prefix: string): AutocompleteItem[] | null =>\n\t\t\t\tthis.getHeartbeatArgumentCompletions(prefix);\n\t\t}\n\n\t\tconst connectionCommands = this.connectionCommands;\n\t\tconst templateCommands: SlashCommand[] = connectionCommands\n\t\t\t.filter((cmd) => cmd.source === \"prompt\")\n\t\t\t.map((cmd) => ({\n\t\t\t\tname: cmd.name,\n\t\t\t\tdescription: cmd.description,\n\t\t\t\tsourceTag: this.getAutocompleteSourceLabel(cmd.sourceInfo),\n\t\t\t\t...(cmd.argumentHint && { argumentHint: cmd.argumentHint }),\n\t\t\t}));\n\n\t\tconst extensionCommands: SlashCommand[] = connectionCommands\n\t\t\t.filter((cmd) => cmd.source === \"extension\")\n\t\t\t.filter((cmd) => !isBuiltinSlashCommandName(cmd.name))\n\t\t\t.map((cmd) => ({\n\t\t\t\tname: cmd.name,\n\t\t\t\tdescription: cmd.description,\n\t\t\t\tsourceTag: this.getAutocompleteSourceLabel(cmd.sourceInfo),\n\t\t\t\tgetArgumentCompletions: this.bindLocalSessionExtensions\n\t\t\t\t\t? this.getLocalSessionHost().getExtensionRunner().getCommand(cmd.name)?.getArgumentCompletions\n\t\t\t\t\t: undefined,\n\t\t\t}));\n\n\t\tthis.skillCommands.clear();\n\t\tconst skillCommandList: SlashCommand[] = [];\n\t\tif (this.settingsManager.getEnableSkillCommands()) {\n\t\t\tfor (const skill of connectionCommands.filter((cmd) => cmd.source === \"skill\")) {\n\t\t\t\tconst commandName = skill.name;\n\t\t\t\tskillCommandList.push({\n\t\t\t\t\tname: commandName,\n\t\t\t\t\tdescription: skill.description,\n\t\t\t\t\tsourceTag: this.getAutocompleteSourceLabel(skill.sourceInfo),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn new CombinedAutocompleteProvider(\n\t\t\t[...slashCommands, ...templateCommands, ...extensionCommands, ...skillCommandList],\n\t\t\tthis.getCurrentCwd(),\n\t\t\tthis.fdPath,\n\t\t);\n\t}\n\n\tprivate setupAutocompleteProvider(): void {\n\t\tlet provider = this.createBaseAutocompleteProvider();\n\t\tfor (const wrapProvider of this.autocompleteProviderWrappers) {\n\t\t\tprovider = wrapProvider(provider);\n\t\t}\n\n\t\tthis.autocompleteProvider = provider;\n\t\tthis.defaultEditor.setAutocompleteProvider(provider);\n\t\tif (this.editor !== this.defaultEditor) {\n\t\t\tthis.editor.setAutocompleteProvider?.(provider);\n\t\t}\n\t}\n\n\tasync init(): Promise<void> {\n\t\tif (this.isInitialized) return;\n\n\t\tthis.registerSignalHandlers();\n\n\t\t// Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)\n\t\t// fd powers autocomplete, and rg is available for shell commands.\n\t\tconst [fdPath, rgResult] = await Promise.all([ensureTool(\"fd\"), ensureToolWithStatus(\"rg\")]);\n\t\tthis.fdPath = fdPath;\n\t\tif (rgResult.status === \"unavailable\") {\n\t\t\tthis.showWarning(formatMissingRipgrepMessage(rgResult));\n\t\t}\n\n\t\tthis.ui.addChild(this.headerContainer);\n\n\t\t// Brand splash: side-panel layout with structured runtime metadata on the right.\n\t\t// The model/cwd are read through live getters, so they fill in once the\n\t\t// connection state loads (rebindCurrentSession below). Onboarding, when\n\t\t// required, renders as a full-screen overlay on top of this header.\n\t\tif (this.options.verbose || !this.settingsManager.getQuietStartup()) {\n\t\t\t// Verbose: include the full keybinding cheatsheet under the brand mark.\n\t\t\tconst hint = (keybinding: AppKeybinding, description: string) => keyHint(keybinding, description);\n\t\t\tconst verboseInstructions = this.options.verbose\n\t\t\t\t? [\n\t\t\t\t\t\thint(\"app.clear\", \"to interrupt\"),\n\t\t\t\t\t\trawKeyHint(`${keyText(\"app.clear\")} twice`, \"to exit\"),\n\t\t\t\t\t\thint(\"app.input.clear\", \"to clear input\"),\n\t\t\t\t\t\thint(\"app.exit\", \"to exit (empty)\"),\n\t\t\t\t\t\thint(\"app.suspend\", \"to suspend\"),\n\t\t\t\t\t\tkeyHint(\"tui.editor.deleteToLineEnd\", \"to delete to end\"),\n\t\t\t\t\t\trawKeyHint(\"/effort\", \"to set thinking level\"),\n\t\t\t\t\t\thint(\"app.model.select\", \"to select model\"),\n\t\t\t\t\t\thint(\"app.tools.expand\", \"to expand tools\"),\n\t\t\t\t\t\thint(\"app.messages.expand\", \"to expand agent messages\"),\n\t\t\t\t\t\thint(\"app.edits.expand\", \"to expand edit diffs\"),\n\t\t\t\t\t\thint(\"app.thinking.toggle\", \"to expand thinking\"),\n\t\t\t\t\t\thint(\"app.subagents.focus\", \"to inspect subagents\"),\n\t\t\t\t\t\thint(\"app.editor.external\", \"for external editor\"),\n\t\t\t\t\t\thint(\"app.prompt.stash\", \"to stash prompt\"),\n\t\t\t\t\t\trawKeyHint(\"/\", \"for commands\"),\n\t\t\t\t\t\thint(\"app.message.followUp\", \"to queue follow-up\"),\n\t\t\t\t\t\thint(\"app.message.navigateOlder\", \"to browse queued messages\"),\n\t\t\t\t\t\thint(\"app.clipboard.pasteImage\", \"to paste image\"),\n\t\t\t\t\t\trawKeyHint(\"drop files\", \"to attach\"),\n\t\t\t\t\t].join(\"\\n\")\n\t\t\t\t: undefined;\n\t\t\tthis.builtInHeader = new BrandSplashHeader(\n\t\t\t\tthis.version,\n\t\t\t\t() => this.getCurrentModelId(),\n\t\t\t\t() => this.getCurrentCwd(),\n\t\t\t\tverboseInstructions,\n\t\t\t\t{\n\t\t\t\t\ttopPadding: true,\n\t\t\t\t\tgetHideStartHint: () => !this.isNewChat(),\n\t\t\t\t\tgetStartHint: () => this.startHint,\n\t\t\t\t},\n\t\t\t);\n\t\t\tthis.headerContainer.addChild(this.builtInHeader);\n\t\t\tthis.headerContainer.addChild(new Spacer(1));\n\t\t} else {\n\t\t\t// Quiet startup: skip the splash and surrounding padding entirely.\n\t\t\tthis.builtInHeader = new Text(\"\", 0, 0);\n\t\t\tthis.headerContainer.addChild(this.builtInHeader);\n\t\t}\n\n\t\tthis.mainContainer.addChild(this.mainViewContainer);\n\t\tthis.renderWidgets(); // Initialize with default spacer\n\t\tthis.mainContainer.addChild(this.widgetContainerAbove);\n\t\tthis.renderRecap();\n\t\tfor (const container of this.getPromptContextContainers()) {\n\t\t\tthis.mainContainer.addChild(container);\n\t\t}\n\t\tthis.mainContainer.addChild(this.editorContainer);\n\t\tthis.mainContainer.addChild(this.subagentSummaryLine);\n\t\tthis.mainContainer.addChild(this.widgetContainerBelow);\n\t\tthis.footerSlot.addChild(this.footer);\n\t\tthis.mainContainer.addChild(this.footerSlot);\n\t\tfor (const component of this.getPromptDockComponents()) {\n\t\t\tthis.promptDock.addChild(component);\n\t\t}\n\t\tthis.ui.addChild(this.mainContainer);\n\t\tthis.ui.setFocus(this.editor);\n\n\t\tthis.setupKeyHandlers();\n\t\tthis.setupEditorSubmitHandler();\n\n\t\tthis.ui.start();\n\t\tthis.fullscreenEnabled =\n\t\t\t(this.options.forceFullscreen === true || this.settingsManager.getFullscreen()) &&\n\t\t\tprocess.stdout.isTTY === true;\n\t\tif (this.fullscreenEnabled) {\n\t\t\tthis.applyFullscreen(true);\n\t\t}\n\t\tthis.isInitialized = true;\n\n\t\tawait this.rebindCurrentSession();\n\n\t\tawait this.renderInitialMessages();\n\n\t\tonThemeChange(() => {\n\t\t\tthis.ui.invalidate();\n\t\t\tthis.updateEditorBorderColor();\n\t\t\tthis.ui.requestRender();\n\t\t});\n\n\t\tthis.footerDataProvider.onBranchChange(() => {\n\t\t\tthis.ui.requestRender();\n\t\t});\n\n\t\tawait this.updateAvailableProviderCount();\n\t}\n\n\tprivate updateTerminalTitle(): void {\n\t\tconst cwdBasename = path.basename(this.getCurrentCwd());\n\t\tconst sessionName = this.getCurrentSessionName();\n\t\tif (sessionName) {\n\t\t\tthis.ui.terminal.setTitle(`${APP_TITLE} - ${sessionName} - ${cwdBasename}`);\n\t\t} else {\n\t\t\tthis.ui.terminal.setTitle(`${APP_TITLE} - ${cwdBasename}`);\n\t\t}\n\t}\n\n\tasync run(): Promise<InteractiveModeRunResult> {\n\t\tawait this.init();\n\t\tthis.restorePromptStashOnOpen();\n\n\t\t// Global, environment-scoped notices (app update, extension updates, tmux setup)\n\t\t// belong on the agents view, not in a conversation. When the agents view already\n\t\t// showed them, skip the checks here entirely. (This is narrower than\n\t\t// `returnToAgentsView`, which is also set for direct daemon attaches that never\n\t\t// rendered the agents view and still want the in-session fallback.)\n\t\tconst ownsGlobalStartupNotices = !this.options.agentsViewOwnsStartupNotices;\n\t\tconst newVersionPromise = ownsGlobalStartupNotices ? checkForNewPiVersion(this.version) : undefined;\n\t\tconst packageUpdatesPromise = ownsGlobalStartupNotices\n\t\t\t? checkForPackageUpdates({\n\t\t\t\t\tcwd: this.getCurrentCwd(),\n\t\t\t\t\tagentDir: getAgentDir(),\n\t\t\t\t\tsettingsManager: this.settingsManager,\n\t\t\t\t})\n\t\t\t: undefined;\n\t\tconst tmuxKeyboardWarningPromise = ownsGlobalStartupNotices ? checkTmuxKeyboardSetup() : undefined;\n\n\t\tconst {\n\t\t\tmigratedProviders,\n\t\t\tmodelFallbackMessage,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t\tinitialMessages,\n\t\t\tinitialPrompts,\n\t\t} = this.options;\n\n\t\tif (migratedProviders && migratedProviders.length > 0) {\n\t\t\tthis.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(\", \")}`);\n\t\t}\n\n\t\tif (this.options.startupNotice) {\n\t\t\tthis.showWarning(this.options.startupNotice);\n\t\t}\n\n\t\tconst modelsJsonError = this.modelRegistry.getError();\n\t\tif (modelsJsonError) {\n\t\t\tthis.showError(`models.json error: ${modelsJsonError}`);\n\t\t}\n\n\t\tconst startupPrompts: InteractiveInitialPrompt[] = [\n\t\t\t...(initialMessage ? [{ text: initialMessage, images: initialImages }] : []),\n\t\t\t...(initialMessages ?? []).map((text) => ({ text })),\n\t\t\t...(initialPrompts ?? []),\n\t\t];\n\t\t// One drive loop owns startup-prompt delivery: it retries on a 250ms cadence\n\t\t// while a model is missing or admission fails transiently, shows every\n\t\t// admission error, and skips a prompt after three failed attempts.\n\t\t// `startupPromptsSettled` is the user-submission barrier (startup prompts\n\t\t// stay ahead of user prompts). Its outcome distinguishes completed admission\n\t\t// from lifecycle cancellation so a resumed submit does not mutate torn-down\n\t\t// editor state or consume the client-owned durable stash.\n\t\tlet startupPromptsDone = false;\n\t\tconst startupAdmissionAbort = new AbortController();\n\t\tlet settleStartupPrompts = (_outcome: StartupPromptBarrierOutcome) => {};\n\t\tconst startupPromptsSettled = new Promise<StartupPromptBarrierOutcome>((resolve) => {\n\t\t\tsettleStartupPrompts = (outcome) => {\n\t\t\t\tstartupPromptsDone = true;\n\t\t\t\tresolve(outcome);\n\t\t\t};\n\t\t});\n\t\t/** Resolves false when the run lifecycle ended before the 250ms retry delay elapsed. */\n\t\tconst startupRetryDelay = () =>\n\t\t\tnew Promise<boolean>((resolve) => {\n\t\t\t\tconst timer = setTimeout(() => resolve(true), 250);\n\t\t\t\ttimer.unref?.();\n\t\t\t\tvoid startupPromptsSettled.then(() => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tresolve(false);\n\t\t\t\t});\n\t\t\t});\n\t\tconst deliverStartupPrompts = async () => {\n\t\t\tlet failures = 0;\n\t\t\tfor (let next = 0; next < startupPrompts.length; ) {\n\t\t\t\t// The run lifecycle can settle the barrier while a prompt is being\n\t\t\t\t// admitted; stop instead of prompting a session we already left.\n\t\t\t\tif (startupPromptsDone) return;\n\t\t\t\tif (!this.getCurrentModel()) {\n\t\t\t\t\tif (!(await startupRetryDelay())) return;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst prompt = startupPrompts[next]!;\n\t\t\t\ttry {\n\t\t\t\t\tawait this.agentConnection.prompt(prompt.text, {\n\t\t\t\t\t\timages: prompt.images,\n\t\t\t\t\t\tstreamingBehavior: next === 0 ? \"steer\" : \"followUp\",\n\t\t\t\t\t\tqueueIfBusy: true,\n\t\t\t\t\t\tsignal: startupAdmissionAbort.signal,\n\t\t\t\t\t});\n\t\t\t\t\tfailures = 0;\n\t\t\t\t\tnext++;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (startupPromptsDone || startupAdmissionAbort.signal.aborted) return;\n\t\t\t\t\t// An uncertain daemon admission may already be session-owned. Retrying\n\t\t\t\t\t// would duplicate it; only an acknowledged pre-ownership cancellation is safe.\n\t\t\t\t\tif (error instanceof AgentConnectionPromptAdmissionError && error.status === \"owned\") {\n\t\t\t\t\t\tfailures = 0;\n\t\t\t\t\t\tnext++;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (error instanceof AgentConnectionPromptAdmissionError && !error.cancelled) {\n\t\t\t\t\t\t// This attempt may already be session-owned, so never retry it. Preserve\n\t\t\t\t\t\t// it and every not-yet-attempted startup prompt in original order.\n\t\t\t\t\t\tthis.retainStartupPromptDrafts(startupPrompts.slice(next));\n\t\t\t\t\t\tthis.showError(error.message);\n\t\t\t\t\t\tsettleStartupPrompts(\"retained\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst errorMessage = error instanceof Error ? error.message : \"Unknown error occurred\";\n\t\t\t\t\tif (++failures < 3) {\n\t\t\t\t\t\tthis.showError(errorMessage);\n\t\t\t\t\t\tif (!(await startupRetryDelay())) return;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tthis.showError(`Skipping startup prompt after 3 failed attempts: ${errorMessage}`);\n\t\t\t\t\tfailures = 0;\n\t\t\t\t\tnext++;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tthis.admitPendingStartupPrompts = startupPrompts.length > 0 ? () => startupPromptsSettled : undefined;\n\n\t\tlet deferredStartupNotificationsShown = false;\n\t\tconst showDeferredStartupNotifications = () => {\n\t\t\tif (deferredStartupNotificationsShown) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdeferredStartupNotificationsShown = true;\n\n\t\t\t// The agents view owns these for daemon sessions. When there is no agents view,\n\t\t\t// show them once at the top of a fresh session, but never append them under a\n\t\t\t// restored conversation where they read as disconnected clutter.\n\t\t\tif (!ownsGlobalStartupNotices || this.sessionHasMessages) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvoid newVersionPromise\n\t\t\t\t?.then((newVersion) => {\n\t\t\t\t\tif (newVersion) {\n\t\t\t\t\t\tthis.showNewVersionNotification(newVersion);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch(() => {});\n\n\t\t\tvoid packageUpdatesPromise\n\t\t\t\t?.then((updates) => {\n\t\t\t\t\tif (updates.length > 0) {\n\t\t\t\t\t\tthis.showPackageUpdateNotification(updates);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch(() => {});\n\n\t\t\tvoid tmuxKeyboardWarningPromise\n\t\t\t\t?.then((warning) => {\n\t\t\t\t\tif (warning) {\n\t\t\t\t\t\tthis.showWarning(warning);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch(() => {});\n\t\t};\n\n\t\tlet modelFallbackWarningShown = false;\n\t\tconst showModelFallbackWarning = () => {\n\t\t\tif (modelFallbackWarningShown) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst action = this.getModelFallbackWarningAction(modelFallbackMessage);\n\t\t\tmodelFallbackWarningShown = true;\n\t\t\tif (action === \"show\" && modelFallbackMessage) {\n\t\t\t\tthis.showWarning(modelFallbackMessage);\n\t\t\t}\n\t\t};\n\n\t\tawait this.runStartupOnboarding();\n\t\tshowDeferredStartupNotifications();\n\t\tshowModelFallbackWarning();\n\t\tvoid this.maybeWarnAboutAnthropicSubscriptionAuth();\n\t\tvoid deliverStartupPrompts().then(\n\t\t\t() => settleStartupPrompts(\"admitted\"),\n\t\t\t() => settleStartupPrompts(\"admitted\"),\n\t\t);\n\n\t\t// Enter/Alt+Enter submit directly through AgentConnection. Wait for the\n\t\t// lifecycle signal exactly once; a returned editor value has already been\n\t\t// admitted and must never be submitted again here.\n\t\ttry {\n\t\t\tawait this.getUserInput();\n\t\t} finally {\n\t\t\tstartupAdmissionAbort.abort();\n\t\t\tsettleStartupPrompts(\"lifecycle-cancelled\");\n\t\t\tthis.admitPendingStartupPrompts = undefined;\n\t\t}\n\n\t\tconst state = this.connectionState;\n\t\treturn {\n\t\t\ttype: this.agentsViewRequest ?? \"agents_view\",\n\t\t\tsource: {\n\t\t\t\tactiveSessionId: state?.activeSessionId,\n\t\t\t\tsessionFile: state?.sessionFile,\n\t\t\t\tsessionId: state?.sessionId ?? this.promptStashSessionId ?? \"\",\n\t\t\t\tsessionName: state?.sessionName,\n\t\t\t\tcwd: state?.cwd ?? this.getCurrentCwd(),\n\t\t\t},\n\t\t};\n\t}\n\n\tprivate getModelFallbackWarningAction(modelFallbackMessage: string | undefined): ModelFallbackWarningAction {\n\t\tif (!modelFallbackMessage) {\n\t\t\treturn \"suppress\";\n\t\t}\n\t\t// The no-models warning is a snapshot from whichever process created the\n\t\t// session; trust the live connection over it (e.g. credentials only\n\t\t// visible to the daemon, or added after the snapshot was taken).\n\t\tif (isNoModelsAvailableMessage(modelFallbackMessage) && this.getCurrentModel()) {\n\t\t\treturn \"suppress\";\n\t\t}\n\t\treturn \"show\";\n\t}\n\n\tprivate getOnboardingState(): OnboardingStartupState {\n\t\treturn {\n\t\t\tsettingsManager: this.settingsManager,\n\t\t\tmodelRegistry: this.modelRegistry,\n\t\t\tmodel: this.getCurrentModel(),\n\t\t};\n\t}\n\n\tprivate shouldRunOnboarding(): boolean {\n\t\treturn shouldRunOnboarding(this.getOnboardingState());\n\t}\n\n\tprivate shouldRunPrimeCliOnboardingSplash(): boolean {\n\t\treturn shouldRunPrimeCliOnboardingSplash(this.getOnboardingState());\n\t}\n\n\tprivate markOnboardingShown(): void {\n\t\tif (!this.settingsManager.getOnboardingShown()) {\n\t\t\tthis.settingsManager.setOnboardingShown(true);\n\t\t}\n\t}\n\n\tprivate async runStartupOnboarding(): Promise<boolean> {\n\t\tif (!this.shouldRunOnboarding()) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst startedAt = Date.now();\n\t\tconst showPrimeCliSplash = this.shouldRunPrimeCliOnboardingSplash();\n\t\tlet outcome: TelemetryOnboardingOutcome = \"aborted\";\n\t\ttry {\n\t\t\tthis.markOnboardingShown();\n\t\t\tawait this.settingsManager.flush();\n\t\t\tawait this.runOnboardingFlow(showPrimeCliSplash);\n\t\t\toutcome = isOnboardingModelReady(this.getOnboardingState()) ? \"success\" : \"aborted\";\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\toutcome = \"error\";\n\t\t\tthrow error;\n\t\t} finally {\n\t\t\tconst model = this.getCurrentModel();\n\t\t\tconst authStatus = model ? this.modelRegistry.getProviderAuthStatus(model.provider) : undefined;\n\t\t\tconst storedCredential = model ? this.modelRegistry.authStorage.get(model.provider) : undefined;\n\t\t\tvoid captureOnboardingCompleted({\n\t\t\t\tagentDir: getAgentDir(),\n\t\t\t\tsettingsManager: this.settingsManager,\n\t\t\t\tdurationMs: Date.now() - startedAt,\n\t\t\t\toutcome,\n\t\t\t\tprovider: model?.provider,\n\t\t\t\tauthSource: authStatus?.source,\n\t\t\t\tstoredCredentialType: storedCredential?.type,\n\t\t\t}).catch(() => {});\n\t\t}\n\t}\n\n\tprivate async showOnboardingModelSelection(splash: OnboardingSplashHandle): Promise<void> {\n\t\ttry {\n\t\t\tawait this.showConfigurationMenu(\"models\");\n\t\t} finally {\n\t\t\tsplash.dismiss();\n\t\t}\n\t}\n\n\tprivate async runOnboardingFlow(showPrimeCliSplash = this.shouldRunPrimeCliOnboardingSplash()): Promise<void> {\n\t\tthis.modelRegistry.refresh();\n\t\tif (showPrimeCliSplash) {\n\t\t\tconst splash = await this.showOnboardingSplash(\"choose a model\");\n\t\t\tif (!splash) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tawait this.showOnboardingModelSelection(splash);\n\t\t\treturn;\n\t\t}\n\n\t\tconst availableModels = await this.getModelCandidates();\n\t\tif (availableModels.length > 0) {\n\t\t\tawait this.showConfigurationMenu(\"models\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst splash = await this.showOnboardingSplash();\n\t\tif (!splash) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tawait this.showConfigurationMenu(\"providers\");\n\t\t} finally {\n\t\t\tsplash.dismiss();\n\t\t}\n\t}\n\n\tprivate getMarkdownThemeWithSettings(): MarkdownTheme {\n\t\treturn {\n\t\t\t...getMarkdownTheme(),\n\t\t\tcodeBlockIndent: this.settingsManager.getCodeBlockIndent(),\n\t\t};\n\t}\n\n\tprivate formatDisplayPath(p: string): string {\n\t\tconst home = os.homedir();\n\t\tlet result = p;\n\n\t\t// Replace home directory with ~\n\t\tif (result.startsWith(home)) {\n\t\t\tresult = `~${result.slice(home.length)}`;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tprivate formatExtensionDisplayPath(path: string): string {\n\t\tlet result = this.formatDisplayPath(path);\n\t\tresult = result.replace(/\\/index\\.ts$/, \"\").replace(/\\/index\\.js$/, \"\");\n\t\treturn result;\n\t}\n\n\tprivate formatContextPath(p: string): string {\n\t\tconst cwd = path.resolve(this.getCurrentCwd());\n\t\tconst absolutePath = path.isAbsolute(p) ? path.resolve(p) : path.resolve(cwd, p);\n\t\tconst relativePath = getCwdRelativePath(absolutePath, cwd);\n\t\tif (relativePath !== undefined) {\n\t\t\treturn relativePath;\n\t\t}\n\n\t\treturn this.formatDisplayPath(absolutePath);\n\t}\n\n\tprivate getStartupExpansionState(): boolean {\n\t\treturn this.options.verbose || this.toolOutputExpanded;\n\t}\n\n\tprivate getShortPath(fullPath: string, sourceInfo?: AgentConnectionSourceInfo): string {\n\t\tconst baseDir = sourceInfo?.baseDir;\n\t\tif (baseDir && this.isPackageSource(sourceInfo)) {\n\t\t\tconst relativePath = path.relative(path.resolve(baseDir), path.resolve(fullPath));\n\t\t\tif (\n\t\t\t\trelativePath &&\n\t\t\t\trelativePath !== \".\" &&\n\t\t\t\t!relativePath.startsWith(\"..\") &&\n\t\t\t\t!relativePath.startsWith(`..${path.sep}`) &&\n\t\t\t\t!path.isAbsolute(relativePath)\n\t\t\t) {\n\t\t\t\treturn relativePath.replace(/\\\\/g, \"/\");\n\t\t\t}\n\t\t}\n\n\t\tconst source = sourceInfo?.source ?? \"\";\n\t\tconst npmMatch = fullPath.match(/node_modules\\/(@?[^/]+(?:\\/[^/]+)?)\\/(.*)/);\n\t\tif (npmMatch && source.startsWith(\"npm:\")) {\n\t\t\treturn npmMatch[2];\n\t\t}\n\n\t\tconst gitMatch = fullPath.match(/git\\/[^/]+\\/[^/]+\\/(.*)/);\n\t\tif (gitMatch && source.startsWith(\"git:\")) {\n\t\t\treturn gitMatch[1];\n\t\t}\n\n\t\treturn this.formatDisplayPath(fullPath);\n\t}\n\n\tprivate getCompactPathLabel(resourcePath: string, sourceInfo?: AgentConnectionSourceInfo): string {\n\t\tconst shortPath = this.getShortPath(resourcePath, sourceInfo);\n\t\tconst normalizedPath = shortPath.replace(/\\\\/g, \"/\");\n\t\tconst segments = normalizedPath.split(\"/\").filter((segment) => segment.length > 0 && segment !== \"~\");\n\t\tif (segments.length > 0) {\n\t\t\treturn segments[segments.length - 1]!;\n\t\t}\n\t\treturn shortPath;\n\t}\n\n\tprivate getCompactPackageSourceLabel(sourceInfo?: AgentConnectionSourceInfo): string {\n\t\tconst source = sourceInfo?.source ?? \"\";\n\t\tif (source.startsWith(\"npm:\")) {\n\t\t\treturn source.slice(\"npm:\".length) || source;\n\t\t}\n\n\t\tconst gitSource = parseGitUrl(source);\n\t\tif (gitSource) {\n\t\t\treturn gitSource.path || source;\n\t\t}\n\n\t\treturn source;\n\t}\n\n\tprivate getCompactExtensionLabel(resourcePath: string, sourceInfo?: AgentConnectionSourceInfo): string {\n\t\tif (!this.isPackageSource(sourceInfo)) {\n\t\t\treturn this.getCompactPathLabel(resourcePath, sourceInfo);\n\t\t}\n\n\t\tconst sourceLabel = this.getCompactPackageSourceLabel(sourceInfo);\n\t\tif (!sourceLabel) {\n\t\t\treturn this.getCompactPathLabel(resourcePath, sourceInfo);\n\t\t}\n\n\t\tconst shortPath = this.getShortPath(resourcePath, sourceInfo).replace(/\\\\/g, \"/\");\n\t\tconst packagePath = shortPath.startsWith(\"extensions/\") ? shortPath.slice(\"extensions/\".length) : shortPath;\n\t\tconst parsedPath = path.posix.parse(packagePath);\n\n\t\tif (parsedPath.name === \"index\") {\n\t\t\treturn !parsedPath.dir || parsedPath.dir === \".\" ? sourceLabel : `${sourceLabel}:${parsedPath.dir}`;\n\t\t}\n\n\t\treturn `${sourceLabel}:${packagePath}`;\n\t}\n\n\tprivate getCompactDisplayPathSegments(resourcePath: string): string[] {\n\t\treturn this.formatDisplayPath(resourcePath)\n\t\t\t.replace(/\\\\/g, \"/\")\n\t\t\t.split(\"/\")\n\t\t\t.filter((segment) => segment.length > 0 && segment !== \"~\");\n\t}\n\n\tprivate getCompactNonPackageExtensionLabel(\n\t\tresourcePath: string,\n\t\tindex: number,\n\t\tallPaths: Array<{ path: string; segments: string[] }>,\n\t): string {\n\t\tconst segments = allPaths[index]?.segments;\n\t\tif (!segments || segments.length === 0) {\n\t\t\treturn this.getCompactPathLabel(resourcePath);\n\t\t}\n\n\t\tfor (let segmentCount = 1; segmentCount <= segments.length; segmentCount += 1) {\n\t\t\tconst candidate = segments.slice(-segmentCount).join(\"/\");\n\t\t\tconst isUnique = allPaths.every((item, itemIndex) => {\n\t\t\t\tif (itemIndex === index) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn item.segments.slice(-segmentCount).join(\"/\") !== candidate;\n\t\t\t});\n\n\t\t\tif (isUnique) {\n\t\t\t\treturn candidate;\n\t\t\t}\n\t\t}\n\n\t\treturn segments.join(\"/\");\n\t}\n\n\tprivate getCompactExtensionLabels(\n\t\textensions: Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>,\n\t): string[] {\n\t\tconst nonPackageExtensions = extensions\n\t\t\t.map((extension) => {\n\t\t\t\tconst segments = this.getCompactDisplayPathSegments(extension.path);\n\t\t\t\tconst lastSegment = segments[segments.length - 1];\n\t\t\t\tif (segments.length > 1 && (lastSegment === \"index.ts\" || lastSegment === \"index.js\")) {\n\t\t\t\t\tsegments.pop();\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tpath: extension.path,\n\t\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t\t\tsegments,\n\t\t\t\t};\n\t\t\t})\n\t\t\t.filter((extension) => !this.isPackageSource(extension.sourceInfo));\n\n\t\treturn extensions.map((extension) => {\n\t\t\tif (this.isPackageSource(extension.sourceInfo)) {\n\t\t\t\treturn this.getCompactExtensionLabel(extension.path, extension.sourceInfo);\n\t\t\t}\n\n\t\t\tconst nonPackageIndex = nonPackageExtensions.findIndex((item) => item.path === extension.path);\n\t\t\tif (nonPackageIndex === -1) {\n\t\t\t\treturn this.getCompactPathLabel(extension.path, extension.sourceInfo);\n\t\t\t}\n\n\t\t\treturn this.getCompactNonPackageExtensionLabel(extension.path, nonPackageIndex, nonPackageExtensions);\n\t\t});\n\t}\n\n\tprivate getDisplaySourceInfo(sourceInfo?: AgentConnectionSourceInfo): {\n\t\tlabel: string;\n\t\tscopeLabel?: string;\n\t\tcolor: \"accent\" | \"muted\";\n\t} {\n\t\tconst source = sourceInfo?.source ?? \"local\";\n\t\tconst scope = sourceInfo?.scope ?? \"project\";\n\t\tif (source === \"local\") {\n\t\t\tif (scope === \"user\") {\n\t\t\t\treturn { label: \"user\", color: \"muted\" };\n\t\t\t}\n\t\t\tif (scope === \"project\") {\n\t\t\t\treturn { label: \"project\", color: \"muted\" };\n\t\t\t}\n\t\t\tif (scope === \"temporary\") {\n\t\t\t\treturn { label: \"path\", scopeLabel: \"temp\", color: \"muted\" };\n\t\t\t}\n\t\t\treturn { label: \"path\", color: \"muted\" };\n\t\t}\n\n\t\tif (source === \"cli\") {\n\t\t\treturn { label: \"path\", scopeLabel: scope === \"temporary\" ? \"temp\" : undefined, color: \"muted\" };\n\t\t}\n\n\t\tconst scopeLabel =\n\t\t\tscope === \"user\" ? \"user\" : scope === \"project\" ? \"project\" : scope === \"temporary\" ? \"temp\" : undefined;\n\t\treturn { label: source, scopeLabel, color: \"accent\" };\n\t}\n\n\tprivate getScopeGroup(sourceInfo?: AgentConnectionSourceInfo): \"user\" | \"project\" | \"path\" {\n\t\tconst source = sourceInfo?.source ?? \"local\";\n\t\tconst scope = sourceInfo?.scope ?? \"project\";\n\t\tif (source === \"cli\" || scope === \"temporary\") return \"path\";\n\t\tif (scope === \"user\") return \"user\";\n\t\tif (scope === \"project\") return \"project\";\n\t\treturn \"path\";\n\t}\n\n\tprivate isPackageSource(sourceInfo?: AgentConnectionSourceInfo): boolean {\n\t\tconst source = sourceInfo?.source ?? \"\";\n\t\treturn source.startsWith(\"npm:\") || source.startsWith(\"git:\");\n\t}\n\n\tprivate buildScopeGroups(items: Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>): Array<{\n\t\tscope: \"user\" | \"project\" | \"path\";\n\t\tpaths: Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>;\n\t\tpackages: Map<string, Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>>;\n\t}> {\n\t\tconst groups: Record<\n\t\t\t\"user\" | \"project\" | \"path\",\n\t\t\t{\n\t\t\t\tscope: \"user\" | \"project\" | \"path\";\n\t\t\t\tpaths: Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>;\n\t\t\t\tpackages: Map<string, Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>>;\n\t\t\t}\n\t\t> = {\n\t\t\tuser: { scope: \"user\", paths: [], packages: new Map() },\n\t\t\tproject: { scope: \"project\", paths: [], packages: new Map() },\n\t\t\tpath: { scope: \"path\", paths: [], packages: new Map() },\n\t\t};\n\n\t\tfor (const item of items) {\n\t\t\tconst groupKey = this.getScopeGroup(item.sourceInfo);\n\t\t\tconst group = groups[groupKey];\n\t\t\tconst source = item.sourceInfo?.source ?? \"local\";\n\n\t\t\tif (this.isPackageSource(item.sourceInfo)) {\n\t\t\t\tconst list = group.packages.get(source) ?? [];\n\t\t\t\tlist.push(item);\n\t\t\t\tgroup.packages.set(source, list);\n\t\t\t} else {\n\t\t\t\tgroup.paths.push(item);\n\t\t\t}\n\t\t}\n\n\t\treturn [groups.project, groups.user, groups.path].filter(\n\t\t\t(group) => group.paths.length > 0 || group.packages.size > 0,\n\t\t);\n\t}\n\n\tprivate formatScopeGroups(\n\t\tgroups: Array<{\n\t\t\tscope: \"user\" | \"project\" | \"path\";\n\t\t\tpaths: Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>;\n\t\t\tpackages: Map<string, Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>>;\n\t\t}>,\n\t\toptions: {\n\t\t\tformatPath: (item: { path: string; sourceInfo?: AgentConnectionSourceInfo }) => string;\n\t\t\tformatPackagePath: (item: { path: string; sourceInfo?: AgentConnectionSourceInfo }, source: string) => string;\n\t\t},\n\t): string {\n\t\tconst lines: string[] = [];\n\n\t\tfor (const group of groups) {\n\t\t\tlines.push(` ${theme.fg(\"accent\", group.scope)}`);\n\n\t\t\tconst sortedPaths = [...group.paths].sort((a, b) => a.path.localeCompare(b.path));\n\t\t\tfor (const item of sortedPaths) {\n\t\t\t\tlines.push(theme.fg(\"dim\", ` ${options.formatPath(item)}`));\n\t\t\t}\n\n\t\t\tconst sortedPackages = Array.from(group.packages.entries()).sort(([a], [b]) => a.localeCompare(b));\n\t\t\tfor (const [source, items] of sortedPackages) {\n\t\t\t\tlines.push(` ${theme.fg(\"mdLink\", source)}`);\n\t\t\t\tconst sortedPackagePaths = [...items].sort((a, b) => a.path.localeCompare(b.path));\n\t\t\t\tfor (const item of sortedPackagePaths) {\n\t\t\t\t\tlines.push(theme.fg(\"dim\", ` ${options.formatPackagePath(item, source)}`));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn lines.join(\"\\n\");\n\t}\n\n\tprivate findSourceInfoForPath(\n\t\tp: string,\n\t\tsourceInfos: Map<string, AgentConnectionSourceInfo>,\n\t): AgentConnectionSourceInfo | undefined {\n\t\tconst exact = sourceInfos.get(p);\n\t\tif (exact) return exact;\n\n\t\tlet current = p;\n\t\twhile (current.includes(\"/\")) {\n\t\t\tcurrent = current.substring(0, current.lastIndexOf(\"/\"));\n\t\t\tconst parent = sourceInfos.get(current);\n\t\t\tif (parent) return parent;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\tprivate formatPathWithSource(p: string, sourceInfo?: AgentConnectionSourceInfo): string {\n\t\tif (sourceInfo) {\n\t\t\tconst shortPath = this.getShortPath(p, sourceInfo);\n\t\t\tconst { label, scopeLabel } = this.getDisplaySourceInfo(sourceInfo);\n\t\t\tconst labelText = scopeLabel ? `${label} (${scopeLabel})` : label;\n\t\t\treturn `${labelText} ${shortPath}`;\n\t\t}\n\t\treturn this.formatDisplayPath(p);\n\t}\n\n\tprivate formatDiagnostics(\n\t\tdiagnostics: readonly AgentConnectionResourceDiagnostic[],\n\t\tsourceInfos: Map<string, AgentConnectionSourceInfo>,\n\t): string {\n\t\tconst lines: string[] = [];\n\n\t\t// Group collision diagnostics by name\n\t\tconst collisions = new Map<string, AgentConnectionResourceDiagnostic[]>();\n\t\tconst otherDiagnostics: AgentConnectionResourceDiagnostic[] = [];\n\n\t\tfor (const d of diagnostics) {\n\t\t\tif (d.type === \"collision\" && d.collision) {\n\t\t\t\tconst list = collisions.get(d.collision.name) ?? [];\n\t\t\t\tlist.push(d);\n\t\t\t\tcollisions.set(d.collision.name, list);\n\t\t\t} else {\n\t\t\t\totherDiagnostics.push(d);\n\t\t\t}\n\t\t}\n\n\t\t// Format collision diagnostics grouped by name\n\t\tfor (const [name, collisionList] of collisions) {\n\t\t\tconst first = collisionList[0]?.collision;\n\t\t\tif (!first) continue;\n\t\t\tlines.push(theme.fg(\"warning\", ` \"${name}\" collision:`));\n\t\t\tlines.push(\n\t\t\t\ttheme.fg(\n\t\t\t\t\t\"dim\",\n\t\t\t\t\t` ${theme.fg(\"success\", \"✓\")} ${this.formatPathWithSource(first.winnerPath, this.findSourceInfoForPath(first.winnerPath, sourceInfos))}`,\n\t\t\t\t),\n\t\t\t);\n\t\t\tfor (const d of collisionList) {\n\t\t\t\tif (d.collision) {\n\t\t\t\t\tlines.push(\n\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t\t` ${theme.fg(\"warning\", \"✗\")} ${this.formatPathWithSource(d.collision.loserPath, this.findSourceInfoForPath(d.collision.loserPath, sourceInfos))} (skipped)`,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst formatMessageLines = (diagnostic: AgentConnectionResourceDiagnostic, indent: number): string[] => {\n\t\t\tconst color = diagnostic.type === \"error\" ? \"error\" : \"warning\";\n\t\t\tconst prefix = \" \".repeat(indent);\n\t\t\treturn diagnostic.message.split(\"\\n\").map((line) => theme.fg(color, `${prefix}${line}`));\n\t\t};\n\n\t\tfor (const d of otherDiagnostics) {\n\t\t\tif (d.path) {\n\t\t\t\tconst formattedPath = this.formatPathWithSource(d.path, this.findSourceInfoForPath(d.path, sourceInfos));\n\t\t\t\tlines.push(theme.fg(d.type === \"error\" ? \"error\" : \"warning\", ` ${formattedPath}`));\n\t\t\t\tlines.push(...formatMessageLines(d, 4));\n\t\t\t} else {\n\t\t\t\tlines.push(...formatMessageLines(d, 2));\n\t\t\t}\n\t\t}\n\n\t\treturn lines.join(\"\\n\");\n\t}\n\n\tprivate showLoadedResources(options?: {\n\t\textensions?: Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>;\n\t\tforce?: boolean;\n\t\tshowDiagnosticsWhenQuiet?: boolean;\n\t}): void {\n\t\tconst showListing = options?.force === true || this.options.verbose === true;\n\t\tconst showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true;\n\t\tif (!showListing && !showDiagnostics) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst sectionHeader = (name: string, color: ThemeColor = \"mdHeading\") => theme.fg(color, `[${name}]`);\n\t\tconst diagnosticsHeader = (name: string, diagnostics: readonly AgentConnectionResourceDiagnostic[]): string => {\n\t\t\tif (diagnostics.some((diagnostic) => diagnostic.type === \"collision\")) {\n\t\t\t\treturn `${name} conflicts`;\n\t\t\t}\n\n\t\t\tconst errorCount = diagnostics.filter((diagnostic) => diagnostic.type === \"error\").length;\n\t\t\tconst warningCount = diagnostics.filter((diagnostic) => diagnostic.type === \"warning\").length;\n\t\t\tif (errorCount > 0 && warningCount > 0) {\n\t\t\t\treturn `${name} diagnostics`;\n\t\t\t}\n\t\t\tif (errorCount > 0) {\n\t\t\t\treturn `${name} error${errorCount === 1 ? \"\" : \"s\"}`;\n\t\t\t}\n\t\t\tif (warningCount > 0) {\n\t\t\t\treturn `${name} warning${warningCount === 1 ? \"\" : \"s\"}`;\n\t\t\t}\n\n\t\t\treturn `${name} diagnostics`;\n\t\t};\n\t\tconst formatCompactList = (items: string[], options?: { sort?: boolean }): string => {\n\t\t\tconst labels = items.map((item) => item.trim()).filter((item) => item.length > 0);\n\t\t\tif (options?.sort !== false) {\n\t\t\t\tlabels.sort((a, b) => a.localeCompare(b));\n\t\t\t}\n\t\t\treturn theme.fg(\"dim\", ` ${labels.join(\", \")}`);\n\t\t};\n\t\tconst addLoadedSection = (\n\t\t\tname: string,\n\t\t\tcollapsedBody: string,\n\t\t\texpandedBody = collapsedBody,\n\t\t\tcolor: ThemeColor = \"mdHeading\",\n\t\t): void => {\n\t\t\tconst section = new ExpandableText(\n\t\t\t\t() => `${sectionHeader(name, color)}\\n${collapsedBody}`,\n\t\t\t\t() => `${sectionHeader(name, color)}\\n${expandedBody}`,\n\t\t\t\tthis.getStartupExpansionState(),\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t);\n\t\t\tthis.chatContainer.addChild(section);\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t};\n\n\t\tconst resourceSnapshot = this.connectionResourceSnapshot;\n\t\tconst skills = resourceSnapshot?.skills ?? [];\n\t\tconst prompts = resourceSnapshot?.prompts ?? [];\n\t\tconst loadedThemes = resourceSnapshot?.themes ?? [];\n\t\tconst contextFiles = resourceSnapshot?.contextFiles ?? [];\n\t\tconst extensions = options?.extensions ?? resourceSnapshot?.extensions ?? [];\n\t\tconst sourceInfos = new Map<string, AgentConnectionSourceInfo>();\n\t\tfor (const extension of extensions) {\n\t\t\tif (extension.sourceInfo) {\n\t\t\t\tsourceInfos.set(extension.path, extension.sourceInfo);\n\t\t\t}\n\t\t}\n\t\tfor (const skill of skills) {\n\t\t\tif (skill.sourceInfo) {\n\t\t\t\tsourceInfos.set(skill.filePath, skill.sourceInfo);\n\t\t\t}\n\t\t}\n\t\tfor (const prompt of prompts) {\n\t\t\tif (prompt.sourceInfo) {\n\t\t\t\tsourceInfos.set(prompt.filePath, prompt.sourceInfo);\n\t\t\t}\n\t\t}\n\t\tfor (const loadedTheme of loadedThemes) {\n\t\t\tif (loadedTheme.sourcePath && loadedTheme.sourceInfo) {\n\t\t\t\tsourceInfos.set(loadedTheme.sourcePath, loadedTheme.sourceInfo);\n\t\t\t}\n\t\t}\n\n\t\tif (showListing) {\n\t\t\tif (contextFiles.length > 0) {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tconst contextList = contextFiles\n\t\t\t\t\t.map((f) => theme.fg(\"dim\", ` ${this.formatDisplayPath(f.path)}`))\n\t\t\t\t\t.join(\"\\n\");\n\t\t\t\tconst contextCompactList = formatCompactList(\n\t\t\t\t\tcontextFiles.map((contextFile) => this.formatContextPath(contextFile.path)),\n\t\t\t\t\t{ sort: false },\n\t\t\t\t);\n\t\t\t\taddLoadedSection(\"Context\", contextCompactList, contextList);\n\t\t\t}\n\n\t\t\tif (skills.length > 0) {\n\t\t\t\tconst groups = this.buildScopeGroups(\n\t\t\t\t\tskills.map((skill) => ({ path: skill.filePath, sourceInfo: skill.sourceInfo })),\n\t\t\t\t);\n\t\t\t\tconst skillList = this.formatScopeGroups(groups, {\n\t\t\t\t\tformatPath: (item) => this.formatDisplayPath(item.path),\n\t\t\t\t\tformatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),\n\t\t\t\t});\n\t\t\t\tconst skillCompactList = formatCompactList(skills.map((skill) => skill.name));\n\t\t\t\taddLoadedSection(\"Skills\", skillCompactList, skillList);\n\t\t\t}\n\n\t\t\tif (prompts.length > 0) {\n\t\t\t\tconst groups = this.buildScopeGroups(\n\t\t\t\t\tprompts.map((template) => ({ path: template.filePath, sourceInfo: template.sourceInfo })),\n\t\t\t\t);\n\t\t\t\tconst templateByPath = new Map(prompts.map((t) => [t.filePath, t]));\n\t\t\t\tconst templateList = this.formatScopeGroups(groups, {\n\t\t\t\t\tformatPath: (item) => {\n\t\t\t\t\t\tconst template = templateByPath.get(item.path);\n\t\t\t\t\t\treturn template ? `/${template.name}` : this.formatDisplayPath(item.path);\n\t\t\t\t\t},\n\t\t\t\t\tformatPackagePath: (item) => {\n\t\t\t\t\t\tconst template = templateByPath.get(item.path);\n\t\t\t\t\t\treturn template ? `/${template.name}` : this.formatDisplayPath(item.path);\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t\tconst promptCompactList = formatCompactList(prompts.map((template) => `/${template.name}`));\n\t\t\t\taddLoadedSection(\"Prompts\", promptCompactList, templateList);\n\t\t\t}\n\n\t\t\tif (extensions.length > 0) {\n\t\t\t\tconst groups = this.buildScopeGroups(extensions);\n\t\t\t\tconst extList = this.formatScopeGroups(groups, {\n\t\t\t\t\tformatPath: (item) => this.formatExtensionDisplayPath(item.path),\n\t\t\t\t\tformatPackagePath: (item) =>\n\t\t\t\t\t\tthis.formatExtensionDisplayPath(this.getShortPath(item.path, item.sourceInfo)),\n\t\t\t\t});\n\t\t\t\tconst extensionCompactList = formatCompactList(this.getCompactExtensionLabels(extensions));\n\t\t\t\taddLoadedSection(\"Extensions\", extensionCompactList, extList, \"mdHeading\");\n\t\t\t}\n\n\t\t\t// Show loaded themes (excluding built-in)\n\t\t\tconst customThemes = loadedThemes.filter((t) => t.sourcePath);\n\t\t\tif (customThemes.length > 0) {\n\t\t\t\tconst groups = this.buildScopeGroups(\n\t\t\t\t\tcustomThemes.map((loadedTheme) => ({\n\t\t\t\t\t\tpath: loadedTheme.sourcePath!,\n\t\t\t\t\t\tsourceInfo: loadedTheme.sourceInfo,\n\t\t\t\t\t})),\n\t\t\t\t);\n\t\t\t\tconst themeList = this.formatScopeGroups(groups, {\n\t\t\t\t\tformatPath: (item) => this.formatDisplayPath(item.path),\n\t\t\t\t\tformatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),\n\t\t\t\t});\n\t\t\t\tconst themeCompactList = formatCompactList(\n\t\t\t\t\tcustomThemes.map(\n\t\t\t\t\t\t(loadedTheme) =>\n\t\t\t\t\t\t\tloadedTheme.name ?? this.getCompactPathLabel(loadedTheme.sourcePath!, loadedTheme.sourceInfo),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\taddLoadedSection(\"Themes\", themeCompactList, themeList);\n\t\t\t}\n\t\t}\n\n\t\tif (showDiagnostics) {\n\t\t\tconst skillDiagnostics = resourceSnapshot?.diagnostics.skills ?? [];\n\t\t\tif (skillDiagnostics.length > 0) {\n\t\t\t\tconst warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos);\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(\n\t\t\t\t\t\t`${sectionHeader(diagnosticsHeader(\"Skill\", skillDiagnostics), \"warning\")}\\n${warningLines}`,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t}\n\n\t\t\tconst promptDiagnostics = resourceSnapshot?.diagnostics.prompts ?? [];\n\t\t\tif (promptDiagnostics.length > 0) {\n\t\t\t\tconst warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos);\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(\n\t\t\t\t\t\t`${sectionHeader(diagnosticsHeader(\"Prompt\", promptDiagnostics), \"warning\")}\\n${warningLines}`,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t}\n\n\t\t\tconst extensionDiagnostics: AgentConnectionResourceDiagnostic[] = [\n\t\t\t\t...(resourceSnapshot?.diagnostics.extensions ?? []),\n\t\t\t];\n\n\t\t\tif (this.bindLocalSessionExtensions) {\n\t\t\t\tconst commandDiagnostics = this.getLocalSessionHost().getExtensionRunner().getCommandDiagnostics();\n\t\t\t\textensionDiagnostics.push(...commandDiagnostics);\n\t\t\t}\n\t\t\textensionDiagnostics.push(...this.getBuiltInCommandConflictDiagnostics(this.connectionCommands));\n\n\t\t\tif (this.bindLocalSessionExtensions) {\n\t\t\t\tconst shortcutDiagnostics = this.getLocalSessionHost().getExtensionRunner().getShortcutDiagnostics();\n\t\t\t\textensionDiagnostics.push(...shortcutDiagnostics);\n\t\t\t}\n\n\t\t\tif (extensionDiagnostics.length > 0) {\n\t\t\t\tconst warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos);\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(\n\t\t\t\t\t\t`${sectionHeader(diagnosticsHeader(\"Extension\", extensionDiagnostics), \"warning\")}\\n${warningLines}`,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t}\n\n\t\t\tconst themeDiagnostics = resourceSnapshot?.diagnostics.themes ?? [];\n\t\t\tif (themeDiagnostics.length > 0) {\n\t\t\t\tconst warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos);\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(\n\t\t\t\t\t\t`${sectionHeader(diagnosticsHeader(\"Theme\", themeDiagnostics), \"warning\")}\\n${warningLines}`,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Initialize the extension system with TUI-based UI context.\n\t */\n\tprivate async bindCurrentSessionExtensions(): Promise<void> {\n\t\tconst localSessionHost = this.getLocalSessionHost();\n\t\tconst uiContext = this.createExtensionUIContext();\n\t\tawait localSessionHost.bindExtensions({\n\t\t\tuiContext,\n\t\t\tcommandContextActions: {\n\t\t\t\twaitForIdle: () => this.agentConnection.waitForIdle(),\n\t\t\t\tnewSession: async (options) => {\n\t\t\t\t\tthis.stopWorkingLoader();\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result =\n\t\t\t\t\t\t\toptions?.setup || options?.withSession\n\t\t\t\t\t\t\t\t? await localSessionHost.newSession(options)\n\t\t\t\t\t\t\t\t: await this.agentConnection.newSession(\n\t\t\t\t\t\t\t\t\t\toptions?.parentSession ? { parentSession: options.parentSession } : undefined,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\tif (!result.cancelled) {\n\t\t\t\t\t\t\tawait this.renderCurrentSessionState();\n\t\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t} catch (error: unknown) {\n\t\t\t\t\t\treturn this.handleFatalRuntimeError(\"Failed to create session\", error);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tfork: async (entryId, options) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = options?.withSession\n\t\t\t\t\t\t\t? await localSessionHost.fork(entryId, options)\n\t\t\t\t\t\t\t: await this.agentConnection.fork(entryId, { position: options?.position });\n\t\t\t\t\t\tif (!result.cancelled) {\n\t\t\t\t\t\t\tawait this.renderCurrentSessionState();\n\t\t\t\t\t\t\tthis.editor.setText(\"selectedText\" in result ? (result.selectedText ?? \"\") : \"\");\n\t\t\t\t\t\t\tthis.showStatus(\"Forked to new session\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn { cancelled: result.cancelled };\n\t\t\t\t\t} catch (error: unknown) {\n\t\t\t\t\t\treturn this.handleFatalRuntimeError(\"Failed to fork session\", error);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tnavigateTree: async (targetId, options) => {\n\t\t\t\t\tconst result = await this.agentConnection.navigateTree(targetId, {\n\t\t\t\t\t\tsummarize: options?.summarize,\n\t\t\t\t\t\tcustomInstructions: options?.customInstructions,\n\t\t\t\t\t\treplaceInstructions: options?.replaceInstructions,\n\t\t\t\t\t\tlabel: options?.label,\n\t\t\t\t\t});\n\t\t\t\t\tif (result.cancelled) {\n\t\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t\t}\n\n\t\t\t\t\tawait this.renderTreeNavigation(result);\n\t\t\t\t\treturn { cancelled: false };\n\t\t\t\t},\n\t\t\t\tswitchSession: async (sessionPath, options) => {\n\t\t\t\t\treturn this.handleResumeSession(sessionPath, options);\n\t\t\t\t},\n\t\t\t\treload: async () => {\n\t\t\t\t\tawait this.handleReloadCommand();\n\t\t\t\t},\n\t\t\t},\n\t\t\tshutdownHandler: () => {\n\t\t\t\tthis.shutdownRequested = true;\n\t\t\t\tif (!this.isAgentStreaming()) {\n\t\t\t\t\tvoid this.shutdown();\n\t\t\t\t}\n\t\t\t},\n\t\t\tonError: (error) => {\n\t\t\t\tthis.showExtensionError(error.extensionPath, error.error, error.stack);\n\t\t\t},\n\t\t});\n\n\t\tsetRegisteredThemes(this.uiServices.getThemes());\n\t\tawait this.refreshConnectionCatalog();\n\t\tthis.setupAutocompleteProvider();\n\n\t\tconst extensionRunner = localSessionHost.getExtensionRunner();\n\t\tthis.setupExtensionShortcuts(extensionRunner);\n\t\tthis.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });\n\t}\n\n\tprivate applyRuntimeSettings(): void {\n\t\tthis.footer.setAutoCompactEnabled(\n\t\t\tthis.connectionState?.autoCompactionEnabled ?? this.settingsManager.getCompactionEnabled(),\n\t\t);\n\t\tthis.footerDataProvider.setCwd(this.getCurrentCwd());\n\t\tthis.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();\n\t\tthis.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());\n\t\tthis.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());\n\t\tconst editorPaddingX = this.settingsManager.getEditorPaddingX();\n\t\tconst autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();\n\t\tthis.defaultEditor.setPaddingX(editorPaddingX);\n\t\tthis.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);\n\t\tif (this.editor !== this.defaultEditor) {\n\t\t\tthis.editor.setPaddingX?.(editorPaddingX);\n\t\t\tthis.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);\n\t\t}\n\t}\n\n\tprivate async refreshConnectionQueue(): Promise<void> {\n\t\tthis.replaceConnectionQueue(await this.agentConnection.getQueue());\n\t}\n\n\tprivate replaceConnectionQueue(queue: AgentConnectionQueueState): void {\n\t\tthis.connectionQueue = {\n\t\t\tsteering: [...queue.steering],\n\t\t\tfollowUp: [...queue.followUp],\n\t\t};\n\t\tconst dropped = this.queueSelection.sync(this.connectionQueue);\n\t\tif (dropped !== undefined) {\n\t\t\tconst editorText = this.editor.getText();\n\t\t\tif (editorText === dropped) {\n\t\t\t\tthis.setEditorTextFromQueueSelection(this.queueSelection.reset());\n\t\t\t} else if (!this.pendingQueueEdit) {\n\t\t\t\tthis.queueSelection.replaceDraft(editorText);\n\t\t\t}\n\t\t}\n\t\tthis.updatePendingMessagesDisplay();\n\t}\n\n\tprivate async refreshConnectionCatalog(): Promise<void> {\n\t\tthis.invalidateConnectionModelRefresh();\n\t\tconst [state, commands, modelCatalog, resources] = await Promise.all([\n\t\t\tthis.agentConnection.getState(),\n\t\t\tthis.agentConnection.getCommands().catch(() => []),\n\t\t\tthis.agentConnection.getModelCatalog(),\n\t\t\tthis.agentConnection.getResourceSnapshot(),\n\t\t]);\n\t\tthis.applyConnectionStateSnapshot(state);\n\t\tthis.connectionCommands = commands;\n\t\tthis.applyConnectionModelCatalog(modelCatalog);\n\t\tthis.connectionModelsFetchedAt = Date.now();\n\t\tthis.connectionResourceSnapshot = resources;\n\t}\n\n\tprivate refreshHeartbeatCatalog(): Promise<void> {\n\t\tif (this.heartbeatRefreshPromise) {\n\t\t\tthis.heartbeatRefreshRequested = true;\n\t\t\treturn this.heartbeatRefreshPromise;\n\t\t}\n\t\tconst connection = this.agentConnection;\n\t\tconst refresh = (async () => {\n\t\t\tdo {\n\t\t\t\tthis.heartbeatRefreshRequested = false;\n\t\t\t\tconst heartbeats = await connection.listHeartbeats();\n\t\t\t\tif (this.agentConnection !== connection) return;\n\t\t\t\tthis.applyHeartbeatCatalog(heartbeats);\n\t\t\t} while (this.heartbeatRefreshRequested);\n\t\t})().finally(() => {\n\t\t\tif (this.heartbeatRefreshPromise === refresh) {\n\t\t\t\tthis.heartbeatRefreshPromise = undefined;\n\t\t\t}\n\t\t});\n\t\tthis.heartbeatRefreshPromise = refresh;\n\t\treturn refresh;\n\t}\n\n\tprivate applyHeartbeatCatalog(heartbeats: AgentConnectionHeartbeat[]): void {\n\t\tthis.heartbeatCatalog = heartbeats;\n\t\tthis.updateScopedHeartbeats();\n\t}\n\n\tprivate updateScopedHeartbeats(): void {\n\t\tconst heartbeats = scopeHeartbeatsToSession(\n\t\t\tthis.heartbeatCatalog,\n\t\t\tthis.connectionState,\n\t\t\tthis.subagentSnapshots.values(),\n\t\t);\n\t\tif (\n\t\t\theartbeats.length === this.heartbeats.length &&\n\t\t\theartbeats.every((heartbeat, index) => heartbeat === this.heartbeats[index])\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tthis.heartbeats = heartbeats;\n\t\tthis.heartbeatManager?.setHeartbeats(heartbeats);\n\t\tthis.scheduleHeartbeatManagerRefresh();\n\t\tthis.updateSubagentSummaryLine();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate applyConnectionStateSnapshot(state: AgentConnectionState): void {\n\t\tthis.bindPromptStashSession(state.sessionId);\n\t\tthis.connectionState = state;\n\t\tthis.updateScopedHeartbeats();\n\t\t// Don't touch contextUsageTokenBaseline: a mid-stream snapshot reflects only completed\n\t\t// turns (the in-flight message isn't persisted yet), so the in-flight delta must keep\n\t\t// accumulating. The baseline is managed at turn end (refreshConnectionContextUsage) and\n\t\t// reset on a new user message.\n\t\tthis.footer.setAutoCompactEnabled(state.autoCompactionEnabled);\n\t\tthis.sessionRecap = state.recap;\n\t\tthis.renderRecap();\n\t\tthis.updateWorkingPulse();\n\t}\n\n\tprivate patchConnectionState(patch: Partial<AgentConnectionState>): void {\n\t\tif (!this.connectionState) {\n\t\t\treturn;\n\t\t}\n\t\tthis.connectionState = { ...this.connectionState, ...patch };\n\t\tthis.updateWorkingPulse();\n\t}\n\n\t// Bake this attempt's output into the snapshot so the tray doesn't dip in the gap between\n\t// isStreaming clearing and the async refresh landing.\n\tprivate applyOptimisticContextUsage(): void {\n\t\tconst snapshot = this.connectionState?.contextUsage;\n\t\tif (!snapshot || snapshot.tokens === null || snapshot.contextWindow <= 0) return;\n\t\tconst completed = Math.max(0, this.activityTracker.getStatus().tokens - this.contextUsageTokenBaseline);\n\t\tif (completed <= 0) return;\n\t\tconst tokens = snapshot.tokens + completed;\n\t\tthis.patchConnectionState({\n\t\t\tcontextUsage: {\n\t\t\t\ttokens,\n\t\t\t\tcontextWindow: snapshot.contextWindow,\n\t\t\t\tpercent: (tokens / snapshot.contextWindow) * 100,\n\t\t\t},\n\t\t});\n\t}\n\n\t/** Refresh the tray's context usage from the session after a turn or compaction completes. */\n\tprivate async refreshConnectionContextUsage(): Promise<void> {\n\t\tconst generation = ++this.contextUsageRefresh.generation;\n\t\tconst connection = this.agentConnection;\n\t\tconst sessionId = this.connectionState?.sessionId;\n\t\tconst stats = await connection?.getSessionStats?.().catch(() => undefined);\n\t\tif (!stats) return;\n\t\t// Drop results superseded by a newer successful refresh as well as results for a replaced session.\n\t\tif (\n\t\t\tgeneration < this.contextUsageRefresh.lastSuccessGeneration ||\n\t\t\tthis.agentConnection !== connection ||\n\t\t\tthis.connectionState?.sessionId !== sessionId\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tthis.contextUsageRefresh.lastSuccessGeneration = generation;\n\t\t// Anything counted so far is now reflected in the snapshot; only later output is in-flight.\n\t\tthis.contextUsageTokenBaseline = this.activityTracker.getStatus().tokens;\n\t\tthis.patchConnectionState({ contextUsage: stats.contextUsage });\n\t}\n\n\tprivate updateConnectionStateFromEvent(event: AgentConnectionSessionEvent): void {\n\t\tif (!this.connectionState) {\n\t\t\treturn;\n\t\t}\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\t\tthis.patchConnectionState({ isStreaming: true, activeToolNames: [] });\n\t\t\t\tbreak;\n\t\t\tcase \"agent_end\":\n\t\t\t\tthis.patchConnectionState({ isStreaming: false, activeToolNames: [] });\n\t\t\t\tbreak;\n\t\t\tcase \"session_action_update\":\n\t\t\t\tthis.patchConnectionState({ sessionActions: event.actions });\n\t\t\t\tbreak;\n\t\t\tcase \"compaction_start\":\n\t\t\t\tthis.patchConnectionState({ isCompacting: true });\n\t\t\t\tbreak;\n\t\t\tcase \"compaction_end\":\n\t\t\t\tthis.patchConnectionState({ isCompacting: false });\n\t\t\t\tbreak;\n\t\t\tcase \"session_info_changed\":\n\t\t\t\tthis.patchConnectionState({ sessionName: event.name });\n\t\t\t\tbreak;\n\t\t\tcase \"thinking_level_changed\":\n\t\t\t\tthis.patchConnectionState({ thinkingLevel: event.level });\n\t\t\t\tbreak;\n\t\t\tcase \"service_tier_changed\":\n\t\t\t\tthis.patchConnectionState({ serviceTier: event.serviceTier });\n\t\t\t\tbreak;\n\t\t\tcase \"auto_retry_start\":\n\t\t\t\tthis.patchConnectionState({ retryAttempt: event.attempt });\n\t\t\t\tbreak;\n\t\t\tcase \"auto_retry_end\":\n\t\t\t\tthis.patchConnectionState({ retryAttempt: 0 });\n\t\t\t\tbreak;\n\t\t\tcase \"goal_update\":\n\t\t\t\tthis.patchConnectionState({ goal: event.goal });\n\t\t\t\tbreak;\n\t\t\tcase \"bash_start\":\n\t\t\t\tthis.patchConnectionState({ isBashRunning: true });\n\t\t\t\tbreak;\n\t\t\tcase \"bash_end\":\n\t\t\t\tthis.patchConnectionState({ isBashRunning: false });\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate getCurrentCwd(): string {\n\t\treturn this.connectionState?.cwd ?? this.uiServices.getInitialCwd();\n\t}\n\n\tprivate getCurrentSessionName(): string | undefined {\n\t\treturn this.connectionState?.sessionName ?? this.uiServices.getInitialSessionName();\n\t}\n\n\tprivate applyAuthStaleEvent(event: Extract<AgentConnectionSessionEvent, { type: \"auth_stale\" }>): void {\n\t\tlet marked = false;\n\t\tfor (const token of event.sourceTokens ?? []) {\n\t\t\tmarked = this.modelRegistry.markProviderAuthSourceStale(token) || marked;\n\t\t}\n\t\tif (!marked) {\n\t\t\tthis.modelRegistry.markProviderAuthStale(event.provider);\n\t\t}\n\t\tthis.footer.invalidate();\n\t\tthis.updateEditorBorderColor();\n\t}\n\n\tprivate getCurrentModel(): AgentConnectionModel | undefined {\n\t\treturn this.connectionState?.model;\n\t}\n\n\tprivate getCurrentModelId(): string | undefined {\n\t\treturn this.getCurrentModel()?.id;\n\t}\n\n\tprivate isAgentStreaming(): boolean {\n\t\treturn this.connectionState?.isStreaming ?? false;\n\t}\n\n\tprivate isAgentCompacting(): boolean {\n\t\treturn this.connectionState?.isCompacting ?? false;\n\t}\n\n\tprivate isBashRunning(): boolean {\n\t\treturn this.connectionState?.isBashRunning ?? false;\n\t}\n\n\tprivate hasInterruptibleWork(): boolean {\n\t\treturn (\n\t\t\tthis.isAgentStreaming() ||\n\t\t\tthis.isAgentCompacting() ||\n\t\t\tthis.isBashRunning() ||\n\t\t\tthis.getRetryAttempt() > 0 ||\n\t\t\tthis.connectionState?.sessionActions.active !== undefined ||\n\t\t\tthis.traceUploadAllAbortController !== undefined ||\n\t\t\tthis.sideQuestionEvent?.status === \"running\"\n\t\t);\n\t}\n\n\tprivate getRetryAttempt(): number {\n\t\treturn this.connectionState?.retryAttempt ?? 0;\n\t}\n\n\tprivate getQueuedActionCount(): number {\n\t\treturn this.connectionState?.sessionActions.queuedCount ?? 0;\n\t}\n\n\tprivate getGoalState(): GoalState {\n\t\treturn this.connectionState?.goal ?? emptyGoalState();\n\t}\n\n\tprivate getConnectionContextUsage(): AgentConnectionState[\"contextUsage\"] {\n\t\tconst snapshot = this.connectionState?.contextUsage;\n\t\tif (!snapshot || snapshot.tokens === null || snapshot.contextWindow <= 0) {\n\t\t\treturn snapshot;\n\t\t}\n\t\t// Add only the output produced since the snapshot was last refreshed. The activity\n\t\t// tracker accumulates across auto-retries within a turn, so subtract the baseline\n\t\t// captured at the last refresh to avoid re-adding a failed attempt's tokens.\n\t\tconst inFlight = this.isAgentStreaming()\n\t\t\t? Math.max(0, this.activityTracker.getStatus().tokens - this.contextUsageTokenBaseline)\n\t\t\t: 0;\n\t\tif (inFlight <= 0) {\n\t\t\treturn snapshot;\n\t\t}\n\t\tconst tokens = snapshot.tokens + inFlight;\n\t\treturn {\n\t\t\ttokens,\n\t\t\tcontextWindow: snapshot.contextWindow,\n\t\t\tpercent: (tokens / snapshot.contextWindow) * 100,\n\t\t} satisfies ContextUsage;\n\t}\n\n\tprivate getScopedModelState(): AgentConnectionState[\"scopedModels\"] {\n\t\treturn this.connectionState?.scopedModels ?? [];\n\t}\n\n\tprivate async rebindCurrentSession(): Promise<void> {\n\t\tthis.unsubscribe?.();\n\t\tthis.unsubscribe = undefined;\n\t\tif (this.localSessionHost) {\n\t\t\tthis.uiServices = this.localSessionHost.createUiServices();\n\t\t}\n\t\tthis.toolDefinitionCache.clear();\n\t\tthis.applyRuntimeSettings();\n\t\tif (this.bindLocalSessionExtensions) {\n\t\t\tawait this.bindCurrentSessionExtensions();\n\t\t} else {\n\t\t\tsetRegisteredThemes(this.uiServices.getThemes());\n\t\t\tawait this.refreshConnectionCatalog();\n\t\t\tthis.setupAutocompleteProvider();\n\t\t\tthis.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });\n\t\t}\n\t\tthis.subscribeToAgent();\n\t\tawait Promise.all([this.refreshConnectionQueue(), this.refreshHeartbeatCatalog().catch(() => undefined)]);\n\t\tawait this.updateAvailableProviderCount();\n\t\tthis.updateEditorBorderColor();\n\t\tthis.updateTerminalTitle();\n\t\tthis.setGoalAnnouncementBaseline(this.getGoalState());\n\t\tthis.syncGoalTray(this.getGoalState());\n\t\tthis.syncWorkingLoader();\n\t}\n\n\tprivate async handleFatalRuntimeError(prefix: string, error: unknown): Promise<never> {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tthis.showError(`${prefix}: ${message}`);\n\t\tstopThemeWatcher();\n\t\tthis.stop();\n\t\tprocess.exit(1);\n\t}\n\n\tprivate resetCurrentSessionRenderState(options?: { clearPromptStash?: boolean }): void {\n\t\tthis.endFeatureHintRun();\n\t\tthis.chatContainer.clear();\n\t\tthis.shortcutGuideContainer.clear();\n\t\tthis.pendingMessagesContainer.clear();\n\t\tthis.queuedMessagesContainer.clear();\n\t\tthis.connectionQueue = { steering: [], followUp: [] };\n\t\tthis.pendingQueueEdit = undefined;\n\t\t// The selection and its stashed draft belong to the previous session;\n\t\t// every editor draft is cleared below, so discard rather than restore.\n\t\tthis.queueSelection.reset();\n\t\tthis.featureHintSuppressedByQueue = false;\n\t\tif (options?.clearPromptStash) {\n\t\t\tthis.promptStash = undefined;\n\t\t\tif (this.promptStashState) this.promptStashState.queuedStashes = undefined;\n\t\t}\n\t\t// Clear every editor's prompt history, draft text, and queues, then prune\n\t\t// any pasted images no longer referenced by the remaining stashed draft.\n\t\tthis.defaultEditor.clearHistory?.();\n\t\tthis.defaultEditor.setText(\"\");\n\t\tif (this.editor !== this.defaultEditor) {\n\t\t\tthis.editor.clearHistory?.();\n\t\t\tthis.editor.setText(\"\");\n\t\t}\n\t\tconst keepImageIds = this.liveImageMarkerIds();\n\t\tfor (const markerId of this.pastedImages.keys()) {\n\t\t\tif (!keepImageIds.has(markerId)) {\n\t\t\t\tthis.pastedImages.delete(markerId);\n\t\t\t}\n\t\t}\n\t\tthis.streamingComponent = undefined;\n\t\tthis.streamingMessage = undefined;\n\t\t// The discarded component's loader interval keeps firing otherwise; no\n\t\t// bash_end will reach it once the reference is dropped.\n\t\tthis.activeBashComponent?.setComplete(undefined, true);\n\t\tthis.activeBashComponent = undefined;\n\t\t// Likewise: the next session's view may never see this refine settle.\n\t\tthis.discardRefineLoader();\n\t\tthis.pendingBashComponents = [];\n\t\tthis.activityTracker.reset();\n\t\tthis.contextUsageTokenBaseline = 0;\n\t\tthis.resetPendingToolState();\n\t\tthis.agentRunFileChanges.clear();\n\t\tthis.renderRecap();\n\t\tthis.ipythonToolComponents.clear();\n\t\tthis.lateIpythonSentAgentMessages.clear();\n\t\tthis.resetSubagentSummary();\n\t\tthis.setGoalAnnouncementBaseline(this.getGoalState());\n\t\tthis.syncGoalTray(this.getGoalState());\n\t}\n\n\tprivate resetPendingToolState(): void {\n\t\tthis.pendingToolGeneration++;\n\t\tthis.pendingTools.clear();\n\t\tthis.pendingToolCreations.clear();\n\t\tthis.startedToolCalls.clear();\n\t}\n\n\tprivate async renderCurrentSessionState(): Promise<void> {\n\t\t// Replacement events own the session-scoped command catalog. The daemon\n\t\t// sends that event before its command response, but its handler may still\n\t\t// be refreshing commands when the response resolves.\n\t\tawait this.sessionEventQueue;\n\t\tthis.resetCurrentSessionRenderState();\n\t\tawait this.renderInitialMessages();\n\t\t// The session transition and transcript are already authoritative here;\n\t\t// a transient queue read must not turn a successful switch into a fatal error.\n\t\tawait this.refreshConnectionQueue().catch(() => undefined);\n\t\tthis.syncWorkingLoader();\n\t}\n\n\tprivate async refreshCommandCatalogForCurrentSession(): Promise<void> {\n\t\ttry {\n\t\t\tthis.connectionCommands = await this.agentConnection.getCommands();\n\t\t} catch {\n\t\t\tthis.connectionCommands = [];\n\t\t}\n\t\tthis.setupAutocompleteProvider();\n\t}\n\n\tprivate async renderResyncedSession(snapshot: AgentConnectionSnapshot): Promise<void> {\n\t\tconst bashFinished = this.isBashRunning() && !snapshot.state.isBashRunning;\n\t\tthis.applyConnectionStateSnapshot(snapshot.state);\n\t\tthis.restoreTurnStartFromMessages(this.getSessionContextFromConnectionSnapshot(snapshot).messages);\n\t\tthis.streamingComponent = undefined;\n\t\tthis.streamingMessage = undefined;\n\t\tthis.rlmNodeId = snapshot.parent?.childId;\n\t\tthis.replaceSubagentSummary(snapshot.children);\n\t\tawait this.renderSessionContext(this.getSessionContextFromConnectionSnapshot(snapshot), {\n\t\t\tclearChat: true,\n\t\t\tupdateFooter: true,\n\t\t});\n\t\tawait this.restoreStreamingMessageFromSnapshot(snapshot.streamingMessage);\n\t\tawait this.refreshConnectionQueue();\n\t\tif (bashFinished) {\n\t\t\tif (this.activeBashComponent) {\n\t\t\t\tthis.activeBashComponent.setComplete(undefined, false);\n\t\t\t\tthis.activeBashComponent = undefined;\n\t\t\t\tif (!snapshot.state.isStreaming) {\n\t\t\t\t\tthis.flushPendingBashComponents();\n\t\t\t\t}\n\t\t\t}\n\t\t\t// A transient side bash is not persisted in the session snapshot, so a\n\t\t\t// reconnect cannot replay its missed bash_end event. Release the pane's\n\t\t\t// local running state when the authoritative snapshot says bash ended.\n\t\t\tif (this.sideQuestionBash) {\n\t\t\t\tthis.sideQuestionComponent?.finishBash();\n\t\t\t\tthis.sideQuestionBash = undefined;\n\t\t\t\tthis.sideQuestionBashComponent = undefined;\n\t\t\t}\n\t\t\tthis.sideQuestionBashDiscarded = undefined;\n\t\t}\n\t\tthis.updateTerminalTitle();\n\t\tthis.setGoalAnnouncementBaseline(this.getGoalState());\n\t\tthis.syncGoalTray(this.getGoalState());\n\t\tthis.syncWorkingLoader();\n\t}\n\n\tprivate getCachedToolDefinition(toolName: string): ToolExecutionDefinition | undefined {\n\t\treturn this.toolDefinitionCache.get(toolName);\n\t}\n\n\tprivate async loadToolDefinition(toolName: string): Promise<ToolExecutionDefinition | undefined> {\n\t\tif (this.toolDefinitionCache.has(toolName)) {\n\t\t\treturn this.toolDefinitionCache.get(toolName);\n\t\t}\n\t\tconst definition = this.createToolExecutionDefinition(\n\t\t\ttoolName,\n\t\t\tawait this.agentConnection.getToolDefinition(toolName),\n\t\t\tthis.localSessionHost?.getToolRendererDefinition(toolName),\n\t\t);\n\t\tthis.toolDefinitionCache.set(toolName, definition);\n\t\treturn definition;\n\t}\n\n\tprivate getLatestStreamingToolCall(toolCallId: string): ToolCall | undefined {\n\t\treturn this.streamingMessage?.content.find(\n\t\t\t(content): content is ToolCall => content.type === \"toolCall\" && content.id === toolCallId,\n\t\t);\n\t}\n\n\tprivate registerIpythonToolComponent(toolName: string, toolCallId: string, component: ToolExecutionComponent): void {\n\t\tif (toolName !== \"ipython\") {\n\t\t\treturn;\n\t\t}\n\t\tthis.ipythonToolComponents.set(toolCallId, component);\n\t\tfor (const lateMessage of this.lateIpythonSentAgentMessages.get(toolCallId) ?? []) {\n\t\t\tcomponent.appendSentAgentMessage(lateMessage);\n\t\t}\n\t}\n\n\tprivate async getOrCreatePendingToolComponent(\n\t\ttoolCall: PendingToolCallRenderInput,\n\t): Promise<ToolExecutionComponent | undefined> {\n\t\tconst existingComponent = this.pendingTools.get(toolCall.id);\n\t\tif (existingComponent) {\n\t\t\texistingComponent.updateArgs(toolCall.arguments);\n\t\t\treturn existingComponent;\n\t\t}\n\t\tif (this.pendingToolCreations.has(toolCall.id)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tthis.pendingToolCreations.add(toolCall.id);\n\t\tconst generation = this.pendingToolGeneration;\n\t\ttry {\n\t\t\tconst toolDefinition = await this.loadToolDefinition(toolCall.name);\n\t\t\tif (generation !== this.pendingToolGeneration) {\n\t\t\t\t// Pending tool state was reset (abort/error) while loading; drop the stale component.\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tconst latestToolCall = this.getLatestStreamingToolCall(toolCall.id) ?? toolCall;\n\t\t\tconst componentAfterLoad = this.pendingTools.get(latestToolCall.id);\n\t\t\tif (componentAfterLoad) {\n\t\t\t\tcomponentAfterLoad.updateArgs(latestToolCall.arguments);\n\t\t\t\treturn componentAfterLoad;\n\t\t\t}\n\n\t\t\tconst component = new ToolExecutionComponent(\n\t\t\t\tlatestToolCall.name,\n\t\t\t\tlatestToolCall.id,\n\t\t\t\tlatestToolCall.arguments,\n\t\t\t\t{\n\t\t\t\t\tshowImages: this.settingsManager.getShowImages(),\n\t\t\t\t},\n\t\t\t\ttoolDefinition,\n\t\t\t\tthis.ui,\n\t\t\t\tthis.getCurrentCwd(),\n\t\t\t);\n\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\tcomponent.setAgentMessagesExpanded(this.agentMessagesExpanded);\n\t\t\tcomponent.setEditDiffsExpanded(this.editDiffsExpanded);\n\t\t\tif (this.startedToolCalls.has(latestToolCall.id)) {\n\t\t\t\tcomponent.markExecutionStarted();\n\t\t\t}\n\t\t\tselectLatestToolExpandHint(this.chatContainer.children, component);\n\t\t\tthis.chatContainer.addChild(component);\n\t\t\tthis.pendingTools.set(latestToolCall.id, component);\n\t\t\tthis.registerIpythonToolComponent(latestToolCall.name, latestToolCall.id, component);\n\t\t\treturn component;\n\t\t} finally {\n\t\t\tthis.pendingToolCreations.delete(toolCall.id);\n\t\t}\n\t}\n\n\tprivate createToolExecutionDefinition(\n\t\ttoolName: string,\n\t\tconnectionDefinition: AgentConnectionToolDefinition | undefined,\n\t\tlocalRendererDefinition: InteractiveModeLocalToolRendererDefinition | undefined,\n\t): ToolExecutionDefinition | undefined {\n\t\tif (!connectionDefinition && !localRendererDefinition) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst definition: ToolExecutionDefinition = {\n\t\t\t...(connectionDefinition ?? {\n\t\t\t\tname: toolName,\n\t\t\t\tlabel: toolName,\n\t\t\t\tdescription: \"\",\n\t\t\t\tparameters: {},\n\t\t\t}),\n\t\t};\n\t\tif (localRendererDefinition?.renderShell !== undefined) {\n\t\t\tdefinition.renderShell = localRendererDefinition.renderShell;\n\t\t}\n\t\tif (localRendererDefinition?.renderCall !== undefined) {\n\t\t\tdefinition.renderCall = localRendererDefinition.renderCall;\n\t\t}\n\t\tif (localRendererDefinition?.renderResult !== undefined) {\n\t\t\tdefinition.renderResult = localRendererDefinition.renderResult;\n\t\t}\n\t\treturn definition;\n\t}\n\n\tprivate async preloadToolDefinitions(toolNames: Iterable<string>): Promise<void> {\n\t\tconst missingToolNames = Array.from(new Set(toolNames)).filter(\n\t\t\t(toolName) => !this.toolDefinitionCache.has(toolName),\n\t\t);\n\t\tif (missingToolNames.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tawait Promise.all(\n\t\t\tmissingToolNames.map(async (toolName) => {\n\t\t\t\tconst definition = this.createToolExecutionDefinition(\n\t\t\t\t\ttoolName,\n\t\t\t\t\tawait this.agentConnection.getToolDefinition(toolName),\n\t\t\t\t\tthis.localSessionHost?.getToolRendererDefinition(toolName),\n\t\t\t\t);\n\t\t\t\tthis.toolDefinitionCache.set(toolName, definition);\n\t\t\t}),\n\t\t);\n\t}\n\n\tprivate setupExtensionShortcuts(extensionRunner: ExtensionRunner): void {\n\t\tconst shortcuts = extensionRunner.getShortcuts(this.keybindings.getEffectiveConfig());\n\t\tif (shortcuts.size === 0) return;\n\n\t\tconst localSessionHost = this.getLocalSessionHost();\n\t\tconst createContext = (): ExtensionContext => ({\n\t\t\tui: this.createExtensionUIContext(),\n\t\t\thasUI: true,\n\t\t\tcwd: this.getCurrentCwd(),\n\t\t\tsessionManager: localSessionHost.getSessionManager(),\n\t\t\tmodelRegistry: this.modelRegistry,\n\t\t\tmodel: this.getCurrentModel(),\n\t\t\tisIdle: () => !this.isAgentStreaming(),\n\t\t\tsignal: localSessionHost.getAbortSignal(),\n\t\t\tabort: () => this.agentConnection.abort(),\n\t\t\thasPendingMessages: () => this.getQueuedActionCount() > 0,\n\t\t\tshutdown: () => {\n\t\t\t\tthis.shutdownRequested = true;\n\t\t\t},\n\t\t\tgetContextUsage: () => this.getConnectionContextUsage(),\n\t\t\tcompact: (options) => {\n\t\t\t\tvoid (async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = await this.agentConnection.compact(options?.customInstructions);\n\t\t\t\t\t\toptions?.onComplete?.(result);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconst err = error instanceof Error ? error : new Error(String(error));\n\t\t\t\t\t\toptions?.onError?.(err);\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t},\n\t\t\tgetSystemPrompt: () => localSessionHost.getSystemPrompt(),\n\t\t});\n\n\t\tthis.defaultEditor.onExtensionShortcut = (data: string) => {\n\t\t\tfor (const [shortcutStr, shortcut] of shortcuts) {\n\t\t\t\tif (matchesKey(data, shortcutStr as KeyId)) {\n\t\t\t\t\tPromise.resolve(shortcut.handler(createContext())).catch((err) => {\n\t\t\t\t\t\tthis.showError(`Shortcut handler error: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\t\t});\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\t}\n\n\tprivate setExtensionStatus(key: string, text: string | undefined): void {\n\t\tthis.footerDataProvider.setExtensionStatus(key, text);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate getWorkingLoaderMessage(): string {\n\t\tconst elapsed =\n\t\t\tthis.workingStartedAt === undefined\n\t\t\t\t? undefined\n\t\t\t\t: this.formatWorkingElapsed(Date.now() - this.workingStartedAt);\n\t\tconst status = this.activityTracker.getStatus();\n\t\t// The subagent count/recaps live in the tree above the loader, so the loader\n\t\t// message itself no longer repeats \"N subagents running\".\n\t\tif (!this.isAgentStreaming()) {\n\t\t\treturn \"\";\n\t\t}\n\t\tif (this.workingMessage !== undefined) {\n\t\t\t// Extensions and tool bootstrap own the message; keep the plain \"<message> <elapsed>\" form.\n\t\t\treturn elapsed === undefined ? this.workingMessage : `${this.workingMessage} ${elapsed}`;\n\t\t}\n\t\tconst parts: string[] = [AGENT_ACTIVITY_LABELS[status.activity]];\n\t\tif (elapsed !== undefined) {\n\t\t\tparts.push(elapsed);\n\t\t}\n\t\tif (status.tokens > 0) {\n\t\t\tparts.push(`${status.direction === \"down\" ? \"↓\" : \"↑\"} ${formatTokenCount(status.tokens)} tokens`);\n\t\t}\n\t\treturn parts.join(\" · \");\n\t}\n\n\tprivate createWorkingLoader(): Loader {\n\t\treturn new Loader(\n\t\t\tthis.ui,\n\t\t\t(spinner) => theme.fg(\"accent\", spinner),\n\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\tthis.getWorkingLoaderMessage(),\n\t\t\tthis.workingIndicatorOptions,\n\t\t);\n\t}\n\n\tprivate formatWorkingElapsed(elapsedMs: number): string {\n\t\tconst totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000));\n\t\tif (totalSeconds < 60) {\n\t\t\treturn `${totalSeconds}s`;\n\t\t}\n\t\tconst minutes = Math.floor(totalSeconds / 60);\n\t\tconst seconds = totalSeconds % 60;\n\t\tif (minutes < 60) {\n\t\t\treturn `${minutes}m ${seconds.toString().padStart(2, \"0\")}s`;\n\t\t}\n\t\tconst hours = Math.floor(minutes / 60);\n\t\tconst remainingMinutes = minutes % 60;\n\t\tif (hours < 24) {\n\t\t\treturn `${hours}h ${remainingMinutes.toString().padStart(2, \"0\")}m ${seconds.toString().padStart(2, \"0\")}s`;\n\t\t}\n\t\tconst days = Math.floor(hours / 24);\n\t\tconst remainingHours = hours % 24;\n\t\treturn `${days}d ${remainingHours.toString().padStart(2, \"0\")}h ${remainingMinutes.toString().padStart(2, \"0\")}m ${seconds.toString().padStart(2, \"0\")}s`;\n\t}\n\n\tprivate updateWorkingLoaderMessage(): void {\n\t\tthis.loadingAnimation?.setMessage(this.getWorkingLoaderMessage());\n\t}\n\n\tprivate startWorkingTimer(): void {\n\t\tif (this.workingTimer) {\n\t\t\tclearInterval(this.workingTimer);\n\t\t}\n\t\tthis.workingTimer = setInterval(() => this.updateWorkingLoaderMessage(), 1000);\n\t\tthis.workingTimer.unref?.();\n\t}\n\n\t// Recover the in-flight run's start from a restored transcript so the elapsed timer survives re-attach.\n\tprivate restoreTurnStartFromMessages(messages: readonly AgentMessage[]): void {\n\t\tthis.turnStartedAt = undefined;\n\t\tif (!this.isAgentStreaming()) return;\n\t\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\t\tconst message = messages[i]!;\n\t\t\tif (startsAgentRun(message)) {\n\t\t\t\tthis.turnStartedAt = message.timestamp;\n\t\t\t} else if (message.role === \"assistant\" && message.stopReason !== \"toolUse\") {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (this.turnStartedAt !== undefined && this.workingStartedAt !== undefined) {\n\t\t\tthis.workingStartedAt = this.turnStartedAt;\n\t\t\tthis.updateWorkingLoaderMessage();\n\t\t}\n\t}\n\n\tprivate startWorkingLoader(): void {\n\t\tthis.stopWorkingLoader();\n\t\tthis.workingStartedAt = this.turnStartedAt ?? Date.now();\n\t\tthis.loadingAnimation = this.createWorkingLoader();\n\t\tthis.statusContainer.addChild(this.loadingAnimation);\n\t\tthis.startWorkingTimer();\n\t\tthis.startFeatureHintPresentation();\n\t}\n\n\tprivate stopWorkingLoader(): void {\n\t\tthis.clearFeatureHintPresentation();\n\t\tif (this.workingTimer) {\n\t\t\tclearInterval(this.workingTimer);\n\t\t\tthis.workingTimer = undefined;\n\t\t}\n\t\tthis.workingStartedAt = undefined;\n\t\tif (this.loadingAnimation) {\n\t\t\tthis.loadingAnimation.stop();\n\t\t\tthis.loadingAnimation = undefined;\n\t\t}\n\t\tthis.statusContainer.clear();\n\t}\n\n\tprivate startFeatureHintPresentation(): void {\n\t\tthis.clearFeatureHintPresentation();\n\t\tif (this.shouldSuppressFeatureHint()) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.featureHintEligibleAt === 0) {\n\t\t\tthis.featureHintEligibleAt = Date.now() + FEATURE_HINT_DELAY_MS;\n\t\t}\n\t\tconst delay = Math.max(0, this.featureHintEligibleAt - Date.now());\n\t\tif (delay === 0) {\n\t\t\tthis.showFeatureHint();\n\t\t\treturn;\n\t\t}\n\t\tthis.featureHintTimer = setTimeout(() => {\n\t\t\tthis.featureHintTimer = undefined;\n\t\t\tthis.showFeatureHint();\n\t\t}, delay);\n\t\tthis.featureHintTimer.unref?.();\n\t}\n\n\tprivate showFeatureHint(): void {\n\t\tif (\n\t\t\tthis.shouldSuppressFeatureHint() ||\n\t\t\t!this.loadingAnimation ||\n\t\t\t!this.shouldShowWorkingLoader() ||\n\t\t\t!this.statusContainer.children.includes(this.loadingAnimation)\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tif (!this.currentFeatureHint) {\n\t\t\tconst hint = this.featureHintDeck.next({\n\t\t\t\tgetKeybinding: (action) => {\n\t\t\t\t\tconst key = keyText(action);\n\t\t\t\t\treturn key ? this.capitalizeKey(key) : undefined;\n\t\t\t\t},\n\t\t\t\tisResidentSession: this.options.returnToAgentsView === true,\n\t\t\t});\n\t\t\tthis.currentFeatureHint = hint?.text;\n\t\t}\n\t\tif (!this.currentFeatureHint) {\n\t\t\treturn;\n\t\t}\n\t\tthis.featureHintComponent = new FeatureHintComponent(this.currentFeatureHint);\n\t\tthis.featureHintContainer.addChild(this.featureHintComponent);\n\t\tthis.renderRecap();\n\t\tthis.featureHintAnimationTimer = setInterval(() => {\n\t\t\tthis.featureHintComponent?.advance();\n\t\t\tthis.ui.requestRender();\n\t\t}, FEATURE_HINT_ANIMATION_INTERVAL_MS);\n\t\tthis.featureHintAnimationTimer.unref?.();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate clearFeatureHintPresentation(): void {\n\t\tif (this.featureHintTimer) {\n\t\t\tclearTimeout(this.featureHintTimer);\n\t\t\tthis.featureHintTimer = undefined;\n\t\t}\n\t\tif (this.featureHintAnimationTimer) {\n\t\t\tclearInterval(this.featureHintAnimationTimer);\n\t\t\tthis.featureHintAnimationTimer = undefined;\n\t\t}\n\t\tif (this.featureHintComponent) {\n\t\t\tthis.featureHintContainer.removeChild(this.featureHintComponent);\n\t\t\tthis.featureHintComponent = undefined;\n\t\t\tthis.renderRecap();\n\t\t}\n\t}\n\n\tprivate resumeFeatureHintPresentation(): void {\n\t\tif (\n\t\t\t!this.shouldSuppressFeatureHint() &&\n\t\t\tthis.loadingAnimation &&\n\t\t\tthis.shouldShowWorkingLoader() &&\n\t\t\tthis.statusContainer.children.includes(this.loadingAnimation)\n\t\t) {\n\t\t\tthis.startFeatureHintPresentation();\n\t\t}\n\t}\n\n\tprivate shouldSuppressFeatureHint(): boolean {\n\t\tconst { steering, followUp } = this.getAllQueuedMessages();\n\t\treturn steering.length > 0 || followUp.length > 0;\n\t}\n\n\tprivate endFeatureHintRun(): void {\n\t\tthis.clearFeatureHintPresentation();\n\t\tthis.currentFeatureHint = undefined;\n\t\tthis.featureHintEligibleAt = 0;\n\t\tthis.featureHintRunPending = false;\n\t}\n\n\tprivate prepareFeatureHintRun(message: AgentMessage): void {\n\t\tif (!this.featureHintRunPending) return;\n\t\tif (message.role === \"assistant\") {\n\t\t\tthis.featureHintRunPending = false;\n\t\t\treturn;\n\t\t}\n\t\tif (!startsAgentRun(message)) return;\n\n\t\tthis.endFeatureHintRun();\n\t\tif (this.shouldShowWorkingLoader()) {\n\t\t\tthis.startFeatureHintPresentation();\n\t\t}\n\t}\n\n\tprivate updateWorkingPulse(): void {\n\t\tconst active = this.isAgentStreaming();\n\t\tif (!active) {\n\t\t\tthis.stopWorkingPulse();\n\t\t\treturn;\n\t\t}\n\t\tif (!this.pulseTimer) {\n\t\t\tthis.pulseTimer = setInterval(() => this.tickWorkingPulse(), WORKING_ICON_INTERVAL_MS);\n\t\t\tthis.pulseTimer.unref?.();\n\t\t}\n\t}\n\n\tprivate tickWorkingPulse(): void {\n\t\tthis.pulseFrame += 1;\n\t\tsetWorkingPulseFrame(this.pulseFrame);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate stopWorkingPulse(): void {\n\t\tif (this.pulseTimer) {\n\t\t\tclearInterval(this.pulseTimer);\n\t\t\tthis.pulseTimer = undefined;\n\t\t}\n\t}\n\n\tprivate shouldShowWorkingLoader(): boolean {\n\t\t// Background subagents (agent turn done, asyncio tasks still running) would\n\t\t// otherwise show a textless spinner; the subagent tree above the loader carries\n\t\t// that state, so the loader only shows while the main agent is itself streaming.\n\t\treturn this.workingVisible && this.isAgentStreaming();\n\t}\n\n\t// Reconcile the loader with current state for transitions that fire no live\n\t// agent_start edge (returning from agents view, resuming mid-stream).\n\tprivate startCompactionLoader(\n\t\treason: \"manual\" | \"requested\" | \"overflow\" | \"threshold\",\n\t\tcustomInstructions?: string,\n\t): void {\n\t\tif (this.settingsManager.getShowTerminalProgress()) {\n\t\t\tthis.ui.terminal.setProgress(true);\n\t\t}\n\t\t// Keep editor active; submissions are queued during compaction.\n\t\t// Fully stop the working loader (not just detach) so it isn't orphaned.\n\t\tthis.stopWorkingLoader();\n\t\tthis.statusContainer.clear();\n\t\tconst cancelHint = `(${keyText(\"app.clear\")} to cancel)`;\n\t\tconst focus = customInstructions ? ` (focus: ${truncateToWidth(customInstructions, 60, \"…\")})` : \"\";\n\t\tconst label =\n\t\t\treason === \"manual\"\n\t\t\t\t? `Compacting context${focus}... ${cancelHint}`\n\t\t\t\t: reason === \"requested\"\n\t\t\t\t\t? `Agent requested compaction, compacting context${focus}... ${cancelHint}`\n\t\t\t\t\t: `${reason === \"overflow\" ? \"Context overflow detected, \" : \"\"}Auto-compacting... ${cancelHint}`;\n\t\tthis.autoCompactionLoader = new Loader(\n\t\t\tthis.ui,\n\t\t\t(spinner) => theme.fg(\"muted\", spinner),\n\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\tlabel,\n\t\t);\n\t\tthis.statusContainer.addChild(this.autoCompactionLoader);\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Live status for a user-issued /refine, mirroring the compaction loader. */\n\tprivate startRefineLoader(): void {\n\t\tthis.stopWorkingLoader();\n\t\tthis.statusContainer.clear();\n\t\tthis.refineLoader = new Loader(\n\t\t\tthis.ui,\n\t\t\t(spinner) => theme.fg(\"muted\", spinner),\n\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\t\"Refining continual harness state...\",\n\t\t);\n\t\tthis.statusContainer.addChild(this.refineLoader);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate stopRefineLoader(): void {\n\t\tif (!this.refineLoader) return;\n\t\tthis.discardRefineLoader();\n\t\tthis.statusContainer.clear();\n\t\tthis.syncWorkingLoader();\n\t}\n\n\t/** Stops and removes the loader without remounting old-session state. */\n\tprivate discardRefineLoader(): void {\n\t\tif (!this.refineLoader) return;\n\t\tthis.refineLoader.stop();\n\t\tthis.statusContainer.removeChild(this.refineLoader);\n\t\tthis.refineLoader = undefined;\n\t}\n\n\tprivate syncWorkingLoader(): void {\n\t\t// A compaction that started before this client attached (or while another\n\t\t// view was open) has no start-event edge; restore its loader from state.\n\t\tif (!this.autoCompactionLoader && this.isAgentCompacting()) {\n\t\t\tthis.startCompactionLoader(\"manual\");\n\t\t\treturn;\n\t\t}\n\t\t// Compaction/retry own the status container while active; don't fight them.\n\t\tif (this.autoCompactionLoader || this.retryLoader) {\n\t\t\treturn;\n\t\t}\n\t\t// Remount the refine loader if another owner (e.g. a compaction) cleared it.\n\t\tif (this.refineLoader) {\n\t\t\tif (!this.statusContainer.children.includes(this.refineLoader)) {\n\t\t\t\tthis.statusContainer.clear();\n\t\t\t\tthis.statusContainer.addChild(this.refineLoader);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (this.shouldShowWorkingLoader()) {\n\t\t\t// A bare `loadingAnimation != null` check isn't proof it's on screen:\n\t\t\t// other paths clear statusContainer without nulling it, orphaning the\n\t\t\t// loader. Re-attach unless it is actually mounted.\n\t\t\tif (!this.loadingAnimation || !this.statusContainer.children.includes(this.loadingAnimation)) {\n\t\t\t\tthis.startWorkingLoader();\n\t\t\t}\n\t\t} else if (this.loadingAnimation) {\n\t\t\tthis.stopWorkingLoader();\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate setWorkingVisible(visible: boolean): void {\n\t\tthis.workingVisible = visible;\n\t\tif (!visible) {\n\t\t\tthis.stopWorkingLoader();\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (this.shouldShowWorkingLoader() && !this.loadingAnimation) {\n\t\t\tthis.statusContainer.clear();\n\t\t\tthis.startWorkingLoader();\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate setWorkingIndicator(options?: LoaderIndicatorOptions): void {\n\t\tthis.workingIndicatorOptions = options;\n\t\tthis.loadingAnimation?.setIndicator(options);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate setHiddenThinkingLabel(label?: string): void {\n\t\tthis.hiddenThinkingLabel = label ?? this.defaultHiddenThinkingLabel;\n\t\tfor (const child of this.chatContainer.children) {\n\t\t\tif (child instanceof AssistantMessageComponent) {\n\t\t\t\tchild.setHiddenThinkingLabel(this.hiddenThinkingLabel);\n\t\t\t}\n\t\t}\n\t\tif (this.streamingComponent) {\n\t\t\tthis.streamingComponent.setHiddenThinkingLabel(this.hiddenThinkingLabel);\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate setExtensionWidget(\n\t\tkey: string,\n\t\tcontent: string[] | ((tui: TUI, thm: Theme) => Component & { dispose?(): void }) | undefined,\n\t\toptions?: ExtensionWidgetOptions,\n\t): void {\n\t\tconst placement = options?.placement ?? \"aboveEditor\";\n\t\tconst removeExisting = (map: Map<string, Component & { dispose?(): void }>) => {\n\t\t\tconst existing = map.get(key);\n\t\t\tif (existing?.dispose) existing.dispose();\n\t\t\tmap.delete(key);\n\t\t};\n\n\t\tremoveExisting(this.extensionWidgetsAbove);\n\t\tremoveExisting(this.extensionWidgetsBelow);\n\n\t\tif (content === undefined) {\n\t\t\tthis.renderWidgets();\n\t\t\treturn;\n\t\t}\n\n\t\tlet component: Component & { dispose?(): void };\n\n\t\tif (Array.isArray(content)) {\n\t\t\tconst container = new Container();\n\t\t\tfor (const line of content.slice(0, InteractiveMode.MAX_WIDGET_LINES)) {\n\t\t\t\tcontainer.addChild(new Text(line, 1, 0));\n\t\t\t}\n\t\t\tif (content.length > InteractiveMode.MAX_WIDGET_LINES) {\n\t\t\t\tcontainer.addChild(new Text(theme.fg(\"muted\", \"... (widget truncated)\"), 1, 0));\n\t\t\t}\n\t\t\tcomponent = container;\n\t\t} else {\n\t\t\tcomponent = content(this.ui, theme);\n\t\t}\n\n\t\tconst targetMap = placement === \"belowEditor\" ? this.extensionWidgetsBelow : this.extensionWidgetsAbove;\n\t\ttargetMap.set(key, component);\n\t\tthis.renderWidgets();\n\t}\n\n\tprivate clearExtensionWidgets(): void {\n\t\tfor (const widget of this.extensionWidgetsAbove.values()) {\n\t\t\twidget.dispose?.();\n\t\t}\n\t\tfor (const widget of this.extensionWidgetsBelow.values()) {\n\t\t\twidget.dispose?.();\n\t\t}\n\t\tthis.extensionWidgetsAbove.clear();\n\t\tthis.extensionWidgetsBelow.clear();\n\t\tthis.renderWidgets();\n\t}\n\n\tprivate resetExtensionUI(): void {\n\t\tthis.cancelActiveConnectionExtensionUiRequests();\n\t\tthis.closeHeartbeatManager();\n\t\tif (this.extensionSelector) {\n\t\t\tthis.hideExtensionSelector();\n\t\t}\n\t\tif (this.extensionInput) {\n\t\t\tthis.hideExtensionInput();\n\t\t}\n\t\tif (this.extensionEditor) {\n\t\t\tthis.hideExtensionEditor();\n\t\t}\n\t\tthis.ui.hideOverlay();\n\t\tthis.clearExtensionTerminalInputListeners();\n\t\tthis.setExtensionFooter(undefined);\n\t\tthis.setExtensionHeader(undefined);\n\t\tthis.clearExtensionWidgets();\n\t\tthis.footerDataProvider.clearExtensionStatuses();\n\t\tthis.footer.invalidate();\n\t\tthis.autocompleteProviderWrappers = [];\n\t\tthis.setCustomEditorComponent(undefined);\n\t\tthis.setupAutocompleteProvider();\n\t\tthis.defaultEditor.onExtensionShortcut = undefined;\n\t\tthis.updateTerminalTitle();\n\t\tthis.workingMessage = undefined;\n\t\tthis.workingVisible = true;\n\t\tthis.setWorkingIndicator();\n\t\tif (this.loadingAnimation) {\n\t\t\tthis.updateWorkingLoaderMessage();\n\t\t}\n\t\tthis.setHiddenThinkingLabel();\n\t}\n\n\tprivate static readonly MAX_WIDGET_LINES = 10;\n\n\tprivate renderWidgets(): void {\n\t\tif (!this.widgetContainerAbove || !this.widgetContainerBelow) return;\n\t\tthis.renderWidgetContainer(this.widgetContainerAbove, this.extensionWidgetsAbove, true, true);\n\t\tthis.renderWidgetContainer(this.widgetContainerBelow, this.extensionWidgetsBelow, false, false);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate renderRecap(): void {\n\t\tif (!this.recapContainer) return;\n\t\tthis.recapContainer.clear();\n\t\tconst recap = this.sessionRecap?.trim();\n\t\tconst showChanges = !this.isAgentStreaming() && this.agentRunFileChanges.size > 0;\n\t\tif (showChanges) {\n\t\t\tthis.recapContainer.addChild(\n\t\t\t\tnew TruncatedText(formatTotalChangeSummary([...this.agentRunFileChanges.values()]), 1, 0),\n\t\t\t);\n\t\t}\n\t\tif (recap) {\n\t\t\tthis.recapContainer.addChild(new TruncatedText(theme.fg(\"dim\", `Recap: ${recap}`), 1, 0));\n\t\t}\n\t\tif ((recap || showChanges) && !this.featureHintComponent) {\n\t\t\tthis.recapContainer.addChild(new Spacer(1));\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate renderWidgetContainer(\n\t\tcontainer: Container,\n\t\twidgets: Map<string, Component & { dispose?(): void }>,\n\t\tspacerWhenEmpty: boolean,\n\t\tleadingSpacer: boolean,\n\t): void {\n\t\tcontainer.clear();\n\n\t\tif (widgets.size === 0) {\n\t\t\tif (spacerWhenEmpty) {\n\t\t\t\tcontainer.addChild(new Spacer(1));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (leadingSpacer) {\n\t\t\tcontainer.addChild(new Spacer(1));\n\t\t}\n\t\tfor (const component of widgets.values()) {\n\t\t\tcontainer.addChild(component);\n\t\t}\n\t}\n\n\tprivate setExtensionFooter(\n\t\tfactory:\n\t\t\t| ((tui: TUI, thm: Theme, footerData: ReadonlyFooterDataProvider) => Component & { dispose?(): void })\n\t\t\t| undefined,\n\t): void {\n\t\tif (this.customFooter?.dispose) {\n\t\t\tthis.customFooter.dispose();\n\t\t}\n\n\t\tif (this.customFooter) {\n\t\t\tthis.footerSlot.removeChild(this.customFooter);\n\t\t} else {\n\t\t\tthis.footerSlot.removeChild(this.footer);\n\t\t}\n\n\t\tif (factory) {\n\t\t\tthis.customFooter = factory(this.ui, theme, this.footerDataProvider);\n\t\t\tthis.footerSlot.addChild(this.customFooter);\n\t\t} else {\n\t\t\tthis.customFooter = undefined;\n\t\t\tthis.footerSlot.addChild(this.footer);\n\t\t}\n\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate setExtensionHeader(factory: ((tui: TUI, thm: Theme) => Component & { dispose?(): void }) | undefined): void {\n\t\t// Header may not be initialized yet if called during early initialization\n\t\tif (!this.builtInHeader) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.customHeader?.dispose) {\n\t\t\tthis.customHeader.dispose();\n\t\t}\n\n\t\t// Find the index of the current header in the header container\n\t\tconst currentHeader = this.customHeader || this.builtInHeader;\n\t\tconst index = this.headerContainer.children.indexOf(currentHeader);\n\n\t\tif (factory) {\n\t\t\tthis.customHeader = factory(this.ui, theme);\n\t\t\tif (isExpandable(this.customHeader)) {\n\t\t\t\tthis.customHeader.setExpanded(this.toolOutputExpanded);\n\t\t\t}\n\t\t\tif (index !== -1) {\n\t\t\t\tthis.headerContainer.children[index] = this.customHeader;\n\t\t\t} else {\n\t\t\t\t// If not found (e.g. builtInHeader was never added), add at the top\n\t\t\t\tthis.headerContainer.children.unshift(this.customHeader);\n\t\t\t}\n\t\t} else {\n\t\t\tthis.customHeader = undefined;\n\t\t\tif (isExpandable(this.builtInHeader)) {\n\t\t\t\tthis.builtInHeader.setExpanded(this.toolOutputExpanded);\n\t\t\t}\n\t\t\tif (index !== -1) {\n\t\t\t\tthis.headerContainer.children[index] = this.builtInHeader;\n\t\t\t}\n\t\t}\n\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate addExtensionTerminalInputListener(\n\t\thandler: (data: string) => { consume?: boolean; data?: string } | undefined,\n\t): () => void {\n\t\tconst unsubscribe = this.ui.addInputListener(handler);\n\t\tthis.extensionTerminalInputUnsubscribers.add(unsubscribe);\n\t\treturn () => {\n\t\t\tunsubscribe();\n\t\t\tthis.extensionTerminalInputUnsubscribers.delete(unsubscribe);\n\t\t};\n\t}\n\n\tprivate clearExtensionTerminalInputListeners(): void {\n\t\tfor (const unsubscribe of this.extensionTerminalInputUnsubscribers) {\n\t\t\tunsubscribe();\n\t\t}\n\t\tthis.extensionTerminalInputUnsubscribers.clear();\n\t}\n\n\tprivate createExtensionUIContext(): ExtensionUIContext {\n\t\treturn {\n\t\t\tselect: (title, options, opts) => this.showExtensionSelector(title, options, opts),\n\t\t\tconfirm: (title, message, opts) => this.showExtensionConfirm(title, message, opts),\n\t\t\tinput: (title, placeholder, opts) => this.showExtensionInput(title, placeholder, opts),\n\t\t\tnotify: (message, type) => this.showExtensionNotify(message, type),\n\t\t\tonTerminalInput: (handler) => this.addExtensionTerminalInputListener(handler),\n\t\t\tsetStatus: (key, text) => this.setExtensionStatus(key, text),\n\t\t\tsetWorkingMessage: (message) => {\n\t\t\t\tthis.workingMessage = message;\n\t\t\t\tif (this.loadingAnimation) {\n\t\t\t\t\tthis.updateWorkingLoaderMessage();\n\t\t\t\t}\n\t\t\t},\n\t\t\tsetWorkingVisible: (visible) => this.setWorkingVisible(visible),\n\t\t\tsetWorkingIndicator: (options) => this.setWorkingIndicator(options),\n\t\t\tsetHiddenThinkingLabel: (label) => this.setHiddenThinkingLabel(label),\n\t\t\tsetWidget: (key, content, options) => this.setExtensionWidget(key, content, options),\n\t\t\tsetFooter: (factory) => this.setExtensionFooter(factory),\n\t\t\tsetHeader: (factory) => this.setExtensionHeader(factory),\n\t\t\tsetTitle: (title) => this.ui.terminal.setTitle(title),\n\t\t\tcustom: (factory, options) => this.showExtensionCustom(factory, options),\n\t\t\tpasteToEditor: (text) => this.editor.handleInput(`\\x1b[200~${text}\\x1b[201~`),\n\t\t\tsetEditorText: (text) => this.editor.setText(text),\n\t\t\tgetEditorText: () => this.editor.getExpandedText?.() ?? this.editor.getText(),\n\t\t\teditor: (title, prefill) => this.showExtensionEditor(title, prefill),\n\t\t\taddAutocompleteProvider: (factory) => {\n\t\t\t\tthis.autocompleteProviderWrappers.push(factory);\n\t\t\t\tthis.setupAutocompleteProvider();\n\t\t\t},\n\t\t\tsetEditorComponent: (factory) => this.setCustomEditorComponent(factory),\n\t\t\tgetEditorComponent: () => this.editorComponentFactory,\n\t\t\tget theme() {\n\t\t\t\treturn theme;\n\t\t\t},\n\t\t\tgetAllThemes: () => getAvailableThemesWithPaths(),\n\t\t\tgetTheme: (name) => getThemeByName(name),\n\t\t\tsetTheme: (themeOrName) => {\n\t\t\t\tif (themeOrName instanceof Theme) {\n\t\t\t\t\tsetThemeInstance(themeOrName);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\treturn { success: true };\n\t\t\t\t}\n\t\t\t\tconst result = setTheme(themeOrName, true);\n\t\t\t\tif (result.success) {\n\t\t\t\t\tif (this.settingsManager.getTheme() !== themeOrName) {\n\t\t\t\t\t\tthis.settingsManager.setTheme(themeOrName);\n\t\t\t\t\t}\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t},\n\t\t\tgetToolsExpanded: () => this.toolOutputExpanded,\n\t\t\tsetToolsExpanded: (expanded) => this.setToolsExpanded(expanded),\n\t\t};\n\t}\n\n\tprivate showExtensionSelector(\n\t\ttitle: string,\n\t\toptions: string[],\n\t\topts?: ExtensionUIDialogOptions,\n\t): Promise<string | undefined> {\n\t\treturn new Promise((resolve) => {\n\t\t\tif (opts?.signal?.aborted) {\n\t\t\t\tresolve(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst onAbort = () => {\n\t\t\t\tthis.hideExtensionSelector();\n\t\t\t\tresolve(undefined);\n\t\t\t};\n\t\t\topts?.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\tthis.extensionSelector = new ExtensionSelectorComponent(\n\t\t\t\ttitle,\n\t\t\t\toptions,\n\t\t\t\t(option) => {\n\t\t\t\t\topts?.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tthis.hideExtensionSelector();\n\t\t\t\t\tresolve(option);\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\topts?.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tthis.hideExtensionSelector();\n\t\t\t\t\tresolve(undefined);\n\t\t\t\t},\n\t\t\t\t{ tui: this.ui, timeout: opts?.timeout },\n\t\t\t);\n\n\t\t\tthis.editorContainer.clear();\n\t\t\tthis.editorContainer.addChild(this.extensionSelector);\n\t\t\tthis.ui.setFocus(this.extensionSelector);\n\t\t\tthis.ui.requestRender();\n\t\t});\n\t}\n\n\tprivate hideExtensionSelector(): void {\n\t\tthis.extensionSelector?.dispose();\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.extensionSelector = undefined;\n\t\tthis.ui.setFocus(this.editor);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async showExtensionConfirm(\n\t\ttitle: string,\n\t\tmessage: string,\n\t\topts?: ExtensionUIDialogOptions,\n\t): Promise<boolean> {\n\t\tconst result = await this.showExtensionSelector(`${title}\\n${message}`, [\"Yes\", \"No\"], opts);\n\t\treturn result === \"Yes\";\n\t}\n\n\tprivate async promptForMissingSessionCwd(error: MissingSessionCwdError): Promise<string | undefined> {\n\t\tconst confirmed = await this.showExtensionConfirm(\n\t\t\t\"Session cwd not found\",\n\t\t\tformatMissingSessionCwdPrompt(error.issue),\n\t\t);\n\t\treturn confirmed ? error.issue.fallbackCwd : undefined;\n\t}\n\n\tprivate showExtensionInput(\n\t\ttitle: string,\n\t\tplaceholder?: string,\n\t\topts?: ExtensionUIDialogOptions,\n\t): Promise<string | undefined> {\n\t\treturn new Promise((resolve) => {\n\t\t\tif (opts?.signal?.aborted) {\n\t\t\t\tresolve(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst onAbort = () => {\n\t\t\t\tthis.hideExtensionInput();\n\t\t\t\tresolve(undefined);\n\t\t\t};\n\t\t\topts?.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\tthis.extensionInput = new ExtensionInputComponent(\n\t\t\t\ttitle,\n\t\t\t\tplaceholder,\n\t\t\t\t(value) => {\n\t\t\t\t\topts?.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tthis.hideExtensionInput();\n\t\t\t\t\tresolve(value);\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\topts?.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tthis.hideExtensionInput();\n\t\t\t\t\tresolve(undefined);\n\t\t\t\t},\n\t\t\t\t{ tui: this.ui, timeout: opts?.timeout },\n\t\t\t);\n\n\t\t\tthis.editorContainer.clear();\n\t\t\tthis.editorContainer.addChild(this.extensionInput);\n\t\t\tthis.ui.setFocus(this.extensionInput);\n\t\t\tthis.ui.requestRender();\n\t\t});\n\t}\n\n\tprivate hideExtensionInput(): void {\n\t\tthis.extensionInput?.dispose();\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.extensionInput = undefined;\n\t\tthis.ui.setFocus(this.editor);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate showExtensionEditor(title: string, prefill?: string): Promise<string | undefined> {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.extensionEditor = new ExtensionEditorComponent(\n\t\t\t\tthis.ui,\n\t\t\t\tthis.keybindings,\n\t\t\t\ttitle,\n\t\t\t\tprefill,\n\t\t\t\t(value) => {\n\t\t\t\t\tthis.hideExtensionEditor();\n\t\t\t\t\tresolve(value);\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tthis.hideExtensionEditor();\n\t\t\t\t\tresolve(undefined);\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tthis.editorContainer.clear();\n\t\t\tthis.editorContainer.addChild(this.extensionEditor);\n\t\t\tthis.ui.setFocus(this.extensionEditor);\n\t\t\tthis.ui.requestRender();\n\t\t});\n\t}\n\n\tprivate hideExtensionEditor(): void {\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.extensionEditor = undefined;\n\t\tthis.ui.setFocus(this.editor);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate setCustomEditorComponent(factory: EditorFactory | undefined): void {\n\t\tthis.editorComponentFactory = factory;\n\t\t// Snapshot the current editor before replacing it. Paste markers are only\n\t\t// meaningful while their originating editor still owns the paste snapshot.\n\t\tconst currentEditor = this.editor;\n\t\tconst currentPromptStash = this.snapshotPromptStashFrom(currentEditor, currentEditor.getText());\n\n\t\tthis.editorContainer.clear();\n\n\t\tif (factory) {\n\t\t\t// Create the custom editor with tui, theme, and keybindings\n\t\t\tconst newEditor = factory(this.ui, getEditorTheme(), this.keybindings);\n\n\t\t\t// Restore before wiring the shared onChange callback: setText may emit a\n\t\t\t// change, and an empty custom editor cannot reconstruct the old snapshot.\n\t\t\tconst canRestorePasteSnapshot =\n\t\t\t\tcurrentPromptStash.pasteSnapshot === undefined || newEditor.restorePasteSnapshot !== undefined;\n\t\t\tnewEditor.setText(\n\t\t\t\tcanRestorePasteSnapshot\n\t\t\t\t\t? currentPromptStash.text\n\t\t\t\t\t: (currentPromptStash.expandedText ?? currentPromptStash.text),\n\t\t\t);\n\t\t\tif (currentPromptStash.pasteSnapshot && newEditor.restorePasteSnapshot) {\n\t\t\t\tnewEditor.restorePasteSnapshot(currentPromptStash.pasteSnapshot);\n\t\t\t}\n\n\t\t\t// Wire up callbacks from the default editor. onChange snapshots the\n\t\t\t// active editor while it still owns paste markers and attachments, so\n\t\t\t// submit remains exact even when an editor clears before calling onSubmit.\n\t\t\tnewEditor.onSubmit = this.defaultEditor.onSubmit;\n\t\t\tnewEditor.onChange = this.defaultEditor.onChange;\n\n\t\t\t// Copy appearance settings if supported\n\t\t\tif (newEditor.borderColor !== undefined) {\n\t\t\t\tnewEditor.borderColor = this.defaultEditor.borderColor;\n\t\t\t}\n\t\t\tif (newEditor.setPaddingX !== undefined) {\n\t\t\t\tnewEditor.setPaddingX(this.defaultEditor.getPaddingX());\n\t\t\t}\n\n\t\t\t// Set autocomplete if supported\n\t\t\tif (newEditor.setAutocompleteProvider && this.autocompleteProvider) {\n\t\t\t\tnewEditor.setAutocompleteProvider(this.autocompleteProvider);\n\t\t\t}\n\n\t\t\t// If extending CustomEditor, copy app-level handlers\n\t\t\t// Use duck typing since instanceof fails across jiti module boundaries\n\t\t\tconst customEditor = newEditor as unknown as Record<string, unknown>;\n\t\t\tif (\"actionHandlers\" in customEditor && customEditor.actionHandlers instanceof Map) {\n\t\t\t\tif (!customEditor.onEscape) {\n\t\t\t\t\tcustomEditor.onEscape = () => this.defaultEditor.onEscape?.();\n\t\t\t\t}\n\t\t\t\tif (!customEditor.onCtrlD) {\n\t\t\t\t\tcustomEditor.onCtrlD = () => this.defaultEditor.onCtrlD?.();\n\t\t\t\t}\n\t\t\t\tif (!customEditor.onPasteImage) {\n\t\t\t\t\tcustomEditor.onPasteImage = () => this.defaultEditor.onPasteImage?.();\n\t\t\t\t}\n\t\t\t\tif (!customEditor.onMoveBelowPrompt) {\n\t\t\t\t\tcustomEditor.onMoveBelowPrompt = () => this.defaultEditor.onMoveBelowPrompt?.();\n\t\t\t\t}\n\t\t\t\tif (!customEditor.onAgentsBack) {\n\t\t\t\t\tcustomEditor.onAgentsBack = () => this.defaultEditor.onAgentsBack?.();\n\t\t\t\t}\n\t\t\t\tif (!customEditor.onExtensionShortcut) {\n\t\t\t\t\tcustomEditor.onExtensionShortcut = (data: string) => this.defaultEditor.onExtensionShortcut?.(data);\n\t\t\t\t}\n\t\t\t\t// Copy action handlers (clear, suspend, model switching, etc.)\n\t\t\t\tfor (const [action, handler] of this.defaultEditor.actionHandlers) {\n\t\t\t\t\t(customEditor.actionHandlers as Map<string, () => void>).set(action, handler);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.editor = newEditor;\n\t\t} else {\n\t\t\t// Restore the default editor with the same rich snapshot (or expanded\n\t\t\t// fallback text if this editor implementation cannot restore it).\n\t\t\tconst canRestorePasteSnapshot =\n\t\t\t\tcurrentPromptStash.pasteSnapshot === undefined || this.defaultEditor.restorePasteSnapshot !== undefined;\n\t\t\tthis.defaultEditor.setText(\n\t\t\t\tcanRestorePasteSnapshot\n\t\t\t\t\t? currentPromptStash.text\n\t\t\t\t\t: (currentPromptStash.expandedText ?? currentPromptStash.text),\n\t\t\t);\n\t\t\tif (currentPromptStash.pasteSnapshot && this.defaultEditor.restorePasteSnapshot) {\n\t\t\t\tthis.defaultEditor.restorePasteSnapshot(currentPromptStash.pasteSnapshot);\n\t\t\t}\n\t\t\tthis.editor = this.defaultEditor;\n\t\t}\n\t\tthis.latestEditorPromptStash = currentPromptStash;\n\n\t\tthis.editorContainer.addChild(this.editor as Component);\n\t\tthis.ui.setFocus(this.editor as Component);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate showExtensionNotify(message: string, type?: \"info\" | \"warning\" | \"error\"): void {\n\t\tif (type === \"error\") {\n\t\t\tthis.showError(message);\n\t\t} else if (type === \"warning\") {\n\t\t\tthis.showWarning(message);\n\t\t} else {\n\t\t\tthis.showStatus(message);\n\t\t}\n\t}\n\n\t/** Show a custom component with keyboard focus. Overlay mode renders on top of existing content. */\n\tprivate async showExtensionCustom<T>(\n\t\tfactory: (\n\t\t\ttui: TUI,\n\t\t\ttheme: Theme,\n\t\t\tkeybindings: KeybindingsManager,\n\t\t\tdone: (result: T) => void,\n\t\t) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,\n\t\toptions?: {\n\t\t\toverlay?: boolean;\n\t\t\toverlayOptions?: OverlayOptions | (() => OverlayOptions);\n\t\t\tonHandle?: (handle: OverlayHandle) => void;\n\t\t},\n\t): Promise<T> {\n\t\tconst savedText = this.editor.getText();\n\t\tconst isOverlay = options?.overlay ?? false;\n\n\t\tconst restoreEditor = () => {\n\t\t\tthis.editorContainer.clear();\n\t\t\tthis.editorContainer.addChild(this.editor);\n\t\t\tthis.editor.setText(savedText);\n\t\t\tthis.ui.setFocus(this.editor);\n\t\t\tthis.ui.requestRender();\n\t\t};\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tlet component: Component & { dispose?(): void };\n\t\t\tlet closed = false;\n\n\t\t\tconst close = (result: T) => {\n\t\t\t\tif (closed) return;\n\t\t\t\tclosed = true;\n\t\t\t\tif (isOverlay) this.ui.hideOverlay();\n\t\t\t\telse restoreEditor();\n\t\t\t\t// Note: both branches above already call requestRender\n\t\t\t\tresolve(result);\n\t\t\t\ttry {\n\t\t\t\t\tcomponent?.dispose?.();\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore dispose errors */\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tPromise.resolve(factory(this.ui, theme, this.keybindings, close))\n\t\t\t\t.then((c) => {\n\t\t\t\t\tif (closed) return;\n\t\t\t\t\tcomponent = c;\n\t\t\t\t\tif (isOverlay) {\n\t\t\t\t\t\t// Resolve overlay options - can be static or dynamic function\n\t\t\t\t\t\tconst resolveOptions = (): OverlayOptions | undefined => {\n\t\t\t\t\t\t\tif (options?.overlayOptions) {\n\t\t\t\t\t\t\t\tconst opts =\n\t\t\t\t\t\t\t\t\ttypeof options.overlayOptions === \"function\"\n\t\t\t\t\t\t\t\t\t\t? options.overlayOptions()\n\t\t\t\t\t\t\t\t\t\t: options.overlayOptions;\n\t\t\t\t\t\t\t\treturn opts;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Fallback: use component's width property if available\n\t\t\t\t\t\t\tconst w = (component as { width?: number }).width;\n\t\t\t\t\t\t\treturn w ? { width: w } : undefined;\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst handle = this.ui.showOverlay(component, resolveOptions());\n\t\t\t\t\t\t// Expose handle to caller for visibility control\n\t\t\t\t\t\toptions?.onHandle?.(handle);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.editorContainer.clear();\n\t\t\t\t\t\tthis.editorContainer.addChild(component);\n\t\t\t\t\t\tthis.ui.setFocus(component);\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tif (closed) return;\n\t\t\t\t\tif (!isOverlay) restoreEditor();\n\t\t\t\t\treject(err);\n\t\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Show an extension error in the UI.\n\t */\n\tprivate showExtensionError(extensionPath: string, error: string, stack?: string): void {\n\t\tconst errorMsg = `Extension \"${extensionPath}\" error: ${error}`;\n\t\tconst errorText = new Text(theme.fg(\"error\", errorMsg), 1, 0);\n\t\tthis.chatContainer.addChild(errorText);\n\t\tif (stack) {\n\t\t\t// Show stack trace in dim color, indented\n\t\t\tconst stackLines = stack\n\t\t\t\t.split(\"\\n\")\n\t\t\t\t.slice(1) // Skip first line (duplicates error message)\n\t\t\t\t.map((line) => theme.fg(\"dim\", ` ${line.trim()}`))\n\t\t\t\t.join(\"\\n\");\n\t\t\tif (stackLines) {\n\t\t\t\tthis.chatContainer.addChild(new Text(stackLines, 1, 0));\n\t\t\t}\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate setupKeyHandlers(): void {\n\t\tthis.defaultEditor.getHeaderLine = () => this.getQueueSelectionHeader();\n\t\t// Set up handlers on defaultEditor - they use this.editor for text access\n\t\t// so they work correctly regardless of which editor is active\n\t\tthis.defaultEditor.onEscape = () => {\n\t\t\tthis.handleEscape();\n\t\t};\n\n\t\tthis.defaultEditor.onAction(\"app.clear\", () => this.handleCtrlC());\n\t\tthis.defaultEditor.onAction(\"app.interrupt\", () => this.handleInterruptKey());\n\t\tthis.defaultEditor.onAction(\"app.shortcuts\", () => this.showShortcutGuide());\n\t\tthis.defaultEditor.onCtrlD = () => this.handleCtrlD();\n\t\tthis.defaultEditor.onAction(\"app.suspend\", () => this.handleCtrlZ());\n\n\t\t// Global debug handler on TUI (works regardless of focus)\n\t\tthis.ui.onDebug = () => {\n\t\t\tvoid this.handleDebugCommand();\n\t\t};\n\t\tthis.defaultEditor.onAction(\"app.model.select\", () => this.showModelSelector());\n\t\tthis.defaultEditor.onAction(\"app.tools.expand\", () => this.toggleToolOutputExpansion());\n\t\tthis.defaultEditor.onAction(\"app.messages.expand\", () => this.toggleAgentMessageExpansion());\n\t\tthis.defaultEditor.onAction(\"app.edits.expand\", () => this.toggleEditDiffExpansion());\n\t\tthis.defaultEditor.onAction(\"app.thinking.toggle\", () => this.toggleThinkingBlockVisibility());\n\t\tthis.defaultEditor.onAction(\"app.subagents.focus\", () => this.focusSubagentSummary());\n\t\tthis.defaultEditor.onAction(\"app.heartbeats.open\", () => {\n\t\t\tvoid this.showHeartbeatManager();\n\t\t});\n\t\tthis.defaultEditor.onAction(\"app.editor.external\", () => this.openExternalEditor());\n\t\tthis.defaultEditor.onAction(\"app.prompt.stash\", () => this.handlePromptStash());\n\t\tthis.defaultEditor.onAction(\"app.message.followUp\", () => this.handleFollowUp());\n\t\tthis.defaultEditor.onAction(\"app.message.navigateOlder\", () => this.browseQueueSelection(-1));\n\t\tthis.defaultEditor.onAction(\"app.message.navigateNewer\", () => this.browseQueueSelection(1));\n\t\tthis.defaultEditor.onAction(\"app.message.moveEarlier\", () => this.moveQueueSelection(-1));\n\t\tthis.defaultEditor.onAction(\"app.message.moveLater\", () => this.moveQueueSelection(1));\n\t\tthis.defaultEditor.onAction(\"app.session.new\", () => this.handleClearCommand());\n\t\tthis.defaultEditor.onAction(\"app.session.tree\", () => {\n\t\t\tvoid this.showTreeSelector();\n\t\t});\n\t\tthis.defaultEditor.onAction(\"app.session.fork\", () => {\n\t\t\tvoid this.showUserMessageSelector();\n\t\t});\n\t\tthis.defaultEditor.onAction(\"app.session.resume\", () => {\n\t\t\tvoid this.requestAgentsView();\n\t\t});\n\t\tthis.defaultEditor.onAgentsBack = () => this.handleAgentsBack();\n\t\tthis.defaultEditor.onMoveBelowPrompt = () => this.focusSubagentSummary();\n\n\t\tthis.defaultEditor.onChange = (text: string) => {\n\t\t\tif (text.length > 0 && !this.isApplyingQueueSelectionText) {\n\t\t\t\tthis.latestEditorPromptStash = this.snapshotPromptStashFrom(this.editor, text);\n\t\t\t}\n\t\t\tif (this.escapeRepeatAction && !this.isApplyingQueueSelectionText) {\n\t\t\t\tthis.clearEscapeRepeat();\n\t\t\t}\n\t\t\tif (text.length > 0) {\n\t\t\t\tthis.clearCtrlCExitHint();\n\t\t\t}\n\t\t};\n\n\t\tthis.defaultEditor.onPasteImage = () => {\n\t\t\tthis.handleClipboardImagePaste();\n\t\t};\n\t}\n\n\tprivate snapshotPromptStashFrom(editor: EditorComponent, text: string): PromptStash {\n\t\tconst pasteSnapshot = editor.getPasteSnapshot?.();\n\t\tconst images = this.getPromptStashImages(text);\n\t\treturn {\n\t\t\ttext,\n\t\t\texpandedText: pasteSnapshot ? (editor.getExpandedText?.() ?? text) : undefined,\n\t\t\tpasteSnapshot,\n\t\t\t...(images.length > 0 ? { images } : {}),\n\t\t};\n\t}\n\n\tprivate snapshotPromptStash(text: string): PromptStash {\n\t\treturn this.snapshotPromptStashFrom(this.editor, text);\n\t}\n\n\tprivate restorePromptStashOnOpen(): void {\n\t\tif (!this.promptStash?.restoreOnOpen) return;\n\t\t// Land the restore notice in its own status block: init may have just posted\n\t\t// a notice (e.g. compaction) that showStatus would otherwise replace.\n\t\tthis.lastStatusText = undefined;\n\t\tthis.lastStatusSpacer = undefined;\n\t\tthis.restorePromptStashIfEditorEmpty();\n\t}\n\n\tprivate stashDraftForAgentsView(): void {\n\t\tconst text = this.editor.getText();\n\t\tif (!text.trim()) return;\n\t\t// Head of the durable queue so it restores first on return; an existing\n\t\t// manual stash stays queued behind it and keeps its manual-stash semantics.\n\t\tconst existing = [this.promptStashState.stash, ...(this.promptStashState.queuedStashes ?? [])].filter(\n\t\t\t(stash): stash is PromptStash => stash !== undefined,\n\t\t);\n\t\tthis.promptStashState.stash = { ...this.snapshotPromptStash(text), restoreOnOpen: true };\n\t\tthis.promptStashState.queuedStashes = existing.length > 0 ? existing : undefined;\n\t}\n\n\tprivate handlePromptStash(): void {\n\t\tconst text = this.editor.getText();\n\t\tif (!text.trim()) {\n\t\t\tif (!this.restorePromptStashIfEditorEmpty()) {\n\t\t\t\tthis.showStatus(\"No prompt to stash\");\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (this.promptStash !== undefined) {\n\t\t\tthis.showStatus(\"Prompt stash already has a draft\");\n\t\t\treturn;\n\t\t}\n\t\tthis.promptStash = this.snapshotPromptStash(text);\n\t\tthis.editor.setText(\"\");\n\t\tthis.showStatus(\"Stashed prompt\");\n\t}\n\n\tprivate restorePromptStashIfEditorEmpty(stash = this.promptStash): boolean {\n\t\tif (stash === undefined || this.editor.getText().trim()) {\n\t\t\treturn false;\n\t\t}\n\t\tif (this.promptStash !== stash) {\n\t\t\treturn false;\n\t\t}\n\t\tthis.promptStash = this.promptStashState?.queuedStashes?.shift();\n\t\tif (this.promptStashState?.queuedStashes?.length === 0) this.promptStashState.queuedStashes = undefined;\n\t\tconst canRestorePasteSnapshot =\n\t\t\tstash.pasteSnapshot === undefined || this.editor.restorePasteSnapshot !== undefined;\n\t\tthis.editor.setText(canRestorePasteSnapshot ? stash.text : (stash.expandedText ?? stash.text));\n\t\tif (stash.pasteSnapshot && this.editor.restorePasteSnapshot) {\n\t\t\tthis.editor.restorePasteSnapshot(stash.pasteSnapshot);\n\t\t}\n\t\tthis.latestEditorPromptStash = this.snapshotPromptStash(this.editor.getText());\n\t\tthis.showStatus(\"Restored stashed prompt\");\n\t\treturn true;\n\t}\n\n\tprivate retainSubmittedDraft(\n\t\tstash: PromptStash,\n\t\tsubmissionGeneration: number,\n\t\tstate: PromptStashState = this.promptStashState,\n\t): void {\n\t\tthis.retainedSubmissionGenerations.set(stash, submissionGeneration);\n\t\tconst ordered = [state.stash, ...(state.queuedStashes ?? [])].filter(\n\t\t\t(candidate): candidate is PromptStash => candidate !== undefined,\n\t\t);\n\t\tconst insertAt = ordered.findIndex((candidate) => {\n\t\t\tconst generation = this.retainedSubmissionGenerations.get(candidate);\n\t\t\treturn generation !== undefined && generation > submissionGeneration;\n\t\t});\n\t\tordered.splice(insertAt === -1 ? ordered.length : insertAt, 0, stash);\n\t\tstate.stash = ordered.shift();\n\t\tstate.queuedStashes = ordered.length > 0 ? ordered : undefined;\n\t}\n\n\tprivate retainStartupPromptDrafts(prompts: readonly InteractiveInitialPrompt[]): void {\n\t\t// Reserve every marker visible anywhere in the retained batch before assigning\n\t\t// any image. This prevents an early prompt's attachment from making a literal\n\t\t// marker in a later prompt resolve to the wrong image.\n\t\tconst reserved = new Set(this.pastedImages.keys());\n\t\tfor (const stash of [this.promptStash, ...(this.promptStashState.queuedStashes ?? [])]) {\n\t\t\tif (stash) for (const markerId of imageMarkerIds(stash.text)) reserved.add(markerId);\n\t\t}\n\t\tfor (const prompt of prompts) {\n\t\t\tfor (const markerId of imageMarkerIds(prompt.text)) reserved.add(markerId);\n\t\t}\n\t\tfor (const markerId of reserved) {\n\t\t\tthis.nextImageMarkerId = Math.max(this.nextImageMarkerId, markerId + 1);\n\t\t}\n\t\tconst allocateMarker = () => {\n\t\t\twhile (reserved.has(this.nextImageMarkerId)) this.nextImageMarkerId++;\n\t\t\tconst markerId = this.nextImageMarkerId++;\n\t\t\treserved.add(markerId);\n\t\t\treturn markerId;\n\t\t};\n\n\t\tconst retained: PromptStash[] = [];\n\t\tfor (const prompt of prompts) {\n\t\t\tlet text = prompt.text;\n\t\t\t// A startup prompt owns only the images passed with it. Remap literal\n\t\t\t// markers that already name registry data so restoring this draft cannot\n\t\t\t// accidentally attach an old or another prompt's image.\n\t\t\tconst literalRemaps = new Map<number, number>();\n\t\t\tfor (const markerId of imageMarkerIds(text)) {\n\t\t\t\tif (this.pastedImages.has(markerId) && !literalRemaps.has(markerId)) {\n\t\t\t\t\tliteralRemaps.set(markerId, allocateMarker());\n\t\t\t\t}\n\t\t\t}\n\t\t\ttext = remapImageMarkers(text, literalRemaps);\n\n\t\t\tconst images: Array<readonly [number, ImageContent]> = [];\n\t\t\tfor (const image of prompt.images ?? []) {\n\t\t\t\tconst markerId = allocateMarker();\n\t\t\t\timages.push([markerId, image]);\n\t\t\t\ttext += `${text.length > 0 && !/\\s$/.test(text) ? \" \" : \"\"}${formatImageMarker(markerId)}`;\n\t\t\t}\n\t\t\tretained.push({\n\t\t\t\ttext,\n\t\t\t\t...(images.length > 0 ? { images } : {}),\n\t\t\t});\n\t\t\tfor (const [markerId, image] of images) this.pastedImages.set(markerId, image);\n\t\t}\n\n\t\t// Startup drafts form the head of the durable queue. Preserve any older\n\t\t// client draft after them, and let submissions released by the barrier append.\n\t\tconst existing = [this.promptStashState.stash, ...(this.promptStashState.queuedStashes ?? [])].filter(\n\t\t\t(stash): stash is PromptStash => stash !== undefined,\n\t\t);\n\t\tconst ordered = [...retained, ...existing];\n\t\tthis.promptStashState.stash = ordered.shift();\n\t\tthis.promptStashState.queuedStashes = ordered.length > 0 ? ordered : undefined;\n\t}\n\n\tprivate getPromptStashImages(text: string): readonly (readonly [number, ImageContent])[] {\n\t\tconst images: Array<readonly [number, ImageContent]> = [];\n\t\tfor (const markerId of imageMarkerIds(text)) {\n\t\t\tconst image = this.pastedImages.get(markerId);\n\t\t\tif (image) {\n\t\t\t\timages.push([markerId, image]);\n\t\t\t}\n\t\t}\n\t\treturn images;\n\t}\n\n\tprivate async handleClipboardImagePaste(): Promise<void> {\n\t\ttry {\n\t\t\tconst image = await readClipboardImage();\n\t\t\tif (!image) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Resize down to the inline image size limit, mirroring the CLI @file\n\t\t\t// path, so large screenshots don't exceed provider limits. Fall back to\n\t\t\t// the raw bytes if resizing is unavailable.\n\t\t\tconst raw: ImageContent = {\n\t\t\t\ttype: \"image\",\n\t\t\t\tdata: Buffer.from(image.bytes).toString(\"base64\"),\n\t\t\t\tmimeType: image.mimeType,\n\t\t\t};\n\t\t\tconst resized = await resizeImage(raw);\n\t\t\tconst attachment: ImageContent = resized\n\t\t\t\t? { type: \"image\", data: resized.data, mimeType: resized.mimeType }\n\t\t\t\t: raw;\n\n\t\t\t// Register the image and insert a visible marker. The image is attached to\n\t\t\t// the prompt as multimodal content rather than written to disk, so a vision\n\t\t\t// model receives it directly.\n\t\t\tconst markerId = this.nextImageMarkerId++;\n\t\t\tthis.rememberPastedImage(markerId, attachment);\n\t\t\tthis.editor.insertTextAtCursor?.(formatImageMarker(markerId));\n\t\t\tthis.ui.requestRender();\n\n\t\t\tconst model = this.getCurrentModel();\n\t\t\tif (model && !model.input.includes(\"image\")) {\n\t\t\t\tthis.showStatus(\"Current model does not support images; the attachment will be omitted.\");\n\t\t\t}\n\t\t} catch {\n\t\t\t// Silently ignore clipboard errors (may not have permission, etc.)\n\t\t}\n\t}\n\n\t/**\n\t * Record a pasted image, evicting the oldest entries once the retained bytes\n\t * exceed {@link MAX_PASTED_IMAGE_BYTES} so a long session stays bounded. The\n\t * just-added image and any whose marker is still referenced (editor or queues)\n\t * are never evicted, so a live marker never loses its image.\n\t */\n\tprivate rememberPastedImage(id: number, image: ImageContent): void {\n\t\tthis.pastedImages.set(id, image);\n\t\tconst keep = this.liveImageMarkerIds();\n\t\tkeep.add(id);\n\t\tevictImagesToBudget(this.pastedImages, (img) => img.data.length, MAX_PASTED_IMAGE_BYTES, keep);\n\t}\n\n\t/**\n\t * Marker ids still reachable — current editor text, prompt history (recallable\n\t * with the up arrow), the compaction queue, and the connection queue. These are\n\t * never evicted so a recall or resend never finds a marker with no image.\n\t */\n\tprivate liveImageMarkerIds(): Set<number> {\n\t\tconst ids = new Set<number>();\n\t\tconst add = (text: string) => {\n\t\t\tfor (const markerId of imageMarkerIds(text)) {\n\t\t\t\tids.add(markerId);\n\t\t\t}\n\t\t};\n\t\tadd(this.editor.getText());\n\t\tfor (const stash of [this.promptStash, ...(this.promptStashState?.queuedStashes ?? [])]) {\n\t\t\tif (stash) add(stash.text);\n\t\t}\n\t\tfor (const entry of this.editor.getHistory?.() ?? []) {\n\t\t\tadd(entry);\n\t\t}\n\t\tfor (const msg of [...this.connectionQueue.steering, ...this.connectionQueue.followUp]) {\n\t\t\tadd(msg);\n\t\t}\n\t\treturn ids;\n\t}\n\n\t/**\n\t * The images whose `[image #N]` markers are present in `text`, or undefined if\n\t * none. Read-only: the registry is never cleared here, so deleting a marker\n\t * simply drops its image while restoring the marker (undo, history, retry,\n\t * dequeue) brings it back. Marker presence in the sent text is the single\n\t * source of truth.\n\t *\n\t * Resolved against the current model: if it has no image input, attachments\n\t * are dropped here (matching the paste-time hint) rather than sent and\n\t * downgraded downstream.\n\t */\n\tprivate collectImagesFor(text: string): ImageContent[] | undefined {\n\t\tconst model = this.getCurrentModel();\n\t\tif (model && !model.input.includes(\"image\")) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst images = collectMarkedImages(this.pastedImages, text);\n\t\treturn images.length > 0 ? images : undefined;\n\t}\n\n\tprivate hasPastedImagesFor(text: string): boolean {\n\t\treturn imageMarkerIds(text).some((id) => this.pastedImages.has(id));\n\t}\n\n\tprivate async handleSideQuestion(question: string): Promise<void> {\n\t\tif (!question) {\n\t\t\tthis.showWarning(\"Usage: /btw <question>\");\n\t\t\treturn;\n\t\t}\n\t\tif (this.activeSideQuestionId) {\n\t\t\tthis.showWarning(\"Wait for the current side question to finish or cancel it first.\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Turns already answered in the open pane seed the follow-up's context.\n\t\tconst previousTurns = this.sideQuestionTurns\n\t\t\t.filter((turn) => turn.answer)\n\t\t\t.map((turn) => ({ question: turn.question, answer: turn.answer }));\n\t\tconst event: AgentConnectionSideQuestionEvent = {\n\t\t\tid: randomUUID(),\n\t\t\tquestion,\n\t\t\tanswer: \"\",\n\t\t\tstatus: \"running\",\n\t\t};\n\t\tthis.activeSideQuestionId = event.id;\n\t\tthis.sideQuestionEvent = event;\n\t\tthis.sideQuestionTurns.push(event);\n\t\tif (this.sideQuestionComponent) {\n\t\t\tthis.sideQuestionComponent.addTurn(event);\n\t\t} else {\n\t\t\tthis.sideQuestionComponent = new SideQuestionComponent(event, this.settingsManager.getEditorPaddingX());\n\t\t\tthis.sideQuestionContainer.addChild(new Spacer(1));\n\t\t\tthis.sideQuestionContainer.addChild(this.sideQuestionComponent);\n\t\t}\n\t\tthis.ui.requestRender();\n\n\t\ttry {\n\t\t\tawait this.agentConnection.startSideQuestion(\n\t\t\t\tevent.id,\n\t\t\t\tquestion,\n\t\t\t\tpreviousTurns.length > 0 ? previousTurns : undefined,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tthis.handleSideQuestionEvent({\n\t\t\t\t...event,\n\t\t\t\tstatus: \"error\",\n\t\t\t\terrorMessage: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate handleSideQuestionEvent(event: AgentConnectionSideQuestionEvent): void {\n\t\tif (event.id === this.activeSideQuestionId && event.status !== \"running\") {\n\t\t\tthis.activeSideQuestionId = undefined;\n\t\t}\n\t\tif (event.id !== this.sideQuestionEvent?.id || !this.sideQuestionComponent) {\n\t\t\treturn;\n\t\t}\n\t\tthis.sideQuestionEvent = event;\n\t\tconst turnIndex = this.sideQuestionTurns.findIndex((turn) => turn.id === event.id);\n\t\tif (turnIndex !== -1) {\n\t\t\tthis.sideQuestionTurns[turnIndex] = event;\n\t\t}\n\t\tthis.sideQuestionComponent.update(event);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate finishSideQuestionBash(\n\t\tevent: Extract<AgentConnectionSessionEvent, { type: \"bash_end\" }>,\n\t\trawOutput: string,\n\t): void {\n\t\t// Release the pane's running state before any early return so the cancel\n\t\t// hint cannot stay stuck if the pending state was already cleared.\n\t\tthis.sideQuestionComponent?.finishBash();\n\t\tconst bash = this.sideQuestionBash;\n\t\tif (!bash) {\n\t\t\treturn;\n\t\t}\n\t\tthis.sideQuestionBash = undefined;\n\t\t// The pane already rendered the run; this only seeds follow-up turns.\n\t\tif (!bash.seedTranscript || event.cancelled || event.errorMessage) {\n\t\t\treturn;\n\t\t}\n\t\tconst truncation = truncateTail(rawOutput);\n\t\tconst output = truncation.content.replace(/\\n+$/, \"\");\n\t\tthis.sideQuestionTurns.push({\n\t\t\tid: `side-bash-${randomUUID()}`,\n\t\t\tquestion: bash.input,\n\t\t\tanswer: bashOutputToText({\n\t\t\t\toutput,\n\t\t\t\texitCode: event.exitCode,\n\t\t\t\tcancelled: false,\n\t\t\t\ttruncated: event.truncated || truncation.truncated,\n\t\t\t\tfullOutputPath: event.fullOutputPath,\n\t\t\t}),\n\t\t\tstatus: \"complete\",\n\t\t});\n\t}\n\n\tprivate clearSideQuestion(options: { abort?: boolean } = {}): void {\n\t\tconst event = this.sideQuestionEvent;\n\t\tif (options.abort && event?.status === \"running\") {\n\t\t\tthis.abortSideQuestion(event.id);\n\t\t}\n\t\tif (this.sideQuestionBash) {\n\t\t\t// A side-conversation bash run dies with its pane. Its bash_* events may\n\t\t\t// still be in flight (even bash_start), so swallow them until bash_end.\n\t\t\tconst ownsRunningBash = this.sideQuestionBashComponent !== undefined;\n\t\t\tthis.sideQuestionBashDiscarded = this.sideQuestionBash.runId;\n\t\t\tthis.sideQuestionBash = undefined;\n\t\t\tthis.sideQuestionBashComponent = undefined;\n\t\t\t// abort_bash is session-scoped. Before our matching bash_start arrives,\n\t\t\t// another client may own the slot, so only abort a run we have observed.\n\t\t\tif (ownsRunningBash) {\n\t\t\t\tvoid this.agentConnection.abortBash().catch(() => undefined);\n\t\t\t}\n\t\t}\n\t\tthis.sideQuestionEvent = undefined;\n\t\tthis.sideQuestionTurns = [];\n\t\tthis.sideQuestionComponent = undefined;\n\t\tthis.sideQuestionContainer.clear();\n\t\tif (this.isInitialized) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate resetSideQuestion(): void {\n\t\tthis.clearSideQuestion({ abort: true });\n\t\tthis.activeSideQuestionId = undefined;\n\t}\n\n\tprivate abortSideQuestion(id: string, reportError = false): void {\n\t\tvoid this.agentConnection\n\t\t\t.abortSideQuestion(id)\n\t\t\t.then((aborted) => {\n\t\t\t\tif (!aborted && this.activeSideQuestionId === id) {\n\t\t\t\t\tthis.activeSideQuestionId = undefined;\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tif (reportError) {\n\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t}\n\t\t\t});\n\t}\n\n\tprivate async renderTreeNavigation(result: { editorText?: string }): Promise<void> {\n\t\tthis.clearSideQuestion({ abort: true });\n\t\tthis.chatContainer.clear();\n\t\tawait this.renderInitialMessages();\n\t\tif (result.editorText && !this.editor.getText().trim()) {\n\t\t\tthis.editor.setText(result.editorText);\n\t\t}\n\t\tthis.showStatus(\"Navigated to selected point\");\n\t}\n\n\tprivate setupEditorSubmitHandler(): void {\n\t\tthis.defaultEditor.onSubmit = async (text: string) => {\n\t\t\tconst streamingBehavior = this.submittedInputBehavior;\n\t\t\tthis.submittedInputBehavior = \"steer\";\n\t\t\tif (this.queueSelection?.isBrowsing && !this.pendingQueueEdit) {\n\t\t\t\tconst targetLane = streamingBehavior === \"followUp\" ? \"followUp\" : \"steering\";\n\t\t\t\ttry {\n\t\t\t\t\tif (await this.applyQueueSelection(text, targetLane)) return;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\ttext = text.trim();\n\t\t\tif (!text) return;\n\t\t\tconst submissionGeneration = ++this.inputSubmissionGeneration;\n\t\t\tthis.inputSubmissionsPending++;\n\t\t\tthis.clearShortcutGuide();\n\t\t\t// A barrier wait can resume after /new repointed the live session fields.\n\t\t\tconst submissionStashState = this.promptStashState;\n\t\t\tconst submissionSessionId = this.promptStashSessionId;\n\t\t\tconst promptStashToRestore = this.promptStash;\n\t\t\tconst liveEditorText = this.editor.getText();\n\t\t\tconst submittedDraft =\n\t\t\t\tthis.pendingSubmittedPromptStash ??\n\t\t\t\t(liveEditorText.trim() ? this.snapshotPromptStash(liveEditorText) : this.latestEditorPromptStash);\n\t\t\tthis.pendingSubmittedPromptStash = undefined;\n\t\t\tlet restorePromptStashAfterSubmit = true;\n\t\t\tlet submissionOutcome: StartupPromptBarrierOutcome = \"admitted\";\n\n\t\t\ttry {\n\t\t\t\tconst slashCommand = parseSlashCommand(text);\n\t\t\t\tconst commandName = slashCommand ? resolveBuiltinSlashCommandName(slashCommand.name) : undefined;\n\t\t\t\tconst commandArgs = slashCommand?.args ?? \"\";\n\t\t\t\tconst canonicalCommandText = commandName ? `/${commandName}${commandArgs ? ` ${commandArgs}` : \"\"}` : text;\n\n\t\t\t\t// Slash commands are disabled while a side conversation is open: they\n\t\t\t\t// act on the main session, which is confusing mid-side-chat. The notice\n\t\t\t\t// renders as a pane response and never reaches the model. A reply that\n\t\t\t\t// merely starts with \"/\" (e.g. an absolute path) is not a command and\n\t\t\t\t// falls through to the side-conversation capture below.\n\t\t\t\tif (\n\t\t\t\t\tthis.sideQuestionComponent &&\n\t\t\t\t\tslashCommand !== undefined &&\n\t\t\t\t\t(isBuiltinSlashCommandName(slashCommand.name) ||\n\t\t\t\t\t\tthis.connectionCommands.some((command) => command.name === slashCommand.name))\n\t\t\t\t) {\n\t\t\t\t\tthis.editor.addToHistory?.(text);\n\t\t\t\t\tthis.sideQuestionComponent.addTurn({\n\t\t\t\t\t\tid: `side-notice-${randomUUID()}`,\n\t\t\t\t\t\tquestion: text,\n\t\t\t\t\t\tanswer:\n\t\t\t\t\t\t\t\"Slash commands are not available in side conversations. Press esc to return to the main thread.\",\n\t\t\t\t\t\tstatus: \"complete\",\n\t\t\t\t\t});\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName) {\n\t\t\t\t\tvoid captureAgentCommandUsed({\n\t\t\t\t\t\tagentDir: getAgentDir(),\n\t\t\t\t\t\tsettingsManager: this.settingsManager,\n\t\t\t\t\t\tcommandName,\n\t\t\t\t\t}).catch(() => {});\n\t\t\t\t}\n\n\t\t\t\tif (commandName === \"btw\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.handleSideQuestion(commandArgs);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"settings\" && !commandArgs) {\n\t\t\t\t\tawait this.showSettingsSelector();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"scoped-models\" && !commandArgs) {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.showModelsSelector();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"model\") {\n\t\t\t\t\tconst searchTerm = commandArgs || undefined;\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.handleModelCommand(searchTerm);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"effort\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tthis.handleEffortCommand(commandArgs);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"fast\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tif (commandArgs) {\n\t\t\t\t\t\tthis.showError(\"Usage: /fast\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.handleFastCommand();\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"export\") {\n\t\t\t\t\tawait this.handleExportCommand(canonicalCommandText);\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"import\") {\n\t\t\t\t\tawait this.handleImportCommand(canonicalCommandText);\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"share\" && !commandArgs) {\n\t\t\t\t\tawait this.handleShareCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"copy\" && !commandArgs) {\n\t\t\t\t\tawait this.handleCopyCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"name\") {\n\t\t\t\t\tawait this.handleNameCommand(canonicalCommandText);\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"rlm-max-depth\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.handleRlmMaxDepthCommand(commandArgs);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"session\" && !commandArgs) {\n\t\t\t\t\tthis.echoLocalCommand(text);\n\t\t\t\t\tawait this.handleSessionCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"system-prompt\" && !commandArgs) {\n\t\t\t\t\tthis.echoLocalCommand(text);\n\t\t\t\t\tawait this.handleSystemPromptCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"traces\") {\n\t\t\t\t\tawait this.handleTracesCommand(canonicalCommandText);\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"context\" && !commandArgs) {\n\t\t\t\t\tthis.echoLocalCommand(text);\n\t\t\t\t\tawait this.handleContextCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"logs\" && !commandArgs) {\n\t\t\t\t\tthis.echoLocalCommand(text);\n\t\t\t\t\tthis.handleLogsCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"heartbeat\") {\n\t\t\t\t\tawait this.handleHeartbeatCommand(canonicalCommandText);\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"heartbeats\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.showHeartbeatManager();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"changelog\" && !commandArgs) {\n\t\t\t\t\tthis.echoLocalCommand(text);\n\t\t\t\t\tthis.handleChangelogCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"hotkeys\" && !commandArgs) {\n\t\t\t\t\tthis.echoLocalCommand(text);\n\t\t\t\t\tthis.handleHotkeysCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"fork\" && !commandArgs) {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.showUserMessageSelector();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"clone\" && !commandArgs) {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.handleCloneCommand();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"tree\" && !commandArgs) {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\trestorePromptStashAfterSubmit = false;\n\t\t\t\t\tawait this.showTreeSelector();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"login\" && !commandArgs) {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.showConfigurationMenu(\"providers\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"logout\" && !commandArgs) {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.showLogoutSelector();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"mcp\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.handleMcpCommand(commandArgs);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (slashCommand?.name === \"clear\") {\n\t\t\t\t\tif (commandArgs) {\n\t\t\t\t\t\tthis.editor.setText(text);\n\t\t\t\t\t\tthis.showError(\"Usage: /clear\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\t\tawait this.handleClearCommand();\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (slashCommand?.name === \"new\") {\n\t\t\t\t\tlet options: ReturnType<typeof parseNewSessionCommand>;\n\t\t\t\t\ttry {\n\t\t\t\t\t\toptions = parseNewSessionCommand(text.slice(4));\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tthis.editor.setText(text);\n\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.handleClearCommand(options);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"resume\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.handleResumeCommand(commandArgs);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"reload\" && !commandArgs) {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.handleReloadCommand();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"update\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tconst updateArgs = parseCommandArgs(commandArgs);\n\t\t\t\t\tif (\n\t\t\t\t\t\t!updateArgsIncludeSelf(updateArgs) &&\n\t\t\t\t\t\t(this.isAgentCompacting() || this.isAgentStreaming() || this.isBashRunning())\n\t\t\t\t\t) {\n\t\t\t\t\t\tthis.showWarning(\"Wait for the current work to finish before updating.\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tawait this.handleUpdateCommand(commandArgs);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"fullscreen\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tconst arg = commandArgs?.trim().toLowerCase();\n\t\t\t\t\tif (arg && arg !== \"on\" && arg !== \"off\") {\n\t\t\t\t\t\tthis.showError(\"Usage: /fullscreen [on|off]\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst enable = arg === \"on\" ? true : arg === \"off\" ? false : !this.fullscreenEnabled;\n\t\t\t\t\tthis.setFullscreenMode(enable);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"debug\" && !commandArgs) {\n\t\t\t\t\tawait this.handleDebugCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (text === \"/arminsayshi\") {\n\t\t\t\t\tthis.handleArminSaysHi();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (text === \"/dementedelves\") {\n\t\t\t\t\tthis.handleDementedDelves();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (text === \"/quit\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.shutdown();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Handle bash command (! for normal, !! for excluded from context)\n\t\t\t\tif (text.startsWith(\"!\")) {\n\t\t\t\t\tconst isExcluded = text.startsWith(\"!!\");\n\t\t\t\t\tconst command = isExcluded ? text.slice(2).trim() : text.slice(1).trim();\n\t\t\t\t\tif (!command) {\n\t\t\t\t\t\t// Bare ! / !! is bash mode with nothing to run; don't send it as a prompt\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (this.isBashRunning()) {\n\t\t\t\t\t\tthis.showWarning(\n\t\t\t\t\t\t\t`A bash command is already running. Press ${keyText(\"app.clear\")} to cancel it first.`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t// A streaming side turn blocks bash just like it blocks follow-up\n\t\t\t\t\t// replies: overlapping pane turns would seed out of order.\n\t\t\t\t\tif (this.sideQuestionComponent && this.activeSideQuestionId) {\n\t\t\t\t\t\tthis.editor.setText(text);\n\t\t\t\t\t\tthis.showWarning(\"Wait for the current side question to finish or cancel it first.\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t// Inside a side conversation the command runs inside the pane (its\n\t\t\t\t\t// bash_start event mounts the usual BashExecutionComponent there),\n\t\t\t\t\t// stays out of the main-session context, and (for !, not !!) seeds\n\t\t\t\t\t// follow-up side questions.\n\t\t\t\t\tconst sideBash = this.sideQuestionComponent\n\t\t\t\t\t\t? { runId: randomUUID(), input: text, seedTranscript: !isExcluded }\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tif (sideBash) {\n\t\t\t\t\t\tthis.sideQuestionBash = sideBash;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.clearSideQuestion({ abort: true });\n\t\t\t\t\t}\n\t\t\t\t\tthis.editor.addToHistory?.(text);\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\t// Optimistic: bash_start only fires after extension dispatch, and the\n\t\t\t\t\t// clear key must already route to abortBash in that window.\n\t\t\t\t\tthis.patchConnectionState({ isBashRunning: true });\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait this.agentConnection.executeBash(command, {\n\t\t\t\t\t\t\texcludeFromContext: isExcluded || sideBash !== undefined,\n\t\t\t\t\t\t\t...(sideBash ? { transient: true, runId: sideBash.runId } : {}),\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t// Re-sync rather than assume idle: the rejection may mean another\n\t\t\t\t\t\t// client's bash run already holds the slot.\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst state = await this.agentConnection.getState();\n\t\t\t\t\t\t\tthis.patchConnectionState({ isBashRunning: state.isBashRunning });\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tthis.patchConnectionState({ isBashRunning: false });\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (this.sideQuestionBash === sideBash) {\n\t\t\t\t\t\t\tthis.sideQuestionBash = undefined;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (sideBash && this.sideQuestionBashDiscarded === sideBash.runId) {\n\t\t\t\t\t\t\t// The pane discarded this run, but it never started, so no\n\t\t\t\t\t\t\t// bash_end will arrive to consume the marker.\n\t\t\t\t\t\t\tthis.sideQuestionBashDiscarded = undefined;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// An open side-question pane captures replies as follow-up side\n\t\t\t\t// questions; ! bash routed above and slash commands were rejected\n\t\t\t\t// earlier with a notice. Esc returns to the main thread.\n\t\t\t\tif (this.sideQuestionComponent) {\n\t\t\t\t\t// A follow-up submitted mid-bash would seed the transcript ahead of\n\t\t\t\t\t// the output it reacts to; make it wait like a running side turn.\n\t\t\t\t\t// The editor cleared its buffer before onSubmit fired, so blocked\n\t\t\t\t\t// paths put the draft back rather than merely skip clearing it.\n\t\t\t\t\tif (this.sideQuestionBash) {\n\t\t\t\t\t\tthis.editor.setText(text);\n\t\t\t\t\t\tthis.showWarning(\"Wait for the running command to finish or cancel it first.\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (this.activeSideQuestionId) {\n\t\t\t\t\t\tthis.editor.setText(text);\n\t\t\t\t\t\tawait this.handleSideQuestion(text);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t// Side questions are text-only end to end; a reply with pasted\n\t\t\t\t\t// images gets an in-pane notice instead of silently dropping them.\n\t\t\t\t\tif (this.hasPastedImagesFor(text)) {\n\t\t\t\t\t\tthis.editor.setText(text);\n\t\t\t\t\t\tthis.sideQuestionComponent.addTurn({\n\t\t\t\t\t\t\tid: `side-notice-${randomUUID()}`,\n\t\t\t\t\t\t\tquestion: text,\n\t\t\t\t\t\t\tanswer: \"Images are not supported in side conversations. Press esc to return to the main thread.\",\n\t\t\t\t\t\t\tstatus: \"complete\",\n\t\t\t\t\t\t});\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.editor.addToHistory?.(text);\n\t\t\t\t\tawait this.handleSideQuestion(text);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tthis.clearSideQuestion({ abort: true });\n\t\t\t\tthis.flushPendingBashComponents();\n\t\t\t\tconst images = this.collectImagesFor(text);\n\t\t\t\tthis.editor.addToHistory?.(text);\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tconst promptStashAfterClear = this.promptStash;\n\t\t\t\tsubmissionOutcome = (await this.admitPendingStartupPrompts?.()) ?? \"admitted\";\n\t\t\t\t// Retention is not admission. Startup drafts were inserted synchronously\n\t\t\t\t// before the barrier settled, so append this blocked submission behind them\n\t\t\t\t// and never let it prompt or overtake them.\n\t\t\t\tif (submissionOutcome === \"retained\") {\n\t\t\t\t\tthis.retainSubmittedDraft(submittedDraft ?? { text }, submissionGeneration);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// The barrier also settles when the run lifecycle ends; a submit resumed\n\t\t\t\t// by teardown must neither prompt nor mutate the editor/durable stash.\n\t\t\t\tif (\n\t\t\t\t\tsubmissionOutcome === \"lifecycle-cancelled\" ||\n\t\t\t\t\tthis.isShuttingDown ||\n\t\t\t\t\tthis.agentsViewRequest ||\n\t\t\t\t\tthis.promptStashSessionId !== submissionSessionId\n\t\t\t\t) {\n\t\t\t\t\t// The editor is already torn down, but its shared session stash outlives\n\t\t\t\t\t// this view. Preserve the submitted draft in the stash of the session it\n\t\t\t\t\t// was typed for, without overwriting an explicit older stash.\n\t\t\t\t\tthis.retainSubmittedDraft(submittedDraft ?? { text }, submissionGeneration, submissionStashState);\n\t\t\t\t\tsubmissionOutcome = \"lifecycle-cancelled\";\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tawait this.agentConnection.prompt(text, {\n\t\t\t\t\t\tstreamingBehavior,\n\t\t\t\t\t\tqueueIfBusy: true,\n\t\t\t\t\t\timages,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Generation guards editor ownership, not draft durability: a stale\n\t\t\t\t\t// rejection must be retained rather than overwrite newer input or vanish.\n\t\t\t\t\tconst rejectedDraft = submittedDraft ?? { text };\n\t\t\t\t\tconst canRestore =\n\t\t\t\t\t\t!this.isShuttingDown &&\n\t\t\t\t\t\t!this.agentsViewRequest &&\n\t\t\t\t\t\tsubmissionGeneration === this.inputSubmissionGeneration &&\n\t\t\t\t\t\tthis.editor.getText().length === 0;\n\t\t\t\t\tif (canRestore) {\n\t\t\t\t\t\tconst canRestorePasteSnapshot =\n\t\t\t\t\t\t\trejectedDraft.pasteSnapshot === undefined || this.editor.restorePasteSnapshot !== undefined;\n\t\t\t\t\t\tthis.editor.setText(\n\t\t\t\t\t\t\tcanRestorePasteSnapshot ? rejectedDraft.text : (rejectedDraft.expandedText ?? rejectedDraft.text),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (rejectedDraft.pasteSnapshot && this.editor.restorePasteSnapshot) {\n\t\t\t\t\t\t\tthis.editor.restorePasteSnapshot(rejectedDraft.pasteSnapshot);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.latestEditorPromptStash = this.snapshotPromptStash(this.editor.getText());\n\t\t\t\t\t\tif (this.promptStash === promptStashAfterClear) this.promptStash = promptStashToRestore;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.retainSubmittedDraft(rejectedDraft, submissionGeneration, submissionStashState);\n\t\t\t\t\t}\n\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t} finally {\n\t\t\t\tif (this.isShuttingDown || this.agentsViewRequest) {\n\t\t\t\t\tsubmissionOutcome = \"lifecycle-cancelled\";\n\t\t\t\t}\n\t\t\t\tif (\n\t\t\t\t\tsubmissionOutcome === \"admitted\" &&\n\t\t\t\t\trestorePromptStashAfterSubmit &&\n\t\t\t\t\tpromptStashToRestore !== undefined &&\n\t\t\t\t\tsubmissionGeneration === this.inputSubmissionGeneration\n\t\t\t\t) {\n\t\t\t\t\tthis.restorePromptStashIfEditorEmpty(promptStashToRestore);\n\t\t\t\t}\n\t\t\t\tthis.inputSubmissionsPending--;\n\t\t\t\tif (this.inputSubmissionsPending === 0 && this.pendingPromptStashReleases.length > 0) {\n\t\t\t\t\tthis.completeDeferredPromptStashRelease();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\tprivate subscribeToAgent(): void {\n\t\tthis.unsubscribe = this.agentConnection.subscribe(async (event) => {\n\t\t\ttry {\n\t\t\t\tif (event.type === \"session_event\") {\n\t\t\t\t\t// Connection adapters dispatch without awaiting, so serialize events.\n\t\t\t\t\t// Replacement advances the generation before entering this queue, which\n\t\t\t\t\t// prevents already-queued source events from mutating the target UI.\n\t\t\t\t\tconst generation = this.sessionEventGeneration;\n\t\t\t\t\tconst run = this.sessionEventQueue.then(() =>\n\t\t\t\t\t\tgeneration === this.sessionEventGeneration ? this.handleEvent(event.event) : undefined,\n\t\t\t\t\t);\n\t\t\t\t\tthis.sessionEventQueue = run.catch(() => {});\n\t\t\t\t\tawait run;\n\t\t\t\t} else if (event.type === \"session_replaced\") {\n\t\t\t\t\tconst generation = ++this.sessionEventGeneration;\n\t\t\t\t\tconst run = this.sessionEventQueue.then(async () => {\n\t\t\t\t\t\tif (generation !== this.sessionEventGeneration) return;\n\t\t\t\t\t\tthis.resetSideQuestion();\n\t\t\t\t\t\tthis.resetExtensionUI();\n\t\t\t\t\t\tthis.applyConnectionStateSnapshot(event.state);\n\t\t\t\t\t\tthis.resetCurrentSessionRenderState();\n\t\t\t\t\t\tawait this.rebindCurrentSession();\n\t\t\t\t\t\tawait this.renderInitialMessages();\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t});\n\t\t\t\t\tthis.sessionEventQueue = run.catch(() => {});\n\t\t\t\t\tawait run;\n\t\t\t\t} else if (event.type === \"session_resynced\") {\n\t\t\t\t\tconst generation = this.sessionEventGeneration;\n\t\t\t\t\tconst run = this.sessionEventQueue.then(async () => {\n\t\t\t\t\t\tif (generation !== this.sessionEventGeneration) return false;\n\t\t\t\t\t\tawait this.refreshCommandCatalogForCurrentSession?.();\n\t\t\t\t\t\tif (generation !== this.sessionEventGeneration) return false;\n\t\t\t\t\t\tawait this.renderResyncedSession(event.snapshot);\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t});\n\t\t\t\t\tthis.sessionEventQueue = run.then(() => undefined).catch(() => {});\n\t\t\t\t\tif (await run) this.ui.requestRender();\n\t\t\t\t} else if (event.type === \"session_status\") {\n\t\t\t\t\tthis.sessionRecap = event.recap;\n\t\t\t\t\tthis.patchConnectionState({ recap: event.recap });\n\t\t\t\t\tthis.renderRecap();\n\t\t\t\t} else if (event.type === \"side_question_event\") {\n\t\t\t\t\tthis.handleSideQuestionEvent(event.event);\n\t\t\t\t} else if (event.type === \"extension_ui_request\") {\n\t\t\t\t\tawait this.handleConnectionExtensionUiRequest(event.request);\n\t\t\t\t} else if (event.type === \"connection_status\") {\n\t\t\t\t\tthis.showStatus(\n\t\t\t\t\t\tevent.status === \"connected\" ? \"Daemon reconnected\" : \"Daemon connection lost; reconnecting…\",\n\t\t\t\t\t\tevent.status === \"reconnecting\" ? \"warning\" : \"dim\",\n\t\t\t\t\t);\n\t\t\t\t\tif (event.status === \"connected\") {\n\t\t\t\t\t\tawait this.refreshHeartbeatCatalog();\n\t\t\t\t\t}\n\t\t\t\t} else if (event.type === \"heartbeats_changed\") {\n\t\t\t\t\tawait this.refreshHeartbeatCatalog();\n\t\t\t\t} else if (event.type === \"closed\") {\n\t\t\t\t\tthis.showError(event.error ?? \"Agent connection closed\");\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate async handleConnectionExtensionUiRequest(request: AgentConnectionExtensionUiRequest): Promise<void> {\n\t\tlet response: AgentConnectionExtensionUiResponse | undefined;\n\t\tconst expectsResponse = this.expectsConnectionExtensionUiResponse(request);\n\n\t\ttry {\n\t\t\tif (expectsResponse) {\n\t\t\t\tlet cancelLocal: (response: AgentConnectionExtensionUiResponse) => void = () => {};\n\t\t\t\tconst cancelled = new Promise<AgentConnectionExtensionUiResponse>((resolve) => {\n\t\t\t\t\tcancelLocal = resolve;\n\t\t\t\t});\n\t\t\t\tthis.activeConnectionExtensionUiRequests.set(request.id, {\n\t\t\t\t\tcancelLocal: () => cancelLocal({ cancelled: true }),\n\t\t\t\t});\n\t\t\t\tresponse = await Promise.race([this.resolveConnectionExtensionUiRequest(request), cancelled]);\n\t\t\t} else {\n\t\t\t\tresponse = await this.resolveConnectionExtensionUiRequest(request);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\tresponse = { cancelled: true };\n\t\t}\n\n\t\tif (response === undefined) {\n\t\t\tthis.activeConnectionExtensionUiRequests.delete(request.id);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.activeConnectionExtensionUiRequests.delete(request.id)) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tawait this.agentConnection.respondToExtensionUiRequest(request.id, response);\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate expectsConnectionExtensionUiResponse(request: AgentConnectionExtensionUiRequest): boolean {\n\t\treturn (\n\t\t\trequest.method === \"select\" ||\n\t\t\trequest.method === \"confirm\" ||\n\t\t\trequest.method === \"input\" ||\n\t\t\trequest.method === \"editor\"\n\t\t);\n\t}\n\n\tprivate cancelActiveConnectionExtensionUiRequests(): void {\n\t\tconst requestIds = [...this.activeConnectionExtensionUiRequests.keys()];\n\t\tfor (const requestId of requestIds) {\n\t\t\tconst activeRequest = this.activeConnectionExtensionUiRequests.get(requestId);\n\t\t\tif (!activeRequest) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.activeConnectionExtensionUiRequests.delete(requestId);\n\t\t\tactiveRequest.cancelLocal();\n\t\t\tvoid this.agentConnection.respondToExtensionUiRequest(requestId, { cancelled: true }).catch((error) => {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate async resolveConnectionExtensionUiRequest(\n\t\trequest: AgentConnectionExtensionUiRequest,\n\t): Promise<AgentConnectionExtensionUiResponse | undefined> {\n\t\tconst { payload } = request;\n\t\tswitch (request.method) {\n\t\t\tcase \"select\": {\n\t\t\t\tconst title = getPayloadString(payload, \"title\");\n\t\t\t\tconst options = getPayloadStringArray(payload, \"options\");\n\t\t\t\tif (!title || !options) {\n\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t}\n\t\t\t\tconst value = await this.showExtensionSelector(title, options, {\n\t\t\t\t\ttimeout: getPayloadNumber(payload, \"timeout\"),\n\t\t\t\t});\n\t\t\t\treturn value === undefined ? { cancelled: true } : { value };\n\t\t\t}\n\t\t\tcase \"confirm\": {\n\t\t\t\tconst title = getPayloadString(payload, \"title\");\n\t\t\t\tconst message = getPayloadString(payload, \"message\");\n\t\t\t\tif (!title || message === undefined) {\n\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t}\n\t\t\t\tconst confirmed = await this.showExtensionConfirm(title, message, {\n\t\t\t\t\ttimeout: getPayloadNumber(payload, \"timeout\"),\n\t\t\t\t});\n\t\t\t\treturn { confirmed };\n\t\t\t}\n\t\t\tcase \"input\": {\n\t\t\t\tconst title = getPayloadString(payload, \"title\");\n\t\t\t\tif (!title) {\n\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t}\n\t\t\t\tconst value = await this.showExtensionInput(title, getPayloadString(payload, \"placeholder\"), {\n\t\t\t\t\ttimeout: getPayloadNumber(payload, \"timeout\"),\n\t\t\t\t});\n\t\t\t\treturn value === undefined ? { cancelled: true } : { value };\n\t\t\t}\n\t\t\tcase \"editor\": {\n\t\t\t\tconst title = getPayloadString(payload, \"title\");\n\t\t\t\tif (!title) {\n\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t}\n\t\t\t\tconst value = await this.showExtensionEditor(title, getPayloadString(payload, \"prefill\"));\n\t\t\t\treturn value === undefined ? { cancelled: true } : { value };\n\t\t\t}\n\t\t\tcase \"notify\": {\n\t\t\t\tconst message = getPayloadString(payload, \"message\");\n\t\t\t\tif (message) {\n\t\t\t\t\tthis.showExtensionNotify(message, getPayloadNotifyType(payload, \"notifyType\"));\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcase \"setStatus\": {\n\t\t\t\tconst key = getPayloadString(payload, \"statusKey\");\n\t\t\t\tif (key) {\n\t\t\t\t\tthis.setExtensionStatus(key, getPayloadString(payload, \"statusText\"));\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcase \"setWorkingMessage\": {\n\t\t\t\tthis.workingMessage = getPayloadString(payload, \"message\");\n\t\t\t\tif (this.loadingAnimation) {\n\t\t\t\t\tthis.updateWorkingLoaderMessage();\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcase \"setWorkingVisible\": {\n\t\t\t\tconst visible = getPayloadBoolean(payload, \"visible\");\n\t\t\t\tif (visible !== undefined) {\n\t\t\t\t\tthis.setWorkingVisible(visible);\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcase \"setWorkingIndicator\": {\n\t\t\t\tthis.setWorkingIndicator(getPayloadWorkingIndicatorOptions(payload, \"options\"));\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcase \"setHiddenThinkingLabel\": {\n\t\t\t\tthis.setHiddenThinkingLabel(getPayloadString(payload, \"label\"));\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcase \"setWidget\": {\n\t\t\t\tconst key = getPayloadString(payload, \"widgetKey\");\n\t\t\t\tif (key) {\n\t\t\t\t\tconst placement = getPayloadWidgetPlacement(payload, \"widgetPlacement\");\n\t\t\t\t\tthis.setExtensionWidget(\n\t\t\t\t\t\tkey,\n\t\t\t\t\t\tgetPayloadStringArray(payload, \"widgetLines\"),\n\t\t\t\t\t\tplacement ? { placement } : undefined,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcase \"setTitle\": {\n\t\t\t\tconst title = getPayloadString(payload, \"title\");\n\t\t\t\tif (title) {\n\t\t\t\t\tthis.ui.terminal.setTitle(title);\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcase \"setEditorText\": {\n\t\t\t\tconst text = getPayloadString(payload, \"text\");\n\t\t\t\tif (text !== undefined) {\n\t\t\t\t\tthis.editor.setText(text);\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\tthis.showStatus(`Unsupported extension UI request: ${request.method}`);\n\t\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async handleEvent(event: AgentConnectionSessionEvent): Promise<void> {\n\t\tif (!this.isInitialized) {\n\t\t\tawait this.init();\n\t\t}\n\n\t\tthis.footer.invalidate();\n\t\tthis.updateConnectionStateFromEvent(event);\n\t\t// A new user message resets the activity tracker to 0, so the in-flight baseline must\n\t\t// reset with it. (agent_start on auto-retry does not reset the tracker.)\n\t\tif (event.type === \"message_start\") {\n\t\t\tthis.prepareFeatureHintRun(event.message);\n\t\t}\n\t\tif (event.type === \"message_start\" && (event.message.role === \"user\" || isAgentSessionMessage(event.message))) {\n\t\t\tthis.contextUsageTokenBaseline = 0;\n\t\t\tthis.setSessionHasMessages(true);\n\t\t\tthis.clearShortcutGuide();\n\t\t\tthis.agentRunFileChanges.clear();\n\t\t\tthis.renderRecap();\n\t\t}\n\t\tthis.activityTracker.handleEvent(event);\n\t\tthis.updateWorkingLoaderMessage();\n\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\t\tthis.featureHintRunPending = this.getRetryAttempt() === 0;\n\t\t\t\tthis.resetPendingToolState();\n\t\t\t\tthis.renderRecap();\n\t\t\t\tif (this.settingsManager.getShowTerminalProgress()) {\n\t\t\t\t\tthis.ui.terminal.setProgress(true);\n\t\t\t\t}\n\t\t\t\tif (this.retryCountdown) {\n\t\t\t\t\tthis.retryCountdown.dispose();\n\t\t\t\t\tthis.retryCountdown = undefined;\n\t\t\t\t}\n\t\t\t\tif (this.retryLoader) {\n\t\t\t\t\tthis.retryLoader.stop();\n\t\t\t\t\tthis.retryLoader = undefined;\n\t\t\t\t}\n\t\t\t\tthis.stopWorkingLoader();\n\t\t\t\tif (this.workingVisible) {\n\t\t\t\t\tthis.startWorkingLoader();\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\n\t\t\tcase \"session_action_update\": {\n\t\t\t\tthis.replaceConnectionQueue({\n\t\t\t\t\tsteering: [...event.actions.steering],\n\t\t\t\t\tfollowUp: [...event.actions.followUps],\n\t\t\t\t});\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"session_info_changed\":\n\t\t\t\tthis.updateTerminalTitle();\n\t\t\t\tthis.footer.invalidate();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\n\t\t\tcase \"thinking_level_changed\":\n\t\t\t\tthis.footer.invalidate();\n\t\t\t\tthis.subagentSummaryLine.invalidate();\n\t\t\t\tthis.updateEditorBorderColor();\n\t\t\t\tbreak;\n\n\t\t\tcase \"service_tier_changed\":\n\t\t\t\tthis.footer.invalidate();\n\t\t\t\tthis.subagentSummaryLine.invalidate();\n\t\t\t\tbreak;\n\n\t\t\tcase \"bash_start\": {\n\t\t\t\tif (this.sideQuestionBashDiscarded !== undefined) {\n\t\t\t\t\tif (event.runId === this.sideQuestionBashDiscarded) {\n\t\t\t\t\t\t// The discarded side run now owns the bash slot. Abort only\n\t\t\t\t\t\t// after matching its identity so a foreign run is never killed.\n\t\t\t\t\t\tvoid this.agentConnection.abortBash().catch(() => undefined);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\t// A different run claimed the slot, so the discarded run lost the\n\t\t\t\t\t// race and can never start (its execute_bash will reject); render\n\t\t\t\t\t// this run normally instead of swallowing it.\n\t\t\t\t\tthis.sideQuestionBashDiscarded = undefined;\n\t\t\t\t}\n\t\t\t\tconst ownSideBash = this.sideQuestionBash !== undefined && event.runId === this.sideQuestionBash.runId;\n\t\t\t\tif (event.transient && !ownSideBash) {\n\t\t\t\t\t// Another client's side-conversation run: it renders only in that\n\t\t\t\t\t// client's pane, never in this window's chat.\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tconst component = new BashExecutionComponent(event.command, this.ui, event.excludeFromContext, {\n\t\t\t\t\tsuppressLeadingSpace: this.chatContainer.children.at(-1) instanceof AgentMessageComponent,\n\t\t\t\t});\n\t\t\t\tif (ownSideBash && this.sideQuestionComponent) {\n\t\t\t\t\t// Same component as the main thread, mounted inside the pane.\n\t\t\t\t\tthis.sideQuestionComponent.addBash(component);\n\t\t\t\t\tthis.sideQuestionBashComponent = component;\n\t\t\t\t} else if (this.isAgentStreaming()) {\n\t\t\t\t\tthis.pendingMessagesContainer.addChild(component);\n\t\t\t\t\tthis.pendingBashComponents.push(component);\n\t\t\t\t} else {\n\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t}\n\t\t\t\tthis.activeBashComponent = component;\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"bash_output\":\n\t\t\t\tif (this.sideQuestionBashDiscarded !== undefined) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (this.activeBashComponent) {\n\t\t\t\t\tthis.activeBashComponent.appendOutput(event.chunk);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"bash_end\": {\n\t\t\t\tif (this.sideQuestionBashDiscarded !== undefined) {\n\t\t\t\t\t// Only the discarded run's own end consumes the marker; bash_start\n\t\t\t\t\t// already cleared it for any other run that claimed the slot.\n\t\t\t\t\tthis.sideQuestionBashDiscarded = undefined;\n\t\t\t\t\tthis.activeBashComponent = undefined;\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tconst component = this.activeBashComponent;\n\t\t\t\tif (component) {\n\t\t\t\t\tif (event.errorMessage) {\n\t\t\t\t\t\tcomponent.setFailed(event.errorMessage);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcomponent.setComplete(\n\t\t\t\t\t\t\tevent.exitCode,\n\t\t\t\t\t\t\tevent.cancelled,\n\t\t\t\t\t\t\tevent.truncated ? ({ truncated: true } as TruncationResult) : undefined,\n\t\t\t\t\t\t\tevent.fullOutputPath,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tthis.activeBashComponent = undefined;\n\t\t\t\t} else if (event.errorMessage && !event.transient) {\n\t\t\t\t\t// Transient failures surface in the owning client's pane, not here.\n\t\t\t\t\tthis.showError(`Bash command failed: ${event.errorMessage}`);\n\t\t\t\t}\n\t\t\t\t// Seed the side transcript only when our own pane-mounted run ended.\n\t\t\t\tif (component !== undefined && component === this.sideQuestionBashComponent) {\n\t\t\t\t\tthis.sideQuestionBashComponent = undefined;\n\t\t\t\t\tthis.finishSideQuestionBash(event, component.getOutput());\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"message_start\":\n\t\t\t\t// The run's first starter anchors the elapsed display; mid-turn steering must not restart it.\n\t\t\t\tif (this.turnStartedAt === undefined && startsAgentRun(event.message)) {\n\t\t\t\t\tthis.turnStartedAt = event.message.timestamp;\n\t\t\t\t\tif (this.workingStartedAt !== undefined) {\n\t\t\t\t\t\tthis.workingStartedAt = event.message.timestamp;\n\t\t\t\t\t\tthis.updateWorkingLoaderMessage();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (event.message.role === \"custom\") {\n\t\t\t\t\tif (isSessionSlashCommandMessage(event.message) && event.message.details.command.name === \"refine\") {\n\t\t\t\t\t\tthis.startRefineLoader();\n\t\t\t\t\t}\n\t\t\t\t\t// The /refine result row is the user refine's settle edge; refine_complete\n\t\t\t\t\t// alone can belong to an agent/auto refinement the queued /refine waited on.\n\t\t\t\t\tif (\n\t\t\t\t\t\tisSessionSlashCommandResultMessage(event.message) &&\n\t\t\t\t\t\tevent.message.details.command.name === \"refine\"\n\t\t\t\t\t) {\n\t\t\t\t\t\tthis.stopRefineLoader();\n\t\t\t\t\t}\n\t\t\t\t\tthis.addMessageToChat(event.message);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t} else if (event.message.role === \"user\") {\n\t\t\t\t\tthis.addMessageToChat(event.message);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t} else if (event.message.role === \"assistant\") {\n\t\t\t\t\tthis.startAssistantStreamingMessage(event.message);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"message_update\":\n\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\tthis.streamingMessage = event.message;\n\t\t\t\t\tthis.ensureAssistantStreamingComponent(event.message).updateContent(this.streamingMessage);\n\n\t\t\t\t\tfor (const content of this.streamingMessage.content) {\n\t\t\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\t\t\tawait this.getOrCreatePendingToolComponent(content);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"message_end\":\n\t\t\t\tif (event.message.role === \"user\") break;\n\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\tthis.streamingMessage = event.message;\n\t\t\t\t\tlet errorMessage: string | undefined;\n\t\t\t\t\tif (this.streamingMessage.stopReason === \"aborted\") {\n\t\t\t\t\t\tconst retryAttempt = this.getRetryAttempt();\n\t\t\t\t\t\tconst elapsedSuffix =\n\t\t\t\t\t\t\tthis.workingStartedAt === undefined\n\t\t\t\t\t\t\t\t? \"\"\n\t\t\t\t\t\t\t\t: ` · ${this.formatWorkingElapsed(Date.now() - this.workingStartedAt)}`;\n\t\t\t\t\t\terrorMessage =\n\t\t\t\t\t\t\tretryAttempt > 0\n\t\t\t\t\t\t\t\t? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? \"s\" : \"\"}${elapsedSuffix}`\n\t\t\t\t\t\t\t\t: `Operation aborted${elapsedSuffix}`;\n\t\t\t\t\t\tthis.streamingMessage.errorMessage = errorMessage;\n\t\t\t\t\t}\n\t\t\t\t\tthis.ensureAssistantStreamingComponent(event.message).updateContent(this.streamingMessage);\n\n\t\t\t\t\tif (this.streamingMessage.stopReason === \"aborted\" || this.streamingMessage.stopReason === \"error\") {\n\t\t\t\t\t\tif (!errorMessage) {\n\t\t\t\t\t\t\terrorMessage = this.streamingMessage.errorMessage || \"Error\";\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfor (const [, component] of this.pendingTools.entries()) {\n\t\t\t\t\t\t\tcomponent.updateResult({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: errorMessage }],\n\t\t\t\t\t\t\t\tisError: true,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.resetPendingToolState();\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Args are now complete - trigger diff computation for edit tools\n\t\t\t\t\t\tfor (const [, component] of this.pendingTools.entries()) {\n\t\t\t\t\t\t\tcomponent.setArgsComplete();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tthis.streamingComponent = undefined;\n\t\t\t\t\tthis.streamingMessage = undefined;\n\t\t\t\t\tthis.footer.invalidate();\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool_execution_start\": {\n\t\t\t\tthis.startedToolCalls.add(event.toolCallId);\n\t\t\t\tlet component = this.pendingTools.get(event.toolCallId);\n\t\t\t\tif (!component) {\n\t\t\t\t\tcomponent = await this.getOrCreatePendingToolComponent({\n\t\t\t\t\t\tid: event.toolCallId,\n\t\t\t\t\t\tname: event.toolName,\n\t\t\t\t\t\targuments: event.args,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (component) {\n\t\t\t\t\tcomponent.markExecutionStarted();\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_update\": {\n\t\t\t\tconst component = this.pendingTools.get(event.toolCallId);\n\t\t\t\tif (component) {\n\t\t\t\t\tcomponent.updateResult({ ...event.partialResult, isError: false }, true);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_end\": {\n\t\t\t\tconst component = this.pendingTools.get(event.toolCallId);\n\t\t\t\tif (component) {\n\t\t\t\t\tcomponent.updateResult({ ...event.result, isError: event.isError });\n\t\t\t\t\tthis.pendingTools.delete(event.toolCallId);\n\t\t\t\t\tthis.startedToolCalls.delete(event.toolCallId);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"ipython_sent_agent_message\": {\n\t\t\t\tconst messages = this.lateIpythonSentAgentMessages.get(event.toolCallId) ?? [];\n\t\t\t\tif (!messages.some((message) => message.id === event.message.id)) {\n\t\t\t\t\tmessages.push(event.message);\n\t\t\t\t\tthis.lateIpythonSentAgentMessages.set(event.toolCallId, messages);\n\t\t\t\t}\n\t\t\t\tthis.ipythonToolComponents.get(event.toolCallId)?.appendSentAgentMessage(event.message);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"turn_end\":\n\t\t\t\tmergeTurnFileChanges(this.agentRunFileChanges, event.message, event.toolResults, this.getCurrentCwd());\n\t\t\t\tbreak;\n\n\t\t\tcase \"agent_end\":\n\t\t\t\tif (this.settingsManager.getShowTerminalProgress()) {\n\t\t\t\t\tthis.ui.terminal.setProgress(false);\n\t\t\t\t}\n\t\t\t\tthis.turnStartedAt = undefined;\n\t\t\t\t// Drops the loader; background subagents are shown by the tree, not the loader.\n\t\t\t\tthis.syncWorkingLoader();\n\t\t\t\tif (this.streamingComponent) {\n\t\t\t\t\tif (this.streamingMessage) {\n\t\t\t\t\t\tthis.streamingComponent.updateContent(this.streamingMessage);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.chatContainer.removeChild(this.streamingComponent);\n\t\t\t\t\t}\n\t\t\t\t\tthis.streamingComponent = undefined;\n\t\t\t\t\tthis.streamingMessage = undefined;\n\t\t\t\t}\n\t\t\t\tthis.flushPendingBashComponents();\n\t\t\t\tthis.resetPendingToolState();\n\t\t\t\tthis.renderRecap();\n\n\t\t\t\tthis.applyOptimisticContextUsage();\n\t\t\t\t// Auto-compaction can start server-side while this event is being handled.\n\t\t\t\t// Do not hold its start event behind a stats RPC; stale refreshes are discarded.\n\t\t\t\tvoid this.refreshConnectionContextUsage();\n\n\t\t\t\tawait this.checkShutdownRequested();\n\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\n\t\t\tcase \"compaction_start\": {\n\t\t\t\tthis.startCompactionLoader(event.reason, event.customInstructions);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"compaction_end\": {\n\t\t\t\tif (this.settingsManager.getShowTerminalProgress()) {\n\t\t\t\t\tthis.ui.terminal.setProgress(false);\n\t\t\t\t}\n\t\t\t\tif (this.autoCompactionLoader) {\n\t\t\t\t\tthis.autoCompactionLoader.stop();\n\t\t\t\t\tthis.autoCompactionLoader = undefined;\n\t\t\t\t\tthis.statusContainer.clear();\n\t\t\t\t}\n\t\t\t\t// Restore the working loader if streaming/subagents still warrant it.\n\t\t\t\tthis.syncWorkingLoader();\n\t\t\t\tif (event.aborted) {\n\t\t\t\t\tif (event.reason === \"manual\") this.showError(\"Compaction cancelled\");\n\t\t\t\t} else if (event.result) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait this.rebuildChatFromMessages();\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\t\tthis.showError(`Compaction succeeded, but the transcript could not be refreshed: ${message}`);\n\t\t\t\t\t}\n\t\t\t\t\tawait this.refreshConnectionContextUsage();\n\t\t\t\t\tthis.footer.invalidate();\n\t\t\t\t} else if (event.errorMessage && event.reason === \"manual\") {\n\t\t\t\t\tif (event.errorSeverity === \"warning\") this.showWarning(event.errorMessage);\n\t\t\t\t\telse this.showError(event.errorMessage);\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"auto_retry_start\": {\n\t\t\t\tthis.stopWorkingLoader();\n\t\t\t\tthis.statusContainer.clear();\n\t\t\t\tthis.retryCountdown?.dispose();\n\t\t\t\tconst retryMessage = (seconds: number) =>\n\t\t\t\t\t`Retrying (${event.attempt}/${event.maxAttempts}) in ${seconds}s... (${keyText(\"app.clear\")} to cancel)`;\n\t\t\t\tthis.retryLoader = new Loader(\n\t\t\t\t\tthis.ui,\n\t\t\t\t\t(spinner) => theme.fg(\"muted\", spinner),\n\t\t\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\t\t\tretryMessage(Math.ceil(event.delayMs / 1000)),\n\t\t\t\t);\n\t\t\t\tthis.retryCountdown = new CountdownTimer(\n\t\t\t\t\tevent.delayMs,\n\t\t\t\t\tthis.ui,\n\t\t\t\t\t(seconds) => {\n\t\t\t\t\t\tthis.retryLoader?.setMessage(retryMessage(seconds));\n\t\t\t\t\t},\n\t\t\t\t\t() => {\n\t\t\t\t\t\tthis.retryCountdown = undefined;\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t\tthis.statusContainer.addChild(this.retryLoader);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"auto_retry_end\": {\n\t\t\t\tif (this.retryCountdown) {\n\t\t\t\t\tthis.retryCountdown.dispose();\n\t\t\t\t\tthis.retryCountdown = undefined;\n\t\t\t\t}\n\t\t\t\tif (this.retryLoader) {\n\t\t\t\t\tthis.retryLoader.stop();\n\t\t\t\t\tthis.retryLoader = undefined;\n\t\t\t\t\tthis.statusContainer.clear();\n\t\t\t\t}\n\t\t\t\t// Restore the working loader if streaming/subagents still warrant it.\n\t\t\t\tthis.syncWorkingLoader();\n\t\t\t\t// Show error only on final failure (success shows normal response)\n\t\t\t\tif (!event.success) {\n\t\t\t\t\tthis.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || \"Unknown error\"}`);\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"auth_stale\": {\n\t\t\t\tthis.applyAuthStaleEvent(event);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"rlm_child_update\":\n\t\t\t\tthis.updateSubagentSummary(event.child);\n\t\t\t\tbreak;\n\n\t\t\tcase \"goal_update\":\n\t\t\t\tthis.handleGoalUpdate(event.goal);\n\t\t\t\tbreak;\n\n\t\t\tcase \"refine_failed\":\n\t\t\t\t// This event has no request identity; the matching command result settles its loader.\n\t\t\t\tthis.showError(`Refinement failed: ${event.error}`);\n\t\t\t\tbreak;\n\n\t\t\tcase \"refine_complete\":\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate startAssistantStreamingMessage(message: AssistantMessage): void {\n\t\tthis.streamingComponent = new AssistantMessageComponent(\n\t\t\tundefined,\n\t\t\tthis.hideThinkingBlock,\n\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t\tthis.hiddenThinkingLabel,\n\t\t\t{\n\t\t\t\texpanded: this.toolOutputExpanded,\n\t\t\t\tprecededByToolActivity:\n\t\t\t\t\tthis.chatContainer.children.at(-1) instanceof ToolExecutionComponent ||\n\t\t\t\t\tthis.chatContainer.children.at(-1) instanceof AgentMessageComponent,\n\t\t\t},\n\t\t);\n\t\tthis.streamingMessage = message;\n\t\tthis.chatContainer.addChild(this.streamingComponent);\n\t\tthis.streamingComponent.updateContent(this.streamingMessage);\n\t}\n\n\tprivate ensureAssistantStreamingComponent(message: AssistantMessage): AssistantMessageComponent {\n\t\tlet component = this.streamingComponent;\n\t\tif (!component) {\n\t\t\tthis.startAssistantStreamingMessage(message);\n\t\t\tcomponent = this.streamingComponent;\n\t\t}\n\t\tif (!component) {\n\t\t\tthrow new Error(\"Failed to create assistant streaming component\");\n\t\t}\n\t\treturn component;\n\t}\n\n\tprivate handleGoalUpdate(goal: GoalState): void {\n\t\tthis.syncGoalTray(goal);\n\t\tif (this.shouldAnnounceGoalUpdate(goal)) {\n\t\t\tthis.showStatus(this.formatGoalStatus(goal));\n\t\t} else {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate syncGoalTray(goal: GoalState): void {\n\t\tthis.subagentSummaryLine.invalidate();\n\t\tthis.updateGoalTrayTimer(goal);\n\t}\n\n\tprivate updateGoalTrayTimer(goal: GoalState): void {\n\t\tif (goal.status === \"active\") {\n\t\t\tif (!this.goalTrayTimer) {\n\t\t\t\tthis.goalTrayTimer = setInterval(() => {\n\t\t\t\t\tthis.subagentSummaryLine.invalidate();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}, 1000);\n\t\t\t\tthis.goalTrayTimer.unref?.();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tthis.stopGoalTrayTimer();\n\t}\n\n\tprivate stopGoalTrayTimer(): void {\n\t\tif (!this.goalTrayTimer) {\n\t\t\treturn;\n\t\t}\n\t\tclearInterval(this.goalTrayTimer);\n\t\tthis.goalTrayTimer = undefined;\n\t}\n\n\tprivate setGoalAnnouncementBaseline(goal: GoalState): void {\n\t\tthis.lastGoalAnnouncement = this.goalAnnouncementSnapshot(goal);\n\t}\n\n\tprivate goalAnnouncementSnapshot(goal: GoalState): GoalAnnouncementSnapshot {\n\t\treturn {\n\t\t\tgoalId: goal.goalId,\n\t\t\tstatus: goal.status,\n\t\t\tobjective: goal.objective,\n\t\t\tlastReason: goal.lastReason,\n\t\t\tlastError: goal.lastError,\n\t\t};\n\t}\n\n\tprivate shouldAnnounceGoalUpdate(goal: GoalState): boolean {\n\t\tconst previous = this.lastGoalAnnouncement;\n\t\tconst next = this.goalAnnouncementSnapshot(goal);\n\t\tthis.lastGoalAnnouncement = next;\n\t\tif (!previous) {\n\t\t\treturn goal.status !== \"idle\";\n\t\t}\n\t\tif (previous.status !== next.status) {\n\t\t\treturn true;\n\t\t}\n\t\tif (previous.goalId !== next.goalId) {\n\t\t\treturn goal.status !== \"idle\";\n\t\t}\n\t\tswitch (goal.status) {\n\t\t\tcase \"active\":\n\t\t\t\treturn false;\n\t\t\tcase \"paused\":\n\t\t\tcase \"budget_limited\":\n\t\t\tcase \"complete\":\n\t\t\t\treturn previous.lastReason !== next.lastReason;\n\t\t\tcase \"error\":\n\t\t\t\treturn previous.lastError !== next.lastError;\n\t\t\tcase \"idle\":\n\t\t\t\treturn false;\n\t\t\tdefault: {\n\t\t\t\tconst _exhaustive: never = goal.status;\n\t\t\t\treturn _exhaustive;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate formatGoalStatus(goal: GoalState): string {\n\t\tconst usage = formatGoalUsage(goal);\n\t\tconst usageText = usage ? ` (${usage})` : \"\";\n\t\tswitch (goal.status) {\n\t\t\tcase \"idle\":\n\t\t\t\treturn \"No active goal\";\n\t\t\tcase \"active\":\n\t\t\t\treturn goal.objective\n\t\t\t\t\t? `Goal${this.formatGoalDetailSuffix(goal.objective, visibleWidth(\"Goal\"))}`\n\t\t\t\t\t: \"Pursuing goal\";\n\t\t\tcase \"paused\":\n\t\t\t\treturn goal.lastReason\n\t\t\t\t\t? `Goal paused${this.formatGoalDetailSuffix(goal.lastReason, visibleWidth(\"Goal paused\"))}`\n\t\t\t\t\t: \"Goal paused (/goal resume)\";\n\t\t\tcase \"budget_limited\":\n\t\t\t\tif (goal.lastReason) {\n\t\t\t\t\tconst prefix = `Goal budget limited${usageText}`;\n\t\t\t\t\treturn prefix + this.formatGoalDetailSuffix(goal.lastReason, visibleWidth(prefix));\n\t\t\t\t}\n\t\t\t\treturn `Goal budget limited${usageText}`;\n\t\t\tcase \"complete\":\n\t\t\t\treturn goal.lastReason\n\t\t\t\t\t? `Goal complete${this.formatGoalDetailSuffix(goal.lastReason, visibleWidth(\"Goal complete\"))}`\n\t\t\t\t\t: \"Goal complete\";\n\t\t\tcase \"error\":\n\t\t\t\treturn goal.lastError\n\t\t\t\t\t? `Goal error${this.formatGoalDetailSuffix(goal.lastError, visibleWidth(\"Goal error\"))}`\n\t\t\t\t\t: \"Goal error\";\n\t\t\tdefault: {\n\t\t\t\tconst _exhaustive: never = goal.status;\n\t\t\t\treturn _exhaustive;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate formatGoalDetailSuffix(value: string | undefined, prefixWidth: number): string {\n\t\tconst detail = value?.replace(/\\s+/g, \" \").trim();\n\t\tif (!detail) {\n\t\t\treturn \"\";\n\t\t}\n\t\tconst availableWidth = Math.min(120, Math.max(1, this.ui.terminal.columns - prefixWidth - 2));\n\t\tif (availableWidth < 8) {\n\t\t\treturn \"\";\n\t\t}\n\t\treturn `: ${truncateToWidth(detail, availableWidth)}`;\n\t}\n\n\tprivate seedSubagentSummary(children: readonly AgentConnectionRlmChildAgentSnapshot[] | undefined): void {\n\t\tfor (const child of children ?? []) {\n\t\t\t// Live updates can arrive before the initial snapshot; do not replace them\n\t\t\t// with the snapshot's older state.\n\t\t\tif (!this.subagentSnapshots.has(child.id) && child.status !== \"cancelled\") {\n\t\t\t\tthis.subagentSnapshots.set(child.id, child);\n\t\t\t}\n\t\t}\n\t\tthis.refreshSubagentSummary();\n\t}\n\n\tprivate replaceSubagentSummary(children: readonly AgentConnectionRlmChildAgentSnapshot[] | undefined): void {\n\t\tconst next = new Map<string, AgentConnectionRlmChildAgentSnapshot>();\n\t\tfor (const child of children ?? []) {\n\t\t\tif (child.status === \"cancelled\") continue;\n\t\t\tconst previous = this.subagentSnapshots.get(child.id);\n\t\t\tnext.set(child.id, previous ? mergeSubagentSnapshot(previous, child) : child);\n\t\t}\n\t\tthis.subagentSnapshots = next;\n\t\tthis.refreshSubagentSummary();\n\t}\n\n\tprivate updateSubagentSummary(child: AgentConnectionRlmChildAgentSnapshot): void {\n\t\tif (child.status === \"cancelled\") {\n\t\t\tthis.removeSubagentSnapshot(child.id);\n\t\t} else {\n\t\t\tconst previous = this.subagentSnapshots.get(child.id);\n\t\t\tthis.subagentSnapshots.set(child.id, previous ? mergeSubagentSnapshot(previous, child) : child);\n\t\t}\n\t\tthis.refreshSubagentSummary();\n\t}\n\n\tprivate refreshSubagentSummary(): void {\n\t\tthis.updateScopedHeartbeats();\n\t\tthis.updateSubagentSummaryLine();\n\t\tthis.updateWorkingPulse();\n\t\tthis.syncWorkingLoader();\n\t\tthis.updateWorkingLoaderMessage();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate updateSubagentSummaryLine(): void {\n\t\tconst activeHeartbeatSessionIds = new Set(\n\t\t\tthis.heartbeatCatalog\n\t\t\t\t.filter((heartbeat) => heartbeat.job.status === \"active\")\n\t\t\t\t.map((heartbeat) => heartbeat.job.activeSessionId),\n\t\t);\n\t\tthis.subagentSummaryLine.setSubagentCounts(\n\t\t\tcountDirectSubagentStatuses(this.subagentSnapshots.values(), this.rlmNodeId, activeHeartbeatSessionIds),\n\t\t);\n\t\tif (!this.subagentSummaryLine.isSelectable() && this.subagentSummaryLine.focused) this.focusEditor();\n\t}\n\n\tprivate removeSubagentSnapshot(id: string): void {\n\t\tthis.subagentSnapshots.delete(id);\n\t\tfor (const child of [...this.subagentSnapshots.values()]) {\n\t\t\tif (child.parentId === id) this.removeSubagentSnapshot(child.id);\n\t\t}\n\t}\n\n\tprivate resetSubagentSummary(): void {\n\t\tthis.subagentSnapshots.clear();\n\t\tthis.rlmNodeId = undefined;\n\t\tthis.updateSubagentSummaryLine();\n\t\tthis.updateScopedHeartbeats();\n\t\t// Clearing snapshots can drop the last running subagent; reconcile the\n\t\t// pulse and loader so neither lingers when nothing is in flight.\n\t\tthis.updateWorkingPulse();\n\t\tthis.syncWorkingLoader();\n\t}\n\n\tprivate focusEditor(): void {\n\t\tthis.ui.setFocus(this.editor);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate focusSubagentSummary(): boolean {\n\t\tif (!this.subagentSummaryLine.isSelectable() || this.getTrayOverrideLabel()) return false;\n\t\tthis.ui.setFocus(this.subagentSummaryLine);\n\t\tthis.ui.requestRender();\n\t\treturn true;\n\t}\n\n\tprivate async openScopedAgentsView(): Promise<void> {\n\t\tif (!this.options.returnToAgentsView) {\n\t\t\tthis.focusEditor();\n\t\t\tthis.showStatus(\"The agents view needs the daemon; start without --no-daemon to browse sessions\");\n\t\t\treturn;\n\t\t}\n\t\tawait this.returnToAgentsView(\"scoped_agents_view\");\n\t}\n\n\tprivate handleSubagentSummaryChatAction(data: string): void {\n\t\tif (this.keybindings.matches(data, \"app.tools.expand\")) {\n\t\t\tthis.toggleToolOutputExpansion();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"app.messages.expand\")) {\n\t\t\tthis.toggleAgentMessageExpansion();\n\t\t\treturn;\n\t\t}\n\t\t// A raw \"\\n\" is a newline for the editor, not ctrl+j.\n\t\tif (data !== \"\\n\" && this.keybindings.matches(data, \"app.edits.expand\")) {\n\t\t\tthis.toggleEditDiffExpansion();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"app.thinking.toggle\")) {\n\t\t\tthis.toggleThinkingBlockVisibility();\n\t\t\treturn;\n\t\t}\n\t\tthis.focusEditor();\n\t\tthis.editor.handleInput(data);\n\t}\n\n\tprivate getTrayOverrideLabel(): string | undefined {\n\t\tif (this.isCtrlCExitHintVisible()) {\n\t\t\tconst clearKey = keyText(\"app.clear\");\n\t\t\treturn clearKey ? `Press ${clearKey} again to exit` : \"Press again to exit\";\n\t\t}\n\t\tconst text = this.editor.getExpandedText?.() ?? this.editor.getText();\n\t\tif (!this.isAgentStreaming() || !text.trim()) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn `${keyText(\"app.message.followUp\")} to queue message`;\n\t}\n\n\tprivate getTrayLocationLabel(): string | undefined {\n\t\tconst modelLabel = this.getModelTrayLabel();\n\t\tconst hasChildren = this.options.sessionHasChildren === true || (this.subagentSnapshots?.size ?? 0) > 0;\n\t\tconst depthLabel = formatAgentDepthLabel(this.options.sessionDepth, hasChildren);\n\t\tconst shortcutsHint = this.getShortcutsTrayHint();\n\t\tconst agentsHint = this.getAgentsViewTrayHint();\n\t\treturn [agentsHint, depthLabel, modelLabel, shortcutsHint]\n\t\t\t.filter((label): label is string => label !== undefined)\n\t\t\t.join(\" \");\n\t}\n\n\tprivate getShortcutsTrayHint(): string | undefined {\n\t\tif (!this.isNewChat() || this.editor.getText().length > 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn keyText(\"app.shortcuts\") ? keyHint(\"app.shortcuts\", \"for shortcuts\") : \"/hotkeys for shortcuts\";\n\t}\n\n\tprivate isNewChat(): boolean {\n\t\treturn !this.sessionHasMessages;\n\t}\n\n\tprivate setSessionHasMessages(hasMessages: boolean): void {\n\t\tif (this.sessionHasMessages === hasMessages) {\n\t\t\treturn;\n\t\t}\n\t\tthis.sessionHasMessages = hasMessages;\n\t\tthis.builtInHeader?.invalidate();\n\t\tthis.subagentSummaryLine.invalidate();\n\t}\n\n\tprivate getModelTrayLabel(): string {\n\t\tconst model = this.getCurrentModel();\n\t\tif (!model) {\n\t\t\treturn \"—\";\n\t\t}\n\t\tconst parts = [model.name];\n\t\tif (model.reasoning) {\n\t\t\tconst level = this.connectionState?.thinkingLevel ?? \"off\";\n\t\t\tif (level !== \"off\") {\n\t\t\t\tparts.push(level);\n\t\t\t}\n\t\t}\n\t\tif (this.connectionState?.serviceTier === \"priority\") {\n\t\t\tparts.push(\"fast\");\n\t\t}\n\t\treturn parts.join(\" • \");\n\t}\n\n\tprivate getAgentsViewTrayHint(): string | undefined {\n\t\tif (!this.options.returnToAgentsView) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn keyHint(\"app.agents.back\", \"agents/resume\");\n\t}\n\n\tprivate getTrayContextLabel(): string | undefined {\n\t\tconst goalLabel = this.getTrayGoalLabel();\n\t\tconst heartbeatLabel = this.getTrayHeartbeatLabel();\n\t\tconst usage = this.getConnectionContextUsage();\n\t\tconst contextLabel =\n\t\t\tusage && typeof usage.tokens === \"number\" && typeof usage.percent === \"number\"\n\t\t\t\t? `${formatTokenCount(usage.tokens)} (${Math.round(usage.percent)}%)`\n\t\t\t\t: undefined;\n\t\treturn [goalLabel, heartbeatLabel, contextLabel].filter((label) => label !== undefined).join(\" · \") || undefined;\n\t}\n\n\tprivate getTrayHeartbeatLabel(): string | undefined {\n\t\tif (this.heartbeats.length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst paused = this.heartbeats.filter((heartbeat) => heartbeat.job.status === \"paused\").length;\n\t\tconst count = `${this.heartbeats.length} heartbeat${this.heartbeats.length === 1 ? \"\" : \"s\"}`;\n\t\tconst pausedLabel = paused ? ` · ${paused} paused` : \"\";\n\t\tconst shortcut = keyText(\"app.heartbeats.open\");\n\t\treturn `${count}${pausedLabel}${shortcut ? ` (${shortcut})` : \"\"}`;\n\t}\n\n\tprivate getTrayGoalLabel(): string | undefined {\n\t\tconst goal = this.getGoalState();\n\t\tswitch (goal.status) {\n\t\t\tcase \"active\":\n\t\t\t\treturn `Pursuing goal (${this.formatGoalElapsed(goal.timeUsedSeconds)})`;\n\t\t\tcase \"paused\":\n\t\t\t\treturn `Goal paused (${this.formatGoalElapsed(goal.timeUsedSeconds)})`;\n\t\t\tcase \"budget_limited\":\n\t\t\t\treturn `Goal budget limited (${this.formatGoalElapsed(goal.timeUsedSeconds)})`;\n\t\t\tcase \"idle\":\n\t\t\tcase \"complete\":\n\t\t\tcase \"error\":\n\t\t\t\treturn undefined;\n\t\t\tdefault: {\n\t\t\t\tconst _exhaustive: never = goal.status;\n\t\t\t\treturn _exhaustive;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate formatGoalElapsed(seconds: number): string {\n\t\tconst totalSeconds = Math.max(0, Math.trunc(seconds));\n\t\tif (totalSeconds < 60) {\n\t\t\treturn `${totalSeconds}s`;\n\t\t}\n\t\tconst minutes = Math.floor(totalSeconds / 60);\n\t\tconst remainingSeconds = totalSeconds % 60;\n\t\tif (minutes < 60) {\n\t\t\treturn `${minutes}m ${remainingSeconds.toString().padStart(2, \"0\")}s`;\n\t\t}\n\t\tconst hours = Math.floor(minutes / 60);\n\t\tconst remainingMinutes = minutes % 60;\n\t\treturn `${hours}h ${remainingMinutes.toString().padStart(2, \"0\")}m`;\n\t}\n\n\tprivate getUserMessageText(message: Message): string {\n\t\tif (message.role !== \"user\") return \"\";\n\t\tconst textBlocks =\n\t\t\ttypeof message.content === \"string\"\n\t\t\t\t? [{ type: \"text\", text: message.content }]\n\t\t\t\t: message.content.filter((c: { type: string }) => c.type === \"text\");\n\t\treturn textBlocks.map((c) => (c as { text: string }).text).join(\"\");\n\t}\n\n\tprivate createLegacyHeartbeatPromptMessage(\n\t\tmessage: Message,\n\t\ttextContent: string,\n\t): ReturnType<typeof createHeartbeatPromptMessage> | undefined {\n\t\tconst heartbeat = this.connectionState?.heartbeat;\n\t\tif (\n\t\t\tmessage.role !== \"user\" ||\n\t\t\t!heartbeat ||\n\t\t\t!this.isTextOnlyUserMessage(message) ||\n\t\t\ttextContent.trim() !== heartbeat.prompt.trim() ||\n\t\t\t!this.isLikelyHeartbeatPromptTimestamp(heartbeat, message.timestamp)\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn createHeartbeatPromptMessage(heartbeat, message.timestamp);\n\t}\n\n\tprivate isTextOnlyUserMessage(message: Message): boolean {\n\t\tif (message.role !== \"user\") {\n\t\t\treturn false;\n\t\t}\n\t\tif (typeof message.content === \"string\") {\n\t\t\treturn true;\n\t\t}\n\t\treturn message.content.every((content) => content.type === \"text\");\n\t}\n\n\tprivate isLikelyHeartbeatPromptTimestamp(job: AgentCronJob, timestamp: number): boolean {\n\t\tconst directRunTimes = [job.lastRunAt, job.nextRunAt]\n\t\t\t.map((value) => (value ? Date.parse(value) : Number.NaN))\n\t\t\t.filter((value) => Number.isFinite(value));\n\t\tconst tolerance = this.heartbeatLegacyPromptToleranceMs(job);\n\t\tif (directRunTimes.some((runAt) => Math.abs(timestamp - runAt) <= tolerance)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate heartbeatLegacyPromptToleranceMs(job: AgentCronJob): number {\n\t\tconst intervalMs = job.schedule.intervalMs;\n\t\tif (!intervalMs || intervalMs <= 0) {\n\t\t\treturn HEARTBEAT_LEGACY_PROMPT_MAX_TOLERANCE_MS;\n\t\t}\n\t\treturn Math.min(\n\t\t\tHEARTBEAT_LEGACY_PROMPT_MAX_TOLERANCE_MS,\n\t\t\tMath.max(HEARTBEAT_LEGACY_PROMPT_MIN_TOLERANCE_MS, intervalMs / 3),\n\t\t);\n\t}\n\n\t/**\n\t * Show a status message in the chat.\n\t *\n\t * If multiple status messages are emitted back-to-back (without anything else being added to the chat),\n\t * we update the previous status line instead of appending new ones to avoid log spam.\n\t */\n\tprivate showStatus(message: string, tone: \"dim\" | \"warning\" = \"dim\"): void {\n\t\tconst children = this.chatContainer.children;\n\t\tconst last = children.length > 0 ? children[children.length - 1] : undefined;\n\t\tconst secondLast = children.length > 1 ? children[children.length - 2] : undefined;\n\n\t\tif (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {\n\t\t\tthis.lastStatusText.setText(theme.fg(tone, message));\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tconst spacer = new Spacer(1);\n\t\tconst text = new Text(theme.fg(tone, message), 1, 0);\n\t\tthis.chatContainer.addChild(spacer);\n\t\tthis.chatContainer.addChild(text);\n\t\tthis.lastStatusSpacer = spacer;\n\t\tthis.lastStatusText = text;\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async copyFullscreenSelection(text: string): Promise<void> {\n\t\ttry {\n\t\t\tawait copyToClipboard(text);\n\t\t\tthis.showStatus(\"Copied selection to clipboard\");\n\t\t} catch (error) {\n\t\t\tthis.showError(`Failed to copy selection: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n\n\t// Local slash commands (/context, /system-prompt, …) print into the chat\n\t// without round-tripping through the agent, so no user message event echoes\n\t// the typed command. Render the turn ourselves, mirroring the \"user\" case\n\t// above, so the output is anchored to a visible command instead of floating.\n\tprivate echoLocalCommand(text: string): void {\n\t\tif (this.chatContainer.children.length > 0) {\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t}\n\t\tthis.chatContainer.addChild(\n\t\t\tnew UserMessageComponent(text, this.getMarkdownThemeWithSettings(), (name) =>\n\t\t\t\tthis.isRecognizedSlashCommand(name),\n\t\t\t),\n\t\t);\n\t}\n\n\tprivate addMessageToEditorHistory(message: AgentMessage): void {\n\t\tif (message.role !== \"user\") {\n\t\t\treturn;\n\t\t}\n\t\tconst textContent = this.getUserMessageText(message);\n\t\tif (textContent && !this.createLegacyHeartbeatPromptMessage(message, textContent)) {\n\t\t\tthis.editor.addToHistory?.(textContent);\n\t\t}\n\t}\n\n\tprivate createDisplayedCustomMessageComponent(message: CustomMessage): Component {\n\t\tif (isSessionSlashCommandMessage(message)) return new SlashCommandMessageComponent(message.content);\n\t\tif (isSessionSlashCommandResultMessage(message)) return new SlashCommandResultMessageComponent(message);\n\t\tif (\n\t\t\tmessage.customType === SESSION_SLASH_COMMAND_CUSTOM_TYPE ||\n\t\t\tmessage.customType === SESSION_SLASH_COMMAND_RESULT_CUSTOM_TYPE\n\t\t) {\n\t\t\treturn new UserMessageComponent(\"[Malformed session command message]\", this.getMarkdownThemeWithSettings());\n\t\t}\n\t\tif (isCompactionOutcomeMessage(message)) return new CompactionOutcomeMessageComponent(message);\n\t\tif (message.customType === COMPACTION_OUTCOME_CUSTOM_TYPE) {\n\t\t\treturn new MalformedCompactionOutcomeMessageComponent();\n\t\t}\n\t\tif (isRefinementOutcomeMessage(message)) return new RefinementOutcomeMessageComponent(message);\n\t\tif (message.customType === REFINEMENT_OUTCOME_CUSTOM_TYPE) {\n\t\t\treturn new MalformedRefinementOutcomeMessageComponent();\n\t\t}\n\t\tif (isAgentSessionMessage(message)) {\n\t\t\treturn new AgentMessageComponent(message, this.getMarkdownThemeWithSettings(), {\n\t\t\t\tsuppressLeadingSpace: isCompactAgentMessageNeighbor(this.chatContainer.children.at(-1)),\n\t\t\t});\n\t\t}\n\t\tif (isInjectedPromptMessage(message)) {\n\t\t\treturn new InjectedPromptMessageComponent(message, this.getMarkdownThemeWithSettings());\n\t\t}\n\t\treturn new CustomMessageComponent(\n\t\t\tmessage,\n\t\t\tthis.bindLocalSessionExtensions\n\t\t\t\t? this.getLocalSessionHost().getExtensionRunner().getMessageRenderer(message.customType)\n\t\t\t\t: undefined,\n\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t);\n\t}\n\n\tprivate addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void {\n\t\tswitch (message.role) {\n\t\t\tcase \"bashExecution\": {\n\t\t\t\tconst component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext, {\n\t\t\t\t\tsuppressLeadingSpace: this.chatContainer.children.at(-1) instanceof AgentMessageComponent,\n\t\t\t\t});\n\t\t\t\tif (message.output) {\n\t\t\t\t\tcomponent.appendOutput(message.output);\n\t\t\t\t}\n\t\t\t\tcomponent.setComplete(\n\t\t\t\t\tmessage.exitCode,\n\t\t\t\t\tmessage.cancelled,\n\t\t\t\t\tmessage.truncated ? ({ truncated: true } as TruncationResult) : undefined,\n\t\t\t\t\tmessage.fullOutputPath,\n\t\t\t\t);\n\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"custom\": {\n\t\t\t\tif (message.display) {\n\t\t\t\t\tconst component = this.createDisplayedCustomMessageComponent(message);\n\t\t\t\t\tif (isExpandable(component)) {\n\t\t\t\t\t\tcomponent.setExpanded(this.expansionStateFor(component));\n\t\t\t\t\t}\n\t\t\t\t\tif (hasEditDiffsExpansion(component)) {\n\t\t\t\t\t\tcomponent.setEditDiffsExpanded(this.editDiffsExpanded);\n\t\t\t\t\t}\n\t\t\t\t\tif (isSessionSlashCommandMessage(message) && this.chatContainer.children.length > 0) {\n\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t}\n\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"compactionSummary\": {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tconst component = new CompactionSummaryMessageComponent(message, this.getMarkdownThemeWithSettings());\n\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"branchSummary\": {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tconst component = new BranchSummaryMessageComponent(message, this.getMarkdownThemeWithSettings());\n\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"user\": {\n\t\t\t\tconst textContent = this.getUserMessageText(message);\n\t\t\t\tif (textContent) {\n\t\t\t\t\tconst heartbeatMessage = this.createLegacyHeartbeatPromptMessage(message, textContent);\n\t\t\t\t\tif (heartbeatMessage) {\n\t\t\t\t\t\tif (this.chatContainer.children.length > 0) {\n\t\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst component = new InjectedPromptMessageComponent(\n\t\t\t\t\t\t\theartbeatMessage,\n\t\t\t\t\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (this.chatContainer.children.length > 0) {\n\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t}\n\t\t\t\t\tconst skillBlock = parseSkillBlock(textContent);\n\t\t\t\t\tif (skillBlock) {\n\t\t\t\t\t\tconst component = new SkillInvocationMessageComponent(\n\t\t\t\t\t\t\tskillBlock,\n\t\t\t\t\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t\t\tif (skillBlock.userMessage) {\n\t\t\t\t\t\t\tconst userComponent = new UserMessageComponent(\n\t\t\t\t\t\t\t\tskillBlock.userMessage,\n\t\t\t\t\t\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t\t\t\t\t\t\t(name) => this.isRecognizedSlashCommand(name),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tthis.chatContainer.addChild(userComponent);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst userComponent = new UserMessageComponent(\n\t\t\t\t\t\t\ttextContent,\n\t\t\t\t\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t\t\t\t\t\t(name) => this.isRecognizedSlashCommand(name),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.chatContainer.addChild(userComponent);\n\t\t\t\t\t}\n\t\t\t\t\tif (options?.populateHistory) {\n\t\t\t\t\t\tthis.editor.addToHistory?.(textContent);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"assistant\": {\n\t\t\t\tconst assistantComponent = new AssistantMessageComponent(\n\t\t\t\t\tmessage,\n\t\t\t\t\tthis.hideThinkingBlock,\n\t\t\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t\t\t\tthis.hiddenThinkingLabel,\n\t\t\t\t\t{\n\t\t\t\t\t\texpanded: this.toolOutputExpanded,\n\t\t\t\t\t\tprecededByToolActivity:\n\t\t\t\t\t\t\tthis.chatContainer.children.at(-1) instanceof ToolExecutionComponent ||\n\t\t\t\t\t\t\tthis.chatContainer.children.at(-1) instanceof AgentMessageComponent,\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t\tthis.chatContainer.addChild(assistantComponent);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"toolResult\": {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\tconst _exhaustive: never = message;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Render session context to chat. Used for initial load and rebuild after compaction.\n\t * @param sessionContext Session context to render\n\t * @param options.updateFooter Update footer state\n\t * @param options.populateHistory Add user messages to editor history\n\t * @param options.clearChat Clear the current transcript immediately before rendering\n\t * @param options.limitTranscript Limit transcript replay to the recent tail\n\t */\n\tprivate orderMessagesForTranscript(messages: AgentMessage[]): AgentMessage[] {\n\t\tconst summaryIndex = messages.findIndex((message) => message.role === \"compactionSummary\");\n\t\tif (summaryIndex === -1) return messages;\n\t\tconst summary = messages[summaryIndex];\n\t\tif (summary.role !== \"compactionSummary\") return messages;\n\t\tconst remaining = messages.filter((_, index) => index !== summaryIndex);\n\t\tif (Number.isSafeInteger(summary.retainedMessageCount) && summary.retainedMessageCount! >= 0) {\n\t\t\tconst boundary = Math.min(summary.retainedMessageCount!, remaining.length);\n\t\t\treturn [...remaining.slice(0, boundary), summary, ...remaining.slice(boundary)];\n\t\t}\n\n\t\t// Compatibility for summaries created before retainedMessageCount was added.\n\t\tconst retained: AgentMessage[] = [];\n\t\tconst later: AgentMessage[] = [];\n\t\tfor (const message of remaining) {\n\t\t\t(message.timestamp < summary.timestamp ? retained : later).push(message);\n\t\t}\n\t\treturn [...retained, summary, ...later];\n\t}\n\n\tprivate async renderSessionContext(\n\t\tsessionContext: AgentConnectionSessionContext,\n\t\toptions: {\n\t\t\tupdateFooter?: boolean;\n\t\t\tpopulateHistory?: boolean;\n\t\t\tclearChat?: boolean;\n\t\t\tlimitTranscript?: boolean;\n\t\t} = {},\n\t): Promise<void> {\n\t\tthis.resetPendingToolState();\n\t\tconst transcriptMessages = this.orderMessagesForTranscript(sessionContext.messages);\n\t\tconst messagesToRender = options.limitTranscript ? initialRenderMessages(transcriptMessages) : transcriptMessages;\n\t\tthis.ipythonToolComponents.clear();\n\t\tthis.lateIpythonSentAgentMessages.clear();\n\t\tconst renderedPendingTools = new Map<string, ToolExecutionComponent>();\n\t\tconst toolNames: string[] = [];\n\t\tfor (const message of messagesToRender) {\n\t\t\tif (message.role !== \"assistant\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const content of message.content) {\n\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\ttoolNames.push(content.name);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tawait this.preloadToolDefinitions(toolNames);\n\n\t\tif (options.clearChat) {\n\t\t\tthis.chatContainer.clear();\n\t\t}\n\n\t\tif (options.updateFooter) {\n\t\t\tthis.footer.invalidate();\n\t\t\tthis.updateEditorBorderColor();\n\t\t}\n\n\t\tif (options.populateHistory) {\n\t\t\tfor (const message of sessionContext.messages) {\n\t\t\t\tthis.addMessageToEditorHistory(message);\n\t\t\t}\n\t\t}\n\n\t\tconst renderOptions = { ...options, populateHistory: false };\n\n\t\tif (messagesToRender.length < sessionContext.messages.length) {\n\t\t\tthis.chatContainer.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t`Showing latest ${messagesToRender.length} of ${sessionContext.messages.length} messages for faster open.`,\n\t\t\t\t\t),\n\t\t\t\t\t1,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t}\n\n\t\tfor (const message of messagesToRender) {\n\t\t\t// Assistant messages need special handling for tool calls\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tthis.addMessageToChat(message);\n\t\t\t\t// Render tool call components\n\t\t\t\tfor (const content of message.content) {\n\t\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\t\tconst component = new ToolExecutionComponent(\n\t\t\t\t\t\t\tcontent.name,\n\t\t\t\t\t\t\tcontent.id,\n\t\t\t\t\t\t\tcontent.arguments,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tshowImages: this.settingsManager.getShowImages(),\n\t\t\t\t\t\t\t\tincludeImageDimensions: false,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tthis.getCachedToolDefinition(content.name),\n\t\t\t\t\t\t\tthis.ui,\n\t\t\t\t\t\t\tthis.getCurrentCwd(),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\t\t\tcomponent.setAgentMessagesExpanded(this.agentMessagesExpanded);\n\t\t\t\t\t\tcomponent.setEditDiffsExpanded(this.editDiffsExpanded);\n\t\t\t\t\t\tselectLatestToolExpandHint(this.chatContainer.children, component);\n\t\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t\t\tthis.registerIpythonToolComponent(content.name, content.id, component);\n\n\t\t\t\t\t\tif (message.stopReason === \"aborted\" || message.stopReason === \"error\") {\n\t\t\t\t\t\t\tlet errorMessage: string;\n\t\t\t\t\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\t\t\t\t\tconst retryAttempt = this.getRetryAttempt();\n\t\t\t\t\t\t\t\terrorMessage =\n\t\t\t\t\t\t\t\t\tretryAttempt > 0\n\t\t\t\t\t\t\t\t\t\t? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? \"s\" : \"\"}`\n\t\t\t\t\t\t\t\t\t\t: message.errorMessage && message.errorMessage !== \"Request was aborted\"\n\t\t\t\t\t\t\t\t\t\t\t? message.errorMessage\n\t\t\t\t\t\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\terrorMessage = message.errorMessage || \"Error\";\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcomponent.updateResult({ content: [{ type: \"text\", text: errorMessage }], isError: true });\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trenderedPendingTools.set(content.id, component);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (message.role === \"toolResult\") {\n\t\t\t\t// Match tool results to pending tool components\n\t\t\t\tconst component = renderedPendingTools.get(message.toolCallId);\n\t\t\t\tif (component) {\n\t\t\t\t\tcomponent.updateResult(message);\n\t\t\t\t\trenderedPendingTools.delete(message.toolCallId);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// All other messages use standard rendering\n\t\t\t\tthis.addMessageToChat(message, renderOptions);\n\t\t\t}\n\t\t}\n\n\t\tfor (const [toolCallId, component] of renderedPendingTools) {\n\t\t\tcomponent.setIncludeImageDimensions(true);\n\t\t\tthis.pendingTools.set(toolCallId, component);\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\tasync renderInitialMessages(): Promise<void> {\n\t\tconst snapshot = await this.agentConnection.getInitialSnapshot();\n\t\tconst context = this.getSessionContextFromConnectionSnapshot(snapshot);\n\t\tconst state = snapshot.state;\n\t\tconst streamingMessage = snapshot.streamingMessage;\n\t\tthis.rlmNodeId = snapshot.parent?.childId;\n\t\tthis.seedSubagentSummary(snapshot.children);\n\t\tthis.setSessionHasMessages(context.messages.length > 0);\n\t\tthis.applyConnectionStateSnapshot(state);\n\t\tthis.restoreTurnStartFromMessages(context.messages);\n\t\tawait this.renderSessionContext(context, {\n\t\t\tupdateFooter: true,\n\t\t\tpopulateHistory: true,\n\t\t\tlimitTranscript: true,\n\t\t});\n\t\tawait this.restoreStreamingMessageFromSnapshot(streamingMessage);\n\n\t\t// Show compaction info if session was compacted\n\t\tconst compactionCount = state.compactionCount;\n\t\tif (compactionCount > 0) {\n\t\t\tconst times = compactionCount === 1 ? \"1 time\" : `${compactionCount} times`;\n\t\t\tthis.showStatus(`Session compacted ${times}`);\n\t\t}\n\t}\n\n\tprivate async restoreStreamingMessageFromSnapshot(message: AgentMessage | undefined): Promise<void> {\n\t\tif (message?.role === \"assistant\") {\n\t\t\tthis.startAssistantStreamingMessage(message);\n\t\t\tfor (const content of message.content) {\n\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\tthis.startedToolCalls.add(content.id);\n\t\t\t\t\tawait this.getOrCreatePendingToolComponent(content);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate getSessionContextFromConnectionSnapshot(snapshot: AgentConnectionSnapshot): AgentConnectionSessionContext {\n\t\tif (snapshot.sessionContext) {\n\t\t\treturn snapshot.sessionContext;\n\t\t}\n\t\treturn {\n\t\t\tmessages: snapshot.messages,\n\t\t\tthinkingLevel: snapshot.state.thinkingLevel,\n\t\t\tserviceTier: snapshot.state.serviceTier,\n\t\t\tmodel: snapshot.state.model\n\t\t\t\t? { provider: snapshot.state.model.provider, modelId: snapshot.state.model.id }\n\t\t\t\t: null,\n\t\t};\n\t}\n\n\tasync getUserInput(): Promise<string | undefined> {\n\t\tif (this.agentsViewRequest) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.onInputCallback = (text: string | undefined) => {\n\t\t\t\tthis.onInputCallback = undefined;\n\t\t\t\tresolve(text);\n\t\t\t};\n\t\t});\n\t}\n\n\tprivate async rebuildChatFromMessages(): Promise<void> {\n\t\tconst context = await this.agentConnection.getSessionContext();\n\t\tawait this.renderSessionContext(context, { clearChat: true });\n\t}\n\n\tprivate handleEscape(): void {\n\t\tthis.clearCtrlCExitHint();\n\t\tif (this.sideQuestionEvent) {\n\t\t\tthis.clearEscapeRepeat();\n\t\t\tthis.clearSideQuestion({ abort: true });\n\t\t\treturn;\n\t\t}\n\t\tconst action = this.takeEscapeRepeatAction();\n\t\tif (action === \"tree\") {\n\t\t\tvoid this.showTreeSelector();\n\t\t\treturn;\n\t\t}\n\t\tif (action === \"clear\") {\n\t\t\tthis.clearInputBar();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.armEscapeRepeat(this.hasInterruptibleWork() || this.editor.getText().length === 0 ? \"tree\" : \"clear\");\n\t\tthis.interruptOrClearInput();\n\t}\n\n\tprivate armEscapeRepeat(action: \"tree\" | \"clear\"): void {\n\t\tthis.clearEscapeRepeat();\n\t\tthis.escapeRepeatAction = action;\n\t\tthis.escapeRepeatExpiresAt = Date.now() + InteractiveMode.ESCAPE_REPEAT_WINDOW_MS;\n\t\tthis.escapeRepeatTimer = setTimeout(() => {\n\t\t\tthis.clearEscapeRepeat();\n\t\t}, InteractiveMode.ESCAPE_REPEAT_WINDOW_MS);\n\t\tthis.escapeRepeatTimer.unref?.();\n\t}\n\n\tprivate takeEscapeRepeatAction(): \"tree\" | \"clear\" | undefined {\n\t\tif (!this.escapeRepeatAction || this.escapeRepeatExpiresAt <= Date.now()) {\n\t\t\tthis.clearEscapeRepeat();\n\t\t\treturn undefined;\n\t\t}\n\t\tconst action = this.escapeRepeatAction;\n\t\tthis.clearEscapeRepeat();\n\t\treturn action;\n\t}\n\n\tprivate clearEscapeRepeat(): void {\n\t\tif (this.escapeRepeatTimer) {\n\t\t\tclearTimeout(this.escapeRepeatTimer);\n\t\t\tthis.escapeRepeatTimer = undefined;\n\t\t}\n\t\tthis.escapeRepeatAction = undefined;\n\t\tthis.escapeRepeatExpiresAt = 0;\n\t}\n\n\tprivate handleCtrlC(): void {\n\t\tthis.clearEscapeRepeat();\n\t\tif (this.isCtrlCExitHintVisible()) {\n\t\t\tvoid this.shutdown();\n\t\t\treturn;\n\t\t}\n\t\tthis.handleInterruptKey();\n\t}\n\n\tprivate handleInterruptKey(): void {\n\t\tthis.clearEscapeRepeat();\n\t\tthis.interruptOrClearInput();\n\t\tthis.showCtrlCExitHint();\n\t}\n\n\tprivate interruptOrClearInput(): void {\n\t\tthis.traceUploadAllAbortController?.abort(new Error(\"Trace upload cancelled\"));\n\t\tif (this.sideQuestionEvent?.status === \"running\") {\n\t\t\tthis.abortSideQuestion(this.sideQuestionEvent.id, true);\n\t\t}\n\t\tif (this.getRetryAttempt() > 0) {\n\t\t\tvoid this.agentConnection.abortRetry();\n\t\t}\n\t\tif (this.isAgentCompacting()) {\n\t\t\tvoid this.agentConnection.abortCompaction();\n\t\t\tvoid this.agentConnection.abortBranchSummary();\n\t\t}\n\t\tif (this.isBashRunning()) {\n\t\t\tvoid this.agentConnection.abortBash();\n\t\t}\n\t\tif (this.isAgentStreaming()) {\n\t\t\t// The queue is preserved server-side; draining resumes on the next\n\t\t\t// submit or queued-message edit.\n\t\t\tvoid this.agentConnection.abort().catch((error) => {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate showCtrlCExitHint(): void {\n\t\tif (this.ctrlCExitHintTimer) {\n\t\t\tclearTimeout(this.ctrlCExitHintTimer);\n\t\t}\n\t\tthis.ctrlCExitHintExpiresAt = Date.now() + InteractiveMode.EXIT_HINT_DURATION_MS;\n\t\tthis.ctrlCExitHintTimer = setTimeout(() => {\n\t\t\tthis.ctrlCExitHintTimer = undefined;\n\t\t\tif (!this.isCtrlCExitHintVisible()) {\n\t\t\t\tthis.ctrlCExitHintExpiresAt = 0;\n\t\t\t\tthis.subagentSummaryLine.invalidate();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t}\n\t\t}, InteractiveMode.EXIT_HINT_DURATION_MS);\n\t\tthis.ctrlCExitHintTimer.unref?.();\n\t\tthis.subagentSummaryLine.invalidate();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate clearCtrlCExitHint(options: { render?: boolean } = {}): void {\n\t\tif (!this.ctrlCExitHintTimer && this.ctrlCExitHintExpiresAt === 0) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.ctrlCExitHintTimer) {\n\t\t\tclearTimeout(this.ctrlCExitHintTimer);\n\t\t\tthis.ctrlCExitHintTimer = undefined;\n\t\t}\n\t\tthis.ctrlCExitHintExpiresAt = 0;\n\t\tif (options.render !== false) {\n\t\t\tthis.subagentSummaryLine.invalidate();\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate isCtrlCExitHintVisible(): boolean {\n\t\treturn this.ctrlCExitHintExpiresAt > Date.now();\n\t}\n\n\tprivate handleCtrlD(): void {\n\t\tvoid this.shutdown();\n\t}\n\n\t/**\n\t * Gracefully shutdown the agent.\n\t * Stops the TUI before emitting shutdown events so extension UI cleanup cannot\n\t * repaint the final frame while the process is exiting.\n\t */\n\tprivate isShuttingDown = false;\n\n\tprivate async shutdown(): Promise<void> {\n\t\tif (this.isShuttingDown) return;\n\t\tthis.isShuttingDown = true;\n\t\tthis.unregisterSignalHandlers();\n\t\tthis.clearCtrlCExitHint({ render: false });\n\n\t\t// Fetch while the connection is still alive; exit must not fail on a stats error.\n\t\tconst sessionStats = await this.agentConnection.getSessionStats().catch(() => undefined);\n\n\t\t// Drain any in-flight Kitty key release events before stopping.\n\t\t// This prevents escape sequences from leaking to the parent shell over slow SSH.\n\t\tawait this.ui.terminal.drainInput(1000);\n\n\t\tthis.stop();\n\t\ttry {\n\t\t\tawait this.agentConnection.dispose();\n\t\t} finally {\n\t\t\tawait this.options.onShutdown?.();\n\t\t}\n\t\tconst resumeHint = formatResumeHint(sessionStats);\n\t\tif (resumeHint) {\n\t\t\tconsole.log(resumeHint);\n\t\t}\n\t\tprocess.exit(0);\n\t}\n\n\t/**\n\t * Tear down the session's terminal UI before handing the terminal back to the\n\t * agents view. Drains in-flight Kitty/SSH key-release sequences so they don't\n\t * leak into the parent UI, then stops the renderer and theme watcher. Safe to\n\t * call from a crash path too; idempotent via stop().\n\t */\n\tasync teardownSessionUi(options: { preserveAltScreen?: boolean } = {}): Promise<void> {\n\t\tawait this.ui.terminal.drainInput(1000).catch(() => undefined);\n\t\tthis.releasePromptStashSession();\n\t\tthis.stop({ preserveAltScreen: options.preserveAltScreen });\n\t\tstopThemeWatcher();\n\t}\n\n\tprivate handleAgentsBack(): boolean {\n\t\tif (this.editor.getText().trim()) {\n\t\t\treturn false;\n\t\t}\n\t\tif (!this.options.returnToAgentsView) {\n\t\t\tvoid this.requestAgentsView();\n\t\t\treturn true;\n\t\t}\n\t\tvoid this.returnToAgentsView();\n\t\treturn true;\n\t}\n\n\tprivate async requestAgentsView(): Promise<void> {\n\t\tif (!this.options.returnToAgentsView) {\n\t\t\tthis.showStatus(\"The agents view needs the daemon; start without --no-daemon to browse sessions\");\n\t\t\treturn;\n\t\t}\n\t\tawait this.returnToAgentsView();\n\t}\n\n\tprivate async returnToAgentsView(request: InteractiveModeRunResult[\"type\"] = \"agents_view\"): Promise<void> {\n\t\tif (this.isShuttingDown || this.agentsViewRequest) return;\n\t\tthis.stashDraftForAgentsView();\n\t\tthis.agentsViewRequest = request;\n\t\tthis.isShuttingDown = true;\n\t\tthis.unregisterSignalHandlers();\n\n\t\tawait this.teardownSessionUi({ preserveAltScreen: true });\n\t\tlet handoffComplete = false;\n\t\ttry {\n\t\t\ttry {\n\t\t\t\tawait this.agentConnection.dispose();\n\t\t\t} finally {\n\t\t\t\tawait this.options.onShutdown?.();\n\t\t\t\tthis.onInputCallback?.(undefined);\n\t\t\t\thandoffComplete = true;\n\t\t\t}\n\t\t} finally {\n\t\t\tif (!handoffComplete) {\n\t\t\t\tthis.ui.terminal.leaveAltScreen();\n\t\t\t\tthis.ui.terminal.showCursor();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate emergencyTerminalExit(): never {\n\t\tthis.isShuttingDown = true;\n\t\tthis.unregisterSignalHandlers();\n\t\tkillTrackedDetachedChildren();\n\t\t// The terminal is gone. Do not run normal shutdown because TUI and\n\t\t// extension cleanup can write restore sequences and re-trigger EIO.\n\t\tprocess.exit(129);\n\t}\n\n\t/**\n\t * Check if shutdown was requested and perform shutdown if so.\n\t */\n\tprivate async checkShutdownRequested(): Promise<void> {\n\t\tif (!this.shutdownRequested) return;\n\t\tawait this.shutdown();\n\t}\n\n\tprivate registerSignalHandlers(): void {\n\t\tthis.unregisterSignalHandlers();\n\n\t\tconst signals: NodeJS.Signals[] = [\"SIGTERM\"];\n\t\tif (process.platform !== \"win32\") {\n\t\t\tsignals.push(\"SIGHUP\");\n\t\t}\n\n\t\tfor (const signal of signals) {\n\t\t\tconst handler = () => {\n\t\t\t\tif (signal === \"SIGHUP\") {\n\t\t\t\t\tthis.emergencyTerminalExit();\n\t\t\t\t}\n\t\t\t\tkillTrackedDetachedChildren();\n\t\t\t\tvoid this.shutdown();\n\t\t\t};\n\t\t\tprocess.prependListener(signal, handler);\n\t\t\tthis.signalCleanupHandlers.push(() => process.off(signal, handler));\n\t\t}\n\n\t\tconst terminalErrorHandler = (error: Error) => {\n\t\t\tif (isDeadTerminalError(error)) {\n\t\t\t\tthis.emergencyTerminalExit();\n\t\t\t}\n\t\t\tthrow error;\n\t\t};\n\t\tprocess.stdout.on(\"error\", terminalErrorHandler);\n\t\tprocess.stderr.on(\"error\", terminalErrorHandler);\n\t\tthis.signalCleanupHandlers.push(() => process.stdout.off(\"error\", terminalErrorHandler));\n\t\tthis.signalCleanupHandlers.push(() => process.stderr.off(\"error\", terminalErrorHandler));\n\t}\n\n\tprivate unregisterSignalHandlers(): void {\n\t\tfor (const cleanup of this.signalCleanupHandlers) {\n\t\t\tcleanup();\n\t\t}\n\t\tthis.signalCleanupHandlers = [];\n\t}\n\n\tprivate handleCtrlZ(): void {\n\t\tif (process.platform === \"win32\") {\n\t\t\tthis.showStatus(\"Suspend to background is not supported on Windows\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Keep the event loop alive while suspended. Without this, stopping the TUI\n\t\t// can leave Node with no ref'ed handles, causing the process to exit on fg\n\t\t// before the SIGCONT handler gets a chance to restore the terminal.\n\t\tconst suspendKeepAlive = setInterval(() => {}, 2 ** 30);\n\n\t\t// Ignore SIGINT while suspended so Ctrl+C in the terminal does not\n\t\t// kill the backgrounded process. The handler is removed on resume.\n\t\tconst ignoreSigint = () => {};\n\t\tprocess.on(\"SIGINT\", ignoreSigint);\n\n\t\t// Set up handler to restore TUI when resumed\n\t\tprocess.once(\"SIGCONT\", () => {\n\t\t\tclearInterval(suspendKeepAlive);\n\t\t\tprocess.removeListener(\"SIGINT\", ignoreSigint);\n\t\t\tthis.ui.start();\n\t\t\t// ui.stop() left the alt screen before suspending; re-enter it\n\t\t\tif (this.fullscreenEnabled) {\n\t\t\t\tthis.applyFullscreen(true);\n\t\t\t}\n\t\t\tthis.ui.requestRender(true);\n\t\t});\n\n\t\ttry {\n\t\t\t// Stop the TUI (restore terminal to normal mode)\n\t\t\tthis.ui.stop();\n\n\t\t\t// Send SIGTSTP to process group (pid=0 means all processes in group)\n\t\t\tprocess.kill(0, \"SIGTSTP\");\n\t\t} catch (error) {\n\t\t\tclearInterval(suspendKeepAlive);\n\t\t\tprocess.removeListener(\"SIGINT\", ignoreSigint);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprivate async handleFollowUp(): Promise<void> {\n\t\tconst editorText = this.editor.getText();\n\t\tconst text = (this.editor.getExpandedText?.() ?? editorText).trim();\n\t\tif (this.queueSelection?.isBrowsing && !this.pendingQueueEdit) {\n\t\t\tawait this.applyQueueSelection(text, \"followUp\").catch((error) =>\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error)),\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tif (!text || !this.editor.onSubmit) return;\n\n\t\t// Unlike Enter, Alt+Enter does not go through Editor.submitValue(), so\n\t\t// capture and clear synchronously before an async/local handler can yield.\n\t\tthis.pendingSubmittedPromptStash = this.snapshotPromptStash(editorText);\n\t\tthis.editor.setText(\"\");\n\t\tthis.submittedInputBehavior = \"followUp\";\n\t\t// onSubmit consumes the behavior flag and bumps the generation synchronously;\n\t\t// capture the generation so an older failed submit never clobbers newer\n\t\t// typing or submissions.\n\t\tconst submission = this.editor.onSubmit(text);\n\t\tthis.submittedInputBehavior = \"steer\";\n\t\tconst submissionGeneration = this.inputSubmissionGeneration;\n\t\ttry {\n\t\t\tawait submission;\n\t\t} catch (error) {\n\t\t\tif (submissionGeneration === this.inputSubmissionGeneration && this.editor.getText().length === 0) {\n\t\t\t\tthis.editor.setText(text);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprivate browseQueueSelection(direction: -1 | 1): void {\n\t\tif (this.pendingQueueEdit) return;\n\t\tconst text = this.queueSelection.move(this.connectionQueue, this.editor.getText(), direction);\n\t\tif (text === undefined) return;\n\t\tthis.setEditorTextFromQueueSelection(text);\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Serializes queue mutations so rapid keypresses never race each other or use stale indices. */\n\tprivate enqueueQueueMutation<T>(run: () => Promise<T>): Promise<T> {\n\t\tconst next = this.queueMutationChain.then(run, run);\n\t\tthis.queueMutationChain = next.then(\n\t\t\t() => undefined,\n\t\t\t() => undefined,\n\t\t);\n\t\treturn next;\n\t}\n\n\tprivate moveQueueSelection(direction: -1 | 1): void {\n\t\tif (this.pendingQueueEdit) return;\n\t\tconst submittedSelection = this.queueSelection.selected;\n\t\tif (!submittedSelection) return;\n\t\tconst sessionGeneration = this.sessionEventGeneration;\n\t\tvoid this.enqueueQueueMutation(async () => {\n\t\t\tif (sessionGeneration !== this.sessionEventGeneration) return;\n\t\t\tconst lane = this.connectionQueue[submittedSelection.lane];\n\t\t\tconst resolvedIndex =\n\t\t\t\tlane[submittedSelection.index] === submittedSelection.text\n\t\t\t\t\t? submittedSelection.index\n\t\t\t\t\t: lane.indexOf(submittedSelection.text);\n\t\t\tif (resolvedIndex < 0) {\n\t\t\t\tthis.showStatus(\"Queue changed; reorder not applied\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst selected = { ...submittedSelection, index: resolvedIndex };\n\t\t\tconst queueBefore = this.connectionQueue;\n\t\t\tconst status = await this.agentConnection.mutateQueuedMessage(selected.lane, selected.index, selected.text, {\n\t\t\t\ttype: \"move\",\n\t\t\t\tdirection,\n\t\t\t});\n\t\t\tif (sessionGeneration !== this.sessionEventGeneration) return;\n\t\t\tif (status === \"applied\") {\n\t\t\t\t// The queue event for this mutation can land before or after the\n\t\t\t\t// response. Patch the mirror only when no event has replaced it\n\t\t\t\t// meanwhile (events always assign a fresh object); patching an\n\t\t\t\t// already-updated mirror would apply the mutation twice.\n\t\t\t\tconst lane = this.connectionQueue[selected.lane];\n\t\t\t\tconst target = selected.index + direction;\n\t\t\t\tif (\n\t\t\t\t\tthis.connectionQueue === queueBefore &&\n\t\t\t\t\tlane[selected.index] === selected.text &&\n\t\t\t\t\ttarget >= 0 &&\n\t\t\t\t\ttarget < lane.length\n\t\t\t\t) {\n\t\t\t\t\t[lane[selected.index], lane[target]] = [lane[target] as string, selected.text];\n\t\t\t\t\tthis.queueSelection.sync(this.connectionQueue);\n\t\t\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t} else if (status === \"unsupported\") this.showStatus(\"Queue editing requires a newer daemon\");\n\t\t\telse this.showStatus(\"Queue changed; reorder not applied\");\n\t\t}).catch((error) => {\n\t\t\tif (sessionGeneration === this.sessionEventGeneration) {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Applies the edited text to the selected queued message. Returns true when\n\t * the submission was consumed (the caller must not treat it as a new prompt).\n\t * Empty text deletes; otherwise replaces, moving the item to `targetLane`.\n\t */\n\tprivate applyQueueSelection(text: string, targetLane: \"steering\" | \"followUp\"): Promise<boolean> {\n\t\tif (this.pendingQueueEdit) return Promise.resolve(false);\n\t\tconst submittedSelection = this.queueSelection.selected;\n\t\tif (!submittedSelection) return Promise.resolve(false);\n\t\tconst pendingQueueEdit = Symbol(\"pending-queue-edit\");\n\t\tthis.pendingQueueEdit = pendingQueueEdit;\n\t\tconst sessionGeneration = this.sessionEventGeneration;\n\t\tconst submissionGeneration = this.inputSubmissionGeneration;\n\t\tconst trimmed = text.trim();\n\t\tconst mutation =\n\t\t\ttrimmed.length === 0\n\t\t\t\t? ({ type: \"delete\" } as const)\n\t\t\t\t: ({\n\t\t\t\t\t\ttype: \"replace\",\n\t\t\t\t\t\ttext: trimmed,\n\t\t\t\t\t\timages: this.collectQueueReplaceImages(trimmed),\n\t\t\t\t\t\tlane: targetLane,\n\t\t\t\t\t} as const);\n\t\tconst editorTextBefore = this.editor.getText();\n\t\tconst discardStaleSelection = (): boolean => {\n\t\t\tif (sessionGeneration === this.sessionEventGeneration) return false;\n\t\t\tif (this.pendingQueueEdit === pendingQueueEdit) {\n\t\t\t\tthis.pendingQueueEdit = undefined;\n\t\t\t\tthis.queueSelection.reset();\n\t\t\t\tif (submissionGeneration === this.inputSubmissionGeneration && this.editor.getText() === editorTextBefore) {\n\t\t\t\t\tthis.setEditorTextFromQueueSelection(\"\");\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\t\treturn this.enqueueQueueMutation(async () => {\n\t\t\tif (discardStaleSelection()) return true;\n\t\t\t// Earlier serialized moves may have changed the selected item's index.\n\t\t\tconst lane = this.connectionQueue[submittedSelection.lane];\n\t\t\tconst resolvedIndex =\n\t\t\t\tlane[submittedSelection.index] === submittedSelection.text\n\t\t\t\t\t? submittedSelection.index\n\t\t\t\t\t: lane.indexOf(submittedSelection.text);\n\t\t\tif (resolvedIndex < 0) {\n\t\t\t\tthis.queueSelection.sync(this.connectionQueue);\n\t\t\t\tconst editorUntouched =\n\t\t\t\t\tsubmissionGeneration === this.inputSubmissionGeneration && this.editor.getText() === editorTextBefore;\n\t\t\t\tif (editorUntouched) {\n\t\t\t\t\tthis.setEditorTextFromQueueSelection(text);\n\t\t\t\t}\n\t\t\t\tthis.queueSelection.replaceDraft(editorUntouched ? text : this.editor.getText());\n\t\t\t\tthis.showStatus(\"Queue changed; edit kept in the editor\");\n\t\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst selected = { ...submittedSelection, index: resolvedIndex };\n\t\t\tconst queueBefore = this.connectionQueue;\n\t\t\tlet status: AgentConnectionQueuedMessageMutationStatus;\n\t\t\ttry {\n\t\t\t\tstatus = await this.agentConnection.mutateQueuedMessage(\n\t\t\t\t\tselected.lane,\n\t\t\t\t\tselected.index,\n\t\t\t\t\tselected.text,\n\t\t\t\t\tmutation,\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tif (discardStaleSelection()) return true;\n\t\t\t\t// The editor was already cleared by Enter; restore the edit before surfacing the error.\n\t\t\t\tconst editorUntouched =\n\t\t\t\t\tsubmissionGeneration === this.inputSubmissionGeneration && this.editor.getText() === editorTextBefore;\n\t\t\t\tif (editorUntouched) {\n\t\t\t\t\tthis.setEditorTextFromQueueSelection(text);\n\t\t\t\t}\n\t\t\t\tif (!this.queueSelection.isBrowsing) {\n\t\t\t\t\tthis.queueSelection.replaceDraft(editorUntouched ? text : this.editor.getText());\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tif (discardStaleSelection()) return true;\n\t\t\tconst editorUntouched =\n\t\t\t\tsubmissionGeneration === this.inputSubmissionGeneration && this.editor.getText() === editorTextBefore;\n\t\t\tif (status === \"applied\") {\n\t\t\t\t// Same optimistic patch as moveQueueSelection, and the same guard:\n\t\t\t\t// skip when a queue event already replaced the mirror.\n\t\t\t\tconst lane = this.connectionQueue[selected.lane];\n\t\t\t\tif (this.connectionQueue === queueBefore && lane[selected.index] === selected.text) {\n\t\t\t\t\tif (!trimmed) lane.splice(selected.index, 1);\n\t\t\t\t\telse if (targetLane === selected.lane) lane[selected.index] = trimmed;\n\t\t\t\t\telse {\n\t\t\t\t\t\tlane.splice(selected.index, 1);\n\t\t\t\t\t\tthis.connectionQueue[targetLane].push(trimmed);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (trimmed) this.editor.addToHistory?.(trimmed);\n\t\t\t\tconst draft = this.queueSelection.reset();\n\t\t\t\tif (editorUntouched) this.setEditorTextFromQueueSelection(draft);\n\t\t\t} else {\n\t\t\t\tthis.queueSelection.sync(this.connectionQueue);\n\t\t\t\t// Enter submissions clear the editor before onSubmit runs; restore the\n\t\t\t\t// edit so a failed mutation never swallows it.\n\t\t\t\tif (editorUntouched) this.setEditorTextFromQueueSelection(text);\n\t\t\t\tif (!this.queueSelection.isBrowsing) {\n\t\t\t\t\tthis.queueSelection.replaceDraft(editorUntouched ? text : this.editor.getText());\n\t\t\t\t}\n\t\t\t\tif (status === \"invalid\")\n\t\t\t\t\tthis.showStatus(\"Edited command is not a valid session command; edit kept in the editor\");\n\t\t\t\telse if (status === \"unsupported\") this.showStatus(\"Queue editing requires a newer daemon\");\n\t\t\t\telse this.showStatus(\"Queue changed; edit kept in the editor\");\n\t\t\t}\n\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\tthis.ui.requestRender();\n\t\t\treturn true;\n\t\t}).finally(() => {\n\t\t\tif (this.pendingQueueEdit === pendingQueueEdit) this.pendingQueueEdit = undefined;\n\t\t});\n\t}\n\n\t/**\n\t * Images for a queue replace: undefined preserves the server's images (some\n\t * markers cannot be resolved by this client), [] clears, a list replaces.\n\t */\n\tprivate collectQueueReplaceImages(text: string): ImageContent[] | undefined {\n\t\tconst markers = [...new Set(imageMarkerIds(text))];\n\t\tif (markers.length === 0) return [];\n\t\tconst resolved = markers.map((markerId) => this.pastedImages.get(markerId));\n\t\treturn resolved.every((image) => image !== undefined)\n\t\t\t? resolved.map((image) => ({ ...(image as ImageContent) }))\n\t\t\t: undefined;\n\t}\n\n\tprivate setEditorTextFromQueueSelection(text: string): void {\n\t\tthis.isApplyingQueueSelectionText = true;\n\t\ttry {\n\t\t\tthis.editor.setText(text);\n\t\t} finally {\n\t\t\tthis.isApplyingQueueSelectionText = false;\n\t\t}\n\t}\n\n\tprivate getQueueSelectionHeader(): string | undefined {\n\t\tconst selected = this.queueSelection.selected;\n\t\tif (!selected) return undefined;\n\t\tconst lane = selected.lane === \"steering\" ? \"steering\" : \"follow-up\";\n\t\tconst older = this.getAppKeyDisplay(\"app.message.navigateOlder\");\n\t\tconst newer = this.getAppKeyDisplay(\"app.message.navigateNewer\");\n\t\tconst earlier = this.getAppKeyDisplay(\"app.message.moveEarlier\");\n\t\tconst later = this.getAppKeyDisplay(\"app.message.moveLater\");\n\t\tconst queue = this.getAppKeyDisplay(\"app.message.followUp\");\n\t\treturn theme.fg(\n\t\t\t\"dim\",\n\t\t\t`${lane} ${selected.index + 1} · ${older}/${newer} browse · ${earlier}/${later} reorder · enter steers · ${queue} queues · empty deletes`,\n\t\t);\n\t}\n\n\tprivate updateEditorBorderColor(): void {\n\t\tconst editorTheme = getEditorTheme();\n\t\tthis.editor.borderColor = editorTheme.borderColor;\n\t\tthis.editor.backgroundColor = editorTheme.backgroundColor;\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate getPromptContextContainers(): Container[] {\n\t\treturn [this.recapContainer, this.featureHintContainer, this.queuedMessagesContainer, this.sideQuestionContainer];\n\t}\n\n\tprivate getPromptDockComponents(): Component[] {\n\t\treturn [this.editorContainer, this.subagentSummaryLine, this.footerSlot];\n\t}\n\n\t/** Enter or leave fullscreen rendering without touching the persisted setting. */\n\tprivate applyFullscreen(enabled: boolean): void {\n\t\tif (enabled) {\n\t\t\tif (!process.stdout.isTTY) return;\n\t\t\tthis.ui.enterFullscreen({\n\t\t\t\tscroll: [\n\t\t\t\t\tthis.headerContainer,\n\t\t\t\t\tthis.mainViewContainer,\n\t\t\t\t\tthis.widgetContainerAbove,\n\t\t\t\t\t...this.getPromptContextContainers(),\n\t\t\t\t\tthis.widgetContainerBelow,\n\t\t\t\t],\n\t\t\t\tdock: this.promptDock,\n\t\t\t\tmouse: this.settingsManager.getFullscreenMouse(),\n\t\t\t});\n\t\t} else {\n\t\t\tthis.ui.exitFullscreen();\n\t\t}\n\t}\n\n\tprivate setFullscreenMode(enabled: boolean): void {\n\t\tthis.settingsManager.setFullscreen(enabled);\n\t\tif (enabled && !process.stdout.isTTY) {\n\t\t\tthis.fullscreenEnabled = false;\n\t\t\tthis.showStatus(\"Fullscreen rendering requires an interactive terminal\");\n\t\t\treturn;\n\t\t}\n\t\tthis.fullscreenEnabled = enabled;\n\t\tthis.applyFullscreen(enabled);\n\t\tconst followKey = this.getEditorKeyDisplay(\"tui.viewport.follow\");\n\t\tthis.showStatus(\n\t\t\tenabled\n\t\t\t\t? `Fullscreen rendering on — wheel/pageUp scroll, ${followKey} follows output`\n\t\t\t\t: \"Fullscreen rendering off\",\n\t\t);\n\t}\n\n\tprivate toggleToolOutputExpansion(): void {\n\t\tthis.setToolsExpanded(!this.toolOutputExpanded);\n\t}\n\n\tprivate toggleAgentMessageExpansion(): void {\n\t\tthis.agentMessagesExpanded = !this.agentMessagesExpanded;\n\t\tthis.applyChatExpansion();\n\t}\n\n\tprivate toggleEditDiffExpansion(): void {\n\t\tthis.editDiffsExpanded = !this.editDiffsExpanded;\n\t\tthis.applyChatExpansion();\n\t}\n\n\tprivate setToolsExpanded(expanded: boolean): void {\n\t\tthis.toolOutputExpanded = expanded;\n\t\tthis.applyChatExpansion();\n\t}\n\n\t/** Expansion state for a chat component: agent messages toggle separately from tools. */\n\tprivate expansionStateFor(component: unknown): boolean {\n\t\treturn component instanceof AgentMessageComponent ? this.agentMessagesExpanded : this.toolOutputExpanded;\n\t}\n\n\tprivate applyChatExpansion(): void {\n\t\tconst activeHeader = this.customHeader ?? this.builtInHeader;\n\t\tif (isExpandable(activeHeader)) {\n\t\t\tactiveHeader.setExpanded(this.toolOutputExpanded);\n\t\t}\n\t\tfor (const child of this.chatContainer.children) {\n\t\t\tif (isExpandable(child)) {\n\t\t\t\tchild.setExpanded(this.expansionStateFor(child));\n\t\t\t}\n\t\t\tif (hasAgentMessagesExpansion(child)) {\n\t\t\t\tchild.setAgentMessagesExpanded(this.agentMessagesExpanded);\n\t\t\t}\n\t\t\tif (hasEditDiffsExpansion(child)) {\n\t\t\t\tchild.setEditDiffsExpanded(this.editDiffsExpanded);\n\t\t\t}\n\t\t}\n\t\t// Expanding/collapsing changes blocks above the viewport, which would\n\t\t// otherwise force a full redraw that scrolls to the top and replays the\n\t\t// whole transcript. Keep the user anchored at their current position.\n\t\t// Fullscreen frames have no scrollback to preserve.\n\t\tif (this.ui.isFullscreen()) {\n\t\t\tthis.ui.requestRender();\n\t\t} else {\n\t\t\tthis.ui.requestRenderPreservingViewport();\n\t\t}\n\t}\n\n\tprivate toggleThinkingBlockVisibility(): void {\n\t\tthis.hideThinkingBlock = !this.hideThinkingBlock;\n\t\tthis.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);\n\n\t\tvoid (async () => {\n\t\t\t// Rebuild chat from session messages\n\t\t\tawait this.rebuildChatFromMessages();\n\n\t\t\t// If streaming, re-add the streaming component with updated visibility and re-render\n\t\t\tif (this.streamingComponent && this.streamingMessage) {\n\t\t\t\tthis.streamingComponent.setHideThinkingBlock(this.hideThinkingBlock);\n\t\t\t\tthis.streamingComponent.updateContent(this.streamingMessage);\n\t\t\t\tthis.chatContainer.addChild(this.streamingComponent);\n\t\t\t}\n\n\t\t\tthis.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? \"hidden\" : \"visible\"}`);\n\t\t})().catch((error) => {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t});\n\t}\n\n\tprivate openExternalEditor(): void {\n\t\t// Determine editor (respect $VISUAL, then $EDITOR)\n\t\tconst editorCmd = process.env.VISUAL || process.env.EDITOR;\n\t\tif (!editorCmd) {\n\t\t\tthis.showWarning(\"No editor configured. Set $VISUAL or $EDITOR environment variable.\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst currentText = this.editor.getExpandedText?.() ?? this.editor.getText();\n\t\tconst tmpFile = path.join(os.tmpdir(), `pi-editor-${Date.now()}.pi.md`);\n\n\t\ttry {\n\t\t\t// Write current content to temp file\n\t\t\tfs.writeFileSync(tmpFile, currentText, \"utf-8\");\n\n\t\t\t// Stop TUI to release terminal\n\t\t\tthis.ui.stop();\n\n\t\t\t// Split by space to support editor arguments (e.g., \"code --wait\")\n\t\t\tconst [editor, ...editorArgs] = editorCmd.split(\" \");\n\n\t\t\t// Spawn editor synchronously with inherited stdio for interactive editing\n\t\t\tconst result = spawnSync(editor, [...editorArgs, tmpFile], {\n\t\t\t\tstdio: \"inherit\",\n\t\t\t\tshell: process.platform === \"win32\",\n\t\t\t});\n\n\t\t\t// On successful exit (status 0), replace editor content\n\t\t\tif (result.status === 0) {\n\t\t\t\tconst newContent = fs.readFileSync(tmpFile, \"utf-8\").replace(/\\n$/, \"\");\n\t\t\t\tthis.editor.setText(newContent);\n\t\t\t}\n\t\t\t// On non-zero exit, keep original text (no action needed)\n\t\t} finally {\n\t\t\t// Clean up temp file\n\t\t\ttry {\n\t\t\t\tfs.unlinkSync(tmpFile);\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\n\t\t\t// Restart TUI\n\t\t\tthis.ui.start();\n\t\t\t// ui.stop() left fullscreen so the editor got a clean terminal\n\t\t\tif (this.fullscreenEnabled) {\n\t\t\t\tthis.applyFullscreen(true);\n\t\t\t}\n\t\t\t// Force full re-render since external editor uses alternate screen\n\t\t\tthis.ui.requestRender(true);\n\t\t}\n\t}\n\n\tprivate clearInputBar(): void {\n\t\tthis.clearEscapeRepeat();\n\t\tthis.clearCtrlCExitHint({ render: false });\n\t\t// Leaving browse mode restores the stashed draft instead of arming an\n\t\t// accidental empty-submit delete of the selected queued message.\n\t\tthis.editor.setText(this.queueSelection.hasDraft ? this.queueSelection.reset() : \"\");\n\t\tthis.ui.requestRender();\n\t}\n\n\tshowError(errorMessage: string): void {\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(theme.fg(\"error\", `Error: ${errorMessage}`), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tshowWarning(warningMessage: string): void {\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(theme.fg(\"warning\", `⚠ ${warningMessage}`), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tshowNewVersionNotification(newVersion: string): void {\n\t\tthis.chatContainer.addChild(new Text(formatUpdateAvailableNotice(newVersion), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tshowPackageUpdateNotification(packages: string[]): void {\n\t\tthis.chatContainer.addChild(new Text(formatPackageUpdateNotice(packages), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate getAllQueuedMessages(): { steering: string[]; followUp: string[] } {\n\t\treturn {\n\t\t\tsteering: [...this.connectionQueue.steering],\n\t\t\tfollowUp: [...this.connectionQueue.followUp],\n\t\t};\n\t}\n\n\tprivate updatePendingMessagesDisplay(): void {\n\t\t// pendingMessagesContainer holds only in-flight bash output for the current\n\t\t// turn, so it stays above the execution indicator. clear() detaches the\n\t\t// components but they stay tracked in pendingBashComponents until flushed.\n\t\tthis.pendingMessagesContainer.clear();\n\t\tfor (const component of this.pendingBashComponents) {\n\t\t\tthis.pendingMessagesContainer.addChild(component);\n\t\t}\n\t\t// Queued steering/follow-up previews are future turns, so they render in\n\t\t// their own container below the execution indicator and recap.\n\t\tthis.queuedMessagesContainer.clear();\n\t\tconst { steering: steeringMessages, followUp: followUpMessages } = this.getAllQueuedMessages();\n\t\tconst hasQueuedMessages = steeringMessages.length > 0 || followUpMessages.length > 0;\n\t\tif (hasQueuedMessages) {\n\t\t\tthis.queuedMessagesContainer.addChild(new Spacer(1));\n\t\t\tfor (const message of steeringMessages) {\n\t\t\t\tconst text = styleQueuedMessagePreview(message, \"Steering\", (name) => this.isRecognizedSlashCommand(name));\n\t\t\t\tthis.queuedMessagesContainer.addChild(new TruncatedText(text, 1, 0));\n\t\t\t}\n\t\t\tfor (const message of followUpMessages) {\n\t\t\t\tconst text = styleQueuedMessagePreview(message, \"Follow-up\", (name) => this.isRecognizedSlashCommand(name));\n\t\t\t\tthis.queuedMessagesContainer.addChild(new TruncatedText(text, 1, 0));\n\t\t\t}\n\t\t\tconst dequeueHint = this.getAppKeyDisplay(\"app.message.navigateOlder\");\n\t\t\tconst hintText = theme.fg(\"dim\", `╰─ ${dequeueHint} to browse and edit queued messages`);\n\t\t\tthis.queuedMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));\n\t\t}\n\t\tif (hasQueuedMessages && !this.featureHintSuppressedByQueue) {\n\t\t\tthis.featureHintSuppressedByQueue = true;\n\t\t\tthis.clearFeatureHintPresentation();\n\t\t} else if (!hasQueuedMessages && this.featureHintSuppressedByQueue) {\n\t\t\tthis.featureHintSuppressedByQueue = false;\n\t\t\tthis.resumeFeatureHintPresentation();\n\t\t}\n\t}\n\n\tprivate flushPendingBashComponents(): void {\n\t\tfor (const component of this.pendingBashComponents) {\n\t\t\tthis.pendingMessagesContainer.removeChild(component);\n\t\t\tthis.chatContainer.addChild(component);\n\t\t}\n\t\tthis.pendingBashComponents = [];\n\t}\n\n\t/**\n\t * Shows a selector component in place of the editor.\n\t * @param create Factory that receives a `done` callback and returns the component and focus target\n\t */\n\tprivate showSelector(create: (done: () => void) => { component: Component; focus: Component }): void {\n\t\tconst done = () => {\n\t\t\tthis.editorContainer.clear();\n\t\t\tthis.editorContainer.addChild(this.editor);\n\t\t\tthis.ui.setFocus(this.editor);\n\t\t};\n\t\tconst { component, focus } = create(done);\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(component);\n\t\tthis.ui.setFocus(focus);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate showFullPaneOverlay(component: Component, options: number | FullPaneOverlayOptions = 80): OverlayHandle {\n\t\treturn showFullPaneOverlay(this.ui, component, options);\n\t}\n\n\tprivate async showSettingsSelector(): Promise<void> {\n\t\tlet state: AgentConnectionState;\n\t\ttry {\n\t\t\tstate = await this.agentConnection.getState();\n\t\t\tthis.applyConnectionStateSnapshot(state);\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\treturn;\n\t\t}\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new SettingsSelectorComponent(\n\t\t\t\t{\n\t\t\t\t\tautoCompact: state.autoCompactionEnabled,\n\t\t\t\t\tidleEvictionMinutes: this.settingsManager.getIdleEvictionMinutes(),\n\t\t\t\t\tshowImages: this.settingsManager.getShowImages(),\n\t\t\t\t\tautoResizeImages: this.settingsManager.getImageAutoResize(),\n\t\t\t\t\tblockImages: this.settingsManager.getBlockImages(),\n\t\t\t\t\tenableSkillCommands: this.settingsManager.getEnableSkillCommands(),\n\t\t\t\t\tenableBuiltinSkills: this.settingsManager.getEnableBuiltinSkills(),\n\t\t\t\t\tsteeringMode: state.steeringMode,\n\t\t\t\t\tfollowUpMode: state.followUpMode,\n\t\t\t\t\ttransport: this.settingsManager.getTransport(),\n\t\t\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t\t\t\tavailableThinkingLevels: state.availableThinkingLevels,\n\t\t\t\t\tcurrentTheme: this.settingsManager.getTheme() || \"asm\",\n\t\t\t\t\tavailableThemes: getAvailableThemes(),\n\t\t\t\t\thideThinkingBlock: this.hideThinkingBlock,\n\t\t\t\t\ttreeFilterMode: this.settingsManager.getTreeFilterMode(),\n\t\t\t\t\tshowHardwareCursor: this.settingsManager.getShowHardwareCursor(),\n\t\t\t\t\teditorPaddingX: this.settingsManager.getEditorPaddingX(),\n\t\t\t\t\tautocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),\n\t\t\t\t\tquietStartup: this.settingsManager.getQuietStartup(),\n\t\t\t\t\tclearOnShrink: this.settingsManager.getClearOnShrink(),\n\t\t\t\t\tshowTerminalProgress: this.settingsManager.getShowTerminalProgress(),\n\t\t\t\t\tfullscreen: this.fullscreenEnabled,\n\t\t\t\t\twarnings: this.settingsManager.getWarnings(),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tonAutoCompactChange: (enabled) => {\n\t\t\t\t\t\tthis.patchConnectionState({ autoCompactionEnabled: enabled });\n\t\t\t\t\t\tvoid this.agentConnection.setAutoCompactionEnabled(enabled).catch((error) => {\n\t\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\t});\n\t\t\t\t\t\tthis.footer.setAutoCompactEnabled(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonIdleEvictionMinutesChange: (value) => {\n\t\t\t\t\t\tthis.settingsManager.setIdleEvictionMinutes(value);\n\t\t\t\t\t},\n\t\t\t\t\tonShowImagesChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setShowImages(enabled);\n\t\t\t\t\t\tfor (const child of this.chatContainer.children) {\n\t\t\t\t\t\t\tif (child instanceof ToolExecutionComponent) {\n\t\t\t\t\t\t\t\tchild.setShowImages(enabled);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonAutoResizeImagesChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setImageAutoResize(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonBlockImagesChange: (blocked) => {\n\t\t\t\t\t\tthis.settingsManager.setBlockImages(blocked);\n\t\t\t\t\t},\n\t\t\t\t\tonEnableSkillCommandsChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setEnableSkillCommands(enabled);\n\t\t\t\t\t\tthis.setupAutocompleteProvider();\n\t\t\t\t\t},\n\t\t\t\t\tonEnableBuiltinSkillsChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setEnableBuiltinSkills(enabled);\n\t\t\t\t\t\tvoid this.handleReloadCommand();\n\t\t\t\t\t},\n\t\t\t\t\tonSteeringModeChange: (mode) => {\n\t\t\t\t\t\tthis.patchConnectionState({ steeringMode: mode });\n\t\t\t\t\t\tvoid this.agentConnection.setSteeringMode(mode).catch((error) => {\n\t\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tonFollowUpModeChange: (mode) => {\n\t\t\t\t\t\tthis.patchConnectionState({ followUpMode: mode });\n\t\t\t\t\t\tvoid this.agentConnection.setFollowUpMode(mode).catch((error) => {\n\t\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tonTransportChange: (transport) => {\n\t\t\t\t\t\tvoid this.agentConnection.setTransport(transport).catch((error) => {\n\t\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tonThinkingLevelChange: (level) => {\n\t\t\t\t\t\tvoid this.agentConnection\n\t\t\t\t\t\t\t.setThinkingLevel(level)\n\t\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\t\tthis.patchConnectionState({ thinkingLevel: level });\n\t\t\t\t\t\t\t\tthis.footer.invalidate();\n\t\t\t\t\t\t\t\tthis.updateEditorBorderColor();\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t.catch((error) => {\n\t\t\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tonThemeChange: (themeName) => {\n\t\t\t\t\t\tconst result = setTheme(themeName, true);\n\t\t\t\t\t\tthis.settingsManager.setTheme(themeName);\n\t\t\t\t\t\tthis.ui.invalidate();\n\t\t\t\t\t\tif (!result.success) {\n\t\t\t\t\t\t\tthis.showError(`Failed to load theme \"${themeName}\": ${result.error}\\nFell back to dark theme.`);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonThemePreview: (themeName) => {\n\t\t\t\t\t\tconst result = setTheme(themeName, true);\n\t\t\t\t\t\tif (result.success) {\n\t\t\t\t\t\t\tthis.ui.invalidate();\n\t\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonHideThinkingBlockChange: (hidden) => {\n\t\t\t\t\t\tthis.hideThinkingBlock = hidden;\n\t\t\t\t\t\tthis.settingsManager.setHideThinkingBlock(hidden);\n\t\t\t\t\t\tfor (const child of this.chatContainer.children) {\n\t\t\t\t\t\t\tif (child instanceof AssistantMessageComponent) {\n\t\t\t\t\t\t\t\tchild.setHideThinkingBlock(hidden);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvoid this.rebuildChatFromMessages().catch((error) => {\n\t\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tonQuietStartupChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setQuietStartup(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonTreeFilterModeChange: (mode) => {\n\t\t\t\t\t\tthis.settingsManager.setTreeFilterMode(mode);\n\t\t\t\t\t},\n\t\t\t\t\tonShowHardwareCursorChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setShowHardwareCursor(enabled);\n\t\t\t\t\t\tthis.ui.setShowHardwareCursor(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonEditorPaddingXChange: (padding) => {\n\t\t\t\t\t\tthis.settingsManager.setEditorPaddingX(padding);\n\t\t\t\t\t\tthis.defaultEditor.setPaddingX(padding);\n\t\t\t\t\t\tif (this.editor !== this.defaultEditor && this.editor.setPaddingX !== undefined) {\n\t\t\t\t\t\t\tthis.editor.setPaddingX(padding);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonAutocompleteMaxVisibleChange: (maxVisible) => {\n\t\t\t\t\t\tthis.settingsManager.setAutocompleteMaxVisible(maxVisible);\n\t\t\t\t\t\tthis.defaultEditor.setAutocompleteMaxVisible(maxVisible);\n\t\t\t\t\t\tif (this.editor !== this.defaultEditor && this.editor.setAutocompleteMaxVisible !== undefined) {\n\t\t\t\t\t\t\tthis.editor.setAutocompleteMaxVisible(maxVisible);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonClearOnShrinkChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setClearOnShrink(enabled);\n\t\t\t\t\t\tthis.ui.setClearOnShrink(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonShowTerminalProgressChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setShowTerminalProgress(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonFullscreenChange: (enabled) => {\n\t\t\t\t\t\tthis.setFullscreenMode(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonWarningsChange: (warnings) => {\n\t\t\t\t\t\tthis.settingsManager.setWarnings(warnings);\n\t\t\t\t\t},\n\t\t\t\t\tonCancel: () => {\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector.getSettingsList() };\n\t\t});\n\t}\n\n\tprivate async handleModelCommand(searchTerm?: string): Promise<void> {\n\t\tif (!searchTerm) {\n\t\t\tthis.showModelSelector();\n\t\t\treturn;\n\t\t}\n\n\t\tconst model = await this.findExactModelMatch(searchTerm);\n\t\tif (model) {\n\t\t\ttry {\n\t\t\t\tconst authFlows = this.createAuthFlows();\n\t\t\t\tconst providerOptions = authFlows.getLoginProviderOptions();\n\t\t\t\tif (!(await this.ensureModelProviderConfigured(model, authFlows, providerOptions))) return;\n\t\t\t\tawait this.completeModelSelection(model);\n\t\t\t} catch (error) {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tthis.showModelSelector(searchTerm);\n\t}\n\n\tprivate async findExactModelMatch(searchTerm: string): Promise<Model<Api> | undefined> {\n\t\tconst cachedMatch = findExactModelReferenceMatch(searchTerm, this.getCachedModelCandidates());\n\t\tif (cachedMatch) {\n\t\t\treturn cachedMatch;\n\t\t}\n\n\t\tconst refreshPromise = this.getModelSelectorRefreshPromise({ force: true });\n\t\tif (!refreshPromise) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\ttry {\n\t\t\treturn findExactModelReferenceMatch(searchTerm, await refreshPromise);\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async applySelectedModel(model: AgentConnectionModel): Promise<void> {\n\t\tconst connection = this.agentConnection;\n\t\tconst sessionId = this.connectionState?.sessionId;\n\t\tawait connection.setModel(model.provider, model.id);\n\t\tconst state = await connection.getState();\n\t\tif (\n\t\t\tthis.agentConnection !== connection ||\n\t\t\tthis.connectionState?.sessionId !== sessionId ||\n\t\t\t(sessionId !== undefined && state.sessionId !== sessionId)\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tthis.settingsManager.setDefaultModelAndProvider(model.provider, model.id);\n\t\tthis.patchConnectionState({\n\t\t\tmodel: state.model ?? model,\n\t\t\tserviceTier: state.serviceTier,\n\t\t\tavailableThinkingLevels: state.availableThinkingLevels,\n\t\t});\n\t\tthis.footer.invalidate();\n\t\tthis.subagentSummaryLine.invalidate();\n\t\tthis.updateEditorBorderColor();\n\t\t// Rebuild so the /effort argument hint reflects the new model's levels.\n\t\tthis.setupAutocompleteProvider();\n\t}\n\n\tprivate async completeModelSelection(model: AgentConnectionModel): Promise<void> {\n\t\tthis.showStatus(`Switching model: ${model.id}`);\n\t\tawait this.applySelectedModel(model);\n\t\tthis.showStatus(`Model: ${model.id}`);\n\t\tvoid this.maybeWarnAboutAnthropicSubscriptionAuth(model);\n\t\tthis.checkDaxnutsEasterEgg(model);\n\t}\n\n\tprivate async ensureModelProviderConfigured(\n\t\tmodel: AgentConnectionModel,\n\t\tauthFlows: ProviderAuthFlows,\n\t\tproviderOptions: ReadonlyArray<AuthSelectorProvider>,\n\t): Promise<boolean> {\n\t\tif (this.isModelProviderConfigured(model)) return true;\n\n\t\tconst provider = providerOptions.find(\n\t\t\t(option) => option.id === model.provider && (option.category ?? \"provider\") === \"provider\",\n\t\t);\n\t\tif (!provider) {\n\t\t\tthis.showError(`Authentication for ${model.provider} must be configured externally.`);\n\t\t\treturn false;\n\t\t}\n\n\t\tconst result = await authFlows.loginProvider(provider);\n\t\tif (result.status !== \"success\") return false;\n\n\t\tthis.invalidateConnectionModels();\n\t\tawait this.getConnectionAvailableModels();\n\t\tif (this.isModelProviderConfigured(model)) return true;\n\n\t\tthis.showError(`Authentication completed, but ${model.provider} is still unavailable.`);\n\t\treturn false;\n\t}\n\n\tprivate isModelProviderConfigured(model: AgentConnectionModel): boolean {\n\t\treturn this.connectionConfiguredProviders.has(model.provider) || this.modelRegistry.hasConfiguredAuth(model);\n\t}\n\n\tprivate applyConnectionModelCatalog(catalog: AgentConnectionModelCatalog): void {\n\t\tthis.connectionModelCatalog = [...catalog.models];\n\t\tthis.connectionConfiguredProviders = new Set(catalog.configuredProviders);\n\t\tthis.connectionModels = catalog.models.filter((model) => this.connectionConfiguredProviders.has(model.provider));\n\t}\n\n\tprivate async getConnectionAvailableModels(): Promise<AgentConnectionModel[]> {\n\t\tconst inFlight = this.connectionModelsRefreshInFlight;\n\t\tif (inFlight && inFlight.version === this.connectionModelsRefreshVersion) {\n\t\t\treturn [...(await inFlight.promise)];\n\t\t}\n\n\t\tconst version = this.connectionModelsRefreshVersion;\n\t\tconst promise = this.agentConnection.getModelCatalog().then((catalog) => {\n\t\t\tif (version !== this.connectionModelsRefreshVersion) {\n\t\t\t\treturn [...this.connectionModels];\n\t\t\t}\n\t\t\tthis.applyConnectionModelCatalog(catalog);\n\t\t\tthis.connectionModelsFetchedAt = Date.now();\n\t\t\treturn [...this.connectionModels];\n\t\t});\n\t\tthis.connectionModelsRefreshInFlight = { version, promise };\n\n\t\ttry {\n\t\t\treturn [...(await promise)];\n\t\t} finally {\n\t\t\tif (this.connectionModelsRefreshInFlight?.promise === promise) {\n\t\t\t\tthis.connectionModelsRefreshInFlight = undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async getConnectionModelCatalog(): Promise<AgentConnectionModel[]> {\n\t\tawait this.getConnectionAvailableModels();\n\t\treturn [...this.connectionModelCatalog];\n\t}\n\n\tprivate getCachedModelCandidates(): AgentConnectionModel[] {\n\t\tconst modelsById = new Map<string, AgentConnectionModel>();\n\t\tfor (const scoped of this.getScopedModelState()) {\n\t\t\tmodelsById.set(`${scoped.model.provider}/${scoped.model.id}`, scoped.model);\n\t\t}\n\t\tfor (const model of this.connectionModelCatalog) {\n\t\t\tmodelsById.set(`${model.provider}/${model.id}`, model);\n\t\t}\n\t\treturn [...modelsById.values()];\n\t}\n\n\tprivate getModelSelectorRefreshPromise(\n\t\toptions: { force?: boolean } = {},\n\t): Promise<AgentConnectionModel[]> | undefined {\n\t\tconst refreshCatalog = () => this.getConnectionAvailableModels().then(() => this.getCachedModelCandidates());\n\t\tif (this.connectionModelsRefreshInFlight) {\n\t\t\treturn refreshCatalog();\n\t\t}\n\t\tif (options.force || this.connectionModelsFetchedAt === 0) {\n\t\t\treturn refreshCatalog();\n\t\t}\n\t\tif (Date.now() - this.connectionModelsFetchedAt > MODEL_CATALOG_REFRESH_TTL_MS) {\n\t\t\treturn refreshCatalog();\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate invalidateConnectionModelRefresh(): void {\n\t\tthis.connectionModelsRefreshVersion++;\n\t\tthis.connectionModelsRefreshInFlight = undefined;\n\t}\n\n\tprivate invalidateConnectionModels(): void {\n\t\tthis.connectionModels = [];\n\t\tthis.connectionConfiguredProviders = new Set();\n\t\tthis.connectionModelsFetchedAt = 0;\n\t\tthis.invalidateConnectionModelRefresh();\n\t}\n\n\tprivate async refreshConnectionModelsAfterAuthChange(): Promise<void> {\n\t\tthis.invalidateConnectionModels();\n\t\tawait this.getConnectionAvailableModels();\n\t}\n\n\tprivate async getModelCandidates(): Promise<AgentConnectionModel[]> {\n\t\tconst scopedModels = this.getScopedModelState();\n\t\tif (scopedModels.length > 0) {\n\t\t\treturn scopedModels.map((scoped) => scoped.model);\n\t\t}\n\n\t\ttry {\n\t\t\treturn await this.getConnectionAvailableModels();\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\n\tprivate getScopedModelsFromModelIds(\n\t\tenabledIds: readonly string[],\n\t\tallModels: readonly AgentConnectionModel[],\n\t): AgentConnectionState[\"scopedModels\"] {\n\t\tconst modelsById = new Map(allModels.map((model) => [`${model.provider}/${model.id}`, model]));\n\t\tconst selectedIds = new Set<string>();\n\t\tconst scopedModels: AgentConnectionState[\"scopedModels\"] = [];\n\n\t\tfor (const id of enabledIds) {\n\t\t\tif (selectedIds.has(id)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst model = modelsById.get(id);\n\t\t\tif (!model) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tselectedIds.add(id);\n\t\t\tscopedModels.push({ model });\n\t\t}\n\n\t\treturn scopedModels;\n\t}\n\n\tprivate async updateAvailableProviderCount(): Promise<void> {\n\t\tconst models = await this.getModelCandidates();\n\t\tconst uniqueProviders = new Set(models.map((m) => m.provider));\n\t\tthis.footerDataProvider.setAvailableProviderCount(uniqueProviders.size);\n\t}\n\n\tprivate async maybeWarnAboutAnthropicSubscriptionAuth(\n\t\tmodel: Model<any> | undefined = this.getCurrentModel(),\n\t): Promise<void> {\n\t\tif (this.settingsManager.getWarnings().anthropicExtraUsage === false) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.anthropicSubscriptionWarningShown) {\n\t\t\treturn;\n\t\t}\n\t\tconst warning = await getAnthropicSubscriptionAuthWarning(this.modelRegistry, model);\n\t\tif (!warning) {\n\t\t\treturn;\n\t\t}\n\t\tthis.anthropicSubscriptionWarningShown = true;\n\t\tthis.showWarning(warning);\n\t}\n\n\tprivate getAvailableThinkingLevels(): ThinkingLevel[] {\n\t\tconst levels = this.connectionState?.availableThinkingLevels ?? [];\n\t\tconst supportsThinking = levels.length > 0 && !(levels.length === 1 && levels[0] === \"off\");\n\t\treturn supportsThinking ? levels : [];\n\t}\n\n\tprivate getThinkingLevelCompletions(prefix: string): AutocompleteItem[] | null {\n\t\tconst levels = this.getAvailableThinkingLevels();\n\t\tif (levels.length === 0) return null;\n\t\tconst current = this.connectionState?.thinkingLevel;\n\t\tconst term = prefix.trim().toLowerCase();\n\t\tconst matches = term ? levels.filter((level) => level.startsWith(term)) : levels;\n\t\tif (matches.length === 0) return null;\n\t\treturn matches.map((level) => ({\n\t\t\tvalue: level,\n\t\t\tlabel: level,\n\t\t\tdescription:\n\t\t\t\tlevel === current ? `${THINKING_LEVEL_DESCRIPTIONS[level]} (current)` : THINKING_LEVEL_DESCRIPTIONS[level],\n\t\t}));\n\t}\n\n\tprivate getHeartbeatArgumentCompletions(prefix: string): AutocompleteItem[] | null {\n\t\tconst term = prefix.trim().toLowerCase();\n\t\tconst filtered = term\n\t\t\t? HEARTBEAT_ARGUMENT_COMPLETIONS.filter(\n\t\t\t\t\t(item) => item.value.toLowerCase().startsWith(term) || item.label.toLowerCase().startsWith(term),\n\t\t\t\t)\n\t\t\t: HEARTBEAT_ARGUMENT_COMPLETIONS;\n\t\treturn filtered.length === 0 ? null : filtered;\n\t}\n\n\tprivate currentModelSupportsFastMode(): boolean {\n\t\tconst model = this.getCurrentModel();\n\t\treturn model !== undefined && supportsFastMode(model);\n\t}\n\n\tprivate handleFastCommand(): void {\n\t\tconst unavailableMessage =\n\t\t\t\"Fast mode requires GPT-5.4, GPT-5.5, or GPT-5.6 with ChatGPT or OpenAI API key authentication\";\n\t\tif (!this.currentModelSupportsFastMode()) {\n\t\t\tthis.showStatus(unavailableMessage);\n\t\t\treturn;\n\t\t}\n\t\tconst connection = this.agentConnection;\n\t\tconst sessionId = this.connectionState?.sessionId;\n\t\tthis.fastModeToggleQueue = this.fastModeToggleQueue\n\t\t\t.then(async () => {\n\t\t\t\tif (this.agentConnection !== connection || this.connectionState?.sessionId !== sessionId) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!this.currentModelSupportsFastMode()) {\n\t\t\t\t\tthis.showStatus(unavailableMessage);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst enabled = this.connectionState?.serviceTier === \"priority\";\n\t\t\t\tconst serviceTier: ServiceTier = enabled ? \"default\" : \"priority\";\n\t\t\t\tawait connection.setServiceTier(serviceTier);\n\t\t\t\tif (this.agentConnection !== connection || this.connectionState?.sessionId !== sessionId) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst state = await connection.getState();\n\t\t\t\tif (\n\t\t\t\t\tthis.agentConnection !== connection ||\n\t\t\t\t\tthis.connectionState?.sessionId !== sessionId ||\n\t\t\t\t\tstate.sessionId !== sessionId\n\t\t\t\t) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.patchConnectionState({ serviceTier: state.serviceTier });\n\t\t\t\tthis.footer.invalidate();\n\t\t\t\tthis.subagentSummaryLine.invalidate();\n\t\t\t\tthis.showStatus(`Fast mode: ${state.serviceTier === \"priority\" ? \"on\" : \"off\"}`);\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t});\n\t}\n\n\tprivate handleEffortCommand(arg: string): void {\n\t\tconst levels = this.getAvailableThinkingLevels();\n\t\tif (levels.length === 0) {\n\t\t\tthis.showStatus(\"Current model does not support thinking\");\n\t\t\treturn;\n\t\t}\n\t\tconst requested = arg.trim().toLowerCase();\n\t\tif (!requested) {\n\t\t\tthis.showThinkingSelector(levels);\n\t\t\treturn;\n\t\t}\n\t\tif (!levels.includes(requested as ThinkingLevel)) {\n\t\t\tthis.showError(`Unknown thinking level '${requested}'. Available: ${levels.join(\", \")}`);\n\t\t\treturn;\n\t\t}\n\t\tthis.applyThinkingLevel(requested as ThinkingLevel);\n\t}\n\n\tprivate showThinkingSelector(levels: ThinkingLevel[] = this.getAvailableThinkingLevels()): void {\n\t\tconst currentLevel = this.connectionState?.thinkingLevel ?? levels[0];\n\t\tif (!currentLevel) {\n\t\t\tthis.showStatus(\"Current model does not support thinking\");\n\t\t\treturn;\n\t\t}\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new ThinkingSelectorComponent(\n\t\t\t\tcurrentLevel,\n\t\t\t\tlevels,\n\t\t\t\t(level) => {\n\t\t\t\t\tdone();\n\t\t\t\t\tthis.applyThinkingLevel(level);\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tdone();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector.getSelectList() };\n\t\t});\n\t}\n\n\tprivate applyThinkingLevel(level: ThinkingLevel): void {\n\t\tvoid this.agentConnection\n\t\t\t.setThinkingLevel(level)\n\t\t\t.then(() => {\n\t\t\t\tthis.patchConnectionState({ thinkingLevel: level });\n\t\t\t\tthis.footer.invalidate();\n\t\t\t\tthis.updateEditorBorderColor();\n\t\t\t\tthis.showStatus(`Thinking level: ${level}`);\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t});\n\t}\n\n\tprivate showModelSelector(initialSearchInput?: string): void {\n\t\tvoid this.showConfigurationMenu(\"models\", initialSearchInput);\n\t}\n\n\tprivate showConfigurationMenu(initialTab: ConfigurationMenuTab, initialModelSearch?: string): Promise<void> {\n\t\tconst modelCatalog = this.getCachedModelCandidates();\n\t\tconst authFlows = this.createAuthFlows();\n\t\tconst providerOptions = authFlows.getLoginProviderOptions();\n\n\t\treturn new Promise((resolve) => {\n\t\t\tlet handle: OverlayHandle | undefined;\n\t\t\tlet settled = false;\n\t\t\tlet hidden = false;\n\t\t\tlet removed = false;\n\t\t\tlet menu: ConfigurationMenuComponent;\n\t\t\tconst hide = () => {\n\t\t\t\tif (removed) return;\n\t\t\t\tremoved = true;\n\t\t\t\thidden = true;\n\t\t\t\thandle?.hide();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t};\n\t\t\tconst conceal = () => {\n\t\t\t\tif (hidden || removed) return;\n\t\t\t\thidden = true;\n\t\t\t\thandle?.setHidden(true);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t};\n\t\t\tconst show = () => {\n\t\t\t\tif (!hidden || removed || settled) return;\n\t\t\t\thidden = false;\n\t\t\t\thandle?.setHidden(false);\n\t\t\t\thandle?.focus();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t};\n\t\t\tconst finish = () => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\thide();\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\tconst refreshModels = (force: boolean) => {\n\t\t\t\tconst refreshPromise = this.getModelSelectorRefreshPromise({ force });\n\t\t\t\tif (!refreshPromise) return;\n\t\t\t\tvoid refreshPromise\n\t\t\t\t\t.then((models) => {\n\t\t\t\t\t\tif (!settled) menu.updateModels(this.getCurrentModel(), models, this.connectionConfiguredProviders);\n\t\t\t\t\t})\n\t\t\t\t\t.catch((error) => {\n\t\t\t\t\t\tif (!settled) this.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t});\n\t\t\t};\n\t\t\tconst authenticate = (provider: AuthSelectorProvider, tab: \"providers\" | \"mcp-connections\") => {\n\t\t\t\tif (settled) return;\n\t\t\t\tvoid authFlows\n\t\t\t\t\t.loginProvider(provider)\n\t\t\t\t\t.then(async (authResult) => {\n\t\t\t\t\t\tif (settled) return;\n\t\t\t\t\t\thandle?.focus();\n\t\t\t\t\t\tmenu.refreshAuthentication();\n\t\t\t\t\t\tif (authResult.status !== \"success\") return;\n\n\t\t\t\t\t\tif (tab === \"mcp-connections\") {\n\t\t\t\t\t\t\tif (!authResult.providerId.startsWith(\"mcp:\")) return;\n\t\t\t\t\t\t\tif (this.isAgentStreaming() || this.isAgentCompacting()) {\n\t\t\t\t\t\t\t\tthis.showStatus(\"Connected. Run /reload (after the current turn) to activate the integration.\");\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfinish();\n\t\t\t\t\t\t\tawait this.handleReloadCommand();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tawait this.prepareForModelSelectionAfterLogin(authResult);\n\t\t\t\t\t\tmenu.updateModels(\n\t\t\t\t\t\t\tthis.getCurrentModel(),\n\t\t\t\t\t\t\tthis.getCachedModelCandidates(),\n\t\t\t\t\t\t\tthis.connectionConfiguredProviders,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tmenu.setActiveTab(\"models\");\n\t\t\t\t\t\trefreshModels(true);\n\t\t\t\t\t})\n\t\t\t\t\t.catch((error) => {\n\t\t\t\t\t\thandle?.focus();\n\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t});\n\t\t\t};\n\n\t\t\tmenu = new ConfigurationMenuComponent({\n\t\t\t\tinitialTab,\n\t\t\t\ttui: this.ui,\n\t\t\t\tauthStorage: this.modelRegistry.authStorage,\n\t\t\t\tproviderOptions,\n\t\t\t\tmodelRegistry: this.modelRegistry,\n\t\t\t\tcurrentModel: this.getCurrentModel(),\n\t\t\t\tscopedModels: this.getScopedModelState(),\n\t\t\t\tavailableModels: modelCatalog,\n\t\t\t\tconfiguredProviders: this.connectionConfiguredProviders,\n\t\t\t\trecentModels: this.settingsManager.getRecentModels(),\n\t\t\t\tinitialModelSearch,\n\t\t\t\tgetRows: () => this.ui.terminal.rows,\n\t\t\t\trequestRender: () => this.ui.requestRender(),\n\t\t\t\tonSelectProvider: (provider) => authenticate(provider, \"providers\"),\n\t\t\t\tonSelectMcpConnection: (provider) => authenticate(provider, \"mcp-connections\"),\n\t\t\t\tonSelectModel: (model) => {\n\t\t\t\t\tvoid (async () => {\n\t\t\t\t\t\tlet completed = false;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst ready = await this.ensureModelProviderConfigured(model, authFlows, providerOptions);\n\t\t\t\t\t\t\thandle?.focus();\n\t\t\t\t\t\t\tmenu.refreshAuthentication();\n\t\t\t\t\t\t\tmenu.updateModels(\n\t\t\t\t\t\t\t\tthis.getCurrentModel(),\n\t\t\t\t\t\t\t\tthis.getCachedModelCandidates(),\n\t\t\t\t\t\t\t\tthis.connectionConfiguredProviders,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tif (!ready || settled) return;\n\t\t\t\t\t\t\tconceal();\n\t\t\t\t\t\t\tawait this.completeModelSelection(model);\n\t\t\t\t\t\t\tcompleted = true;\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tshow();\n\t\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\t} finally {\n\t\t\t\t\t\t\tif (completed) finish();\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t\tonCancel: finish,\n\t\t\t});\n\t\t\thandle = this.showFullPaneOverlay(menu, 96);\n\t\t\trefreshModels(initialModelSearch !== undefined);\n\t\t});\n\t}\n\n\tprivate async showModelsSelector(): Promise<void> {\n\t\tlet allModels: AgentConnectionModel[];\n\t\ttry {\n\t\t\tallModels = await this.getConnectionModelCatalog();\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\treturn;\n\t\t}\n\n\t\tif (allModels.length === 0) {\n\t\t\tthis.showStatus(\"No models available\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if session has scoped models (from previous session-only changes or CLI --models)\n\t\tconst sessionScopedModels = this.getScopedModelState();\n\t\tconst hasSessionScope = sessionScopedModels.length > 0;\n\n\t\t// Build enabled model IDs from session state or settings\n\t\tlet currentEnabledIds: string[] | null = null;\n\n\t\tif (hasSessionScope) {\n\t\t\t// Use current session's scoped models\n\t\t\tcurrentEnabledIds = sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);\n\t\t} else {\n\t\t\t// Fall back to settings\n\t\t\tconst patterns = this.settingsManager.getEnabledModels();\n\t\t\tif (patterns !== undefined && patterns.length > 0) {\n\t\t\t\tconst scopedModels = resolveModelScopeFromModels(patterns, allModels);\n\t\t\t\tcurrentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);\n\t\t\t}\n\t\t}\n\n\t\t// Helper to update session's scoped models (session-only, no persist)\n\t\tconst updateSessionModels = async (enabledIds: string[] | null) => {\n\t\t\tcurrentEnabledIds = enabledIds === null ? null : [...enabledIds];\n\t\t\tif (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {\n\t\t\t\tconst scopedModels = this.getScopedModelsFromModelIds(enabledIds, allModels);\n\t\t\t\tawait this.agentConnection.setScopedModels(scopedModels);\n\t\t\t\tthis.patchConnectionState({ scopedModels });\n\t\t\t} else {\n\t\t\t\t// All enabled or none enabled = no filter\n\t\t\t\tawait this.agentConnection.setScopedModels([]);\n\t\t\t\tthis.patchConnectionState({ scopedModels: [] });\n\t\t\t}\n\t\t\tawait this.updateAvailableProviderCount();\n\t\t\tthis.ui.requestRender();\n\t\t};\n\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new ScopedModelsSelectorComponent(\n\t\t\t\t{\n\t\t\t\t\tallModels,\n\t\t\t\t\tenabledModelIds: currentEnabledIds,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tonChange: async (enabledIds) => {\n\t\t\t\t\t\tawait updateSessionModels(enabledIds);\n\t\t\t\t\t},\n\t\t\t\t\tonPersist: (enabledIds) => {\n\t\t\t\t\t\t// Persist to settings\n\t\t\t\t\t\tconst newPatterns =\n\t\t\t\t\t\t\tenabledIds === null || enabledIds.length === allModels.length\n\t\t\t\t\t\t\t\t? undefined // All enabled = clear filter\n\t\t\t\t\t\t\t\t: enabledIds;\n\t\t\t\t\t\tthis.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);\n\t\t\t\t\t\tthis.showStatus(\"Model selection saved to settings\");\n\t\t\t\t\t},\n\t\t\t\t\tonCancel: () => {\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector };\n\t\t});\n\t}\n\n\tprivate async showUserMessageSelector(): Promise<void> {\n\t\tlet userMessages: Array<{ entryId: string; text: string }>;\n\t\ttry {\n\t\t\tuserMessages = await this.agentConnection.getUserMessagesForForking();\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\treturn;\n\t\t}\n\n\t\tif (userMessages.length === 0) {\n\t\t\tthis.showStatus(\"No messages to fork from\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst initialSelectedId = userMessages[userMessages.length - 1]?.entryId;\n\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new UserMessageSelectorComponent(\n\t\t\t\tuserMessages.map((m) => ({ id: m.entryId, text: m.text })),\n\t\t\t\tasync (entryId) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = await this.agentConnection.fork(entryId);\n\t\t\t\t\t\tif (result.cancelled) {\n\t\t\t\t\t\t\tdone();\n\t\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tawait this.renderCurrentSessionState();\n\t\t\t\t\t\tthis.editor.setText(result.selectedText ?? \"\");\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.showStatus(\"Forked to new session\");\n\t\t\t\t\t} catch (error: unknown) {\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tdone();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t},\n\t\t\t\tinitialSelectedId,\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector.getMessageList() };\n\t\t});\n\t}\n\n\tprivate async handleCloneCommand(): Promise<void> {\n\t\ttry {\n\t\t\tconst { leafId } = await this.agentConnection.getSessionTree();\n\t\t\tif (!leafId) {\n\t\t\t\tthis.showStatus(\"Nothing to clone yet\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst result = await this.agentConnection.fork(leafId, { position: \"at\" });\n\t\t\tif (result.cancelled) {\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tawait this.renderCurrentSessionState();\n\t\t\tthis.editor.setText(\"\");\n\t\t\tthis.showStatus(\"Cloned to new session\");\n\t\t} catch (error: unknown) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate async showTreeSelector(initialSelectedId?: string): Promise<void> {\n\t\tlet tree: AgentConnectionSessionTreeNode[];\n\t\tlet realLeafId: string | null;\n\t\ttry {\n\t\t\tconst sessionTree = await this.agentConnection.getSessionTree();\n\t\t\ttree = sessionTree.tree;\n\t\t\trealLeafId = sessionTree.leafId;\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\treturn;\n\t\t}\n\t\tconst initialFilterMode = this.settingsManager.getTreeFilterMode();\n\n\t\tif (tree.length === 0) {\n\t\t\tthis.showStatus(\"No entries in session\");\n\t\t\treturn;\n\t\t}\n\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new TreeSelectorComponent(\n\t\t\t\ttree,\n\t\t\t\trealLeafId,\n\t\t\t\tthis.ui.terminal.rows,\n\t\t\t\tasync (entryId) => {\n\t\t\t\t\t// Selecting the current leaf is a no-op (already there)\n\t\t\t\t\tif (entryId === realLeafId) {\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.showStatus(\"Already at this point\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Ask about summarization\n\t\t\t\t\tdone(); // Close selector first\n\n\t\t\t\t\t// Loop until user makes a complete choice or cancels to tree\n\t\t\t\t\tlet wantsSummary = false;\n\t\t\t\t\tlet customInstructions: string | undefined;\n\n\t\t\t\t\t// Check if we should skip the prompt (user preference to always default to no summary)\n\t\t\t\t\tif (!this.settingsManager.getBranchSummarySkipPrompt()) {\n\t\t\t\t\t\twhile (true) {\n\t\t\t\t\t\t\tconst summaryChoice = await this.showExtensionSelector(\"Summarize branch?\", [\n\t\t\t\t\t\t\t\t\"No summary\",\n\t\t\t\t\t\t\t\t\"Summarize\",\n\t\t\t\t\t\t\t\t\"Summarize with custom prompt\",\n\t\t\t\t\t\t\t]);\n\n\t\t\t\t\t\t\tif (summaryChoice === undefined) {\n\t\t\t\t\t\t\t\t// User pressed escape - re-show tree selector with same selection\n\t\t\t\t\t\t\t\tvoid this.showTreeSelector(entryId);\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\twantsSummary = summaryChoice !== \"No summary\";\n\n\t\t\t\t\t\t\tif (summaryChoice === \"Summarize with custom prompt\") {\n\t\t\t\t\t\t\t\tcustomInstructions = await this.showExtensionEditor(\"Custom summarization instructions\");\n\t\t\t\t\t\t\t\tif (customInstructions === undefined) {\n\t\t\t\t\t\t\t\t\t// User cancelled - loop back to summary selector\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// User made a complete choice\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Set up escape handler and loader if summarizing\n\t\t\t\t\tlet summaryLoader: Loader | undefined;\n\t\t\t\t\tif (wantsSummary) {\n\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t\tsummaryLoader = new Loader(\n\t\t\t\t\t\t\tthis.ui,\n\t\t\t\t\t\t\t(spinner) => theme.fg(\"muted\", spinner),\n\t\t\t\t\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\t\t\t\t\t`Summarizing branch... (${keyText(\"app.clear\")} to cancel)`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.statusContainer.addChild(summaryLoader);\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t}\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = await this.agentConnection.navigateTree(entryId, {\n\t\t\t\t\t\t\tsummarize: wantsSummary,\n\t\t\t\t\t\t\tcustomInstructions,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tif (result.aborted) {\n\t\t\t\t\t\t\t// Summarization aborted - re-show tree selector with same selection\n\t\t\t\t\t\t\tthis.showStatus(\"Branch summarization cancelled\");\n\t\t\t\t\t\t\tvoid this.showTreeSelector(entryId);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (result.cancelled) {\n\t\t\t\t\t\t\tthis.showStatus(\"Navigation cancelled\");\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tawait this.renderTreeNavigation(result);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif (summaryLoader) {\n\t\t\t\t\t\t\tsummaryLoader.stop();\n\t\t\t\t\t\t\tthis.statusContainer.clear();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tdone();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t},\n\t\t\t\t(entryId, label) => {\n\t\t\t\t\tvoid this.agentConnection\n\t\t\t\t\t\t.setSessionEntryLabel(entryId, label)\n\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch((error) => {\n\t\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\tinitialSelectedId,\n\t\t\t\tinitialFilterMode,\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector };\n\t\t});\n\t}\n\n\tprivate async handleResumeCommand(args: string): Promise<void> {\n\t\tconst selector = args.trim();\n\t\tif (!selector) {\n\t\t\tawait this.requestAgentsView();\n\t\t\treturn;\n\t\t}\n\t\tlet sessionPath: string;\n\t\ttry {\n\t\t\tsessionPath = (await resolveSessionPath(selector, this.getCurrentCwd(), this.connectionState?.sessionDir))\n\t\t\t\t.path;\n\t\t} catch (error) {\n\t\t\tif (error instanceof SessionSelectorError) {\n\t\t\t\tconst suggestion =\n\t\t\t\t\terror instanceof SessionSelectorNotFoundError && error.suggestion\n\t\t\t\t\t\t? ` Did you mean '${error.suggestion}'?`\n\t\t\t\t\t\t: \"\";\n\t\t\t\tthis.showError(`${error.message}.${suggestion}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tawait this.handleResumeSession(sessionPath);\n\t}\n\n\tprivate async handleResumeSession(\n\t\tsessionPath: string,\n\t\toptions?: Parameters<ExtensionCommandContext[\"switchSession\"]>[1],\n\t): Promise<{ cancelled: boolean }> {\n\t\tthis.stopWorkingLoader();\n\t\ttry {\n\t\t\tconst result = options?.withSession\n\t\t\t\t? await this.getLocalSessionHost().switchSession(sessionPath, {\n\t\t\t\t\t\twithSession: options.withSession,\n\t\t\t\t\t})\n\t\t\t\t: await this.agentConnection.switchSession(sessionPath);\n\t\t\tif (result.cancelled) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tawait this.renderCurrentSessionState();\n\t\t\tthis.showStatus(\"Resumed session\");\n\t\t\treturn result;\n\t\t} catch (error: unknown) {\n\t\t\tif (error instanceof MissingSessionCwdError) {\n\t\t\t\tconst selectedCwd = await this.promptForMissingSessionCwd(error);\n\t\t\t\tif (!selectedCwd) {\n\t\t\t\t\tthis.showStatus(\"Resume cancelled\");\n\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t}\n\t\t\t\tconst result = options?.withSession\n\t\t\t\t\t? await this.getLocalSessionHost().switchSession(sessionPath, {\n\t\t\t\t\t\t\tcwdOverride: selectedCwd,\n\t\t\t\t\t\t\twithSession: options.withSession,\n\t\t\t\t\t\t})\n\t\t\t\t\t: await this.agentConnection.switchSession(sessionPath, { cwdOverride: selectedCwd });\n\t\t\t\tif (result.cancelled) {\n\t\t\t\t\treturn result;\n\t\t\t\t}\n\t\t\t\tawait this.renderCurrentSessionState();\n\t\t\t\tthis.showStatus(\"Resumed session in current cwd\");\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\treturn this.handleFatalRuntimeError(\"Failed to resume session\", error);\n\t\t}\n\t}\n\n\tprivate showOnboardingSplash(continueActionLabel?: string): Promise<OnboardingSplashHandle | undefined> {\n\t\treturn new Promise((resolve) => {\n\t\t\tlet settled = false;\n\t\t\tlet dismissed = false;\n\t\t\tlet handle: OverlayHandle | undefined;\n\t\t\tlet selector: OnboardingSplashComponent | undefined;\n\t\t\tconst settle = (result: OnboardingSplashHandle | undefined) => {\n\t\t\t\tif (settled) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsettled = true;\n\t\t\t\tresolve(result);\n\t\t\t};\n\t\t\tconst dismiss = () => {\n\t\t\t\tif (dismissed) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tdismissed = true;\n\t\t\t\tselector?.dispose();\n\t\t\t\thandle?.hide();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t};\n\t\t\tselector = new OnboardingSplashComponent(\n\t\t\t\t() => {\n\t\t\t\t\tselector?.dispose();\n\t\t\t\t\tsettle({\n\t\t\t\t\t\tshowProgress: (message) => selector?.showProgress(message),\n\t\t\t\t\t\tdismiss,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tdismiss();\n\t\t\t\t\tsettle(undefined);\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tgetRows: () => this.ui.terminal.rows,\n\t\t\t\t\trequestRender: () => this.ui.requestRender(),\n\t\t\t\t\t...(continueActionLabel ? { continueActionLabel } : {}),\n\t\t\t\t},\n\t\t\t);\n\t\t\thandle = this.ui.showOverlay(selector, {\n\t\t\t\twidth: \"100%\",\n\t\t\t\tmaxHeight: \"100%\",\n\t\t\t\trow: 0,\n\t\t\t\tcol: 0,\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate createAuthFlows(): ProviderAuthFlows {\n\t\treturn new ProviderAuthFlows({\n\t\t\tui: this.ui,\n\t\t\tmodelRegistry: this.modelRegistry,\n\t\t\tshowStatus: (message) => this.showStatus(message),\n\t\t\tshowError: (message) => this.showError(message),\n\t\t\tgetAvailableModels: () => this.getConnectionAvailableModels(),\n\t\t\tonAuthChanged: async () => {\n\t\t\t\tawait this.refreshConnectionModelsAfterAuthChange();\n\t\t\t\tawait this.updateAvailableProviderCount();\n\t\t\t\tthis.footer.invalidate();\n\t\t\t\tthis.updateEditorBorderColor();\n\t\t\t},\n\t\t\tonLoginCompleted: () => {\n\t\t\t\tvoid this.maybeWarnAboutAnthropicSubscriptionAuth();\n\t\t\t},\n\t\t});\n\t}\n\n\tprivate async prepareForModelSelectionAfterLogin(authResult: AuthenticationResult): Promise<boolean> {\n\t\tconst currentModel = this.getCurrentModel();\n\t\t// The agent core uses unknown/unknown as its no-model sentinel.\n\t\tconst selectedModel =\n\t\t\tcurrentModel?.provider === \"unknown\" && currentModel.id === \"unknown\" ? undefined : currentModel;\n\t\tlet action = resolvePrimeInferencePostLoginModelAction(authResult, selectedModel, this.modelRegistry);\n\t\tif (!action.openModelPicker) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (!selectedModel) {\n\t\t\ttry {\n\t\t\t\tconst availableModels = await this.getConnectionAvailableModels();\n\t\t\t\taction = resolvePrimeInferencePostLoginModelAction(authResult, selectedModel, {\n\t\t\t\t\tfind: (provider, modelId) =>\n\t\t\t\t\t\tavailableModels.find((model) => model.provider === provider && model.id === modelId) ??\n\t\t\t\t\t\tthis.modelRegistry.find(provider, modelId),\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\t// Preserve the registry fallback so selection can still report a specific failure below.\n\t\t\t}\n\t\t}\n\n\t\tif (action.fallbackModel) {\n\t\t\ttry {\n\t\t\t\tawait this.applySelectedModel(action.fallbackModel);\n\t\t\t\tawait this.settingsManager.flush();\n\t\t\t} catch (error) {\n\t\t\t\tthis.showError(\n\t\t\t\t\t`Prime Inference login succeeded, but the default model could not be selected: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t} else if (!selectedModel) {\n\t\t\tthis.showError(\"Prime Inference login succeeded, but the default GLM 5.2 model is unavailable.\");\n\t\t}\n\n\t\treturn true;\n\t}\n\n\tprivate async handleMcpCommand(args: string | undefined): Promise<void> {\n\t\tconst argv = parseCommandArgs((args ?? \"\").trim());\n\t\tconst [sub, server] = argv;\n\t\tif (!sub) {\n\t\t\tawait this.showConfigurationMenu(\"mcp-connections\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst authStorage = this.modelRegistry.authStorage;\n\t\tconst isAuthed = (name: string) => authStorage.get(`mcp:${name}`) !== undefined;\n\t\tif (sub === \"login\") {\n\t\t\tif (!server || argv.length !== 2) {\n\t\t\t\tthis.showError(\"Usage: /mcp login <name> (e.g. /mcp login linear)\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst result = await this.createAuthFlows().runMcpLogin(server);\n\t\t\tif (result.status === \"success\") await this.reloadAfterMcpChange(`Connected ${server}.`);\n\t\t\treturn;\n\t\t}\n\n\t\tif (sub === \"logout\") {\n\t\t\tif (!server || argv.length !== 2) {\n\t\t\t\tthis.showError(\"Usage: /mcp logout <name>\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!isAuthed(server)) {\n\t\t\t\tthis.showStatus(`${server} is not connected.`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauthStorage.logout(`mcp:${server}`);\n\t\t\tawait this.reloadAfterMcpChange(`Disconnected ${server}.`);\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await runMcpManagementCommand(argv, this.settingsManager, this.modelRegistry.authStorage);\n\t\t\tif (result.changed && result.serverChange) {\n\t\t\t\tconst { name, transport, verb, usesOAuth } = result.serverChange;\n\t\t\t\tconst hasMcpProviderRefresh = this.uiServices.refreshMcpProviders !== undefined;\n\t\t\t\tthis.uiServices.refreshMcpProviders?.();\n\t\t\t\tconst successMessage =\n\t\t\t\t\tverb === \"removed\"\n\t\t\t\t\t\t? `Removed MCP server \"${name}\" (${transport}). It is no longer available through mcp.`\n\t\t\t\t\t\t: usesOAuth\n\t\t\t\t\t\t\t? `${verb === \"replaced\" ? \"Replaced\" : \"Added\"} MCP server \"${name}\" (${transport}). ${hasMcpProviderRefresh ? \"Run\" : \"Restart ASM Agent, then run\"} /mcp login ${name} to connect.`\n\t\t\t\t\t\t\t: `${verb === \"replaced\" ? \"Replaced\" : \"Added\"} MCP server \"${name}\" (${transport}). Available next turn through mcp.`;\n\t\t\t\tawait this.reloadAfterMcpChange(usesOAuth ? successMessage : result.message, successMessage);\n\t\t\t} else if (result.action === \"list\") {\n\t\t\t\tconst builtins = BUILTIN_MCP_CATALOG.map(\n\t\t\t\t\t(entry) => `${entry.label} (${entry.server}): ${isAuthed(entry.server) ? \"connected\" : \"not connected\"}`,\n\t\t\t\t).join(\"\\n\");\n\t\t\t\tthis.showStatus(\n\t\t\t\t\t`Built-in MCP integrations:\\n${builtins}\\n\\nUser-configured MCP servers:\\n${result.message}`,\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tthis.showStatus(result.message);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate async reloadAfterMcpChange(message: string, successMessage = message): Promise<void> {\n\t\tif (this.isAgentStreaming() || this.isAgentCompacting()) {\n\t\t\tthis.showStatus(`${message} The change was saved. Run /reload after the current turn to activate it.`);\n\t\t\treturn;\n\t\t}\n\t\tconst reloaded = await this.handleReloadCommand();\n\t\tif (reloaded) {\n\t\t\tthis.showStatus(successMessage);\n\t\t} else {\n\t\t\tthis.showWarning(`${message} The change remains saved, but it is not active in this session.`);\n\t\t}\n\t}\n\n\tprivate async showLogoutSelector(): Promise<void> {\n\t\t// Only reload when an MCP integration was actually removed (its skill must\n\t\t// be disabled); a cancelled or non-MCP logout needs no reload.\n\t\tconst loggedOut = await this.createAuthFlows().runLogout();\n\t\tif (loggedOut?.startsWith(\"mcp:\")) {\n\t\t\tawait this.handleReloadCommand();\n\t\t}\n\t}\n\n\tprivate async handleUpdateCommand(args: string): Promise<void> {\n\t\tconst entrypoint = process.argv[1];\n\t\tif (!entrypoint) {\n\t\t\tthis.showError(\"Cannot determine current CLI entrypoint for update\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst updateArgs = parseCommandArgs(args);\n\t\tconst includesSelf = updateArgsIncludeSelf(updateArgs);\n\t\tconst updateCwd = this.getCurrentCwd();\n\t\tconst daemonSocketPath = resolveInteractiveUpdateDaemonSocketPath(\n\t\t\tupdateArgs,\n\t\t\tresolveDaemonUpdateRestartSocketPath(this.options.daemonSocketPath),\n\t\t);\n\t\tconst updateChildArgs = includesSelf ? buildUpdateChildArgs(updateArgs, daemonSocketPath) : updateArgs;\n\t\tthis.stopWorkingLoader();\n\t\tawait this.ui.terminal.drainInput(1000).catch(() => undefined);\n\t\tthis.ui.stop();\n\n\t\tconst updateEnv = includesSelf ? { ...process.env, [SELF_UPDATE_INTERACTIVE_CHILD_ENV]: \"1\" } : process.env;\n\t\tconst updateResult = spawnSync(\n\t\t\tprocess.execPath,\n\t\t\t[...process.execArgv, entrypoint, \"update\", ...updateChildArgs],\n\t\t\t{\n\t\t\t\tstdio: \"inherit\",\n\t\t\t\tcwd: updateCwd,\n\t\t\t\tenv: updateEnv,\n\t\t\t},\n\t\t);\n\t\tconst updateExitCode = updateResult.status ?? (updateResult.signal ? 1 : 0);\n\t\tconst selfUpdateNotAttempted =\n\t\t\tincludesSelf && !updateResult.error && updateExitCode === SELF_UPDATE_NOT_ATTEMPTED_EXIT_CODE;\n\n\t\tif (includesSelf && !selfUpdateNotAttempted) {\n\t\t\tconst relaunchArgs = buildUpdateRelaunchArgs(process.argv.slice(2), this.connectionState?.sessionFile);\n\t\t\tif (updateResult.error) {\n\t\t\t\tconsole.error(`Update failed: ${updateResult.error.message}`);\n\t\t\t\tconsole.error(`Relaunching ${APP_NAME}...`);\n\t\t\t} else if (updateExitCode !== 0) {\n\t\t\t\tconsole.error(\n\t\t\t\t\tupdateResult.signal\n\t\t\t\t\t\t? `Update terminated by signal ${updateResult.signal}`\n\t\t\t\t\t\t: `Update exited with code ${updateExitCode}`,\n\t\t\t\t);\n\t\t\t\tconsole.error(`Relaunching ${APP_NAME}...`);\n\t\t\t}\n\t\t\tthis.stop();\n\t\t\tawait this.agentConnection.dispose().catch(() => undefined);\n\t\t\ttry {\n\t\t\t\tawait this.options.onShutdown?.();\n\t\t\t} catch {\n\t\t\t\t// The update already completed; do not block relaunch on local teardown.\n\t\t\t}\n\t\t\tif (!updateResult.error && updateExitCode === 0) {\n\t\t\t\ttry {\n\t\t\t\t\tconst status = await launchDaemonUpdateRestartCoordinator({\n\t\t\t\t\t\tsocketPath: daemonSocketPath,\n\t\t\t\t\t\tagentDir: getAgentDir(),\n\t\t\t\t\t\tcwd: updateCwd,\n\t\t\t\t\t\toriginActiveSessionId: this.connectionState?.activeSessionId,\n\t\t\t\t\t});\n\t\t\t\t\tconst report = buildDaemonUpdateRestartReport(status);\n\t\t\t\t\tfor (const message of report.info) {\n\t\t\t\t\t\tconsole.log(message);\n\t\t\t\t\t}\n\t\t\t\t\tfor (const warning of report.warnings) {\n\t\t\t\t\t\tconsole.error(`Warning: ${warning}`);\n\t\t\t\t\t}\n\t\t\t\t} catch (error: unknown) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`Warning: updated, but could not coordinate the daemon restart (${error instanceof Error ? error.message : String(error)}).`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst relaunchResult = spawnSync(process.execPath, [...process.execArgv, entrypoint, ...relaunchArgs], {\n\t\t\t\tstdio: \"inherit\",\n\t\t\t\tcwd: updateCwd,\n\t\t\t\tenv: process.env,\n\t\t\t});\n\t\t\tif (relaunchResult.error) {\n\t\t\t\tconsole.error(`Failed to relaunch ${APP_NAME}: ${relaunchResult.error.message}`);\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t\tprocess.exit(relaunchResult.status ?? (relaunchResult.signal ? 1 : 0));\n\t\t}\n\n\t\tthis.ui.start();\n\t\tif (this.fullscreenEnabled) {\n\t\t\tthis.applyFullscreen(true);\n\t\t}\n\t\tthis.ui.requestRender(true);\n\n\t\tif (selfUpdateNotAttempted) {\n\t\t\tthis.showStatus(`Update did not change ${APP_NAME}. Reloading resources...`);\n\t\t\tawait this.handleReloadCommand();\n\t\t\treturn;\n\t\t}\n\t\tif (updateResult.error) {\n\t\t\tthis.showError(`Update failed: ${updateResult.error.message}`);\n\t\t\treturn;\n\t\t}\n\t\tif (updateExitCode !== 0) {\n\t\t\tthis.showError(\n\t\t\t\tupdateResult.signal\n\t\t\t\t\t? `Update terminated by signal ${updateResult.signal}`\n\t\t\t\t\t: `Update exited with code ${updateExitCode}`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tthis.showStatus(\"Packages updated. Reloading resources...\");\n\t\tawait this.handleReloadCommand();\n\t}\n\n\tprivate async handleReloadCommand(): Promise<boolean> {\n\t\tif (this.isAgentStreaming()) {\n\t\t\tthis.showWarning(\"Wait for the current response to finish before reloading.\");\n\t\t\treturn false;\n\t\t}\n\t\tif (this.isAgentCompacting()) {\n\t\t\tthis.showWarning(\"Wait for compaction to finish before reloading.\");\n\t\t\treturn false;\n\t\t}\n\n\t\tthis.resetExtensionUI();\n\n\t\tconst reloadBox = new Container();\n\t\tconst borderColor = (s: string) => theme.fg(\"border\", s);\n\t\treloadBox.addChild(new DynamicBorder(borderColor));\n\t\treloadBox.addChild(new Spacer(1));\n\t\treloadBox.addChild(\n\t\t\tnew Text(theme.fg(\"muted\", \"Reloading keybindings, extensions, skills, prompts, themes...\"), 1, 0),\n\t\t);\n\t\treloadBox.addChild(new Spacer(1));\n\t\treloadBox.addChild(new DynamicBorder(borderColor));\n\n\t\tconst previousEditor = this.editor;\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(reloadBox);\n\t\tthis.ui.setFocus(reloadBox);\n\t\tthis.ui.requestRender(true);\n\t\tawait new Promise((resolve) => process.nextTick(resolve));\n\n\t\tconst dismissReloadBox = (editor: Component) => {\n\t\t\tthis.editorContainer.clear();\n\t\t\tthis.editorContainer.addChild(editor);\n\t\t\tthis.ui.setFocus(editor);\n\t\t\tthis.ui.requestRender();\n\t\t};\n\n\t\ttry {\n\t\t\tawait this.agentConnection.reload();\n\t\t\tthis.toolDefinitionCache.clear();\n\t\t\tthis.keybindings.reload();\n\t\t\tconst activeHeader = this.customHeader ?? this.builtInHeader;\n\t\t\tif (isExpandable(activeHeader)) {\n\t\t\t\tactiveHeader.setExpanded(this.toolOutputExpanded);\n\t\t\t}\n\t\t\tsetRegisteredThemes(this.uiServices.getThemes());\n\t\t\tthis.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();\n\t\t\tconst themeName = this.settingsManager.getTheme();\n\t\t\tconst themeResult = themeName ? setTheme(themeName, true) : { success: true };\n\t\t\tif (!themeResult.success) {\n\t\t\t\tthis.showError(`Failed to load theme \"${themeName}\": ${themeResult.error}\\nFell back to dark theme.`);\n\t\t\t}\n\t\t\tconst editorPaddingX = this.settingsManager.getEditorPaddingX();\n\t\t\tconst autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();\n\t\t\tthis.defaultEditor.setPaddingX(editorPaddingX);\n\t\t\tthis.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);\n\t\t\tif (this.editor !== this.defaultEditor) {\n\t\t\t\tthis.editor.setPaddingX?.(editorPaddingX);\n\t\t\t\tthis.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);\n\t\t\t}\n\t\t\tthis.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());\n\t\t\tthis.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());\n\t\t\tawait this.refreshConnectionCatalog();\n\t\t\tthis.setupAutocompleteProvider();\n\t\t\tif (this.bindLocalSessionExtensions) {\n\t\t\t\tconst runner = this.getLocalSessionHost().getExtensionRunner();\n\t\t\t\tthis.setupExtensionShortcuts(runner);\n\t\t\t}\n\t\t\tawait this.rebuildChatFromMessages();\n\t\t\tdismissReloadBox(this.editor as Component);\n\t\t\tthis.showLoadedResources({\n\t\t\t\tforce: false,\n\t\t\t\tshowDiagnosticsWhenQuiet: true,\n\t\t\t});\n\t\t\tconst modelsJsonError = this.modelRegistry.getError();\n\t\t\tif (modelsJsonError) {\n\t\t\t\tthis.showError(`models.json error: ${modelsJsonError}`);\n\t\t\t}\n\t\t\tthis.showStatus(\"Reloaded keybindings, extensions, skills, prompts, themes\");\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tdismissReloadBox(previousEditor as Component);\n\t\t\tthis.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate async handleExportCommand(text: string): Promise<void> {\n\t\tconst outputPath = this.getPathCommandArgument(text, \"/export\");\n\n\t\ttry {\n\t\t\tif (outputPath?.endsWith(\".jsonl\")) {\n\t\t\t\tconst filePath = await this.agentConnection.exportToJsonl(outputPath);\n\t\t\t\tthis.showStatus(`Session exported to: ${filePath}`);\n\t\t\t} else {\n\t\t\t\tconst filePath = await this.agentConnection.exportToHtml(outputPath);\n\t\t\t\tthis.showStatus(`Session exported to: ${filePath}`);\n\t\t\t}\n\t\t} catch (error: unknown) {\n\t\t\tthis.showError(`Failed to export session: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t}\n\t}\n\n\tprivate getPathCommandArgument(text: string, command: \"/export\" | \"/import\"): string | undefined {\n\t\tif (text === command) {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (!text.startsWith(`${command} `)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst argsString = text.slice(command.length + 1).trimStart();\n\t\tif (!argsString) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst firstChar = argsString[0];\n\t\tif (firstChar === '\"' || firstChar === \"'\") {\n\t\t\tconst closingQuoteIndex = argsString.indexOf(firstChar, 1);\n\t\t\tif (closingQuoteIndex < 0) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\treturn argsString.slice(1, closingQuoteIndex);\n\t\t}\n\n\t\tconst firstWhitespaceIndex = argsString.search(/\\s/);\n\t\tif (firstWhitespaceIndex < 0) {\n\t\t\treturn argsString;\n\t\t}\n\t\treturn argsString.slice(0, firstWhitespaceIndex);\n\t}\n\n\tprivate async handleImportCommand(text: string): Promise<void> {\n\t\tconst inputPath = this.getPathCommandArgument(text, \"/import\");\n\t\tif (!inputPath) {\n\t\t\tthis.showError(\"Usage: /import <path.jsonl>\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst confirmed = await this.showExtensionConfirm(\"Import session\", `Replace current session with ${inputPath}?`);\n\t\tif (!confirmed) {\n\t\t\tthis.showStatus(\"Import cancelled\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.stopWorkingLoader();\n\t\t\tconst result = await this.agentConnection.importFromJsonl(inputPath);\n\t\t\tif (result.cancelled) {\n\t\t\t\tthis.showStatus(\"Import cancelled\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait this.renderCurrentSessionState();\n\t\t\tthis.showStatus(`Session imported from: ${inputPath}`);\n\t\t} catch (error: unknown) {\n\t\t\tif (error instanceof MissingSessionCwdError) {\n\t\t\t\tconst selectedCwd = await this.promptForMissingSessionCwd(error);\n\t\t\t\tif (!selectedCwd) {\n\t\t\t\t\tthis.showStatus(\"Import cancelled\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst result = await this.agentConnection.importFromJsonl(inputPath, selectedCwd);\n\t\t\t\tif (result.cancelled) {\n\t\t\t\t\tthis.showStatus(\"Import cancelled\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait this.renderCurrentSessionState();\n\t\t\t\tthis.showStatus(`Session imported from: ${inputPath}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (error instanceof SessionImportFileNotFoundError) {\n\t\t\t\tthis.showError(`Failed to import session: ${error.message}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait this.handleFatalRuntimeError(\"Failed to import session\", error);\n\t\t}\n\t}\n\n\tprivate async handleShareCommand(): Promise<void> {\n\t\t// Check if gh is available and logged in\n\t\ttry {\n\t\t\tconst authResult = spawnSync(\"gh\", [\"auth\", \"status\"], { encoding: \"utf-8\" });\n\t\t\tif (authResult.status !== 0) {\n\t\t\t\tthis.showError(\"GitHub CLI is not logged in. Run 'gh auth login' first.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch {\n\t\t\tthis.showError(\"GitHub CLI (gh) is not installed. Install it from https://cli.github.com/\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Export to a temp file\n\t\tconst tmpFile = path.join(os.tmpdir(), \"session.html\");\n\t\ttry {\n\t\t\tawait this.agentConnection.exportToHtml(tmpFile);\n\t\t} catch (error: unknown) {\n\t\t\tthis.showError(`Failed to export session: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t\treturn;\n\t\t}\n\n\t\t// Show cancellable loader, replacing the editor\n\t\tconst loader = new BorderedLoader(this.ui, theme, \"Creating gist...\");\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(loader);\n\t\tthis.ui.setFocus(loader);\n\t\tthis.ui.requestRender();\n\n\t\tconst restoreEditor = () => {\n\t\t\tloader.dispose();\n\t\t\tthis.editorContainer.clear();\n\t\t\tthis.editorContainer.addChild(this.editor);\n\t\t\tthis.ui.setFocus(this.editor);\n\t\t\ttry {\n\t\t\t\tfs.unlinkSync(tmpFile);\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t};\n\n\t\t// Create a secret gist asynchronously\n\t\tlet proc: ReturnType<typeof spawn> | null = null;\n\n\t\tloader.onAbort = () => {\n\t\t\tproc?.kill();\n\t\t\trestoreEditor();\n\t\t\tthis.showStatus(\"Share cancelled\");\n\t\t};\n\n\t\ttry {\n\t\t\tconst result = await new Promise<{ stdout: string; stderr: string; code: number | null }>((resolve) => {\n\t\t\t\tproc = spawn(\"gh\", [\"gist\", \"create\", \"--public=false\", tmpFile]);\n\t\t\t\tlet stdout = \"\";\n\t\t\t\tlet stderr = \"\";\n\t\t\t\tproc.stdout?.on(\"data\", (data) => {\n\t\t\t\t\tstdout += data.toString();\n\t\t\t\t});\n\t\t\t\tproc.stderr?.on(\"data\", (data) => {\n\t\t\t\t\tstderr += data.toString();\n\t\t\t\t});\n\t\t\t\tproc.on(\"close\", (code) => resolve({ stdout, stderr, code }));\n\t\t\t});\n\n\t\t\tif (loader.signal.aborted) return;\n\n\t\t\trestoreEditor();\n\n\t\t\tif (result.code !== 0) {\n\t\t\t\tconst errorMsg = result.stderr?.trim() || \"Unknown error\";\n\t\t\t\tthis.showError(`Failed to create gist: ${errorMsg}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Extract gist ID from the URL returned by gh\n\t\t\t// gh returns something like: https://gist.github.com/username/GIST_ID\n\t\t\tconst gistUrl = result.stdout?.trim();\n\t\t\tconst gistId = gistUrl?.split(\"/\").pop();\n\t\t\tif (!gistId) {\n\t\t\t\tthis.showError(\"Failed to parse gist ID from gh output\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Create the preview URL\n\t\t\tconst previewUrl = getShareViewerUrl(gistId);\n\t\t\tthis.showStatus(`Share URL: ${previewUrl}\\nGist: ${gistUrl}`);\n\t\t} catch (error: unknown) {\n\t\t\tif (!loader.signal.aborted) {\n\t\t\t\trestoreEditor();\n\t\t\t\tthis.showError(`Failed to create gist: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async handleCopyCommand(): Promise<void> {\n\t\tconst text = await this.agentConnection.getLastAssistantText();\n\t\tif (!text) {\n\t\t\tthis.showError(\"No agent messages to copy yet.\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tawait copyToClipboard(text);\n\t\t\tthis.showStatus(\"Copied last agent message to clipboard\");\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate async handleNameCommand(text: string): Promise<void> {\n\t\tconst name = text.replace(/^\\/name\\s*/, \"\").trim();\n\t\tif (!name) {\n\t\t\tconst currentName = this.getCurrentSessionName();\n\t\t\tif (currentName) {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", `Session name: ${currentName}`), 1, 0));\n\t\t\t} else {\n\t\t\t\tthis.showWarning(\"Usage: /name <name>\");\n\t\t\t}\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tawait this.agentConnection.setSessionName(name);\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", `Session name set: ${name}`), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async handleRlmMaxDepthCommand(args: string): Promise<void> {\n\t\tconst tokens = args ? args.split(/\\s+/) : [];\n\t\tif (tokens.length === 0) {\n\t\t\ttry {\n\t\t\t\tconst status = await this.agentConnection.getRlmMaxDepthStatus();\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(theme.fg(\"dim\", `RLM max depth: ${status.maxDepth} (${status.source})`), 1, 0),\n\t\t\t\t);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t} catch (error) {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst global = tokens[1] === \"--global\";\n\t\tif (tokens.length > (global ? 2 : 1) || !/^\\d+$/.test(tokens[0] ?? \"\")) {\n\t\t\tthis.showWarning(\"Usage: /rlm-max-depth [<non-negative integer> [--global]]\");\n\t\t\treturn;\n\t\t}\n\t\tconst maxDepth = Number(tokens[0]);\n\t\tif (!Number.isSafeInteger(maxDepth)) {\n\t\t\tthis.showWarning(\"RLM max depth must be a non-negative integer.\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await this.agentConnection.setRlmMaxDepth(maxDepth, { global });\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t`RLM max depth set: ${result.maxDepth}${result.globalSaved ? \" and saved as global default\" : \"\"}`,\n\t\t\t\t\t),\n\t\t\t\t\t1,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.ui.requestRender();\n\t\t\tif (result.globalError) {\n\t\t\t\tthis.showError(\n\t\t\t\t\t`RLM max depth set for this chat, but the global default was not saved: ${result.globalError}`,\n\t\t\t\t);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate async handleSessionCommand(): Promise<void> {\n\t\tconst stats = await this.agentConnection.getSessionStats();\n\t\tconst sessionName = this.getCurrentSessionName();\n\n\t\tlet info = `${theme.bold(\"Session Info\")}\\n\\n`;\n\t\tif (sessionName) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Name:\")} ${sessionName}\\n`;\n\t\t}\n\t\tinfo += `${theme.fg(\"dim\", \"File:\")} ${stats.sessionFile ?? \"In-memory\"}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"ID:\")} ${stats.sessionId}\\n\\n`;\n\t\tinfo += `${theme.bold(\"Messages\")}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"User:\")} ${stats.userMessages}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Assistant:\")} ${stats.assistantMessages}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Tool Calls:\")} ${stats.toolCalls}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Tool Results:\")} ${stats.toolResults}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${stats.totalMessages}\\n\\n`;\n\t\tinfo += theme.fg(\"dim\", \"Use /context for token, cost, and context usage.\");\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(info, 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleLogsCommand(): void {\n\t\tconst logsDir = getLogsDir();\n\t\tlet info = `${theme.bold(\"Logs\")}\\n\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Directory:\")} ${logsDir}\\n\\n`;\n\n\t\tlet files: string[] = [];\n\t\ttry {\n\t\t\tif (fs.existsSync(logsDir)) {\n\t\t\t\tfiles = fs.readdirSync(logsDir).filter((name) => !name.startsWith(\".\"));\n\t\t\t}\n\t\t} catch {\n\t\t\t// Fall through to the empty-state line below.\n\t\t}\n\t\tif (files.length === 0) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"No logs written yet.\")}\\n`;\n\t\t} else {\n\t\t\tfor (const name of files.sort()) {\n\t\t\t\tlet size = \"\";\n\t\t\t\ttry {\n\t\t\t\t\tsize = ` ${theme.fg(\"dim\", `(${(fs.statSync(path.join(logsDir, name)).size / 1024).toFixed(1)} KB)`)}`;\n\t\t\t\t} catch {\n\t\t\t\t\t// Skip the size if the file vanished between readdir and stat.\n\t\t\t\t}\n\t\t\t\tinfo += `${theme.fg(\"dim\", \"•\")} ${name}${size}\\n`;\n\t\t\t}\n\t\t}\n\t\tinfo += `\\n${theme.fg(\"dim\", \"Daemon crashes log to <socket>.log; agent-open failures log to client-errors.log.\")}`;\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(info, 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async handleSystemPromptCommand(): Promise<void> {\n\t\tconst prompt = await this.agentConnection.getSystemPrompt();\n\t\tconst header = `${theme.bold(\"System Prompt\")} ${theme.fg(\"dim\", `(${prompt.length} chars)`)}`;\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(header, 1, 0));\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(prompt, 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate formatTraceUploadResult(result: AgentTraceUploadResult): string {\n\t\tswitch (result.status) {\n\t\t\tcase \"uploaded\":\n\t\t\t\treturn `Trace uploaded (${result.bytesStored.toLocaleString()} bytes).`;\n\t\t\tcase \"disabled\":\n\t\t\t\treturn \"Trace sharing is disabled.\";\n\t\t\tcase \"missing_credentials\":\n\t\t\t\treturn \"Trace sharing needs a Prime API key. Run /traces login.\";\n\t\t\tcase \"no_session_file\":\n\t\t\t\treturn \"Current session has no persisted trace yet.\";\n\t\t\tcase \"empty_session\":\n\t\t\t\treturn \"Current session trace is empty.\";\n\t\t\tcase \"invalid_session\":\n\t\t\t\treturn `Trace upload skipped: ${result.message}.`;\n\t\t\tcase \"too_large\":\n\t\t\t\treturn `Trace upload skipped: session file is ${result.size.toLocaleString()} bytes; limit is ${result.maxBytes.toLocaleString()} bytes.`;\n\t\t\tcase \"failed\":\n\t\t\t\tif (result.statusCode === 404) {\n\t\t\t\t\treturn \"Trace upload endpoint was not found. The platform API may not be deployed yet, or ASM_AGENT_TRACES_BASE_URL points at the wrong API.\";\n\t\t\t\t}\n\t\t\t\treturn `Trace upload failed: ${result.statusCode ? `HTTP ${result.statusCode}: ` : \"\"}${result.message}. See ${getAgentTracesLogPath()} for details.`;\n\t\t}\n\t}\n\n\tprivate async uploadCurrentTraceOnce(): Promise<AgentTraceUploadResult> {\n\t\tconst state = await this.agentConnection.getState();\n\t\treturn uploadAgentTraceFile({\n\t\t\tsessionFile: state.sessionFile,\n\t\t\tauthStorage: this.modelRegistry.authStorage,\n\t\t\tsettingsManager: this.settingsManager,\n\t\t\trequireEnabled: false,\n\t\t\treloadConfig: false,\n\t\t});\n\t}\n\n\tprivate async previewCurrentTrace(): Promise<void> {\n\t\tconst state = await this.agentConnection.getState();\n\t\tconst result = await previewAgentTraceFile({ sessionFile: state.sessionFile });\n\t\tlet info: string;\n\t\tswitch (result.status) {\n\t\t\tcase \"no_session_file\":\n\t\t\t\tinfo = \"Trace preview is unavailable until the current session has a persisted assistant response.\";\n\t\t\t\tbreak;\n\t\t\tcase \"empty_session\":\n\t\t\t\tinfo = \"The current trace is empty.\";\n\t\t\t\tbreak;\n\t\t\tcase \"invalid_session\":\n\t\t\tcase \"failed\":\n\t\t\t\tinfo = `Trace preview failed: ${result.message}.`;\n\t\t\t\tbreak;\n\t\t\tcase \"ready\":\n\t\t\t\tinfo = this.formatTracePreview(result);\n\t\t\t\tbreak;\n\t\t}\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(info, 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate formatTracePreview(result: Extract<AgentTracePreviewResult, { status: \"ready\" }>): string {\n\t\tconst lines = [\n\t\t\ttheme.bold(\"Trace Preview\"),\n\t\t\ttheme.fg(\"dim\", \"Nothing has been uploaded by this command.\"),\n\t\t\t\"\",\n\t\t\t`${theme.fg(\"dim\", \"File:\")} ${result.sessionFile}`,\n\t\t\t`${theme.fg(\"dim\", \"Size:\")} ${result.size.toLocaleString()} bytes`,\n\t\t\t`${theme.fg(\"dim\", \"Uploadable:\")} ${result.uploadable ? \"Yes\" : `No (limit ${result.maxBytes.toLocaleString()} bytes)`}`,\n\t\t\t`${theme.fg(\"dim\", \"Endpoint:\")} ${result.endpoint}`,\n\t\t\t`${theme.fg(\"dim\", \"Session ID:\")} ${result.sessionId}`,\n\t\t\t`${theme.fg(\"dim\", \"Trace ID:\")} ${result.traceId}`,\n\t\t];\n\t\tif (result.parentSessionId) {\n\t\t\tlines.push(`${theme.fg(\"dim\", \"Parent session:\")} ${result.parentSessionId}`);\n\t\t}\n\t\tif (result.gitRepo) {\n\t\t\tlines.push(`${theme.fg(\"dim\", \"Git repository:\")} ${result.gitRepo}`);\n\t\t}\n\t\tif (result.gitCommit) {\n\t\t\tlines.push(`${theme.fg(\"dim\", \"Git commit:\")} ${result.gitCommit}`);\n\t\t}\n\t\tlines.push(\"\", theme.bold(\"Raw JSONL payload preview\"));\n\t\tif (result.contentPreview) {\n\t\t\tlines.push(result.contentPreview);\n\t\t\tif (result.truncated) {\n\t\t\t\tlines.push(\"\", theme.fg(\"dim\", \"Preview truncated; upload sends the complete file.\"));\n\t\t\t}\n\t\t} else {\n\t\t\tlines.push(theme.fg(\"dim\", \"Payload omitted because the trace exceeds the upload limit.\"));\n\t\t}\n\t\treturn lines.join(\"\\n\");\n\t}\n\n\tprivate async uploadAllTraces(sessionDir?: string, signal?: AbortSignal): Promise<AgentTraceUploadAllResult> {\n\t\treturn uploadAllAgentTraces({\n\t\t\tauthStorage: this.modelRegistry.authStorage,\n\t\t\tsettingsManager: this.settingsManager,\n\t\t\tsessionDir,\n\t\t\trequireEnabled: false,\n\t\t\treloadConfig: false,\n\t\t\tsignal,\n\t\t\tonProgress: ({ completed, total }) => {\n\t\t\t\tif (total > 0 && (completed === 0 || completed === total || completed % 10 === 0)) {\n\t\t\t\t\tthis.showStatus(\n\t\t\t\t\t\t`Uploading traces: ${completed.toLocaleString()}/${total.toLocaleString()} (${keyText(\"app.clear\")} to cancel)`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t}\n\n\tprivate async handleTracesCommand(text: string): Promise<void> {\n\t\tconst command =\n\t\t\ttext\n\t\t\t\t.replace(/^\\/traces\\b/, \"\")\n\t\t\t\t.trim()\n\t\t\t\t.toLowerCase() || \"status\";\n\n\t\tif (command === \"status\") {\n\t\t\tawait this.settingsManager.reload().catch(() => undefined);\n\t\t\tconst credential = await getAsmAgentTraceCredential(this.modelRegistry.authStorage);\n\t\t\tconst state = await this.agentConnection.getState();\n\t\t\tconst info = [\n\t\t\t\ttheme.bold(\"Trace Sharing\"),\n\t\t\t\t\"\",\n\t\t\t\t`${theme.fg(\"dim\", \"Automatic uploads:\")} ${this.settingsManager.getAgentTracesEnabled() ? \"Enabled\" : \"Disabled\"}`,\n\t\t\t\t`${theme.fg(\"dim\", \"Credential:\")} ${credential?.label ?? \"Not configured\"}`,\n\t\t\t\t`${theme.fg(\"dim\", \"Endpoint:\")} ${resolveAsmAgentTracesBaseUrl()}`,\n\t\t\t\t`${theme.fg(\"dim\", \"Session file:\")} ${state.sessionFile ?? \"In-memory\"}`,\n\t\t\t\t\"\",\n\t\t\t\ttheme.fg(\n\t\t\t\t\t\"dim\",\n\t\t\t\t\t\"Commands: /traces on, /traces off, /traces preview, /traces upload-current, /traces upload-all, /traces login\",\n\t\t\t\t),\n\t\t\t].join(\"\\n\");\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(info, 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tif (command === \"off\" || command === \"disable\") {\n\t\t\tthis.settingsManager.setAgentTracesEnabled(false);\n\t\t\tawait this.settingsManager.flush();\n\t\t\tthis.showStatus(\"Trace sharing disabled.\");\n\t\t\treturn;\n\t\t}\n\n\t\tif (command === \"login\") {\n\t\t\tawait this.createAuthFlows().runAsmAgentTracesLogin();\n\t\t\treturn;\n\t\t}\n\n\t\tif (command === \"preview\") {\n\t\t\tawait this.previewCurrentTrace();\n\t\t\treturn;\n\t\t}\n\n\t\tif (command === \"on\" || command === \"enable\") {\n\t\t\tlet credential = await getAsmAgentTraceCredential(this.modelRegistry.authStorage);\n\t\t\tif (!credential) {\n\t\t\t\tconst authResult = await this.createAuthFlows().runAsmAgentTracesLogin();\n\t\t\t\tif (authResult.status !== \"success\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcredential = await getAsmAgentTraceCredential(this.modelRegistry.authStorage);\n\t\t\t}\n\t\t\tif (!credential) {\n\t\t\t\tthis.showError(\"Trace sharing needs a Prime API key.\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.settingsManager.setAgentTracesEnabled(true);\n\t\t\tawait this.settingsManager.flush();\n\t\t\tconst uploadResult = await this.uploadCurrentTraceOnce();\n\t\t\tconst uploadMessage =\n\t\t\t\tuploadResult.status === \"no_session_file\" || uploadResult.status === \"empty_session\"\n\t\t\t\t\t? \"Current session will upload after the first assistant response.\"\n\t\t\t\t\t: this.formatTraceUploadResult(uploadResult);\n\t\t\tthis.showStatus(`Trace sharing enabled. ${uploadMessage}`);\n\t\t\treturn;\n\t\t}\n\n\t\tif (command === \"upload\" || command === \"upload-current\") {\n\t\t\tconst credential = await getAsmAgentTraceCredential(this.modelRegistry.authStorage);\n\t\t\tif (!credential) {\n\t\t\t\tthis.showError(\"Trace sharing needs a Prime API key. Run /traces login.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst uploadResult = await this.uploadCurrentTraceOnce();\n\t\t\tconst message = this.formatTraceUploadResult(uploadResult);\n\t\t\tif (uploadResult.status === \"failed\") {\n\t\t\t\tthis.showError(message);\n\t\t\t} else {\n\t\t\t\tthis.showStatus(message);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (command === \"upload-all\") {\n\t\t\tconst credential = await getAsmAgentTraceCredential(this.modelRegistry.authStorage);\n\t\t\tif (!credential) {\n\t\t\t\tthis.showError(\"Trace sharing needs a Prime API key. Run /traces login.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (this.traceUploadAllAbortController) {\n\t\t\t\tthis.showWarning(\"A trace upload is already running. Cancel it before starting another.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst state = await this.agentConnection.getState();\n\t\t\tconst abortController = new AbortController();\n\t\t\tthis.traceUploadAllAbortController = abortController;\n\t\t\tlet result: AgentTraceUploadAllResult;\n\t\t\ttry {\n\t\t\t\tresult = await this.uploadAllTraces(state.sessionDir, abortController.signal);\n\t\t\t} finally {\n\t\t\t\tif (this.traceUploadAllAbortController === abortController) {\n\t\t\t\t\tthis.traceUploadAllAbortController = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (abortController.signal.aborted) {\n\t\t\t\tthis.showStatus(\"Trace upload cancelled.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (result.total === 0) {\n\t\t\t\tthis.showStatus(\"No persisted traces were found.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst summary = [\n\t\t\t\t`Uploaded ${result.uploaded.toLocaleString()} of ${result.total.toLocaleString()} traces`,\n\t\t\t\tresult.skipped > 0 ? `${result.skipped.toLocaleString()} skipped` : undefined,\n\t\t\t\tresult.failed > 0 ? `${result.failed.toLocaleString()} failed` : undefined,\n\t\t\t\t`${result.bytesStored.toLocaleString()} bytes stored`,\n\t\t\t]\n\t\t\t\t.filter((part): part is string => part !== undefined)\n\t\t\t\t.join(\"; \");\n\t\t\tif (result.failed > 0) {\n\t\t\t\tthis.showWarning(`${summary}. See ${getAgentTracesLogPath()} for details.`);\n\t\t\t} else {\n\t\t\t\tthis.showStatus(`${summary}.`);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tthis.showWarning(\"Usage: /traces [status|on|off|preview|upload|upload-current|upload-all|login]\");\n\t}\n\n\tprivate async handleContextCommand(): Promise<void> {\n\t\tlet info: string;\n\t\ttry {\n\t\t\tconst tree = await this.agentConnection.getContextTree();\n\t\t\tconst width = Math.max(60, Math.min(this.ui.terminal.columns - 2, 120));\n\t\t\tinfo = formatContextTree(tree, width);\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\treturn;\n\t\t}\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(info, 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async handleHeartbeatCommand(text: string): Promise<void> {\n\t\ttry {\n\t\t\tconst command = parseHeartbeatCommand(text);\n\t\t\tswitch (command.type) {\n\t\t\t\tcase \"status\": {\n\t\t\t\t\tconst heartbeat = await this.agentConnection.getHeartbeat();\n\t\t\t\t\tthis.patchConnectionState({ heartbeat: heartbeat ?? null });\n\t\t\t\t\tawait this.refreshHeartbeatCatalog();\n\t\t\t\t\tthis.showHeartbeat(heartbeat);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcase \"set\": {\n\t\t\t\t\tconst heartbeat = await this.agentConnection.setHeartbeat(\n\t\t\t\t\t\tcommand.schedule,\n\t\t\t\t\t\tcommand.instruction,\n\t\t\t\t\t\tcommand.deliveryMode,\n\t\t\t\t\t);\n\t\t\t\t\tthis.patchConnectionState({ heartbeat });\n\t\t\t\t\tawait this.refreshHeartbeatCatalog();\n\t\t\t\t\tthis.showStatus(\n\t\t\t\t\t\t`Heartbeat set\\nDelivery: ${heartbeat.deliveryMode ?? DEFAULT_HEARTBEAT_DELIVERY_MODE}\\nNext run: ${heartbeat.nextRunAt ?? \"-\"}`,\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcase \"pause\": {\n\t\t\t\t\tconst heartbeat = await this.agentConnection.updateHeartbeat(\"pause\");\n\t\t\t\t\tif (!heartbeat) {\n\t\t\t\t\t\tthis.showStatus(\"No active heartbeat\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.patchConnectionState({ heartbeat });\n\t\t\t\t\tawait this.refreshHeartbeatCatalog();\n\t\t\t\t\tthis.showStatus(\"Heartbeat paused\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcase \"resume\": {\n\t\t\t\t\tconst heartbeat = await this.agentConnection.updateHeartbeat(\"resume\");\n\t\t\t\t\tif (!heartbeat) {\n\t\t\t\t\t\tthis.showStatus(\"No active heartbeat\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.patchConnectionState({ heartbeat });\n\t\t\t\t\tawait this.refreshHeartbeatCatalog();\n\t\t\t\t\tthis.showStatus(`Heartbeat resumed\\nNext run: ${heartbeat.nextRunAt ?? \"-\"}`);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcase \"clear\": {\n\t\t\t\t\tconst heartbeat = await this.agentConnection.updateHeartbeat(\"clear\");\n\t\t\t\t\tif (!heartbeat) {\n\t\t\t\t\t\tthis.showStatus(\"No active heartbeat\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.patchConnectionState({ heartbeat: null });\n\t\t\t\t\tawait this.refreshHeartbeatCatalog();\n\t\t\t\t\tthis.showStatus(\"Heartbeat cleared\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate async showHeartbeatManager(): Promise<void> {\n\t\tif (this.heartbeatManagerHandle) {\n\t\t\tthis.heartbeatManagerHandle.focus();\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tawait this.refreshHeartbeatCatalog();\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\treturn;\n\t\t}\n\t\tconst manager = new HeartbeatManagerComponent(this.heartbeats, {\n\t\t\tgetRows: () => this.ui.terminal.rows,\n\t\t\tonAction: (heartbeat, action) => this.manageHeartbeat(heartbeat, action),\n\t\t\tonClose: () => this.closeHeartbeatManager(),\n\t\t\trequestRender: () => this.ui.requestRender(),\n\t\t});\n\t\tthis.heartbeatManager = manager;\n\t\tthis.heartbeatManagerHandle = this.showFullPaneOverlay(manager, {\n\t\t\tfullWidth: true,\n\t\t\tsuspendFullscreenMouse: true,\n\t\t});\n\t\tthis.scheduleHeartbeatManagerRefresh();\n\t}\n\n\tprivate closeHeartbeatManager(): void {\n\t\tif (this.heartbeatManagerRefreshTimer) {\n\t\t\tclearTimeout(this.heartbeatManagerRefreshTimer);\n\t\t\tthis.heartbeatManagerRefreshTimer = undefined;\n\t\t}\n\t\tthis.heartbeatManagerHandle?.hide();\n\t\tthis.heartbeatManagerHandle = undefined;\n\t\tthis.heartbeatManager = undefined;\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate scheduleHeartbeatManagerRefresh(): void {\n\t\tif (this.heartbeatManagerRefreshTimer) {\n\t\t\tclearTimeout(this.heartbeatManagerRefreshTimer);\n\t\t\tthis.heartbeatManagerRefreshTimer = undefined;\n\t\t}\n\t\tif (!this.heartbeatManager) {\n\t\t\treturn;\n\t\t}\n\t\tconst nextRunAt = this.heartbeats\n\t\t\t.filter((heartbeat) => heartbeat.job.status === \"active\" && heartbeat.job.nextRunAt)\n\t\t\t.map((heartbeat) => Date.parse(heartbeat.job.nextRunAt!))\n\t\t\t.filter(Number.isFinite)\n\t\t\t.sort((left, right) => left - right)[0];\n\t\tif (nextRunAt === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tconst untilNextRun = nextRunAt - Date.now();\n\t\tconst delay = untilNextRun > 0 ? Math.min(60_000, untilNextRun + 250) : 5_000;\n\t\tthis.heartbeatManagerRefreshTimer = setTimeout(() => {\n\t\t\tthis.heartbeatManagerRefreshTimer = undefined;\n\t\t\tif (!this.heartbeatManager) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvoid this.refreshHeartbeatCatalog().catch(() => this.scheduleHeartbeatManagerRefresh());\n\t\t}, delay);\n\t\tthis.heartbeatManagerRefreshTimer.unref?.();\n\t}\n\n\tprivate async manageHeartbeat(\n\t\theartbeat: AgentConnectionHeartbeat,\n\t\taction: AgentHeartbeatManagementAction,\n\t): Promise<void> {\n\t\tconst updated = await this.agentConnection.manageHeartbeat(\n\t\t\theartbeat.job.activeSessionId,\n\t\t\theartbeat.job.id,\n\t\t\taction,\n\t\t);\n\t\tif (updated.source === \"heartbeat\" && updated.activeSessionId === this.connectionState?.activeSessionId) {\n\t\t\tthis.patchConnectionState({ heartbeat: action === \"stop\" ? null : updated });\n\t\t}\n\t\tconst remaining = this.heartbeatCatalog.filter((entry) => entry.job.id !== updated.id);\n\t\tthis.applyHeartbeatCatalog(\n\t\t\tupdated.status === \"active\" || updated.status === \"paused\"\n\t\t\t\t? [...remaining, { ...heartbeat, job: updated }]\n\t\t\t\t: remaining,\n\t\t);\n\t\tvoid this.refreshHeartbeatCatalog().catch(() => undefined);\n\t}\n\n\tprivate showHeartbeat(job: AgentCronJob | undefined): void {\n\t\tif (!job) {\n\t\t\tthis.showStatus(\"No active heartbeat\");\n\t\t\treturn;\n\t\t}\n\t\tconst next = job.nextRunAt ? new Date(job.nextRunAt).toLocaleString() : \"-\";\n\t\tconst last = job.lastRunAt ? new Date(job.lastRunAt).toLocaleString() : \"-\";\n\t\tconst lines = [\n\t\t\ttheme.bold(\"Heartbeat\"),\n\t\t\t\"\",\n\t\t\t`${theme.fg(\"dim\", \"Status:\")} ${job.status}`,\n\t\t\t`${theme.fg(\"dim\", \"Every:\")} ${job.schedule.expression}`,\n\t\t\t`${theme.fg(\"dim\", \"Delivery:\")} ${job.deliveryMode ?? DEFAULT_HEARTBEAT_DELIVERY_MODE}`,\n\t\t\t`${theme.fg(\"dim\", \"Instruction:\")} ${job.prompt}`,\n\t\t\t`${theme.fg(\"dim\", \"Next:\")} ${next}`,\n\t\t\t`${theme.fg(\"dim\", \"Last:\")} ${last}`,\n\t\t\t`${theme.fg(\"dim\", \"Runs:\")} ${job.runCount}`,\n\t\t];\n\t\tif (job.lastError) {\n\t\t\tlines.push(`${theme.fg(\"dim\", \"Error:\")} ${job.lastError}`);\n\t\t}\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(lines.join(\"\\n\"), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleChangelogCommand(): void {\n\t\tconst changelogPath = getChangelogPath();\n\t\tconst allEntries = parseChangelog(changelogPath);\n\n\t\tconst changelogMarkdown =\n\t\t\tallEntries.length > 0\n\t\t\t\t? allEntries\n\t\t\t\t\t\t.reverse()\n\t\t\t\t\t\t.map((e) => e.content)\n\t\t\t\t\t\t.join(\"\\n\\n\")\n\t\t\t\t: \"No changelog entries found.\";\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new DynamicBorder());\n\t\tthis.chatContainer.addChild(new Text(theme.bold(theme.fg(\"accent\", \"What's New\")), 1, 0));\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, this.getMarkdownThemeWithSettings()));\n\t\tthis.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate capitalizeKey(key: string): string {\n\t\treturn key\n\t\t\t.split(\"/\")\n\t\t\t.map((k) =>\n\t\t\t\tk\n\t\t\t\t\t.split(\"+\")\n\t\t\t\t\t.map((part) => (part === \"esc\" ? part : part.charAt(0).toUpperCase() + part.slice(1)))\n\t\t\t\t\t.join(\"+\"),\n\t\t\t)\n\t\t\t.join(\"/\");\n\t}\n\n\tprivate getAppKeyDisplay(action: AppKeybinding): string {\n\t\treturn this.capitalizeKey(keyText(action));\n\t}\n\n\tprivate getEditorKeyDisplay(action: Keybinding): string {\n\t\treturn this.capitalizeKey(keyText(action));\n\t}\n\n\tprivate getShortcutGuide(): string {\n\t\tconst tab = this.getEditorKeyDisplay(\"tui.input.tab\");\n\t\tconst newLine = this.getEditorKeyDisplay(\"tui.input.newLine\");\n\t\tconst clearInput = this.getAppKeyDisplay(\"app.input.clear\");\n\t\tconst shortcutsKey = this.getAppKeyDisplay(\"app.shortcuts\");\n\t\tconst selectModel = this.getAppKeyDisplay(\"app.model.select\");\n\t\tconst expandTools = this.getAppKeyDisplay(\"app.tools.expand\");\n\t\tconst expandMessages = this.getAppKeyDisplay(\"app.messages.expand\");\n\t\tconst expandEdits = this.getAppKeyDisplay(\"app.edits.expand\");\n\t\tconst toggleThinking = this.getAppKeyDisplay(\"app.thinking.toggle\");\n\t\tconst externalEditor = this.getAppKeyDisplay(\"app.editor.external\");\n\t\tconst promptStash = this.getAppKeyDisplay(\"app.prompt.stash\");\n\t\tconst pasteImage = this.getAppKeyDisplay(\"app.clipboard.pasteImage\");\n\n\t\treturn `\n**Prompt**\n\\`!\\` shell mode · \\`/\\` commands · \\`@\\` file paths\n\\`${tab}\\` complete paths · \\`${newLine}\\` new line\n\\`${clearInput}\\` interrupt · press twice to rewind or clear the prompt\n\n**Controls**\n\\`${selectModel}\\` select model · \\`/effort\\` set reasoning · \\`${expandTools}\\` tool output\n\\`${expandMessages}\\` agent messages · \\`${expandEdits}\\` edit diffs · \\`${toggleThinking}\\` thinking blocks · \\`${promptStash}\\` stash prompt · \\`${externalEditor}\\` edit in \\`$EDITOR\\`\n\\`${pasteImage}\\` paste image\n\n**Help**\n${shortcutsKey ? `\\`${shortcutsKey}\\` quick shortcuts · ` : \"\"}\\`/hotkeys\\` full reference\n`;\n\t}\n\n\tprivate getHotkeysGuide(): string {\n\t\tconst cursorUp = this.getEditorKeyDisplay(\"tui.editor.cursorUp\");\n\t\tconst cursorDown = this.getEditorKeyDisplay(\"tui.editor.cursorDown\");\n\t\tconst cursorLeft = this.getEditorKeyDisplay(\"tui.editor.cursorLeft\");\n\t\tconst cursorRight = this.getEditorKeyDisplay(\"tui.editor.cursorRight\");\n\t\tconst cursorWordLeft = this.getEditorKeyDisplay(\"tui.editor.cursorWordLeft\");\n\t\tconst cursorWordRight = this.getEditorKeyDisplay(\"tui.editor.cursorWordRight\");\n\t\tconst cursorLineStart = this.getEditorKeyDisplay(\"tui.editor.cursorLineStart\");\n\t\tconst cursorLineEnd = this.getEditorKeyDisplay(\"tui.editor.cursorLineEnd\");\n\t\tconst jumpForward = this.getEditorKeyDisplay(\"tui.editor.jumpForward\");\n\t\tconst jumpBackward = this.getEditorKeyDisplay(\"tui.editor.jumpBackward\");\n\t\tconst pageUp = this.getEditorKeyDisplay(\"tui.editor.pageUp\");\n\t\tconst pageDown = this.getEditorKeyDisplay(\"tui.editor.pageDown\");\n\t\tconst submit = this.getEditorKeyDisplay(\"tui.input.submit\");\n\t\tconst newLine = this.getEditorKeyDisplay(\"tui.input.newLine\");\n\t\tconst deleteWordBackward = this.getEditorKeyDisplay(\"tui.editor.deleteWordBackward\");\n\t\tconst deleteWordForward = this.getEditorKeyDisplay(\"tui.editor.deleteWordForward\");\n\t\tconst deleteToLineStart = this.getEditorKeyDisplay(\"tui.editor.deleteToLineStart\");\n\t\tconst deleteToLineEnd = this.getEditorKeyDisplay(\"tui.editor.deleteToLineEnd\");\n\t\tconst yank = this.getEditorKeyDisplay(\"tui.editor.yank\");\n\t\tconst yankPop = this.getEditorKeyDisplay(\"tui.editor.yankPop\");\n\t\tconst undo = this.getEditorKeyDisplay(\"tui.editor.undo\");\n\t\tconst tab = this.getEditorKeyDisplay(\"tui.input.tab\");\n\t\tconst clear = this.getAppKeyDisplay(\"app.clear\");\n\t\tconst clearInput = this.getAppKeyDisplay(\"app.input.clear\");\n\t\tconst interrupt = this.getAppKeyDisplay(\"app.interrupt\");\n\t\tconst shortcutsKey = this.getAppKeyDisplay(\"app.shortcuts\");\n\t\tconst exit = this.getAppKeyDisplay(\"app.exit\");\n\t\tconst selectModel = this.getAppKeyDisplay(\"app.model.select\");\n\t\tconst expandTools = this.getAppKeyDisplay(\"app.tools.expand\");\n\t\tconst expandMessages = this.getAppKeyDisplay(\"app.messages.expand\");\n\t\tconst expandEdits = this.getAppKeyDisplay(\"app.edits.expand\");\n\t\tconst toggleThinking = this.getAppKeyDisplay(\"app.thinking.toggle\");\n\t\tconst focusSubagents = this.getAppKeyDisplay(\"app.subagents.focus\");\n\t\tconst manageHeartbeats = this.getAppKeyDisplay(\"app.heartbeats.open\");\n\t\tconst externalEditor = this.getAppKeyDisplay(\"app.editor.external\");\n\t\tconst promptStash = this.getAppKeyDisplay(\"app.prompt.stash\");\n\t\tconst followUp = this.getAppKeyDisplay(\"app.message.followUp\");\n\t\tconst browseQueue = this.getAppKeyDisplay(\"app.message.navigateOlder\");\n\t\tconst reorderQueue = `${this.getAppKeyDisplay(\"app.message.moveEarlier\")} / ${this.getAppKeyDisplay(\"app.message.moveLater\")}`;\n\t\tconst pasteImage = this.getAppKeyDisplay(\"app.clipboard.pasteImage\");\n\t\tconst viewportPageUp = this.getEditorKeyDisplay(\"tui.viewport.pageUp\");\n\t\tconst viewportPageDown = this.getEditorKeyDisplay(\"tui.viewport.pageDown\");\n\t\tconst viewportTop = this.getEditorKeyDisplay(\"tui.viewport.top\");\n\t\tconst viewportFollow = this.getEditorKeyDisplay(\"tui.viewport.follow\");\n\n\t\tlet hotkeys = `\n**Navigation**\n| Key | Action |\n|-----|--------|\n| \\`${cursorUp}\\` / \\`${cursorDown}\\` / \\`${cursorLeft}\\` / \\`${cursorRight}\\` | Move cursor / browse history (Up when empty) |\n| \\`${cursorWordLeft}\\` / \\`${cursorWordRight}\\` | Move by word |\n| \\`${cursorLineStart}\\` | Start of line |\n| \\`${cursorLineEnd}\\` | End of line |\n| \\`${jumpForward}\\` | Jump forward to character |\n| \\`${jumpBackward}\\` | Jump backward to character |\n| \\`${pageUp}\\` / \\`${pageDown}\\` | Scroll by page |\n\n**Editing**\n| Key | Action |\n|-----|--------|\n| \\`${submit}\\` | Send message |\n| \\`${newLine}\\` | New line${process.platform === \"win32\" ? \" (Ctrl+Enter on Windows Terminal)\" : \"\"} |\n| \\`${deleteWordBackward}\\` | Delete word backwards |\n| \\`${deleteWordForward}\\` | Delete word forwards |\n| \\`${deleteToLineStart}\\` | Delete to start of line |\n| \\`${deleteToLineEnd}\\` | Delete to end of line |\n| \\`${yank}\\` | Paste the most-recently-deleted text |\n| \\`${yankPop}\\` | Cycle through the deleted text after pasting |\n| \\`${undo}\\` | Undo |\n\n**Other**\n| Key | Action |\n|-----|--------|\n| \\`${tab}\\` | Path completion / accept autocomplete |\n| \\`${clearInput}\\` | Clear input / cancel autocomplete |\n| \\`${clear}\\` | Interrupt current operation (first) / exit (second) |\n${interrupt ? `| \\`${interrupt}\\` | Interrupt current operation |\\n` : \"\"}${shortcutsKey ? `| \\`${shortcutsKey}\\` | Show quick shortcuts |\\n` : \"\"}| \\`${exit}\\` | Exit (when editor is empty) |\n| \\`${selectModel}\\` | Open model selector |\n| \\`${expandTools}\\` | Toggle tool output expansion |\n| \\`${expandMessages}\\` | Toggle agent message expansion |\n| \\`${expandEdits}\\` | Toggle edit diff expansion |\n| \\`${toggleThinking}\\` | Toggle thinking block visibility |\n| \\`${focusSubagents}\\` | Focus the subagent summary / open the scoped agents view |\n| \\`${manageHeartbeats}\\` | Manage heartbeats |\n| \\`${externalEditor}\\` | Edit message in external editor |\n| \\`${promptStash}\\` | Stash or restore draft prompt |\n| \\`${followUp}\\` | Queue follow-up message |\n| \\`${browseQueue}\\` | Browse and edit queued messages |\n| \\`${reorderQueue}\\` | Reorder the selected queued message |\n| \\`${pasteImage}\\` | Paste image from clipboard |\n| \\`/\\` | Slash commands |\n\n**Fullscreen mode (\\`/fullscreen\\`)**\n| Key | Action |\n|-----|--------|\n| \\`${viewportPageUp}\\` / \\`${viewportPageDown}\\` | Scroll transcript by page |\n| \\`${viewportTop}\\` | Scroll to top |\n| \\`${viewportFollow}\\` | Scroll to bottom and follow output |\n| mouse wheel | Scroll transcript |\n| mouse drag | Select and copy text |\n| mouse click on link | Open link in browser |\n`;\n\n\t\tconst shortcuts = this.bindLocalSessionExtensions\n\t\t\t? this.getLocalSessionHost().getExtensionRunner().getShortcuts(this.keybindings.getEffectiveConfig())\n\t\t\t: undefined;\n\t\tif (shortcuts && shortcuts.size > 0) {\n\t\t\thotkeys += `\n**Extensions**\n| Key | Action |\n|-----|--------|\n`;\n\t\t\tfor (const [key, shortcut] of shortcuts) {\n\t\t\t\tconst description = shortcut.description ?? shortcut.extensionPath;\n\t\t\t\thotkeys += `| \\`${formatKeyText(key)}\\` | ${description} |\\n`;\n\t\t\t}\n\t\t}\n\n\t\treturn hotkeys;\n\t}\n\n\tprivate showShortcutGuide(): void {\n\t\tconst hotkeys = this.getShortcutGuide();\n\n\t\tthis.shortcutGuideContainer.clear();\n\t\tthis.shortcutGuideContainer.addChild(new Spacer(1));\n\t\tthis.shortcutGuideContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, this.getMarkdownThemeWithSettings()));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleHotkeysCommand(): void {\n\t\tconst hotkeys = this.getHotkeysGuide();\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, this.getMarkdownThemeWithSettings()));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate clearShortcutGuide(): void {\n\t\tif (this.shortcutGuideContainer.children.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tthis.shortcutGuideContainer.clear();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async handleClearCommand(options: { name?: string; prompt?: string } = {}): Promise<void> {\n\t\tthis.stopWorkingLoader();\n\t\tconst retainedImages = options.prompt ? this.getPromptStashImages(options.prompt) : [];\n\t\tconst restorePrompt = () => {\n\t\t\tif (!options.prompt) return;\n\t\t\tfor (const [id, image] of retainedImages) this.pastedImages.set(id, image);\n\t\t\tthis.editor.setText(options.prompt);\n\t\t};\n\t\tlet created = false;\n\t\ttry {\n\t\t\tconst result = await this.agentConnection.newSession();\n\t\t\tif (result.cancelled) {\n\t\t\t\trestorePrompt();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcreated = true;\n\t\t\tawait this.renderCurrentSessionState();\n\t\t\tfor (const [id, image] of retainedImages) this.pastedImages.set(id, image);\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(`${theme.fg(\"accent\", \"✓ New session started\")}`, 1, 1));\n\t\t\tthis.ui.requestRender();\n\t\t\tconst images = options.prompt ? this.collectImagesFor(options.prompt) : undefined;\n\t\t\tif (options.name) await this.agentConnection.setSessionName(options.name);\n\t\t\tif (options.prompt) {\n\t\t\t\tthis.editor.addToHistory?.(options.prompt);\n\t\t\t\tawait this.agentConnection.prompt(options.prompt, { images });\n\t\t\t}\n\t\t} catch (error: unknown) {\n\t\t\tif (!created) {\n\t\t\t\tawait this.handleFatalRuntimeError(\"Failed to create session\", error);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\trestorePrompt();\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate async handleDebugCommand(): Promise<void> {\n\t\tconst width = this.ui.terminal.columns;\n\t\tconst height = this.ui.terminal.rows;\n\t\tconst allLines = this.ui.render(width);\n\t\ttry {\n\t\t\tconst messages = await this.agentConnection.getMessages();\n\t\t\tconst debugLogPath = getDebugLogPath();\n\t\t\tconst debugData = [\n\t\t\t\t`Debug output at ${new Date().toISOString()}`,\n\t\t\t\t`Terminal: ${width}x${height}`,\n\t\t\t\t`Total lines: ${allLines.length}`,\n\t\t\t\t\"\",\n\t\t\t\t\"=== All rendered lines with visible widths ===\",\n\t\t\t\t...allLines.map((line, idx) => {\n\t\t\t\t\tconst vw = visibleWidth(line);\n\t\t\t\t\tconst escaped = JSON.stringify(line);\n\t\t\t\t\treturn `[${idx}] (w=${vw}) ${escaped}`;\n\t\t\t\t}),\n\t\t\t\t\"\",\n\t\t\t\t\"=== Agent messages (JSONL) ===\",\n\t\t\t\t...messages.map((msg) => JSON.stringify(msg)),\n\t\t\t\t\"\",\n\t\t\t].join(\"\\n\");\n\n\t\t\tfs.mkdirSync(path.dirname(debugLogPath), { recursive: true });\n\t\t\tfs.writeFileSync(debugLogPath, debugData);\n\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(\n\t\t\t\tnew Text(`${theme.fg(\"accent\", \"✓ Debug log written\")}\\n${theme.fg(\"muted\", debugLogPath)}`, 1, 1),\n\t\t\t);\n\t\t\tthis.ui.requestRender();\n\t\t} catch (error: unknown) {\n\t\t\tthis.showError(`Failed to write debug log: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n\n\tprivate handleArminSaysHi(): void {\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new ArminComponent(this.ui));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleDementedDelves(): void {\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new EarendilAnnouncementComponent());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleDaxnuts(): void {\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new DaxnutsComponent(this.ui));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate checkDaxnutsEasterEgg(model: { provider: string; id: string }): void {\n\t\tif (model.provider === \"opencode\" && model.id.toLowerCase().includes(\"kimi-k2.5\")) {\n\t\t\tthis.handleDaxnuts();\n\t\t}\n\t}\n\n\tstop(options: { preserveAltScreen?: boolean } = {}): void {\n\t\tthis.unregisterSignalHandlers();\n\t\tthis.clearCtrlCExitHint({ render: false });\n\t\tthis.clearEscapeRepeat();\n\t\tif (this.settingsManager.getShowTerminalProgress()) {\n\t\t\tthis.ui.terminal.setProgress(false);\n\t\t}\n\t\tthis.stopWorkingLoader();\n\t\tthis.discardRefineLoader();\n\t\tthis.endFeatureHintRun();\n\t\tthis.stopWorkingPulse();\n\t\tthis.stopGoalTrayTimer();\n\t\tthis.closeHeartbeatManager();\n\t\tthis.clearExtensionTerminalInputListeners();\n\t\tthis.footer.dispose();\n\t\tthis.footerDataProvider.dispose();\n\t\tif (this.unsubscribe) {\n\t\t\tthis.unsubscribe();\n\t\t}\n\t\tif (this.isInitialized) {\n\t\t\tthis.ui.stop({\n\t\t\t\tpreserveAltScreen: options.preserveAltScreen,\n\t\t\t\tflushFullscreen: options.preserveAltScreen ? false : undefined,\n\t\t\t});\n\t\t\tthis.isInitialized = false;\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"interactive-mode.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/interactive-mode.ts"],"names":[],"mappings":"AAKA,OAAO,EAGN,KAAK,YAAY,EAMjB,MAAM,eAAe,CAAC;AAavB,OAAO,EAEN,KAAK,SAAS,EAcd,MAAM,gBAAgB,CAAC;AAsFxB,OAAO,KAAK,EACX,eAAe,EAkBf,oBAAoB,EAEpB,MAAM,8BAA8B,CAAC;AA6EtC,OAAO,KAAK,EACX,+BAA+B,EAE/B,yBAAyB,EACzB,MAAM,gCAAgC,CAAC;AAExC,OAAO,KAAK,EAAE,sBAAsB,EAAiC,MAAM,yBAAyB,CAAC;AAoCrG,eAAO,MAAM,WAAW,0MAMd,CAAC;AAEX,wBAAgB,kBAAkB,CAAC,MAAM,eAAc,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAErF;AAUD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,WAAW,GAAG,MAAM,CAEnG;AAED,wBAAgB,yBAAyB,CACxC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,UAAU,GAAG,WAAW,EAC/B,wBAAwB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GACjD,MAAM,CAKR;AAgDD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAWnD;AAqBD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAsBvE;AAED,MAAM,WAAW,uBAAuB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,wBAAwB;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,SAAS,uBAAuB,EAAE,CAAC;IAC5D,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED,qBAAa,iBAAkB,YAAW,SAAS;IAOjD,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAVzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAW;IACnC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAK;IAC5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAK;IAEhC,YACkB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,MAAM,GAAG,SAAS,EACpC,MAAM,EAAE,MAAM,MAAM,EACpB,mBAAmB,CAAC,oBAAQ,EAC5B,OAAO,GAAE,wBAA6B,EAIvD;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAmD9B;CACD;AA6MD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA8BtE;AAWD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAM1G;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,gBAAgB,EAAE,MAAM,GAAG,MAAM,EAAE,CAEhG;AAED,wBAAgB,wCAAwC,CACvD,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,sBAAsB,EAAE,MAAM,GAC5B,MAAM,CAGR;AAED,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACtC,gEAAgE;IAChE,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,4DAA4D;IAC5D,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wCAAwC;IACxC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,aAAa,CAAC,EAAE,YAAY,EAAE,CAAC;IAC/B,uEAAuE;IACvE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,2EAA2E;IAC3E,cAAc,CAAC,EAAE,wBAAwB,EAAE,CAAC;IAC5C,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yGAAyG;IACzG,eAAe,EAAE,eAAe,CAAC;IACjC,iFAAiF;IACjF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,+BAA+B,CAAC;IACnD,qGAAqG;IACrG,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,uHAAuH;IACvH,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,0HAA0H;IAC1H,UAAU,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,sGAAsG;IACtG,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,0EAA0E;IAC1E,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;IAC1C,kEAAkE;IAClE,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,aAAa,GAAG,oBAAoB,CAAC;IAC3C,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,iBAAiB,GAAG,aAAa,GAAG,WAAW,GAAG,aAAa,GAAG,KAAK,CAAC,CAAC;CAC5G;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAGzG;AAED,qBAAa,eAAe;IA8Nf,OAAO,CAAC,OAAO;IA7N3B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAQ;IACrD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,uBAAuB,CAAO;IAEtD,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,gBAAgB,CAA8C;IACtE,OAAO,CAAC,0BAA0B,CAAU;IAC5C,OAAO,CAAC,EAAE,CAAM;IAChB,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,sBAAsB,CAAY;IAC1C,OAAO,CAAC,wBAAwB,CAAY;IAC5C,OAAO,CAAC,eAAe,CAAY;IACnC,OAAO,CAAC,uBAAuB,CAAY;IAC3C,OAAO,CAAC,qBAAqB,CAAY;IACzC,OAAO,CAAC,oBAAoB,CAAY;IACxC,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqC;IACtE,OAAO,CAAC,oBAAoB,CAAqB;IACjD,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,sBAAsB,CAA4B;IAC1D,OAAO,CAAC,oBAAoB,CAAmC;IAC/D,OAAO,CAAC,4BAA4B,CAAqC;IACzE,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,iBAAiB,CAAY;IAErC,OAAO,CAAC,UAAU,CAAY;IAE9B,OAAO,CAAC,UAAU,CAAY;IAC9B,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,eAAe,CAAY;IACnC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,kBAAkB,CAAqB;IAE/C,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAClD,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAAC,CAAqC;IAC7D,OAAO,CAAC,sBAAsB,CAAiC;IAC/D,OAAO,CAAC,uBAAuB,CAA0B;IACzD,OAAO,CAAC,2BAA2B,CAA0B;IAC7D,OAAO,CAAC,yBAAyB,CAAK;IACtC,OAAO,CAAC,uBAAuB,CAAK;IACpC,OAAO,CAAC,0BAA0B,CAAwD;IAC1F,OAAO,CAAC,QAAQ,CAAC,6BAA6B,CAAsC;IACpF,OAAO,CAAC,0BAA0B,CAA2D;IAC7F,OAAO,CAAC,iBAAiB,CAA+C;IACxE,OAAO,CAAC,gBAAgB,CAAiC;IACzD,OAAO,CAAC,cAAc,CAAiC;IACvD,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,uBAAuB,CAAiD;IAChF,OAAO,CAAC,gBAAgB,CAAiC;IAEzD,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,YAAY,CAAyC;IAC7D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyB;IACzD,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,gBAAgB,CAA6B;IACrD,OAAO,CAAC,yBAAyB,CAA6B;IAC9D,OAAO,CAAC,oBAAoB,CAAmC;IAC/D,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,4BAA4B,CAAS;IAC7C,OAAO,CAAC,UAAU,CAAyC;IAC3D,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA8B;IAG9D,OAAO,CAAC,yBAAyB,CAAK;IAEtC,OAAO,CAAC,mBAAmB,CAA+C;IAC1E,OAAO,CAAC,QAAQ,CAAC,0BAA0B,CAAiB;IAC5D,OAAO,CAAC,mBAAmB,CAAmC;IAE9D,OAAO,CAAC,sBAAsB,CAAK;IACnC,OAAO,CAAC,kBAAkB,CAAwD;IAClF,OAAO,CAAC,kBAAkB,CAA+B;IACzD,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,iBAAiB,CAAwD;IACjF,OAAO,CAAC,iCAAiC,CAAS;IAElD,OAAO,CAAC,gBAAgB,CAAiC;IACzD,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,oBAAoB,CAAmD;IAC/E,OAAO,CAAC,aAAa,CAAyC;IAE9D,OAAO,CAAC,kBAAkB,CAAoD;IAC9E,OAAO,CAAC,gBAAgB,CAA2C;IACnE,OAAO,CAAC,qBAAqB,CAAoC;IACjE,OAAO,CAAC,iBAAiB,CAA+C;IACxE,OAAO,CAAC,iBAAiB,CAA0C;IACnE,OAAO,CAAC,oBAAoB,CAAqB;IAKjD,OAAO,CAAC,gBAAgB,CAAwE;IAGhG,OAAO,CAAC,yBAAyB,CAAqC;IAItE,OAAO,CAAC,yBAAyB,CAAqB;IAGtD,OAAO,CAAC,mBAAmB,CAAiD;IAC5E,OAAO,CAAC,qBAAqB,CAAgC;IAG7D,OAAO,CAAC,iBAAiB,CAAoC;IAC7D,OAAO,CAAC,sBAAsB,CAAK;IACnC,OAAO,CAAC,mBAAmB,CAAoC;IAE/D,OAAO,CAAC,YAAY,CAA6C;IACjE,OAAO,CAAC,qBAAqB,CAA6C;IAC1E,OAAO,CAAC,4BAA4B,CAA+C;IACnF,OAAO,CAAC,oBAAoB,CAAqB;IACjD,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,mBAAmB,CAA0D;IACrF,OAAO,CAAC,mBAAmB,CAAwC;IAGnE,OAAO,CAAC,mBAAmB,CAAsB;IACjD,OAAO,CAAC,iBAAiB,CAA2D;IACpF,OAAO,CAAC,SAAS,CAAqB;IAEtC,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,iBAAiB,CAAS;IAElC,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAGtC;IAEH,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,kBAAkB,CAAqC;IAC/D,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,sBAAsB,CAA8B;IAC5D,OAAO,CAAC,6BAA6B,CAAqB;IAC1D,OAAO,CAAC,yBAAyB,CAAK;IACtC,OAAO,CAAC,8BAA8B,CAAK;IAC3C,OAAO,CAAC,+BAA+B,CAA4E;IACnH,OAAO,CAAC,eAAe,CAAmC;IAC1D,OAAO,CAAC,0BAA0B,CAA8C;IAChF,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,UAAU,CAAkC;IACpD,OAAO,CAAC,uBAAuB,CAA4B;IAC3D,OAAO,CAAC,yBAAyB,CAAS;IAC1C,OAAO,CAAC,gBAAgB,CAAwC;IAChE,OAAO,CAAC,sBAAsB,CAA4B;IAC1D,OAAO,CAAC,4BAA4B,CAA4C;IAOhF,OAAO,CAAC,YAAY,CAAmC;IACvD,OAAO,CAAC,iBAAiB,CAAK;IAE9B,OAAO,CAAC,WAAW,CAAC,CAAa;IACjC,OAAO,CAAC,qBAAqB,CAAyB;IAEtD,OAAO,CAAC,oBAAoB,CAAiC;IAC7D,OAAO,CAAC,YAAY,CAAiC;IAErD,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,cAAc,CAAyC;IAG/D,OAAO,CAAC,eAAe,CAA6D;IACpF,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwB;IACvD,OAAO,CAAC,4BAA4B,CAAS;IAC7C,OAAO,CAAC,kBAAkB,CAAoC;IAC9D,OAAO,CAAC,gBAAgB,CAAqB;IAE7C,OAAO,CAAC,iBAAiB,CAAS;IAElC,OAAO,CAAC,iBAAiB,CAAqD;IAC9E,OAAO,CAAC,cAAc,CAAkD;IACxE,OAAO,CAAC,eAAe,CAAmD;IAC1E,OAAO,CAAC,mCAAmC,CAAyB;IACpE,OAAO,CAAC,mCAAmC,CAAkD;IAE7F,OAAO,CAAC,qBAAqB,CAAuD;IACpF,OAAO,CAAC,qBAAqB,CAAuD;IACpF,OAAO,CAAC,oBAAoB,CAAa;IACzC,OAAO,CAAC,oBAAoB,CAAa;IAGzC,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,YAAY,CAAqB;IAEzC,OAAO,CAAC,YAAY,CAA6D;IAEjF,OAAO,CAAC,eAAe,CAAY;IAEnC,OAAO,CAAC,aAAa,CAAoC;IAEzD,OAAO,CAAC,YAAY,CAA6D;IAEjF,OAAO,CAAC,mBAAmB;IAM3B,OAAO,KAAK,eAAe,GAE1B;IACD,OAAO,KAAK,aAAa,GAExB;IAED,YAAoB,OAAO,EAAE,sBAAsB,EAgFlD;IAED,OAAO,KAAK,WAAW,GAEtB;IAED,OAAO,KAAK,WAAW,QAEtB;IAED,OAAO,CAAC,kBAAkB;IAa1B,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,yBAAyB;IA0BjC,OAAO,CAAC,kCAAkC;IAU1C,OAAO,CAAC,wBAAwB;IA8BhC,OAAO,CAAC,0BAA0B;IAKlC,OAAO,CAAC,oCAAoC;IAgB5C,OAAO,CAAC,wBAAwB;IAIhC,OAAO,CAAC,8BAA8B;IA2EtC,OAAO,CAAC,yBAAyB;IAa3B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA8G1B;IAED,OAAO,CAAC,mBAAmB;IAUrB,GAAG,IAAI,OAAO,CAAC,wBAAwB,CAAC,CA8M7C;IAED,OAAO,CAAC,6BAA6B;IAarC,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,iCAAiC;IAIzC,OAAO,CAAC,mBAAmB;YAMb,oBAAoB;YAYpB,4BAA4B;YAQ5B,iBAAiB;IA8B/B,OAAO,CAAC,4BAA4B;IAOpC,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,0BAA0B;IAMlC,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,wBAAwB;IAIhC,OAAO,CAAC,YAAY;IA6BpB,OAAO,CAAC,mBAAmB;IAU3B,OAAO,CAAC,4BAA4B;IAcpC,OAAO,CAAC,wBAAwB;IAqBhC,OAAO,CAAC,6BAA6B;IAOrC,OAAO,CAAC,kCAAkC;IA2B1C,OAAO,CAAC,yBAAyB;IAgCjC,OAAO,CAAC,oBAAoB;IA6B5B,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,eAAe;IAKvB,OAAO,CAAC,gBAAgB;IAqCxB,OAAO,CAAC,iBAAiB;IAkCzB,OAAO,CAAC,qBAAqB;IAiB7B,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,iBAAiB;IA8DzB,OAAO,CAAC,mBAAmB;YAyOb,4BAA4B;IAiF1C,OAAO,CAAC,oBAAoB;YAkBd,sBAAsB;IAIpC,OAAO,CAAC,sBAAsB;YAiBhB,wBAAwB;IAetC,OAAO,CAAC,uBAAuB;IAsB/B,OAAO,CAAC,qBAAqB;IAK7B,OAAO,CAAC,sBAAsB;IAmB9B,OAAO,CAAC,4BAA4B;IAcpC,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,2BAA2B;YAgBrB,6BAA6B;IAoB3C,OAAO,CAAC,8BAA8B;IA+CtC,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,mBAAmB;IAY3B,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,yBAAyB;IAsBjC,OAAO,CAAC,mBAAmB;YAIb,oBAAoB;YA0BpB,uBAAuB;IAQrC,OAAO,CAAC,8BAA8B;IAmDtC,OAAO,CAAC,qBAAqB;YAOf,yBAAyB;YAazB,sCAAsC;YAStC,qBAAqB;IAsCnC,OAAO,CAAC,uBAAuB;YAIjB,kBAAkB;IAahC,OAAO,CAAC,0BAA0B;IAMlC,OAAO,CAAC,4BAA4B;YAUtB,+BAA+B;IAsD7C,OAAO,CAAC,6BAA6B;YA6BvB,sBAAsB;IAmBpC,OAAO,CAAC,uBAAuB;IA+C/B,OAAO,CAAC,kBAAkB;IAK1B,OAAO,CAAC,uBAAuB;IAyB/B,OAAO,CAAC,mBAAmB;IAU3B,OAAO,CAAC,oBAAoB;IAoB5B,OAAO,CAAC,0BAA0B;IAIlC,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,4BAA4B;IAiBpC,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,4BAA4B;IAoBpC,OAAO,CAAC,eAAe;IAiCvB,OAAO,CAAC,4BAA4B;IAgBpC,OAAO,CAAC,6BAA6B;IAWrC,OAAO,CAAC,yBAAyB;IAKjC,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,qBAAqB;IAc7B,OAAO,CAAC,kBAAkB;IAY1B,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,uBAAuB;IAS/B,OAAO,CAAC,qBAAqB;IA6B7B,8EAA8E;IAC9E,OAAO,CAAC,iBAAiB;IAazB,OAAO,CAAC,gBAAgB;IAOxB,yEAAyE;IACzE,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,iBAAiB;IAgCzB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,mBAAmB;IAM3B,OAAO,CAAC,sBAAsB;IAa9B,OAAO,CAAC,kBAAkB;IAwC1B,OAAO,CAAC,qBAAqB;IAY7B,OAAO,CAAC,gBAAgB;IAiCxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAM;IAE9C,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,WAAW;IAmBnB,OAAO,CAAC,qBAAqB;IAuB7B,OAAO,CAAC,kBAAkB;IA0B1B,OAAO,CAAC,kBAAkB;IAsC1B,OAAO,CAAC,iCAAiC;IAWzC,OAAO,CAAC,oCAAoC;IAO5C,OAAO,CAAC,wBAAwB;IAyDhC,OAAO,CAAC,qBAAqB;IAwC7B,OAAO,CAAC,qBAAqB;YASf,oBAAoB;YASpB,0BAA0B;IAQxC,OAAO,CAAC,kBAAkB;IAwC1B,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,mBAAmB;IAwB3B,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,wBAAwB;IAgGhC,OAAO,CAAC,mBAAmB;YAWb,mBAAmB;IA8EjC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAkB1B,OAAO,CAAC,gBAAgB;IAgExB,OAAO,CAAC,uBAAuB;IAW/B,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,wBAAwB;IAShC,OAAO,CAAC,uBAAuB;IAY/B,OAAO,CAAC,iBAAiB;IAiBzB,OAAO,CAAC,+BAA+B;IAoBvC,OAAO,CAAC,oBAAoB;IAkB5B,OAAO,CAAC,yBAAyB;IA0DjC,OAAO,CAAC,oBAAoB;YAWd,yBAAyB;IAqCvC;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAO3B;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAoB1B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,kBAAkB;YAIZ,kBAAkB;IA+ChC,OAAO,CAAC,uBAAuB;IAgB/B,OAAO,CAAC,sBAAsB;IAgC9B,OAAO,CAAC,iBAAiB;IA2BzB,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,iBAAiB;YAeX,oBAAoB;IAUlC,OAAO,CAAC,wBAAwB;IA8chC,OAAO,CAAC,gBAAgB;YAiEV,kCAAkC;IAsChD,OAAO,CAAC,oCAAoC;IAS5C,OAAO,CAAC,yCAAyC;YAenC,mCAAmC;YAiHnC,WAAW;IA8azB,OAAO,CAAC,8BAA8B;IAmBtC,OAAO,CAAC,iCAAiC;IAYzC,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,mBAAmB;IAc3B,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,2BAA2B;IAInC,OAAO,CAAC,wBAAwB;IAUhC,OAAO,CAAC,wBAAwB;IA+BhC,OAAO,CAAC,gBAAgB;IAmCxB,OAAO,CAAC,sBAAsB;IAY9B,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,sBAAsB;IAW9B,OAAO,CAAC,qBAAqB;IAU7B,OAAO,CAAC,sBAAsB;IAS9B,OAAO,CAAC,yBAAyB;IAYjC,OAAO,CAAC,sBAAsB;IAO9B,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,oBAAoB;YAOd,oBAAoB;IASlC,OAAO,CAAC,+BAA+B;IAsBvC,OAAO,CAAC,oBAAoB;IAY5B,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,qBAAqB;IAS7B,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,qBAAqB;IAO7B,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,gBAAgB;IAoBxB,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,kCAAkC;IAkB1C,OAAO,CAAC,qBAAqB;IAU7B,OAAO,CAAC,gCAAgC;IAWxC,OAAO,CAAC,gCAAgC;IAWxC;;;;;OAKG;IACH,OAAO,CAAC,UAAU;YAoBJ,uBAAuB;IAarC,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,yBAAyB;IAUjC,OAAO,CAAC,qCAAqC;IAkC7C,OAAO,CAAC,gBAAgB;IA4HxB;;;;;;;OAOG;IACH,OAAO,CAAC,0BAA0B;YAoBpB,oBAAoB;IA4H5B,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAuB3C;YAEa,mCAAmC;IAYjD,OAAO,CAAC,uCAAuC;IAczC,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAUhD;YAEa,uBAAuB;IAKrC,OAAO,CAAC,YAAY;IAqBpB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,qBAAqB;IAuB7B,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,kBAAkB;IAe1B,OAAO,CAAC,sBAAsB;IAI9B,OAAO,CAAC,WAAW;IAInB;;;;OAIG;IACH,OAAO,CAAC,cAAc,CAAS;YAEjB,QAAQ;IA0BtB;;;;;OAKG;IACG,iBAAiB,CAAC,OAAO,GAAE;QAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAKpF;IAED,OAAO,CAAC,gBAAgB;YAYV,iBAAiB;YAQjB,kBAAkB;IAyBhC,OAAO,CAAC,qBAAqB;YAYf,sBAAsB;IAKpC,OAAO,CAAC,sBAAsB;IAgC9B,OAAO,CAAC,wBAAwB;IAOhC,OAAO,CAAC,WAAW;YAyCL,cAAc;IAgC5B,OAAO,CAAC,oBAAoB;IAQ5B,iGAAiG;IACjG,OAAO,CAAC,oBAAoB;IAS5B,OAAO,CAAC,kBAAkB;IAkD1B;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAkH3B;;;OAGG;IACH,OAAO,CAAC,yBAAyB;IASjC,OAAO,CAAC,+BAA+B;IASvC,OAAO,CAAC,uBAAuB;IAe/B,OAAO,CAAC,uBAAuB;IAO/B,OAAO,CAAC,0BAA0B;IAIlC,OAAO,CAAC,uBAAuB;IAI/B,kFAAkF;IAClF,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,iBAAiB;IAiBzB,OAAO,CAAC,yBAAyB;IAIjC,OAAO,CAAC,2BAA2B;IAKnC,OAAO,CAAC,uBAAuB;IAK/B,OAAO,CAAC,gBAAgB;IAKxB,yFAAyF;IACzF,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,kBAAkB;IA2B1B,OAAO,CAAC,6BAA6B;IAqBrC,OAAO,CAAC,kBAAkB;IAoD1B,OAAO,CAAC,aAAa;IASrB,SAAS,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAIpC;IAED,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAIxC;IAED,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAGnD;IAED,6BAA6B,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAGtD;IAED,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,4BAA4B;IAoCpC,OAAO,CAAC,0BAA0B;IAQlC;;;OAGG;IACH,OAAO,CAAC,YAAY;IAapB,OAAO,CAAC,mBAAmB;YAIb,oBAAoB;YAmLpB,kBAAkB;YAsBlB,mBAAmB;YAkBnB,kBAAkB;YAyBlB,sBAAsB;YAQtB,6BAA6B;IA0B3C,OAAO,CAAC,yBAAyB;IAIjC,OAAO,CAAC,2BAA2B;YAMrB,4BAA4B;YA0B5B,yBAAyB;IAKvC,OAAO,CAAC,wBAAwB;IAWhC,OAAO,CAAC,8BAA8B;IAgBtC,OAAO,CAAC,gCAAgC;IAKxC,OAAO,CAAC,0BAA0B;YAOpB,sCAAsC;YAKtC,kBAAkB;IAahC,OAAO,CAAC,2BAA2B;YAyBrB,4BAA4B;YAM5B,uCAAuC;IAiBrD,OAAO,CAAC,0BAA0B;IAMlC,OAAO,CAAC,2BAA2B;IAenC,OAAO,CAAC,+BAA+B;IAUvC,OAAO,CAAC,4BAA4B;IAKpC,OAAO,CAAC,iBAAiB;IA0CzB,OAAO,CAAC,mBAAmB;IAkB3B,OAAO,CAAC,oBAAoB;IAuB5B,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,qBAAqB;YAmIf,kBAAkB;YA8ElB,uBAAuB;YA+CvB,kBAAkB;YAsBlB,gBAAgB;YAkIhB,mBAAmB;YAwBnB,mBAAmB;IAyCjC,OAAO,CAAC,oBAAoB;IAiD5B,OAAO,CAAC,eAAe;YAmBT,kCAAkC;YAuClC,gBAAgB;YA8DhB,oBAAoB;YAapB,kBAAkB;YASlB,mBAAmB;YAiHnB,mBAAmB;YAsFnB,mBAAmB;IAgBjC,OAAO,CAAC,sBAAsB;YA6BhB,mBAAmB;YA8CnB,kBAAkB;YA8FlB,iBAAiB;YAejB,iBAAiB;YAoBjB,wBAAwB;YAmDxB,oBAAoB;IAuBlC,OAAO,CAAC,iBAAiB;YAiCX,yBAAyB;YAWzB,oBAAoB;YAgBpB,sBAAsB;YA+DtB,oBAAoB;IAyBlC,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,+BAA+B;YA4BzB,eAAe;IAqB7B,OAAO,CAAC,aAAa;IA0BrB,OAAO,CAAC,sBAAsB;IAqB9B,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,gBAAgB;IA8BxB,OAAO,CAAC,eAAe;IA0HvB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,oBAAoB;IAQ5B,OAAO,CAAC,kBAAkB;YAQZ,kBAAkB;YAqClB,kBAAkB;IAqChC,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,qBAAqB;IAM7B,IAAI,CAAC,OAAO,GAAE;QAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,IAAI,CA0BxD;CACD","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { AgentMessage, ThinkingLevel } from \"@asm-agent/agent\";\nimport {\n\ttype Api,\n\ttype AssistantMessage,\n\ttype ImageContent,\n\ttype Message,\n\ttype Model,\n\ttype ServiceTier,\n\tsupportsFastMode,\n\ttype ToolCall,\n} from \"@asm-agent/ai\";\nimport { BUILTIN_MCP_CATALOG } from \"@asm-agent/ai/mcp\";\nimport type {\n\tAutocompleteItem,\n\tAutocompleteProvider,\n\tEditorComponent,\n\tKeybinding,\n\tKeyId,\n\tMarkdownTheme,\n\tOverlayHandle,\n\tOverlayOptions,\n\tSlashCommand,\n} from \"@asm-agent/tui\";\nimport {\n\tCombinedAutocompleteProvider,\n\ttype Component,\n\tContainer,\n\tLoader,\n\ttype LoaderIndicatorOptions,\n\tMarkdown,\n\tmatchesKey,\n\tProcessTerminal,\n\tSpacer,\n\tsetKeybindings,\n\tText,\n\tTruncatedText,\n\tTUI,\n\ttruncateToWidth,\n\tvisibleWidth,\n} from \"@asm-agent/tui\";\nimport { spawn, spawnSync } from \"child_process\";\nimport {\n\tbuildDaemonUpdateRestartReport,\n\tlaunchDaemonUpdateRestartCoordinator,\n\tresolveDaemonUpdateRestartSocketPath,\n} from \"../../cli/daemon-update-restart.js\";\nimport {\n\tAPP_NAME,\n\tAPP_TITLE,\n\tgetAgentDir,\n\tgetDebugLogPath,\n\tgetLogsDir,\n\tgetShareViewerUrl,\n\tSELF_UPDATE_INTERACTIVE_CHILD_ENV,\n\tSELF_UPDATE_NOT_ATTEMPTED_EXIT_CODE,\n\tVERSION,\n} from \"../../config.js\";\nimport {\n\tAGENT_MESSAGE_RECEIVED_PREVIEW_LABEL,\n\tisAgentSessionMessage,\n\tstartsAgentRun,\n} from \"../../core/agent-messages.js\";\nimport { isNoModelsAvailableMessage } from \"../../core/auth-guidance.js\";\nimport {\n\ttype AgentCronJob,\n\ttype AgentHeartbeatManagementAction,\n\tDEFAULT_HEARTBEAT_DELIVERY_MODE,\n\tparseHeartbeatCommand,\n} from \"../../core/cron-jobs.js\";\nimport type {\n\tAutocompleteProviderFactory,\n\tContextUsage,\n\tEditorFactory,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tExtensionRunner,\n\tExtensionUIContext,\n\tExtensionUIDialogOptions,\n\tExtensionWidgetOptions,\n} from \"../../core/extensions/index.js\";\nimport { FooterDataProvider, type ReadonlyFooterDataProvider } from \"../../core/footer-data-provider.js\";\nimport { emptyGoalState, formatGoalUsage, GOAL_CONTEXT_PREVIEW_LABEL, type GoalState } from \"../../core/goals.js\";\nimport type { KernelSentAgentMessage } from \"../../core/kernel/index.js\";\nimport { type AppKeybinding, KeybindingsManager } from \"../../core/keybindings.js\";\nimport { runMcpManagementCommand } from \"../../core/mcp/mcp-command.js\";\nimport {\n\tbashOutputToText,\n\tCOMPACTION_OUTCOME_CUSTOM_TYPE,\n\ttype CustomMessage,\n\tcreateHeartbeatPromptMessage,\n\tHEARTBEAT_PROMPT_PREVIEW_LABEL,\n\tisCompactionOutcomeMessage,\n\tisRefinementOutcomeMessage,\n\tisSessionSlashCommandMessage,\n\tisSessionSlashCommandResultMessage,\n\tREFINEMENT_OUTCOME_CUSTOM_TYPE,\n\tSESSION_SLASH_COMMAND_CUSTOM_TYPE,\n\tSESSION_SLASH_COMMAND_RESULT_CUSTOM_TYPE,\n} from \"../../core/messages.js\";\nimport { findExactModelReferenceMatch, resolveModelScopeFromModels } from \"../../core/model-resolver.js\";\nimport { parseNewSessionCommand } from \"../../core/new-session-command.js\";\nimport { resolvePrimeInferencePostLoginModelAction } from \"../../core/prime-inference-model-selection.js\";\nimport { parseCommandArgs } from \"../../core/prompt-templates.js\";\nimport { formatMissingSessionCwdPrompt, MissingSessionCwdError } from \"../../core/session-cwd.js\";\nimport { SessionImportFileNotFoundError } from \"../../core/session-import-errors.js\";\nimport { resolveSessionPath, SessionSelectorError, SessionSelectorNotFoundError } from \"../../core/session-resolver.js\";\nimport { parseSkillBlock } from \"../../core/skill-blocks.js\";\nimport {\n\tBUILTIN_SLASH_COMMANDS,\n\tbuiltinSlashCommandTakesArgument,\n\tisBuiltinSlashCommandName,\n\tparseSlashCommand,\n\tresolveBuiltinSlashCommandName,\n} from \"../../core/slash-commands.js\";\nimport { type TruncationResult, truncateTail } from \"../../core/tools/truncate.js\";\nimport { ASM_LOGO } from \"../../themes/asm-logo.js\";\nimport { getChangelogPath, parseChangelog } from \"../../utils/changelog.js\";\nimport { copyToClipboard } from \"../../utils/clipboard.js\";\nimport { readClipboardImage } from \"../../utils/clipboard-image.js\";\nimport { parseGitUrl } from \"../../utils/git.js\";\nimport { resizeImage } from \"../../utils/image-resize.js\";\nimport { getCwdRelativePath } from \"../../utils/paths.js\";\nimport { killTrackedDetachedChildren } from \"../../utils/shell.js\";\nimport { ensureTool, ensureToolWithStatus, formatMissingRipgrepMessage } from \"../../utils/tools-manager.js\";\nimport { checkForNewPiVersion } from \"../../utils/version-check.js\";\nimport type {\n\tAgentConnection,\n\tAgentConnectionExtensionUiRequest,\n\tAgentConnectionExtensionUiResponse,\n\tAgentConnectionHeartbeat,\n\tAgentConnectionModel,\n\tAgentConnectionModelCatalog,\n\tAgentConnectionQueuedMessageMutationStatus,\n\tAgentConnectionQueueState,\n\tAgentConnectionResourceDiagnostic,\n\tAgentConnectionResourceSnapshot,\n\tAgentConnectionRlmChildAgentSnapshot,\n\tAgentConnectionSessionContext,\n\tAgentConnectionSessionEvent,\n\tAgentConnectionSessionTreeNode,\n\tAgentConnectionSideQuestionEvent,\n\tAgentConnectionSlashCommand,\n\tAgentConnectionSnapshot,\n\tAgentConnectionSourceInfo,\n\tAgentConnectionState,\n\tAgentConnectionToolDefinition,\n} from \"../agent-connection/index.js\";\nimport { AgentConnectionPromptAdmissionError } from \"../agent-connection/index.js\";\nimport { getModelArgumentCompletions } from \"../model-autocomplete.js\";\nimport {\n\tcheckForPackageUpdates,\n\tcheckTmuxKeyboardSetup,\n\tformatPackageUpdateNotice,\n\tformatUpdateAvailableNotice,\n} from \"../shared/startup-notices.js\";\nimport { AGENT_ACTIVITY_LABELS, AgentActivityTracker, formatTokenCount } from \"./agent-activity.js\";\nimport { type AuthenticationResult, getAnthropicSubscriptionAuthWarning, ProviderAuthFlows } from \"./auth-flows.js\";\nimport { AgentMessageComponent } from \"./components/agent-message.js\";\nimport { ArminComponent } from \"./components/armin.js\";\nimport { AssistantMessageComponent } from \"./components/assistant-message.js\";\nimport { BashExecutionComponent } from \"./components/bash-execution.js\";\nimport { BorderedLoader } from \"./components/bordered-loader.js\";\nimport { BranchSummaryMessageComponent } from \"./components/branch-summary-message.js\";\nimport { type FullPaneOverlayOptions, showFullPaneOverlay } from \"./components/centered-overlay.js\";\nimport {\n\tCompactionOutcomeMessageComponent,\n\tMalformedCompactionOutcomeMessageComponent,\n} from \"./components/compaction-outcome-message.js\";\nimport { CompactionSummaryMessageComponent } from \"./components/compaction-summary-message.js\";\nimport { ConfigurationMenuComponent, type ConfigurationMenuTab } from \"./components/configuration-menu.js\";\nimport { formatContextTree } from \"./components/context-tree-format.js\";\nimport { isCompactAgentMessageNeighbor } from \"./components/conversation-components.js\";\nimport { CountdownTimer } from \"./components/countdown-timer.js\";\nimport { CustomEditor } from \"./components/custom-editor.js\";\nimport { CustomMessageComponent } from \"./components/custom-message.js\";\nimport { DaxnutsComponent } from \"./components/daxnuts.js\";\nimport { DynamicBorder } from \"./components/dynamic-border.js\";\nimport { EarendilAnnouncementComponent } from \"./components/earendil-announcement.js\";\nimport { type FileChangeSummary, formatTotalChangeSummary, mergeTurnFileChanges } from \"./components/edit-summary.js\";\nimport { ExtensionEditorComponent } from \"./components/extension-editor.js\";\nimport { ExtensionInputComponent } from \"./components/extension-input.js\";\nimport { ExtensionSelectorComponent } from \"./components/extension-selector.js\";\nimport { FEATURE_HINT_ANIMATION_INTERVAL_MS, FeatureHintComponent } from \"./components/feature-hint.js\";\nimport { FooterComponent } from \"./components/footer.js\";\nimport { HeartbeatManagerComponent } from \"./components/heartbeat-manager.js\";\nimport { InjectedPromptMessageComponent, isInjectedPromptMessage } from \"./components/injected-prompt-message.js\";\nimport { formatKeyText, keyHint, keyText, rawKeyHint } from \"./components/keybinding-hints.js\";\nimport { createMermaidMarkdownTransform } from \"./components/mermaid.js\";\nimport type { AuthSelectorProvider } from \"./components/oauth-selector.js\";\nimport { OnboardingSplashComponent } from \"./components/onboarding-splash.js\";\nimport {\n\tMalformedRefinementOutcomeMessageComponent,\n\tRefinementOutcomeMessageComponent,\n} from \"./components/refinement-outcome-message.js\";\nimport { ScopedModelsSelectorComponent } from \"./components/scoped-models-selector.js\";\nimport { SettingsSelectorComponent } from \"./components/settings-selector.js\";\nimport { SideQuestionComponent } from \"./components/side-question.js\";\nimport { SkillInvocationMessageComponent } from \"./components/skill-invocation-message.js\";\nimport {\n\tisLeadingSlashCommand,\n\tSlashCommandMessageComponent,\n\tstyleSlashCommandText,\n} from \"./components/slash-command-message.js\";\nimport { SlashCommandResultMessageComponent } from \"./components/slash-command-result-message.js\";\nimport { countDirectSubagentStatuses, SubagentSummaryLine } from \"./components/subagent-summary-line.js\";\nimport { ThinkingSelectorComponent } from \"./components/thinking-selector.js\";\nimport {\n\tselectLatestToolExpandHint,\n\tToolExecutionComponent,\n\ttype ToolExecutionDefinition,\n} from \"./components/tool-execution.js\";\nimport { TreeSelectorComponent } from \"./components/tree-selector.js\";\nimport { UserMessageComponent } from \"./components/user-message.js\";\nimport { UserMessageSelectorComponent } from \"./components/user-message-selector.js\";\nimport { FeatureHintDeck } from \"./feature-hints.js\";\nimport { scopeHeartbeatsToSession } from \"./heartbeat-scope.js\";\nimport {\n\tcollectMarkedImages,\n\tevictImagesToBudget,\n\tformatImageMarker,\n\timageMarkerIds,\n\tremapImageMarkers,\n} from \"./image-markers.js\";\nimport type {\n\tInteractiveModeLocalSessionHost,\n\tInteractiveModeLocalToolRendererDefinition,\n\tInteractiveModeUiServices,\n} from \"./interactive-mode-services.js\";\nimport { type OnboardingStartupState, shouldRunOnboarding, shouldRunPrimeCliOnboardingSplash } from \"./onboarding.js\";\nimport type { ClientPromptStashStore, PromptStash, PromptStashState } from \"./prompt-stash-state.js\";\nimport { QueueSelection } from \"./queue-selection.js\";\nimport { formatResumeHint } from \"./resume-hint.js\";\nimport {\n\tgetAvailableThemes,\n\tgetAvailableThemesWithPaths,\n\tgetEditorTheme,\n\tgetMarkdownTheme,\n\tgetThemeByName,\n\tinitTheme,\n\tonThemeChange,\n\tsetRegisteredThemes,\n\tsetTheme,\n\tsetThemeInstance,\n\tstopThemeWatcher,\n\tTheme,\n\ttype ThemeColor,\n\ttheme,\n} from \"./theme/theme.js\";\nimport { setWorkingPulseFrame, WORKING_ICON_INTERVAL_MS } from \"./theme/working-icon.js\";\n\ninterface Expandable {\n\tsetExpanded(expanded: boolean): void;\n}\n\ninterface PendingToolCallRenderInput {\n\tid: string;\n\tname: string;\n\targuments: ToolCall[\"arguments\"];\n}\n\nconst HEARTBEAT_LEGACY_PROMPT_MIN_TOLERANCE_MS = 15_000;\nconst HEARTBEAT_LEGACY_PROMPT_MAX_TOLERANCE_MS = 120_000;\nconst MODEL_CATALOG_REFRESH_TTL_MS = 60_000;\nconst FEATURE_HINT_DELAY_MS = 5_000;\n\nexport const START_HINTS = [\n\t'Try \"refactor @<filepath>\"',\n\t'Try \"fix bugs in @<filepath>\"',\n\t'Try \"add tests for @<filepath>\"',\n\t'Try \"explain how @<filepath> works\"',\n\t'Try \"improve performance in @<filepath>\"',\n] as const;\n\nexport function getRandomStartHint(random = Math.random): (typeof START_HINTS)[number] {\n\treturn START_HINTS[Math.floor(random() * START_HINTS.length)] ?? START_HINTS[0];\n}\n\nfunction isLabeledQueuedPreview(message: string): boolean {\n\treturn (\n\t\tmessage.startsWith(`${HEARTBEAT_PROMPT_PREVIEW_LABEL}: `) ||\n\t\tmessage.startsWith(`${GOAL_CONTEXT_PREVIEW_LABEL}: `) ||\n\t\tmessage.startsWith(`${AGENT_MESSAGE_RECEIVED_PREVIEW_LABEL}: `)\n\t);\n}\n\nexport function formatQueuedMessagePreview(message: string, label: \"Steering\" | \"Follow-up\"): string {\n\treturn isLabeledQueuedPreview(message) ? message : `${label}: ${message}`;\n}\n\nexport function styleQueuedMessagePreview(\n\tmessage: string,\n\tlabel: \"Steering\" | \"Follow-up\",\n\tisRecognizedSlashCommand: (name: string) => boolean,\n): string {\n\tconst preview = formatQueuedMessagePreview(message, label);\n\tif (!isLeadingSlashCommand(message, isRecognizedSlashCommand)) return theme.fg(\"dim\", preview);\n\tconst prefix = preview.slice(0, preview.length - message.length);\n\treturn `${theme.fg(\"dim\", prefix)}${styleSlashCommandText(message, (rest) => theme.fg(\"dim\", rest))}`;\n}\n\nfunction isExpandable(obj: unknown): obj is Expandable {\n\treturn typeof obj === \"object\" && obj !== null && \"setExpanded\" in obj && typeof obj.setExpanded === \"function\";\n}\n\ninterface AgentMessagesExpandable {\n\tsetAgentMessagesExpanded(expanded: boolean): void;\n}\n\nfunction hasAgentMessagesExpansion(obj: unknown): obj is AgentMessagesExpandable {\n\treturn (\n\t\ttypeof obj === \"object\" &&\n\t\tobj !== null &&\n\t\t\"setAgentMessagesExpanded\" in obj &&\n\t\ttypeof (obj as AgentMessagesExpandable).setAgentMessagesExpanded === \"function\"\n\t);\n}\n\ninterface EditDiffsExpandable {\n\tsetEditDiffsExpanded(expanded: boolean): void;\n}\n\nfunction hasEditDiffsExpansion(obj: unknown): obj is EditDiffsExpandable {\n\treturn (\n\t\ttypeof obj === \"object\" &&\n\t\tobj !== null &&\n\t\t\"setEditDiffsExpanded\" in obj &&\n\t\ttypeof (obj as EditDiffsExpandable).setEditDiffsExpanded === \"function\"\n\t);\n}\n\nclass ExpandableText extends Text implements Expandable {\n\tconstructor(\n\t\tprivate readonly getCollapsedText: () => string,\n\t\tprivate readonly getExpandedText: () => string,\n\t\texpanded = false,\n\t\tpaddingX = 0,\n\t\tpaddingY = 0,\n\t) {\n\t\tsuper(expanded ? getExpandedText() : getCollapsedText(), paddingX, paddingY);\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.setText(expanded ? this.getExpandedText() : this.getCollapsedText());\n\t}\n}\n\nexport function formatSplashCwd(cwd: string): string {\n\tconst normalized = cwd.replace(/\\\\/g, \"/\");\n\tconst home = os.homedir().replace(/\\\\/g, \"/\");\n\tif (home && normalized === home) {\n\t\treturn \"~\";\n\t}\n\tif (home && normalized.startsWith(`${home}/`)) {\n\t\treturn `~${normalized.slice(home.length)}`;\n\t}\n\n\treturn normalized;\n}\n\nfunction mergeSubagentSnapshot(\n\tprevious: AgentConnectionRlmChildAgentSnapshot,\n\tincoming: AgentConnectionRlmChildAgentSnapshot,\n): AgentConnectionRlmChildAgentSnapshot {\n\tconst active = incoming.status === \"running\" || incoming.status === \"queued\";\n\treturn {\n\t\t...previous,\n\t\t...incoming,\n\t\tparentId: incoming.parentId ?? previous.parentId,\n\t\t// Active updates may omit a previously known daemon session id, but a\n\t\t// terminal update without one means the child is no longer resident.\n\t\tactiveSessionId: active ? (incoming.activeSessionId ?? previous.activeSessionId) : incoming.activeSessionId,\n\t\t// A completed retained child can become active again when it receives a\n\t\t// follow-up. Its RLM run status stays terminal, so activity must remain an\n\t\t// independent projection of the live session state.\n\t\tactivity: active ? (incoming.activity ?? previous.activity) : incoming.activity,\n\t};\n}\n\nexport function truncatePathMiddle(value: string, width: number): string {\n\tif (visibleWidth(value) <= width) {\n\t\treturn value;\n\t}\n\tif (width <= 1) {\n\t\treturn truncateToWidth(value, width, \"\");\n\t}\n\n\tconst ellipsis = \"…\";\n\tconst normalized = value.replace(/\\\\/g, \"/\");\n\tconst prefix = normalized.startsWith(\"~/\") ? \"~/\" : normalized.startsWith(\"/\") ? \"/\" : \"\";\n\tconst body = prefix ? normalized.slice(prefix.length) : normalized;\n\tconst parts = body.split(\"/\").filter((part) => part.length > 0);\n\tconst last = parts.pop() ?? \"\";\n\tconst previous = parts.pop();\n\tconst suffix = previous ? `${previous}/${last}` : last;\n\tconst candidate = `${prefix}${ellipsis}/${suffix}`;\n\tif (visibleWidth(candidate) <= width) {\n\t\treturn candidate;\n\t}\n\n\treturn truncateToWidth(candidate, width);\n}\n\nexport interface BrandSplashMetadataLine {\n\tlabel: string;\n\tvalue: string;\n}\n\nexport interface BrandSplashHeaderOptions {\n\tlogo?: string;\n\ttopPadding?: boolean;\n\tgetExtraMetadata?: () => readonly BrandSplashMetadataLine[];\n\tgetHideStartHint?: () => boolean;\n\tgetStartHint?: () => string;\n}\n\nexport class BrandSplashHeader implements Component {\n\tprivate readonly logoRaw: string[];\n\tprivate readonly logoCanvasWidth: number;\n\tprivate readonly gutter = 4;\n\tprivate readonly labelWidth = 9;\n\n\tconstructor(\n\t\tprivate readonly version: string,\n\t\tprivate readonly getModelId: () => string | undefined,\n\t\tprivate readonly getCwd: () => string,\n\t\tprivate readonly verboseInstructions?: string,\n\t\tprivate readonly options: BrandSplashHeaderOptions = {},\n\t) {\n\t\tthis.logoRaw = (options.logo ?? ASM_LOGO).split(\"\\n\");\n\t\tthis.logoCanvasWidth = this.logoRaw.reduce((max, line) => Math.max(max, visibleWidth(line)), 0);\n\t}\n\n\tinvalidate(): void {\n\t\t// Render output is derived from current theme/session state.\n\t}\n\n\trender(width: number): string[] {\n\t\tconst safeWidth = Math.max(1, width);\n\t\tconst paddingX = safeWidth > 1 ? 1 : 0;\n\t\tconst contentWidth = Math.max(1, safeWidth - paddingX * 2);\n\t\tconst metaWidth = contentWidth - this.logoCanvasWidth - this.gutter;\n\t\tconst showMeta = metaWidth >= this.labelWidth + 8;\n\t\tconst valueWidth = Math.max(1, metaWidth - this.labelWidth);\n\t\tconst labelled = (label: string, value: string) => {\n\t\t\tconst displayValue =\n\t\t\t\tlabel === \"cwd\" ? truncatePathMiddle(value, valueWidth) : truncateToWidth(value, valueWidth);\n\t\t\treturn theme.fg(\"dim\", label.padEnd(this.labelWidth)) + theme.fg(\"muted\", displayValue);\n\t\t};\n\t\tconst extraMetadata = this.options.getExtraMetadata?.() ?? [];\n\t\tconst hideStartHint = this.options.getHideStartHint?.() ?? false;\n\t\tconst startHint = this.options.getStartHint?.() ?? \"type to search sessions\";\n\t\tconst metaLines = showMeta\n\t\t\t? [\n\t\t\t\t\tlabelled(\"version\", `v${this.version}`),\n\t\t\t\t\tlabelled(\"model\", this.getModelId() ?? \"—\"),\n\t\t\t\t\tlabelled(\"cwd\", formatSplashCwd(this.getCwd())),\n\t\t\t\t\t...extraMetadata.map((line) => labelled(line.label, line.value)),\n\t\t\t\t\t...(hideStartHint ? [] : [\"\", theme.fg(\"dim\", startHint)]),\n\t\t\t\t]\n\t\t\t: [];\n\t\tconst metaStart = Math.max(0, Math.floor((this.logoRaw.length - metaLines.length) / 2));\n\t\tconst lines = this.options.topPadding ? [\"\"] : [];\n\t\tlines.push(\n\t\t\t...this.logoRaw.map((line, index) => {\n\t\t\t\tconst colored = theme.fg(\"text\", line);\n\t\t\t\tconst meta = index >= metaStart && index < metaStart + metaLines.length ? metaLines[index - metaStart] : \"\";\n\t\t\t\tconst padding = showMeta\n\t\t\t\t\t? \" \".repeat(Math.max(0, this.logoCanvasWidth - visibleWidth(line) + this.gutter))\n\t\t\t\t\t: \"\";\n\t\t\t\tconst content = truncateToWidth(colored + padding + meta, contentWidth, \"\");\n\t\t\t\treturn (\n\t\t\t\t\t\" \".repeat(paddingX) + content + \" \".repeat(Math.max(0, safeWidth - paddingX - visibleWidth(content)))\n\t\t\t\t);\n\t\t\t}),\n\t\t);\n\n\t\tif (this.verboseInstructions) {\n\t\t\tlines.push(\" \".repeat(safeWidth));\n\t\t\tfor (const instruction of this.verboseInstructions.split(\"\\n\")) {\n\t\t\t\tconst content = truncateToWidth(instruction, contentWidth);\n\t\t\t\tlines.push(\n\t\t\t\t\t\" \".repeat(paddingX) + content + \" \".repeat(Math.max(0, safeWidth - paddingX - visibleWidth(content))),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n\ntype StartupPromptBarrierOutcome = \"admitted\" | \"retained\" | \"lifecycle-cancelled\";\n\ntype GoalAnnouncementSnapshot = {\n\tgoalId?: string;\n\tstatus: GoalState[\"status\"];\n\tobjective?: string;\n\tlastReason?: string;\n\tlastError?: string;\n};\n\ntype ModelFallbackWarningAction = \"show\" | \"suppress\";\n\ninterface OnboardingSplashHandle {\n\tshowProgress(message: string): void;\n\tdismiss(): void;\n}\n\nconst THINKING_LEVEL_DESCRIPTIONS: Record<ThinkingLevel, string> = {\n\toff: \"No reasoning\",\n\tminimal: \"Very brief reasoning\",\n\tlow: \"Light reasoning\",\n\tmedium: \"Moderate reasoning\",\n\thigh: \"Deep reasoning\",\n\txhigh: \"Very deep reasoning\",\n\tmax: \"Maximum reasoning\",\n};\n\nconst HEARTBEAT_ARGUMENT_COMPLETIONS: AutocompleteItem[] = [\n\t{\n\t\tvalue: \"every \",\n\t\tlabel: \"every <duration> <instruction>\",\n\t\tdescription: \"Set an interval, then add an instruction: /heartbeat every 10s Scan the logs\",\n\t},\n\t{\n\t\tvalue: \"--steer \",\n\t\tlabel: \"--steer <instruction>\",\n\t\tdescription: \"Deliver by interrupting the current turn (default)\",\n\t},\n\t{\n\t\tvalue: \"--follow-up \",\n\t\tlabel: \"--follow-up <instruction>\",\n\t\tdescription: \"Deliver as a follow-up after the current turn finishes\",\n\t},\n];\n\nconst DEAD_TERMINAL_ERROR_CODES = new Set([\"EIO\", \"EPIPE\", \"ENOTCONN\"]);\n\n// Cap on retained pasted-image bytes (base64). Images are resized below the\n// inline limit before storing, so this holds many recent pastes; the oldest are\n// evicted past the cap to keep a long session bounded.\nconst MAX_PASTED_IMAGE_BYTES = 64 * 1024 * 1024;\nconst INITIAL_TRANSCRIPT_RENDER_MESSAGE_LIMIT = 400;\n\nfunction initialRenderMessages(messages: AgentMessage[]): AgentMessage[] {\n\tif (messages.length <= INITIAL_TRANSCRIPT_RENDER_MESSAGE_LIMIT) {\n\t\treturn messages;\n\t}\n\tconst toolCallMessages = new Map<string, { index: number; message: Extract<AgentMessage, { role: \"assistant\" }> }>();\n\tfor (const [index, message] of messages.entries()) {\n\t\tif (message.role !== \"assistant\") {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const content of message.content) {\n\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\ttoolCallMessages.set(content.id, { index, message });\n\t\t\t}\n\t\t}\n\t}\n\n\tconst initialStartIndex = messages.length - INITIAL_TRANSCRIPT_RENDER_MESSAGE_LIMIT;\n\tfor (let startIndex = initialStartIndex; startIndex < messages.length; startIndex++) {\n\t\tconst visibleMessages = messages.slice(startIndex);\n\t\tconst visibleToolCallIds = new Set<string>();\n\t\tfor (const message of visibleMessages) {\n\t\t\tif (message.role !== \"assistant\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const content of message.content) {\n\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\tvisibleToolCallIds.add(content.id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst requiredToolCallIdsByMessage = new Map<\n\t\t\tnumber,\n\t\t\t{ message: Extract<AgentMessage, { role: \"assistant\" }>; toolCallIds: Set<string> }\n\t\t>();\n\t\tfor (const message of visibleMessages) {\n\t\t\tif (message.role !== \"toolResult\" || visibleToolCallIds.has(message.toolCallId)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst toolCallMessage = toolCallMessages.get(message.toolCallId);\n\t\t\tif (!toolCallMessage || toolCallMessage.index >= startIndex) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst requiredMessage = requiredToolCallIdsByMessage.get(toolCallMessage.index) ?? {\n\t\t\t\tmessage: toolCallMessage.message,\n\t\t\t\ttoolCallIds: new Set<string>(),\n\t\t\t};\n\t\t\trequiredMessage.toolCallIds.add(message.toolCallId);\n\t\t\trequiredToolCallIdsByMessage.set(toolCallMessage.index, requiredMessage);\n\t\t}\n\n\t\tif (visibleMessages.length + requiredToolCallIdsByMessage.size > INITIAL_TRANSCRIPT_RENDER_MESSAGE_LIMIT) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst requiredToolCallMessages = [...requiredToolCallIdsByMessage.entries()]\n\t\t\t.sort(([leftIndex], [rightIndex]) => leftIndex - rightIndex)\n\t\t\t.map(([, { message, toolCallIds }]) => ({\n\t\t\t\t...message,\n\t\t\t\tcontent: message.content.filter((content) => content.type !== \"toolCall\" || toolCallIds.has(content.id)),\n\t\t\t}));\n\t\treturn omitOrphanToolResults([...requiredToolCallMessages, ...visibleMessages]);\n\t}\n\n\treturn [];\n}\n\nfunction omitOrphanToolResults(messages: AgentMessage[]): AgentMessage[] {\n\tconst renderedToolCallIds = new Set<string>();\n\tconst renderableMessages: AgentMessage[] = [];\n\tfor (const message of messages) {\n\t\tif (message.role === \"assistant\") {\n\t\t\tfor (const content of message.content) {\n\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\trenderedToolCallIds.add(content.id);\n\t\t\t\t}\n\t\t\t}\n\t\t\trenderableMessages.push(message);\n\t\t} else if (message.role === \"toolResult\") {\n\t\t\tif (renderedToolCallIds.has(message.toolCallId)) {\n\t\t\t\trenderableMessages.push(message);\n\t\t\t}\n\t\t} else {\n\t\t\trenderableMessages.push(message);\n\t\t}\n\t}\n\treturn renderableMessages;\n}\n\nfunction isDeadTerminalError(error: unknown): boolean {\n\tif (!error || typeof error !== \"object\" || !(\"code\" in error)) {\n\t\treturn false;\n\t}\n\tconst code = (error as NodeJS.ErrnoException).code;\n\treturn code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);\n}\n\nfunction getPayloadString(payload: Record<string, unknown>, key: string): string | undefined {\n\tconst value = payload[key];\n\treturn typeof value === \"string\" ? value : undefined;\n}\n\nfunction getPayloadNumber(payload: Record<string, unknown>, key: string): number | undefined {\n\tconst value = payload[key];\n\treturn typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n}\n\nfunction getPayloadBoolean(payload: Record<string, unknown>, key: string): boolean | undefined {\n\tconst value = payload[key];\n\treturn typeof value === \"boolean\" ? value : undefined;\n}\n\nfunction getPayloadStringArray(payload: Record<string, unknown>, key: string): string[] | undefined {\n\tconst value = payload[key];\n\tif (value === undefined) {\n\t\treturn undefined;\n\t}\n\treturn Array.isArray(value) && value.every((item): item is string => typeof item === \"string\") ? value : undefined;\n}\n\nfunction getPayloadNotifyType(payload: Record<string, unknown>, key: string): \"info\" | \"warning\" | \"error\" | undefined {\n\tconst value = payload[key];\n\treturn value === \"info\" || value === \"warning\" || value === \"error\" ? value : undefined;\n}\n\nfunction getPayloadWidgetPlacement(\n\tpayload: Record<string, unknown>,\n\tkey: string,\n): \"aboveEditor\" | \"belowEditor\" | undefined {\n\tconst value = payload[key];\n\treturn value === \"aboveEditor\" || value === \"belowEditor\" ? value : undefined;\n}\n\nfunction getPayloadWorkingIndicatorOptions(\n\tpayload: Record<string, unknown>,\n\tkey: string,\n): LoaderIndicatorOptions | undefined {\n\tconst value = payload[key];\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) {\n\t\treturn undefined;\n\t}\n\tconst optionsPayload = value as Record<string, unknown>;\n\tconst frames = getPayloadStringArray(optionsPayload, \"frames\");\n\tconst intervalMs = getPayloadNumber(optionsPayload, \"intervalMs\");\n\treturn {\n\t\t...(frames === undefined ? {} : { frames }),\n\t\t...(intervalMs === undefined ? {} : { intervalMs }),\n\t};\n}\n\nexport function updateArgsIncludeSelf(args: readonly string[]): boolean {\n\tlet selfFlag = false;\n\tlet extensionsOnlyFlag = false;\n\tlet positional: string | undefined;\n\tfor (let index = 0; index < args.length; index++) {\n\t\tconst arg = args[index];\n\t\tif (arg === \"--self\") {\n\t\t\tselfFlag = true;\n\t\t} else if (arg === \"--extensions\") {\n\t\t\textensionsOnlyFlag = true;\n\t\t} else if (arg === \"--extension\") {\n\t\t\textensionsOnlyFlag = true;\n\t\t\tindex++;\n\t\t} else if (arg === \"--daemon-socket\") {\n\t\t\tindex++;\n\t\t} else if (arg && !arg.startsWith(\"-\") && positional === undefined) {\n\t\t\tpositional = arg;\n\t\t}\n\t}\n\tif (selfFlag) {\n\t\treturn true;\n\t}\n\tif (extensionsOnlyFlag) {\n\t\treturn false;\n\t}\n\tif (!positional) {\n\t\treturn true;\n\t}\n\tconst normalized = positional.toLowerCase();\n\treturn normalized === \"self\" || normalized === \"pi\" || normalized === APP_NAME.toLowerCase();\n}\n\nfunction argsIncludeSessionSelection(args: readonly string[]): boolean {\n\tfor (const arg of args) {\n\t\tif (arg === \"--resume\" || arg === \"-r\" || arg === \"--continue\" || arg === \"-c\" || arg === \"--fork\") {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nexport function buildUpdateRelaunchArgs(args: readonly string[], sessionFile: string | undefined): string[] {\n\tconst relaunchArgs = [...args];\n\tif (sessionFile && !argsIncludeSessionSelection(relaunchArgs)) {\n\t\trelaunchArgs.push(\"--resume\", sessionFile);\n\t}\n\treturn relaunchArgs;\n}\n\nexport function buildUpdateChildArgs(args: readonly string[], daemonSocketPath: string): string[] {\n\treturn args.includes(\"--daemon-socket\") ? [...args] : [...args, \"--daemon-socket\", daemonSocketPath];\n}\n\nexport function resolveInteractiveUpdateDaemonSocketPath(\n\targs: readonly string[],\n\tactiveDaemonSocketPath: string,\n): string {\n\tconst socketFlagIndex = args.indexOf(\"--daemon-socket\");\n\treturn socketFlagIndex === -1 ? activeDaemonSocketPath : (args[socketFlagIndex + 1] ?? activeDaemonSocketPath);\n}\n\nexport interface InteractiveInitialPrompt {\n\ttext: string;\n\timages?: ImageContent[];\n}\n\nexport interface InteractiveModeOptions {\n\t/** Providers that were migrated to auth.json (shows warning) */\n\tmigratedProviders?: string[];\n\t/** Warning message if session model couldn't be restored */\n\tmodelFallbackMessage?: string;\n\t/** One-off warning shown on startup. */\n\tstartupNotice?: string;\n\t/** Initial message to send on startup (can include @file content) */\n\tinitialMessage?: string;\n\t/** Images to attach to the initial message */\n\tinitialImages?: ImageContent[];\n\t/** Additional text-only messages to send after the initial message. */\n\tinitialMessages?: string[];\n\t/** Additional image-bearing prompts to send after the initial messages. */\n\tinitialPrompts?: InteractiveInitialPrompt[];\n\t/** Force verbose startup (overrides quietStartup setting) */\n\tverbose?: boolean;\n\t/** Agent execution boundary. InteractiveMode never talks directly to AgentSession for core execution. */\n\tagentConnection: AgentConnection;\n\t/** Exact daemon socket to preserve across an interactive self-update restart. */\n\tdaemonSocketPath?: string;\n\t/**\n\t * Local-only host for in-process extension binding and callback-bearing session operations.\n\t * This must remain optional adapter glue, not a generic execution dependency.\n\t */\n\tlocalSessionHost?: InteractiveModeLocalSessionHost;\n\t/** Bind extension handlers in the local session host. Disabled for daemon/gateway-backed clients. */\n\tbindLocalSessionExtensions?: boolean;\n\t/** UI-local services used for settings, auth, resources, and rendering. Defaults to services from localSessionHost. */\n\tuiServices?: InteractiveModeUiServices;\n\t/** Extra cleanup for externally-owned UI service hosts. Runs after the connection is disposed and before process exit. */\n\tonShutdown?: () => void | Promise<void>;\n\t/** Allow returning from a full session to the agents view without stopping the daemon-owned agent. */\n\treturnToAgentsView?: boolean;\n\t/** Enter fullscreen regardless of the persisted fullscreen preference. */\n\tforceFullscreen?: boolean;\n\t/**\n\t * The agents view already surfaced global startup notices (app/extension updates, tmux setup),\n\t * so this session must not repeat them in its chat stream. Distinct from `returnToAgentsView`,\n\t * which also covers direct daemon attaches where the agents view was never shown.\n\t */\n\tagentsViewOwnsStartupNotices?: boolean;\n\t/** Persisted RLM depth supplied by the daemon SessionSummary. */\n\tsessionDepth?: number;\n\t/** Whether the unified daemon/catalog projection had any direct children. */\n\tsessionHasChildren?: boolean;\n\t/** Client-owned stash store shared across chat views in this TUI process. */\n\tpromptStashStore?: ClientPromptStashStore;\n\t/** Initial stable session id used to scope prompt stash state. */\n\tpromptStashSessionId?: string;\n}\n\nexport interface InteractiveModeRunResult {\n\ttype: \"agents_view\" | \"scoped_agents_view\";\n\tsource: Pick<AgentConnectionState, \"activeSessionId\" | \"sessionFile\" | \"sessionId\" | \"sessionName\" | \"cwd\">;\n}\n\nexport function formatAgentDepthLabel(depth: number | undefined, hasChildren: boolean): string | undefined {\n\tif (depth === undefined || (depth === 0 && !hasChildren)) return undefined;\n\treturn `depth ${depth}`;\n}\n\nexport class InteractiveMode {\n\tprivate static readonly EXIT_HINT_DURATION_MS = 2000;\n\tprivate static readonly ESCAPE_REPEAT_WINDOW_MS = 500;\n\n\tprivate uiServices: InteractiveModeUiServices;\n\tprivate agentConnection: AgentConnection;\n\tprivate localSessionHost: InteractiveModeLocalSessionHost | undefined;\n\tprivate bindLocalSessionExtensions: boolean;\n\tprivate ui: TUI;\n\tprivate chatContainer: Container;\n\tprivate shortcutGuideContainer: Container;\n\tprivate pendingMessagesContainer: Container;\n\tprivate statusContainer: Container;\n\tprivate queuedMessagesContainer: Container;\n\tprivate sideQuestionContainer: Container;\n\tprivate featureHintContainer: Container;\n\tprivate defaultEditor: CustomEditor;\n\tprivate editor: EditorComponent;\n\tprivate readonly promptStashStore: ClientPromptStashStore | undefined;\n\tprivate promptStashSessionId: string | undefined;\n\tprivate promptStashState: PromptStashState;\n\tprivate editorComponentFactory: EditorFactory | undefined;\n\tprivate autocompleteProvider: AutocompleteProvider | undefined;\n\tprivate autocompleteProviderWrappers: AutocompleteProviderFactory[] = [];\n\tprivate fdPath: string | undefined;\n\tprivate mainContainer: Container;\n\tprivate mainViewContainer: Container;\n\t// prompt bar (editor + footer slot) — the only thing pinned to the bottom in fullscreen\n\tprivate promptDock: Container;\n\t// wraps the active footer so custom-footer swaps reflect in both layouts\n\tprivate footerSlot: Container;\n\tprivate fullscreenEnabled = false;\n\tprivate editorContainer: Container;\n\tprivate footer: FooterComponent;\n\tprivate footerDataProvider: FooterDataProvider;\n\t// Stored so the same manager can be injected into custom editors, selectors, and extension UI.\n\tprivate keybindings: KeybindingsManager;\n\tprivate version: string;\n\tprivate readonly startHint = getRandomStartHint();\n\tprivate isInitialized = false;\n\tprivate onInputCallback?: (text: string | undefined) => void;\n\tprivate submittedInputBehavior: \"steer\" | \"followUp\" = \"steer\";\n\tprivate latestEditorPromptStash: PromptStash | undefined;\n\tprivate pendingSubmittedPromptStash: PromptStash | undefined;\n\tprivate inputSubmissionGeneration = 0;\n\tprivate inputSubmissionsPending = 0;\n\tprivate pendingPromptStashReleases: { sessionId: string; state: PromptStashState }[] = [];\n\tprivate readonly retainedSubmissionGenerations = new WeakMap<PromptStash, number>();\n\tprivate admitPendingStartupPrompts: (() => Promise<StartupPromptBarrierOutcome>) | undefined;\n\tprivate agentsViewRequest: InteractiveModeRunResult[\"type\"] | undefined;\n\tprivate loadingAnimation: Loader | undefined = undefined;\n\tprivate workingMessage: string | undefined = undefined;\n\tprivate workingVisible = true;\n\tprivate workingIndicatorOptions: LoaderIndicatorOptions | undefined = undefined;\n\tprivate workingStartedAt: number | undefined = undefined;\n\t// Start of the in-flight run; survives loader teardown so the elapsed display doesn't reset on re-entry.\n\tprivate turnStartedAt: number | undefined = undefined;\n\tprivate workingTimer: NodeJS.Timeout | undefined = undefined;\n\tprivate readonly featureHintDeck = new FeatureHintDeck();\n\tprivate currentFeatureHint: string | undefined;\n\tprivate featureHintEligibleAt = 0;\n\tprivate featureHintTimer: NodeJS.Timeout | undefined;\n\tprivate featureHintAnimationTimer: NodeJS.Timeout | undefined;\n\tprivate featureHintComponent: FeatureHintComponent | undefined;\n\tprivate featureHintRunPending = false;\n\tprivate featureHintSuppressedByQueue = false;\n\tprivate pulseTimer: NodeJS.Timeout | undefined = undefined;\n\tprivate pulseFrame = 0;\n\tprivate readonly activityTracker = new AgentActivityTracker();\n\t// activityTracker token count already folded into the context snapshot; only output beyond\n\t// this counts as live in-flight (keeps auto-retries from re-adding a failed attempt).\n\tprivate contextUsageTokenBaseline = 0;\n\t// Refresh ordering: a stale failure must never clobber a newer success.\n\tprivate contextUsageRefresh = { generation: 0, lastSuccessGeneration: 0 };\n\tprivate readonly defaultHiddenThinkingLabel = \"Thinking...\";\n\tprivate hiddenThinkingLabel = this.defaultHiddenThinkingLabel;\n\n\tprivate ctrlCExitHintExpiresAt = 0;\n\tprivate ctrlCExitHintTimer: ReturnType<typeof setTimeout> | undefined = undefined;\n\tprivate escapeRepeatAction: \"tree\" | \"clear\" | undefined;\n\tprivate escapeRepeatExpiresAt = 0;\n\tprivate escapeRepeatTimer: ReturnType<typeof setTimeout> | undefined = undefined;\n\tprivate anthropicSubscriptionWarningShown = false;\n\n\tprivate lastStatusSpacer: Spacer | undefined = undefined;\n\tprivate lastStatusText: Text | undefined = undefined;\n\tprivate lastGoalAnnouncement: GoalAnnouncementSnapshot | undefined = undefined;\n\tprivate goalTrayTimer: NodeJS.Timeout | undefined = undefined;\n\n\tprivate streamingComponent: AssistantMessageComponent | undefined = undefined;\n\tprivate streamingMessage: AssistantMessage | undefined = undefined;\n\tprivate sideQuestionComponent: SideQuestionComponent | undefined;\n\tprivate sideQuestionEvent: AgentConnectionSideQuestionEvent | undefined;\n\tprivate sideQuestionTurns: AgentConnectionSideQuestionEvent[] = [];\n\tprivate activeSideQuestionId: string | undefined;\n\t// Set while a ! bash command runs inside the side conversation: its\n\t// BashExecutionComponent renders inside the pane instead of the main chat.\n\t// bash_* events broadcast to every attached client, so runs correlate by\n\t// runId — the runId we generate here is echoed on our run's events.\n\tprivate sideQuestionBash: { runId: string; input: string; seedTranscript: boolean } | undefined;\n\t// The pane-mounted component of our own side run; bash_end seeds the side\n\t// transcript only when it ends this exact component.\n\tprivate sideQuestionBashComponent: BashExecutionComponent | undefined;\n\t// Holds the runId of a side bash abandoned at pane close: that run's\n\t// remaining bash_* events are swallowed (until its bash_end) instead of\n\t// leaking into the main transcript.\n\tprivate sideQuestionBashDiscarded: string | undefined;\n\n\t// User bash execution tracking (! / !! prefix), driven by bash_* session events\n\tprivate activeBashComponent: BashExecutionComponent | undefined = undefined;\n\tprivate pendingBashComponents: BashExecutionComponent[] = [];\n\n\t// Serializes session event handling; see subscribeToAgent\n\tprivate sessionEventQueue: Promise<void> = Promise.resolve();\n\tprivate sessionEventGeneration = 0;\n\tprivate fastModeToggleQueue: Promise<void> = Promise.resolve();\n\n\tprivate pendingTools = new Map<string, ToolExecutionComponent>();\n\tprivate ipythonToolComponents = new Map<string, ToolExecutionComponent>();\n\tprivate lateIpythonSentAgentMessages = new Map<string, KernelSentAgentMessage[]>();\n\tprivate pendingToolCreations = new Set<string>();\n\tprivate startedToolCalls = new Set<string>();\n\tprivate pendingToolGeneration = 0;\n\tprivate toolDefinitionCache = new Map<string, ToolExecutionDefinition | undefined>();\n\tprivate agentRunFileChanges = new Map<string, FileChangeSummary>();\n\n\t// One summary line below the editor, backed by the existing child-status stream.\n\tprivate subagentSummaryLine: SubagentSummaryLine;\n\tprivate subagentSnapshots = new Map<string, AgentConnectionRlmChildAgentSnapshot>();\n\tprivate rlmNodeId: string | undefined;\n\n\tprivate toolOutputExpanded = false;\n\tprivate agentMessagesExpanded = false;\n\tprivate editDiffsExpanded = false;\n\n\tprivate hideThinkingBlock = false;\n\tprivate readonly mermaidMarkdownTransform = createMermaidMarkdownTransform({\n\t\tgetMode: () => this.settingsManager.getMermaidRenderingMode(),\n\t\ttheme,\n\t});\n\n\tprivate skillCommands = new Map<string, string>();\n\tprivate connectionCommands: AgentConnectionSlashCommand[] = [];\n\tprivate connectionModels: AgentConnectionModel[] = [];\n\tprivate connectionModelCatalog: AgentConnectionModel[] = [];\n\tprivate connectionConfiguredProviders = new Set<string>();\n\tprivate connectionModelsFetchedAt = 0;\n\tprivate connectionModelsRefreshVersion = 0;\n\tprivate connectionModelsRefreshInFlight: { version: number; promise: Promise<AgentConnectionModel[]> } | undefined;\n\tprivate connectionState: AgentConnectionState | undefined;\n\tprivate connectionResourceSnapshot: AgentConnectionResourceSnapshot | undefined;\n\tprivate sessionHasMessages = false;\n\tprivate heartbeatCatalog: AgentConnectionHeartbeat[] = [];\n\tprivate heartbeats: AgentConnectionHeartbeat[] = [];\n\tprivate heartbeatRefreshPromise: Promise<void> | undefined;\n\tprivate heartbeatRefreshRequested = false;\n\tprivate heartbeatManager: HeartbeatManagerComponent | undefined;\n\tprivate heartbeatManagerHandle: OverlayHandle | undefined;\n\tprivate heartbeatManagerRefreshTimer: ReturnType<typeof setTimeout> | undefined;\n\n\t// Registry of images pasted this session, keyed by the `[image #N]` marker\n\t// shown to the user. Insertion-ordered; the bytes persist (bounded by\n\t// MAX_PASTED_IMAGE_BYTES) so a marker resolves to its image whenever the text\n\t// reappears — on submit, undo, history recall, retry, or dequeue. A submission\n\t// attaches only the images whose markers are present in the sent text.\n\tprivate pastedImages = new Map<number, ImageContent>();\n\tprivate nextImageMarkerId = 1;\n\n\tprivate unsubscribe?: () => void;\n\tprivate signalCleanupHandlers: Array<() => void> = [];\n\n\tprivate autoCompactionLoader: Loader | undefined = undefined;\n\tprivate refineLoader: Loader | undefined = undefined;\n\n\tprivate retryLoader: Loader | undefined = undefined;\n\tprivate retryCountdown: CountdownTimer | undefined = undefined;\n\n\t// Session-owned queued messages mirrored from connection events.\n\tprivate connectionQueue: AgentConnectionQueueState = { steering: [], followUp: [] };\n\tprivate readonly queueSelection = new QueueSelection();\n\tprivate isApplyingQueueSelectionText = false;\n\tprivate queueMutationChain: Promise<void> = Promise.resolve();\n\tprivate pendingQueueEdit: symbol | undefined;\n\n\tprivate shutdownRequested = false;\n\n\tprivate extensionSelector: ExtensionSelectorComponent | undefined = undefined;\n\tprivate extensionInput: ExtensionInputComponent | undefined = undefined;\n\tprivate extensionEditor: ExtensionEditorComponent | undefined = undefined;\n\tprivate extensionTerminalInputUnsubscribers = new Set<() => void>();\n\tprivate activeConnectionExtensionUiRequests = new Map<string, { cancelLocal: () => void }>();\n\n\tprivate extensionWidgetsAbove = new Map<string, Component & { dispose?(): void }>();\n\tprivate extensionWidgetsBelow = new Map<string, Component & { dispose?(): void }>();\n\tprivate widgetContainerAbove!: Container;\n\tprivate widgetContainerBelow!: Container;\n\n\t// One-line recap of the agent's recent work, rendered just above the editor.\n\tprivate recapContainer!: Container;\n\tprivate sessionRecap: string | undefined;\n\n\tprivate customFooter: (Component & { dispose?(): void }) | undefined = undefined;\n\n\tprivate headerContainer: Container;\n\n\tprivate builtInHeader: Component | undefined = undefined;\n\n\tprivate customHeader: (Component & { dispose?(): void }) | undefined = undefined;\n\n\tprivate getLocalSessionHost(): InteractiveModeLocalSessionHost {\n\t\tif (!this.localSessionHost) {\n\t\t\tthrow new Error(\"Local session host is not available in connection-backed interactive mode\");\n\t\t}\n\t\treturn this.localSessionHost;\n\t}\n\tprivate get settingsManager() {\n\t\treturn this.uiServices.settingsManager;\n\t}\n\tprivate get modelRegistry() {\n\t\treturn this.uiServices.modelRegistry;\n\t}\n\n\tconstructor(private options: InteractiveModeOptions) {\n\t\tconst uiServices = options.uiServices ?? options.localSessionHost?.createUiServices();\n\t\tif (!uiServices) {\n\t\t\tthrow new Error(\"InteractiveMode requires uiServices when no localSessionHost is supplied\");\n\t\t}\n\t\tthis.uiServices = uiServices;\n\t\tthis.agentConnection = options.agentConnection;\n\t\tthis.promptStashStore = options.promptStashStore;\n\t\tthis.promptStashSessionId = options.promptStashSessionId;\n\t\tthis.promptStashState =\n\t\t\tthis.promptStashStore && this.promptStashSessionId\n\t\t\t\t? this.promptStashStore.forSession(this.promptStashSessionId)\n\t\t\t\t: {};\n\t\tthis.hydratePromptStash();\n\t\tthis.localSessionHost = options.localSessionHost;\n\t\tthis.bindLocalSessionExtensions = options.bindLocalSessionExtensions ?? options.localSessionHost !== undefined;\n\t\tif (this.bindLocalSessionExtensions && !options.localSessionHost) {\n\t\t\tthrow new Error(\"Local extension binding requires localSessionHost\");\n\t\t}\n\t\tthis.agentConnection.onBeforeSessionInvalidate(() => {\n\t\t\tthis.resetExtensionUI();\n\t\t\tthis.resetSideQuestion();\n\t\t});\n\t\tthis.version = VERSION;\n\t\tthis.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());\n\t\tthis.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());\n\t\tthis.ui.onCopy = (text) => {\n\t\t\tvoid this.copyFullscreenSelection(text);\n\t\t};\n\t\tthis.headerContainer = new Container();\n\t\tthis.chatContainer = new Container();\n\t\tthis.shortcutGuideContainer = new Container();\n\t\tthis.pendingMessagesContainer = new Container();\n\t\tthis.statusContainer = new Container();\n\t\tthis.queuedMessagesContainer = new Container();\n\t\tthis.sideQuestionContainer = new Container();\n\t\tthis.featureHintContainer = new Container();\n\t\tthis.widgetContainerAbove = new Container();\n\t\tthis.widgetContainerBelow = new Container();\n\t\tthis.recapContainer = new Container();\n\t\tthis.keybindings = KeybindingsManager.create();\n\t\tsetKeybindings(this.keybindings);\n\t\tconst editorPaddingX = this.settingsManager.getEditorPaddingX();\n\t\tconst autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();\n\t\tthis.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, {\n\t\t\tpaddingX: editorPaddingX,\n\t\t\tautocompleteMaxVisible,\n\t\t\tisArgumentCommand: builtinSlashCommandTakesArgument,\n\t\t\tplaceholder: this.startHint,\n\t\t\tplaceholderColor: (text) => theme.fg(\"dim\", text),\n\t\t});\n\t\tthis.editor = this.defaultEditor;\n\t\tthis.mainContainer = new Container();\n\t\tthis.mainViewContainer = new Container();\n\t\tthis.promptDock = new Container();\n\t\tthis.footerSlot = new Container();\n\t\tthis.mainViewContainer.addChild(this.chatContainer);\n\t\tthis.mainViewContainer.addChild(this.shortcutGuideContainer);\n\t\tthis.mainViewContainer.addChild(this.pendingMessagesContainer);\n\t\tthis.mainViewContainer.addChild(this.statusContainer);\n\t\tthis.editorContainer = new Container();\n\t\tthis.editorContainer.addChild(this.editor as Component);\n\t\tthis.subagentSummaryLine = new SubagentSummaryLine(\n\t\t\t() => this.getTrayLocationLabel(),\n\t\t\t() => this.getTrayContextLabel(),\n\t\t\t() => this.getTrayOverrideLabel(),\n\t\t);\n\t\tthis.subagentSummaryLine.setOpenable(this.options.returnToAgentsView === true);\n\t\tthis.subagentSummaryLine.onOpen = () => void this.openScopedAgentsView();\n\t\tthis.subagentSummaryLine.onCancel = () => this.focusEditor();\n\t\tthis.subagentSummaryLine.onChatAction = (data) => this.handleSubagentSummaryChatAction(data);\n\t\tthis.footerDataProvider = new FooterDataProvider(this.uiServices.getInitialCwd());\n\t\tthis.footer = new FooterComponent(this.footerDataProvider);\n\t\tthis.footer.setAutoCompactEnabled(this.settingsManager.getCompactionEnabled());\n\t\tthis.setGoalAnnouncementBaseline(emptyGoalState());\n\n\t\tthis.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();\n\n\t\tsetRegisteredThemes(this.uiServices.getThemes());\n\t\tinitTheme(this.settingsManager.getTheme(), true);\n\t}\n\n\tprivate get promptStash(): PromptStash | undefined {\n\t\treturn this.promptStashState.stash;\n\t}\n\n\tprivate set promptStash(stash: PromptStash | undefined) {\n\t\tthis.promptStashState.stash = stash;\n\t}\n\n\tprivate hydratePromptStash(): void {\n\t\tfor (const stash of [this.promptStash, ...(this.promptStashState?.queuedStashes ?? [])]) {\n\t\t\tif (!stash) continue;\n\t\t\tfor (const [markerId, image] of stash.images ?? []) {\n\t\t\t\tthis.pastedImages.set(markerId, image);\n\t\t\t\tthis.nextImageMarkerId = Math.max(this.nextImageMarkerId, markerId + 1);\n\t\t\t}\n\t\t\tfor (const markerId of imageMarkerIds(stash.text)) {\n\t\t\t\tthis.nextImageMarkerId = Math.max(this.nextImageMarkerId, markerId + 1);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate bindPromptStashSession(sessionId: string): void {\n\t\tif (!this.promptStashStore || this.promptStashSessionId === sessionId) {\n\t\t\treturn;\n\t\t}\n\t\tthis.releasePromptStashSession();\n\t\tthis.promptStashSessionId = sessionId;\n\t\tthis.promptStashState = this.promptStashStore.forSession(sessionId);\n\t\tthis.hydratePromptStash();\n\t}\n\n\tprivate releasePromptStashSession(): void {\n\t\tif (this.inputSubmissionsPending > 0) {\n\t\t\t// Capture the pair: a rebind may repoint the fields before the deferred\n\t\t\t// release fires, and repeated rebinds/teardowns each defer their own pair.\n\t\t\tif (\n\t\t\t\tthis.promptStashSessionId &&\n\t\t\t\t!this.pendingPromptStashReleases.some((pending) => pending.sessionId === this.promptStashSessionId)\n\t\t\t) {\n\t\t\t\tthis.pendingPromptStashReleases.push({\n\t\t\t\t\tsessionId: this.promptStashSessionId,\n\t\t\t\t\tstate: this.promptStashState,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst pending = this.pendingPromptStashReleases;\n\t\tthis.pendingPromptStashReleases = [];\n\t\tif (!this.promptStashStore) return;\n\t\tfor (const release of pending) {\n\t\t\tthis.promptStashStore.release(release.sessionId, release.state);\n\t\t}\n\t\tif (this.promptStashSessionId) {\n\t\t\tthis.promptStashStore.release(this.promptStashSessionId, this.promptStashState);\n\t\t}\n\t}\n\n\tprivate completeDeferredPromptStashRelease(): void {\n\t\tconst pending = this.pendingPromptStashReleases;\n\t\tif (pending.length === 0) return;\n\t\tthis.pendingPromptStashReleases = [];\n\t\tif (!this.promptStashStore) return;\n\t\tfor (const release of pending) {\n\t\t\tthis.promptStashStore.release(release.sessionId, release.state);\n\t\t}\n\t}\n\n\tprivate getAutocompleteSourceTag(sourceInfo?: AgentConnectionSourceInfo): string | undefined {\n\t\tif (!sourceInfo) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst scopePrefix =\n\t\t\tsourceInfo.scope === \"user\" ? \"user\" : sourceInfo.scope === \"project\" ? \"project\" : \"temporary\";\n\t\tconst source = sourceInfo.source.trim();\n\n\t\tif (source === \"builtin\") {\n\t\t\treturn \"builtin\";\n\t\t}\n\n\t\tif (source === \"auto\" || source === \"local\" || source === \"cli\") {\n\t\t\treturn scopePrefix;\n\t\t}\n\n\t\tif (source.startsWith(\"npm:\")) {\n\t\t\treturn `${scopePrefix}:${source}`;\n\t\t}\n\n\t\tconst gitSource = parseGitUrl(source);\n\t\tif (gitSource) {\n\t\t\tconst ref = gitSource.ref ? `@${gitSource.ref}` : \"\";\n\t\t\treturn `${scopePrefix}:git:${gitSource.host}/${gitSource.path}${ref}`;\n\t\t}\n\n\t\treturn scopePrefix;\n\t}\n\n\tprivate getAutocompleteSourceLabel(sourceInfo?: AgentConnectionSourceInfo): string | undefined {\n\t\tconst sourceTag = this.getAutocompleteSourceTag(sourceInfo);\n\t\treturn sourceTag ? `#${sourceTag}` : undefined;\n\t}\n\n\tprivate getBuiltInCommandConflictDiagnostics(\n\t\tcommands: readonly AgentConnectionSlashCommand[],\n\t): AgentConnectionResourceDiagnostic[] {\n\t\treturn commands\n\t\t\t.filter((command) => command.source === \"extension\")\n\t\t\t.filter((command) => isBuiltinSlashCommandName(command.registeredName ?? command.name))\n\t\t\t.map((command) => ({\n\t\t\t\ttype: \"warning\" as const,\n\t\t\t\tmessage:\n\t\t\t\t\tcommand.name === (command.registeredName ?? command.name)\n\t\t\t\t\t\t? `Extension command '/${command.name}' conflicts with built-in interactive command. Skipping in autocomplete.`\n\t\t\t\t\t\t: `Extension command '/${command.registeredName}' conflicts with built-in interactive command. Available as '/${command.name}'.`,\n\t\t\t\tpath: command.sourceInfo.path,\n\t\t\t}));\n\t}\n\n\tprivate isRecognizedSlashCommand(name: string): boolean {\n\t\treturn isBuiltinSlashCommandName(name) || this.connectionCommands.some((command) => command.name === name);\n\t}\n\n\tprivate createBaseAutocompleteProvider(): AutocompleteProvider {\n\t\tconst slashCommands: SlashCommand[] = BUILTIN_SLASH_COMMANDS.filter(\n\t\t\t(command) => command.name !== \"fast\" || this.currentModelSupportsFastMode(),\n\t\t).map((command) => ({\n\t\t\tname: command.name,\n\t\t\taliases: command.aliases,\n\t\t\tdescription: command.description,\n\t\t\targumentHint: command.argumentHint,\n\t\t\ttakesArgument: command.takesArgument,\n\t\t}));\n\n\t\tconst modelCommand = slashCommands.find((command) => command.name === \"model\");\n\t\tif (modelCommand) {\n\t\t\tmodelCommand.getArgumentCompletions = (prefix: string): AutocompleteItem[] | null =>\n\t\t\t\tgetModelArgumentCompletions(prefix, this.getCachedModelCandidates());\n\t\t}\n\n\t\tconst effortCommand = slashCommands.find((command) => command.name === \"effort\");\n\t\tif (effortCommand) {\n\t\t\teffortCommand.getArgumentCompletions = (prefix: string): AutocompleteItem[] | null =>\n\t\t\t\tthis.getThinkingLevelCompletions(prefix);\n\t\t\tconst levels = this.getAvailableThinkingLevels();\n\t\t\tif (levels.length > 0) {\n\t\t\t\teffortCommand.argumentHint = `[${levels.join(\"/\")}]`;\n\t\t\t}\n\t\t}\n\n\t\tconst heartbeatCommand = slashCommands.find((command) => command.name === \"heartbeat\");\n\t\tif (heartbeatCommand) {\n\t\t\theartbeatCommand.getArgumentCompletions = (prefix: string): AutocompleteItem[] | null =>\n\t\t\t\tthis.getHeartbeatArgumentCompletions(prefix);\n\t\t}\n\n\t\tconst connectionCommands = this.connectionCommands;\n\t\tconst templateCommands: SlashCommand[] = connectionCommands\n\t\t\t.filter((cmd) => cmd.source === \"prompt\")\n\t\t\t.map((cmd) => ({\n\t\t\t\tname: cmd.name,\n\t\t\t\tdescription: cmd.description,\n\t\t\t\tsourceTag: this.getAutocompleteSourceLabel(cmd.sourceInfo),\n\t\t\t\t...(cmd.argumentHint && { argumentHint: cmd.argumentHint }),\n\t\t\t}));\n\n\t\tconst extensionCommands: SlashCommand[] = connectionCommands\n\t\t\t.filter((cmd) => cmd.source === \"extension\")\n\t\t\t.filter((cmd) => !isBuiltinSlashCommandName(cmd.name))\n\t\t\t.map((cmd) => ({\n\t\t\t\tname: cmd.name,\n\t\t\t\tdescription: cmd.description,\n\t\t\t\tsourceTag: this.getAutocompleteSourceLabel(cmd.sourceInfo),\n\t\t\t\tgetArgumentCompletions: this.bindLocalSessionExtensions\n\t\t\t\t\t? this.getLocalSessionHost().getExtensionRunner().getCommand(cmd.name)?.getArgumentCompletions\n\t\t\t\t\t: undefined,\n\t\t\t}));\n\n\t\tthis.skillCommands.clear();\n\t\tconst skillCommandList: SlashCommand[] = [];\n\t\tif (this.settingsManager.getEnableSkillCommands()) {\n\t\t\tfor (const skill of connectionCommands.filter((cmd) => cmd.source === \"skill\")) {\n\t\t\t\tconst commandName = skill.name;\n\t\t\t\tskillCommandList.push({\n\t\t\t\t\tname: commandName,\n\t\t\t\t\tdescription: skill.description,\n\t\t\t\t\tsourceTag: this.getAutocompleteSourceLabel(skill.sourceInfo),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn new CombinedAutocompleteProvider(\n\t\t\t[...slashCommands, ...templateCommands, ...extensionCommands, ...skillCommandList],\n\t\t\tthis.getCurrentCwd(),\n\t\t\tthis.fdPath,\n\t\t);\n\t}\n\n\tprivate setupAutocompleteProvider(): void {\n\t\tlet provider = this.createBaseAutocompleteProvider();\n\t\tfor (const wrapProvider of this.autocompleteProviderWrappers) {\n\t\t\tprovider = wrapProvider(provider);\n\t\t}\n\n\t\tthis.autocompleteProvider = provider;\n\t\tthis.defaultEditor.setAutocompleteProvider(provider);\n\t\tif (this.editor !== this.defaultEditor) {\n\t\t\tthis.editor.setAutocompleteProvider?.(provider);\n\t\t}\n\t}\n\n\tasync init(): Promise<void> {\n\t\tif (this.isInitialized) return;\n\n\t\tthis.registerSignalHandlers();\n\n\t\t// Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)\n\t\t// fd powers autocomplete, and rg is available for shell commands.\n\t\tconst [fdPath, rgResult] = await Promise.all([ensureTool(\"fd\"), ensureToolWithStatus(\"rg\")]);\n\t\tthis.fdPath = fdPath;\n\t\tif (rgResult.status === \"unavailable\") {\n\t\t\tthis.showWarning(formatMissingRipgrepMessage(rgResult));\n\t\t}\n\n\t\tthis.ui.addChild(this.headerContainer);\n\n\t\t// Brand splash: side-panel layout with structured runtime metadata on the right.\n\t\t// The model/cwd are read through live getters, so they fill in once the\n\t\t// connection state loads (rebindCurrentSession below). Onboarding, when\n\t\t// required, renders as a full-screen overlay on top of this header.\n\t\tif (this.options.verbose || !this.settingsManager.getQuietStartup()) {\n\t\t\t// Verbose: include the full keybinding cheatsheet under the brand mark.\n\t\t\tconst hint = (keybinding: AppKeybinding, description: string) => keyHint(keybinding, description);\n\t\t\tconst verboseInstructions = this.options.verbose\n\t\t\t\t? [\n\t\t\t\t\t\thint(\"app.clear\", \"to interrupt\"),\n\t\t\t\t\t\trawKeyHint(`${keyText(\"app.clear\")} twice`, \"to exit\"),\n\t\t\t\t\t\thint(\"app.input.clear\", \"to clear input\"),\n\t\t\t\t\t\thint(\"app.exit\", \"to exit (empty)\"),\n\t\t\t\t\t\thint(\"app.suspend\", \"to suspend\"),\n\t\t\t\t\t\tkeyHint(\"tui.editor.deleteToLineEnd\", \"to delete to end\"),\n\t\t\t\t\t\trawKeyHint(\"/effort\", \"to set thinking level\"),\n\t\t\t\t\t\thint(\"app.model.select\", \"to select model\"),\n\t\t\t\t\t\thint(\"app.tools.expand\", \"to expand tools\"),\n\t\t\t\t\t\thint(\"app.messages.expand\", \"to expand agent messages\"),\n\t\t\t\t\t\thint(\"app.edits.expand\", \"to expand edit diffs\"),\n\t\t\t\t\t\thint(\"app.thinking.toggle\", \"to expand thinking\"),\n\t\t\t\t\t\thint(\"app.subagents.focus\", \"to inspect subagents\"),\n\t\t\t\t\t\thint(\"app.editor.external\", \"for external editor\"),\n\t\t\t\t\t\thint(\"app.prompt.stash\", \"to stash prompt\"),\n\t\t\t\t\t\trawKeyHint(\"/\", \"for commands\"),\n\t\t\t\t\t\thint(\"app.message.followUp\", \"to queue follow-up\"),\n\t\t\t\t\t\thint(\"app.message.navigateOlder\", \"to browse queued messages\"),\n\t\t\t\t\t\thint(\"app.clipboard.pasteImage\", \"to paste image\"),\n\t\t\t\t\t\trawKeyHint(\"drop files\", \"to attach\"),\n\t\t\t\t\t].join(\"\\n\")\n\t\t\t\t: undefined;\n\t\t\tthis.builtInHeader = new BrandSplashHeader(\n\t\t\t\tthis.version,\n\t\t\t\t() => this.getCurrentModelId(),\n\t\t\t\t() => this.getCurrentCwd(),\n\t\t\t\tverboseInstructions,\n\t\t\t\t{\n\t\t\t\t\ttopPadding: true,\n\t\t\t\t\tgetHideStartHint: () => !this.isNewChat(),\n\t\t\t\t\tgetStartHint: () => this.startHint,\n\t\t\t\t},\n\t\t\t);\n\t\t\tthis.headerContainer.addChild(this.builtInHeader);\n\t\t\tthis.headerContainer.addChild(new Spacer(1));\n\t\t} else {\n\t\t\t// Quiet startup: skip the splash and surrounding padding entirely.\n\t\t\tthis.builtInHeader = new Text(\"\", 0, 0);\n\t\t\tthis.headerContainer.addChild(this.builtInHeader);\n\t\t}\n\n\t\tthis.mainContainer.addChild(this.mainViewContainer);\n\t\tthis.renderWidgets(); // Initialize with default spacer\n\t\tthis.mainContainer.addChild(this.widgetContainerAbove);\n\t\tthis.renderRecap();\n\t\tfor (const container of this.getPromptContextContainers()) {\n\t\t\tthis.mainContainer.addChild(container);\n\t\t}\n\t\tthis.mainContainer.addChild(this.editorContainer);\n\t\tthis.mainContainer.addChild(this.subagentSummaryLine);\n\t\tthis.mainContainer.addChild(this.widgetContainerBelow);\n\t\tthis.footerSlot.addChild(this.footer);\n\t\tthis.mainContainer.addChild(this.footerSlot);\n\t\tfor (const component of this.getPromptDockComponents()) {\n\t\t\tthis.promptDock.addChild(component);\n\t\t}\n\t\tthis.ui.addChild(this.mainContainer);\n\t\tthis.ui.setFocus(this.editor);\n\n\t\tthis.setupKeyHandlers();\n\t\tthis.setupEditorSubmitHandler();\n\n\t\tthis.ui.start();\n\t\tthis.fullscreenEnabled =\n\t\t\t(this.options.forceFullscreen === true || this.settingsManager.getFullscreen()) &&\n\t\t\tprocess.stdout.isTTY === true;\n\t\tif (this.fullscreenEnabled) {\n\t\t\tthis.applyFullscreen(true);\n\t\t}\n\t\tthis.isInitialized = true;\n\n\t\tawait this.rebindCurrentSession();\n\n\t\tawait this.renderInitialMessages();\n\n\t\tonThemeChange(() => {\n\t\t\tthis.ui.invalidate();\n\t\t\tthis.updateEditorBorderColor();\n\t\t\tthis.ui.requestRender();\n\t\t});\n\n\t\tthis.footerDataProvider.onBranchChange(() => {\n\t\t\tthis.ui.requestRender();\n\t\t});\n\n\t\tawait this.updateAvailableProviderCount();\n\t}\n\n\tprivate updateTerminalTitle(): void {\n\t\tconst cwdBasename = path.basename(this.getCurrentCwd());\n\t\tconst sessionName = this.getCurrentSessionName();\n\t\tif (sessionName) {\n\t\t\tthis.ui.terminal.setTitle(`${APP_TITLE} - ${sessionName} - ${cwdBasename}`);\n\t\t} else {\n\t\t\tthis.ui.terminal.setTitle(`${APP_TITLE} - ${cwdBasename}`);\n\t\t}\n\t}\n\n\tasync run(): Promise<InteractiveModeRunResult> {\n\t\tawait this.init();\n\t\tthis.restorePromptStashOnOpen();\n\n\t\t// Global, environment-scoped notices (app update, extension updates, tmux setup)\n\t\t// belong on the agents view, not in a conversation. When the agents view already\n\t\t// showed them, skip the checks here entirely. (This is narrower than\n\t\t// `returnToAgentsView`, which is also set for direct daemon attaches that never\n\t\t// rendered the agents view and still want the in-session fallback.)\n\t\tconst ownsGlobalStartupNotices = !this.options.agentsViewOwnsStartupNotices;\n\t\tconst newVersionPromise = ownsGlobalStartupNotices ? checkForNewPiVersion(this.version) : undefined;\n\t\tconst packageUpdatesPromise = ownsGlobalStartupNotices\n\t\t\t? checkForPackageUpdates({\n\t\t\t\t\tcwd: this.getCurrentCwd(),\n\t\t\t\t\tagentDir: getAgentDir(),\n\t\t\t\t\tsettingsManager: this.settingsManager,\n\t\t\t\t})\n\t\t\t: undefined;\n\t\tconst tmuxKeyboardWarningPromise = ownsGlobalStartupNotices ? checkTmuxKeyboardSetup() : undefined;\n\n\t\tconst {\n\t\t\tmigratedProviders,\n\t\t\tmodelFallbackMessage,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t\tinitialMessages,\n\t\t\tinitialPrompts,\n\t\t} = this.options;\n\n\t\tif (migratedProviders && migratedProviders.length > 0) {\n\t\t\tthis.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(\", \")}`);\n\t\t}\n\n\t\tif (this.options.startupNotice) {\n\t\t\tthis.showWarning(this.options.startupNotice);\n\t\t}\n\n\t\tconst modelsJsonError = this.modelRegistry.getError();\n\t\tif (modelsJsonError) {\n\t\t\tthis.showError(`models.json error: ${modelsJsonError}`);\n\t\t}\n\n\t\tconst startupPrompts: InteractiveInitialPrompt[] = [\n\t\t\t...(initialMessage ? [{ text: initialMessage, images: initialImages }] : []),\n\t\t\t...(initialMessages ?? []).map((text) => ({ text })),\n\t\t\t...(initialPrompts ?? []),\n\t\t];\n\t\t// One drive loop owns startup-prompt delivery: it retries on a 250ms cadence\n\t\t// while a model is missing or admission fails transiently, shows every\n\t\t// admission error, and skips a prompt after three failed attempts.\n\t\t// `startupPromptsSettled` is the user-submission barrier (startup prompts\n\t\t// stay ahead of user prompts). Its outcome distinguishes completed admission\n\t\t// from lifecycle cancellation so a resumed submit does not mutate torn-down\n\t\t// editor state or consume the client-owned durable stash.\n\t\tlet startupPromptsDone = false;\n\t\tconst startupAdmissionAbort = new AbortController();\n\t\tlet settleStartupPrompts = (_outcome: StartupPromptBarrierOutcome) => {};\n\t\tconst startupPromptsSettled = new Promise<StartupPromptBarrierOutcome>((resolve) => {\n\t\t\tsettleStartupPrompts = (outcome) => {\n\t\t\t\tstartupPromptsDone = true;\n\t\t\t\tresolve(outcome);\n\t\t\t};\n\t\t});\n\t\t/** Resolves false when the run lifecycle ended before the 250ms retry delay elapsed. */\n\t\tconst startupRetryDelay = () =>\n\t\t\tnew Promise<boolean>((resolve) => {\n\t\t\t\tconst timer = setTimeout(() => resolve(true), 250);\n\t\t\t\ttimer.unref?.();\n\t\t\t\tvoid startupPromptsSettled.then(() => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tresolve(false);\n\t\t\t\t});\n\t\t\t});\n\t\tconst deliverStartupPrompts = async () => {\n\t\t\tlet failures = 0;\n\t\t\tfor (let next = 0; next < startupPrompts.length; ) {\n\t\t\t\t// The run lifecycle can settle the barrier while a prompt is being\n\t\t\t\t// admitted; stop instead of prompting a session we already left.\n\t\t\t\tif (startupPromptsDone) return;\n\t\t\t\tif (!this.getCurrentModel()) {\n\t\t\t\t\tif (!(await startupRetryDelay())) return;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst prompt = startupPrompts[next]!;\n\t\t\t\ttry {\n\t\t\t\t\tawait this.agentConnection.prompt(prompt.text, {\n\t\t\t\t\t\timages: prompt.images,\n\t\t\t\t\t\tstreamingBehavior: next === 0 ? \"steer\" : \"followUp\",\n\t\t\t\t\t\tqueueIfBusy: true,\n\t\t\t\t\t\tsignal: startupAdmissionAbort.signal,\n\t\t\t\t\t});\n\t\t\t\t\tfailures = 0;\n\t\t\t\t\tnext++;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (startupPromptsDone || startupAdmissionAbort.signal.aborted) return;\n\t\t\t\t\t// An uncertain daemon admission may already be session-owned. Retrying\n\t\t\t\t\t// would duplicate it; only an acknowledged pre-ownership cancellation is safe.\n\t\t\t\t\tif (error instanceof AgentConnectionPromptAdmissionError && error.status === \"owned\") {\n\t\t\t\t\t\tfailures = 0;\n\t\t\t\t\t\tnext++;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (error instanceof AgentConnectionPromptAdmissionError && !error.cancelled) {\n\t\t\t\t\t\t// This attempt may already be session-owned, so never retry it. Preserve\n\t\t\t\t\t\t// it and every not-yet-attempted startup prompt in original order.\n\t\t\t\t\t\tthis.retainStartupPromptDrafts(startupPrompts.slice(next));\n\t\t\t\t\t\tthis.showError(error.message);\n\t\t\t\t\t\tsettleStartupPrompts(\"retained\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst errorMessage = error instanceof Error ? error.message : \"Unknown error occurred\";\n\t\t\t\t\tif (++failures < 3) {\n\t\t\t\t\t\tthis.showError(errorMessage);\n\t\t\t\t\t\tif (!(await startupRetryDelay())) return;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tthis.showError(`Skipping startup prompt after 3 failed attempts: ${errorMessage}`);\n\t\t\t\t\tfailures = 0;\n\t\t\t\t\tnext++;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tthis.admitPendingStartupPrompts = startupPrompts.length > 0 ? () => startupPromptsSettled : undefined;\n\n\t\tlet deferredStartupNotificationsShown = false;\n\t\tconst showDeferredStartupNotifications = () => {\n\t\t\tif (deferredStartupNotificationsShown) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdeferredStartupNotificationsShown = true;\n\n\t\t\t// The agents view owns these for daemon sessions. When there is no agents view,\n\t\t\t// show them once at the top of a fresh session, but never append them under a\n\t\t\t// restored conversation where they read as disconnected clutter.\n\t\t\tif (!ownsGlobalStartupNotices || this.sessionHasMessages) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvoid newVersionPromise\n\t\t\t\t?.then((newVersion) => {\n\t\t\t\t\tif (newVersion) {\n\t\t\t\t\t\tthis.showNewVersionNotification(newVersion);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch(() => {});\n\n\t\t\tvoid packageUpdatesPromise\n\t\t\t\t?.then((updates) => {\n\t\t\t\t\tif (updates.length > 0) {\n\t\t\t\t\t\tthis.showPackageUpdateNotification(updates);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch(() => {});\n\n\t\t\tvoid tmuxKeyboardWarningPromise\n\t\t\t\t?.then((warning) => {\n\t\t\t\t\tif (warning) {\n\t\t\t\t\t\tthis.showWarning(warning);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch(() => {});\n\t\t};\n\n\t\tlet modelFallbackWarningShown = false;\n\t\tconst showModelFallbackWarning = () => {\n\t\t\tif (modelFallbackWarningShown) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst action = this.getModelFallbackWarningAction(modelFallbackMessage);\n\t\t\tmodelFallbackWarningShown = true;\n\t\t\tif (action === \"show\" && modelFallbackMessage) {\n\t\t\t\tthis.showWarning(modelFallbackMessage);\n\t\t\t}\n\t\t};\n\n\t\tawait this.runStartupOnboarding();\n\t\tshowDeferredStartupNotifications();\n\t\tshowModelFallbackWarning();\n\t\tvoid this.maybeWarnAboutAnthropicSubscriptionAuth();\n\t\tvoid deliverStartupPrompts().then(\n\t\t\t() => settleStartupPrompts(\"admitted\"),\n\t\t\t() => settleStartupPrompts(\"admitted\"),\n\t\t);\n\n\t\t// Enter/Alt+Enter submit directly through AgentConnection. Wait for the\n\t\t// lifecycle signal exactly once; a returned editor value has already been\n\t\t// admitted and must never be submitted again here.\n\t\ttry {\n\t\t\tawait this.getUserInput();\n\t\t} finally {\n\t\t\tstartupAdmissionAbort.abort();\n\t\t\tsettleStartupPrompts(\"lifecycle-cancelled\");\n\t\t\tthis.admitPendingStartupPrompts = undefined;\n\t\t}\n\n\t\tconst state = this.connectionState;\n\t\treturn {\n\t\t\ttype: this.agentsViewRequest ?? \"agents_view\",\n\t\t\tsource: {\n\t\t\t\tactiveSessionId: state?.activeSessionId,\n\t\t\t\tsessionFile: state?.sessionFile,\n\t\t\t\tsessionId: state?.sessionId ?? this.promptStashSessionId ?? \"\",\n\t\t\t\tsessionName: state?.sessionName,\n\t\t\t\tcwd: state?.cwd ?? this.getCurrentCwd(),\n\t\t\t},\n\t\t};\n\t}\n\n\tprivate getModelFallbackWarningAction(modelFallbackMessage: string | undefined): ModelFallbackWarningAction {\n\t\tif (!modelFallbackMessage) {\n\t\t\treturn \"suppress\";\n\t\t}\n\t\t// The no-models warning is a snapshot from whichever process created the\n\t\t// session; trust the live connection over it (e.g. credentials only\n\t\t// visible to the daemon, or added after the snapshot was taken).\n\t\tif (isNoModelsAvailableMessage(modelFallbackMessage) && this.getCurrentModel()) {\n\t\t\treturn \"suppress\";\n\t\t}\n\t\treturn \"show\";\n\t}\n\n\tprivate getOnboardingState(): OnboardingStartupState {\n\t\treturn {\n\t\t\tsettingsManager: this.settingsManager,\n\t\t\tmodelRegistry: this.modelRegistry,\n\t\t\tmodel: this.getCurrentModel(),\n\t\t};\n\t}\n\n\tprivate shouldRunOnboarding(): boolean {\n\t\treturn shouldRunOnboarding(this.getOnboardingState());\n\t}\n\n\tprivate shouldRunPrimeCliOnboardingSplash(): boolean {\n\t\treturn shouldRunPrimeCliOnboardingSplash(this.getOnboardingState());\n\t}\n\n\tprivate markOnboardingShown(): void {\n\t\tif (!this.settingsManager.getOnboardingShown()) {\n\t\t\tthis.settingsManager.setOnboardingShown(true);\n\t\t}\n\t}\n\n\tprivate async runStartupOnboarding(): Promise<boolean> {\n\t\tif (!this.shouldRunOnboarding()) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst showPrimeCliSplash = this.shouldRunPrimeCliOnboardingSplash();\n\t\tthis.markOnboardingShown();\n\t\tawait this.settingsManager.flush();\n\t\tawait this.runOnboardingFlow(showPrimeCliSplash);\n\t\treturn true;\n\t}\n\n\tprivate async showOnboardingModelSelection(splash: OnboardingSplashHandle): Promise<void> {\n\t\ttry {\n\t\t\tawait this.showConfigurationMenu(\"models\");\n\t\t} finally {\n\t\t\tsplash.dismiss();\n\t\t}\n\t}\n\n\tprivate async runOnboardingFlow(showPrimeCliSplash = this.shouldRunPrimeCliOnboardingSplash()): Promise<void> {\n\t\tthis.modelRegistry.refresh();\n\t\tif (showPrimeCliSplash) {\n\t\t\tconst splash = await this.showOnboardingSplash(\"choose a model\");\n\t\t\tif (!splash) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tawait this.showOnboardingModelSelection(splash);\n\t\t\treturn;\n\t\t}\n\n\t\tconst availableModels = await this.getModelCandidates();\n\t\tif (availableModels.length > 0) {\n\t\t\tawait this.showConfigurationMenu(\"models\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst splash = await this.showOnboardingSplash();\n\t\tif (!splash) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tawait this.showConfigurationMenu(\"providers\");\n\t\t} finally {\n\t\t\tsplash.dismiss();\n\t\t}\n\t}\n\n\tprivate getMarkdownThemeWithSettings(): MarkdownTheme {\n\t\treturn {\n\t\t\t...getMarkdownTheme(),\n\t\t\tcodeBlockIndent: this.settingsManager.getCodeBlockIndent(),\n\t\t};\n\t}\n\n\tprivate formatDisplayPath(p: string): string {\n\t\tconst home = os.homedir();\n\t\tlet result = p;\n\n\t\t// Replace home directory with ~\n\t\tif (result.startsWith(home)) {\n\t\t\tresult = `~${result.slice(home.length)}`;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tprivate formatExtensionDisplayPath(path: string): string {\n\t\tlet result = this.formatDisplayPath(path);\n\t\tresult = result.replace(/\\/index\\.ts$/, \"\").replace(/\\/index\\.js$/, \"\");\n\t\treturn result;\n\t}\n\n\tprivate formatContextPath(p: string): string {\n\t\tconst cwd = path.resolve(this.getCurrentCwd());\n\t\tconst absolutePath = path.isAbsolute(p) ? path.resolve(p) : path.resolve(cwd, p);\n\t\tconst relativePath = getCwdRelativePath(absolutePath, cwd);\n\t\tif (relativePath !== undefined) {\n\t\t\treturn relativePath;\n\t\t}\n\n\t\treturn this.formatDisplayPath(absolutePath);\n\t}\n\n\tprivate getStartupExpansionState(): boolean {\n\t\treturn this.options.verbose || this.toolOutputExpanded;\n\t}\n\n\tprivate getShortPath(fullPath: string, sourceInfo?: AgentConnectionSourceInfo): string {\n\t\tconst baseDir = sourceInfo?.baseDir;\n\t\tif (baseDir && this.isPackageSource(sourceInfo)) {\n\t\t\tconst relativePath = path.relative(path.resolve(baseDir), path.resolve(fullPath));\n\t\t\tif (\n\t\t\t\trelativePath &&\n\t\t\t\trelativePath !== \".\" &&\n\t\t\t\t!relativePath.startsWith(\"..\") &&\n\t\t\t\t!relativePath.startsWith(`..${path.sep}`) &&\n\t\t\t\t!path.isAbsolute(relativePath)\n\t\t\t) {\n\t\t\t\treturn relativePath.replace(/\\\\/g, \"/\");\n\t\t\t}\n\t\t}\n\n\t\tconst source = sourceInfo?.source ?? \"\";\n\t\tconst npmMatch = fullPath.match(/node_modules\\/(@?[^/]+(?:\\/[^/]+)?)\\/(.*)/);\n\t\tif (npmMatch && source.startsWith(\"npm:\")) {\n\t\t\treturn npmMatch[2];\n\t\t}\n\n\t\tconst gitMatch = fullPath.match(/git\\/[^/]+\\/[^/]+\\/(.*)/);\n\t\tif (gitMatch && source.startsWith(\"git:\")) {\n\t\t\treturn gitMatch[1];\n\t\t}\n\n\t\treturn this.formatDisplayPath(fullPath);\n\t}\n\n\tprivate getCompactPathLabel(resourcePath: string, sourceInfo?: AgentConnectionSourceInfo): string {\n\t\tconst shortPath = this.getShortPath(resourcePath, sourceInfo);\n\t\tconst normalizedPath = shortPath.replace(/\\\\/g, \"/\");\n\t\tconst segments = normalizedPath.split(\"/\").filter((segment) => segment.length > 0 && segment !== \"~\");\n\t\tif (segments.length > 0) {\n\t\t\treturn segments[segments.length - 1]!;\n\t\t}\n\t\treturn shortPath;\n\t}\n\n\tprivate getCompactPackageSourceLabel(sourceInfo?: AgentConnectionSourceInfo): string {\n\t\tconst source = sourceInfo?.source ?? \"\";\n\t\tif (source.startsWith(\"npm:\")) {\n\t\t\treturn source.slice(\"npm:\".length) || source;\n\t\t}\n\n\t\tconst gitSource = parseGitUrl(source);\n\t\tif (gitSource) {\n\t\t\treturn gitSource.path || source;\n\t\t}\n\n\t\treturn source;\n\t}\n\n\tprivate getCompactExtensionLabel(resourcePath: string, sourceInfo?: AgentConnectionSourceInfo): string {\n\t\tif (!this.isPackageSource(sourceInfo)) {\n\t\t\treturn this.getCompactPathLabel(resourcePath, sourceInfo);\n\t\t}\n\n\t\tconst sourceLabel = this.getCompactPackageSourceLabel(sourceInfo);\n\t\tif (!sourceLabel) {\n\t\t\treturn this.getCompactPathLabel(resourcePath, sourceInfo);\n\t\t}\n\n\t\tconst shortPath = this.getShortPath(resourcePath, sourceInfo).replace(/\\\\/g, \"/\");\n\t\tconst packagePath = shortPath.startsWith(\"extensions/\") ? shortPath.slice(\"extensions/\".length) : shortPath;\n\t\tconst parsedPath = path.posix.parse(packagePath);\n\n\t\tif (parsedPath.name === \"index\") {\n\t\t\treturn !parsedPath.dir || parsedPath.dir === \".\" ? sourceLabel : `${sourceLabel}:${parsedPath.dir}`;\n\t\t}\n\n\t\treturn `${sourceLabel}:${packagePath}`;\n\t}\n\n\tprivate getCompactDisplayPathSegments(resourcePath: string): string[] {\n\t\treturn this.formatDisplayPath(resourcePath)\n\t\t\t.replace(/\\\\/g, \"/\")\n\t\t\t.split(\"/\")\n\t\t\t.filter((segment) => segment.length > 0 && segment !== \"~\");\n\t}\n\n\tprivate getCompactNonPackageExtensionLabel(\n\t\tresourcePath: string,\n\t\tindex: number,\n\t\tallPaths: Array<{ path: string; segments: string[] }>,\n\t): string {\n\t\tconst segments = allPaths[index]?.segments;\n\t\tif (!segments || segments.length === 0) {\n\t\t\treturn this.getCompactPathLabel(resourcePath);\n\t\t}\n\n\t\tfor (let segmentCount = 1; segmentCount <= segments.length; segmentCount += 1) {\n\t\t\tconst candidate = segments.slice(-segmentCount).join(\"/\");\n\t\t\tconst isUnique = allPaths.every((item, itemIndex) => {\n\t\t\t\tif (itemIndex === index) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn item.segments.slice(-segmentCount).join(\"/\") !== candidate;\n\t\t\t});\n\n\t\t\tif (isUnique) {\n\t\t\t\treturn candidate;\n\t\t\t}\n\t\t}\n\n\t\treturn segments.join(\"/\");\n\t}\n\n\tprivate getCompactExtensionLabels(\n\t\textensions: Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>,\n\t): string[] {\n\t\tconst nonPackageExtensions = extensions\n\t\t\t.map((extension) => {\n\t\t\t\tconst segments = this.getCompactDisplayPathSegments(extension.path);\n\t\t\t\tconst lastSegment = segments[segments.length - 1];\n\t\t\t\tif (segments.length > 1 && (lastSegment === \"index.ts\" || lastSegment === \"index.js\")) {\n\t\t\t\t\tsegments.pop();\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tpath: extension.path,\n\t\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t\t\tsegments,\n\t\t\t\t};\n\t\t\t})\n\t\t\t.filter((extension) => !this.isPackageSource(extension.sourceInfo));\n\n\t\treturn extensions.map((extension) => {\n\t\t\tif (this.isPackageSource(extension.sourceInfo)) {\n\t\t\t\treturn this.getCompactExtensionLabel(extension.path, extension.sourceInfo);\n\t\t\t}\n\n\t\t\tconst nonPackageIndex = nonPackageExtensions.findIndex((item) => item.path === extension.path);\n\t\t\tif (nonPackageIndex === -1) {\n\t\t\t\treturn this.getCompactPathLabel(extension.path, extension.sourceInfo);\n\t\t\t}\n\n\t\t\treturn this.getCompactNonPackageExtensionLabel(extension.path, nonPackageIndex, nonPackageExtensions);\n\t\t});\n\t}\n\n\tprivate getDisplaySourceInfo(sourceInfo?: AgentConnectionSourceInfo): {\n\t\tlabel: string;\n\t\tscopeLabel?: string;\n\t\tcolor: \"accent\" | \"muted\";\n\t} {\n\t\tconst source = sourceInfo?.source ?? \"local\";\n\t\tconst scope = sourceInfo?.scope ?? \"project\";\n\t\tif (source === \"local\") {\n\t\t\tif (scope === \"user\") {\n\t\t\t\treturn { label: \"user\", color: \"muted\" };\n\t\t\t}\n\t\t\tif (scope === \"project\") {\n\t\t\t\treturn { label: \"project\", color: \"muted\" };\n\t\t\t}\n\t\t\tif (scope === \"temporary\") {\n\t\t\t\treturn { label: \"path\", scopeLabel: \"temp\", color: \"muted\" };\n\t\t\t}\n\t\t\treturn { label: \"path\", color: \"muted\" };\n\t\t}\n\n\t\tif (source === \"cli\") {\n\t\t\treturn { label: \"path\", scopeLabel: scope === \"temporary\" ? \"temp\" : undefined, color: \"muted\" };\n\t\t}\n\n\t\tconst scopeLabel =\n\t\t\tscope === \"user\" ? \"user\" : scope === \"project\" ? \"project\" : scope === \"temporary\" ? \"temp\" : undefined;\n\t\treturn { label: source, scopeLabel, color: \"accent\" };\n\t}\n\n\tprivate getScopeGroup(sourceInfo?: AgentConnectionSourceInfo): \"user\" | \"project\" | \"path\" {\n\t\tconst source = sourceInfo?.source ?? \"local\";\n\t\tconst scope = sourceInfo?.scope ?? \"project\";\n\t\tif (source === \"cli\" || scope === \"temporary\") return \"path\";\n\t\tif (scope === \"user\") return \"user\";\n\t\tif (scope === \"project\") return \"project\";\n\t\treturn \"path\";\n\t}\n\n\tprivate isPackageSource(sourceInfo?: AgentConnectionSourceInfo): boolean {\n\t\tconst source = sourceInfo?.source ?? \"\";\n\t\treturn source.startsWith(\"npm:\") || source.startsWith(\"git:\");\n\t}\n\n\tprivate buildScopeGroups(items: Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>): Array<{\n\t\tscope: \"user\" | \"project\" | \"path\";\n\t\tpaths: Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>;\n\t\tpackages: Map<string, Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>>;\n\t}> {\n\t\tconst groups: Record<\n\t\t\t\"user\" | \"project\" | \"path\",\n\t\t\t{\n\t\t\t\tscope: \"user\" | \"project\" | \"path\";\n\t\t\t\tpaths: Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>;\n\t\t\t\tpackages: Map<string, Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>>;\n\t\t\t}\n\t\t> = {\n\t\t\tuser: { scope: \"user\", paths: [], packages: new Map() },\n\t\t\tproject: { scope: \"project\", paths: [], packages: new Map() },\n\t\t\tpath: { scope: \"path\", paths: [], packages: new Map() },\n\t\t};\n\n\t\tfor (const item of items) {\n\t\t\tconst groupKey = this.getScopeGroup(item.sourceInfo);\n\t\t\tconst group = groups[groupKey];\n\t\t\tconst source = item.sourceInfo?.source ?? \"local\";\n\n\t\t\tif (this.isPackageSource(item.sourceInfo)) {\n\t\t\t\tconst list = group.packages.get(source) ?? [];\n\t\t\t\tlist.push(item);\n\t\t\t\tgroup.packages.set(source, list);\n\t\t\t} else {\n\t\t\t\tgroup.paths.push(item);\n\t\t\t}\n\t\t}\n\n\t\treturn [groups.project, groups.user, groups.path].filter(\n\t\t\t(group) => group.paths.length > 0 || group.packages.size > 0,\n\t\t);\n\t}\n\n\tprivate formatScopeGroups(\n\t\tgroups: Array<{\n\t\t\tscope: \"user\" | \"project\" | \"path\";\n\t\t\tpaths: Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>;\n\t\t\tpackages: Map<string, Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>>;\n\t\t}>,\n\t\toptions: {\n\t\t\tformatPath: (item: { path: string; sourceInfo?: AgentConnectionSourceInfo }) => string;\n\t\t\tformatPackagePath: (item: { path: string; sourceInfo?: AgentConnectionSourceInfo }, source: string) => string;\n\t\t},\n\t): string {\n\t\tconst lines: string[] = [];\n\n\t\tfor (const group of groups) {\n\t\t\tlines.push(` ${theme.fg(\"accent\", group.scope)}`);\n\n\t\t\tconst sortedPaths = [...group.paths].sort((a, b) => a.path.localeCompare(b.path));\n\t\t\tfor (const item of sortedPaths) {\n\t\t\t\tlines.push(theme.fg(\"dim\", ` ${options.formatPath(item)}`));\n\t\t\t}\n\n\t\t\tconst sortedPackages = Array.from(group.packages.entries()).sort(([a], [b]) => a.localeCompare(b));\n\t\t\tfor (const [source, items] of sortedPackages) {\n\t\t\t\tlines.push(` ${theme.fg(\"mdLink\", source)}`);\n\t\t\t\tconst sortedPackagePaths = [...items].sort((a, b) => a.path.localeCompare(b.path));\n\t\t\t\tfor (const item of sortedPackagePaths) {\n\t\t\t\t\tlines.push(theme.fg(\"dim\", ` ${options.formatPackagePath(item, source)}`));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn lines.join(\"\\n\");\n\t}\n\n\tprivate findSourceInfoForPath(\n\t\tp: string,\n\t\tsourceInfos: Map<string, AgentConnectionSourceInfo>,\n\t): AgentConnectionSourceInfo | undefined {\n\t\tconst exact = sourceInfos.get(p);\n\t\tif (exact) return exact;\n\n\t\tlet current = p;\n\t\twhile (current.includes(\"/\")) {\n\t\t\tcurrent = current.substring(0, current.lastIndexOf(\"/\"));\n\t\t\tconst parent = sourceInfos.get(current);\n\t\t\tif (parent) return parent;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\tprivate formatPathWithSource(p: string, sourceInfo?: AgentConnectionSourceInfo): string {\n\t\tif (sourceInfo) {\n\t\t\tconst shortPath = this.getShortPath(p, sourceInfo);\n\t\t\tconst { label, scopeLabel } = this.getDisplaySourceInfo(sourceInfo);\n\t\t\tconst labelText = scopeLabel ? `${label} (${scopeLabel})` : label;\n\t\t\treturn `${labelText} ${shortPath}`;\n\t\t}\n\t\treturn this.formatDisplayPath(p);\n\t}\n\n\tprivate formatDiagnostics(\n\t\tdiagnostics: readonly AgentConnectionResourceDiagnostic[],\n\t\tsourceInfos: Map<string, AgentConnectionSourceInfo>,\n\t): string {\n\t\tconst lines: string[] = [];\n\n\t\t// Group collision diagnostics by name\n\t\tconst collisions = new Map<string, AgentConnectionResourceDiagnostic[]>();\n\t\tconst otherDiagnostics: AgentConnectionResourceDiagnostic[] = [];\n\n\t\tfor (const d of diagnostics) {\n\t\t\tif (d.type === \"collision\" && d.collision) {\n\t\t\t\tconst list = collisions.get(d.collision.name) ?? [];\n\t\t\t\tlist.push(d);\n\t\t\t\tcollisions.set(d.collision.name, list);\n\t\t\t} else {\n\t\t\t\totherDiagnostics.push(d);\n\t\t\t}\n\t\t}\n\n\t\t// Format collision diagnostics grouped by name\n\t\tfor (const [name, collisionList] of collisions) {\n\t\t\tconst first = collisionList[0]?.collision;\n\t\t\tif (!first) continue;\n\t\t\tlines.push(theme.fg(\"warning\", ` \"${name}\" collision:`));\n\t\t\tlines.push(\n\t\t\t\ttheme.fg(\n\t\t\t\t\t\"dim\",\n\t\t\t\t\t` ${theme.fg(\"success\", \"✓\")} ${this.formatPathWithSource(first.winnerPath, this.findSourceInfoForPath(first.winnerPath, sourceInfos))}`,\n\t\t\t\t),\n\t\t\t);\n\t\t\tfor (const d of collisionList) {\n\t\t\t\tif (d.collision) {\n\t\t\t\t\tlines.push(\n\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t\t` ${theme.fg(\"warning\", \"✗\")} ${this.formatPathWithSource(d.collision.loserPath, this.findSourceInfoForPath(d.collision.loserPath, sourceInfos))} (skipped)`,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst formatMessageLines = (diagnostic: AgentConnectionResourceDiagnostic, indent: number): string[] => {\n\t\t\tconst color = diagnostic.type === \"error\" ? \"error\" : \"warning\";\n\t\t\tconst prefix = \" \".repeat(indent);\n\t\t\treturn diagnostic.message.split(\"\\n\").map((line) => theme.fg(color, `${prefix}${line}`));\n\t\t};\n\n\t\tfor (const d of otherDiagnostics) {\n\t\t\tif (d.path) {\n\t\t\t\tconst formattedPath = this.formatPathWithSource(d.path, this.findSourceInfoForPath(d.path, sourceInfos));\n\t\t\t\tlines.push(theme.fg(d.type === \"error\" ? \"error\" : \"warning\", ` ${formattedPath}`));\n\t\t\t\tlines.push(...formatMessageLines(d, 4));\n\t\t\t} else {\n\t\t\t\tlines.push(...formatMessageLines(d, 2));\n\t\t\t}\n\t\t}\n\n\t\treturn lines.join(\"\\n\");\n\t}\n\n\tprivate showLoadedResources(options?: {\n\t\textensions?: Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>;\n\t\tforce?: boolean;\n\t\tshowDiagnosticsWhenQuiet?: boolean;\n\t}): void {\n\t\tconst showListing = options?.force === true || this.options.verbose === true;\n\t\tconst showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true;\n\t\tif (!showListing && !showDiagnostics) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst sectionHeader = (name: string, color: ThemeColor = \"mdHeading\") => theme.fg(color, `[${name}]`);\n\t\tconst diagnosticsHeader = (name: string, diagnostics: readonly AgentConnectionResourceDiagnostic[]): string => {\n\t\t\tif (diagnostics.some((diagnostic) => diagnostic.type === \"collision\")) {\n\t\t\t\treturn `${name} conflicts`;\n\t\t\t}\n\n\t\t\tconst errorCount = diagnostics.filter((diagnostic) => diagnostic.type === \"error\").length;\n\t\t\tconst warningCount = diagnostics.filter((diagnostic) => diagnostic.type === \"warning\").length;\n\t\t\tif (errorCount > 0 && warningCount > 0) {\n\t\t\t\treturn `${name} diagnostics`;\n\t\t\t}\n\t\t\tif (errorCount > 0) {\n\t\t\t\treturn `${name} error${errorCount === 1 ? \"\" : \"s\"}`;\n\t\t\t}\n\t\t\tif (warningCount > 0) {\n\t\t\t\treturn `${name} warning${warningCount === 1 ? \"\" : \"s\"}`;\n\t\t\t}\n\n\t\t\treturn `${name} diagnostics`;\n\t\t};\n\t\tconst formatCompactList = (items: string[], options?: { sort?: boolean }): string => {\n\t\t\tconst labels = items.map((item) => item.trim()).filter((item) => item.length > 0);\n\t\t\tif (options?.sort !== false) {\n\t\t\t\tlabels.sort((a, b) => a.localeCompare(b));\n\t\t\t}\n\t\t\treturn theme.fg(\"dim\", ` ${labels.join(\", \")}`);\n\t\t};\n\t\tconst addLoadedSection = (\n\t\t\tname: string,\n\t\t\tcollapsedBody: string,\n\t\t\texpandedBody = collapsedBody,\n\t\t\tcolor: ThemeColor = \"mdHeading\",\n\t\t): void => {\n\t\t\tconst section = new ExpandableText(\n\t\t\t\t() => `${sectionHeader(name, color)}\\n${collapsedBody}`,\n\t\t\t\t() => `${sectionHeader(name, color)}\\n${expandedBody}`,\n\t\t\t\tthis.getStartupExpansionState(),\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t);\n\t\t\tthis.chatContainer.addChild(section);\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t};\n\n\t\tconst resourceSnapshot = this.connectionResourceSnapshot;\n\t\tconst skills = resourceSnapshot?.skills ?? [];\n\t\tconst prompts = resourceSnapshot?.prompts ?? [];\n\t\tconst loadedThemes = resourceSnapshot?.themes ?? [];\n\t\tconst contextFiles = resourceSnapshot?.contextFiles ?? [];\n\t\tconst extensions = options?.extensions ?? resourceSnapshot?.extensions ?? [];\n\t\tconst sourceInfos = new Map<string, AgentConnectionSourceInfo>();\n\t\tfor (const extension of extensions) {\n\t\t\tif (extension.sourceInfo) {\n\t\t\t\tsourceInfos.set(extension.path, extension.sourceInfo);\n\t\t\t}\n\t\t}\n\t\tfor (const skill of skills) {\n\t\t\tif (skill.sourceInfo) {\n\t\t\t\tsourceInfos.set(skill.filePath, skill.sourceInfo);\n\t\t\t}\n\t\t}\n\t\tfor (const prompt of prompts) {\n\t\t\tif (prompt.sourceInfo) {\n\t\t\t\tsourceInfos.set(prompt.filePath, prompt.sourceInfo);\n\t\t\t}\n\t\t}\n\t\tfor (const loadedTheme of loadedThemes) {\n\t\t\tif (loadedTheme.sourcePath && loadedTheme.sourceInfo) {\n\t\t\t\tsourceInfos.set(loadedTheme.sourcePath, loadedTheme.sourceInfo);\n\t\t\t}\n\t\t}\n\n\t\tif (showListing) {\n\t\t\tif (contextFiles.length > 0) {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tconst contextList = contextFiles\n\t\t\t\t\t.map((f) => theme.fg(\"dim\", ` ${this.formatDisplayPath(f.path)}`))\n\t\t\t\t\t.join(\"\\n\");\n\t\t\t\tconst contextCompactList = formatCompactList(\n\t\t\t\t\tcontextFiles.map((contextFile) => this.formatContextPath(contextFile.path)),\n\t\t\t\t\t{ sort: false },\n\t\t\t\t);\n\t\t\t\taddLoadedSection(\"Context\", contextCompactList, contextList);\n\t\t\t}\n\n\t\t\tif (skills.length > 0) {\n\t\t\t\tconst groups = this.buildScopeGroups(\n\t\t\t\t\tskills.map((skill) => ({ path: skill.filePath, sourceInfo: skill.sourceInfo })),\n\t\t\t\t);\n\t\t\t\tconst skillList = this.formatScopeGroups(groups, {\n\t\t\t\t\tformatPath: (item) => this.formatDisplayPath(item.path),\n\t\t\t\t\tformatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),\n\t\t\t\t});\n\t\t\t\tconst skillCompactList = formatCompactList(skills.map((skill) => skill.name));\n\t\t\t\taddLoadedSection(\"Skills\", skillCompactList, skillList);\n\t\t\t}\n\n\t\t\tif (prompts.length > 0) {\n\t\t\t\tconst groups = this.buildScopeGroups(\n\t\t\t\t\tprompts.map((template) => ({ path: template.filePath, sourceInfo: template.sourceInfo })),\n\t\t\t\t);\n\t\t\t\tconst templateByPath = new Map(prompts.map((t) => [t.filePath, t]));\n\t\t\t\tconst templateList = this.formatScopeGroups(groups, {\n\t\t\t\t\tformatPath: (item) => {\n\t\t\t\t\t\tconst template = templateByPath.get(item.path);\n\t\t\t\t\t\treturn template ? `/${template.name}` : this.formatDisplayPath(item.path);\n\t\t\t\t\t},\n\t\t\t\t\tformatPackagePath: (item) => {\n\t\t\t\t\t\tconst template = templateByPath.get(item.path);\n\t\t\t\t\t\treturn template ? `/${template.name}` : this.formatDisplayPath(item.path);\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t\tconst promptCompactList = formatCompactList(prompts.map((template) => `/${template.name}`));\n\t\t\t\taddLoadedSection(\"Prompts\", promptCompactList, templateList);\n\t\t\t}\n\n\t\t\tif (extensions.length > 0) {\n\t\t\t\tconst groups = this.buildScopeGroups(extensions);\n\t\t\t\tconst extList = this.formatScopeGroups(groups, {\n\t\t\t\t\tformatPath: (item) => this.formatExtensionDisplayPath(item.path),\n\t\t\t\t\tformatPackagePath: (item) =>\n\t\t\t\t\t\tthis.formatExtensionDisplayPath(this.getShortPath(item.path, item.sourceInfo)),\n\t\t\t\t});\n\t\t\t\tconst extensionCompactList = formatCompactList(this.getCompactExtensionLabels(extensions));\n\t\t\t\taddLoadedSection(\"Extensions\", extensionCompactList, extList, \"mdHeading\");\n\t\t\t}\n\n\t\t\t// Show loaded themes (excluding built-in)\n\t\t\tconst customThemes = loadedThemes.filter((t) => t.sourcePath);\n\t\t\tif (customThemes.length > 0) {\n\t\t\t\tconst groups = this.buildScopeGroups(\n\t\t\t\t\tcustomThemes.map((loadedTheme) => ({\n\t\t\t\t\t\tpath: loadedTheme.sourcePath!,\n\t\t\t\t\t\tsourceInfo: loadedTheme.sourceInfo,\n\t\t\t\t\t})),\n\t\t\t\t);\n\t\t\t\tconst themeList = this.formatScopeGroups(groups, {\n\t\t\t\t\tformatPath: (item) => this.formatDisplayPath(item.path),\n\t\t\t\t\tformatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),\n\t\t\t\t});\n\t\t\t\tconst themeCompactList = formatCompactList(\n\t\t\t\t\tcustomThemes.map(\n\t\t\t\t\t\t(loadedTheme) =>\n\t\t\t\t\t\t\tloadedTheme.name ?? this.getCompactPathLabel(loadedTheme.sourcePath!, loadedTheme.sourceInfo),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\taddLoadedSection(\"Themes\", themeCompactList, themeList);\n\t\t\t}\n\t\t}\n\n\t\tif (showDiagnostics) {\n\t\t\tconst skillDiagnostics = resourceSnapshot?.diagnostics.skills ?? [];\n\t\t\tif (skillDiagnostics.length > 0) {\n\t\t\t\tconst warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos);\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(\n\t\t\t\t\t\t`${sectionHeader(diagnosticsHeader(\"Skill\", skillDiagnostics), \"warning\")}\\n${warningLines}`,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t}\n\n\t\t\tconst promptDiagnostics = resourceSnapshot?.diagnostics.prompts ?? [];\n\t\t\tif (promptDiagnostics.length > 0) {\n\t\t\t\tconst warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos);\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(\n\t\t\t\t\t\t`${sectionHeader(diagnosticsHeader(\"Prompt\", promptDiagnostics), \"warning\")}\\n${warningLines}`,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t}\n\n\t\t\tconst extensionDiagnostics: AgentConnectionResourceDiagnostic[] = [\n\t\t\t\t...(resourceSnapshot?.diagnostics.extensions ?? []),\n\t\t\t];\n\n\t\t\tif (this.bindLocalSessionExtensions) {\n\t\t\t\tconst commandDiagnostics = this.getLocalSessionHost().getExtensionRunner().getCommandDiagnostics();\n\t\t\t\textensionDiagnostics.push(...commandDiagnostics);\n\t\t\t}\n\t\t\textensionDiagnostics.push(...this.getBuiltInCommandConflictDiagnostics(this.connectionCommands));\n\n\t\t\tif (this.bindLocalSessionExtensions) {\n\t\t\t\tconst shortcutDiagnostics = this.getLocalSessionHost().getExtensionRunner().getShortcutDiagnostics();\n\t\t\t\textensionDiagnostics.push(...shortcutDiagnostics);\n\t\t\t}\n\n\t\t\tif (extensionDiagnostics.length > 0) {\n\t\t\t\tconst warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos);\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(\n\t\t\t\t\t\t`${sectionHeader(diagnosticsHeader(\"Extension\", extensionDiagnostics), \"warning\")}\\n${warningLines}`,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t}\n\n\t\t\tconst themeDiagnostics = resourceSnapshot?.diagnostics.themes ?? [];\n\t\t\tif (themeDiagnostics.length > 0) {\n\t\t\t\tconst warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos);\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(\n\t\t\t\t\t\t`${sectionHeader(diagnosticsHeader(\"Theme\", themeDiagnostics), \"warning\")}\\n${warningLines}`,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Initialize the extension system with TUI-based UI context.\n\t */\n\tprivate async bindCurrentSessionExtensions(): Promise<void> {\n\t\tconst localSessionHost = this.getLocalSessionHost();\n\t\tconst uiContext = this.createExtensionUIContext();\n\t\tawait localSessionHost.bindExtensions({\n\t\t\tuiContext,\n\t\t\tcommandContextActions: {\n\t\t\t\twaitForIdle: () => this.agentConnection.waitForIdle(),\n\t\t\t\tnewSession: async (options) => {\n\t\t\t\t\tthis.stopWorkingLoader();\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result =\n\t\t\t\t\t\t\toptions?.setup || options?.withSession\n\t\t\t\t\t\t\t\t? await localSessionHost.newSession(options)\n\t\t\t\t\t\t\t\t: await this.agentConnection.newSession(\n\t\t\t\t\t\t\t\t\t\toptions?.parentSession ? { parentSession: options.parentSession } : undefined,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\tif (!result.cancelled) {\n\t\t\t\t\t\t\tawait this.renderCurrentSessionState();\n\t\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t} catch (error: unknown) {\n\t\t\t\t\t\treturn this.handleFatalRuntimeError(\"Failed to create session\", error);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tfork: async (entryId, options) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = options?.withSession\n\t\t\t\t\t\t\t? await localSessionHost.fork(entryId, options)\n\t\t\t\t\t\t\t: await this.agentConnection.fork(entryId, { position: options?.position });\n\t\t\t\t\t\tif (!result.cancelled) {\n\t\t\t\t\t\t\tawait this.renderCurrentSessionState();\n\t\t\t\t\t\t\tthis.editor.setText(\"selectedText\" in result ? (result.selectedText ?? \"\") : \"\");\n\t\t\t\t\t\t\tthis.showStatus(\"Forked to new session\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn { cancelled: result.cancelled };\n\t\t\t\t\t} catch (error: unknown) {\n\t\t\t\t\t\treturn this.handleFatalRuntimeError(\"Failed to fork session\", error);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tnavigateTree: async (targetId, options) => {\n\t\t\t\t\tconst result = await this.agentConnection.navigateTree(targetId, {\n\t\t\t\t\t\tsummarize: options?.summarize,\n\t\t\t\t\t\tcustomInstructions: options?.customInstructions,\n\t\t\t\t\t\treplaceInstructions: options?.replaceInstructions,\n\t\t\t\t\t\tlabel: options?.label,\n\t\t\t\t\t});\n\t\t\t\t\tif (result.cancelled) {\n\t\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t\t}\n\n\t\t\t\t\tawait this.renderTreeNavigation(result);\n\t\t\t\t\treturn { cancelled: false };\n\t\t\t\t},\n\t\t\t\tswitchSession: async (sessionPath, options) => {\n\t\t\t\t\treturn this.handleResumeSession(sessionPath, options);\n\t\t\t\t},\n\t\t\t\treload: async () => {\n\t\t\t\t\tawait this.handleReloadCommand();\n\t\t\t\t},\n\t\t\t},\n\t\t\tshutdownHandler: () => {\n\t\t\t\tthis.shutdownRequested = true;\n\t\t\t\tif (!this.isAgentStreaming()) {\n\t\t\t\t\tvoid this.shutdown();\n\t\t\t\t}\n\t\t\t},\n\t\t\tonError: (error) => {\n\t\t\t\tthis.showExtensionError(error.extensionPath, error.error, error.stack);\n\t\t\t},\n\t\t});\n\n\t\tsetRegisteredThemes(this.uiServices.getThemes());\n\t\tawait this.refreshConnectionCatalog();\n\t\tthis.setupAutocompleteProvider();\n\n\t\tconst extensionRunner = localSessionHost.getExtensionRunner();\n\t\tthis.setupExtensionShortcuts(extensionRunner);\n\t\tthis.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });\n\t}\n\n\tprivate applyRuntimeSettings(): void {\n\t\tthis.footer.setAutoCompactEnabled(\n\t\t\tthis.connectionState?.autoCompactionEnabled ?? this.settingsManager.getCompactionEnabled(),\n\t\t);\n\t\tthis.footerDataProvider.setCwd(this.getCurrentCwd());\n\t\tthis.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();\n\t\tthis.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());\n\t\tthis.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());\n\t\tconst editorPaddingX = this.settingsManager.getEditorPaddingX();\n\t\tconst autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();\n\t\tthis.defaultEditor.setPaddingX(editorPaddingX);\n\t\tthis.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);\n\t\tif (this.editor !== this.defaultEditor) {\n\t\t\tthis.editor.setPaddingX?.(editorPaddingX);\n\t\t\tthis.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);\n\t\t}\n\t}\n\n\tprivate async refreshConnectionQueue(): Promise<void> {\n\t\tthis.replaceConnectionQueue(await this.agentConnection.getQueue());\n\t}\n\n\tprivate replaceConnectionQueue(queue: AgentConnectionQueueState): void {\n\t\tthis.connectionQueue = {\n\t\t\tsteering: [...queue.steering],\n\t\t\tfollowUp: [...queue.followUp],\n\t\t};\n\t\tconst dropped = this.queueSelection.sync(this.connectionQueue);\n\t\tif (dropped !== undefined) {\n\t\t\tconst editorText = this.editor.getText();\n\t\t\tif (editorText === dropped) {\n\t\t\t\tthis.setEditorTextFromQueueSelection(this.queueSelection.reset());\n\t\t\t} else if (!this.pendingQueueEdit) {\n\t\t\t\tthis.queueSelection.replaceDraft(editorText);\n\t\t\t}\n\t\t}\n\t\tthis.updatePendingMessagesDisplay();\n\t}\n\n\tprivate async refreshConnectionCatalog(): Promise<void> {\n\t\tthis.invalidateConnectionModelRefresh();\n\t\tconst [state, commands, modelCatalog, resources] = await Promise.all([\n\t\t\tthis.agentConnection.getState(),\n\t\t\tthis.agentConnection.getCommands().catch(() => []),\n\t\t\tthis.agentConnection.getModelCatalog(),\n\t\t\tthis.agentConnection.getResourceSnapshot(),\n\t\t]);\n\t\tthis.applyConnectionStateSnapshot(state);\n\t\tthis.connectionCommands = commands;\n\t\tthis.applyConnectionModelCatalog(modelCatalog);\n\t\tthis.connectionModelsFetchedAt = Date.now();\n\t\tthis.connectionResourceSnapshot = resources;\n\t}\n\n\tprivate refreshHeartbeatCatalog(): Promise<void> {\n\t\tif (this.heartbeatRefreshPromise) {\n\t\t\tthis.heartbeatRefreshRequested = true;\n\t\t\treturn this.heartbeatRefreshPromise;\n\t\t}\n\t\tconst connection = this.agentConnection;\n\t\tconst refresh = (async () => {\n\t\t\tdo {\n\t\t\t\tthis.heartbeatRefreshRequested = false;\n\t\t\t\tconst heartbeats = await connection.listHeartbeats();\n\t\t\t\tif (this.agentConnection !== connection) return;\n\t\t\t\tthis.applyHeartbeatCatalog(heartbeats);\n\t\t\t} while (this.heartbeatRefreshRequested);\n\t\t})().finally(() => {\n\t\t\tif (this.heartbeatRefreshPromise === refresh) {\n\t\t\t\tthis.heartbeatRefreshPromise = undefined;\n\t\t\t}\n\t\t});\n\t\tthis.heartbeatRefreshPromise = refresh;\n\t\treturn refresh;\n\t}\n\n\tprivate applyHeartbeatCatalog(heartbeats: AgentConnectionHeartbeat[]): void {\n\t\tthis.heartbeatCatalog = heartbeats;\n\t\tthis.updateScopedHeartbeats();\n\t}\n\n\tprivate updateScopedHeartbeats(): void {\n\t\tconst heartbeats = scopeHeartbeatsToSession(\n\t\t\tthis.heartbeatCatalog,\n\t\t\tthis.connectionState,\n\t\t\tthis.subagentSnapshots.values(),\n\t\t);\n\t\tif (\n\t\t\theartbeats.length === this.heartbeats.length &&\n\t\t\theartbeats.every((heartbeat, index) => heartbeat === this.heartbeats[index])\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tthis.heartbeats = heartbeats;\n\t\tthis.heartbeatManager?.setHeartbeats(heartbeats);\n\t\tthis.scheduleHeartbeatManagerRefresh();\n\t\tthis.updateSubagentSummaryLine();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate applyConnectionStateSnapshot(state: AgentConnectionState): void {\n\t\tthis.bindPromptStashSession(state.sessionId);\n\t\tthis.connectionState = state;\n\t\tthis.updateScopedHeartbeats();\n\t\t// Don't touch contextUsageTokenBaseline: a mid-stream snapshot reflects only completed\n\t\t// turns (the in-flight message isn't persisted yet), so the in-flight delta must keep\n\t\t// accumulating. The baseline is managed at turn end (refreshConnectionContextUsage) and\n\t\t// reset on a new user message.\n\t\tthis.footer.setAutoCompactEnabled(state.autoCompactionEnabled);\n\t\tthis.sessionRecap = state.recap;\n\t\tthis.renderRecap();\n\t\tthis.updateWorkingPulse();\n\t}\n\n\tprivate patchConnectionState(patch: Partial<AgentConnectionState>): void {\n\t\tif (!this.connectionState) {\n\t\t\treturn;\n\t\t}\n\t\tthis.connectionState = { ...this.connectionState, ...patch };\n\t\tthis.updateWorkingPulse();\n\t}\n\n\t// Bake this attempt's output into the snapshot so the tray doesn't dip in the gap between\n\t// isStreaming clearing and the async refresh landing.\n\tprivate applyOptimisticContextUsage(): void {\n\t\tconst snapshot = this.connectionState?.contextUsage;\n\t\tif (!snapshot || snapshot.tokens === null || snapshot.contextWindow <= 0) return;\n\t\tconst completed = Math.max(0, this.activityTracker.getStatus().tokens - this.contextUsageTokenBaseline);\n\t\tif (completed <= 0) return;\n\t\tconst tokens = snapshot.tokens + completed;\n\t\tthis.patchConnectionState({\n\t\t\tcontextUsage: {\n\t\t\t\ttokens,\n\t\t\t\tcontextWindow: snapshot.contextWindow,\n\t\t\t\tpercent: (tokens / snapshot.contextWindow) * 100,\n\t\t\t},\n\t\t});\n\t}\n\n\t/** Refresh the tray's context usage from the session after a turn or compaction completes. */\n\tprivate async refreshConnectionContextUsage(): Promise<void> {\n\t\tconst generation = ++this.contextUsageRefresh.generation;\n\t\tconst connection = this.agentConnection;\n\t\tconst sessionId = this.connectionState?.sessionId;\n\t\tconst stats = await connection?.getSessionStats?.().catch(() => undefined);\n\t\tif (!stats) return;\n\t\t// Drop results superseded by a newer successful refresh as well as results for a replaced session.\n\t\tif (\n\t\t\tgeneration < this.contextUsageRefresh.lastSuccessGeneration ||\n\t\t\tthis.agentConnection !== connection ||\n\t\t\tthis.connectionState?.sessionId !== sessionId\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tthis.contextUsageRefresh.lastSuccessGeneration = generation;\n\t\t// Anything counted so far is now reflected in the snapshot; only later output is in-flight.\n\t\tthis.contextUsageTokenBaseline = this.activityTracker.getStatus().tokens;\n\t\tthis.patchConnectionState({ contextUsage: stats.contextUsage });\n\t}\n\n\tprivate updateConnectionStateFromEvent(event: AgentConnectionSessionEvent): void {\n\t\tif (!this.connectionState) {\n\t\t\treturn;\n\t\t}\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\t\tthis.patchConnectionState({ isStreaming: true, activeToolNames: [] });\n\t\t\t\tbreak;\n\t\t\tcase \"agent_end\":\n\t\t\t\tthis.patchConnectionState({ isStreaming: false, activeToolNames: [] });\n\t\t\t\tbreak;\n\t\t\tcase \"session_action_update\":\n\t\t\t\tthis.patchConnectionState({ sessionActions: event.actions });\n\t\t\t\tbreak;\n\t\t\tcase \"compaction_start\":\n\t\t\t\tthis.patchConnectionState({ isCompacting: true });\n\t\t\t\tbreak;\n\t\t\tcase \"compaction_end\":\n\t\t\t\tthis.patchConnectionState({ isCompacting: false });\n\t\t\t\tbreak;\n\t\t\tcase \"session_info_changed\":\n\t\t\t\tthis.patchConnectionState({ sessionName: event.name });\n\t\t\t\tbreak;\n\t\t\tcase \"thinking_level_changed\":\n\t\t\t\tthis.patchConnectionState({ thinkingLevel: event.level });\n\t\t\t\tbreak;\n\t\t\tcase \"service_tier_changed\":\n\t\t\t\tthis.patchConnectionState({ serviceTier: event.serviceTier });\n\t\t\t\tbreak;\n\t\t\tcase \"auto_retry_start\":\n\t\t\t\tthis.patchConnectionState({ retryAttempt: event.attempt });\n\t\t\t\tbreak;\n\t\t\tcase \"auto_retry_end\":\n\t\t\t\tthis.patchConnectionState({ retryAttempt: 0 });\n\t\t\t\tbreak;\n\t\t\tcase \"goal_update\":\n\t\t\t\tthis.patchConnectionState({ goal: event.goal });\n\t\t\t\tbreak;\n\t\t\tcase \"bash_start\":\n\t\t\t\tthis.patchConnectionState({ isBashRunning: true });\n\t\t\t\tbreak;\n\t\t\tcase \"bash_end\":\n\t\t\t\tthis.patchConnectionState({ isBashRunning: false });\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate getCurrentCwd(): string {\n\t\treturn this.connectionState?.cwd ?? this.uiServices.getInitialCwd();\n\t}\n\n\tprivate getCurrentSessionName(): string | undefined {\n\t\treturn this.connectionState?.sessionName ?? this.uiServices.getInitialSessionName();\n\t}\n\n\tprivate applyAuthStaleEvent(event: Extract<AgentConnectionSessionEvent, { type: \"auth_stale\" }>): void {\n\t\tlet marked = false;\n\t\tfor (const token of event.sourceTokens ?? []) {\n\t\t\tmarked = this.modelRegistry.markProviderAuthSourceStale(token) || marked;\n\t\t}\n\t\tif (!marked) {\n\t\t\tthis.modelRegistry.markProviderAuthStale(event.provider);\n\t\t}\n\t\tthis.footer.invalidate();\n\t\tthis.updateEditorBorderColor();\n\t}\n\n\tprivate getCurrentModel(): AgentConnectionModel | undefined {\n\t\treturn this.connectionState?.model;\n\t}\n\n\tprivate getCurrentModelId(): string | undefined {\n\t\treturn this.getCurrentModel()?.id;\n\t}\n\n\tprivate isAgentStreaming(): boolean {\n\t\treturn this.connectionState?.isStreaming ?? false;\n\t}\n\n\tprivate isAgentCompacting(): boolean {\n\t\treturn this.connectionState?.isCompacting ?? false;\n\t}\n\n\tprivate isBashRunning(): boolean {\n\t\treturn this.connectionState?.isBashRunning ?? false;\n\t}\n\n\tprivate hasInterruptibleWork(): boolean {\n\t\treturn (\n\t\t\tthis.isAgentStreaming() ||\n\t\t\tthis.isAgentCompacting() ||\n\t\t\tthis.isBashRunning() ||\n\t\t\tthis.getRetryAttempt() > 0 ||\n\t\t\tthis.connectionState?.sessionActions.active !== undefined ||\n\t\t\tthis.sideQuestionEvent?.status === \"running\"\n\t\t);\n\t}\n\n\tprivate getRetryAttempt(): number {\n\t\treturn this.connectionState?.retryAttempt ?? 0;\n\t}\n\n\tprivate getQueuedActionCount(): number {\n\t\treturn this.connectionState?.sessionActions.queuedCount ?? 0;\n\t}\n\n\tprivate getGoalState(): GoalState {\n\t\treturn this.connectionState?.goal ?? emptyGoalState();\n\t}\n\n\tprivate getConnectionContextUsage(): AgentConnectionState[\"contextUsage\"] {\n\t\tconst snapshot = this.connectionState?.contextUsage;\n\t\tif (!snapshot || snapshot.tokens === null || snapshot.contextWindow <= 0) {\n\t\t\treturn snapshot;\n\t\t}\n\t\t// Add only the output produced since the snapshot was last refreshed. The activity\n\t\t// tracker accumulates across auto-retries within a turn, so subtract the baseline\n\t\t// captured at the last refresh to avoid re-adding a failed attempt's tokens.\n\t\tconst inFlight = this.isAgentStreaming()\n\t\t\t? Math.max(0, this.activityTracker.getStatus().tokens - this.contextUsageTokenBaseline)\n\t\t\t: 0;\n\t\tif (inFlight <= 0) {\n\t\t\treturn snapshot;\n\t\t}\n\t\tconst tokens = snapshot.tokens + inFlight;\n\t\treturn {\n\t\t\ttokens,\n\t\t\tcontextWindow: snapshot.contextWindow,\n\t\t\tpercent: (tokens / snapshot.contextWindow) * 100,\n\t\t} satisfies ContextUsage;\n\t}\n\n\tprivate getScopedModelState(): AgentConnectionState[\"scopedModels\"] {\n\t\treturn this.connectionState?.scopedModels ?? [];\n\t}\n\n\tprivate async rebindCurrentSession(): Promise<void> {\n\t\tthis.unsubscribe?.();\n\t\tthis.unsubscribe = undefined;\n\t\tif (this.localSessionHost) {\n\t\t\tthis.uiServices = this.localSessionHost.createUiServices();\n\t\t}\n\t\tthis.toolDefinitionCache.clear();\n\t\tthis.applyRuntimeSettings();\n\t\tif (this.bindLocalSessionExtensions) {\n\t\t\tawait this.bindCurrentSessionExtensions();\n\t\t} else {\n\t\t\tsetRegisteredThemes(this.uiServices.getThemes());\n\t\t\tawait this.refreshConnectionCatalog();\n\t\t\tthis.setupAutocompleteProvider();\n\t\t\tthis.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });\n\t\t}\n\t\tthis.subscribeToAgent();\n\t\tawait Promise.all([this.refreshConnectionQueue(), this.refreshHeartbeatCatalog().catch(() => undefined)]);\n\t\tawait this.updateAvailableProviderCount();\n\t\tthis.updateEditorBorderColor();\n\t\tthis.updateTerminalTitle();\n\t\tthis.setGoalAnnouncementBaseline(this.getGoalState());\n\t\tthis.syncGoalTray(this.getGoalState());\n\t\tthis.syncWorkingLoader();\n\t}\n\n\tprivate async handleFatalRuntimeError(prefix: string, error: unknown): Promise<never> {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tthis.showError(`${prefix}: ${message}`);\n\t\tstopThemeWatcher();\n\t\tthis.stop();\n\t\tprocess.exit(1);\n\t}\n\n\tprivate resetCurrentSessionRenderState(options?: { clearPromptStash?: boolean }): void {\n\t\tthis.endFeatureHintRun();\n\t\tthis.chatContainer.clear();\n\t\tthis.shortcutGuideContainer.clear();\n\t\tthis.pendingMessagesContainer.clear();\n\t\tthis.queuedMessagesContainer.clear();\n\t\tthis.connectionQueue = { steering: [], followUp: [] };\n\t\tthis.pendingQueueEdit = undefined;\n\t\t// The selection and its stashed draft belong to the previous session;\n\t\t// every editor draft is cleared below, so discard rather than restore.\n\t\tthis.queueSelection.reset();\n\t\tthis.featureHintSuppressedByQueue = false;\n\t\tif (options?.clearPromptStash) {\n\t\t\tthis.promptStash = undefined;\n\t\t\tif (this.promptStashState) this.promptStashState.queuedStashes = undefined;\n\t\t}\n\t\t// Clear every editor's prompt history, draft text, and queues, then prune\n\t\t// any pasted images no longer referenced by the remaining stashed draft.\n\t\tthis.defaultEditor.clearHistory?.();\n\t\tthis.defaultEditor.setText(\"\");\n\t\tif (this.editor !== this.defaultEditor) {\n\t\t\tthis.editor.clearHistory?.();\n\t\t\tthis.editor.setText(\"\");\n\t\t}\n\t\tconst keepImageIds = this.liveImageMarkerIds();\n\t\tfor (const markerId of this.pastedImages.keys()) {\n\t\t\tif (!keepImageIds.has(markerId)) {\n\t\t\t\tthis.pastedImages.delete(markerId);\n\t\t\t}\n\t\t}\n\t\tthis.streamingComponent = undefined;\n\t\tthis.streamingMessage = undefined;\n\t\t// The discarded component's loader interval keeps firing otherwise; no\n\t\t// bash_end will reach it once the reference is dropped.\n\t\tthis.activeBashComponent?.setComplete(undefined, true);\n\t\tthis.activeBashComponent = undefined;\n\t\t// Likewise: the next session's view may never see this refine settle.\n\t\tthis.discardRefineLoader();\n\t\tthis.pendingBashComponents = [];\n\t\tthis.activityTracker.reset();\n\t\tthis.contextUsageTokenBaseline = 0;\n\t\tthis.resetPendingToolState();\n\t\tthis.agentRunFileChanges.clear();\n\t\tthis.renderRecap();\n\t\tthis.ipythonToolComponents.clear();\n\t\tthis.lateIpythonSentAgentMessages.clear();\n\t\tthis.resetSubagentSummary();\n\t\tthis.setGoalAnnouncementBaseline(this.getGoalState());\n\t\tthis.syncGoalTray(this.getGoalState());\n\t}\n\n\tprivate resetPendingToolState(): void {\n\t\tthis.pendingToolGeneration++;\n\t\tthis.pendingTools.clear();\n\t\tthis.pendingToolCreations.clear();\n\t\tthis.startedToolCalls.clear();\n\t}\n\n\tprivate async renderCurrentSessionState(): Promise<void> {\n\t\t// Replacement events own the session-scoped command catalog. The daemon\n\t\t// sends that event before its command response, but its handler may still\n\t\t// be refreshing commands when the response resolves.\n\t\tawait this.sessionEventQueue;\n\t\tthis.resetCurrentSessionRenderState();\n\t\tawait this.renderInitialMessages();\n\t\t// The session transition and transcript are already authoritative here;\n\t\t// a transient queue read must not turn a successful switch into a fatal error.\n\t\tawait this.refreshConnectionQueue().catch(() => undefined);\n\t\tthis.syncWorkingLoader();\n\t}\n\n\tprivate async refreshCommandCatalogForCurrentSession(): Promise<void> {\n\t\ttry {\n\t\t\tthis.connectionCommands = await this.agentConnection.getCommands();\n\t\t} catch {\n\t\t\tthis.connectionCommands = [];\n\t\t}\n\t\tthis.setupAutocompleteProvider();\n\t}\n\n\tprivate async renderResyncedSession(snapshot: AgentConnectionSnapshot): Promise<void> {\n\t\tconst bashFinished = this.isBashRunning() && !snapshot.state.isBashRunning;\n\t\tthis.applyConnectionStateSnapshot(snapshot.state);\n\t\tthis.restoreTurnStartFromMessages(this.getSessionContextFromConnectionSnapshot(snapshot).messages);\n\t\tthis.streamingComponent = undefined;\n\t\tthis.streamingMessage = undefined;\n\t\tthis.rlmNodeId = snapshot.parent?.childId;\n\t\tthis.replaceSubagentSummary(snapshot.children);\n\t\tawait this.renderSessionContext(this.getSessionContextFromConnectionSnapshot(snapshot), {\n\t\t\tclearChat: true,\n\t\t\tupdateFooter: true,\n\t\t});\n\t\tawait this.restoreStreamingMessageFromSnapshot(snapshot.streamingMessage);\n\t\tawait this.refreshConnectionQueue();\n\t\tif (bashFinished) {\n\t\t\tif (this.activeBashComponent) {\n\t\t\t\tthis.activeBashComponent.setComplete(undefined, false);\n\t\t\t\tthis.activeBashComponent = undefined;\n\t\t\t\tif (!snapshot.state.isStreaming) {\n\t\t\t\t\tthis.flushPendingBashComponents();\n\t\t\t\t}\n\t\t\t}\n\t\t\t// A transient side bash is not persisted in the session snapshot, so a\n\t\t\t// reconnect cannot replay its missed bash_end event. Release the pane's\n\t\t\t// local running state when the authoritative snapshot says bash ended.\n\t\t\tif (this.sideQuestionBash) {\n\t\t\t\tthis.sideQuestionComponent?.finishBash();\n\t\t\t\tthis.sideQuestionBash = undefined;\n\t\t\t\tthis.sideQuestionBashComponent = undefined;\n\t\t\t}\n\t\t\tthis.sideQuestionBashDiscarded = undefined;\n\t\t}\n\t\tthis.updateTerminalTitle();\n\t\tthis.setGoalAnnouncementBaseline(this.getGoalState());\n\t\tthis.syncGoalTray(this.getGoalState());\n\t\tthis.syncWorkingLoader();\n\t}\n\n\tprivate getCachedToolDefinition(toolName: string): ToolExecutionDefinition | undefined {\n\t\treturn this.toolDefinitionCache.get(toolName);\n\t}\n\n\tprivate async loadToolDefinition(toolName: string): Promise<ToolExecutionDefinition | undefined> {\n\t\tif (this.toolDefinitionCache.has(toolName)) {\n\t\t\treturn this.toolDefinitionCache.get(toolName);\n\t\t}\n\t\tconst definition = this.createToolExecutionDefinition(\n\t\t\ttoolName,\n\t\t\tawait this.agentConnection.getToolDefinition(toolName),\n\t\t\tthis.localSessionHost?.getToolRendererDefinition(toolName),\n\t\t);\n\t\tthis.toolDefinitionCache.set(toolName, definition);\n\t\treturn definition;\n\t}\n\n\tprivate getLatestStreamingToolCall(toolCallId: string): ToolCall | undefined {\n\t\treturn this.streamingMessage?.content.find(\n\t\t\t(content): content is ToolCall => content.type === \"toolCall\" && content.id === toolCallId,\n\t\t);\n\t}\n\n\tprivate registerIpythonToolComponent(toolName: string, toolCallId: string, component: ToolExecutionComponent): void {\n\t\tif (toolName !== \"ipython\") {\n\t\t\treturn;\n\t\t}\n\t\tthis.ipythonToolComponents.set(toolCallId, component);\n\t\tfor (const lateMessage of this.lateIpythonSentAgentMessages.get(toolCallId) ?? []) {\n\t\t\tcomponent.appendSentAgentMessage(lateMessage);\n\t\t}\n\t}\n\n\tprivate async getOrCreatePendingToolComponent(\n\t\ttoolCall: PendingToolCallRenderInput,\n\t): Promise<ToolExecutionComponent | undefined> {\n\t\tconst existingComponent = this.pendingTools.get(toolCall.id);\n\t\tif (existingComponent) {\n\t\t\texistingComponent.updateArgs(toolCall.arguments);\n\t\t\treturn existingComponent;\n\t\t}\n\t\tif (this.pendingToolCreations.has(toolCall.id)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tthis.pendingToolCreations.add(toolCall.id);\n\t\tconst generation = this.pendingToolGeneration;\n\t\ttry {\n\t\t\tconst toolDefinition = await this.loadToolDefinition(toolCall.name);\n\t\t\tif (generation !== this.pendingToolGeneration) {\n\t\t\t\t// Pending tool state was reset (abort/error) while loading; drop the stale component.\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tconst latestToolCall = this.getLatestStreamingToolCall(toolCall.id) ?? toolCall;\n\t\t\tconst componentAfterLoad = this.pendingTools.get(latestToolCall.id);\n\t\t\tif (componentAfterLoad) {\n\t\t\t\tcomponentAfterLoad.updateArgs(latestToolCall.arguments);\n\t\t\t\treturn componentAfterLoad;\n\t\t\t}\n\n\t\t\tconst component = new ToolExecutionComponent(\n\t\t\t\tlatestToolCall.name,\n\t\t\t\tlatestToolCall.id,\n\t\t\t\tlatestToolCall.arguments,\n\t\t\t\t{\n\t\t\t\t\tshowImages: this.settingsManager.getShowImages(),\n\t\t\t\t},\n\t\t\t\ttoolDefinition,\n\t\t\t\tthis.ui,\n\t\t\t\tthis.getCurrentCwd(),\n\t\t\t);\n\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\tcomponent.setAgentMessagesExpanded(this.agentMessagesExpanded);\n\t\t\tcomponent.setEditDiffsExpanded(this.editDiffsExpanded);\n\t\t\tif (this.startedToolCalls.has(latestToolCall.id)) {\n\t\t\t\tcomponent.markExecutionStarted();\n\t\t\t}\n\t\t\tselectLatestToolExpandHint(this.chatContainer.children, component);\n\t\t\tthis.chatContainer.addChild(component);\n\t\t\tthis.pendingTools.set(latestToolCall.id, component);\n\t\t\tthis.registerIpythonToolComponent(latestToolCall.name, latestToolCall.id, component);\n\t\t\treturn component;\n\t\t} finally {\n\t\t\tthis.pendingToolCreations.delete(toolCall.id);\n\t\t}\n\t}\n\n\tprivate createToolExecutionDefinition(\n\t\ttoolName: string,\n\t\tconnectionDefinition: AgentConnectionToolDefinition | undefined,\n\t\tlocalRendererDefinition: InteractiveModeLocalToolRendererDefinition | undefined,\n\t): ToolExecutionDefinition | undefined {\n\t\tif (!connectionDefinition && !localRendererDefinition) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst definition: ToolExecutionDefinition = {\n\t\t\t...(connectionDefinition ?? {\n\t\t\t\tname: toolName,\n\t\t\t\tlabel: toolName,\n\t\t\t\tdescription: \"\",\n\t\t\t\tparameters: {},\n\t\t\t}),\n\t\t};\n\t\tif (localRendererDefinition?.renderShell !== undefined) {\n\t\t\tdefinition.renderShell = localRendererDefinition.renderShell;\n\t\t}\n\t\tif (localRendererDefinition?.renderCall !== undefined) {\n\t\t\tdefinition.renderCall = localRendererDefinition.renderCall;\n\t\t}\n\t\tif (localRendererDefinition?.renderResult !== undefined) {\n\t\t\tdefinition.renderResult = localRendererDefinition.renderResult;\n\t\t}\n\t\treturn definition;\n\t}\n\n\tprivate async preloadToolDefinitions(toolNames: Iterable<string>): Promise<void> {\n\t\tconst missingToolNames = Array.from(new Set(toolNames)).filter(\n\t\t\t(toolName) => !this.toolDefinitionCache.has(toolName),\n\t\t);\n\t\tif (missingToolNames.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tawait Promise.all(\n\t\t\tmissingToolNames.map(async (toolName) => {\n\t\t\t\tconst definition = this.createToolExecutionDefinition(\n\t\t\t\t\ttoolName,\n\t\t\t\t\tawait this.agentConnection.getToolDefinition(toolName),\n\t\t\t\t\tthis.localSessionHost?.getToolRendererDefinition(toolName),\n\t\t\t\t);\n\t\t\t\tthis.toolDefinitionCache.set(toolName, definition);\n\t\t\t}),\n\t\t);\n\t}\n\n\tprivate setupExtensionShortcuts(extensionRunner: ExtensionRunner): void {\n\t\tconst shortcuts = extensionRunner.getShortcuts(this.keybindings.getEffectiveConfig());\n\t\tif (shortcuts.size === 0) return;\n\n\t\tconst localSessionHost = this.getLocalSessionHost();\n\t\tconst createContext = (): ExtensionContext => ({\n\t\t\tui: this.createExtensionUIContext(),\n\t\t\thasUI: true,\n\t\t\tcwd: this.getCurrentCwd(),\n\t\t\tsessionManager: localSessionHost.getSessionManager(),\n\t\t\tmodelRegistry: this.modelRegistry,\n\t\t\tmodel: this.getCurrentModel(),\n\t\t\tisIdle: () => !this.isAgentStreaming(),\n\t\t\tsignal: localSessionHost.getAbortSignal(),\n\t\t\tabort: () => this.agentConnection.abort(),\n\t\t\thasPendingMessages: () => this.getQueuedActionCount() > 0,\n\t\t\tshutdown: () => {\n\t\t\t\tthis.shutdownRequested = true;\n\t\t\t},\n\t\t\tgetContextUsage: () => this.getConnectionContextUsage(),\n\t\t\tcompact: (options) => {\n\t\t\t\tvoid (async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = await this.agentConnection.compact(options?.customInstructions);\n\t\t\t\t\t\toptions?.onComplete?.(result);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconst err = error instanceof Error ? error : new Error(String(error));\n\t\t\t\t\t\toptions?.onError?.(err);\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t},\n\t\t\tgetSystemPrompt: () => localSessionHost.getSystemPrompt(),\n\t\t});\n\n\t\tthis.defaultEditor.onExtensionShortcut = (data: string) => {\n\t\t\tfor (const [shortcutStr, shortcut] of shortcuts) {\n\t\t\t\tif (matchesKey(data, shortcutStr as KeyId)) {\n\t\t\t\t\tPromise.resolve(shortcut.handler(createContext())).catch((err) => {\n\t\t\t\t\t\tthis.showError(`Shortcut handler error: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\t\t});\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\t}\n\n\tprivate setExtensionStatus(key: string, text: string | undefined): void {\n\t\tthis.footerDataProvider.setExtensionStatus(key, text);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate getWorkingLoaderMessage(): string {\n\t\tconst elapsed =\n\t\t\tthis.workingStartedAt === undefined\n\t\t\t\t? undefined\n\t\t\t\t: this.formatWorkingElapsed(Date.now() - this.workingStartedAt);\n\t\tconst status = this.activityTracker.getStatus();\n\t\t// The subagent count/recaps live in the tree above the loader, so the loader\n\t\t// message itself no longer repeats \"N subagents running\".\n\t\tif (!this.isAgentStreaming()) {\n\t\t\treturn \"\";\n\t\t}\n\t\tif (this.workingMessage !== undefined) {\n\t\t\t// Extensions and tool bootstrap own the message; keep the plain \"<message> <elapsed>\" form.\n\t\t\treturn elapsed === undefined ? this.workingMessage : `${this.workingMessage} ${elapsed}`;\n\t\t}\n\t\tconst parts: string[] = [AGENT_ACTIVITY_LABELS[status.activity]];\n\t\tif (elapsed !== undefined) {\n\t\t\tparts.push(elapsed);\n\t\t}\n\t\tif (status.tokens > 0) {\n\t\t\tparts.push(`${status.direction === \"down\" ? \"↓\" : \"↑\"} ${formatTokenCount(status.tokens)} tokens`);\n\t\t}\n\t\treturn parts.join(\" · \");\n\t}\n\n\tprivate createWorkingLoader(): Loader {\n\t\treturn new Loader(\n\t\t\tthis.ui,\n\t\t\t(spinner) => theme.fg(\"accent\", spinner),\n\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\tthis.getWorkingLoaderMessage(),\n\t\t\tthis.workingIndicatorOptions,\n\t\t);\n\t}\n\n\tprivate formatWorkingElapsed(elapsedMs: number): string {\n\t\tconst totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000));\n\t\tif (totalSeconds < 60) {\n\t\t\treturn `${totalSeconds}s`;\n\t\t}\n\t\tconst minutes = Math.floor(totalSeconds / 60);\n\t\tconst seconds = totalSeconds % 60;\n\t\tif (minutes < 60) {\n\t\t\treturn `${minutes}m ${seconds.toString().padStart(2, \"0\")}s`;\n\t\t}\n\t\tconst hours = Math.floor(minutes / 60);\n\t\tconst remainingMinutes = minutes % 60;\n\t\tif (hours < 24) {\n\t\t\treturn `${hours}h ${remainingMinutes.toString().padStart(2, \"0\")}m ${seconds.toString().padStart(2, \"0\")}s`;\n\t\t}\n\t\tconst days = Math.floor(hours / 24);\n\t\tconst remainingHours = hours % 24;\n\t\treturn `${days}d ${remainingHours.toString().padStart(2, \"0\")}h ${remainingMinutes.toString().padStart(2, \"0\")}m ${seconds.toString().padStart(2, \"0\")}s`;\n\t}\n\n\tprivate updateWorkingLoaderMessage(): void {\n\t\tthis.loadingAnimation?.setMessage(this.getWorkingLoaderMessage());\n\t}\n\n\tprivate startWorkingTimer(): void {\n\t\tif (this.workingTimer) {\n\t\t\tclearInterval(this.workingTimer);\n\t\t}\n\t\tthis.workingTimer = setInterval(() => this.updateWorkingLoaderMessage(), 1000);\n\t\tthis.workingTimer.unref?.();\n\t}\n\n\t// Recover the in-flight run's start from a restored transcript so the elapsed timer survives re-attach.\n\tprivate restoreTurnStartFromMessages(messages: readonly AgentMessage[]): void {\n\t\tthis.turnStartedAt = undefined;\n\t\tif (!this.isAgentStreaming()) return;\n\t\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\t\tconst message = messages[i]!;\n\t\t\tif (startsAgentRun(message)) {\n\t\t\t\tthis.turnStartedAt = message.timestamp;\n\t\t\t} else if (message.role === \"assistant\" && message.stopReason !== \"toolUse\") {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (this.turnStartedAt !== undefined && this.workingStartedAt !== undefined) {\n\t\t\tthis.workingStartedAt = this.turnStartedAt;\n\t\t\tthis.updateWorkingLoaderMessage();\n\t\t}\n\t}\n\n\tprivate startWorkingLoader(): void {\n\t\tthis.stopWorkingLoader();\n\t\tthis.workingStartedAt = this.turnStartedAt ?? Date.now();\n\t\tthis.loadingAnimation = this.createWorkingLoader();\n\t\tthis.statusContainer.addChild(this.loadingAnimation);\n\t\tthis.startWorkingTimer();\n\t\tthis.startFeatureHintPresentation();\n\t}\n\n\tprivate stopWorkingLoader(): void {\n\t\tthis.clearFeatureHintPresentation();\n\t\tif (this.workingTimer) {\n\t\t\tclearInterval(this.workingTimer);\n\t\t\tthis.workingTimer = undefined;\n\t\t}\n\t\tthis.workingStartedAt = undefined;\n\t\tif (this.loadingAnimation) {\n\t\t\tthis.loadingAnimation.stop();\n\t\t\tthis.loadingAnimation = undefined;\n\t\t}\n\t\tthis.statusContainer.clear();\n\t}\n\n\tprivate startFeatureHintPresentation(): void {\n\t\tthis.clearFeatureHintPresentation();\n\t\tif (this.shouldSuppressFeatureHint()) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.featureHintEligibleAt === 0) {\n\t\t\tthis.featureHintEligibleAt = Date.now() + FEATURE_HINT_DELAY_MS;\n\t\t}\n\t\tconst delay = Math.max(0, this.featureHintEligibleAt - Date.now());\n\t\tif (delay === 0) {\n\t\t\tthis.showFeatureHint();\n\t\t\treturn;\n\t\t}\n\t\tthis.featureHintTimer = setTimeout(() => {\n\t\t\tthis.featureHintTimer = undefined;\n\t\t\tthis.showFeatureHint();\n\t\t}, delay);\n\t\tthis.featureHintTimer.unref?.();\n\t}\n\n\tprivate showFeatureHint(): void {\n\t\tif (\n\t\t\tthis.shouldSuppressFeatureHint() ||\n\t\t\t!this.loadingAnimation ||\n\t\t\t!this.shouldShowWorkingLoader() ||\n\t\t\t!this.statusContainer.children.includes(this.loadingAnimation)\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tif (!this.currentFeatureHint) {\n\t\t\tconst hint = this.featureHintDeck.next({\n\t\t\t\tgetKeybinding: (action) => {\n\t\t\t\t\tconst key = keyText(action);\n\t\t\t\t\treturn key ? this.capitalizeKey(key) : undefined;\n\t\t\t\t},\n\t\t\t\tisResidentSession: this.options.returnToAgentsView === true,\n\t\t\t});\n\t\t\tthis.currentFeatureHint = hint?.text;\n\t\t}\n\t\tif (!this.currentFeatureHint) {\n\t\t\treturn;\n\t\t}\n\t\tthis.featureHintComponent = new FeatureHintComponent(this.currentFeatureHint);\n\t\tthis.featureHintContainer.addChild(this.featureHintComponent);\n\t\tthis.renderRecap();\n\t\tthis.featureHintAnimationTimer = setInterval(() => {\n\t\t\tthis.featureHintComponent?.advance();\n\t\t\tthis.ui.requestRender();\n\t\t}, FEATURE_HINT_ANIMATION_INTERVAL_MS);\n\t\tthis.featureHintAnimationTimer.unref?.();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate clearFeatureHintPresentation(): void {\n\t\tif (this.featureHintTimer) {\n\t\t\tclearTimeout(this.featureHintTimer);\n\t\t\tthis.featureHintTimer = undefined;\n\t\t}\n\t\tif (this.featureHintAnimationTimer) {\n\t\t\tclearInterval(this.featureHintAnimationTimer);\n\t\t\tthis.featureHintAnimationTimer = undefined;\n\t\t}\n\t\tif (this.featureHintComponent) {\n\t\t\tthis.featureHintContainer.removeChild(this.featureHintComponent);\n\t\t\tthis.featureHintComponent = undefined;\n\t\t\tthis.renderRecap();\n\t\t}\n\t}\n\n\tprivate resumeFeatureHintPresentation(): void {\n\t\tif (\n\t\t\t!this.shouldSuppressFeatureHint() &&\n\t\t\tthis.loadingAnimation &&\n\t\t\tthis.shouldShowWorkingLoader() &&\n\t\t\tthis.statusContainer.children.includes(this.loadingAnimation)\n\t\t) {\n\t\t\tthis.startFeatureHintPresentation();\n\t\t}\n\t}\n\n\tprivate shouldSuppressFeatureHint(): boolean {\n\t\tconst { steering, followUp } = this.getAllQueuedMessages();\n\t\treturn steering.length > 0 || followUp.length > 0;\n\t}\n\n\tprivate endFeatureHintRun(): void {\n\t\tthis.clearFeatureHintPresentation();\n\t\tthis.currentFeatureHint = undefined;\n\t\tthis.featureHintEligibleAt = 0;\n\t\tthis.featureHintRunPending = false;\n\t}\n\n\tprivate prepareFeatureHintRun(message: AgentMessage): void {\n\t\tif (!this.featureHintRunPending) return;\n\t\tif (message.role === \"assistant\") {\n\t\t\tthis.featureHintRunPending = false;\n\t\t\treturn;\n\t\t}\n\t\tif (!startsAgentRun(message)) return;\n\n\t\tthis.endFeatureHintRun();\n\t\tif (this.shouldShowWorkingLoader()) {\n\t\t\tthis.startFeatureHintPresentation();\n\t\t}\n\t}\n\n\tprivate updateWorkingPulse(): void {\n\t\tconst active = this.isAgentStreaming();\n\t\tif (!active) {\n\t\t\tthis.stopWorkingPulse();\n\t\t\treturn;\n\t\t}\n\t\tif (!this.pulseTimer) {\n\t\t\tthis.pulseTimer = setInterval(() => this.tickWorkingPulse(), WORKING_ICON_INTERVAL_MS);\n\t\t\tthis.pulseTimer.unref?.();\n\t\t}\n\t}\n\n\tprivate tickWorkingPulse(): void {\n\t\tthis.pulseFrame += 1;\n\t\tsetWorkingPulseFrame(this.pulseFrame);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate stopWorkingPulse(): void {\n\t\tif (this.pulseTimer) {\n\t\t\tclearInterval(this.pulseTimer);\n\t\t\tthis.pulseTimer = undefined;\n\t\t}\n\t}\n\n\tprivate shouldShowWorkingLoader(): boolean {\n\t\t// Background subagents (agent turn done, asyncio tasks still running) would\n\t\t// otherwise show a textless spinner; the subagent tree above the loader carries\n\t\t// that state, so the loader only shows while the main agent is itself streaming.\n\t\treturn this.workingVisible && this.isAgentStreaming();\n\t}\n\n\t// Reconcile the loader with current state for transitions that fire no live\n\t// agent_start edge (returning from agents view, resuming mid-stream).\n\tprivate startCompactionLoader(\n\t\treason: \"manual\" | \"requested\" | \"overflow\" | \"threshold\",\n\t\tcustomInstructions?: string,\n\t): void {\n\t\tif (this.settingsManager.getShowTerminalProgress()) {\n\t\t\tthis.ui.terminal.setProgress(true);\n\t\t}\n\t\t// Keep editor active; submissions are queued during compaction.\n\t\t// Fully stop the working loader (not just detach) so it isn't orphaned.\n\t\tthis.stopWorkingLoader();\n\t\tthis.statusContainer.clear();\n\t\tconst cancelHint = `(${keyText(\"app.clear\")} to cancel)`;\n\t\tconst focus = customInstructions ? ` (focus: ${truncateToWidth(customInstructions, 60, \"…\")})` : \"\";\n\t\tconst label =\n\t\t\treason === \"manual\"\n\t\t\t\t? `Compacting context${focus}... ${cancelHint}`\n\t\t\t\t: reason === \"requested\"\n\t\t\t\t\t? `Agent requested compaction, compacting context${focus}... ${cancelHint}`\n\t\t\t\t\t: `${reason === \"overflow\" ? \"Context overflow detected, \" : \"\"}Auto-compacting... ${cancelHint}`;\n\t\tthis.autoCompactionLoader = new Loader(\n\t\t\tthis.ui,\n\t\t\t(spinner) => theme.fg(\"muted\", spinner),\n\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\tlabel,\n\t\t);\n\t\tthis.statusContainer.addChild(this.autoCompactionLoader);\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Live status for a user-issued /refine, mirroring the compaction loader. */\n\tprivate startRefineLoader(): void {\n\t\tthis.stopWorkingLoader();\n\t\tthis.statusContainer.clear();\n\t\tthis.refineLoader = new Loader(\n\t\t\tthis.ui,\n\t\t\t(spinner) => theme.fg(\"muted\", spinner),\n\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\t\"Refining continual harness state...\",\n\t\t);\n\t\tthis.statusContainer.addChild(this.refineLoader);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate stopRefineLoader(): void {\n\t\tif (!this.refineLoader) return;\n\t\tthis.discardRefineLoader();\n\t\tthis.statusContainer.clear();\n\t\tthis.syncWorkingLoader();\n\t}\n\n\t/** Stops and removes the loader without remounting old-session state. */\n\tprivate discardRefineLoader(): void {\n\t\tif (!this.refineLoader) return;\n\t\tthis.refineLoader.stop();\n\t\tthis.statusContainer.removeChild(this.refineLoader);\n\t\tthis.refineLoader = undefined;\n\t}\n\n\tprivate syncWorkingLoader(): void {\n\t\t// A compaction that started before this client attached (or while another\n\t\t// view was open) has no start-event edge; restore its loader from state.\n\t\tif (!this.autoCompactionLoader && this.isAgentCompacting()) {\n\t\t\tthis.startCompactionLoader(\"manual\");\n\t\t\treturn;\n\t\t}\n\t\t// Compaction/retry own the status container while active; don't fight them.\n\t\tif (this.autoCompactionLoader || this.retryLoader) {\n\t\t\treturn;\n\t\t}\n\t\t// Remount the refine loader if another owner (e.g. a compaction) cleared it.\n\t\tif (this.refineLoader) {\n\t\t\tif (!this.statusContainer.children.includes(this.refineLoader)) {\n\t\t\t\tthis.statusContainer.clear();\n\t\t\t\tthis.statusContainer.addChild(this.refineLoader);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (this.shouldShowWorkingLoader()) {\n\t\t\t// A bare `loadingAnimation != null` check isn't proof it's on screen:\n\t\t\t// other paths clear statusContainer without nulling it, orphaning the\n\t\t\t// loader. Re-attach unless it is actually mounted.\n\t\t\tif (!this.loadingAnimation || !this.statusContainer.children.includes(this.loadingAnimation)) {\n\t\t\t\tthis.startWorkingLoader();\n\t\t\t}\n\t\t} else if (this.loadingAnimation) {\n\t\t\tthis.stopWorkingLoader();\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate setWorkingVisible(visible: boolean): void {\n\t\tthis.workingVisible = visible;\n\t\tif (!visible) {\n\t\t\tthis.stopWorkingLoader();\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (this.shouldShowWorkingLoader() && !this.loadingAnimation) {\n\t\t\tthis.statusContainer.clear();\n\t\t\tthis.startWorkingLoader();\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate setWorkingIndicator(options?: LoaderIndicatorOptions): void {\n\t\tthis.workingIndicatorOptions = options;\n\t\tthis.loadingAnimation?.setIndicator(options);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate setHiddenThinkingLabel(label?: string): void {\n\t\tthis.hiddenThinkingLabel = label ?? this.defaultHiddenThinkingLabel;\n\t\tfor (const child of this.chatContainer.children) {\n\t\t\tif (child instanceof AssistantMessageComponent) {\n\t\t\t\tchild.setHiddenThinkingLabel(this.hiddenThinkingLabel);\n\t\t\t}\n\t\t}\n\t\tif (this.streamingComponent) {\n\t\t\tthis.streamingComponent.setHiddenThinkingLabel(this.hiddenThinkingLabel);\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate setExtensionWidget(\n\t\tkey: string,\n\t\tcontent: string[] | ((tui: TUI, thm: Theme) => Component & { dispose?(): void }) | undefined,\n\t\toptions?: ExtensionWidgetOptions,\n\t): void {\n\t\tconst placement = options?.placement ?? \"aboveEditor\";\n\t\tconst removeExisting = (map: Map<string, Component & { dispose?(): void }>) => {\n\t\t\tconst existing = map.get(key);\n\t\t\tif (existing?.dispose) existing.dispose();\n\t\t\tmap.delete(key);\n\t\t};\n\n\t\tremoveExisting(this.extensionWidgetsAbove);\n\t\tremoveExisting(this.extensionWidgetsBelow);\n\n\t\tif (content === undefined) {\n\t\t\tthis.renderWidgets();\n\t\t\treturn;\n\t\t}\n\n\t\tlet component: Component & { dispose?(): void };\n\n\t\tif (Array.isArray(content)) {\n\t\t\tconst container = new Container();\n\t\t\tfor (const line of content.slice(0, InteractiveMode.MAX_WIDGET_LINES)) {\n\t\t\t\tcontainer.addChild(new Text(line, 1, 0));\n\t\t\t}\n\t\t\tif (content.length > InteractiveMode.MAX_WIDGET_LINES) {\n\t\t\t\tcontainer.addChild(new Text(theme.fg(\"muted\", \"... (widget truncated)\"), 1, 0));\n\t\t\t}\n\t\t\tcomponent = container;\n\t\t} else {\n\t\t\tcomponent = content(this.ui, theme);\n\t\t}\n\n\t\tconst targetMap = placement === \"belowEditor\" ? this.extensionWidgetsBelow : this.extensionWidgetsAbove;\n\t\ttargetMap.set(key, component);\n\t\tthis.renderWidgets();\n\t}\n\n\tprivate clearExtensionWidgets(): void {\n\t\tfor (const widget of this.extensionWidgetsAbove.values()) {\n\t\t\twidget.dispose?.();\n\t\t}\n\t\tfor (const widget of this.extensionWidgetsBelow.values()) {\n\t\t\twidget.dispose?.();\n\t\t}\n\t\tthis.extensionWidgetsAbove.clear();\n\t\tthis.extensionWidgetsBelow.clear();\n\t\tthis.renderWidgets();\n\t}\n\n\tprivate resetExtensionUI(): void {\n\t\tthis.cancelActiveConnectionExtensionUiRequests();\n\t\tthis.closeHeartbeatManager();\n\t\tif (this.extensionSelector) {\n\t\t\tthis.hideExtensionSelector();\n\t\t}\n\t\tif (this.extensionInput) {\n\t\t\tthis.hideExtensionInput();\n\t\t}\n\t\tif (this.extensionEditor) {\n\t\t\tthis.hideExtensionEditor();\n\t\t}\n\t\tthis.ui.hideOverlay();\n\t\tthis.clearExtensionTerminalInputListeners();\n\t\tthis.setExtensionFooter(undefined);\n\t\tthis.setExtensionHeader(undefined);\n\t\tthis.clearExtensionWidgets();\n\t\tthis.footerDataProvider.clearExtensionStatuses();\n\t\tthis.footer.invalidate();\n\t\tthis.autocompleteProviderWrappers = [];\n\t\tthis.setCustomEditorComponent(undefined);\n\t\tthis.setupAutocompleteProvider();\n\t\tthis.defaultEditor.onExtensionShortcut = undefined;\n\t\tthis.updateTerminalTitle();\n\t\tthis.workingMessage = undefined;\n\t\tthis.workingVisible = true;\n\t\tthis.setWorkingIndicator();\n\t\tif (this.loadingAnimation) {\n\t\t\tthis.updateWorkingLoaderMessage();\n\t\t}\n\t\tthis.setHiddenThinkingLabel();\n\t}\n\n\tprivate static readonly MAX_WIDGET_LINES = 10;\n\n\tprivate renderWidgets(): void {\n\t\tif (!this.widgetContainerAbove || !this.widgetContainerBelow) return;\n\t\tthis.renderWidgetContainer(this.widgetContainerAbove, this.extensionWidgetsAbove, true, true);\n\t\tthis.renderWidgetContainer(this.widgetContainerBelow, this.extensionWidgetsBelow, false, false);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate renderRecap(): void {\n\t\tif (!this.recapContainer) return;\n\t\tthis.recapContainer.clear();\n\t\tconst recap = this.sessionRecap?.trim();\n\t\tconst showChanges = !this.isAgentStreaming() && this.agentRunFileChanges.size > 0;\n\t\tif (showChanges) {\n\t\t\tthis.recapContainer.addChild(\n\t\t\t\tnew TruncatedText(formatTotalChangeSummary([...this.agentRunFileChanges.values()]), 1, 0),\n\t\t\t);\n\t\t}\n\t\tif (recap) {\n\t\t\tthis.recapContainer.addChild(new TruncatedText(theme.fg(\"dim\", `Recap: ${recap}`), 1, 0));\n\t\t}\n\t\tif ((recap || showChanges) && !this.featureHintComponent) {\n\t\t\tthis.recapContainer.addChild(new Spacer(1));\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate renderWidgetContainer(\n\t\tcontainer: Container,\n\t\twidgets: Map<string, Component & { dispose?(): void }>,\n\t\tspacerWhenEmpty: boolean,\n\t\tleadingSpacer: boolean,\n\t): void {\n\t\tcontainer.clear();\n\n\t\tif (widgets.size === 0) {\n\t\t\tif (spacerWhenEmpty) {\n\t\t\t\tcontainer.addChild(new Spacer(1));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (leadingSpacer) {\n\t\t\tcontainer.addChild(new Spacer(1));\n\t\t}\n\t\tfor (const component of widgets.values()) {\n\t\t\tcontainer.addChild(component);\n\t\t}\n\t}\n\n\tprivate setExtensionFooter(\n\t\tfactory:\n\t\t\t| ((tui: TUI, thm: Theme, footerData: ReadonlyFooterDataProvider) => Component & { dispose?(): void })\n\t\t\t| undefined,\n\t): void {\n\t\tif (this.customFooter?.dispose) {\n\t\t\tthis.customFooter.dispose();\n\t\t}\n\n\t\tif (this.customFooter) {\n\t\t\tthis.footerSlot.removeChild(this.customFooter);\n\t\t} else {\n\t\t\tthis.footerSlot.removeChild(this.footer);\n\t\t}\n\n\t\tif (factory) {\n\t\t\tthis.customFooter = factory(this.ui, theme, this.footerDataProvider);\n\t\t\tthis.footerSlot.addChild(this.customFooter);\n\t\t} else {\n\t\t\tthis.customFooter = undefined;\n\t\t\tthis.footerSlot.addChild(this.footer);\n\t\t}\n\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate setExtensionHeader(factory: ((tui: TUI, thm: Theme) => Component & { dispose?(): void }) | undefined): void {\n\t\t// Header may not be initialized yet if called during early initialization\n\t\tif (!this.builtInHeader) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.customHeader?.dispose) {\n\t\t\tthis.customHeader.dispose();\n\t\t}\n\n\t\t// Find the index of the current header in the header container\n\t\tconst currentHeader = this.customHeader || this.builtInHeader;\n\t\tconst index = this.headerContainer.children.indexOf(currentHeader);\n\n\t\tif (factory) {\n\t\t\tthis.customHeader = factory(this.ui, theme);\n\t\t\tif (isExpandable(this.customHeader)) {\n\t\t\t\tthis.customHeader.setExpanded(this.toolOutputExpanded);\n\t\t\t}\n\t\t\tif (index !== -1) {\n\t\t\t\tthis.headerContainer.children[index] = this.customHeader;\n\t\t\t} else {\n\t\t\t\t// If not found (e.g. builtInHeader was never added), add at the top\n\t\t\t\tthis.headerContainer.children.unshift(this.customHeader);\n\t\t\t}\n\t\t} else {\n\t\t\tthis.customHeader = undefined;\n\t\t\tif (isExpandable(this.builtInHeader)) {\n\t\t\t\tthis.builtInHeader.setExpanded(this.toolOutputExpanded);\n\t\t\t}\n\t\t\tif (index !== -1) {\n\t\t\t\tthis.headerContainer.children[index] = this.builtInHeader;\n\t\t\t}\n\t\t}\n\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate addExtensionTerminalInputListener(\n\t\thandler: (data: string) => { consume?: boolean; data?: string } | undefined,\n\t): () => void {\n\t\tconst unsubscribe = this.ui.addInputListener(handler);\n\t\tthis.extensionTerminalInputUnsubscribers.add(unsubscribe);\n\t\treturn () => {\n\t\t\tunsubscribe();\n\t\t\tthis.extensionTerminalInputUnsubscribers.delete(unsubscribe);\n\t\t};\n\t}\n\n\tprivate clearExtensionTerminalInputListeners(): void {\n\t\tfor (const unsubscribe of this.extensionTerminalInputUnsubscribers) {\n\t\t\tunsubscribe();\n\t\t}\n\t\tthis.extensionTerminalInputUnsubscribers.clear();\n\t}\n\n\tprivate createExtensionUIContext(): ExtensionUIContext {\n\t\treturn {\n\t\t\tselect: (title, options, opts) => this.showExtensionSelector(title, options, opts),\n\t\t\tconfirm: (title, message, opts) => this.showExtensionConfirm(title, message, opts),\n\t\t\tinput: (title, placeholder, opts) => this.showExtensionInput(title, placeholder, opts),\n\t\t\tnotify: (message, type) => this.showExtensionNotify(message, type),\n\t\t\tonTerminalInput: (handler) => this.addExtensionTerminalInputListener(handler),\n\t\t\tsetStatus: (key, text) => this.setExtensionStatus(key, text),\n\t\t\tsetWorkingMessage: (message) => {\n\t\t\t\tthis.workingMessage = message;\n\t\t\t\tif (this.loadingAnimation) {\n\t\t\t\t\tthis.updateWorkingLoaderMessage();\n\t\t\t\t}\n\t\t\t},\n\t\t\tsetWorkingVisible: (visible) => this.setWorkingVisible(visible),\n\t\t\tsetWorkingIndicator: (options) => this.setWorkingIndicator(options),\n\t\t\tsetHiddenThinkingLabel: (label) => this.setHiddenThinkingLabel(label),\n\t\t\tsetWidget: (key, content, options) => this.setExtensionWidget(key, content, options),\n\t\t\tsetFooter: (factory) => this.setExtensionFooter(factory),\n\t\t\tsetHeader: (factory) => this.setExtensionHeader(factory),\n\t\t\tsetTitle: (title) => this.ui.terminal.setTitle(title),\n\t\t\tcustom: (factory, options) => this.showExtensionCustom(factory, options),\n\t\t\tpasteToEditor: (text) => this.editor.handleInput(`\\x1b[200~${text}\\x1b[201~`),\n\t\t\tsetEditorText: (text) => this.editor.setText(text),\n\t\t\tgetEditorText: () => this.editor.getExpandedText?.() ?? this.editor.getText(),\n\t\t\teditor: (title, prefill) => this.showExtensionEditor(title, prefill),\n\t\t\taddAutocompleteProvider: (factory) => {\n\t\t\t\tthis.autocompleteProviderWrappers.push(factory);\n\t\t\t\tthis.setupAutocompleteProvider();\n\t\t\t},\n\t\t\tsetEditorComponent: (factory) => this.setCustomEditorComponent(factory),\n\t\t\tgetEditorComponent: () => this.editorComponentFactory,\n\t\t\tget theme() {\n\t\t\t\treturn theme;\n\t\t\t},\n\t\t\tgetAllThemes: () => getAvailableThemesWithPaths(),\n\t\t\tgetTheme: (name) => getThemeByName(name),\n\t\t\tsetTheme: (themeOrName) => {\n\t\t\t\tif (themeOrName instanceof Theme) {\n\t\t\t\t\tsetThemeInstance(themeOrName);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\treturn { success: true };\n\t\t\t\t}\n\t\t\t\tconst result = setTheme(themeOrName, true);\n\t\t\t\tif (result.success) {\n\t\t\t\t\tif (this.settingsManager.getTheme() !== themeOrName) {\n\t\t\t\t\t\tthis.settingsManager.setTheme(themeOrName);\n\t\t\t\t\t}\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t},\n\t\t\tgetToolsExpanded: () => this.toolOutputExpanded,\n\t\t\tsetToolsExpanded: (expanded) => this.setToolsExpanded(expanded),\n\t\t};\n\t}\n\n\tprivate showExtensionSelector(\n\t\ttitle: string,\n\t\toptions: string[],\n\t\topts?: ExtensionUIDialogOptions,\n\t): Promise<string | undefined> {\n\t\treturn new Promise((resolve) => {\n\t\t\tif (opts?.signal?.aborted) {\n\t\t\t\tresolve(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst onAbort = () => {\n\t\t\t\tthis.hideExtensionSelector();\n\t\t\t\tresolve(undefined);\n\t\t\t};\n\t\t\topts?.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\tthis.extensionSelector = new ExtensionSelectorComponent(\n\t\t\t\ttitle,\n\t\t\t\toptions,\n\t\t\t\t(option) => {\n\t\t\t\t\topts?.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tthis.hideExtensionSelector();\n\t\t\t\t\tresolve(option);\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\topts?.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tthis.hideExtensionSelector();\n\t\t\t\t\tresolve(undefined);\n\t\t\t\t},\n\t\t\t\t{ tui: this.ui, timeout: opts?.timeout },\n\t\t\t);\n\n\t\t\tthis.editorContainer.clear();\n\t\t\tthis.editorContainer.addChild(this.extensionSelector);\n\t\t\tthis.ui.setFocus(this.extensionSelector);\n\t\t\tthis.ui.requestRender();\n\t\t});\n\t}\n\n\tprivate hideExtensionSelector(): void {\n\t\tthis.extensionSelector?.dispose();\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.extensionSelector = undefined;\n\t\tthis.ui.setFocus(this.editor);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async showExtensionConfirm(\n\t\ttitle: string,\n\t\tmessage: string,\n\t\topts?: ExtensionUIDialogOptions,\n\t): Promise<boolean> {\n\t\tconst result = await this.showExtensionSelector(`${title}\\n${message}`, [\"Yes\", \"No\"], opts);\n\t\treturn result === \"Yes\";\n\t}\n\n\tprivate async promptForMissingSessionCwd(error: MissingSessionCwdError): Promise<string | undefined> {\n\t\tconst confirmed = await this.showExtensionConfirm(\n\t\t\t\"Session cwd not found\",\n\t\t\tformatMissingSessionCwdPrompt(error.issue),\n\t\t);\n\t\treturn confirmed ? error.issue.fallbackCwd : undefined;\n\t}\n\n\tprivate showExtensionInput(\n\t\ttitle: string,\n\t\tplaceholder?: string,\n\t\topts?: ExtensionUIDialogOptions,\n\t): Promise<string | undefined> {\n\t\treturn new Promise((resolve) => {\n\t\t\tif (opts?.signal?.aborted) {\n\t\t\t\tresolve(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst onAbort = () => {\n\t\t\t\tthis.hideExtensionInput();\n\t\t\t\tresolve(undefined);\n\t\t\t};\n\t\t\topts?.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\tthis.extensionInput = new ExtensionInputComponent(\n\t\t\t\ttitle,\n\t\t\t\tplaceholder,\n\t\t\t\t(value) => {\n\t\t\t\t\topts?.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tthis.hideExtensionInput();\n\t\t\t\t\tresolve(value);\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\topts?.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tthis.hideExtensionInput();\n\t\t\t\t\tresolve(undefined);\n\t\t\t\t},\n\t\t\t\t{ tui: this.ui, timeout: opts?.timeout },\n\t\t\t);\n\n\t\t\tthis.editorContainer.clear();\n\t\t\tthis.editorContainer.addChild(this.extensionInput);\n\t\t\tthis.ui.setFocus(this.extensionInput);\n\t\t\tthis.ui.requestRender();\n\t\t});\n\t}\n\n\tprivate hideExtensionInput(): void {\n\t\tthis.extensionInput?.dispose();\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.extensionInput = undefined;\n\t\tthis.ui.setFocus(this.editor);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate showExtensionEditor(title: string, prefill?: string): Promise<string | undefined> {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.extensionEditor = new ExtensionEditorComponent(\n\t\t\t\tthis.ui,\n\t\t\t\tthis.keybindings,\n\t\t\t\ttitle,\n\t\t\t\tprefill,\n\t\t\t\t(value) => {\n\t\t\t\t\tthis.hideExtensionEditor();\n\t\t\t\t\tresolve(value);\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tthis.hideExtensionEditor();\n\t\t\t\t\tresolve(undefined);\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tthis.editorContainer.clear();\n\t\t\tthis.editorContainer.addChild(this.extensionEditor);\n\t\t\tthis.ui.setFocus(this.extensionEditor);\n\t\t\tthis.ui.requestRender();\n\t\t});\n\t}\n\n\tprivate hideExtensionEditor(): void {\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.extensionEditor = undefined;\n\t\tthis.ui.setFocus(this.editor);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate setCustomEditorComponent(factory: EditorFactory | undefined): void {\n\t\tthis.editorComponentFactory = factory;\n\t\t// Snapshot the current editor before replacing it. Paste markers are only\n\t\t// meaningful while their originating editor still owns the paste snapshot.\n\t\tconst currentEditor = this.editor;\n\t\tconst currentPromptStash = this.snapshotPromptStashFrom(currentEditor, currentEditor.getText());\n\n\t\tthis.editorContainer.clear();\n\n\t\tif (factory) {\n\t\t\t// Create the custom editor with tui, theme, and keybindings\n\t\t\tconst newEditor = factory(this.ui, getEditorTheme(), this.keybindings);\n\n\t\t\t// Restore before wiring the shared onChange callback: setText may emit a\n\t\t\t// change, and an empty custom editor cannot reconstruct the old snapshot.\n\t\t\tconst canRestorePasteSnapshot =\n\t\t\t\tcurrentPromptStash.pasteSnapshot === undefined || newEditor.restorePasteSnapshot !== undefined;\n\t\t\tnewEditor.setText(\n\t\t\t\tcanRestorePasteSnapshot\n\t\t\t\t\t? currentPromptStash.text\n\t\t\t\t\t: (currentPromptStash.expandedText ?? currentPromptStash.text),\n\t\t\t);\n\t\t\tif (currentPromptStash.pasteSnapshot && newEditor.restorePasteSnapshot) {\n\t\t\t\tnewEditor.restorePasteSnapshot(currentPromptStash.pasteSnapshot);\n\t\t\t}\n\n\t\t\t// Wire up callbacks from the default editor. onChange snapshots the\n\t\t\t// active editor while it still owns paste markers and attachments, so\n\t\t\t// submit remains exact even when an editor clears before calling onSubmit.\n\t\t\tnewEditor.onSubmit = this.defaultEditor.onSubmit;\n\t\t\tnewEditor.onChange = this.defaultEditor.onChange;\n\n\t\t\t// Copy appearance settings if supported\n\t\t\tif (newEditor.borderColor !== undefined) {\n\t\t\t\tnewEditor.borderColor = this.defaultEditor.borderColor;\n\t\t\t}\n\t\t\tif (newEditor.setPaddingX !== undefined) {\n\t\t\t\tnewEditor.setPaddingX(this.defaultEditor.getPaddingX());\n\t\t\t}\n\n\t\t\t// Set autocomplete if supported\n\t\t\tif (newEditor.setAutocompleteProvider && this.autocompleteProvider) {\n\t\t\t\tnewEditor.setAutocompleteProvider(this.autocompleteProvider);\n\t\t\t}\n\n\t\t\t// If extending CustomEditor, copy app-level handlers\n\t\t\t// Use duck typing since instanceof fails across jiti module boundaries\n\t\t\tconst customEditor = newEditor as unknown as Record<string, unknown>;\n\t\t\tif (\"actionHandlers\" in customEditor && customEditor.actionHandlers instanceof Map) {\n\t\t\t\tif (!customEditor.onEscape) {\n\t\t\t\t\tcustomEditor.onEscape = () => this.defaultEditor.onEscape?.();\n\t\t\t\t}\n\t\t\t\tif (!customEditor.onCtrlD) {\n\t\t\t\t\tcustomEditor.onCtrlD = () => this.defaultEditor.onCtrlD?.();\n\t\t\t\t}\n\t\t\t\tif (!customEditor.onPasteImage) {\n\t\t\t\t\tcustomEditor.onPasteImage = () => this.defaultEditor.onPasteImage?.();\n\t\t\t\t}\n\t\t\t\tif (!customEditor.onMoveBelowPrompt) {\n\t\t\t\t\tcustomEditor.onMoveBelowPrompt = () => this.defaultEditor.onMoveBelowPrompt?.();\n\t\t\t\t}\n\t\t\t\tif (!customEditor.onAgentsBack) {\n\t\t\t\t\tcustomEditor.onAgentsBack = () => this.defaultEditor.onAgentsBack?.();\n\t\t\t\t}\n\t\t\t\tif (!customEditor.onExtensionShortcut) {\n\t\t\t\t\tcustomEditor.onExtensionShortcut = (data: string) => this.defaultEditor.onExtensionShortcut?.(data);\n\t\t\t\t}\n\t\t\t\t// Copy action handlers (clear, suspend, model switching, etc.)\n\t\t\t\tfor (const [action, handler] of this.defaultEditor.actionHandlers) {\n\t\t\t\t\t(customEditor.actionHandlers as Map<string, () => void>).set(action, handler);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.editor = newEditor;\n\t\t} else {\n\t\t\t// Restore the default editor with the same rich snapshot (or expanded\n\t\t\t// fallback text if this editor implementation cannot restore it).\n\t\t\tconst canRestorePasteSnapshot =\n\t\t\t\tcurrentPromptStash.pasteSnapshot === undefined || this.defaultEditor.restorePasteSnapshot !== undefined;\n\t\t\tthis.defaultEditor.setText(\n\t\t\t\tcanRestorePasteSnapshot\n\t\t\t\t\t? currentPromptStash.text\n\t\t\t\t\t: (currentPromptStash.expandedText ?? currentPromptStash.text),\n\t\t\t);\n\t\t\tif (currentPromptStash.pasteSnapshot && this.defaultEditor.restorePasteSnapshot) {\n\t\t\t\tthis.defaultEditor.restorePasteSnapshot(currentPromptStash.pasteSnapshot);\n\t\t\t}\n\t\t\tthis.editor = this.defaultEditor;\n\t\t}\n\t\tthis.latestEditorPromptStash = currentPromptStash;\n\n\t\tthis.editorContainer.addChild(this.editor as Component);\n\t\tthis.ui.setFocus(this.editor as Component);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate showExtensionNotify(message: string, type?: \"info\" | \"warning\" | \"error\"): void {\n\t\tif (type === \"error\") {\n\t\t\tthis.showError(message);\n\t\t} else if (type === \"warning\") {\n\t\t\tthis.showWarning(message);\n\t\t} else {\n\t\t\tthis.showStatus(message);\n\t\t}\n\t}\n\n\t/** Show a custom component with keyboard focus. Overlay mode renders on top of existing content. */\n\tprivate async showExtensionCustom<T>(\n\t\tfactory: (\n\t\t\ttui: TUI,\n\t\t\ttheme: Theme,\n\t\t\tkeybindings: KeybindingsManager,\n\t\t\tdone: (result: T) => void,\n\t\t) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,\n\t\toptions?: {\n\t\t\toverlay?: boolean;\n\t\t\toverlayOptions?: OverlayOptions | (() => OverlayOptions);\n\t\t\tonHandle?: (handle: OverlayHandle) => void;\n\t\t},\n\t): Promise<T> {\n\t\tconst savedText = this.editor.getText();\n\t\tconst isOverlay = options?.overlay ?? false;\n\n\t\tconst restoreEditor = () => {\n\t\t\tthis.editorContainer.clear();\n\t\t\tthis.editorContainer.addChild(this.editor);\n\t\t\tthis.editor.setText(savedText);\n\t\t\tthis.ui.setFocus(this.editor);\n\t\t\tthis.ui.requestRender();\n\t\t};\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tlet component: Component & { dispose?(): void };\n\t\t\tlet closed = false;\n\n\t\t\tconst close = (result: T) => {\n\t\t\t\tif (closed) return;\n\t\t\t\tclosed = true;\n\t\t\t\tif (isOverlay) this.ui.hideOverlay();\n\t\t\t\telse restoreEditor();\n\t\t\t\t// Note: both branches above already call requestRender\n\t\t\t\tresolve(result);\n\t\t\t\ttry {\n\t\t\t\t\tcomponent?.dispose?.();\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore dispose errors */\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tPromise.resolve(factory(this.ui, theme, this.keybindings, close))\n\t\t\t\t.then((c) => {\n\t\t\t\t\tif (closed) return;\n\t\t\t\t\tcomponent = c;\n\t\t\t\t\tif (isOverlay) {\n\t\t\t\t\t\t// Resolve overlay options - can be static or dynamic function\n\t\t\t\t\t\tconst resolveOptions = (): OverlayOptions | undefined => {\n\t\t\t\t\t\t\tif (options?.overlayOptions) {\n\t\t\t\t\t\t\t\tconst opts =\n\t\t\t\t\t\t\t\t\ttypeof options.overlayOptions === \"function\"\n\t\t\t\t\t\t\t\t\t\t? options.overlayOptions()\n\t\t\t\t\t\t\t\t\t\t: options.overlayOptions;\n\t\t\t\t\t\t\t\treturn opts;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Fallback: use component's width property if available\n\t\t\t\t\t\t\tconst w = (component as { width?: number }).width;\n\t\t\t\t\t\t\treturn w ? { width: w } : undefined;\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst handle = this.ui.showOverlay(component, resolveOptions());\n\t\t\t\t\t\t// Expose handle to caller for visibility control\n\t\t\t\t\t\toptions?.onHandle?.(handle);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.editorContainer.clear();\n\t\t\t\t\t\tthis.editorContainer.addChild(component);\n\t\t\t\t\t\tthis.ui.setFocus(component);\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tif (closed) return;\n\t\t\t\t\tif (!isOverlay) restoreEditor();\n\t\t\t\t\treject(err);\n\t\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Show an extension error in the UI.\n\t */\n\tprivate showExtensionError(extensionPath: string, error: string, stack?: string): void {\n\t\tconst errorMsg = `Extension \"${extensionPath}\" error: ${error}`;\n\t\tconst errorText = new Text(theme.fg(\"error\", errorMsg), 1, 0);\n\t\tthis.chatContainer.addChild(errorText);\n\t\tif (stack) {\n\t\t\t// Show stack trace in dim color, indented\n\t\t\tconst stackLines = stack\n\t\t\t\t.split(\"\\n\")\n\t\t\t\t.slice(1) // Skip first line (duplicates error message)\n\t\t\t\t.map((line) => theme.fg(\"dim\", ` ${line.trim()}`))\n\t\t\t\t.join(\"\\n\");\n\t\t\tif (stackLines) {\n\t\t\t\tthis.chatContainer.addChild(new Text(stackLines, 1, 0));\n\t\t\t}\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate setupKeyHandlers(): void {\n\t\tthis.defaultEditor.getHeaderLine = () => this.getQueueSelectionHeader();\n\t\t// Set up handlers on defaultEditor - they use this.editor for text access\n\t\t// so they work correctly regardless of which editor is active\n\t\tthis.defaultEditor.onEscape = () => {\n\t\t\tthis.handleEscape();\n\t\t};\n\n\t\tthis.defaultEditor.onAction(\"app.clear\", () => this.handleCtrlC());\n\t\tthis.defaultEditor.onAction(\"app.interrupt\", () => this.handleInterruptKey());\n\t\tthis.defaultEditor.onAction(\"app.shortcuts\", () => this.showShortcutGuide());\n\t\tthis.defaultEditor.onCtrlD = () => this.handleCtrlD();\n\t\tthis.defaultEditor.onAction(\"app.suspend\", () => this.handleCtrlZ());\n\n\t\t// Global debug handler on TUI (works regardless of focus)\n\t\tthis.ui.onDebug = () => {\n\t\t\tvoid this.handleDebugCommand();\n\t\t};\n\t\tthis.defaultEditor.onAction(\"app.model.select\", () => this.showModelSelector());\n\t\tthis.defaultEditor.onAction(\"app.tools.expand\", () => this.toggleToolOutputExpansion());\n\t\tthis.defaultEditor.onAction(\"app.messages.expand\", () => this.toggleAgentMessageExpansion());\n\t\tthis.defaultEditor.onAction(\"app.edits.expand\", () => this.toggleEditDiffExpansion());\n\t\tthis.defaultEditor.onAction(\"app.thinking.toggle\", () => this.toggleThinkingBlockVisibility());\n\t\tthis.defaultEditor.onAction(\"app.subagents.focus\", () => this.focusSubagentSummary());\n\t\tthis.defaultEditor.onAction(\"app.heartbeats.open\", () => {\n\t\t\tvoid this.showHeartbeatManager();\n\t\t});\n\t\tthis.defaultEditor.onAction(\"app.editor.external\", () => this.openExternalEditor());\n\t\tthis.defaultEditor.onAction(\"app.prompt.stash\", () => this.handlePromptStash());\n\t\tthis.defaultEditor.onAction(\"app.message.followUp\", () => this.handleFollowUp());\n\t\tthis.defaultEditor.onAction(\"app.message.navigateOlder\", () => this.browseQueueSelection(-1));\n\t\tthis.defaultEditor.onAction(\"app.message.navigateNewer\", () => this.browseQueueSelection(1));\n\t\tthis.defaultEditor.onAction(\"app.message.moveEarlier\", () => this.moveQueueSelection(-1));\n\t\tthis.defaultEditor.onAction(\"app.message.moveLater\", () => this.moveQueueSelection(1));\n\t\tthis.defaultEditor.onAction(\"app.session.new\", () => this.handleClearCommand());\n\t\tthis.defaultEditor.onAction(\"app.session.tree\", () => {\n\t\t\tvoid this.showTreeSelector();\n\t\t});\n\t\tthis.defaultEditor.onAction(\"app.session.fork\", () => {\n\t\t\tvoid this.showUserMessageSelector();\n\t\t});\n\t\tthis.defaultEditor.onAction(\"app.session.resume\", () => {\n\t\t\tvoid this.requestAgentsView();\n\t\t});\n\t\tthis.defaultEditor.onAgentsBack = () => this.handleAgentsBack();\n\t\tthis.defaultEditor.onMoveBelowPrompt = () => this.focusSubagentSummary();\n\n\t\tthis.defaultEditor.onChange = (text: string) => {\n\t\t\tif (text.length > 0 && !this.isApplyingQueueSelectionText) {\n\t\t\t\tthis.latestEditorPromptStash = this.snapshotPromptStashFrom(this.editor, text);\n\t\t\t}\n\t\t\tif (this.escapeRepeatAction && !this.isApplyingQueueSelectionText) {\n\t\t\t\tthis.clearEscapeRepeat();\n\t\t\t}\n\t\t\tif (text.length > 0) {\n\t\t\t\tthis.clearCtrlCExitHint();\n\t\t\t}\n\t\t};\n\n\t\tthis.defaultEditor.onPasteImage = () => {\n\t\t\tthis.handleClipboardImagePaste();\n\t\t};\n\t}\n\n\tprivate snapshotPromptStashFrom(editor: EditorComponent, text: string): PromptStash {\n\t\tconst pasteSnapshot = editor.getPasteSnapshot?.();\n\t\tconst images = this.getPromptStashImages(text);\n\t\treturn {\n\t\t\ttext,\n\t\t\texpandedText: pasteSnapshot ? (editor.getExpandedText?.() ?? text) : undefined,\n\t\t\tpasteSnapshot,\n\t\t\t...(images.length > 0 ? { images } : {}),\n\t\t};\n\t}\n\n\tprivate snapshotPromptStash(text: string): PromptStash {\n\t\treturn this.snapshotPromptStashFrom(this.editor, text);\n\t}\n\n\tprivate restorePromptStashOnOpen(): void {\n\t\tif (!this.promptStash?.restoreOnOpen) return;\n\t\t// Land the restore notice in its own status block: init may have just posted\n\t\t// a notice (e.g. compaction) that showStatus would otherwise replace.\n\t\tthis.lastStatusText = undefined;\n\t\tthis.lastStatusSpacer = undefined;\n\t\tthis.restorePromptStashIfEditorEmpty();\n\t}\n\n\tprivate stashDraftForAgentsView(): void {\n\t\tconst text = this.editor.getText();\n\t\tif (!text.trim()) return;\n\t\t// Head of the durable queue so it restores first on return; an existing\n\t\t// manual stash stays queued behind it and keeps its manual-stash semantics.\n\t\tconst existing = [this.promptStashState.stash, ...(this.promptStashState.queuedStashes ?? [])].filter(\n\t\t\t(stash): stash is PromptStash => stash !== undefined,\n\t\t);\n\t\tthis.promptStashState.stash = { ...this.snapshotPromptStash(text), restoreOnOpen: true };\n\t\tthis.promptStashState.queuedStashes = existing.length > 0 ? existing : undefined;\n\t}\n\n\tprivate handlePromptStash(): void {\n\t\tconst text = this.editor.getText();\n\t\tif (!text.trim()) {\n\t\t\tif (!this.restorePromptStashIfEditorEmpty()) {\n\t\t\t\tthis.showStatus(\"No prompt to stash\");\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (this.promptStash !== undefined) {\n\t\t\tthis.showStatus(\"Prompt stash already has a draft\");\n\t\t\treturn;\n\t\t}\n\t\tthis.promptStash = this.snapshotPromptStash(text);\n\t\tthis.editor.setText(\"\");\n\t\tthis.showStatus(\"Stashed prompt\");\n\t}\n\n\tprivate restorePromptStashIfEditorEmpty(stash = this.promptStash): boolean {\n\t\tif (stash === undefined || this.editor.getText().trim()) {\n\t\t\treturn false;\n\t\t}\n\t\tif (this.promptStash !== stash) {\n\t\t\treturn false;\n\t\t}\n\t\tthis.promptStash = this.promptStashState?.queuedStashes?.shift();\n\t\tif (this.promptStashState?.queuedStashes?.length === 0) this.promptStashState.queuedStashes = undefined;\n\t\tconst canRestorePasteSnapshot =\n\t\t\tstash.pasteSnapshot === undefined || this.editor.restorePasteSnapshot !== undefined;\n\t\tthis.editor.setText(canRestorePasteSnapshot ? stash.text : (stash.expandedText ?? stash.text));\n\t\tif (stash.pasteSnapshot && this.editor.restorePasteSnapshot) {\n\t\t\tthis.editor.restorePasteSnapshot(stash.pasteSnapshot);\n\t\t}\n\t\tthis.latestEditorPromptStash = this.snapshotPromptStash(this.editor.getText());\n\t\tthis.showStatus(\"Restored stashed prompt\");\n\t\treturn true;\n\t}\n\n\tprivate retainSubmittedDraft(\n\t\tstash: PromptStash,\n\t\tsubmissionGeneration: number,\n\t\tstate: PromptStashState = this.promptStashState,\n\t): void {\n\t\tthis.retainedSubmissionGenerations.set(stash, submissionGeneration);\n\t\tconst ordered = [state.stash, ...(state.queuedStashes ?? [])].filter(\n\t\t\t(candidate): candidate is PromptStash => candidate !== undefined,\n\t\t);\n\t\tconst insertAt = ordered.findIndex((candidate) => {\n\t\t\tconst generation = this.retainedSubmissionGenerations.get(candidate);\n\t\t\treturn generation !== undefined && generation > submissionGeneration;\n\t\t});\n\t\tordered.splice(insertAt === -1 ? ordered.length : insertAt, 0, stash);\n\t\tstate.stash = ordered.shift();\n\t\tstate.queuedStashes = ordered.length > 0 ? ordered : undefined;\n\t}\n\n\tprivate retainStartupPromptDrafts(prompts: readonly InteractiveInitialPrompt[]): void {\n\t\t// Reserve every marker visible anywhere in the retained batch before assigning\n\t\t// any image. This prevents an early prompt's attachment from making a literal\n\t\t// marker in a later prompt resolve to the wrong image.\n\t\tconst reserved = new Set(this.pastedImages.keys());\n\t\tfor (const stash of [this.promptStash, ...(this.promptStashState.queuedStashes ?? [])]) {\n\t\t\tif (stash) for (const markerId of imageMarkerIds(stash.text)) reserved.add(markerId);\n\t\t}\n\t\tfor (const prompt of prompts) {\n\t\t\tfor (const markerId of imageMarkerIds(prompt.text)) reserved.add(markerId);\n\t\t}\n\t\tfor (const markerId of reserved) {\n\t\t\tthis.nextImageMarkerId = Math.max(this.nextImageMarkerId, markerId + 1);\n\t\t}\n\t\tconst allocateMarker = () => {\n\t\t\twhile (reserved.has(this.nextImageMarkerId)) this.nextImageMarkerId++;\n\t\t\tconst markerId = this.nextImageMarkerId++;\n\t\t\treserved.add(markerId);\n\t\t\treturn markerId;\n\t\t};\n\n\t\tconst retained: PromptStash[] = [];\n\t\tfor (const prompt of prompts) {\n\t\t\tlet text = prompt.text;\n\t\t\t// A startup prompt owns only the images passed with it. Remap literal\n\t\t\t// markers that already name registry data so restoring this draft cannot\n\t\t\t// accidentally attach an old or another prompt's image.\n\t\t\tconst literalRemaps = new Map<number, number>();\n\t\t\tfor (const markerId of imageMarkerIds(text)) {\n\t\t\t\tif (this.pastedImages.has(markerId) && !literalRemaps.has(markerId)) {\n\t\t\t\t\tliteralRemaps.set(markerId, allocateMarker());\n\t\t\t\t}\n\t\t\t}\n\t\t\ttext = remapImageMarkers(text, literalRemaps);\n\n\t\t\tconst images: Array<readonly [number, ImageContent]> = [];\n\t\t\tfor (const image of prompt.images ?? []) {\n\t\t\t\tconst markerId = allocateMarker();\n\t\t\t\timages.push([markerId, image]);\n\t\t\t\ttext += `${text.length > 0 && !/\\s$/.test(text) ? \" \" : \"\"}${formatImageMarker(markerId)}`;\n\t\t\t}\n\t\t\tretained.push({\n\t\t\t\ttext,\n\t\t\t\t...(images.length > 0 ? { images } : {}),\n\t\t\t});\n\t\t\tfor (const [markerId, image] of images) this.pastedImages.set(markerId, image);\n\t\t}\n\n\t\t// Startup drafts form the head of the durable queue. Preserve any older\n\t\t// client draft after them, and let submissions released by the barrier append.\n\t\tconst existing = [this.promptStashState.stash, ...(this.promptStashState.queuedStashes ?? [])].filter(\n\t\t\t(stash): stash is PromptStash => stash !== undefined,\n\t\t);\n\t\tconst ordered = [...retained, ...existing];\n\t\tthis.promptStashState.stash = ordered.shift();\n\t\tthis.promptStashState.queuedStashes = ordered.length > 0 ? ordered : undefined;\n\t}\n\n\tprivate getPromptStashImages(text: string): readonly (readonly [number, ImageContent])[] {\n\t\tconst images: Array<readonly [number, ImageContent]> = [];\n\t\tfor (const markerId of imageMarkerIds(text)) {\n\t\t\tconst image = this.pastedImages.get(markerId);\n\t\t\tif (image) {\n\t\t\t\timages.push([markerId, image]);\n\t\t\t}\n\t\t}\n\t\treturn images;\n\t}\n\n\tprivate async handleClipboardImagePaste(): Promise<void> {\n\t\ttry {\n\t\t\tconst image = await readClipboardImage();\n\t\t\tif (!image) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Resize down to the inline image size limit, mirroring the CLI @file\n\t\t\t// path, so large screenshots don't exceed provider limits. Fall back to\n\t\t\t// the raw bytes if resizing is unavailable.\n\t\t\tconst raw: ImageContent = {\n\t\t\t\ttype: \"image\",\n\t\t\t\tdata: Buffer.from(image.bytes).toString(\"base64\"),\n\t\t\t\tmimeType: image.mimeType,\n\t\t\t};\n\t\t\tconst resized = await resizeImage(raw);\n\t\t\tconst attachment: ImageContent = resized\n\t\t\t\t? { type: \"image\", data: resized.data, mimeType: resized.mimeType }\n\t\t\t\t: raw;\n\n\t\t\t// Register the image and insert a visible marker. The image is attached to\n\t\t\t// the prompt as multimodal content rather than written to disk, so a vision\n\t\t\t// model receives it directly.\n\t\t\tconst markerId = this.nextImageMarkerId++;\n\t\t\tthis.rememberPastedImage(markerId, attachment);\n\t\t\tthis.editor.insertTextAtCursor?.(formatImageMarker(markerId));\n\t\t\tthis.ui.requestRender();\n\n\t\t\tconst model = this.getCurrentModel();\n\t\t\tif (model && !model.input.includes(\"image\")) {\n\t\t\t\tthis.showStatus(\"Current model does not support images; the attachment will be omitted.\");\n\t\t\t}\n\t\t} catch {\n\t\t\t// Silently ignore clipboard errors (may not have permission, etc.)\n\t\t}\n\t}\n\n\t/**\n\t * Record a pasted image, evicting the oldest entries once the retained bytes\n\t * exceed {@link MAX_PASTED_IMAGE_BYTES} so a long session stays bounded. The\n\t * just-added image and any whose marker is still referenced (editor or queues)\n\t * are never evicted, so a live marker never loses its image.\n\t */\n\tprivate rememberPastedImage(id: number, image: ImageContent): void {\n\t\tthis.pastedImages.set(id, image);\n\t\tconst keep = this.liveImageMarkerIds();\n\t\tkeep.add(id);\n\t\tevictImagesToBudget(this.pastedImages, (img) => img.data.length, MAX_PASTED_IMAGE_BYTES, keep);\n\t}\n\n\t/**\n\t * Marker ids still reachable — current editor text, prompt history (recallable\n\t * with the up arrow), the compaction queue, and the connection queue. These are\n\t * never evicted so a recall or resend never finds a marker with no image.\n\t */\n\tprivate liveImageMarkerIds(): Set<number> {\n\t\tconst ids = new Set<number>();\n\t\tconst add = (text: string) => {\n\t\t\tfor (const markerId of imageMarkerIds(text)) {\n\t\t\t\tids.add(markerId);\n\t\t\t}\n\t\t};\n\t\tadd(this.editor.getText());\n\t\tfor (const stash of [this.promptStash, ...(this.promptStashState?.queuedStashes ?? [])]) {\n\t\t\tif (stash) add(stash.text);\n\t\t}\n\t\tfor (const entry of this.editor.getHistory?.() ?? []) {\n\t\t\tadd(entry);\n\t\t}\n\t\tfor (const msg of [...this.connectionQueue.steering, ...this.connectionQueue.followUp]) {\n\t\t\tadd(msg);\n\t\t}\n\t\treturn ids;\n\t}\n\n\t/**\n\t * The images whose `[image #N]` markers are present in `text`, or undefined if\n\t * none. Read-only: the registry is never cleared here, so deleting a marker\n\t * simply drops its image while restoring the marker (undo, history, retry,\n\t * dequeue) brings it back. Marker presence in the sent text is the single\n\t * source of truth.\n\t *\n\t * Resolved against the current model: if it has no image input, attachments\n\t * are dropped here (matching the paste-time hint) rather than sent and\n\t * downgraded downstream.\n\t */\n\tprivate collectImagesFor(text: string): ImageContent[] | undefined {\n\t\tconst model = this.getCurrentModel();\n\t\tif (model && !model.input.includes(\"image\")) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst images = collectMarkedImages(this.pastedImages, text);\n\t\treturn images.length > 0 ? images : undefined;\n\t}\n\n\tprivate hasPastedImagesFor(text: string): boolean {\n\t\treturn imageMarkerIds(text).some((id) => this.pastedImages.has(id));\n\t}\n\n\tprivate async handleSideQuestion(question: string): Promise<void> {\n\t\tif (!question) {\n\t\t\tthis.showWarning(\"Usage: /btw <question>\");\n\t\t\treturn;\n\t\t}\n\t\tif (this.activeSideQuestionId) {\n\t\t\tthis.showWarning(\"Wait for the current side question to finish or cancel it first.\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Turns already answered in the open pane seed the follow-up's context.\n\t\tconst previousTurns = this.sideQuestionTurns\n\t\t\t.filter((turn) => turn.answer)\n\t\t\t.map((turn) => ({ question: turn.question, answer: turn.answer }));\n\t\tconst event: AgentConnectionSideQuestionEvent = {\n\t\t\tid: randomUUID(),\n\t\t\tquestion,\n\t\t\tanswer: \"\",\n\t\t\tstatus: \"running\",\n\t\t};\n\t\tthis.activeSideQuestionId = event.id;\n\t\tthis.sideQuestionEvent = event;\n\t\tthis.sideQuestionTurns.push(event);\n\t\tif (this.sideQuestionComponent) {\n\t\t\tthis.sideQuestionComponent.addTurn(event);\n\t\t} else {\n\t\t\tthis.sideQuestionComponent = new SideQuestionComponent(event, this.settingsManager.getEditorPaddingX());\n\t\t\tthis.sideQuestionContainer.addChild(new Spacer(1));\n\t\t\tthis.sideQuestionContainer.addChild(this.sideQuestionComponent);\n\t\t}\n\t\tthis.ui.requestRender();\n\n\t\ttry {\n\t\t\tawait this.agentConnection.startSideQuestion(\n\t\t\t\tevent.id,\n\t\t\t\tquestion,\n\t\t\t\tpreviousTurns.length > 0 ? previousTurns : undefined,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tthis.handleSideQuestionEvent({\n\t\t\t\t...event,\n\t\t\t\tstatus: \"error\",\n\t\t\t\terrorMessage: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate handleSideQuestionEvent(event: AgentConnectionSideQuestionEvent): void {\n\t\tif (event.id === this.activeSideQuestionId && event.status !== \"running\") {\n\t\t\tthis.activeSideQuestionId = undefined;\n\t\t}\n\t\tif (event.id !== this.sideQuestionEvent?.id || !this.sideQuestionComponent) {\n\t\t\treturn;\n\t\t}\n\t\tthis.sideQuestionEvent = event;\n\t\tconst turnIndex = this.sideQuestionTurns.findIndex((turn) => turn.id === event.id);\n\t\tif (turnIndex !== -1) {\n\t\t\tthis.sideQuestionTurns[turnIndex] = event;\n\t\t}\n\t\tthis.sideQuestionComponent.update(event);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate finishSideQuestionBash(\n\t\tevent: Extract<AgentConnectionSessionEvent, { type: \"bash_end\" }>,\n\t\trawOutput: string,\n\t): void {\n\t\t// Release the pane's running state before any early return so the cancel\n\t\t// hint cannot stay stuck if the pending state was already cleared.\n\t\tthis.sideQuestionComponent?.finishBash();\n\t\tconst bash = this.sideQuestionBash;\n\t\tif (!bash) {\n\t\t\treturn;\n\t\t}\n\t\tthis.sideQuestionBash = undefined;\n\t\t// The pane already rendered the run; this only seeds follow-up turns.\n\t\tif (!bash.seedTranscript || event.cancelled || event.errorMessage) {\n\t\t\treturn;\n\t\t}\n\t\tconst truncation = truncateTail(rawOutput);\n\t\tconst output = truncation.content.replace(/\\n+$/, \"\");\n\t\tthis.sideQuestionTurns.push({\n\t\t\tid: `side-bash-${randomUUID()}`,\n\t\t\tquestion: bash.input,\n\t\t\tanswer: bashOutputToText({\n\t\t\t\toutput,\n\t\t\t\texitCode: event.exitCode,\n\t\t\t\tcancelled: false,\n\t\t\t\ttruncated: event.truncated || truncation.truncated,\n\t\t\t\tfullOutputPath: event.fullOutputPath,\n\t\t\t}),\n\t\t\tstatus: \"complete\",\n\t\t});\n\t}\n\n\tprivate clearSideQuestion(options: { abort?: boolean } = {}): void {\n\t\tconst event = this.sideQuestionEvent;\n\t\tif (options.abort && event?.status === \"running\") {\n\t\t\tthis.abortSideQuestion(event.id);\n\t\t}\n\t\tif (this.sideQuestionBash) {\n\t\t\t// A side-conversation bash run dies with its pane. Its bash_* events may\n\t\t\t// still be in flight (even bash_start), so swallow them until bash_end.\n\t\t\tconst ownsRunningBash = this.sideQuestionBashComponent !== undefined;\n\t\t\tthis.sideQuestionBashDiscarded = this.sideQuestionBash.runId;\n\t\t\tthis.sideQuestionBash = undefined;\n\t\t\tthis.sideQuestionBashComponent = undefined;\n\t\t\t// abort_bash is session-scoped. Before our matching bash_start arrives,\n\t\t\t// another client may own the slot, so only abort a run we have observed.\n\t\t\tif (ownsRunningBash) {\n\t\t\t\tvoid this.agentConnection.abortBash().catch(() => undefined);\n\t\t\t}\n\t\t}\n\t\tthis.sideQuestionEvent = undefined;\n\t\tthis.sideQuestionTurns = [];\n\t\tthis.sideQuestionComponent = undefined;\n\t\tthis.sideQuestionContainer.clear();\n\t\tif (this.isInitialized) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate resetSideQuestion(): void {\n\t\tthis.clearSideQuestion({ abort: true });\n\t\tthis.activeSideQuestionId = undefined;\n\t}\n\n\tprivate abortSideQuestion(id: string, reportError = false): void {\n\t\tvoid this.agentConnection\n\t\t\t.abortSideQuestion(id)\n\t\t\t.then((aborted) => {\n\t\t\t\tif (!aborted && this.activeSideQuestionId === id) {\n\t\t\t\t\tthis.activeSideQuestionId = undefined;\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tif (reportError) {\n\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t}\n\t\t\t});\n\t}\n\n\tprivate async renderTreeNavigation(result: { editorText?: string }): Promise<void> {\n\t\tthis.clearSideQuestion({ abort: true });\n\t\tthis.chatContainer.clear();\n\t\tawait this.renderInitialMessages();\n\t\tif (result.editorText && !this.editor.getText().trim()) {\n\t\t\tthis.editor.setText(result.editorText);\n\t\t}\n\t\tthis.showStatus(\"Navigated to selected point\");\n\t}\n\n\tprivate setupEditorSubmitHandler(): void {\n\t\tthis.defaultEditor.onSubmit = async (text: string) => {\n\t\t\tconst streamingBehavior = this.submittedInputBehavior;\n\t\t\tthis.submittedInputBehavior = \"steer\";\n\t\t\tif (this.queueSelection?.isBrowsing && !this.pendingQueueEdit) {\n\t\t\t\tconst targetLane = streamingBehavior === \"followUp\" ? \"followUp\" : \"steering\";\n\t\t\t\ttry {\n\t\t\t\t\tif (await this.applyQueueSelection(text, targetLane)) return;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\ttext = text.trim();\n\t\t\tif (!text) return;\n\t\t\tconst submissionGeneration = ++this.inputSubmissionGeneration;\n\t\t\tthis.inputSubmissionsPending++;\n\t\t\tthis.clearShortcutGuide();\n\t\t\t// A barrier wait can resume after /new repointed the live session fields.\n\t\t\tconst submissionStashState = this.promptStashState;\n\t\t\tconst submissionSessionId = this.promptStashSessionId;\n\t\t\tconst promptStashToRestore = this.promptStash;\n\t\t\tconst liveEditorText = this.editor.getText();\n\t\t\tconst submittedDraft =\n\t\t\t\tthis.pendingSubmittedPromptStash ??\n\t\t\t\t(liveEditorText.trim() ? this.snapshotPromptStash(liveEditorText) : this.latestEditorPromptStash);\n\t\t\tthis.pendingSubmittedPromptStash = undefined;\n\t\t\tlet restorePromptStashAfterSubmit = true;\n\t\t\tlet submissionOutcome: StartupPromptBarrierOutcome = \"admitted\";\n\n\t\t\ttry {\n\t\t\t\tconst slashCommand = parseSlashCommand(text);\n\t\t\t\tconst commandName = slashCommand ? resolveBuiltinSlashCommandName(slashCommand.name) : undefined;\n\t\t\t\tconst commandArgs = slashCommand?.args ?? \"\";\n\t\t\t\tconst canonicalCommandText = commandName ? `/${commandName}${commandArgs ? ` ${commandArgs}` : \"\"}` : text;\n\n\t\t\t\t// Slash commands are disabled while a side conversation is open: they\n\t\t\t\t// act on the main session, which is confusing mid-side-chat. The notice\n\t\t\t\t// renders as a pane response and never reaches the model. A reply that\n\t\t\t\t// merely starts with \"/\" (e.g. an absolute path) is not a command and\n\t\t\t\t// falls through to the side-conversation capture below.\n\t\t\t\tif (\n\t\t\t\t\tthis.sideQuestionComponent &&\n\t\t\t\t\tslashCommand !== undefined &&\n\t\t\t\t\t(isBuiltinSlashCommandName(slashCommand.name) ||\n\t\t\t\t\t\tthis.connectionCommands.some((command) => command.name === slashCommand.name))\n\t\t\t\t) {\n\t\t\t\t\tthis.editor.addToHistory?.(text);\n\t\t\t\t\tthis.sideQuestionComponent.addTurn({\n\t\t\t\t\t\tid: `side-notice-${randomUUID()}`,\n\t\t\t\t\t\tquestion: text,\n\t\t\t\t\t\tanswer:\n\t\t\t\t\t\t\t\"Slash commands are not available in side conversations. Press esc to return to the main thread.\",\n\t\t\t\t\t\tstatus: \"complete\",\n\t\t\t\t\t});\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"btw\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.handleSideQuestion(commandArgs);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"settings\" && !commandArgs) {\n\t\t\t\t\tawait this.showSettingsSelector();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"scoped-models\" && !commandArgs) {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.showModelsSelector();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"model\") {\n\t\t\t\t\tconst searchTerm = commandArgs || undefined;\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.handleModelCommand(searchTerm);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"effort\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tthis.handleEffortCommand(commandArgs);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"fast\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tif (commandArgs) {\n\t\t\t\t\t\tthis.showError(\"Usage: /fast\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.handleFastCommand();\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"export\") {\n\t\t\t\t\tawait this.handleExportCommand(canonicalCommandText);\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"import\") {\n\t\t\t\t\tawait this.handleImportCommand(canonicalCommandText);\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"share\" && !commandArgs) {\n\t\t\t\t\tawait this.handleShareCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"copy\" && !commandArgs) {\n\t\t\t\t\tawait this.handleCopyCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"name\") {\n\t\t\t\t\tawait this.handleNameCommand(canonicalCommandText);\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"rlm-max-depth\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.handleRlmMaxDepthCommand(commandArgs);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"session\" && !commandArgs) {\n\t\t\t\t\tthis.echoLocalCommand(text);\n\t\t\t\t\tawait this.handleSessionCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"system-prompt\" && !commandArgs) {\n\t\t\t\t\tthis.echoLocalCommand(text);\n\t\t\t\t\tawait this.handleSystemPromptCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"context\" && !commandArgs) {\n\t\t\t\t\tthis.echoLocalCommand(text);\n\t\t\t\t\tawait this.handleContextCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"logs\" && !commandArgs) {\n\t\t\t\t\tthis.echoLocalCommand(text);\n\t\t\t\t\tthis.handleLogsCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"heartbeat\") {\n\t\t\t\t\tawait this.handleHeartbeatCommand(canonicalCommandText);\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"heartbeats\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.showHeartbeatManager();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"changelog\" && !commandArgs) {\n\t\t\t\t\tthis.echoLocalCommand(text);\n\t\t\t\t\tthis.handleChangelogCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"hotkeys\" && !commandArgs) {\n\t\t\t\t\tthis.echoLocalCommand(text);\n\t\t\t\t\tthis.handleHotkeysCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"fork\" && !commandArgs) {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.showUserMessageSelector();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"clone\" && !commandArgs) {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.handleCloneCommand();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"tree\" && !commandArgs) {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\trestorePromptStashAfterSubmit = false;\n\t\t\t\t\tawait this.showTreeSelector();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"login\" && !commandArgs) {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.showConfigurationMenu(\"providers\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"logout\" && !commandArgs) {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.showLogoutSelector();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"mcp\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.handleMcpCommand(commandArgs);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (slashCommand?.name === \"clear\") {\n\t\t\t\t\tif (commandArgs) {\n\t\t\t\t\t\tthis.editor.setText(text);\n\t\t\t\t\t\tthis.showError(\"Usage: /clear\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\t\tawait this.handleClearCommand();\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (slashCommand?.name === \"new\") {\n\t\t\t\t\tlet options: ReturnType<typeof parseNewSessionCommand>;\n\t\t\t\t\ttry {\n\t\t\t\t\t\toptions = parseNewSessionCommand(text.slice(4));\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tthis.editor.setText(text);\n\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.handleClearCommand(options);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"resume\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.handleResumeCommand(commandArgs);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"reload\" && !commandArgs) {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.handleReloadCommand();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"update\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tconst updateArgs = parseCommandArgs(commandArgs);\n\t\t\t\t\tif (\n\t\t\t\t\t\t!updateArgsIncludeSelf(updateArgs) &&\n\t\t\t\t\t\t(this.isAgentCompacting() || this.isAgentStreaming() || this.isBashRunning())\n\t\t\t\t\t) {\n\t\t\t\t\t\tthis.showWarning(\"Wait for the current work to finish before updating.\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tawait this.handleUpdateCommand(commandArgs);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"fullscreen\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tconst arg = commandArgs?.trim().toLowerCase();\n\t\t\t\t\tif (arg && arg !== \"on\" && arg !== \"off\") {\n\t\t\t\t\t\tthis.showError(\"Usage: /fullscreen [on|off]\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst enable = arg === \"on\" ? true : arg === \"off\" ? false : !this.fullscreenEnabled;\n\t\t\t\t\tthis.setFullscreenMode(enable);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (commandName === \"debug\" && !commandArgs) {\n\t\t\t\t\tawait this.handleDebugCommand();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (text === \"/arminsayshi\") {\n\t\t\t\t\tthis.handleArminSaysHi();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (text === \"/dementedelves\") {\n\t\t\t\t\tthis.handleDementedDelves();\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (text === \"/quit\") {\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.shutdown();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Handle bash command (! for normal, !! for excluded from context)\n\t\t\t\tif (text.startsWith(\"!\")) {\n\t\t\t\t\tconst isExcluded = text.startsWith(\"!!\");\n\t\t\t\t\tconst command = isExcluded ? text.slice(2).trim() : text.slice(1).trim();\n\t\t\t\t\tif (!command) {\n\t\t\t\t\t\t// Bare ! / !! is bash mode with nothing to run; don't send it as a prompt\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (this.isBashRunning()) {\n\t\t\t\t\t\tthis.showWarning(\n\t\t\t\t\t\t\t`A bash command is already running. Press ${keyText(\"app.clear\")} to cancel it first.`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t// A streaming side turn blocks bash just like it blocks follow-up\n\t\t\t\t\t// replies: overlapping pane turns would seed out of order.\n\t\t\t\t\tif (this.sideQuestionComponent && this.activeSideQuestionId) {\n\t\t\t\t\t\tthis.editor.setText(text);\n\t\t\t\t\t\tthis.showWarning(\"Wait for the current side question to finish or cancel it first.\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t// Inside a side conversation the command runs inside the pane (its\n\t\t\t\t\t// bash_start event mounts the usual BashExecutionComponent there),\n\t\t\t\t\t// stays out of the main-session context, and (for !, not !!) seeds\n\t\t\t\t\t// follow-up side questions.\n\t\t\t\t\tconst sideBash = this.sideQuestionComponent\n\t\t\t\t\t\t? { runId: randomUUID(), input: text, seedTranscript: !isExcluded }\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tif (sideBash) {\n\t\t\t\t\t\tthis.sideQuestionBash = sideBash;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.clearSideQuestion({ abort: true });\n\t\t\t\t\t}\n\t\t\t\t\tthis.editor.addToHistory?.(text);\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\t// Optimistic: bash_start only fires after extension dispatch, and the\n\t\t\t\t\t// clear key must already route to abortBash in that window.\n\t\t\t\t\tthis.patchConnectionState({ isBashRunning: true });\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait this.agentConnection.executeBash(command, {\n\t\t\t\t\t\t\texcludeFromContext: isExcluded || sideBash !== undefined,\n\t\t\t\t\t\t\t...(sideBash ? { transient: true, runId: sideBash.runId } : {}),\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t// Re-sync rather than assume idle: the rejection may mean another\n\t\t\t\t\t\t// client's bash run already holds the slot.\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst state = await this.agentConnection.getState();\n\t\t\t\t\t\t\tthis.patchConnectionState({ isBashRunning: state.isBashRunning });\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tthis.patchConnectionState({ isBashRunning: false });\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (this.sideQuestionBash === sideBash) {\n\t\t\t\t\t\t\tthis.sideQuestionBash = undefined;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (sideBash && this.sideQuestionBashDiscarded === sideBash.runId) {\n\t\t\t\t\t\t\t// The pane discarded this run, but it never started, so no\n\t\t\t\t\t\t\t// bash_end will arrive to consume the marker.\n\t\t\t\t\t\t\tthis.sideQuestionBashDiscarded = undefined;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// An open side-question pane captures replies as follow-up side\n\t\t\t\t// questions; ! bash routed above and slash commands were rejected\n\t\t\t\t// earlier with a notice. Esc returns to the main thread.\n\t\t\t\tif (this.sideQuestionComponent) {\n\t\t\t\t\t// A follow-up submitted mid-bash would seed the transcript ahead of\n\t\t\t\t\t// the output it reacts to; make it wait like a running side turn.\n\t\t\t\t\t// The editor cleared its buffer before onSubmit fired, so blocked\n\t\t\t\t\t// paths put the draft back rather than merely skip clearing it.\n\t\t\t\t\tif (this.sideQuestionBash) {\n\t\t\t\t\t\tthis.editor.setText(text);\n\t\t\t\t\t\tthis.showWarning(\"Wait for the running command to finish or cancel it first.\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (this.activeSideQuestionId) {\n\t\t\t\t\t\tthis.editor.setText(text);\n\t\t\t\t\t\tawait this.handleSideQuestion(text);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t// Side questions are text-only end to end; a reply with pasted\n\t\t\t\t\t// images gets an in-pane notice instead of silently dropping them.\n\t\t\t\t\tif (this.hasPastedImagesFor(text)) {\n\t\t\t\t\t\tthis.editor.setText(text);\n\t\t\t\t\t\tthis.sideQuestionComponent.addTurn({\n\t\t\t\t\t\t\tid: `side-notice-${randomUUID()}`,\n\t\t\t\t\t\t\tquestion: text,\n\t\t\t\t\t\t\tanswer: \"Images are not supported in side conversations. Press esc to return to the main thread.\",\n\t\t\t\t\t\t\tstatus: \"complete\",\n\t\t\t\t\t\t});\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.editor.addToHistory?.(text);\n\t\t\t\t\tawait this.handleSideQuestion(text);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tthis.clearSideQuestion({ abort: true });\n\t\t\t\tthis.flushPendingBashComponents();\n\t\t\t\tconst images = this.collectImagesFor(text);\n\t\t\t\tthis.editor.addToHistory?.(text);\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tconst promptStashAfterClear = this.promptStash;\n\t\t\t\tsubmissionOutcome = (await this.admitPendingStartupPrompts?.()) ?? \"admitted\";\n\t\t\t\t// Retention is not admission. Startup drafts were inserted synchronously\n\t\t\t\t// before the barrier settled, so append this blocked submission behind them\n\t\t\t\t// and never let it prompt or overtake them.\n\t\t\t\tif (submissionOutcome === \"retained\") {\n\t\t\t\t\tthis.retainSubmittedDraft(submittedDraft ?? { text }, submissionGeneration);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// The barrier also settles when the run lifecycle ends; a submit resumed\n\t\t\t\t// by teardown must neither prompt nor mutate the editor/durable stash.\n\t\t\t\tif (\n\t\t\t\t\tsubmissionOutcome === \"lifecycle-cancelled\" ||\n\t\t\t\t\tthis.isShuttingDown ||\n\t\t\t\t\tthis.agentsViewRequest ||\n\t\t\t\t\tthis.promptStashSessionId !== submissionSessionId\n\t\t\t\t) {\n\t\t\t\t\t// The editor is already torn down, but its shared session stash outlives\n\t\t\t\t\t// this view. Preserve the submitted draft in the stash of the session it\n\t\t\t\t\t// was typed for, without overwriting an explicit older stash.\n\t\t\t\t\tthis.retainSubmittedDraft(submittedDraft ?? { text }, submissionGeneration, submissionStashState);\n\t\t\t\t\tsubmissionOutcome = \"lifecycle-cancelled\";\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tawait this.agentConnection.prompt(text, {\n\t\t\t\t\t\tstreamingBehavior,\n\t\t\t\t\t\tqueueIfBusy: true,\n\t\t\t\t\t\timages,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Generation guards editor ownership, not draft durability: a stale\n\t\t\t\t\t// rejection must be retained rather than overwrite newer input or vanish.\n\t\t\t\t\tconst rejectedDraft = submittedDraft ?? { text };\n\t\t\t\t\tconst canRestore =\n\t\t\t\t\t\t!this.isShuttingDown &&\n\t\t\t\t\t\t!this.agentsViewRequest &&\n\t\t\t\t\t\tsubmissionGeneration === this.inputSubmissionGeneration &&\n\t\t\t\t\t\tthis.editor.getText().length === 0;\n\t\t\t\t\tif (canRestore) {\n\t\t\t\t\t\tconst canRestorePasteSnapshot =\n\t\t\t\t\t\t\trejectedDraft.pasteSnapshot === undefined || this.editor.restorePasteSnapshot !== undefined;\n\t\t\t\t\t\tthis.editor.setText(\n\t\t\t\t\t\t\tcanRestorePasteSnapshot ? rejectedDraft.text : (rejectedDraft.expandedText ?? rejectedDraft.text),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (rejectedDraft.pasteSnapshot && this.editor.restorePasteSnapshot) {\n\t\t\t\t\t\t\tthis.editor.restorePasteSnapshot(rejectedDraft.pasteSnapshot);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.latestEditorPromptStash = this.snapshotPromptStash(this.editor.getText());\n\t\t\t\t\t\tif (this.promptStash === promptStashAfterClear) this.promptStash = promptStashToRestore;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.retainSubmittedDraft(rejectedDraft, submissionGeneration, submissionStashState);\n\t\t\t\t\t}\n\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t} finally {\n\t\t\t\tif (this.isShuttingDown || this.agentsViewRequest) {\n\t\t\t\t\tsubmissionOutcome = \"lifecycle-cancelled\";\n\t\t\t\t}\n\t\t\t\tif (\n\t\t\t\t\tsubmissionOutcome === \"admitted\" &&\n\t\t\t\t\trestorePromptStashAfterSubmit &&\n\t\t\t\t\tpromptStashToRestore !== undefined &&\n\t\t\t\t\tsubmissionGeneration === this.inputSubmissionGeneration\n\t\t\t\t) {\n\t\t\t\t\tthis.restorePromptStashIfEditorEmpty(promptStashToRestore);\n\t\t\t\t}\n\t\t\t\tthis.inputSubmissionsPending--;\n\t\t\t\tif (this.inputSubmissionsPending === 0 && this.pendingPromptStashReleases.length > 0) {\n\t\t\t\t\tthis.completeDeferredPromptStashRelease();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\tprivate subscribeToAgent(): void {\n\t\tthis.unsubscribe = this.agentConnection.subscribe(async (event) => {\n\t\t\ttry {\n\t\t\t\tif (event.type === \"session_event\") {\n\t\t\t\t\t// Connection adapters dispatch without awaiting, so serialize events.\n\t\t\t\t\t// Replacement advances the generation before entering this queue, which\n\t\t\t\t\t// prevents already-queued source events from mutating the target UI.\n\t\t\t\t\tconst generation = this.sessionEventGeneration;\n\t\t\t\t\tconst run = this.sessionEventQueue.then(() =>\n\t\t\t\t\t\tgeneration === this.sessionEventGeneration ? this.handleEvent(event.event) : undefined,\n\t\t\t\t\t);\n\t\t\t\t\tthis.sessionEventQueue = run.catch(() => {});\n\t\t\t\t\tawait run;\n\t\t\t\t} else if (event.type === \"session_replaced\") {\n\t\t\t\t\tconst generation = ++this.sessionEventGeneration;\n\t\t\t\t\tconst run = this.sessionEventQueue.then(async () => {\n\t\t\t\t\t\tif (generation !== this.sessionEventGeneration) return;\n\t\t\t\t\t\tthis.resetSideQuestion();\n\t\t\t\t\t\tthis.resetExtensionUI();\n\t\t\t\t\t\tthis.applyConnectionStateSnapshot(event.state);\n\t\t\t\t\t\tthis.resetCurrentSessionRenderState();\n\t\t\t\t\t\tawait this.rebindCurrentSession();\n\t\t\t\t\t\tawait this.renderInitialMessages();\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t});\n\t\t\t\t\tthis.sessionEventQueue = run.catch(() => {});\n\t\t\t\t\tawait run;\n\t\t\t\t} else if (event.type === \"session_resynced\") {\n\t\t\t\t\tconst generation = this.sessionEventGeneration;\n\t\t\t\t\tconst run = this.sessionEventQueue.then(async () => {\n\t\t\t\t\t\tif (generation !== this.sessionEventGeneration) return false;\n\t\t\t\t\t\tawait this.refreshCommandCatalogForCurrentSession?.();\n\t\t\t\t\t\tif (generation !== this.sessionEventGeneration) return false;\n\t\t\t\t\t\tawait this.renderResyncedSession(event.snapshot);\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t});\n\t\t\t\t\tthis.sessionEventQueue = run.then(() => undefined).catch(() => {});\n\t\t\t\t\tif (await run) this.ui.requestRender();\n\t\t\t\t} else if (event.type === \"session_status\") {\n\t\t\t\t\tthis.sessionRecap = event.recap;\n\t\t\t\t\tthis.patchConnectionState({ recap: event.recap });\n\t\t\t\t\tthis.renderRecap();\n\t\t\t\t} else if (event.type === \"side_question_event\") {\n\t\t\t\t\tthis.handleSideQuestionEvent(event.event);\n\t\t\t\t} else if (event.type === \"extension_ui_request\") {\n\t\t\t\t\tawait this.handleConnectionExtensionUiRequest(event.request);\n\t\t\t\t} else if (event.type === \"connection_status\") {\n\t\t\t\t\tthis.showStatus(\n\t\t\t\t\t\tevent.status === \"connected\" ? \"Daemon reconnected\" : \"Daemon connection lost; reconnecting…\",\n\t\t\t\t\t\tevent.status === \"reconnecting\" ? \"warning\" : \"dim\",\n\t\t\t\t\t);\n\t\t\t\t\tif (event.status === \"connected\") {\n\t\t\t\t\t\tawait this.refreshHeartbeatCatalog();\n\t\t\t\t\t}\n\t\t\t\t} else if (event.type === \"heartbeats_changed\") {\n\t\t\t\t\tawait this.refreshHeartbeatCatalog();\n\t\t\t\t} else if (event.type === \"closed\") {\n\t\t\t\t\tthis.showError(event.error ?? \"Agent connection closed\");\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate async handleConnectionExtensionUiRequest(request: AgentConnectionExtensionUiRequest): Promise<void> {\n\t\tlet response: AgentConnectionExtensionUiResponse | undefined;\n\t\tconst expectsResponse = this.expectsConnectionExtensionUiResponse(request);\n\n\t\ttry {\n\t\t\tif (expectsResponse) {\n\t\t\t\tlet cancelLocal: (response: AgentConnectionExtensionUiResponse) => void = () => {};\n\t\t\t\tconst cancelled = new Promise<AgentConnectionExtensionUiResponse>((resolve) => {\n\t\t\t\t\tcancelLocal = resolve;\n\t\t\t\t});\n\t\t\t\tthis.activeConnectionExtensionUiRequests.set(request.id, {\n\t\t\t\t\tcancelLocal: () => cancelLocal({ cancelled: true }),\n\t\t\t\t});\n\t\t\t\tresponse = await Promise.race([this.resolveConnectionExtensionUiRequest(request), cancelled]);\n\t\t\t} else {\n\t\t\t\tresponse = await this.resolveConnectionExtensionUiRequest(request);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\tresponse = { cancelled: true };\n\t\t}\n\n\t\tif (response === undefined) {\n\t\t\tthis.activeConnectionExtensionUiRequests.delete(request.id);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.activeConnectionExtensionUiRequests.delete(request.id)) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tawait this.agentConnection.respondToExtensionUiRequest(request.id, response);\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate expectsConnectionExtensionUiResponse(request: AgentConnectionExtensionUiRequest): boolean {\n\t\treturn (\n\t\t\trequest.method === \"select\" ||\n\t\t\trequest.method === \"confirm\" ||\n\t\t\trequest.method === \"input\" ||\n\t\t\trequest.method === \"editor\"\n\t\t);\n\t}\n\n\tprivate cancelActiveConnectionExtensionUiRequests(): void {\n\t\tconst requestIds = [...this.activeConnectionExtensionUiRequests.keys()];\n\t\tfor (const requestId of requestIds) {\n\t\t\tconst activeRequest = this.activeConnectionExtensionUiRequests.get(requestId);\n\t\t\tif (!activeRequest) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.activeConnectionExtensionUiRequests.delete(requestId);\n\t\t\tactiveRequest.cancelLocal();\n\t\t\tvoid this.agentConnection.respondToExtensionUiRequest(requestId, { cancelled: true }).catch((error) => {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate async resolveConnectionExtensionUiRequest(\n\t\trequest: AgentConnectionExtensionUiRequest,\n\t): Promise<AgentConnectionExtensionUiResponse | undefined> {\n\t\tconst { payload } = request;\n\t\tswitch (request.method) {\n\t\t\tcase \"select\": {\n\t\t\t\tconst title = getPayloadString(payload, \"title\");\n\t\t\t\tconst options = getPayloadStringArray(payload, \"options\");\n\t\t\t\tif (!title || !options) {\n\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t}\n\t\t\t\tconst value = await this.showExtensionSelector(title, options, {\n\t\t\t\t\ttimeout: getPayloadNumber(payload, \"timeout\"),\n\t\t\t\t});\n\t\t\t\treturn value === undefined ? { cancelled: true } : { value };\n\t\t\t}\n\t\t\tcase \"confirm\": {\n\t\t\t\tconst title = getPayloadString(payload, \"title\");\n\t\t\t\tconst message = getPayloadString(payload, \"message\");\n\t\t\t\tif (!title || message === undefined) {\n\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t}\n\t\t\t\tconst confirmed = await this.showExtensionConfirm(title, message, {\n\t\t\t\t\ttimeout: getPayloadNumber(payload, \"timeout\"),\n\t\t\t\t});\n\t\t\t\treturn { confirmed };\n\t\t\t}\n\t\t\tcase \"input\": {\n\t\t\t\tconst title = getPayloadString(payload, \"title\");\n\t\t\t\tif (!title) {\n\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t}\n\t\t\t\tconst value = await this.showExtensionInput(title, getPayloadString(payload, \"placeholder\"), {\n\t\t\t\t\ttimeout: getPayloadNumber(payload, \"timeout\"),\n\t\t\t\t});\n\t\t\t\treturn value === undefined ? { cancelled: true } : { value };\n\t\t\t}\n\t\t\tcase \"editor\": {\n\t\t\t\tconst title = getPayloadString(payload, \"title\");\n\t\t\t\tif (!title) {\n\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t}\n\t\t\t\tconst value = await this.showExtensionEditor(title, getPayloadString(payload, \"prefill\"));\n\t\t\t\treturn value === undefined ? { cancelled: true } : { value };\n\t\t\t}\n\t\t\tcase \"notify\": {\n\t\t\t\tconst message = getPayloadString(payload, \"message\");\n\t\t\t\tif (message) {\n\t\t\t\t\tthis.showExtensionNotify(message, getPayloadNotifyType(payload, \"notifyType\"));\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcase \"setStatus\": {\n\t\t\t\tconst key = getPayloadString(payload, \"statusKey\");\n\t\t\t\tif (key) {\n\t\t\t\t\tthis.setExtensionStatus(key, getPayloadString(payload, \"statusText\"));\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcase \"setWorkingMessage\": {\n\t\t\t\tthis.workingMessage = getPayloadString(payload, \"message\");\n\t\t\t\tif (this.loadingAnimation) {\n\t\t\t\t\tthis.updateWorkingLoaderMessage();\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcase \"setWorkingVisible\": {\n\t\t\t\tconst visible = getPayloadBoolean(payload, \"visible\");\n\t\t\t\tif (visible !== undefined) {\n\t\t\t\t\tthis.setWorkingVisible(visible);\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcase \"setWorkingIndicator\": {\n\t\t\t\tthis.setWorkingIndicator(getPayloadWorkingIndicatorOptions(payload, \"options\"));\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcase \"setHiddenThinkingLabel\": {\n\t\t\t\tthis.setHiddenThinkingLabel(getPayloadString(payload, \"label\"));\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcase \"setWidget\": {\n\t\t\t\tconst key = getPayloadString(payload, \"widgetKey\");\n\t\t\t\tif (key) {\n\t\t\t\t\tconst placement = getPayloadWidgetPlacement(payload, \"widgetPlacement\");\n\t\t\t\t\tthis.setExtensionWidget(\n\t\t\t\t\t\tkey,\n\t\t\t\t\t\tgetPayloadStringArray(payload, \"widgetLines\"),\n\t\t\t\t\t\tplacement ? { placement } : undefined,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcase \"setTitle\": {\n\t\t\t\tconst title = getPayloadString(payload, \"title\");\n\t\t\t\tif (title) {\n\t\t\t\t\tthis.ui.terminal.setTitle(title);\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcase \"setEditorText\": {\n\t\t\t\tconst text = getPayloadString(payload, \"text\");\n\t\t\t\tif (text !== undefined) {\n\t\t\t\t\tthis.editor.setText(text);\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\tthis.showStatus(`Unsupported extension UI request: ${request.method}`);\n\t\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async handleEvent(event: AgentConnectionSessionEvent): Promise<void> {\n\t\tif (!this.isInitialized) {\n\t\t\tawait this.init();\n\t\t}\n\n\t\tthis.footer.invalidate();\n\t\tthis.updateConnectionStateFromEvent(event);\n\t\t// A new user message resets the activity tracker to 0, so the in-flight baseline must\n\t\t// reset with it. (agent_start on auto-retry does not reset the tracker.)\n\t\tif (event.type === \"message_start\") {\n\t\t\tthis.prepareFeatureHintRun(event.message);\n\t\t}\n\t\tif (event.type === \"message_start\" && (event.message.role === \"user\" || isAgentSessionMessage(event.message))) {\n\t\t\tthis.contextUsageTokenBaseline = 0;\n\t\t\tthis.setSessionHasMessages(true);\n\t\t\tthis.clearShortcutGuide();\n\t\t\tthis.agentRunFileChanges.clear();\n\t\t\tthis.renderRecap();\n\t\t}\n\t\tthis.activityTracker.handleEvent(event);\n\t\tthis.updateWorkingLoaderMessage();\n\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\t\tthis.featureHintRunPending = this.getRetryAttempt() === 0;\n\t\t\t\tthis.resetPendingToolState();\n\t\t\t\tthis.renderRecap();\n\t\t\t\tif (this.settingsManager.getShowTerminalProgress()) {\n\t\t\t\t\tthis.ui.terminal.setProgress(true);\n\t\t\t\t}\n\t\t\t\tif (this.retryCountdown) {\n\t\t\t\t\tthis.retryCountdown.dispose();\n\t\t\t\t\tthis.retryCountdown = undefined;\n\t\t\t\t}\n\t\t\t\tif (this.retryLoader) {\n\t\t\t\t\tthis.retryLoader.stop();\n\t\t\t\t\tthis.retryLoader = undefined;\n\t\t\t\t}\n\t\t\t\tthis.stopWorkingLoader();\n\t\t\t\tif (this.workingVisible) {\n\t\t\t\t\tthis.startWorkingLoader();\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\n\t\t\tcase \"session_action_update\": {\n\t\t\t\tthis.replaceConnectionQueue({\n\t\t\t\t\tsteering: [...event.actions.steering],\n\t\t\t\t\tfollowUp: [...event.actions.followUps],\n\t\t\t\t});\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"session_info_changed\":\n\t\t\t\tthis.updateTerminalTitle();\n\t\t\t\tthis.footer.invalidate();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\n\t\t\tcase \"thinking_level_changed\":\n\t\t\t\tthis.footer.invalidate();\n\t\t\t\tthis.subagentSummaryLine.invalidate();\n\t\t\t\tthis.updateEditorBorderColor();\n\t\t\t\tbreak;\n\n\t\t\tcase \"service_tier_changed\":\n\t\t\t\tthis.footer.invalidate();\n\t\t\t\tthis.subagentSummaryLine.invalidate();\n\t\t\t\tbreak;\n\n\t\t\tcase \"bash_start\": {\n\t\t\t\tif (this.sideQuestionBashDiscarded !== undefined) {\n\t\t\t\t\tif (event.runId === this.sideQuestionBashDiscarded) {\n\t\t\t\t\t\t// The discarded side run now owns the bash slot. Abort only\n\t\t\t\t\t\t// after matching its identity so a foreign run is never killed.\n\t\t\t\t\t\tvoid this.agentConnection.abortBash().catch(() => undefined);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\t// A different run claimed the slot, so the discarded run lost the\n\t\t\t\t\t// race and can never start (its execute_bash will reject); render\n\t\t\t\t\t// this run normally instead of swallowing it.\n\t\t\t\t\tthis.sideQuestionBashDiscarded = undefined;\n\t\t\t\t}\n\t\t\t\tconst ownSideBash = this.sideQuestionBash !== undefined && event.runId === this.sideQuestionBash.runId;\n\t\t\t\tif (event.transient && !ownSideBash) {\n\t\t\t\t\t// Another client's side-conversation run: it renders only in that\n\t\t\t\t\t// client's pane, never in this window's chat.\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tconst component = new BashExecutionComponent(event.command, this.ui, event.excludeFromContext, {\n\t\t\t\t\tsuppressLeadingSpace: this.chatContainer.children.at(-1) instanceof AgentMessageComponent,\n\t\t\t\t});\n\t\t\t\tif (ownSideBash && this.sideQuestionComponent) {\n\t\t\t\t\t// Same component as the main thread, mounted inside the pane.\n\t\t\t\t\tthis.sideQuestionComponent.addBash(component);\n\t\t\t\t\tthis.sideQuestionBashComponent = component;\n\t\t\t\t} else if (this.isAgentStreaming()) {\n\t\t\t\t\tthis.pendingMessagesContainer.addChild(component);\n\t\t\t\t\tthis.pendingBashComponents.push(component);\n\t\t\t\t} else {\n\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t}\n\t\t\t\tthis.activeBashComponent = component;\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"bash_output\":\n\t\t\t\tif (this.sideQuestionBashDiscarded !== undefined) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (this.activeBashComponent) {\n\t\t\t\t\tthis.activeBashComponent.appendOutput(event.chunk);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"bash_end\": {\n\t\t\t\tif (this.sideQuestionBashDiscarded !== undefined) {\n\t\t\t\t\t// Only the discarded run's own end consumes the marker; bash_start\n\t\t\t\t\t// already cleared it for any other run that claimed the slot.\n\t\t\t\t\tthis.sideQuestionBashDiscarded = undefined;\n\t\t\t\t\tthis.activeBashComponent = undefined;\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tconst component = this.activeBashComponent;\n\t\t\t\tif (component) {\n\t\t\t\t\tif (event.errorMessage) {\n\t\t\t\t\t\tcomponent.setFailed(event.errorMessage);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcomponent.setComplete(\n\t\t\t\t\t\t\tevent.exitCode,\n\t\t\t\t\t\t\tevent.cancelled,\n\t\t\t\t\t\t\tevent.truncated ? ({ truncated: true } as TruncationResult) : undefined,\n\t\t\t\t\t\t\tevent.fullOutputPath,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tthis.activeBashComponent = undefined;\n\t\t\t\t} else if (event.errorMessage && !event.transient) {\n\t\t\t\t\t// Transient failures surface in the owning client's pane, not here.\n\t\t\t\t\tthis.showError(`Bash command failed: ${event.errorMessage}`);\n\t\t\t\t}\n\t\t\t\t// Seed the side transcript only when our own pane-mounted run ended.\n\t\t\t\tif (component !== undefined && component === this.sideQuestionBashComponent) {\n\t\t\t\t\tthis.sideQuestionBashComponent = undefined;\n\t\t\t\t\tthis.finishSideQuestionBash(event, component.getOutput());\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"message_start\":\n\t\t\t\t// The run's first starter anchors the elapsed display; mid-turn steering must not restart it.\n\t\t\t\tif (this.turnStartedAt === undefined && startsAgentRun(event.message)) {\n\t\t\t\t\tthis.turnStartedAt = event.message.timestamp;\n\t\t\t\t\tif (this.workingStartedAt !== undefined) {\n\t\t\t\t\t\tthis.workingStartedAt = event.message.timestamp;\n\t\t\t\t\t\tthis.updateWorkingLoaderMessage();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (event.message.role === \"custom\") {\n\t\t\t\t\tif (isSessionSlashCommandMessage(event.message) && event.message.details.command.name === \"refine\") {\n\t\t\t\t\t\tthis.startRefineLoader();\n\t\t\t\t\t}\n\t\t\t\t\t// The /refine result row is the user refine's settle edge; refine_complete\n\t\t\t\t\t// alone can belong to an agent/auto refinement the queued /refine waited on.\n\t\t\t\t\tif (\n\t\t\t\t\t\tisSessionSlashCommandResultMessage(event.message) &&\n\t\t\t\t\t\tevent.message.details.command.name === \"refine\"\n\t\t\t\t\t) {\n\t\t\t\t\t\tthis.stopRefineLoader();\n\t\t\t\t\t}\n\t\t\t\t\tthis.addMessageToChat(event.message);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t} else if (event.message.role === \"user\") {\n\t\t\t\t\tthis.addMessageToChat(event.message);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t} else if (event.message.role === \"assistant\") {\n\t\t\t\t\tthis.startAssistantStreamingMessage(event.message);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"message_update\":\n\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\tthis.streamingMessage = event.message;\n\t\t\t\t\tthis.ensureAssistantStreamingComponent(event.message).updateContent(this.streamingMessage, true);\n\n\t\t\t\t\tfor (const content of this.streamingMessage.content) {\n\t\t\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\t\t\tawait this.getOrCreatePendingToolComponent(content);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"message_end\":\n\t\t\t\tif (event.message.role === \"user\") break;\n\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\tthis.streamingMessage = event.message;\n\t\t\t\t\tlet errorMessage: string | undefined;\n\t\t\t\t\tif (this.streamingMessage.stopReason === \"aborted\") {\n\t\t\t\t\t\tconst retryAttempt = this.getRetryAttempt();\n\t\t\t\t\t\tconst elapsedSuffix =\n\t\t\t\t\t\t\tthis.workingStartedAt === undefined\n\t\t\t\t\t\t\t\t? \"\"\n\t\t\t\t\t\t\t\t: ` · ${this.formatWorkingElapsed(Date.now() - this.workingStartedAt)}`;\n\t\t\t\t\t\terrorMessage =\n\t\t\t\t\t\t\tretryAttempt > 0\n\t\t\t\t\t\t\t\t? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? \"s\" : \"\"}${elapsedSuffix}`\n\t\t\t\t\t\t\t\t: `Operation aborted${elapsedSuffix}`;\n\t\t\t\t\t\tthis.streamingMessage.errorMessage = errorMessage;\n\t\t\t\t\t}\n\t\t\t\t\tthis.ensureAssistantStreamingComponent(event.message).updateContent(this.streamingMessage, false);\n\n\t\t\t\t\tif (this.streamingMessage.stopReason === \"aborted\" || this.streamingMessage.stopReason === \"error\") {\n\t\t\t\t\t\tif (!errorMessage) {\n\t\t\t\t\t\t\terrorMessage = this.streamingMessage.errorMessage || \"Error\";\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfor (const [, component] of this.pendingTools.entries()) {\n\t\t\t\t\t\t\tcomponent.updateResult({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: errorMessage }],\n\t\t\t\t\t\t\t\tisError: true,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.resetPendingToolState();\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Args are now complete - trigger diff computation for edit tools\n\t\t\t\t\t\tfor (const [, component] of this.pendingTools.entries()) {\n\t\t\t\t\t\t\tcomponent.setArgsComplete();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tthis.streamingComponent = undefined;\n\t\t\t\t\tthis.streamingMessage = undefined;\n\t\t\t\t\tthis.footer.invalidate();\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool_execution_start\": {\n\t\t\t\tthis.startedToolCalls.add(event.toolCallId);\n\t\t\t\tlet component = this.pendingTools.get(event.toolCallId);\n\t\t\t\tif (!component) {\n\t\t\t\t\tcomponent = await this.getOrCreatePendingToolComponent({\n\t\t\t\t\t\tid: event.toolCallId,\n\t\t\t\t\t\tname: event.toolName,\n\t\t\t\t\t\targuments: event.args,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (component) {\n\t\t\t\t\tcomponent.markExecutionStarted();\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_update\": {\n\t\t\t\tconst component = this.pendingTools.get(event.toolCallId);\n\t\t\t\tif (component) {\n\t\t\t\t\tcomponent.updateResult({ ...event.partialResult, isError: false }, true);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_end\": {\n\t\t\t\tconst component = this.pendingTools.get(event.toolCallId);\n\t\t\t\tif (component) {\n\t\t\t\t\tcomponent.updateResult({ ...event.result, isError: event.isError });\n\t\t\t\t\tthis.pendingTools.delete(event.toolCallId);\n\t\t\t\t\tthis.startedToolCalls.delete(event.toolCallId);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"ipython_sent_agent_message\": {\n\t\t\t\tconst messages = this.lateIpythonSentAgentMessages.get(event.toolCallId) ?? [];\n\t\t\t\tif (!messages.some((message) => message.id === event.message.id)) {\n\t\t\t\t\tmessages.push(event.message);\n\t\t\t\t\tthis.lateIpythonSentAgentMessages.set(event.toolCallId, messages);\n\t\t\t\t}\n\t\t\t\tthis.ipythonToolComponents.get(event.toolCallId)?.appendSentAgentMessage(event.message);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"turn_end\":\n\t\t\t\tmergeTurnFileChanges(this.agentRunFileChanges, event.message, event.toolResults, this.getCurrentCwd());\n\t\t\t\tbreak;\n\n\t\t\tcase \"agent_end\":\n\t\t\t\tif (this.settingsManager.getShowTerminalProgress()) {\n\t\t\t\t\tthis.ui.terminal.setProgress(false);\n\t\t\t\t}\n\t\t\t\tthis.turnStartedAt = undefined;\n\t\t\t\t// Drops the loader; background subagents are shown by the tree, not the loader.\n\t\t\t\tthis.syncWorkingLoader();\n\t\t\t\tif (this.streamingComponent) {\n\t\t\t\t\tif (this.streamingMessage) {\n\t\t\t\t\t\tthis.streamingComponent.updateContent(this.streamingMessage, false);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.chatContainer.removeChild(this.streamingComponent);\n\t\t\t\t\t}\n\t\t\t\t\tthis.streamingComponent = undefined;\n\t\t\t\t\tthis.streamingMessage = undefined;\n\t\t\t\t}\n\t\t\t\tthis.flushPendingBashComponents();\n\t\t\t\tthis.resetPendingToolState();\n\t\t\t\tthis.renderRecap();\n\n\t\t\t\tthis.applyOptimisticContextUsage();\n\t\t\t\t// Auto-compaction can start server-side while this event is being handled.\n\t\t\t\t// Do not hold its start event behind a stats RPC; stale refreshes are discarded.\n\t\t\t\tvoid this.refreshConnectionContextUsage();\n\n\t\t\t\tawait this.checkShutdownRequested();\n\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\n\t\t\tcase \"compaction_start\": {\n\t\t\t\tthis.startCompactionLoader(event.reason, event.customInstructions);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"compaction_end\": {\n\t\t\t\tif (this.settingsManager.getShowTerminalProgress()) {\n\t\t\t\t\tthis.ui.terminal.setProgress(false);\n\t\t\t\t}\n\t\t\t\tif (this.autoCompactionLoader) {\n\t\t\t\t\tthis.autoCompactionLoader.stop();\n\t\t\t\t\tthis.autoCompactionLoader = undefined;\n\t\t\t\t\tthis.statusContainer.clear();\n\t\t\t\t}\n\t\t\t\t// Restore the working loader if streaming/subagents still warrant it.\n\t\t\t\tthis.syncWorkingLoader();\n\t\t\t\tif (event.aborted) {\n\t\t\t\t\tif (event.reason === \"manual\") this.showError(\"Compaction cancelled\");\n\t\t\t\t} else if (event.result) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait this.rebuildChatFromMessages();\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\t\tthis.showError(`Compaction succeeded, but the transcript could not be refreshed: ${message}`);\n\t\t\t\t\t}\n\t\t\t\t\tawait this.refreshConnectionContextUsage();\n\t\t\t\t\tthis.footer.invalidate();\n\t\t\t\t} else if (event.errorMessage && event.reason === \"manual\") {\n\t\t\t\t\tif (event.errorSeverity === \"warning\") this.showWarning(event.errorMessage);\n\t\t\t\t\telse this.showError(event.errorMessage);\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"auto_retry_start\": {\n\t\t\t\tthis.stopWorkingLoader();\n\t\t\t\tthis.statusContainer.clear();\n\t\t\t\tthis.retryCountdown?.dispose();\n\t\t\t\tconst retryMessage = (seconds: number) =>\n\t\t\t\t\t`Retrying (${event.attempt}/${event.maxAttempts}) in ${seconds}s... (${keyText(\"app.clear\")} to cancel)`;\n\t\t\t\tthis.retryLoader = new Loader(\n\t\t\t\t\tthis.ui,\n\t\t\t\t\t(spinner) => theme.fg(\"muted\", spinner),\n\t\t\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\t\t\tretryMessage(Math.ceil(event.delayMs / 1000)),\n\t\t\t\t);\n\t\t\t\tthis.retryCountdown = new CountdownTimer(\n\t\t\t\t\tevent.delayMs,\n\t\t\t\t\tthis.ui,\n\t\t\t\t\t(seconds) => {\n\t\t\t\t\t\tthis.retryLoader?.setMessage(retryMessage(seconds));\n\t\t\t\t\t},\n\t\t\t\t\t() => {\n\t\t\t\t\t\tthis.retryCountdown = undefined;\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t\tthis.statusContainer.addChild(this.retryLoader);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"auto_retry_end\": {\n\t\t\t\tif (this.retryCountdown) {\n\t\t\t\t\tthis.retryCountdown.dispose();\n\t\t\t\t\tthis.retryCountdown = undefined;\n\t\t\t\t}\n\t\t\t\tif (this.retryLoader) {\n\t\t\t\t\tthis.retryLoader.stop();\n\t\t\t\t\tthis.retryLoader = undefined;\n\t\t\t\t\tthis.statusContainer.clear();\n\t\t\t\t}\n\t\t\t\t// Restore the working loader if streaming/subagents still warrant it.\n\t\t\t\tthis.syncWorkingLoader();\n\t\t\t\t// Show error only on final failure (success shows normal response)\n\t\t\t\tif (!event.success) {\n\t\t\t\t\tthis.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || \"Unknown error\"}`);\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"auth_stale\": {\n\t\t\t\tthis.applyAuthStaleEvent(event);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"rlm_child_update\":\n\t\t\t\tthis.updateSubagentSummary(event.child);\n\t\t\t\tbreak;\n\n\t\t\tcase \"goal_update\":\n\t\t\t\tthis.handleGoalUpdate(event.goal);\n\t\t\t\tbreak;\n\n\t\t\tcase \"refine_failed\":\n\t\t\t\t// This event has no request identity; the matching command result settles its loader.\n\t\t\t\tthis.showError(`Refinement failed: ${event.error}`);\n\t\t\t\tbreak;\n\n\t\t\tcase \"refine_complete\":\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate startAssistantStreamingMessage(message: AssistantMessage): void {\n\t\tthis.streamingComponent = new AssistantMessageComponent(\n\t\t\tundefined,\n\t\t\tthis.hideThinkingBlock,\n\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t\tthis.hiddenThinkingLabel,\n\t\t\t{\n\t\t\t\texpanded: this.toolOutputExpanded,\n\t\t\t\tprecededByToolActivity:\n\t\t\t\t\tthis.chatContainer.children.at(-1) instanceof ToolExecutionComponent ||\n\t\t\t\t\tthis.chatContainer.children.at(-1) instanceof AgentMessageComponent,\n\t\t\t\tmermaidTransform: this.mermaidMarkdownTransform,\n\t\t\t},\n\t\t);\n\t\tthis.streamingMessage = message;\n\t\tthis.chatContainer.addChild(this.streamingComponent);\n\t\tthis.streamingComponent.updateContent(this.streamingMessage, true);\n\t}\n\n\tprivate ensureAssistantStreamingComponent(message: AssistantMessage): AssistantMessageComponent {\n\t\tlet component = this.streamingComponent;\n\t\tif (!component) {\n\t\t\tthis.startAssistantStreamingMessage(message);\n\t\t\tcomponent = this.streamingComponent;\n\t\t}\n\t\tif (!component) {\n\t\t\tthrow new Error(\"Failed to create assistant streaming component\");\n\t\t}\n\t\treturn component;\n\t}\n\n\tprivate handleGoalUpdate(goal: GoalState): void {\n\t\tthis.syncGoalTray(goal);\n\t\tif (this.shouldAnnounceGoalUpdate(goal)) {\n\t\t\tthis.showStatus(this.formatGoalStatus(goal));\n\t\t} else {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate syncGoalTray(goal: GoalState): void {\n\t\tthis.subagentSummaryLine.invalidate();\n\t\tthis.updateGoalTrayTimer(goal);\n\t}\n\n\tprivate updateGoalTrayTimer(goal: GoalState): void {\n\t\tif (goal.status === \"active\") {\n\t\t\tif (!this.goalTrayTimer) {\n\t\t\t\tthis.goalTrayTimer = setInterval(() => {\n\t\t\t\t\tthis.subagentSummaryLine.invalidate();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}, 1000);\n\t\t\t\tthis.goalTrayTimer.unref?.();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tthis.stopGoalTrayTimer();\n\t}\n\n\tprivate stopGoalTrayTimer(): void {\n\t\tif (!this.goalTrayTimer) {\n\t\t\treturn;\n\t\t}\n\t\tclearInterval(this.goalTrayTimer);\n\t\tthis.goalTrayTimer = undefined;\n\t}\n\n\tprivate setGoalAnnouncementBaseline(goal: GoalState): void {\n\t\tthis.lastGoalAnnouncement = this.goalAnnouncementSnapshot(goal);\n\t}\n\n\tprivate goalAnnouncementSnapshot(goal: GoalState): GoalAnnouncementSnapshot {\n\t\treturn {\n\t\t\tgoalId: goal.goalId,\n\t\t\tstatus: goal.status,\n\t\t\tobjective: goal.objective,\n\t\t\tlastReason: goal.lastReason,\n\t\t\tlastError: goal.lastError,\n\t\t};\n\t}\n\n\tprivate shouldAnnounceGoalUpdate(goal: GoalState): boolean {\n\t\tconst previous = this.lastGoalAnnouncement;\n\t\tconst next = this.goalAnnouncementSnapshot(goal);\n\t\tthis.lastGoalAnnouncement = next;\n\t\tif (!previous) {\n\t\t\treturn goal.status !== \"idle\";\n\t\t}\n\t\tif (previous.status !== next.status) {\n\t\t\treturn true;\n\t\t}\n\t\tif (previous.goalId !== next.goalId) {\n\t\t\treturn goal.status !== \"idle\";\n\t\t}\n\t\tswitch (goal.status) {\n\t\t\tcase \"active\":\n\t\t\t\treturn false;\n\t\t\tcase \"paused\":\n\t\t\tcase \"budget_limited\":\n\t\t\tcase \"complete\":\n\t\t\t\treturn previous.lastReason !== next.lastReason;\n\t\t\tcase \"error\":\n\t\t\t\treturn previous.lastError !== next.lastError;\n\t\t\tcase \"idle\":\n\t\t\t\treturn false;\n\t\t\tdefault: {\n\t\t\t\tconst _exhaustive: never = goal.status;\n\t\t\t\treturn _exhaustive;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate formatGoalStatus(goal: GoalState): string {\n\t\tconst usage = formatGoalUsage(goal);\n\t\tconst usageText = usage ? ` (${usage})` : \"\";\n\t\tswitch (goal.status) {\n\t\t\tcase \"idle\":\n\t\t\t\treturn \"No active goal\";\n\t\t\tcase \"active\":\n\t\t\t\treturn goal.objective\n\t\t\t\t\t? `Goal${this.formatGoalDetailSuffix(goal.objective, visibleWidth(\"Goal\"))}`\n\t\t\t\t\t: \"Pursuing goal\";\n\t\t\tcase \"paused\":\n\t\t\t\treturn goal.lastReason\n\t\t\t\t\t? `Goal paused${this.formatGoalDetailSuffix(goal.lastReason, visibleWidth(\"Goal paused\"))}`\n\t\t\t\t\t: \"Goal paused (/goal resume)\";\n\t\t\tcase \"budget_limited\":\n\t\t\t\tif (goal.lastReason) {\n\t\t\t\t\tconst prefix = `Goal budget limited${usageText}`;\n\t\t\t\t\treturn prefix + this.formatGoalDetailSuffix(goal.lastReason, visibleWidth(prefix));\n\t\t\t\t}\n\t\t\t\treturn `Goal budget limited${usageText}`;\n\t\t\tcase \"complete\":\n\t\t\t\treturn goal.lastReason\n\t\t\t\t\t? `Goal complete${this.formatGoalDetailSuffix(goal.lastReason, visibleWidth(\"Goal complete\"))}`\n\t\t\t\t\t: \"Goal complete\";\n\t\t\tcase \"error\":\n\t\t\t\treturn goal.lastError\n\t\t\t\t\t? `Goal error${this.formatGoalDetailSuffix(goal.lastError, visibleWidth(\"Goal error\"))}`\n\t\t\t\t\t: \"Goal error\";\n\t\t\tdefault: {\n\t\t\t\tconst _exhaustive: never = goal.status;\n\t\t\t\treturn _exhaustive;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate formatGoalDetailSuffix(value: string | undefined, prefixWidth: number): string {\n\t\tconst detail = value?.replace(/\\s+/g, \" \").trim();\n\t\tif (!detail) {\n\t\t\treturn \"\";\n\t\t}\n\t\tconst availableWidth = Math.min(120, Math.max(1, this.ui.terminal.columns - prefixWidth - 2));\n\t\tif (availableWidth < 8) {\n\t\t\treturn \"\";\n\t\t}\n\t\treturn `: ${truncateToWidth(detail, availableWidth)}`;\n\t}\n\n\tprivate seedSubagentSummary(children: readonly AgentConnectionRlmChildAgentSnapshot[] | undefined): void {\n\t\tfor (const child of children ?? []) {\n\t\t\t// Live updates can arrive before the initial snapshot; do not replace them\n\t\t\t// with the snapshot's older state.\n\t\t\tif (!this.subagentSnapshots.has(child.id) && child.status !== \"cancelled\") {\n\t\t\t\tthis.subagentSnapshots.set(child.id, child);\n\t\t\t}\n\t\t}\n\t\tthis.refreshSubagentSummary();\n\t}\n\n\tprivate replaceSubagentSummary(children: readonly AgentConnectionRlmChildAgentSnapshot[] | undefined): void {\n\t\tconst next = new Map<string, AgentConnectionRlmChildAgentSnapshot>();\n\t\tfor (const child of children ?? []) {\n\t\t\tif (child.status === \"cancelled\") continue;\n\t\t\tconst previous = this.subagentSnapshots.get(child.id);\n\t\t\tnext.set(child.id, previous ? mergeSubagentSnapshot(previous, child) : child);\n\t\t}\n\t\tthis.subagentSnapshots = next;\n\t\tthis.refreshSubagentSummary();\n\t}\n\n\tprivate updateSubagentSummary(child: AgentConnectionRlmChildAgentSnapshot): void {\n\t\tif (child.status === \"cancelled\") {\n\t\t\tthis.removeSubagentSnapshot(child.id);\n\t\t} else {\n\t\t\tconst previous = this.subagentSnapshots.get(child.id);\n\t\t\tthis.subagentSnapshots.set(child.id, previous ? mergeSubagentSnapshot(previous, child) : child);\n\t\t}\n\t\tthis.refreshSubagentSummary();\n\t}\n\n\tprivate refreshSubagentSummary(): void {\n\t\tthis.updateScopedHeartbeats();\n\t\tthis.updateSubagentSummaryLine();\n\t\tthis.updateWorkingPulse();\n\t\tthis.syncWorkingLoader();\n\t\tthis.updateWorkingLoaderMessage();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate updateSubagentSummaryLine(): void {\n\t\tconst activeHeartbeatSessionIds = new Set(\n\t\t\tthis.heartbeatCatalog\n\t\t\t\t.filter((heartbeat) => heartbeat.job.status === \"active\")\n\t\t\t\t.map((heartbeat) => heartbeat.job.activeSessionId),\n\t\t);\n\t\tthis.subagentSummaryLine.setSubagentCounts(\n\t\t\tcountDirectSubagentStatuses(this.subagentSnapshots.values(), this.rlmNodeId, activeHeartbeatSessionIds),\n\t\t);\n\t\tif (!this.subagentSummaryLine.isSelectable() && this.subagentSummaryLine.focused) this.focusEditor();\n\t}\n\n\tprivate removeSubagentSnapshot(id: string): void {\n\t\tthis.subagentSnapshots.delete(id);\n\t\tfor (const child of [...this.subagentSnapshots.values()]) {\n\t\t\tif (child.parentId === id) this.removeSubagentSnapshot(child.id);\n\t\t}\n\t}\n\n\tprivate resetSubagentSummary(): void {\n\t\tthis.subagentSnapshots.clear();\n\t\tthis.rlmNodeId = undefined;\n\t\tthis.updateSubagentSummaryLine();\n\t\tthis.updateScopedHeartbeats();\n\t\t// Clearing snapshots can drop the last running subagent; reconcile the\n\t\t// pulse and loader so neither lingers when nothing is in flight.\n\t\tthis.updateWorkingPulse();\n\t\tthis.syncWorkingLoader();\n\t}\n\n\tprivate focusEditor(): void {\n\t\tthis.ui.setFocus(this.editor);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate focusSubagentSummary(): boolean {\n\t\tif (!this.subagentSummaryLine.isSelectable() || this.getTrayOverrideLabel()) return false;\n\t\tthis.ui.setFocus(this.subagentSummaryLine);\n\t\tthis.ui.requestRender();\n\t\treturn true;\n\t}\n\n\tprivate async openScopedAgentsView(): Promise<void> {\n\t\tif (!this.options.returnToAgentsView) {\n\t\t\tthis.focusEditor();\n\t\t\tthis.showStatus(\"The agents view needs the daemon; start without --no-daemon to browse sessions\");\n\t\t\treturn;\n\t\t}\n\t\tawait this.returnToAgentsView(\"scoped_agents_view\");\n\t}\n\n\tprivate handleSubagentSummaryChatAction(data: string): void {\n\t\tif (this.keybindings.matches(data, \"app.tools.expand\")) {\n\t\t\tthis.toggleToolOutputExpansion();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"app.messages.expand\")) {\n\t\t\tthis.toggleAgentMessageExpansion();\n\t\t\treturn;\n\t\t}\n\t\t// A raw \"\\n\" is a newline for the editor, not ctrl+j.\n\t\tif (data !== \"\\n\" && this.keybindings.matches(data, \"app.edits.expand\")) {\n\t\t\tthis.toggleEditDiffExpansion();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"app.thinking.toggle\")) {\n\t\t\tthis.toggleThinkingBlockVisibility();\n\t\t\treturn;\n\t\t}\n\t\tthis.focusEditor();\n\t\tthis.editor.handleInput(data);\n\t}\n\n\tprivate getTrayOverrideLabel(): string | undefined {\n\t\tif (this.isCtrlCExitHintVisible()) {\n\t\t\tconst clearKey = keyText(\"app.clear\");\n\t\t\treturn clearKey ? `Press ${clearKey} again to exit` : \"Press again to exit\";\n\t\t}\n\t\tconst text = this.editor.getExpandedText?.() ?? this.editor.getText();\n\t\tif (!this.isAgentStreaming() || !text.trim()) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn `${keyText(\"app.message.followUp\")} to queue message`;\n\t}\n\n\tprivate getTrayLocationLabel(): string | undefined {\n\t\tconst modelLabel = this.getModelTrayLabel();\n\t\tconst hasChildren = this.options.sessionHasChildren === true || (this.subagentSnapshots?.size ?? 0) > 0;\n\t\tconst depthLabel = formatAgentDepthLabel(this.options.sessionDepth, hasChildren);\n\t\tconst shortcutsHint = this.getShortcutsTrayHint();\n\t\tconst agentsHint = this.getAgentsViewTrayHint();\n\t\treturn [agentsHint, depthLabel, modelLabel, shortcutsHint]\n\t\t\t.filter((label): label is string => label !== undefined)\n\t\t\t.join(\" \");\n\t}\n\n\tprivate getShortcutsTrayHint(): string | undefined {\n\t\tif (!this.isNewChat() || this.editor.getText().length > 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn keyText(\"app.shortcuts\") ? keyHint(\"app.shortcuts\", \"for shortcuts\") : \"/hotkeys for shortcuts\";\n\t}\n\n\tprivate isNewChat(): boolean {\n\t\treturn !this.sessionHasMessages;\n\t}\n\n\tprivate setSessionHasMessages(hasMessages: boolean): void {\n\t\tif (this.sessionHasMessages === hasMessages) {\n\t\t\treturn;\n\t\t}\n\t\tthis.sessionHasMessages = hasMessages;\n\t\tthis.builtInHeader?.invalidate();\n\t\tthis.subagentSummaryLine.invalidate();\n\t}\n\n\tprivate getModelTrayLabel(): string {\n\t\tconst model = this.getCurrentModel();\n\t\tif (!model) {\n\t\t\treturn \"—\";\n\t\t}\n\t\tconst parts = [model.name];\n\t\tif (model.reasoning) {\n\t\t\tconst level = this.connectionState?.thinkingLevel ?? \"off\";\n\t\t\tif (level !== \"off\") {\n\t\t\t\tparts.push(level);\n\t\t\t}\n\t\t}\n\t\tif (this.connectionState?.serviceTier === \"priority\") {\n\t\t\tparts.push(\"fast\");\n\t\t}\n\t\treturn parts.join(\" • \");\n\t}\n\n\tprivate getAgentsViewTrayHint(): string | undefined {\n\t\tif (!this.options.returnToAgentsView) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn keyHint(\"app.agents.back\", \"agents/resume\");\n\t}\n\n\tprivate getTrayContextLabel(): string | undefined {\n\t\tconst goalLabel = this.getTrayGoalLabel();\n\t\tconst heartbeatLabel = this.getTrayHeartbeatLabel();\n\t\tconst usage = this.getConnectionContextUsage();\n\t\tconst contextLabel =\n\t\t\tusage && typeof usage.tokens === \"number\" && typeof usage.percent === \"number\"\n\t\t\t\t? `${formatTokenCount(usage.tokens)} (${Math.round(usage.percent)}%)`\n\t\t\t\t: undefined;\n\t\treturn [goalLabel, heartbeatLabel, contextLabel].filter((label) => label !== undefined).join(\" · \") || undefined;\n\t}\n\n\tprivate getTrayHeartbeatLabel(): string | undefined {\n\t\tif (this.heartbeats.length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst paused = this.heartbeats.filter((heartbeat) => heartbeat.job.status === \"paused\").length;\n\t\tconst count = `${this.heartbeats.length} heartbeat${this.heartbeats.length === 1 ? \"\" : \"s\"}`;\n\t\tconst pausedLabel = paused ? ` · ${paused} paused` : \"\";\n\t\tconst shortcut = keyText(\"app.heartbeats.open\");\n\t\treturn `${count}${pausedLabel}${shortcut ? ` (${shortcut})` : \"\"}`;\n\t}\n\n\tprivate getTrayGoalLabel(): string | undefined {\n\t\tconst goal = this.getGoalState();\n\t\tswitch (goal.status) {\n\t\t\tcase \"active\":\n\t\t\t\treturn `Pursuing goal (${this.formatGoalElapsed(goal.timeUsedSeconds)})`;\n\t\t\tcase \"paused\":\n\t\t\t\treturn `Goal paused (${this.formatGoalElapsed(goal.timeUsedSeconds)})`;\n\t\t\tcase \"budget_limited\":\n\t\t\t\treturn `Goal budget limited (${this.formatGoalElapsed(goal.timeUsedSeconds)})`;\n\t\t\tcase \"idle\":\n\t\t\tcase \"complete\":\n\t\t\tcase \"error\":\n\t\t\t\treturn undefined;\n\t\t\tdefault: {\n\t\t\t\tconst _exhaustive: never = goal.status;\n\t\t\t\treturn _exhaustive;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate formatGoalElapsed(seconds: number): string {\n\t\tconst totalSeconds = Math.max(0, Math.trunc(seconds));\n\t\tif (totalSeconds < 60) {\n\t\t\treturn `${totalSeconds}s`;\n\t\t}\n\t\tconst minutes = Math.floor(totalSeconds / 60);\n\t\tconst remainingSeconds = totalSeconds % 60;\n\t\tif (minutes < 60) {\n\t\t\treturn `${minutes}m ${remainingSeconds.toString().padStart(2, \"0\")}s`;\n\t\t}\n\t\tconst hours = Math.floor(minutes / 60);\n\t\tconst remainingMinutes = minutes % 60;\n\t\treturn `${hours}h ${remainingMinutes.toString().padStart(2, \"0\")}m`;\n\t}\n\n\tprivate getUserMessageText(message: Message): string {\n\t\tif (message.role !== \"user\") return \"\";\n\t\tconst textBlocks =\n\t\t\ttypeof message.content === \"string\"\n\t\t\t\t? [{ type: \"text\", text: message.content }]\n\t\t\t\t: message.content.filter((c: { type: string }) => c.type === \"text\");\n\t\treturn textBlocks.map((c) => (c as { text: string }).text).join(\"\");\n\t}\n\n\tprivate createLegacyHeartbeatPromptMessage(\n\t\tmessage: Message,\n\t\ttextContent: string,\n\t): ReturnType<typeof createHeartbeatPromptMessage> | undefined {\n\t\tconst heartbeat = this.connectionState?.heartbeat;\n\t\tif (\n\t\t\tmessage.role !== \"user\" ||\n\t\t\t!heartbeat ||\n\t\t\t!this.isTextOnlyUserMessage(message) ||\n\t\t\ttextContent.trim() !== heartbeat.prompt.trim() ||\n\t\t\t!this.isLikelyHeartbeatPromptTimestamp(heartbeat, message.timestamp)\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn createHeartbeatPromptMessage(heartbeat, message.timestamp);\n\t}\n\n\tprivate isTextOnlyUserMessage(message: Message): boolean {\n\t\tif (message.role !== \"user\") {\n\t\t\treturn false;\n\t\t}\n\t\tif (typeof message.content === \"string\") {\n\t\t\treturn true;\n\t\t}\n\t\treturn message.content.every((content) => content.type === \"text\");\n\t}\n\n\tprivate isLikelyHeartbeatPromptTimestamp(job: AgentCronJob, timestamp: number): boolean {\n\t\tconst directRunTimes = [job.lastRunAt, job.nextRunAt]\n\t\t\t.map((value) => (value ? Date.parse(value) : Number.NaN))\n\t\t\t.filter((value) => Number.isFinite(value));\n\t\tconst tolerance = this.heartbeatLegacyPromptToleranceMs(job);\n\t\tif (directRunTimes.some((runAt) => Math.abs(timestamp - runAt) <= tolerance)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate heartbeatLegacyPromptToleranceMs(job: AgentCronJob): number {\n\t\tconst intervalMs = job.schedule.intervalMs;\n\t\tif (!intervalMs || intervalMs <= 0) {\n\t\t\treturn HEARTBEAT_LEGACY_PROMPT_MAX_TOLERANCE_MS;\n\t\t}\n\t\treturn Math.min(\n\t\t\tHEARTBEAT_LEGACY_PROMPT_MAX_TOLERANCE_MS,\n\t\t\tMath.max(HEARTBEAT_LEGACY_PROMPT_MIN_TOLERANCE_MS, intervalMs / 3),\n\t\t);\n\t}\n\n\t/**\n\t * Show a status message in the chat.\n\t *\n\t * If multiple status messages are emitted back-to-back (without anything else being added to the chat),\n\t * we update the previous status line instead of appending new ones to avoid log spam.\n\t */\n\tprivate showStatus(message: string, tone: \"dim\" | \"warning\" = \"dim\"): void {\n\t\tconst children = this.chatContainer.children;\n\t\tconst last = children.length > 0 ? children[children.length - 1] : undefined;\n\t\tconst secondLast = children.length > 1 ? children[children.length - 2] : undefined;\n\n\t\tif (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {\n\t\t\tthis.lastStatusText.setText(theme.fg(tone, message));\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tconst spacer = new Spacer(1);\n\t\tconst text = new Text(theme.fg(tone, message), 1, 0);\n\t\tthis.chatContainer.addChild(spacer);\n\t\tthis.chatContainer.addChild(text);\n\t\tthis.lastStatusSpacer = spacer;\n\t\tthis.lastStatusText = text;\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async copyFullscreenSelection(text: string): Promise<void> {\n\t\ttry {\n\t\t\tawait copyToClipboard(text);\n\t\t\tthis.showStatus(\"Copied selection to clipboard\");\n\t\t} catch (error) {\n\t\t\tthis.showError(`Failed to copy selection: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n\n\t// Local slash commands (/context, /system-prompt, …) print into the chat\n\t// without round-tripping through the agent, so no user message event echoes\n\t// the typed command. Render the turn ourselves, mirroring the \"user\" case\n\t// above, so the output is anchored to a visible command instead of floating.\n\tprivate echoLocalCommand(text: string): void {\n\t\tif (this.chatContainer.children.length > 0) {\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t}\n\t\tthis.chatContainer.addChild(\n\t\t\tnew UserMessageComponent(text, this.getMarkdownThemeWithSettings(), (name) =>\n\t\t\t\tthis.isRecognizedSlashCommand(name),\n\t\t\t),\n\t\t);\n\t}\n\n\tprivate addMessageToEditorHistory(message: AgentMessage): void {\n\t\tif (message.role !== \"user\") {\n\t\t\treturn;\n\t\t}\n\t\tconst textContent = this.getUserMessageText(message);\n\t\tif (textContent && !this.createLegacyHeartbeatPromptMessage(message, textContent)) {\n\t\t\tthis.editor.addToHistory?.(textContent);\n\t\t}\n\t}\n\n\tprivate createDisplayedCustomMessageComponent(message: CustomMessage): Component {\n\t\tif (isSessionSlashCommandMessage(message)) return new SlashCommandMessageComponent(message.content);\n\t\tif (isSessionSlashCommandResultMessage(message)) return new SlashCommandResultMessageComponent(message);\n\t\tif (\n\t\t\tmessage.customType === SESSION_SLASH_COMMAND_CUSTOM_TYPE ||\n\t\t\tmessage.customType === SESSION_SLASH_COMMAND_RESULT_CUSTOM_TYPE\n\t\t) {\n\t\t\treturn new UserMessageComponent(\"[Malformed session command message]\", this.getMarkdownThemeWithSettings());\n\t\t}\n\t\tif (isCompactionOutcomeMessage(message)) return new CompactionOutcomeMessageComponent(message);\n\t\tif (message.customType === COMPACTION_OUTCOME_CUSTOM_TYPE) {\n\t\t\treturn new MalformedCompactionOutcomeMessageComponent();\n\t\t}\n\t\tif (isRefinementOutcomeMessage(message)) return new RefinementOutcomeMessageComponent(message);\n\t\tif (message.customType === REFINEMENT_OUTCOME_CUSTOM_TYPE) {\n\t\t\treturn new MalformedRefinementOutcomeMessageComponent();\n\t\t}\n\t\tif (isAgentSessionMessage(message)) {\n\t\t\treturn new AgentMessageComponent(message, this.getMarkdownThemeWithSettings(), {\n\t\t\t\tsuppressLeadingSpace: isCompactAgentMessageNeighbor(this.chatContainer.children.at(-1)),\n\t\t\t});\n\t\t}\n\t\tif (isInjectedPromptMessage(message)) {\n\t\t\treturn new InjectedPromptMessageComponent(message, this.getMarkdownThemeWithSettings());\n\t\t}\n\t\treturn new CustomMessageComponent(\n\t\t\tmessage,\n\t\t\tthis.bindLocalSessionExtensions\n\t\t\t\t? this.getLocalSessionHost().getExtensionRunner().getMessageRenderer(message.customType)\n\t\t\t\t: undefined,\n\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t);\n\t}\n\n\tprivate addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void {\n\t\tswitch (message.role) {\n\t\t\tcase \"bashExecution\": {\n\t\t\t\tconst component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext, {\n\t\t\t\t\tsuppressLeadingSpace: this.chatContainer.children.at(-1) instanceof AgentMessageComponent,\n\t\t\t\t});\n\t\t\t\tif (message.output) {\n\t\t\t\t\tcomponent.appendOutput(message.output);\n\t\t\t\t}\n\t\t\t\tcomponent.setComplete(\n\t\t\t\t\tmessage.exitCode,\n\t\t\t\t\tmessage.cancelled,\n\t\t\t\t\tmessage.truncated ? ({ truncated: true } as TruncationResult) : undefined,\n\t\t\t\t\tmessage.fullOutputPath,\n\t\t\t\t);\n\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"custom\": {\n\t\t\t\tif (message.display) {\n\t\t\t\t\tconst component = this.createDisplayedCustomMessageComponent(message);\n\t\t\t\t\tif (isExpandable(component)) {\n\t\t\t\t\t\tcomponent.setExpanded(this.expansionStateFor(component));\n\t\t\t\t\t}\n\t\t\t\t\tif (hasEditDiffsExpansion(component)) {\n\t\t\t\t\t\tcomponent.setEditDiffsExpanded(this.editDiffsExpanded);\n\t\t\t\t\t}\n\t\t\t\t\tif (isSessionSlashCommandMessage(message) && this.chatContainer.children.length > 0) {\n\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t}\n\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"compactionSummary\": {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tconst component = new CompactionSummaryMessageComponent(message, this.getMarkdownThemeWithSettings());\n\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"branchSummary\": {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tconst component = new BranchSummaryMessageComponent(message, this.getMarkdownThemeWithSettings());\n\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"user\": {\n\t\t\t\tconst textContent = this.getUserMessageText(message);\n\t\t\t\tif (textContent) {\n\t\t\t\t\tconst heartbeatMessage = this.createLegacyHeartbeatPromptMessage(message, textContent);\n\t\t\t\t\tif (heartbeatMessage) {\n\t\t\t\t\t\tif (this.chatContainer.children.length > 0) {\n\t\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst component = new InjectedPromptMessageComponent(\n\t\t\t\t\t\t\theartbeatMessage,\n\t\t\t\t\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (this.chatContainer.children.length > 0) {\n\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t}\n\t\t\t\t\tconst skillBlock = parseSkillBlock(textContent);\n\t\t\t\t\tif (skillBlock) {\n\t\t\t\t\t\tconst component = new SkillInvocationMessageComponent(\n\t\t\t\t\t\t\tskillBlock,\n\t\t\t\t\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t\t\tif (skillBlock.userMessage) {\n\t\t\t\t\t\t\tconst userComponent = new UserMessageComponent(\n\t\t\t\t\t\t\t\tskillBlock.userMessage,\n\t\t\t\t\t\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t\t\t\t\t\t\t(name) => this.isRecognizedSlashCommand(name),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tthis.chatContainer.addChild(userComponent);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst userComponent = new UserMessageComponent(\n\t\t\t\t\t\t\ttextContent,\n\t\t\t\t\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t\t\t\t\t\t(name) => this.isRecognizedSlashCommand(name),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.chatContainer.addChild(userComponent);\n\t\t\t\t\t}\n\t\t\t\t\tif (options?.populateHistory) {\n\t\t\t\t\t\tthis.editor.addToHistory?.(textContent);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"assistant\": {\n\t\t\t\tconst assistantComponent = new AssistantMessageComponent(\n\t\t\t\t\tmessage,\n\t\t\t\t\tthis.hideThinkingBlock,\n\t\t\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t\t\t\tthis.hiddenThinkingLabel,\n\t\t\t\t\t{\n\t\t\t\t\t\texpanded: this.toolOutputExpanded,\n\t\t\t\t\t\tprecededByToolActivity:\n\t\t\t\t\t\t\tthis.chatContainer.children.at(-1) instanceof ToolExecutionComponent ||\n\t\t\t\t\t\t\tthis.chatContainer.children.at(-1) instanceof AgentMessageComponent,\n\t\t\t\t\t\tmermaidTransform: this.mermaidMarkdownTransform,\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t\tthis.chatContainer.addChild(assistantComponent);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"toolResult\": {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\tconst _exhaustive: never = message;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Render session context to chat. Used for initial load and rebuild after compaction.\n\t * @param sessionContext Session context to render\n\t * @param options.updateFooter Update footer state\n\t * @param options.populateHistory Add user messages to editor history\n\t * @param options.clearChat Clear the current transcript immediately before rendering\n\t * @param options.limitTranscript Limit transcript replay to the recent tail\n\t */\n\tprivate orderMessagesForTranscript(messages: AgentMessage[]): AgentMessage[] {\n\t\tconst summaryIndex = messages.findIndex((message) => message.role === \"compactionSummary\");\n\t\tif (summaryIndex === -1) return messages;\n\t\tconst summary = messages[summaryIndex];\n\t\tif (summary.role !== \"compactionSummary\") return messages;\n\t\tconst remaining = messages.filter((_, index) => index !== summaryIndex);\n\t\tif (Number.isSafeInteger(summary.retainedMessageCount) && summary.retainedMessageCount! >= 0) {\n\t\t\tconst boundary = Math.min(summary.retainedMessageCount!, remaining.length);\n\t\t\treturn [...remaining.slice(0, boundary), summary, ...remaining.slice(boundary)];\n\t\t}\n\n\t\t// Compatibility for summaries created before retainedMessageCount was added.\n\t\tconst retained: AgentMessage[] = [];\n\t\tconst later: AgentMessage[] = [];\n\t\tfor (const message of remaining) {\n\t\t\t(message.timestamp < summary.timestamp ? retained : later).push(message);\n\t\t}\n\t\treturn [...retained, summary, ...later];\n\t}\n\n\tprivate async renderSessionContext(\n\t\tsessionContext: AgentConnectionSessionContext,\n\t\toptions: {\n\t\t\tupdateFooter?: boolean;\n\t\t\tpopulateHistory?: boolean;\n\t\t\tclearChat?: boolean;\n\t\t\tlimitTranscript?: boolean;\n\t\t} = {},\n\t): Promise<void> {\n\t\tthis.resetPendingToolState();\n\t\tconst transcriptMessages = this.orderMessagesForTranscript(sessionContext.messages);\n\t\tconst messagesToRender = options.limitTranscript ? initialRenderMessages(transcriptMessages) : transcriptMessages;\n\t\tthis.ipythonToolComponents.clear();\n\t\tthis.lateIpythonSentAgentMessages.clear();\n\t\tconst renderedPendingTools = new Map<string, ToolExecutionComponent>();\n\t\tconst toolNames: string[] = [];\n\t\tfor (const message of messagesToRender) {\n\t\t\tif (message.role !== \"assistant\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const content of message.content) {\n\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\ttoolNames.push(content.name);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tawait this.preloadToolDefinitions(toolNames);\n\n\t\tif (options.clearChat) {\n\t\t\tthis.chatContainer.clear();\n\t\t}\n\n\t\tif (options.updateFooter) {\n\t\t\tthis.footer.invalidate();\n\t\t\tthis.updateEditorBorderColor();\n\t\t}\n\n\t\tif (options.populateHistory) {\n\t\t\tfor (const message of sessionContext.messages) {\n\t\t\t\tthis.addMessageToEditorHistory(message);\n\t\t\t}\n\t\t}\n\n\t\tconst renderOptions = { ...options, populateHistory: false };\n\n\t\tif (messagesToRender.length < sessionContext.messages.length) {\n\t\t\tthis.chatContainer.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t`Showing latest ${messagesToRender.length} of ${sessionContext.messages.length} messages for faster open.`,\n\t\t\t\t\t),\n\t\t\t\t\t1,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t}\n\n\t\tfor (const message of messagesToRender) {\n\t\t\t// Assistant messages need special handling for tool calls\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tthis.addMessageToChat(message);\n\t\t\t\t// Render tool call components\n\t\t\t\tfor (const content of message.content) {\n\t\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\t\tconst component = new ToolExecutionComponent(\n\t\t\t\t\t\t\tcontent.name,\n\t\t\t\t\t\t\tcontent.id,\n\t\t\t\t\t\t\tcontent.arguments,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tshowImages: this.settingsManager.getShowImages(),\n\t\t\t\t\t\t\t\tincludeImageDimensions: false,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tthis.getCachedToolDefinition(content.name),\n\t\t\t\t\t\t\tthis.ui,\n\t\t\t\t\t\t\tthis.getCurrentCwd(),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\t\t\tcomponent.setAgentMessagesExpanded(this.agentMessagesExpanded);\n\t\t\t\t\t\tcomponent.setEditDiffsExpanded(this.editDiffsExpanded);\n\t\t\t\t\t\tselectLatestToolExpandHint(this.chatContainer.children, component);\n\t\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t\t\tthis.registerIpythonToolComponent(content.name, content.id, component);\n\n\t\t\t\t\t\tif (message.stopReason === \"aborted\" || message.stopReason === \"error\") {\n\t\t\t\t\t\t\tlet errorMessage: string;\n\t\t\t\t\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\t\t\t\t\tconst retryAttempt = this.getRetryAttempt();\n\t\t\t\t\t\t\t\terrorMessage =\n\t\t\t\t\t\t\t\t\tretryAttempt > 0\n\t\t\t\t\t\t\t\t\t\t? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? \"s\" : \"\"}`\n\t\t\t\t\t\t\t\t\t\t: message.errorMessage && message.errorMessage !== \"Request was aborted\"\n\t\t\t\t\t\t\t\t\t\t\t? message.errorMessage\n\t\t\t\t\t\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\terrorMessage = message.errorMessage || \"Error\";\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcomponent.updateResult({ content: [{ type: \"text\", text: errorMessage }], isError: true });\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trenderedPendingTools.set(content.id, component);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (message.role === \"toolResult\") {\n\t\t\t\t// Match tool results to pending tool components\n\t\t\t\tconst component = renderedPendingTools.get(message.toolCallId);\n\t\t\t\tif (component) {\n\t\t\t\t\tcomponent.updateResult(message);\n\t\t\t\t\trenderedPendingTools.delete(message.toolCallId);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// All other messages use standard rendering\n\t\t\t\tthis.addMessageToChat(message, renderOptions);\n\t\t\t}\n\t\t}\n\n\t\tfor (const [toolCallId, component] of renderedPendingTools) {\n\t\t\tcomponent.setIncludeImageDimensions(true);\n\t\t\tthis.pendingTools.set(toolCallId, component);\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\tasync renderInitialMessages(): Promise<void> {\n\t\tconst snapshot = await this.agentConnection.getInitialSnapshot();\n\t\tconst context = this.getSessionContextFromConnectionSnapshot(snapshot);\n\t\tconst state = snapshot.state;\n\t\tconst streamingMessage = snapshot.streamingMessage;\n\t\tthis.rlmNodeId = snapshot.parent?.childId;\n\t\tthis.seedSubagentSummary(snapshot.children);\n\t\tthis.setSessionHasMessages(context.messages.length > 0);\n\t\tthis.applyConnectionStateSnapshot(state);\n\t\tthis.restoreTurnStartFromMessages(context.messages);\n\t\tawait this.renderSessionContext(context, {\n\t\t\tupdateFooter: true,\n\t\t\tpopulateHistory: true,\n\t\t\tlimitTranscript: true,\n\t\t});\n\t\tawait this.restoreStreamingMessageFromSnapshot(streamingMessage);\n\n\t\t// Show compaction info if session was compacted\n\t\tconst compactionCount = state.compactionCount;\n\t\tif (compactionCount > 0) {\n\t\t\tconst times = compactionCount === 1 ? \"1 time\" : `${compactionCount} times`;\n\t\t\tthis.showStatus(`Session compacted ${times}`);\n\t\t}\n\t}\n\n\tprivate async restoreStreamingMessageFromSnapshot(message: AgentMessage | undefined): Promise<void> {\n\t\tif (message?.role === \"assistant\") {\n\t\t\tthis.startAssistantStreamingMessage(message);\n\t\t\tfor (const content of message.content) {\n\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\tthis.startedToolCalls.add(content.id);\n\t\t\t\t\tawait this.getOrCreatePendingToolComponent(content);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate getSessionContextFromConnectionSnapshot(snapshot: AgentConnectionSnapshot): AgentConnectionSessionContext {\n\t\tif (snapshot.sessionContext) {\n\t\t\treturn snapshot.sessionContext;\n\t\t}\n\t\treturn {\n\t\t\tmessages: snapshot.messages,\n\t\t\tthinkingLevel: snapshot.state.thinkingLevel,\n\t\t\tserviceTier: snapshot.state.serviceTier,\n\t\t\tmodel: snapshot.state.model\n\t\t\t\t? { provider: snapshot.state.model.provider, modelId: snapshot.state.model.id }\n\t\t\t\t: null,\n\t\t};\n\t}\n\n\tasync getUserInput(): Promise<string | undefined> {\n\t\tif (this.agentsViewRequest) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.onInputCallback = (text: string | undefined) => {\n\t\t\t\tthis.onInputCallback = undefined;\n\t\t\t\tresolve(text);\n\t\t\t};\n\t\t});\n\t}\n\n\tprivate async rebuildChatFromMessages(): Promise<void> {\n\t\tconst context = await this.agentConnection.getSessionContext();\n\t\tawait this.renderSessionContext(context, { clearChat: true });\n\t}\n\n\tprivate handleEscape(): void {\n\t\tthis.clearCtrlCExitHint();\n\t\tif (this.sideQuestionEvent) {\n\t\t\tthis.clearEscapeRepeat();\n\t\t\tthis.clearSideQuestion({ abort: true });\n\t\t\treturn;\n\t\t}\n\t\tconst action = this.takeEscapeRepeatAction();\n\t\tif (action === \"tree\") {\n\t\t\tvoid this.showTreeSelector();\n\t\t\treturn;\n\t\t}\n\t\tif (action === \"clear\") {\n\t\t\tthis.clearInputBar();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.armEscapeRepeat(this.hasInterruptibleWork() || this.editor.getText().length === 0 ? \"tree\" : \"clear\");\n\t\tthis.interruptOrClearInput();\n\t}\n\n\tprivate armEscapeRepeat(action: \"tree\" | \"clear\"): void {\n\t\tthis.clearEscapeRepeat();\n\t\tthis.escapeRepeatAction = action;\n\t\tthis.escapeRepeatExpiresAt = Date.now() + InteractiveMode.ESCAPE_REPEAT_WINDOW_MS;\n\t\tthis.escapeRepeatTimer = setTimeout(() => {\n\t\t\tthis.clearEscapeRepeat();\n\t\t}, InteractiveMode.ESCAPE_REPEAT_WINDOW_MS);\n\t\tthis.escapeRepeatTimer.unref?.();\n\t}\n\n\tprivate takeEscapeRepeatAction(): \"tree\" | \"clear\" | undefined {\n\t\tif (!this.escapeRepeatAction || this.escapeRepeatExpiresAt <= Date.now()) {\n\t\t\tthis.clearEscapeRepeat();\n\t\t\treturn undefined;\n\t\t}\n\t\tconst action = this.escapeRepeatAction;\n\t\tthis.clearEscapeRepeat();\n\t\treturn action;\n\t}\n\n\tprivate clearEscapeRepeat(): void {\n\t\tif (this.escapeRepeatTimer) {\n\t\t\tclearTimeout(this.escapeRepeatTimer);\n\t\t\tthis.escapeRepeatTimer = undefined;\n\t\t}\n\t\tthis.escapeRepeatAction = undefined;\n\t\tthis.escapeRepeatExpiresAt = 0;\n\t}\n\n\tprivate handleCtrlC(): void {\n\t\tthis.clearEscapeRepeat();\n\t\tif (this.isCtrlCExitHintVisible()) {\n\t\t\tvoid this.shutdown();\n\t\t\treturn;\n\t\t}\n\t\tthis.handleInterruptKey();\n\t}\n\n\tprivate handleInterruptKey(): void {\n\t\tthis.clearEscapeRepeat();\n\t\tthis.interruptOrClearInput();\n\t\tthis.showCtrlCExitHint();\n\t}\n\n\tprivate interruptOrClearInput(): void {\n\t\tif (this.sideQuestionEvent?.status === \"running\") {\n\t\t\tthis.abortSideQuestion(this.sideQuestionEvent.id, true);\n\t\t}\n\t\tif (this.getRetryAttempt() > 0) {\n\t\t\tvoid this.agentConnection.abortRetry();\n\t\t}\n\t\tif (this.isAgentCompacting()) {\n\t\t\tvoid this.agentConnection.abortCompaction();\n\t\t\tvoid this.agentConnection.abortBranchSummary();\n\t\t}\n\t\tif (this.isBashRunning()) {\n\t\t\tvoid this.agentConnection.abortBash();\n\t\t}\n\t\tif (this.isAgentStreaming()) {\n\t\t\t// The queue is preserved server-side; draining resumes on the next\n\t\t\t// submit or queued-message edit.\n\t\t\tvoid this.agentConnection.abort().catch((error) => {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate showCtrlCExitHint(): void {\n\t\tif (this.ctrlCExitHintTimer) {\n\t\t\tclearTimeout(this.ctrlCExitHintTimer);\n\t\t}\n\t\tthis.ctrlCExitHintExpiresAt = Date.now() + InteractiveMode.EXIT_HINT_DURATION_MS;\n\t\tthis.ctrlCExitHintTimer = setTimeout(() => {\n\t\t\tthis.ctrlCExitHintTimer = undefined;\n\t\t\tif (!this.isCtrlCExitHintVisible()) {\n\t\t\t\tthis.ctrlCExitHintExpiresAt = 0;\n\t\t\t\tthis.subagentSummaryLine.invalidate();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t}\n\t\t}, InteractiveMode.EXIT_HINT_DURATION_MS);\n\t\tthis.ctrlCExitHintTimer.unref?.();\n\t\tthis.subagentSummaryLine.invalidate();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate clearCtrlCExitHint(options: { render?: boolean } = {}): void {\n\t\tif (!this.ctrlCExitHintTimer && this.ctrlCExitHintExpiresAt === 0) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.ctrlCExitHintTimer) {\n\t\t\tclearTimeout(this.ctrlCExitHintTimer);\n\t\t\tthis.ctrlCExitHintTimer = undefined;\n\t\t}\n\t\tthis.ctrlCExitHintExpiresAt = 0;\n\t\tif (options.render !== false) {\n\t\t\tthis.subagentSummaryLine.invalidate();\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate isCtrlCExitHintVisible(): boolean {\n\t\treturn this.ctrlCExitHintExpiresAt > Date.now();\n\t}\n\n\tprivate handleCtrlD(): void {\n\t\tvoid this.shutdown();\n\t}\n\n\t/**\n\t * Gracefully shutdown the agent.\n\t * Stops the TUI before emitting shutdown events so extension UI cleanup cannot\n\t * repaint the final frame while the process is exiting.\n\t */\n\tprivate isShuttingDown = false;\n\n\tprivate async shutdown(): Promise<void> {\n\t\tif (this.isShuttingDown) return;\n\t\tthis.isShuttingDown = true;\n\t\tthis.unregisterSignalHandlers();\n\t\tthis.clearCtrlCExitHint({ render: false });\n\n\t\t// Fetch while the connection is still alive; exit must not fail on a stats error.\n\t\tconst sessionStats = await this.agentConnection.getSessionStats().catch(() => undefined);\n\n\t\t// Drain any in-flight Kitty key release events before stopping.\n\t\t// This prevents escape sequences from leaking to the parent shell over slow SSH.\n\t\tawait this.ui.terminal.drainInput(1000);\n\n\t\tthis.stop();\n\t\ttry {\n\t\t\tawait this.agentConnection.dispose();\n\t\t} finally {\n\t\t\tawait this.options.onShutdown?.();\n\t\t}\n\t\tconst resumeHint = formatResumeHint(sessionStats);\n\t\tif (resumeHint) {\n\t\t\tconsole.log(resumeHint);\n\t\t}\n\t\tprocess.exit(0);\n\t}\n\n\t/**\n\t * Tear down the session's terminal UI before handing the terminal back to the\n\t * agents view. Drains in-flight Kitty/SSH key-release sequences so they don't\n\t * leak into the parent UI, then stops the renderer and theme watcher. Safe to\n\t * call from a crash path too; idempotent via stop().\n\t */\n\tasync teardownSessionUi(options: { preserveAltScreen?: boolean } = {}): Promise<void> {\n\t\tawait this.ui.terminal.drainInput(1000).catch(() => undefined);\n\t\tthis.releasePromptStashSession();\n\t\tthis.stop({ preserveAltScreen: options.preserveAltScreen });\n\t\tstopThemeWatcher();\n\t}\n\n\tprivate handleAgentsBack(): boolean {\n\t\tif (this.editor.getText().trim()) {\n\t\t\treturn false;\n\t\t}\n\t\tif (!this.options.returnToAgentsView) {\n\t\t\tvoid this.requestAgentsView();\n\t\t\treturn true;\n\t\t}\n\t\tvoid this.returnToAgentsView();\n\t\treturn true;\n\t}\n\n\tprivate async requestAgentsView(): Promise<void> {\n\t\tif (!this.options.returnToAgentsView) {\n\t\t\tthis.showStatus(\"The agents view needs the daemon; start without --no-daemon to browse sessions\");\n\t\t\treturn;\n\t\t}\n\t\tawait this.returnToAgentsView();\n\t}\n\n\tprivate async returnToAgentsView(request: InteractiveModeRunResult[\"type\"] = \"agents_view\"): Promise<void> {\n\t\tif (this.isShuttingDown || this.agentsViewRequest) return;\n\t\tthis.stashDraftForAgentsView();\n\t\tthis.agentsViewRequest = request;\n\t\tthis.isShuttingDown = true;\n\t\tthis.unregisterSignalHandlers();\n\n\t\tawait this.teardownSessionUi({ preserveAltScreen: true });\n\t\tlet handoffComplete = false;\n\t\ttry {\n\t\t\ttry {\n\t\t\t\tawait this.agentConnection.dispose();\n\t\t\t} finally {\n\t\t\t\tawait this.options.onShutdown?.();\n\t\t\t\tthis.onInputCallback?.(undefined);\n\t\t\t\thandoffComplete = true;\n\t\t\t}\n\t\t} finally {\n\t\t\tif (!handoffComplete) {\n\t\t\t\tthis.ui.terminal.leaveAltScreen();\n\t\t\t\tthis.ui.terminal.showCursor();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate emergencyTerminalExit(): never {\n\t\tthis.isShuttingDown = true;\n\t\tthis.unregisterSignalHandlers();\n\t\tkillTrackedDetachedChildren();\n\t\t// The terminal is gone. Do not run normal shutdown because TUI and\n\t\t// extension cleanup can write restore sequences and re-trigger EIO.\n\t\tprocess.exit(129);\n\t}\n\n\t/**\n\t * Check if shutdown was requested and perform shutdown if so.\n\t */\n\tprivate async checkShutdownRequested(): Promise<void> {\n\t\tif (!this.shutdownRequested) return;\n\t\tawait this.shutdown();\n\t}\n\n\tprivate registerSignalHandlers(): void {\n\t\tthis.unregisterSignalHandlers();\n\n\t\tconst signals: NodeJS.Signals[] = [\"SIGTERM\"];\n\t\tif (process.platform !== \"win32\") {\n\t\t\tsignals.push(\"SIGHUP\");\n\t\t}\n\n\t\tfor (const signal of signals) {\n\t\t\tconst handler = () => {\n\t\t\t\tif (signal === \"SIGHUP\") {\n\t\t\t\t\tthis.emergencyTerminalExit();\n\t\t\t\t}\n\t\t\t\tkillTrackedDetachedChildren();\n\t\t\t\tvoid this.shutdown();\n\t\t\t};\n\t\t\tprocess.prependListener(signal, handler);\n\t\t\tthis.signalCleanupHandlers.push(() => process.off(signal, handler));\n\t\t}\n\n\t\tconst terminalErrorHandler = (error: Error) => {\n\t\t\tif (isDeadTerminalError(error)) {\n\t\t\t\tthis.emergencyTerminalExit();\n\t\t\t}\n\t\t\tthrow error;\n\t\t};\n\t\tprocess.stdout.on(\"error\", terminalErrorHandler);\n\t\tprocess.stderr.on(\"error\", terminalErrorHandler);\n\t\tthis.signalCleanupHandlers.push(() => process.stdout.off(\"error\", terminalErrorHandler));\n\t\tthis.signalCleanupHandlers.push(() => process.stderr.off(\"error\", terminalErrorHandler));\n\t}\n\n\tprivate unregisterSignalHandlers(): void {\n\t\tfor (const cleanup of this.signalCleanupHandlers) {\n\t\t\tcleanup();\n\t\t}\n\t\tthis.signalCleanupHandlers = [];\n\t}\n\n\tprivate handleCtrlZ(): void {\n\t\tif (process.platform === \"win32\") {\n\t\t\tthis.showStatus(\"Suspend to background is not supported on Windows\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Keep the event loop alive while suspended. Without this, stopping the TUI\n\t\t// can leave Node with no ref'ed handles, causing the process to exit on fg\n\t\t// before the SIGCONT handler gets a chance to restore the terminal.\n\t\tconst suspendKeepAlive = setInterval(() => {}, 2 ** 30);\n\n\t\t// Ignore SIGINT while suspended so Ctrl+C in the terminal does not\n\t\t// kill the backgrounded process. The handler is removed on resume.\n\t\tconst ignoreSigint = () => {};\n\t\tprocess.on(\"SIGINT\", ignoreSigint);\n\n\t\t// Set up handler to restore TUI when resumed\n\t\tprocess.once(\"SIGCONT\", () => {\n\t\t\tclearInterval(suspendKeepAlive);\n\t\t\tprocess.removeListener(\"SIGINT\", ignoreSigint);\n\t\t\tthis.ui.start();\n\t\t\t// ui.stop() left the alt screen before suspending; re-enter it\n\t\t\tif (this.fullscreenEnabled) {\n\t\t\t\tthis.applyFullscreen(true);\n\t\t\t}\n\t\t\tthis.ui.requestRender(true);\n\t\t});\n\n\t\ttry {\n\t\t\t// Stop the TUI (restore terminal to normal mode)\n\t\t\tthis.ui.stop();\n\n\t\t\t// Send SIGTSTP to process group (pid=0 means all processes in group)\n\t\t\tprocess.kill(0, \"SIGTSTP\");\n\t\t} catch (error) {\n\t\t\tclearInterval(suspendKeepAlive);\n\t\t\tprocess.removeListener(\"SIGINT\", ignoreSigint);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprivate async handleFollowUp(): Promise<void> {\n\t\tconst editorText = this.editor.getText();\n\t\tconst text = (this.editor.getExpandedText?.() ?? editorText).trim();\n\t\tif (this.queueSelection?.isBrowsing && !this.pendingQueueEdit) {\n\t\t\tawait this.applyQueueSelection(text, \"followUp\").catch((error) =>\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error)),\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tif (!text || !this.editor.onSubmit) return;\n\n\t\t// Unlike Enter, Alt+Enter does not go through Editor.submitValue(), so\n\t\t// capture and clear synchronously before an async/local handler can yield.\n\t\tthis.pendingSubmittedPromptStash = this.snapshotPromptStash(editorText);\n\t\tthis.editor.setText(\"\");\n\t\tthis.submittedInputBehavior = \"followUp\";\n\t\t// onSubmit consumes the behavior flag and bumps the generation synchronously;\n\t\t// capture the generation so an older failed submit never clobbers newer\n\t\t// typing or submissions.\n\t\tconst submission = this.editor.onSubmit(text);\n\t\tthis.submittedInputBehavior = \"steer\";\n\t\tconst submissionGeneration = this.inputSubmissionGeneration;\n\t\ttry {\n\t\t\tawait submission;\n\t\t} catch (error) {\n\t\t\tif (submissionGeneration === this.inputSubmissionGeneration && this.editor.getText().length === 0) {\n\t\t\t\tthis.editor.setText(text);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprivate browseQueueSelection(direction: -1 | 1): void {\n\t\tif (this.pendingQueueEdit) return;\n\t\tconst text = this.queueSelection.move(this.connectionQueue, this.editor.getText(), direction);\n\t\tif (text === undefined) return;\n\t\tthis.setEditorTextFromQueueSelection(text);\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Serializes queue mutations so rapid keypresses never race each other or use stale indices. */\n\tprivate enqueueQueueMutation<T>(run: () => Promise<T>): Promise<T> {\n\t\tconst next = this.queueMutationChain.then(run, run);\n\t\tthis.queueMutationChain = next.then(\n\t\t\t() => undefined,\n\t\t\t() => undefined,\n\t\t);\n\t\treturn next;\n\t}\n\n\tprivate moveQueueSelection(direction: -1 | 1): void {\n\t\tif (this.pendingQueueEdit) return;\n\t\tconst submittedSelection = this.queueSelection.selected;\n\t\tif (!submittedSelection) return;\n\t\tconst sessionGeneration = this.sessionEventGeneration;\n\t\tvoid this.enqueueQueueMutation(async () => {\n\t\t\tif (sessionGeneration !== this.sessionEventGeneration) return;\n\t\t\tconst lane = this.connectionQueue[submittedSelection.lane];\n\t\t\tconst resolvedIndex =\n\t\t\t\tlane[submittedSelection.index] === submittedSelection.text\n\t\t\t\t\t? submittedSelection.index\n\t\t\t\t\t: lane.indexOf(submittedSelection.text);\n\t\t\tif (resolvedIndex < 0) {\n\t\t\t\tthis.showStatus(\"Queue changed; reorder not applied\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst selected = { ...submittedSelection, index: resolvedIndex };\n\t\t\tconst queueBefore = this.connectionQueue;\n\t\t\tconst status = await this.agentConnection.mutateQueuedMessage(selected.lane, selected.index, selected.text, {\n\t\t\t\ttype: \"move\",\n\t\t\t\tdirection,\n\t\t\t});\n\t\t\tif (sessionGeneration !== this.sessionEventGeneration) return;\n\t\t\tif (status === \"applied\") {\n\t\t\t\t// The queue event for this mutation can land before or after the\n\t\t\t\t// response. Patch the mirror only when no event has replaced it\n\t\t\t\t// meanwhile (events always assign a fresh object); patching an\n\t\t\t\t// already-updated mirror would apply the mutation twice.\n\t\t\t\tconst lane = this.connectionQueue[selected.lane];\n\t\t\t\tconst target = selected.index + direction;\n\t\t\t\tif (\n\t\t\t\t\tthis.connectionQueue === queueBefore &&\n\t\t\t\t\tlane[selected.index] === selected.text &&\n\t\t\t\t\ttarget >= 0 &&\n\t\t\t\t\ttarget < lane.length\n\t\t\t\t) {\n\t\t\t\t\t[lane[selected.index], lane[target]] = [lane[target] as string, selected.text];\n\t\t\t\t\tthis.queueSelection.sync(this.connectionQueue);\n\t\t\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t} else if (status === \"unsupported\") this.showStatus(\"Queue editing requires a newer daemon\");\n\t\t\telse this.showStatus(\"Queue changed; reorder not applied\");\n\t\t}).catch((error) => {\n\t\t\tif (sessionGeneration === this.sessionEventGeneration) {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Applies the edited text to the selected queued message. Returns true when\n\t * the submission was consumed (the caller must not treat it as a new prompt).\n\t * Empty text deletes; otherwise replaces, moving the item to `targetLane`.\n\t */\n\tprivate applyQueueSelection(text: string, targetLane: \"steering\" | \"followUp\"): Promise<boolean> {\n\t\tif (this.pendingQueueEdit) return Promise.resolve(false);\n\t\tconst submittedSelection = this.queueSelection.selected;\n\t\tif (!submittedSelection) return Promise.resolve(false);\n\t\tconst pendingQueueEdit = Symbol(\"pending-queue-edit\");\n\t\tthis.pendingQueueEdit = pendingQueueEdit;\n\t\tconst sessionGeneration = this.sessionEventGeneration;\n\t\tconst submissionGeneration = this.inputSubmissionGeneration;\n\t\tconst trimmed = text.trim();\n\t\tconst mutation =\n\t\t\ttrimmed.length === 0\n\t\t\t\t? ({ type: \"delete\" } as const)\n\t\t\t\t: ({\n\t\t\t\t\t\ttype: \"replace\",\n\t\t\t\t\t\ttext: trimmed,\n\t\t\t\t\t\timages: this.collectQueueReplaceImages(trimmed),\n\t\t\t\t\t\tlane: targetLane,\n\t\t\t\t\t} as const);\n\t\tconst editorTextBefore = this.editor.getText();\n\t\tconst discardStaleSelection = (): boolean => {\n\t\t\tif (sessionGeneration === this.sessionEventGeneration) return false;\n\t\t\tif (this.pendingQueueEdit === pendingQueueEdit) {\n\t\t\t\tthis.pendingQueueEdit = undefined;\n\t\t\t\tthis.queueSelection.reset();\n\t\t\t\tif (submissionGeneration === this.inputSubmissionGeneration && this.editor.getText() === editorTextBefore) {\n\t\t\t\t\tthis.setEditorTextFromQueueSelection(\"\");\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\t\treturn this.enqueueQueueMutation(async () => {\n\t\t\tif (discardStaleSelection()) return true;\n\t\t\t// Earlier serialized moves may have changed the selected item's index.\n\t\t\tconst lane = this.connectionQueue[submittedSelection.lane];\n\t\t\tconst resolvedIndex =\n\t\t\t\tlane[submittedSelection.index] === submittedSelection.text\n\t\t\t\t\t? submittedSelection.index\n\t\t\t\t\t: lane.indexOf(submittedSelection.text);\n\t\t\tif (resolvedIndex < 0) {\n\t\t\t\tthis.queueSelection.sync(this.connectionQueue);\n\t\t\t\tconst editorUntouched =\n\t\t\t\t\tsubmissionGeneration === this.inputSubmissionGeneration && this.editor.getText() === editorTextBefore;\n\t\t\t\tif (editorUntouched) {\n\t\t\t\t\tthis.setEditorTextFromQueueSelection(text);\n\t\t\t\t}\n\t\t\t\tthis.queueSelection.replaceDraft(editorUntouched ? text : this.editor.getText());\n\t\t\t\tthis.showStatus(\"Queue changed; edit kept in the editor\");\n\t\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst selected = { ...submittedSelection, index: resolvedIndex };\n\t\t\tconst queueBefore = this.connectionQueue;\n\t\t\tlet status: AgentConnectionQueuedMessageMutationStatus;\n\t\t\ttry {\n\t\t\t\tstatus = await this.agentConnection.mutateQueuedMessage(\n\t\t\t\t\tselected.lane,\n\t\t\t\t\tselected.index,\n\t\t\t\t\tselected.text,\n\t\t\t\t\tmutation,\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tif (discardStaleSelection()) return true;\n\t\t\t\t// The editor was already cleared by Enter; restore the edit before surfacing the error.\n\t\t\t\tconst editorUntouched =\n\t\t\t\t\tsubmissionGeneration === this.inputSubmissionGeneration && this.editor.getText() === editorTextBefore;\n\t\t\t\tif (editorUntouched) {\n\t\t\t\t\tthis.setEditorTextFromQueueSelection(text);\n\t\t\t\t}\n\t\t\t\tif (!this.queueSelection.isBrowsing) {\n\t\t\t\t\tthis.queueSelection.replaceDraft(editorUntouched ? text : this.editor.getText());\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tif (discardStaleSelection()) return true;\n\t\t\tconst editorUntouched =\n\t\t\t\tsubmissionGeneration === this.inputSubmissionGeneration && this.editor.getText() === editorTextBefore;\n\t\t\tif (status === \"applied\") {\n\t\t\t\t// Same optimistic patch as moveQueueSelection, and the same guard:\n\t\t\t\t// skip when a queue event already replaced the mirror.\n\t\t\t\tconst lane = this.connectionQueue[selected.lane];\n\t\t\t\tif (this.connectionQueue === queueBefore && lane[selected.index] === selected.text) {\n\t\t\t\t\tif (!trimmed) lane.splice(selected.index, 1);\n\t\t\t\t\telse if (targetLane === selected.lane) lane[selected.index] = trimmed;\n\t\t\t\t\telse {\n\t\t\t\t\t\tlane.splice(selected.index, 1);\n\t\t\t\t\t\tthis.connectionQueue[targetLane].push(trimmed);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (trimmed) this.editor.addToHistory?.(trimmed);\n\t\t\t\tconst draft = this.queueSelection.reset();\n\t\t\t\tif (editorUntouched) this.setEditorTextFromQueueSelection(draft);\n\t\t\t} else {\n\t\t\t\tthis.queueSelection.sync(this.connectionQueue);\n\t\t\t\t// Enter submissions clear the editor before onSubmit runs; restore the\n\t\t\t\t// edit so a failed mutation never swallows it.\n\t\t\t\tif (editorUntouched) this.setEditorTextFromQueueSelection(text);\n\t\t\t\tif (!this.queueSelection.isBrowsing) {\n\t\t\t\t\tthis.queueSelection.replaceDraft(editorUntouched ? text : this.editor.getText());\n\t\t\t\t}\n\t\t\t\tif (status === \"invalid\")\n\t\t\t\t\tthis.showStatus(\"Edited command is not a valid session command; edit kept in the editor\");\n\t\t\t\telse if (status === \"unsupported\") this.showStatus(\"Queue editing requires a newer daemon\");\n\t\t\t\telse this.showStatus(\"Queue changed; edit kept in the editor\");\n\t\t\t}\n\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\tthis.ui.requestRender();\n\t\t\treturn true;\n\t\t}).finally(() => {\n\t\t\tif (this.pendingQueueEdit === pendingQueueEdit) this.pendingQueueEdit = undefined;\n\t\t});\n\t}\n\n\t/**\n\t * Images for a queue replace: undefined preserves the server's images (some\n\t * markers cannot be resolved by this client), [] clears, a list replaces.\n\t */\n\tprivate collectQueueReplaceImages(text: string): ImageContent[] | undefined {\n\t\tconst markers = [...new Set(imageMarkerIds(text))];\n\t\tif (markers.length === 0) return [];\n\t\tconst resolved = markers.map((markerId) => this.pastedImages.get(markerId));\n\t\treturn resolved.every((image) => image !== undefined)\n\t\t\t? resolved.map((image) => ({ ...(image as ImageContent) }))\n\t\t\t: undefined;\n\t}\n\n\tprivate setEditorTextFromQueueSelection(text: string): void {\n\t\tthis.isApplyingQueueSelectionText = true;\n\t\ttry {\n\t\t\tthis.editor.setText(text);\n\t\t} finally {\n\t\t\tthis.isApplyingQueueSelectionText = false;\n\t\t}\n\t}\n\n\tprivate getQueueSelectionHeader(): string | undefined {\n\t\tconst selected = this.queueSelection.selected;\n\t\tif (!selected) return undefined;\n\t\tconst lane = selected.lane === \"steering\" ? \"steering\" : \"follow-up\";\n\t\tconst older = this.getAppKeyDisplay(\"app.message.navigateOlder\");\n\t\tconst newer = this.getAppKeyDisplay(\"app.message.navigateNewer\");\n\t\tconst earlier = this.getAppKeyDisplay(\"app.message.moveEarlier\");\n\t\tconst later = this.getAppKeyDisplay(\"app.message.moveLater\");\n\t\tconst queue = this.getAppKeyDisplay(\"app.message.followUp\");\n\t\treturn theme.fg(\n\t\t\t\"dim\",\n\t\t\t`${lane} ${selected.index + 1} · ${older}/${newer} browse · ${earlier}/${later} reorder · enter steers · ${queue} queues · empty deletes`,\n\t\t);\n\t}\n\n\tprivate updateEditorBorderColor(): void {\n\t\tconst editorTheme = getEditorTheme();\n\t\tthis.editor.borderColor = editorTheme.borderColor;\n\t\tthis.editor.backgroundColor = editorTheme.backgroundColor;\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate getPromptContextContainers(): Container[] {\n\t\treturn [this.recapContainer, this.featureHintContainer, this.queuedMessagesContainer, this.sideQuestionContainer];\n\t}\n\n\tprivate getPromptDockComponents(): Component[] {\n\t\treturn [this.editorContainer, this.subagentSummaryLine, this.footerSlot];\n\t}\n\n\t/** Enter or leave fullscreen rendering without touching the persisted setting. */\n\tprivate applyFullscreen(enabled: boolean): void {\n\t\tif (enabled) {\n\t\t\tif (!process.stdout.isTTY) return;\n\t\t\tthis.ui.enterFullscreen({\n\t\t\t\tscroll: [\n\t\t\t\t\tthis.headerContainer,\n\t\t\t\t\tthis.mainViewContainer,\n\t\t\t\t\tthis.widgetContainerAbove,\n\t\t\t\t\t...this.getPromptContextContainers(),\n\t\t\t\t\tthis.widgetContainerBelow,\n\t\t\t\t],\n\t\t\t\tdock: this.promptDock,\n\t\t\t\tmouse: this.settingsManager.getFullscreenMouse(),\n\t\t\t});\n\t\t} else {\n\t\t\tthis.ui.exitFullscreen();\n\t\t}\n\t}\n\n\tprivate setFullscreenMode(enabled: boolean): void {\n\t\tthis.settingsManager.setFullscreen(enabled);\n\t\tif (enabled && !process.stdout.isTTY) {\n\t\t\tthis.fullscreenEnabled = false;\n\t\t\tthis.showStatus(\"Fullscreen rendering requires an interactive terminal\");\n\t\t\treturn;\n\t\t}\n\t\tthis.fullscreenEnabled = enabled;\n\t\tthis.applyFullscreen(enabled);\n\t\tconst followKey = this.getEditorKeyDisplay(\"tui.viewport.follow\");\n\t\tthis.showStatus(\n\t\t\tenabled\n\t\t\t\t? `Fullscreen rendering on — wheel/pageUp scroll, ${followKey} follows output`\n\t\t\t\t: \"Fullscreen rendering off\",\n\t\t);\n\t}\n\n\tprivate toggleToolOutputExpansion(): void {\n\t\tthis.setToolsExpanded(!this.toolOutputExpanded);\n\t}\n\n\tprivate toggleAgentMessageExpansion(): void {\n\t\tthis.agentMessagesExpanded = !this.agentMessagesExpanded;\n\t\tthis.applyChatExpansion();\n\t}\n\n\tprivate toggleEditDiffExpansion(): void {\n\t\tthis.editDiffsExpanded = !this.editDiffsExpanded;\n\t\tthis.applyChatExpansion();\n\t}\n\n\tprivate setToolsExpanded(expanded: boolean): void {\n\t\tthis.toolOutputExpanded = expanded;\n\t\tthis.applyChatExpansion();\n\t}\n\n\t/** Expansion state for a chat component: agent messages toggle separately from tools. */\n\tprivate expansionStateFor(component: unknown): boolean {\n\t\treturn component instanceof AgentMessageComponent ? this.agentMessagesExpanded : this.toolOutputExpanded;\n\t}\n\n\tprivate applyChatExpansion(): void {\n\t\tconst activeHeader = this.customHeader ?? this.builtInHeader;\n\t\tif (isExpandable(activeHeader)) {\n\t\t\tactiveHeader.setExpanded(this.toolOutputExpanded);\n\t\t}\n\t\tfor (const child of this.chatContainer.children) {\n\t\t\tif (isExpandable(child)) {\n\t\t\t\tchild.setExpanded(this.expansionStateFor(child));\n\t\t\t}\n\t\t\tif (hasAgentMessagesExpansion(child)) {\n\t\t\t\tchild.setAgentMessagesExpanded(this.agentMessagesExpanded);\n\t\t\t}\n\t\t\tif (hasEditDiffsExpansion(child)) {\n\t\t\t\tchild.setEditDiffsExpanded(this.editDiffsExpanded);\n\t\t\t}\n\t\t}\n\t\t// Expanding/collapsing changes blocks above the viewport, which would\n\t\t// otherwise force a full redraw that scrolls to the top and replays the\n\t\t// whole transcript. Keep the user anchored at their current position.\n\t\t// Fullscreen frames have no scrollback to preserve.\n\t\tif (this.ui.isFullscreen()) {\n\t\t\tthis.ui.requestRender();\n\t\t} else {\n\t\t\tthis.ui.requestRenderPreservingViewport();\n\t\t}\n\t}\n\n\tprivate toggleThinkingBlockVisibility(): void {\n\t\tthis.hideThinkingBlock = !this.hideThinkingBlock;\n\t\tthis.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);\n\n\t\tvoid (async () => {\n\t\t\t// Rebuild chat from session messages\n\t\t\tawait this.rebuildChatFromMessages();\n\n\t\t\t// If streaming, re-add the streaming component with updated visibility and re-render\n\t\t\tif (this.streamingComponent && this.streamingMessage) {\n\t\t\t\tthis.streamingComponent.setHideThinkingBlock(this.hideThinkingBlock);\n\t\t\t\tthis.streamingComponent.updateContent(this.streamingMessage);\n\t\t\t\tthis.chatContainer.addChild(this.streamingComponent);\n\t\t\t}\n\n\t\t\tthis.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? \"hidden\" : \"visible\"}`);\n\t\t})().catch((error) => {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t});\n\t}\n\n\tprivate openExternalEditor(): void {\n\t\t// Determine editor (respect $VISUAL, then $EDITOR)\n\t\tconst editorCmd = process.env.VISUAL || process.env.EDITOR;\n\t\tif (!editorCmd) {\n\t\t\tthis.showWarning(\"No editor configured. Set $VISUAL or $EDITOR environment variable.\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst currentText = this.editor.getExpandedText?.() ?? this.editor.getText();\n\t\tconst tmpFile = path.join(os.tmpdir(), `pi-editor-${Date.now()}.pi.md`);\n\n\t\ttry {\n\t\t\t// Write current content to temp file\n\t\t\tfs.writeFileSync(tmpFile, currentText, \"utf-8\");\n\n\t\t\t// Stop TUI to release terminal\n\t\t\tthis.ui.stop();\n\n\t\t\t// Split by space to support editor arguments (e.g., \"code --wait\")\n\t\t\tconst [editor, ...editorArgs] = editorCmd.split(\" \");\n\n\t\t\t// Spawn editor synchronously with inherited stdio for interactive editing\n\t\t\tconst result = spawnSync(editor, [...editorArgs, tmpFile], {\n\t\t\t\tstdio: \"inherit\",\n\t\t\t\tshell: process.platform === \"win32\",\n\t\t\t});\n\n\t\t\t// On successful exit (status 0), replace editor content\n\t\t\tif (result.status === 0) {\n\t\t\t\tconst newContent = fs.readFileSync(tmpFile, \"utf-8\").replace(/\\n$/, \"\");\n\t\t\t\tthis.editor.setText(newContent);\n\t\t\t}\n\t\t\t// On non-zero exit, keep original text (no action needed)\n\t\t} finally {\n\t\t\t// Clean up temp file\n\t\t\ttry {\n\t\t\t\tfs.unlinkSync(tmpFile);\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\n\t\t\t// Restart TUI\n\t\t\tthis.ui.start();\n\t\t\t// ui.stop() left fullscreen so the editor got a clean terminal\n\t\t\tif (this.fullscreenEnabled) {\n\t\t\t\tthis.applyFullscreen(true);\n\t\t\t}\n\t\t\t// Force full re-render since external editor uses alternate screen\n\t\t\tthis.ui.requestRender(true);\n\t\t}\n\t}\n\n\tprivate clearInputBar(): void {\n\t\tthis.clearEscapeRepeat();\n\t\tthis.clearCtrlCExitHint({ render: false });\n\t\t// Leaving browse mode restores the stashed draft instead of arming an\n\t\t// accidental empty-submit delete of the selected queued message.\n\t\tthis.editor.setText(this.queueSelection.hasDraft ? this.queueSelection.reset() : \"\");\n\t\tthis.ui.requestRender();\n\t}\n\n\tshowError(errorMessage: string): void {\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(theme.fg(\"error\", `Error: ${errorMessage}`), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tshowWarning(warningMessage: string): void {\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(theme.fg(\"warning\", `⚠ ${warningMessage}`), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tshowNewVersionNotification(newVersion: string): void {\n\t\tthis.chatContainer.addChild(new Text(formatUpdateAvailableNotice(newVersion), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tshowPackageUpdateNotification(packages: string[]): void {\n\t\tthis.chatContainer.addChild(new Text(formatPackageUpdateNotice(packages), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate getAllQueuedMessages(): { steering: string[]; followUp: string[] } {\n\t\treturn {\n\t\t\tsteering: [...this.connectionQueue.steering],\n\t\t\tfollowUp: [...this.connectionQueue.followUp],\n\t\t};\n\t}\n\n\tprivate updatePendingMessagesDisplay(): void {\n\t\t// pendingMessagesContainer holds only in-flight bash output for the current\n\t\t// turn, so it stays above the execution indicator. clear() detaches the\n\t\t// components but they stay tracked in pendingBashComponents until flushed.\n\t\tthis.pendingMessagesContainer.clear();\n\t\tfor (const component of this.pendingBashComponents) {\n\t\t\tthis.pendingMessagesContainer.addChild(component);\n\t\t}\n\t\t// Queued steering/follow-up previews are future turns, so they render in\n\t\t// their own container below the execution indicator and recap.\n\t\tthis.queuedMessagesContainer.clear();\n\t\tconst { steering: steeringMessages, followUp: followUpMessages } = this.getAllQueuedMessages();\n\t\tconst hasQueuedMessages = steeringMessages.length > 0 || followUpMessages.length > 0;\n\t\tif (hasQueuedMessages) {\n\t\t\tthis.queuedMessagesContainer.addChild(new Spacer(1));\n\t\t\tfor (const message of steeringMessages) {\n\t\t\t\tconst text = styleQueuedMessagePreview(message, \"Steering\", (name) => this.isRecognizedSlashCommand(name));\n\t\t\t\tthis.queuedMessagesContainer.addChild(new TruncatedText(text, 1, 0));\n\t\t\t}\n\t\t\tfor (const message of followUpMessages) {\n\t\t\t\tconst text = styleQueuedMessagePreview(message, \"Follow-up\", (name) => this.isRecognizedSlashCommand(name));\n\t\t\t\tthis.queuedMessagesContainer.addChild(new TruncatedText(text, 1, 0));\n\t\t\t}\n\t\t\tconst dequeueHint = this.getAppKeyDisplay(\"app.message.navigateOlder\");\n\t\t\tconst hintText = theme.fg(\"dim\", `╰─ ${dequeueHint} to browse and edit queued messages`);\n\t\t\tthis.queuedMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));\n\t\t}\n\t\tif (hasQueuedMessages && !this.featureHintSuppressedByQueue) {\n\t\t\tthis.featureHintSuppressedByQueue = true;\n\t\t\tthis.clearFeatureHintPresentation();\n\t\t} else if (!hasQueuedMessages && this.featureHintSuppressedByQueue) {\n\t\t\tthis.featureHintSuppressedByQueue = false;\n\t\t\tthis.resumeFeatureHintPresentation();\n\t\t}\n\t}\n\n\tprivate flushPendingBashComponents(): void {\n\t\tfor (const component of this.pendingBashComponents) {\n\t\t\tthis.pendingMessagesContainer.removeChild(component);\n\t\t\tthis.chatContainer.addChild(component);\n\t\t}\n\t\tthis.pendingBashComponents = [];\n\t}\n\n\t/**\n\t * Shows a selector component in place of the editor.\n\t * @param create Factory that receives a `done` callback and returns the component and focus target\n\t */\n\tprivate showSelector(create: (done: () => void) => { component: Component; focus: Component }): void {\n\t\tconst done = () => {\n\t\t\tthis.editorContainer.clear();\n\t\t\tthis.editorContainer.addChild(this.editor);\n\t\t\tthis.ui.setFocus(this.editor);\n\t\t};\n\t\tconst { component, focus } = create(done);\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(component);\n\t\tthis.ui.setFocus(focus);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate showFullPaneOverlay(component: Component, options: number | FullPaneOverlayOptions = 80): OverlayHandle {\n\t\treturn showFullPaneOverlay(this.ui, component, options);\n\t}\n\n\tprivate async showSettingsSelector(): Promise<void> {\n\t\tlet state: AgentConnectionState;\n\t\ttry {\n\t\t\tstate = await this.agentConnection.getState();\n\t\t\tthis.applyConnectionStateSnapshot(state);\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\treturn;\n\t\t}\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new SettingsSelectorComponent(\n\t\t\t\t{\n\t\t\t\t\tautoCompact: state.autoCompactionEnabled,\n\t\t\t\t\tidleEvictionMinutes: this.settingsManager.getIdleEvictionMinutes(),\n\t\t\t\t\tshowImages: this.settingsManager.getShowImages(),\n\t\t\t\t\tautoResizeImages: this.settingsManager.getImageAutoResize(),\n\t\t\t\t\tblockImages: this.settingsManager.getBlockImages(),\n\t\t\t\t\tenableSkillCommands: this.settingsManager.getEnableSkillCommands(),\n\t\t\t\t\tenableBuiltinSkills: this.settingsManager.getEnableBuiltinSkills(),\n\t\t\t\t\tsteeringMode: state.steeringMode,\n\t\t\t\t\tfollowUpMode: state.followUpMode,\n\t\t\t\t\ttransport: this.settingsManager.getTransport(),\n\t\t\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t\t\t\tavailableThinkingLevels: state.availableThinkingLevels,\n\t\t\t\t\tcurrentTheme: this.settingsManager.getTheme() || \"asm\",\n\t\t\t\t\tavailableThemes: getAvailableThemes(),\n\t\t\t\t\thideThinkingBlock: this.hideThinkingBlock,\n\t\t\t\t\tmermaidRenderingMode: this.settingsManager.getMermaidRenderingMode(),\n\t\t\t\t\ttreeFilterMode: this.settingsManager.getTreeFilterMode(),\n\t\t\t\t\tshowHardwareCursor: this.settingsManager.getShowHardwareCursor(),\n\t\t\t\t\teditorPaddingX: this.settingsManager.getEditorPaddingX(),\n\t\t\t\t\tautocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),\n\t\t\t\t\tquietStartup: this.settingsManager.getQuietStartup(),\n\t\t\t\t\tclearOnShrink: this.settingsManager.getClearOnShrink(),\n\t\t\t\t\tshowTerminalProgress: this.settingsManager.getShowTerminalProgress(),\n\t\t\t\t\tfullscreen: this.fullscreenEnabled,\n\t\t\t\t\twarnings: this.settingsManager.getWarnings(),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tonAutoCompactChange: (enabled) => {\n\t\t\t\t\t\tthis.patchConnectionState({ autoCompactionEnabled: enabled });\n\t\t\t\t\t\tvoid this.agentConnection.setAutoCompactionEnabled(enabled).catch((error) => {\n\t\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\t});\n\t\t\t\t\t\tthis.footer.setAutoCompactEnabled(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonIdleEvictionMinutesChange: (value) => {\n\t\t\t\t\t\tthis.settingsManager.setIdleEvictionMinutes(value);\n\t\t\t\t\t},\n\t\t\t\t\tonShowImagesChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setShowImages(enabled);\n\t\t\t\t\t\tfor (const child of this.chatContainer.children) {\n\t\t\t\t\t\t\tif (child instanceof ToolExecutionComponent) {\n\t\t\t\t\t\t\t\tchild.setShowImages(enabled);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonAutoResizeImagesChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setImageAutoResize(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonBlockImagesChange: (blocked) => {\n\t\t\t\t\t\tthis.settingsManager.setBlockImages(blocked);\n\t\t\t\t\t},\n\t\t\t\t\tonEnableSkillCommandsChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setEnableSkillCommands(enabled);\n\t\t\t\t\t\tthis.setupAutocompleteProvider();\n\t\t\t\t\t},\n\t\t\t\t\tonEnableBuiltinSkillsChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setEnableBuiltinSkills(enabled);\n\t\t\t\t\t\tvoid this.handleReloadCommand();\n\t\t\t\t\t},\n\t\t\t\t\tonSteeringModeChange: (mode) => {\n\t\t\t\t\t\tthis.patchConnectionState({ steeringMode: mode });\n\t\t\t\t\t\tvoid this.agentConnection.setSteeringMode(mode).catch((error) => {\n\t\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tonFollowUpModeChange: (mode) => {\n\t\t\t\t\t\tthis.patchConnectionState({ followUpMode: mode });\n\t\t\t\t\t\tvoid this.agentConnection.setFollowUpMode(mode).catch((error) => {\n\t\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tonTransportChange: (transport) => {\n\t\t\t\t\t\tvoid this.agentConnection.setTransport(transport).catch((error) => {\n\t\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tonThinkingLevelChange: (level) => {\n\t\t\t\t\t\tvoid this.agentConnection\n\t\t\t\t\t\t\t.setThinkingLevel(level)\n\t\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\t\tthis.patchConnectionState({ thinkingLevel: level });\n\t\t\t\t\t\t\t\tthis.footer.invalidate();\n\t\t\t\t\t\t\t\tthis.updateEditorBorderColor();\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t.catch((error) => {\n\t\t\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tonThemeChange: (themeName) => {\n\t\t\t\t\t\tconst result = setTheme(themeName, true);\n\t\t\t\t\t\tthis.settingsManager.setTheme(themeName);\n\t\t\t\t\t\tthis.ui.invalidate();\n\t\t\t\t\t\tif (!result.success) {\n\t\t\t\t\t\t\tthis.showError(`Failed to load theme \"${themeName}\": ${result.error}\\nFell back to dark theme.`);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonThemePreview: (themeName) => {\n\t\t\t\t\t\tconst result = setTheme(themeName, true);\n\t\t\t\t\t\tif (result.success) {\n\t\t\t\t\t\t\tthis.ui.invalidate();\n\t\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonHideThinkingBlockChange: (hidden) => {\n\t\t\t\t\t\tthis.hideThinkingBlock = hidden;\n\t\t\t\t\t\tthis.settingsManager.setHideThinkingBlock(hidden);\n\t\t\t\t\t\tfor (const child of this.chatContainer.children) {\n\t\t\t\t\t\t\tif (child instanceof AssistantMessageComponent) {\n\t\t\t\t\t\t\t\tchild.setHideThinkingBlock(hidden);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvoid this.rebuildChatFromMessages().catch((error) => {\n\t\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tonMermaidRenderingModeChange: (mode) => {\n\t\t\t\t\t\tthis.settingsManager.setMermaidRenderingMode(mode);\n\t\t\t\t\t\tthis.chatContainer.invalidate();\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t},\n\t\t\t\t\tonQuietStartupChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setQuietStartup(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonTreeFilterModeChange: (mode) => {\n\t\t\t\t\t\tthis.settingsManager.setTreeFilterMode(mode);\n\t\t\t\t\t},\n\t\t\t\t\tonShowHardwareCursorChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setShowHardwareCursor(enabled);\n\t\t\t\t\t\tthis.ui.setShowHardwareCursor(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonEditorPaddingXChange: (padding) => {\n\t\t\t\t\t\tthis.settingsManager.setEditorPaddingX(padding);\n\t\t\t\t\t\tthis.defaultEditor.setPaddingX(padding);\n\t\t\t\t\t\tif (this.editor !== this.defaultEditor && this.editor.setPaddingX !== undefined) {\n\t\t\t\t\t\t\tthis.editor.setPaddingX(padding);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonAutocompleteMaxVisibleChange: (maxVisible) => {\n\t\t\t\t\t\tthis.settingsManager.setAutocompleteMaxVisible(maxVisible);\n\t\t\t\t\t\tthis.defaultEditor.setAutocompleteMaxVisible(maxVisible);\n\t\t\t\t\t\tif (this.editor !== this.defaultEditor && this.editor.setAutocompleteMaxVisible !== undefined) {\n\t\t\t\t\t\t\tthis.editor.setAutocompleteMaxVisible(maxVisible);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonClearOnShrinkChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setClearOnShrink(enabled);\n\t\t\t\t\t\tthis.ui.setClearOnShrink(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonShowTerminalProgressChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setShowTerminalProgress(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonFullscreenChange: (enabled) => {\n\t\t\t\t\t\tthis.setFullscreenMode(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonWarningsChange: (warnings) => {\n\t\t\t\t\t\tthis.settingsManager.setWarnings(warnings);\n\t\t\t\t\t},\n\t\t\t\t\tonCancel: () => {\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector.getSettingsList() };\n\t\t});\n\t}\n\n\tprivate async handleModelCommand(searchTerm?: string): Promise<void> {\n\t\tif (!searchTerm) {\n\t\t\tthis.showModelSelector();\n\t\t\treturn;\n\t\t}\n\n\t\tconst model = await this.findExactModelMatch(searchTerm);\n\t\tif (model) {\n\t\t\ttry {\n\t\t\t\tconst authFlows = this.createAuthFlows();\n\t\t\t\tconst providerOptions = authFlows.getLoginProviderOptions();\n\t\t\t\tif (!(await this.ensureModelProviderConfigured(model, authFlows, providerOptions))) return;\n\t\t\t\tawait this.completeModelSelection(model);\n\t\t\t} catch (error) {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tthis.showModelSelector(searchTerm);\n\t}\n\n\tprivate async findExactModelMatch(searchTerm: string): Promise<Model<Api> | undefined> {\n\t\tconst cachedMatch = findExactModelReferenceMatch(searchTerm, this.getCachedModelCandidates());\n\t\tif (cachedMatch) {\n\t\t\treturn cachedMatch;\n\t\t}\n\n\t\tconst refreshPromise = this.getModelSelectorRefreshPromise({ force: true });\n\t\tif (!refreshPromise) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\ttry {\n\t\t\treturn findExactModelReferenceMatch(searchTerm, await refreshPromise);\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async applySelectedModel(model: AgentConnectionModel): Promise<void> {\n\t\tconst connection = this.agentConnection;\n\t\tconst sessionId = this.connectionState?.sessionId;\n\t\tawait connection.setModel(model.provider, model.id);\n\t\tconst state = await connection.getState();\n\t\tif (\n\t\t\tthis.agentConnection !== connection ||\n\t\t\tthis.connectionState?.sessionId !== sessionId ||\n\t\t\t(sessionId !== undefined && state.sessionId !== sessionId)\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tthis.settingsManager.setDefaultModelAndProvider(model.provider, model.id);\n\t\tthis.patchConnectionState({\n\t\t\tmodel: state.model ?? model,\n\t\t\tserviceTier: state.serviceTier,\n\t\t\tavailableThinkingLevels: state.availableThinkingLevels,\n\t\t});\n\t\tthis.footer.invalidate();\n\t\tthis.subagentSummaryLine.invalidate();\n\t\tthis.updateEditorBorderColor();\n\t\t// Rebuild so the /effort argument hint reflects the new model's levels.\n\t\tthis.setupAutocompleteProvider();\n\t}\n\n\tprivate async completeModelSelection(model: AgentConnectionModel): Promise<void> {\n\t\tthis.showStatus(`Switching model: ${model.id}`);\n\t\tawait this.applySelectedModel(model);\n\t\tthis.showStatus(`Model: ${model.id}`);\n\t\tvoid this.maybeWarnAboutAnthropicSubscriptionAuth(model);\n\t\tthis.checkDaxnutsEasterEgg(model);\n\t}\n\n\tprivate async ensureModelProviderConfigured(\n\t\tmodel: AgentConnectionModel,\n\t\tauthFlows: ProviderAuthFlows,\n\t\tproviderOptions: ReadonlyArray<AuthSelectorProvider>,\n\t): Promise<boolean> {\n\t\tif (this.isModelProviderConfigured(model)) return true;\n\n\t\tconst provider = providerOptions.find(\n\t\t\t(option) => option.id === model.provider && (option.category ?? \"provider\") === \"provider\",\n\t\t);\n\t\tif (!provider) {\n\t\t\tthis.showError(`Authentication for ${model.provider} must be configured externally.`);\n\t\t\treturn false;\n\t\t}\n\n\t\tconst result = await authFlows.loginProvider(provider);\n\t\tif (result.status !== \"success\") return false;\n\n\t\tthis.invalidateConnectionModels();\n\t\tawait this.getConnectionAvailableModels();\n\t\tif (this.isModelProviderConfigured(model)) return true;\n\n\t\tthis.showError(`Authentication completed, but ${model.provider} is still unavailable.`);\n\t\treturn false;\n\t}\n\n\tprivate isModelProviderConfigured(model: AgentConnectionModel): boolean {\n\t\treturn this.connectionConfiguredProviders.has(model.provider) || this.modelRegistry.hasConfiguredAuth(model);\n\t}\n\n\tprivate applyConnectionModelCatalog(catalog: AgentConnectionModelCatalog): void {\n\t\tthis.connectionModelCatalog = [...catalog.models];\n\t\tthis.connectionConfiguredProviders = new Set(catalog.configuredProviders);\n\t\tthis.connectionModels = catalog.models.filter((model) => this.connectionConfiguredProviders.has(model.provider));\n\t}\n\n\tprivate async getConnectionAvailableModels(): Promise<AgentConnectionModel[]> {\n\t\tconst inFlight = this.connectionModelsRefreshInFlight;\n\t\tif (inFlight && inFlight.version === this.connectionModelsRefreshVersion) {\n\t\t\treturn [...(await inFlight.promise)];\n\t\t}\n\n\t\tconst version = this.connectionModelsRefreshVersion;\n\t\tconst promise = this.agentConnection.getModelCatalog().then((catalog) => {\n\t\t\tif (version !== this.connectionModelsRefreshVersion) {\n\t\t\t\treturn [...this.connectionModels];\n\t\t\t}\n\t\t\tthis.applyConnectionModelCatalog(catalog);\n\t\t\tthis.connectionModelsFetchedAt = Date.now();\n\t\t\treturn [...this.connectionModels];\n\t\t});\n\t\tthis.connectionModelsRefreshInFlight = { version, promise };\n\n\t\ttry {\n\t\t\treturn [...(await promise)];\n\t\t} finally {\n\t\t\tif (this.connectionModelsRefreshInFlight?.promise === promise) {\n\t\t\t\tthis.connectionModelsRefreshInFlight = undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async getConnectionModelCatalog(): Promise<AgentConnectionModel[]> {\n\t\tawait this.getConnectionAvailableModels();\n\t\treturn [...this.connectionModelCatalog];\n\t}\n\n\tprivate getCachedModelCandidates(): AgentConnectionModel[] {\n\t\tconst modelsById = new Map<string, AgentConnectionModel>();\n\t\tfor (const scoped of this.getScopedModelState()) {\n\t\t\tmodelsById.set(`${scoped.model.provider}/${scoped.model.id}`, scoped.model);\n\t\t}\n\t\tfor (const model of this.connectionModelCatalog) {\n\t\t\tmodelsById.set(`${model.provider}/${model.id}`, model);\n\t\t}\n\t\treturn [...modelsById.values()];\n\t}\n\n\tprivate getModelSelectorRefreshPromise(\n\t\toptions: { force?: boolean } = {},\n\t): Promise<AgentConnectionModel[]> | undefined {\n\t\tconst refreshCatalog = () => this.getConnectionAvailableModels().then(() => this.getCachedModelCandidates());\n\t\tif (this.connectionModelsRefreshInFlight) {\n\t\t\treturn refreshCatalog();\n\t\t}\n\t\tif (options.force || this.connectionModelsFetchedAt === 0) {\n\t\t\treturn refreshCatalog();\n\t\t}\n\t\tif (Date.now() - this.connectionModelsFetchedAt > MODEL_CATALOG_REFRESH_TTL_MS) {\n\t\t\treturn refreshCatalog();\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate invalidateConnectionModelRefresh(): void {\n\t\tthis.connectionModelsRefreshVersion++;\n\t\tthis.connectionModelsRefreshInFlight = undefined;\n\t}\n\n\tprivate invalidateConnectionModels(): void {\n\t\tthis.connectionModels = [];\n\t\tthis.connectionConfiguredProviders = new Set();\n\t\tthis.connectionModelsFetchedAt = 0;\n\t\tthis.invalidateConnectionModelRefresh();\n\t}\n\n\tprivate async refreshConnectionModelsAfterAuthChange(): Promise<void> {\n\t\tthis.invalidateConnectionModels();\n\t\tawait this.getConnectionAvailableModels();\n\t}\n\n\tprivate async getModelCandidates(): Promise<AgentConnectionModel[]> {\n\t\tconst scopedModels = this.getScopedModelState();\n\t\tif (scopedModels.length > 0) {\n\t\t\treturn scopedModels.map((scoped) => scoped.model);\n\t\t}\n\n\t\ttry {\n\t\t\treturn await this.getConnectionAvailableModels();\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\n\tprivate getScopedModelsFromModelIds(\n\t\tenabledIds: readonly string[],\n\t\tallModels: readonly AgentConnectionModel[],\n\t): AgentConnectionState[\"scopedModels\"] {\n\t\tconst modelsById = new Map(allModels.map((model) => [`${model.provider}/${model.id}`, model]));\n\t\tconst selectedIds = new Set<string>();\n\t\tconst scopedModels: AgentConnectionState[\"scopedModels\"] = [];\n\n\t\tfor (const id of enabledIds) {\n\t\t\tif (selectedIds.has(id)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst model = modelsById.get(id);\n\t\t\tif (!model) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tselectedIds.add(id);\n\t\t\tscopedModels.push({ model });\n\t\t}\n\n\t\treturn scopedModels;\n\t}\n\n\tprivate async updateAvailableProviderCount(): Promise<void> {\n\t\tconst models = await this.getModelCandidates();\n\t\tconst uniqueProviders = new Set(models.map((m) => m.provider));\n\t\tthis.footerDataProvider.setAvailableProviderCount(uniqueProviders.size);\n\t}\n\n\tprivate async maybeWarnAboutAnthropicSubscriptionAuth(\n\t\tmodel: Model<any> | undefined = this.getCurrentModel(),\n\t): Promise<void> {\n\t\tif (this.settingsManager.getWarnings().anthropicExtraUsage === false) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.anthropicSubscriptionWarningShown) {\n\t\t\treturn;\n\t\t}\n\t\tconst warning = await getAnthropicSubscriptionAuthWarning(this.modelRegistry, model);\n\t\tif (!warning) {\n\t\t\treturn;\n\t\t}\n\t\tthis.anthropicSubscriptionWarningShown = true;\n\t\tthis.showWarning(warning);\n\t}\n\n\tprivate getAvailableThinkingLevels(): ThinkingLevel[] {\n\t\tconst levels = this.connectionState?.availableThinkingLevels ?? [];\n\t\tconst supportsThinking = levels.length > 0 && !(levels.length === 1 && levels[0] === \"off\");\n\t\treturn supportsThinking ? levels : [];\n\t}\n\n\tprivate getThinkingLevelCompletions(prefix: string): AutocompleteItem[] | null {\n\t\tconst levels = this.getAvailableThinkingLevels();\n\t\tif (levels.length === 0) return null;\n\t\tconst current = this.connectionState?.thinkingLevel;\n\t\tconst term = prefix.trim().toLowerCase();\n\t\tconst matches = term ? levels.filter((level) => level.startsWith(term)) : levels;\n\t\tif (matches.length === 0) return null;\n\t\treturn matches.map((level) => ({\n\t\t\tvalue: level,\n\t\t\tlabel: level,\n\t\t\tdescription:\n\t\t\t\tlevel === current ? `${THINKING_LEVEL_DESCRIPTIONS[level]} (current)` : THINKING_LEVEL_DESCRIPTIONS[level],\n\t\t}));\n\t}\n\n\tprivate getHeartbeatArgumentCompletions(prefix: string): AutocompleteItem[] | null {\n\t\tconst term = prefix.trim().toLowerCase();\n\t\tconst filtered = term\n\t\t\t? HEARTBEAT_ARGUMENT_COMPLETIONS.filter(\n\t\t\t\t\t(item) => item.value.toLowerCase().startsWith(term) || item.label.toLowerCase().startsWith(term),\n\t\t\t\t)\n\t\t\t: HEARTBEAT_ARGUMENT_COMPLETIONS;\n\t\treturn filtered.length === 0 ? null : filtered;\n\t}\n\n\tprivate currentModelSupportsFastMode(): boolean {\n\t\tconst model = this.getCurrentModel();\n\t\treturn model !== undefined && supportsFastMode(model);\n\t}\n\n\tprivate handleFastCommand(): void {\n\t\tconst unavailableMessage =\n\t\t\t\"Fast mode requires GPT-5.4, GPT-5.5, or GPT-5.6 with ChatGPT or OpenAI API key authentication\";\n\t\tif (!this.currentModelSupportsFastMode()) {\n\t\t\tthis.showStatus(unavailableMessage);\n\t\t\treturn;\n\t\t}\n\t\tconst connection = this.agentConnection;\n\t\tconst sessionId = this.connectionState?.sessionId;\n\t\tthis.fastModeToggleQueue = this.fastModeToggleQueue\n\t\t\t.then(async () => {\n\t\t\t\tif (this.agentConnection !== connection || this.connectionState?.sessionId !== sessionId) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!this.currentModelSupportsFastMode()) {\n\t\t\t\t\tthis.showStatus(unavailableMessage);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst enabled = this.connectionState?.serviceTier === \"priority\";\n\t\t\t\tconst serviceTier: ServiceTier = enabled ? \"default\" : \"priority\";\n\t\t\t\tawait connection.setServiceTier(serviceTier);\n\t\t\t\tif (this.agentConnection !== connection || this.connectionState?.sessionId !== sessionId) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst state = await connection.getState();\n\t\t\t\tif (\n\t\t\t\t\tthis.agentConnection !== connection ||\n\t\t\t\t\tthis.connectionState?.sessionId !== sessionId ||\n\t\t\t\t\tstate.sessionId !== sessionId\n\t\t\t\t) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.patchConnectionState({ serviceTier: state.serviceTier });\n\t\t\t\tthis.footer.invalidate();\n\t\t\t\tthis.subagentSummaryLine.invalidate();\n\t\t\t\tthis.showStatus(`Fast mode: ${state.serviceTier === \"priority\" ? \"on\" : \"off\"}`);\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t});\n\t}\n\n\tprivate handleEffortCommand(arg: string): void {\n\t\tconst levels = this.getAvailableThinkingLevels();\n\t\tif (levels.length === 0) {\n\t\t\tthis.showStatus(\"Current model does not support thinking\");\n\t\t\treturn;\n\t\t}\n\t\tconst requested = arg.trim().toLowerCase();\n\t\tif (!requested) {\n\t\t\tthis.showThinkingSelector(levels);\n\t\t\treturn;\n\t\t}\n\t\tif (!levels.includes(requested as ThinkingLevel)) {\n\t\t\tthis.showError(`Unknown thinking level '${requested}'. Available: ${levels.join(\", \")}`);\n\t\t\treturn;\n\t\t}\n\t\tthis.applyThinkingLevel(requested as ThinkingLevel);\n\t}\n\n\tprivate showThinkingSelector(levels: ThinkingLevel[] = this.getAvailableThinkingLevels()): void {\n\t\tconst currentLevel = this.connectionState?.thinkingLevel ?? levels[0];\n\t\tif (!currentLevel) {\n\t\t\tthis.showStatus(\"Current model does not support thinking\");\n\t\t\treturn;\n\t\t}\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new ThinkingSelectorComponent(\n\t\t\t\tcurrentLevel,\n\t\t\t\tlevels,\n\t\t\t\t(level) => {\n\t\t\t\t\tdone();\n\t\t\t\t\tthis.applyThinkingLevel(level);\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tdone();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector.getSelectList() };\n\t\t});\n\t}\n\n\tprivate applyThinkingLevel(level: ThinkingLevel): void {\n\t\tvoid this.agentConnection\n\t\t\t.setThinkingLevel(level)\n\t\t\t.then(() => {\n\t\t\t\tthis.patchConnectionState({ thinkingLevel: level });\n\t\t\t\tthis.footer.invalidate();\n\t\t\t\tthis.updateEditorBorderColor();\n\t\t\t\tthis.showStatus(`Thinking level: ${level}`);\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t});\n\t}\n\n\tprivate showModelSelector(initialSearchInput?: string): void {\n\t\tvoid this.showConfigurationMenu(\"models\", initialSearchInput);\n\t}\n\n\tprivate showConfigurationMenu(initialTab: ConfigurationMenuTab, initialModelSearch?: string): Promise<void> {\n\t\tconst modelCatalog = this.getCachedModelCandidates();\n\t\tconst authFlows = this.createAuthFlows();\n\t\tconst providerOptions = authFlows.getLoginProviderOptions();\n\n\t\treturn new Promise((resolve) => {\n\t\t\tlet handle: OverlayHandle | undefined;\n\t\t\tlet settled = false;\n\t\t\tlet hidden = false;\n\t\t\tlet removed = false;\n\t\t\tlet menu: ConfigurationMenuComponent;\n\t\t\tconst hide = () => {\n\t\t\t\tif (removed) return;\n\t\t\t\tremoved = true;\n\t\t\t\thidden = true;\n\t\t\t\thandle?.hide();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t};\n\t\t\tconst conceal = () => {\n\t\t\t\tif (hidden || removed) return;\n\t\t\t\thidden = true;\n\t\t\t\thandle?.setHidden(true);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t};\n\t\t\tconst show = () => {\n\t\t\t\tif (!hidden || removed || settled) return;\n\t\t\t\thidden = false;\n\t\t\t\thandle?.setHidden(false);\n\t\t\t\thandle?.focus();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t};\n\t\t\tconst finish = () => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\thide();\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\tconst refreshModels = (force: boolean) => {\n\t\t\t\tconst refreshPromise = this.getModelSelectorRefreshPromise({ force });\n\t\t\t\tif (!refreshPromise) return;\n\t\t\t\tvoid refreshPromise\n\t\t\t\t\t.then((models) => {\n\t\t\t\t\t\tif (!settled) menu.updateModels(this.getCurrentModel(), models, this.connectionConfiguredProviders);\n\t\t\t\t\t})\n\t\t\t\t\t.catch((error) => {\n\t\t\t\t\t\tif (!settled) this.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t});\n\t\t\t};\n\t\t\tconst authenticate = (provider: AuthSelectorProvider, tab: \"providers\" | \"mcp-connections\") => {\n\t\t\t\tif (settled) return;\n\t\t\t\tvoid authFlows\n\t\t\t\t\t.loginProvider(provider)\n\t\t\t\t\t.then(async (authResult) => {\n\t\t\t\t\t\tif (settled) return;\n\t\t\t\t\t\thandle?.focus();\n\t\t\t\t\t\tmenu.refreshAuthentication();\n\t\t\t\t\t\tif (authResult.status !== \"success\") return;\n\n\t\t\t\t\t\tif (tab === \"mcp-connections\") {\n\t\t\t\t\t\t\tif (!authResult.providerId.startsWith(\"mcp:\")) return;\n\t\t\t\t\t\t\tif (this.isAgentStreaming() || this.isAgentCompacting()) {\n\t\t\t\t\t\t\t\tthis.showStatus(\"Connected. Run /reload (after the current turn) to activate the integration.\");\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfinish();\n\t\t\t\t\t\t\tawait this.handleReloadCommand();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tawait this.prepareForModelSelectionAfterLogin(authResult);\n\t\t\t\t\t\tmenu.updateModels(\n\t\t\t\t\t\t\tthis.getCurrentModel(),\n\t\t\t\t\t\t\tthis.getCachedModelCandidates(),\n\t\t\t\t\t\t\tthis.connectionConfiguredProviders,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tmenu.setActiveTab(\"models\");\n\t\t\t\t\t\trefreshModels(true);\n\t\t\t\t\t})\n\t\t\t\t\t.catch((error) => {\n\t\t\t\t\t\thandle?.focus();\n\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t});\n\t\t\t};\n\n\t\t\tmenu = new ConfigurationMenuComponent({\n\t\t\t\tinitialTab,\n\t\t\t\ttui: this.ui,\n\t\t\t\tauthStorage: this.modelRegistry.authStorage,\n\t\t\t\tproviderOptions,\n\t\t\t\tmodelRegistry: this.modelRegistry,\n\t\t\t\tcurrentModel: this.getCurrentModel(),\n\t\t\t\tscopedModels: this.getScopedModelState(),\n\t\t\t\tavailableModels: modelCatalog,\n\t\t\t\tconfiguredProviders: this.connectionConfiguredProviders,\n\t\t\t\trecentModels: this.settingsManager.getRecentModels(),\n\t\t\t\tinitialModelSearch,\n\t\t\t\tgetRows: () => this.ui.terminal.rows,\n\t\t\t\trequestRender: () => this.ui.requestRender(),\n\t\t\t\tonSelectProvider: (provider) => authenticate(provider, \"providers\"),\n\t\t\t\tonSelectMcpConnection: (provider) => authenticate(provider, \"mcp-connections\"),\n\t\t\t\tonSelectModel: (model) => {\n\t\t\t\t\tvoid (async () => {\n\t\t\t\t\t\tlet completed = false;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst ready = await this.ensureModelProviderConfigured(model, authFlows, providerOptions);\n\t\t\t\t\t\t\thandle?.focus();\n\t\t\t\t\t\t\tmenu.refreshAuthentication();\n\t\t\t\t\t\t\tmenu.updateModels(\n\t\t\t\t\t\t\t\tthis.getCurrentModel(),\n\t\t\t\t\t\t\t\tthis.getCachedModelCandidates(),\n\t\t\t\t\t\t\t\tthis.connectionConfiguredProviders,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tif (!ready || settled) return;\n\t\t\t\t\t\t\tconceal();\n\t\t\t\t\t\t\tawait this.completeModelSelection(model);\n\t\t\t\t\t\t\tcompleted = true;\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tshow();\n\t\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\t} finally {\n\t\t\t\t\t\t\tif (completed) finish();\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t\tonCancel: finish,\n\t\t\t});\n\t\t\thandle = this.showFullPaneOverlay(menu, 96);\n\t\t\trefreshModels(initialModelSearch !== undefined);\n\t\t});\n\t}\n\n\tprivate async showModelsSelector(): Promise<void> {\n\t\tlet allModels: AgentConnectionModel[];\n\t\ttry {\n\t\t\tallModels = await this.getConnectionModelCatalog();\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\treturn;\n\t\t}\n\n\t\tif (allModels.length === 0) {\n\t\t\tthis.showStatus(\"No models available\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if session has scoped models (from previous session-only changes or CLI --models)\n\t\tconst sessionScopedModels = this.getScopedModelState();\n\t\tconst hasSessionScope = sessionScopedModels.length > 0;\n\n\t\t// Build enabled model IDs from session state or settings\n\t\tlet currentEnabledIds: string[] | null = null;\n\n\t\tif (hasSessionScope) {\n\t\t\t// Use current session's scoped models\n\t\t\tcurrentEnabledIds = sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);\n\t\t} else {\n\t\t\t// Fall back to settings\n\t\t\tconst patterns = this.settingsManager.getEnabledModels();\n\t\t\tif (patterns !== undefined && patterns.length > 0) {\n\t\t\t\tconst scopedModels = resolveModelScopeFromModels(patterns, allModels);\n\t\t\t\tcurrentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);\n\t\t\t}\n\t\t}\n\n\t\t// Helper to update session's scoped models (session-only, no persist)\n\t\tconst updateSessionModels = async (enabledIds: string[] | null) => {\n\t\t\tcurrentEnabledIds = enabledIds === null ? null : [...enabledIds];\n\t\t\tif (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {\n\t\t\t\tconst scopedModels = this.getScopedModelsFromModelIds(enabledIds, allModels);\n\t\t\t\tawait this.agentConnection.setScopedModels(scopedModels);\n\t\t\t\tthis.patchConnectionState({ scopedModels });\n\t\t\t} else {\n\t\t\t\t// All enabled or none enabled = no filter\n\t\t\t\tawait this.agentConnection.setScopedModels([]);\n\t\t\t\tthis.patchConnectionState({ scopedModels: [] });\n\t\t\t}\n\t\t\tawait this.updateAvailableProviderCount();\n\t\t\tthis.ui.requestRender();\n\t\t};\n\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new ScopedModelsSelectorComponent(\n\t\t\t\t{\n\t\t\t\t\tallModels,\n\t\t\t\t\tenabledModelIds: currentEnabledIds,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tonChange: async (enabledIds) => {\n\t\t\t\t\t\tawait updateSessionModels(enabledIds);\n\t\t\t\t\t},\n\t\t\t\t\tonPersist: (enabledIds) => {\n\t\t\t\t\t\t// Persist to settings\n\t\t\t\t\t\tconst newPatterns =\n\t\t\t\t\t\t\tenabledIds === null || enabledIds.length === allModels.length\n\t\t\t\t\t\t\t\t? undefined // All enabled = clear filter\n\t\t\t\t\t\t\t\t: enabledIds;\n\t\t\t\t\t\tthis.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);\n\t\t\t\t\t\tthis.showStatus(\"Model selection saved to settings\");\n\t\t\t\t\t},\n\t\t\t\t\tonCancel: () => {\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector };\n\t\t});\n\t}\n\n\tprivate async showUserMessageSelector(): Promise<void> {\n\t\tlet userMessages: Array<{ entryId: string; text: string }>;\n\t\ttry {\n\t\t\tuserMessages = await this.agentConnection.getUserMessagesForForking();\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\treturn;\n\t\t}\n\n\t\tif (userMessages.length === 0) {\n\t\t\tthis.showStatus(\"No messages to fork from\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst initialSelectedId = userMessages[userMessages.length - 1]?.entryId;\n\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new UserMessageSelectorComponent(\n\t\t\t\tuserMessages.map((m) => ({ id: m.entryId, text: m.text })),\n\t\t\t\tasync (entryId) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = await this.agentConnection.fork(entryId);\n\t\t\t\t\t\tif (result.cancelled) {\n\t\t\t\t\t\t\tdone();\n\t\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tawait this.renderCurrentSessionState();\n\t\t\t\t\t\tthis.editor.setText(result.selectedText ?? \"\");\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.showStatus(\"Forked to new session\");\n\t\t\t\t\t} catch (error: unknown) {\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tdone();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t},\n\t\t\t\tinitialSelectedId,\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector.getMessageList() };\n\t\t});\n\t}\n\n\tprivate async handleCloneCommand(): Promise<void> {\n\t\ttry {\n\t\t\tconst { leafId } = await this.agentConnection.getSessionTree();\n\t\t\tif (!leafId) {\n\t\t\t\tthis.showStatus(\"Nothing to clone yet\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst result = await this.agentConnection.fork(leafId, { position: \"at\" });\n\t\t\tif (result.cancelled) {\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tawait this.renderCurrentSessionState();\n\t\t\tthis.editor.setText(\"\");\n\t\t\tthis.showStatus(\"Cloned to new session\");\n\t\t} catch (error: unknown) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate async showTreeSelector(initialSelectedId?: string): Promise<void> {\n\t\tlet tree: AgentConnectionSessionTreeNode[];\n\t\tlet realLeafId: string | null;\n\t\ttry {\n\t\t\tconst sessionTree = await this.agentConnection.getSessionTree();\n\t\t\ttree = sessionTree.tree;\n\t\t\trealLeafId = sessionTree.leafId;\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\treturn;\n\t\t}\n\t\tconst initialFilterMode = this.settingsManager.getTreeFilterMode();\n\n\t\tif (tree.length === 0) {\n\t\t\tthis.showStatus(\"No entries in session\");\n\t\t\treturn;\n\t\t}\n\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new TreeSelectorComponent(\n\t\t\t\ttree,\n\t\t\t\trealLeafId,\n\t\t\t\tthis.ui.terminal.rows,\n\t\t\t\tasync (entryId) => {\n\t\t\t\t\t// Selecting the current leaf is a no-op (already there)\n\t\t\t\t\tif (entryId === realLeafId) {\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.showStatus(\"Already at this point\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Ask about summarization\n\t\t\t\t\tdone(); // Close selector first\n\n\t\t\t\t\t// Loop until user makes a complete choice or cancels to tree\n\t\t\t\t\tlet wantsSummary = false;\n\t\t\t\t\tlet customInstructions: string | undefined;\n\n\t\t\t\t\t// Check if we should skip the prompt (user preference to always default to no summary)\n\t\t\t\t\tif (!this.settingsManager.getBranchSummarySkipPrompt()) {\n\t\t\t\t\t\twhile (true) {\n\t\t\t\t\t\t\tconst summaryChoice = await this.showExtensionSelector(\"Summarize branch?\", [\n\t\t\t\t\t\t\t\t\"No summary\",\n\t\t\t\t\t\t\t\t\"Summarize\",\n\t\t\t\t\t\t\t\t\"Summarize with custom prompt\",\n\t\t\t\t\t\t\t]);\n\n\t\t\t\t\t\t\tif (summaryChoice === undefined) {\n\t\t\t\t\t\t\t\t// User pressed escape - re-show tree selector with same selection\n\t\t\t\t\t\t\t\tvoid this.showTreeSelector(entryId);\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\twantsSummary = summaryChoice !== \"No summary\";\n\n\t\t\t\t\t\t\tif (summaryChoice === \"Summarize with custom prompt\") {\n\t\t\t\t\t\t\t\tcustomInstructions = await this.showExtensionEditor(\"Custom summarization instructions\");\n\t\t\t\t\t\t\t\tif (customInstructions === undefined) {\n\t\t\t\t\t\t\t\t\t// User cancelled - loop back to summary selector\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// User made a complete choice\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Set up escape handler and loader if summarizing\n\t\t\t\t\tlet summaryLoader: Loader | undefined;\n\t\t\t\t\tif (wantsSummary) {\n\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t\tsummaryLoader = new Loader(\n\t\t\t\t\t\t\tthis.ui,\n\t\t\t\t\t\t\t(spinner) => theme.fg(\"muted\", spinner),\n\t\t\t\t\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\t\t\t\t\t`Summarizing branch... (${keyText(\"app.clear\")} to cancel)`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.statusContainer.addChild(summaryLoader);\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t}\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = await this.agentConnection.navigateTree(entryId, {\n\t\t\t\t\t\t\tsummarize: wantsSummary,\n\t\t\t\t\t\t\tcustomInstructions,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tif (result.aborted) {\n\t\t\t\t\t\t\t// Summarization aborted - re-show tree selector with same selection\n\t\t\t\t\t\t\tthis.showStatus(\"Branch summarization cancelled\");\n\t\t\t\t\t\t\tvoid this.showTreeSelector(entryId);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (result.cancelled) {\n\t\t\t\t\t\t\tthis.showStatus(\"Navigation cancelled\");\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tawait this.renderTreeNavigation(result);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif (summaryLoader) {\n\t\t\t\t\t\t\tsummaryLoader.stop();\n\t\t\t\t\t\t\tthis.statusContainer.clear();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tdone();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t},\n\t\t\t\t(entryId, label) => {\n\t\t\t\t\tvoid this.agentConnection\n\t\t\t\t\t\t.setSessionEntryLabel(entryId, label)\n\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch((error) => {\n\t\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\tinitialSelectedId,\n\t\t\t\tinitialFilterMode,\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector };\n\t\t});\n\t}\n\n\tprivate async handleResumeCommand(args: string): Promise<void> {\n\t\tconst selector = args.trim();\n\t\tif (!selector) {\n\t\t\tawait this.requestAgentsView();\n\t\t\treturn;\n\t\t}\n\t\tlet sessionPath: string;\n\t\ttry {\n\t\t\tsessionPath = (await resolveSessionPath(selector, this.getCurrentCwd(), this.connectionState?.sessionDir))\n\t\t\t\t.path;\n\t\t} catch (error) {\n\t\t\tif (error instanceof SessionSelectorError) {\n\t\t\t\tconst suggestion =\n\t\t\t\t\terror instanceof SessionSelectorNotFoundError && error.suggestion\n\t\t\t\t\t\t? ` Did you mean '${error.suggestion}'?`\n\t\t\t\t\t\t: \"\";\n\t\t\t\tthis.showError(`${error.message}.${suggestion}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tawait this.handleResumeSession(sessionPath);\n\t}\n\n\tprivate async handleResumeSession(\n\t\tsessionPath: string,\n\t\toptions?: Parameters<ExtensionCommandContext[\"switchSession\"]>[1],\n\t): Promise<{ cancelled: boolean }> {\n\t\tthis.stopWorkingLoader();\n\t\ttry {\n\t\t\tconst result = options?.withSession\n\t\t\t\t? await this.getLocalSessionHost().switchSession(sessionPath, {\n\t\t\t\t\t\twithSession: options.withSession,\n\t\t\t\t\t})\n\t\t\t\t: await this.agentConnection.switchSession(sessionPath);\n\t\t\tif (result.cancelled) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tawait this.renderCurrentSessionState();\n\t\t\tthis.showStatus(\"Resumed session\");\n\t\t\treturn result;\n\t\t} catch (error: unknown) {\n\t\t\tif (error instanceof MissingSessionCwdError) {\n\t\t\t\tconst selectedCwd = await this.promptForMissingSessionCwd(error);\n\t\t\t\tif (!selectedCwd) {\n\t\t\t\t\tthis.showStatus(\"Resume cancelled\");\n\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t}\n\t\t\t\tconst result = options?.withSession\n\t\t\t\t\t? await this.getLocalSessionHost().switchSession(sessionPath, {\n\t\t\t\t\t\t\tcwdOverride: selectedCwd,\n\t\t\t\t\t\t\twithSession: options.withSession,\n\t\t\t\t\t\t})\n\t\t\t\t\t: await this.agentConnection.switchSession(sessionPath, { cwdOverride: selectedCwd });\n\t\t\t\tif (result.cancelled) {\n\t\t\t\t\treturn result;\n\t\t\t\t}\n\t\t\t\tawait this.renderCurrentSessionState();\n\t\t\t\tthis.showStatus(\"Resumed session in current cwd\");\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\treturn this.handleFatalRuntimeError(\"Failed to resume session\", error);\n\t\t}\n\t}\n\n\tprivate showOnboardingSplash(continueActionLabel?: string): Promise<OnboardingSplashHandle | undefined> {\n\t\treturn new Promise((resolve) => {\n\t\t\tlet settled = false;\n\t\t\tlet dismissed = false;\n\t\t\tlet handle: OverlayHandle | undefined;\n\t\t\tlet selector: OnboardingSplashComponent | undefined;\n\t\t\tconst settle = (result: OnboardingSplashHandle | undefined) => {\n\t\t\t\tif (settled) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsettled = true;\n\t\t\t\tresolve(result);\n\t\t\t};\n\t\t\tconst dismiss = () => {\n\t\t\t\tif (dismissed) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tdismissed = true;\n\t\t\t\tselector?.dispose();\n\t\t\t\thandle?.hide();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t};\n\t\t\tselector = new OnboardingSplashComponent(\n\t\t\t\t() => {\n\t\t\t\t\tselector?.dispose();\n\t\t\t\t\tsettle({\n\t\t\t\t\t\tshowProgress: (message) => selector?.showProgress(message),\n\t\t\t\t\t\tdismiss,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tdismiss();\n\t\t\t\t\tsettle(undefined);\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tgetRows: () => this.ui.terminal.rows,\n\t\t\t\t\trequestRender: () => this.ui.requestRender(),\n\t\t\t\t\t...(continueActionLabel ? { continueActionLabel } : {}),\n\t\t\t\t},\n\t\t\t);\n\t\t\thandle = this.ui.showOverlay(selector, {\n\t\t\t\twidth: \"100%\",\n\t\t\t\tmaxHeight: \"100%\",\n\t\t\t\trow: 0,\n\t\t\t\tcol: 0,\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate createAuthFlows(): ProviderAuthFlows {\n\t\treturn new ProviderAuthFlows({\n\t\t\tui: this.ui,\n\t\t\tmodelRegistry: this.modelRegistry,\n\t\t\tshowStatus: (message) => this.showStatus(message),\n\t\t\tshowError: (message) => this.showError(message),\n\t\t\tgetAvailableModels: () => this.getConnectionAvailableModels(),\n\t\t\tonAuthChanged: async () => {\n\t\t\t\tawait this.refreshConnectionModelsAfterAuthChange();\n\t\t\t\tawait this.updateAvailableProviderCount();\n\t\t\t\tthis.footer.invalidate();\n\t\t\t\tthis.updateEditorBorderColor();\n\t\t\t},\n\t\t\tonLoginCompleted: () => {\n\t\t\t\tvoid this.maybeWarnAboutAnthropicSubscriptionAuth();\n\t\t\t},\n\t\t});\n\t}\n\n\tprivate async prepareForModelSelectionAfterLogin(authResult: AuthenticationResult): Promise<boolean> {\n\t\tconst currentModel = this.getCurrentModel();\n\t\t// The agent core uses unknown/unknown as its no-model sentinel.\n\t\tconst selectedModel =\n\t\t\tcurrentModel?.provider === \"unknown\" && currentModel.id === \"unknown\" ? undefined : currentModel;\n\t\tlet action = resolvePrimeInferencePostLoginModelAction(authResult, selectedModel, this.modelRegistry);\n\t\tif (!action.openModelPicker) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (!selectedModel) {\n\t\t\ttry {\n\t\t\t\tconst availableModels = await this.getConnectionAvailableModels();\n\t\t\t\taction = resolvePrimeInferencePostLoginModelAction(authResult, selectedModel, {\n\t\t\t\t\tfind: (provider, modelId) =>\n\t\t\t\t\t\tavailableModels.find((model) => model.provider === provider && model.id === modelId) ??\n\t\t\t\t\t\tthis.modelRegistry.find(provider, modelId),\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\t// Preserve the registry fallback so selection can still report a specific failure below.\n\t\t\t}\n\t\t}\n\n\t\tif (action.fallbackModel) {\n\t\t\ttry {\n\t\t\t\tawait this.applySelectedModel(action.fallbackModel);\n\t\t\t\tawait this.settingsManager.flush();\n\t\t\t} catch (error) {\n\t\t\t\tthis.showError(\n\t\t\t\t\t`Prime Inference login succeeded, but the default model could not be selected: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t} else if (!selectedModel) {\n\t\t\tthis.showError(\"Prime Inference login succeeded, but the default GLM 5.2 model is unavailable.\");\n\t\t}\n\n\t\treturn true;\n\t}\n\n\tprivate async handleMcpCommand(args: string | undefined): Promise<void> {\n\t\tconst argv = parseCommandArgs((args ?? \"\").trim());\n\t\tconst [sub, server] = argv;\n\t\tif (!sub) {\n\t\t\tawait this.showConfigurationMenu(\"mcp-connections\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst authStorage = this.modelRegistry.authStorage;\n\t\tconst isAuthed = (name: string) => authStorage.get(`mcp:${name}`) !== undefined;\n\t\tif (sub === \"login\") {\n\t\t\tif (!server || argv.length !== 2) {\n\t\t\t\tthis.showError(\"Usage: /mcp login <name> (e.g. /mcp login linear)\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst result = await this.createAuthFlows().runMcpLogin(server);\n\t\t\tif (result.status === \"success\") await this.reloadAfterMcpChange(`Connected ${server}.`);\n\t\t\treturn;\n\t\t}\n\n\t\tif (sub === \"logout\") {\n\t\t\tif (!server || argv.length !== 2) {\n\t\t\t\tthis.showError(\"Usage: /mcp logout <name>\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!isAuthed(server)) {\n\t\t\t\tthis.showStatus(`${server} is not connected.`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauthStorage.logout(`mcp:${server}`);\n\t\t\tawait this.reloadAfterMcpChange(`Disconnected ${server}.`);\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await runMcpManagementCommand(argv, this.settingsManager, this.modelRegistry.authStorage);\n\t\t\tif (result.changed && result.serverChange) {\n\t\t\t\tconst { name, transport, verb, usesOAuth } = result.serverChange;\n\t\t\t\tconst hasMcpProviderRefresh = this.uiServices.refreshMcpProviders !== undefined;\n\t\t\t\tthis.uiServices.refreshMcpProviders?.();\n\t\t\t\tconst successMessage =\n\t\t\t\t\tverb === \"removed\"\n\t\t\t\t\t\t? `Removed MCP server \"${name}\" (${transport}). It is no longer available through mcp.`\n\t\t\t\t\t\t: usesOAuth\n\t\t\t\t\t\t\t? `${verb === \"replaced\" ? \"Replaced\" : \"Added\"} MCP server \"${name}\" (${transport}). ${hasMcpProviderRefresh ? \"Run\" : \"Restart ASM Agent, then run\"} /mcp login ${name} to connect.`\n\t\t\t\t\t\t\t: `${verb === \"replaced\" ? \"Replaced\" : \"Added\"} MCP server \"${name}\" (${transport}). Available next turn through mcp.`;\n\t\t\t\tawait this.reloadAfterMcpChange(usesOAuth ? successMessage : result.message, successMessage);\n\t\t\t} else if (result.action === \"list\") {\n\t\t\t\tconst builtins = BUILTIN_MCP_CATALOG.map(\n\t\t\t\t\t(entry) => `${entry.label} (${entry.server}): ${isAuthed(entry.server) ? \"connected\" : \"not connected\"}`,\n\t\t\t\t).join(\"\\n\");\n\t\t\t\tthis.showStatus(\n\t\t\t\t\t`Built-in MCP integrations:\\n${builtins}\\n\\nUser-configured MCP servers:\\n${result.message}`,\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tthis.showStatus(result.message);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate async reloadAfterMcpChange(message: string, successMessage = message): Promise<void> {\n\t\tif (this.isAgentStreaming() || this.isAgentCompacting()) {\n\t\t\tthis.showStatus(`${message} The change was saved. Run /reload after the current turn to activate it.`);\n\t\t\treturn;\n\t\t}\n\t\tconst reloaded = await this.handleReloadCommand();\n\t\tif (reloaded) {\n\t\t\tthis.showStatus(successMessage);\n\t\t} else {\n\t\t\tthis.showWarning(`${message} The change remains saved, but it is not active in this session.`);\n\t\t}\n\t}\n\n\tprivate async showLogoutSelector(): Promise<void> {\n\t\t// Only reload when an MCP integration was actually removed (its skill must\n\t\t// be disabled); a cancelled or non-MCP logout needs no reload.\n\t\tconst loggedOut = await this.createAuthFlows().runLogout();\n\t\tif (loggedOut?.startsWith(\"mcp:\")) {\n\t\t\tawait this.handleReloadCommand();\n\t\t}\n\t}\n\n\tprivate async handleUpdateCommand(args: string): Promise<void> {\n\t\tconst entrypoint = process.argv[1];\n\t\tif (!entrypoint) {\n\t\t\tthis.showError(\"Cannot determine current CLI entrypoint for update\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst updateArgs = parseCommandArgs(args);\n\t\tconst includesSelf = updateArgsIncludeSelf(updateArgs);\n\t\tconst updateCwd = this.getCurrentCwd();\n\t\tconst daemonSocketPath = resolveInteractiveUpdateDaemonSocketPath(\n\t\t\tupdateArgs,\n\t\t\tresolveDaemonUpdateRestartSocketPath(this.options.daemonSocketPath),\n\t\t);\n\t\tconst updateChildArgs = includesSelf ? buildUpdateChildArgs(updateArgs, daemonSocketPath) : updateArgs;\n\t\tthis.stopWorkingLoader();\n\t\tawait this.ui.terminal.drainInput(1000).catch(() => undefined);\n\t\tthis.ui.stop();\n\n\t\tconst updateEnv = includesSelf ? { ...process.env, [SELF_UPDATE_INTERACTIVE_CHILD_ENV]: \"1\" } : process.env;\n\t\tconst updateResult = spawnSync(\n\t\t\tprocess.execPath,\n\t\t\t[...process.execArgv, entrypoint, \"update\", ...updateChildArgs],\n\t\t\t{\n\t\t\t\tstdio: \"inherit\",\n\t\t\t\tcwd: updateCwd,\n\t\t\t\tenv: updateEnv,\n\t\t\t},\n\t\t);\n\t\tconst updateExitCode = updateResult.status ?? (updateResult.signal ? 1 : 0);\n\t\tconst selfUpdateNotAttempted =\n\t\t\tincludesSelf && !updateResult.error && updateExitCode === SELF_UPDATE_NOT_ATTEMPTED_EXIT_CODE;\n\n\t\tif (includesSelf && !selfUpdateNotAttempted) {\n\t\t\tconst relaunchArgs = buildUpdateRelaunchArgs(process.argv.slice(2), this.connectionState?.sessionFile);\n\t\t\tif (updateResult.error) {\n\t\t\t\tconsole.error(`Update failed: ${updateResult.error.message}`);\n\t\t\t\tconsole.error(`Relaunching ${APP_NAME}...`);\n\t\t\t} else if (updateExitCode !== 0) {\n\t\t\t\tconsole.error(\n\t\t\t\t\tupdateResult.signal\n\t\t\t\t\t\t? `Update terminated by signal ${updateResult.signal}`\n\t\t\t\t\t\t: `Update exited with code ${updateExitCode}`,\n\t\t\t\t);\n\t\t\t\tconsole.error(`Relaunching ${APP_NAME}...`);\n\t\t\t}\n\t\t\tthis.stop();\n\t\t\tawait this.agentConnection.dispose().catch(() => undefined);\n\t\t\ttry {\n\t\t\t\tawait this.options.onShutdown?.();\n\t\t\t} catch {\n\t\t\t\t// The update already completed; do not block relaunch on local teardown.\n\t\t\t}\n\t\t\tif (!updateResult.error && updateExitCode === 0) {\n\t\t\t\ttry {\n\t\t\t\t\tconst status = await launchDaemonUpdateRestartCoordinator({\n\t\t\t\t\t\tsocketPath: daemonSocketPath,\n\t\t\t\t\t\tagentDir: getAgentDir(),\n\t\t\t\t\t\tcwd: updateCwd,\n\t\t\t\t\t\toriginActiveSessionId: this.connectionState?.activeSessionId,\n\t\t\t\t\t});\n\t\t\t\t\tconst report = buildDaemonUpdateRestartReport(status);\n\t\t\t\t\tfor (const message of report.info) {\n\t\t\t\t\t\tconsole.log(message);\n\t\t\t\t\t}\n\t\t\t\t\tfor (const warning of report.warnings) {\n\t\t\t\t\t\tconsole.error(`Warning: ${warning}`);\n\t\t\t\t\t}\n\t\t\t\t} catch (error: unknown) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`Warning: updated, but could not coordinate the daemon restart (${error instanceof Error ? error.message : String(error)}).`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst relaunchResult = spawnSync(process.execPath, [...process.execArgv, entrypoint, ...relaunchArgs], {\n\t\t\t\tstdio: \"inherit\",\n\t\t\t\tcwd: updateCwd,\n\t\t\t\tenv: process.env,\n\t\t\t});\n\t\t\tif (relaunchResult.error) {\n\t\t\t\tconsole.error(`Failed to relaunch ${APP_NAME}: ${relaunchResult.error.message}`);\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t\tprocess.exit(relaunchResult.status ?? (relaunchResult.signal ? 1 : 0));\n\t\t}\n\n\t\tthis.ui.start();\n\t\tif (this.fullscreenEnabled) {\n\t\t\tthis.applyFullscreen(true);\n\t\t}\n\t\tthis.ui.requestRender(true);\n\n\t\tif (selfUpdateNotAttempted) {\n\t\t\tthis.showStatus(`Update did not change ${APP_NAME}. Reloading resources...`);\n\t\t\tawait this.handleReloadCommand();\n\t\t\treturn;\n\t\t}\n\t\tif (updateResult.error) {\n\t\t\tthis.showError(`Update failed: ${updateResult.error.message}`);\n\t\t\treturn;\n\t\t}\n\t\tif (updateExitCode !== 0) {\n\t\t\tthis.showError(\n\t\t\t\tupdateResult.signal\n\t\t\t\t\t? `Update terminated by signal ${updateResult.signal}`\n\t\t\t\t\t: `Update exited with code ${updateExitCode}`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tthis.showStatus(\"Packages updated. Reloading resources...\");\n\t\tawait this.handleReloadCommand();\n\t}\n\n\tprivate async handleReloadCommand(): Promise<boolean> {\n\t\tif (this.isAgentStreaming()) {\n\t\t\tthis.showWarning(\"Wait for the current response to finish before reloading.\");\n\t\t\treturn false;\n\t\t}\n\t\tif (this.isAgentCompacting()) {\n\t\t\tthis.showWarning(\"Wait for compaction to finish before reloading.\");\n\t\t\treturn false;\n\t\t}\n\n\t\tthis.resetExtensionUI();\n\n\t\tconst reloadBox = new Container();\n\t\tconst borderColor = (s: string) => theme.fg(\"border\", s);\n\t\treloadBox.addChild(new DynamicBorder(borderColor));\n\t\treloadBox.addChild(new Spacer(1));\n\t\treloadBox.addChild(\n\t\t\tnew Text(theme.fg(\"muted\", \"Reloading keybindings, extensions, skills, prompts, themes...\"), 1, 0),\n\t\t);\n\t\treloadBox.addChild(new Spacer(1));\n\t\treloadBox.addChild(new DynamicBorder(borderColor));\n\n\t\tconst previousEditor = this.editor;\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(reloadBox);\n\t\tthis.ui.setFocus(reloadBox);\n\t\tthis.ui.requestRender(true);\n\t\tawait new Promise((resolve) => process.nextTick(resolve));\n\n\t\tconst dismissReloadBox = (editor: Component) => {\n\t\t\tthis.editorContainer.clear();\n\t\t\tthis.editorContainer.addChild(editor);\n\t\t\tthis.ui.setFocus(editor);\n\t\t\tthis.ui.requestRender();\n\t\t};\n\n\t\ttry {\n\t\t\tawait this.agentConnection.reload();\n\t\t\tthis.toolDefinitionCache.clear();\n\t\t\tthis.keybindings.reload();\n\t\t\tconst activeHeader = this.customHeader ?? this.builtInHeader;\n\t\t\tif (isExpandable(activeHeader)) {\n\t\t\t\tactiveHeader.setExpanded(this.toolOutputExpanded);\n\t\t\t}\n\t\t\tsetRegisteredThemes(this.uiServices.getThemes());\n\t\t\tthis.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();\n\t\t\tconst themeName = this.settingsManager.getTheme();\n\t\t\tconst themeResult = themeName ? setTheme(themeName, true) : { success: true };\n\t\t\tif (!themeResult.success) {\n\t\t\t\tthis.showError(`Failed to load theme \"${themeName}\": ${themeResult.error}\\nFell back to dark theme.`);\n\t\t\t}\n\t\t\tconst editorPaddingX = this.settingsManager.getEditorPaddingX();\n\t\t\tconst autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();\n\t\t\tthis.defaultEditor.setPaddingX(editorPaddingX);\n\t\t\tthis.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);\n\t\t\tif (this.editor !== this.defaultEditor) {\n\t\t\t\tthis.editor.setPaddingX?.(editorPaddingX);\n\t\t\t\tthis.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);\n\t\t\t}\n\t\t\tthis.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());\n\t\t\tthis.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());\n\t\t\tawait this.refreshConnectionCatalog();\n\t\t\tthis.setupAutocompleteProvider();\n\t\t\tif (this.bindLocalSessionExtensions) {\n\t\t\t\tconst runner = this.getLocalSessionHost().getExtensionRunner();\n\t\t\t\tthis.setupExtensionShortcuts(runner);\n\t\t\t}\n\t\t\tawait this.rebuildChatFromMessages();\n\t\t\tdismissReloadBox(this.editor as Component);\n\t\t\tthis.showLoadedResources({\n\t\t\t\tforce: false,\n\t\t\t\tshowDiagnosticsWhenQuiet: true,\n\t\t\t});\n\t\t\tconst modelsJsonError = this.modelRegistry.getError();\n\t\t\tif (modelsJsonError) {\n\t\t\t\tthis.showError(`models.json error: ${modelsJsonError}`);\n\t\t\t}\n\t\t\tthis.showStatus(\"Reloaded keybindings, extensions, skills, prompts, themes\");\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tdismissReloadBox(previousEditor as Component);\n\t\t\tthis.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate async handleExportCommand(text: string): Promise<void> {\n\t\tconst outputPath = this.getPathCommandArgument(text, \"/export\");\n\n\t\ttry {\n\t\t\tif (outputPath?.endsWith(\".jsonl\")) {\n\t\t\t\tconst filePath = await this.agentConnection.exportToJsonl(outputPath);\n\t\t\t\tthis.showStatus(`Session exported to: ${filePath}`);\n\t\t\t} else {\n\t\t\t\tconst filePath = await this.agentConnection.exportToHtml(outputPath);\n\t\t\t\tthis.showStatus(`Session exported to: ${filePath}`);\n\t\t\t}\n\t\t} catch (error: unknown) {\n\t\t\tthis.showError(`Failed to export session: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t}\n\t}\n\n\tprivate getPathCommandArgument(text: string, command: \"/export\" | \"/import\"): string | undefined {\n\t\tif (text === command) {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (!text.startsWith(`${command} `)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst argsString = text.slice(command.length + 1).trimStart();\n\t\tif (!argsString) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst firstChar = argsString[0];\n\t\tif (firstChar === '\"' || firstChar === \"'\") {\n\t\t\tconst closingQuoteIndex = argsString.indexOf(firstChar, 1);\n\t\t\tif (closingQuoteIndex < 0) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\treturn argsString.slice(1, closingQuoteIndex);\n\t\t}\n\n\t\tconst firstWhitespaceIndex = argsString.search(/\\s/);\n\t\tif (firstWhitespaceIndex < 0) {\n\t\t\treturn argsString;\n\t\t}\n\t\treturn argsString.slice(0, firstWhitespaceIndex);\n\t}\n\n\tprivate async handleImportCommand(text: string): Promise<void> {\n\t\tconst inputPath = this.getPathCommandArgument(text, \"/import\");\n\t\tif (!inputPath) {\n\t\t\tthis.showError(\"Usage: /import <path.jsonl>\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst confirmed = await this.showExtensionConfirm(\"Import session\", `Replace current session with ${inputPath}?`);\n\t\tif (!confirmed) {\n\t\t\tthis.showStatus(\"Import cancelled\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.stopWorkingLoader();\n\t\t\tconst result = await this.agentConnection.importFromJsonl(inputPath);\n\t\t\tif (result.cancelled) {\n\t\t\t\tthis.showStatus(\"Import cancelled\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait this.renderCurrentSessionState();\n\t\t\tthis.showStatus(`Session imported from: ${inputPath}`);\n\t\t} catch (error: unknown) {\n\t\t\tif (error instanceof MissingSessionCwdError) {\n\t\t\t\tconst selectedCwd = await this.promptForMissingSessionCwd(error);\n\t\t\t\tif (!selectedCwd) {\n\t\t\t\t\tthis.showStatus(\"Import cancelled\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst result = await this.agentConnection.importFromJsonl(inputPath, selectedCwd);\n\t\t\t\tif (result.cancelled) {\n\t\t\t\t\tthis.showStatus(\"Import cancelled\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait this.renderCurrentSessionState();\n\t\t\t\tthis.showStatus(`Session imported from: ${inputPath}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (error instanceof SessionImportFileNotFoundError) {\n\t\t\t\tthis.showError(`Failed to import session: ${error.message}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait this.handleFatalRuntimeError(\"Failed to import session\", error);\n\t\t}\n\t}\n\n\tprivate async handleShareCommand(): Promise<void> {\n\t\t// Check if gh is available and logged in\n\t\ttry {\n\t\t\tconst authResult = spawnSync(\"gh\", [\"auth\", \"status\"], { encoding: \"utf-8\" });\n\t\t\tif (authResult.status !== 0) {\n\t\t\t\tthis.showError(\"GitHub CLI is not logged in. Run 'gh auth login' first.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch {\n\t\t\tthis.showError(\"GitHub CLI (gh) is not installed. Install it from https://cli.github.com/\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Export to a temp file\n\t\tconst tmpFile = path.join(os.tmpdir(), \"session.html\");\n\t\ttry {\n\t\t\tawait this.agentConnection.exportToHtml(tmpFile);\n\t\t} catch (error: unknown) {\n\t\t\tthis.showError(`Failed to export session: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t\treturn;\n\t\t}\n\n\t\t// Show cancellable loader, replacing the editor\n\t\tconst loader = new BorderedLoader(this.ui, theme, \"Creating gist...\");\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(loader);\n\t\tthis.ui.setFocus(loader);\n\t\tthis.ui.requestRender();\n\n\t\tconst restoreEditor = () => {\n\t\t\tloader.dispose();\n\t\t\tthis.editorContainer.clear();\n\t\t\tthis.editorContainer.addChild(this.editor);\n\t\t\tthis.ui.setFocus(this.editor);\n\t\t\ttry {\n\t\t\t\tfs.unlinkSync(tmpFile);\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t};\n\n\t\t// Create a secret gist asynchronously\n\t\tlet proc: ReturnType<typeof spawn> | null = null;\n\n\t\tloader.onAbort = () => {\n\t\t\tproc?.kill();\n\t\t\trestoreEditor();\n\t\t\tthis.showStatus(\"Share cancelled\");\n\t\t};\n\n\t\ttry {\n\t\t\tconst result = await new Promise<{ stdout: string; stderr: string; code: number | null }>((resolve) => {\n\t\t\t\tproc = spawn(\"gh\", [\"gist\", \"create\", \"--public=false\", tmpFile]);\n\t\t\t\tlet stdout = \"\";\n\t\t\t\tlet stderr = \"\";\n\t\t\t\tproc.stdout?.on(\"data\", (data) => {\n\t\t\t\t\tstdout += data.toString();\n\t\t\t\t});\n\t\t\t\tproc.stderr?.on(\"data\", (data) => {\n\t\t\t\t\tstderr += data.toString();\n\t\t\t\t});\n\t\t\t\tproc.on(\"close\", (code) => resolve({ stdout, stderr, code }));\n\t\t\t});\n\n\t\t\tif (loader.signal.aborted) return;\n\n\t\t\trestoreEditor();\n\n\t\t\tif (result.code !== 0) {\n\t\t\t\tconst errorMsg = result.stderr?.trim() || \"Unknown error\";\n\t\t\t\tthis.showError(`Failed to create gist: ${errorMsg}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Extract gist ID from the URL returned by gh\n\t\t\t// gh returns something like: https://gist.github.com/username/GIST_ID\n\t\t\tconst gistUrl = result.stdout?.trim();\n\t\t\tconst gistId = gistUrl?.split(\"/\").pop();\n\t\t\tif (!gistId) {\n\t\t\t\tthis.showError(\"Failed to parse gist ID from gh output\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Create the preview URL\n\t\t\tconst previewUrl = getShareViewerUrl(gistId);\n\t\t\tthis.showStatus(`Share URL: ${previewUrl}\\nGist: ${gistUrl}`);\n\t\t} catch (error: unknown) {\n\t\t\tif (!loader.signal.aborted) {\n\t\t\t\trestoreEditor();\n\t\t\t\tthis.showError(`Failed to create gist: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async handleCopyCommand(): Promise<void> {\n\t\tconst text = await this.agentConnection.getLastAssistantText();\n\t\tif (!text) {\n\t\t\tthis.showError(\"No agent messages to copy yet.\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tawait copyToClipboard(text);\n\t\t\tthis.showStatus(\"Copied last agent message to clipboard\");\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate async handleNameCommand(text: string): Promise<void> {\n\t\tconst name = text.replace(/^\\/name\\s*/, \"\").trim();\n\t\tif (!name) {\n\t\t\tconst currentName = this.getCurrentSessionName();\n\t\t\tif (currentName) {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", `Session name: ${currentName}`), 1, 0));\n\t\t\t} else {\n\t\t\t\tthis.showWarning(\"Usage: /name <name>\");\n\t\t\t}\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tawait this.agentConnection.setSessionName(name);\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", `Session name set: ${name}`), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async handleRlmMaxDepthCommand(args: string): Promise<void> {\n\t\tconst tokens = args ? args.split(/\\s+/) : [];\n\t\tif (tokens.length === 0) {\n\t\t\ttry {\n\t\t\t\tconst status = await this.agentConnection.getRlmMaxDepthStatus();\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(theme.fg(\"dim\", `RLM max depth: ${status.maxDepth} (${status.source})`), 1, 0),\n\t\t\t\t);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t} catch (error) {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst global = tokens[1] === \"--global\";\n\t\tif (tokens.length > (global ? 2 : 1) || !/^\\d+$/.test(tokens[0] ?? \"\")) {\n\t\t\tthis.showWarning(\"Usage: /rlm-max-depth [<non-negative integer> [--global]]\");\n\t\t\treturn;\n\t\t}\n\t\tconst maxDepth = Number(tokens[0]);\n\t\tif (!Number.isSafeInteger(maxDepth)) {\n\t\t\tthis.showWarning(\"RLM max depth must be a non-negative integer.\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await this.agentConnection.setRlmMaxDepth(maxDepth, { global });\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t`RLM max depth set: ${result.maxDepth}${result.globalSaved ? \" and saved as global default\" : \"\"}`,\n\t\t\t\t\t),\n\t\t\t\t\t1,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.ui.requestRender();\n\t\t\tif (result.globalError) {\n\t\t\t\tthis.showError(\n\t\t\t\t\t`RLM max depth set for this chat, but the global default was not saved: ${result.globalError}`,\n\t\t\t\t);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate async handleSessionCommand(): Promise<void> {\n\t\tconst stats = await this.agentConnection.getSessionStats();\n\t\tconst sessionName = this.getCurrentSessionName();\n\n\t\tlet info = `${theme.bold(\"Session Info\")}\\n\\n`;\n\t\tif (sessionName) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Name:\")} ${sessionName}\\n`;\n\t\t}\n\t\tinfo += `${theme.fg(\"dim\", \"File:\")} ${stats.sessionFile ?? \"In-memory\"}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"ID:\")} ${stats.sessionId}\\n\\n`;\n\t\tinfo += `${theme.bold(\"Messages\")}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"User:\")} ${stats.userMessages}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Assistant:\")} ${stats.assistantMessages}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Tool Calls:\")} ${stats.toolCalls}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Tool Results:\")} ${stats.toolResults}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${stats.totalMessages}\\n\\n`;\n\t\tinfo += theme.fg(\"dim\", \"Use /context for token, cost, and context usage.\");\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(info, 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleLogsCommand(): void {\n\t\tconst logsDir = getLogsDir();\n\t\tlet info = `${theme.bold(\"Logs\")}\\n\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Directory:\")} ${logsDir}\\n\\n`;\n\n\t\tlet files: string[] = [];\n\t\ttry {\n\t\t\tif (fs.existsSync(logsDir)) {\n\t\t\t\tfiles = fs.readdirSync(logsDir).filter((name) => !name.startsWith(\".\"));\n\t\t\t}\n\t\t} catch {\n\t\t\t// Fall through to the empty-state line below.\n\t\t}\n\t\tif (files.length === 0) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"No logs written yet.\")}\\n`;\n\t\t} else {\n\t\t\tfor (const name of files.sort()) {\n\t\t\t\tlet size = \"\";\n\t\t\t\ttry {\n\t\t\t\t\tsize = ` ${theme.fg(\"dim\", `(${(fs.statSync(path.join(logsDir, name)).size / 1024).toFixed(1)} KB)`)}`;\n\t\t\t\t} catch {\n\t\t\t\t\t// Skip the size if the file vanished between readdir and stat.\n\t\t\t\t}\n\t\t\t\tinfo += `${theme.fg(\"dim\", \"•\")} ${name}${size}\\n`;\n\t\t\t}\n\t\t}\n\t\tinfo += `\\n${theme.fg(\"dim\", \"Daemon crashes log to <socket>.log; agent-open failures log to client-errors.log.\")}`;\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(info, 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async handleSystemPromptCommand(): Promise<void> {\n\t\tconst prompt = await this.agentConnection.getSystemPrompt();\n\t\tconst header = `${theme.bold(\"System Prompt\")} ${theme.fg(\"dim\", `(${prompt.length} chars)`)}`;\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(header, 1, 0));\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(prompt, 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async handleContextCommand(): Promise<void> {\n\t\tlet info: string;\n\t\ttry {\n\t\t\tconst tree = await this.agentConnection.getContextTree();\n\t\t\tconst width = Math.max(60, Math.min(this.ui.terminal.columns - 2, 120));\n\t\t\tinfo = formatContextTree(tree, width);\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\treturn;\n\t\t}\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(info, 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async handleHeartbeatCommand(text: string): Promise<void> {\n\t\ttry {\n\t\t\tconst command = parseHeartbeatCommand(text);\n\t\t\tswitch (command.type) {\n\t\t\t\tcase \"status\": {\n\t\t\t\t\tconst heartbeat = await this.agentConnection.getHeartbeat();\n\t\t\t\t\tthis.patchConnectionState({ heartbeat: heartbeat ?? null });\n\t\t\t\t\tawait this.refreshHeartbeatCatalog();\n\t\t\t\t\tthis.showHeartbeat(heartbeat);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcase \"set\": {\n\t\t\t\t\tconst heartbeat = await this.agentConnection.setHeartbeat(\n\t\t\t\t\t\tcommand.schedule,\n\t\t\t\t\t\tcommand.instruction,\n\t\t\t\t\t\tcommand.deliveryMode,\n\t\t\t\t\t);\n\t\t\t\t\tthis.patchConnectionState({ heartbeat });\n\t\t\t\t\tawait this.refreshHeartbeatCatalog();\n\t\t\t\t\tthis.showStatus(\n\t\t\t\t\t\t`Heartbeat set\\nDelivery: ${heartbeat.deliveryMode ?? DEFAULT_HEARTBEAT_DELIVERY_MODE}\\nNext run: ${heartbeat.nextRunAt ?? \"-\"}`,\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcase \"pause\": {\n\t\t\t\t\tconst heartbeat = await this.agentConnection.updateHeartbeat(\"pause\");\n\t\t\t\t\tif (!heartbeat) {\n\t\t\t\t\t\tthis.showStatus(\"No active heartbeat\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.patchConnectionState({ heartbeat });\n\t\t\t\t\tawait this.refreshHeartbeatCatalog();\n\t\t\t\t\tthis.showStatus(\"Heartbeat paused\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcase \"resume\": {\n\t\t\t\t\tconst heartbeat = await this.agentConnection.updateHeartbeat(\"resume\");\n\t\t\t\t\tif (!heartbeat) {\n\t\t\t\t\t\tthis.showStatus(\"No active heartbeat\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.patchConnectionState({ heartbeat });\n\t\t\t\t\tawait this.refreshHeartbeatCatalog();\n\t\t\t\t\tthis.showStatus(`Heartbeat resumed\\nNext run: ${heartbeat.nextRunAt ?? \"-\"}`);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcase \"clear\": {\n\t\t\t\t\tconst heartbeat = await this.agentConnection.updateHeartbeat(\"clear\");\n\t\t\t\t\tif (!heartbeat) {\n\t\t\t\t\t\tthis.showStatus(\"No active heartbeat\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.patchConnectionState({ heartbeat: null });\n\t\t\t\t\tawait this.refreshHeartbeatCatalog();\n\t\t\t\t\tthis.showStatus(\"Heartbeat cleared\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate async showHeartbeatManager(): Promise<void> {\n\t\tif (this.heartbeatManagerHandle) {\n\t\t\tthis.heartbeatManagerHandle.focus();\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tawait this.refreshHeartbeatCatalog();\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\treturn;\n\t\t}\n\t\tconst manager = new HeartbeatManagerComponent(this.heartbeats, {\n\t\t\tgetRows: () => this.ui.terminal.rows,\n\t\t\tonAction: (heartbeat, action) => this.manageHeartbeat(heartbeat, action),\n\t\t\tonClose: () => this.closeHeartbeatManager(),\n\t\t\trequestRender: () => this.ui.requestRender(),\n\t\t});\n\t\tthis.heartbeatManager = manager;\n\t\tthis.heartbeatManagerHandle = this.showFullPaneOverlay(manager, {\n\t\t\tfullWidth: true,\n\t\t\tsuspendFullscreenMouse: true,\n\t\t});\n\t\tthis.scheduleHeartbeatManagerRefresh();\n\t}\n\n\tprivate closeHeartbeatManager(): void {\n\t\tif (this.heartbeatManagerRefreshTimer) {\n\t\t\tclearTimeout(this.heartbeatManagerRefreshTimer);\n\t\t\tthis.heartbeatManagerRefreshTimer = undefined;\n\t\t}\n\t\tthis.heartbeatManagerHandle?.hide();\n\t\tthis.heartbeatManagerHandle = undefined;\n\t\tthis.heartbeatManager = undefined;\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate scheduleHeartbeatManagerRefresh(): void {\n\t\tif (this.heartbeatManagerRefreshTimer) {\n\t\t\tclearTimeout(this.heartbeatManagerRefreshTimer);\n\t\t\tthis.heartbeatManagerRefreshTimer = undefined;\n\t\t}\n\t\tif (!this.heartbeatManager) {\n\t\t\treturn;\n\t\t}\n\t\tconst nextRunAt = this.heartbeats\n\t\t\t.filter((heartbeat) => heartbeat.job.status === \"active\" && heartbeat.job.nextRunAt)\n\t\t\t.map((heartbeat) => Date.parse(heartbeat.job.nextRunAt!))\n\t\t\t.filter(Number.isFinite)\n\t\t\t.sort((left, right) => left - right)[0];\n\t\tif (nextRunAt === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tconst untilNextRun = nextRunAt - Date.now();\n\t\tconst delay = untilNextRun > 0 ? Math.min(60_000, untilNextRun + 250) : 5_000;\n\t\tthis.heartbeatManagerRefreshTimer = setTimeout(() => {\n\t\t\tthis.heartbeatManagerRefreshTimer = undefined;\n\t\t\tif (!this.heartbeatManager) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvoid this.refreshHeartbeatCatalog().catch(() => this.scheduleHeartbeatManagerRefresh());\n\t\t}, delay);\n\t\tthis.heartbeatManagerRefreshTimer.unref?.();\n\t}\n\n\tprivate async manageHeartbeat(\n\t\theartbeat: AgentConnectionHeartbeat,\n\t\taction: AgentHeartbeatManagementAction,\n\t): Promise<void> {\n\t\tconst updated = await this.agentConnection.manageHeartbeat(\n\t\t\theartbeat.job.activeSessionId,\n\t\t\theartbeat.job.id,\n\t\t\taction,\n\t\t);\n\t\tif (updated.source === \"heartbeat\" && updated.activeSessionId === this.connectionState?.activeSessionId) {\n\t\t\tthis.patchConnectionState({ heartbeat: action === \"stop\" ? null : updated });\n\t\t}\n\t\tconst remaining = this.heartbeatCatalog.filter((entry) => entry.job.id !== updated.id);\n\t\tthis.applyHeartbeatCatalog(\n\t\t\tupdated.status === \"active\" || updated.status === \"paused\"\n\t\t\t\t? [...remaining, { ...heartbeat, job: updated }]\n\t\t\t\t: remaining,\n\t\t);\n\t\tvoid this.refreshHeartbeatCatalog().catch(() => undefined);\n\t}\n\n\tprivate showHeartbeat(job: AgentCronJob | undefined): void {\n\t\tif (!job) {\n\t\t\tthis.showStatus(\"No active heartbeat\");\n\t\t\treturn;\n\t\t}\n\t\tconst next = job.nextRunAt ? new Date(job.nextRunAt).toLocaleString() : \"-\";\n\t\tconst last = job.lastRunAt ? new Date(job.lastRunAt).toLocaleString() : \"-\";\n\t\tconst lines = [\n\t\t\ttheme.bold(\"Heartbeat\"),\n\t\t\t\"\",\n\t\t\t`${theme.fg(\"dim\", \"Status:\")} ${job.status}`,\n\t\t\t`${theme.fg(\"dim\", \"Every:\")} ${job.schedule.expression}`,\n\t\t\t`${theme.fg(\"dim\", \"Delivery:\")} ${job.deliveryMode ?? DEFAULT_HEARTBEAT_DELIVERY_MODE}`,\n\t\t\t`${theme.fg(\"dim\", \"Instruction:\")} ${job.prompt}`,\n\t\t\t`${theme.fg(\"dim\", \"Next:\")} ${next}`,\n\t\t\t`${theme.fg(\"dim\", \"Last:\")} ${last}`,\n\t\t\t`${theme.fg(\"dim\", \"Runs:\")} ${job.runCount}`,\n\t\t];\n\t\tif (job.lastError) {\n\t\t\tlines.push(`${theme.fg(\"dim\", \"Error:\")} ${job.lastError}`);\n\t\t}\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(lines.join(\"\\n\"), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleChangelogCommand(): void {\n\t\tconst changelogPath = getChangelogPath();\n\t\tconst allEntries = parseChangelog(changelogPath);\n\n\t\tconst changelogMarkdown =\n\t\t\tallEntries.length > 0\n\t\t\t\t? allEntries\n\t\t\t\t\t\t.reverse()\n\t\t\t\t\t\t.map((e) => e.content)\n\t\t\t\t\t\t.join(\"\\n\\n\")\n\t\t\t\t: \"No changelog entries found.\";\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new DynamicBorder());\n\t\tthis.chatContainer.addChild(new Text(theme.bold(theme.fg(\"accent\", \"What's New\")), 1, 0));\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, this.getMarkdownThemeWithSettings()));\n\t\tthis.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate capitalizeKey(key: string): string {\n\t\treturn key\n\t\t\t.split(\"/\")\n\t\t\t.map((k) =>\n\t\t\t\tk\n\t\t\t\t\t.split(\"+\")\n\t\t\t\t\t.map((part) => (part === \"esc\" ? part : part.charAt(0).toUpperCase() + part.slice(1)))\n\t\t\t\t\t.join(\"+\"),\n\t\t\t)\n\t\t\t.join(\"/\");\n\t}\n\n\tprivate getAppKeyDisplay(action: AppKeybinding): string {\n\t\treturn this.capitalizeKey(keyText(action));\n\t}\n\n\tprivate getEditorKeyDisplay(action: Keybinding): string {\n\t\treturn this.capitalizeKey(keyText(action));\n\t}\n\n\tprivate getShortcutGuide(): string {\n\t\tconst tab = this.getEditorKeyDisplay(\"tui.input.tab\");\n\t\tconst newLine = this.getEditorKeyDisplay(\"tui.input.newLine\");\n\t\tconst clearInput = this.getAppKeyDisplay(\"app.input.clear\");\n\t\tconst shortcutsKey = this.getAppKeyDisplay(\"app.shortcuts\");\n\t\tconst selectModel = this.getAppKeyDisplay(\"app.model.select\");\n\t\tconst expandTools = this.getAppKeyDisplay(\"app.tools.expand\");\n\t\tconst expandMessages = this.getAppKeyDisplay(\"app.messages.expand\");\n\t\tconst expandEdits = this.getAppKeyDisplay(\"app.edits.expand\");\n\t\tconst toggleThinking = this.getAppKeyDisplay(\"app.thinking.toggle\");\n\t\tconst externalEditor = this.getAppKeyDisplay(\"app.editor.external\");\n\t\tconst promptStash = this.getAppKeyDisplay(\"app.prompt.stash\");\n\t\tconst pasteImage = this.getAppKeyDisplay(\"app.clipboard.pasteImage\");\n\n\t\treturn `\n**Prompt**\n\\`!\\` shell mode · \\`/\\` commands · \\`@\\` file paths\n\\`${tab}\\` complete paths · \\`${newLine}\\` new line\n\\`${clearInput}\\` interrupt · press twice to rewind or clear the prompt\n\n**Controls**\n\\`${selectModel}\\` select model · \\`/effort\\` set reasoning · \\`${expandTools}\\` tool output\n\\`${expandMessages}\\` agent messages · \\`${expandEdits}\\` edit diffs · \\`${toggleThinking}\\` thinking blocks · \\`${promptStash}\\` stash prompt · \\`${externalEditor}\\` edit in \\`$EDITOR\\`\n\\`${pasteImage}\\` paste image\n\n**Help**\n${shortcutsKey ? `\\`${shortcutsKey}\\` quick shortcuts · ` : \"\"}\\`/hotkeys\\` full reference\n`;\n\t}\n\n\tprivate getHotkeysGuide(): string {\n\t\tconst cursorUp = this.getEditorKeyDisplay(\"tui.editor.cursorUp\");\n\t\tconst cursorDown = this.getEditorKeyDisplay(\"tui.editor.cursorDown\");\n\t\tconst cursorLeft = this.getEditorKeyDisplay(\"tui.editor.cursorLeft\");\n\t\tconst cursorRight = this.getEditorKeyDisplay(\"tui.editor.cursorRight\");\n\t\tconst cursorWordLeft = this.getEditorKeyDisplay(\"tui.editor.cursorWordLeft\");\n\t\tconst cursorWordRight = this.getEditorKeyDisplay(\"tui.editor.cursorWordRight\");\n\t\tconst cursorLineStart = this.getEditorKeyDisplay(\"tui.editor.cursorLineStart\");\n\t\tconst cursorLineEnd = this.getEditorKeyDisplay(\"tui.editor.cursorLineEnd\");\n\t\tconst jumpForward = this.getEditorKeyDisplay(\"tui.editor.jumpForward\");\n\t\tconst jumpBackward = this.getEditorKeyDisplay(\"tui.editor.jumpBackward\");\n\t\tconst pageUp = this.getEditorKeyDisplay(\"tui.editor.pageUp\");\n\t\tconst pageDown = this.getEditorKeyDisplay(\"tui.editor.pageDown\");\n\t\tconst submit = this.getEditorKeyDisplay(\"tui.input.submit\");\n\t\tconst newLine = this.getEditorKeyDisplay(\"tui.input.newLine\");\n\t\tconst deleteWordBackward = this.getEditorKeyDisplay(\"tui.editor.deleteWordBackward\");\n\t\tconst deleteWordForward = this.getEditorKeyDisplay(\"tui.editor.deleteWordForward\");\n\t\tconst deleteToLineStart = this.getEditorKeyDisplay(\"tui.editor.deleteToLineStart\");\n\t\tconst deleteToLineEnd = this.getEditorKeyDisplay(\"tui.editor.deleteToLineEnd\");\n\t\tconst yank = this.getEditorKeyDisplay(\"tui.editor.yank\");\n\t\tconst yankPop = this.getEditorKeyDisplay(\"tui.editor.yankPop\");\n\t\tconst undo = this.getEditorKeyDisplay(\"tui.editor.undo\");\n\t\tconst tab = this.getEditorKeyDisplay(\"tui.input.tab\");\n\t\tconst clear = this.getAppKeyDisplay(\"app.clear\");\n\t\tconst clearInput = this.getAppKeyDisplay(\"app.input.clear\");\n\t\tconst interrupt = this.getAppKeyDisplay(\"app.interrupt\");\n\t\tconst shortcutsKey = this.getAppKeyDisplay(\"app.shortcuts\");\n\t\tconst exit = this.getAppKeyDisplay(\"app.exit\");\n\t\tconst selectModel = this.getAppKeyDisplay(\"app.model.select\");\n\t\tconst expandTools = this.getAppKeyDisplay(\"app.tools.expand\");\n\t\tconst expandMessages = this.getAppKeyDisplay(\"app.messages.expand\");\n\t\tconst expandEdits = this.getAppKeyDisplay(\"app.edits.expand\");\n\t\tconst toggleThinking = this.getAppKeyDisplay(\"app.thinking.toggle\");\n\t\tconst focusSubagents = this.getAppKeyDisplay(\"app.subagents.focus\");\n\t\tconst manageHeartbeats = this.getAppKeyDisplay(\"app.heartbeats.open\");\n\t\tconst externalEditor = this.getAppKeyDisplay(\"app.editor.external\");\n\t\tconst promptStash = this.getAppKeyDisplay(\"app.prompt.stash\");\n\t\tconst followUp = this.getAppKeyDisplay(\"app.message.followUp\");\n\t\tconst browseQueue = this.getAppKeyDisplay(\"app.message.navigateOlder\");\n\t\tconst reorderQueue = `${this.getAppKeyDisplay(\"app.message.moveEarlier\")} / ${this.getAppKeyDisplay(\"app.message.moveLater\")}`;\n\t\tconst pasteImage = this.getAppKeyDisplay(\"app.clipboard.pasteImage\");\n\t\tconst viewportPageUp = this.getEditorKeyDisplay(\"tui.viewport.pageUp\");\n\t\tconst viewportPageDown = this.getEditorKeyDisplay(\"tui.viewport.pageDown\");\n\t\tconst viewportTop = this.getEditorKeyDisplay(\"tui.viewport.top\");\n\t\tconst viewportFollow = this.getEditorKeyDisplay(\"tui.viewport.follow\");\n\n\t\tlet hotkeys = `\n**Navigation**\n| Key | Action |\n|-----|--------|\n| \\`${cursorUp}\\` / \\`${cursorDown}\\` / \\`${cursorLeft}\\` / \\`${cursorRight}\\` | Move cursor / browse history (Up when empty) |\n| \\`${cursorWordLeft}\\` / \\`${cursorWordRight}\\` | Move by word |\n| \\`${cursorLineStart}\\` | Start of line |\n| \\`${cursorLineEnd}\\` | End of line |\n| \\`${jumpForward}\\` | Jump forward to character |\n| \\`${jumpBackward}\\` | Jump backward to character |\n| \\`${pageUp}\\` / \\`${pageDown}\\` | Scroll by page |\n\n**Editing**\n| Key | Action |\n|-----|--------|\n| \\`${submit}\\` | Send message |\n| \\`${newLine}\\` | New line${process.platform === \"win32\" ? \" (Ctrl+Enter on Windows Terminal)\" : \"\"} |\n| \\`${deleteWordBackward}\\` | Delete word backwards |\n| \\`${deleteWordForward}\\` | Delete word forwards |\n| \\`${deleteToLineStart}\\` | Delete to start of line |\n| \\`${deleteToLineEnd}\\` | Delete to end of line |\n| \\`${yank}\\` | Paste the most-recently-deleted text |\n| \\`${yankPop}\\` | Cycle through the deleted text after pasting |\n| \\`${undo}\\` | Undo |\n\n**Other**\n| Key | Action |\n|-----|--------|\n| \\`${tab}\\` | Path completion / accept autocomplete |\n| \\`${clearInput}\\` | Clear input / cancel autocomplete |\n| \\`${clear}\\` | Interrupt current operation (first) / exit (second) |\n${interrupt ? `| \\`${interrupt}\\` | Interrupt current operation |\\n` : \"\"}${shortcutsKey ? `| \\`${shortcutsKey}\\` | Show quick shortcuts |\\n` : \"\"}| \\`${exit}\\` | Exit (when editor is empty) |\n| \\`${selectModel}\\` | Open model selector |\n| \\`${expandTools}\\` | Toggle tool output expansion |\n| \\`${expandMessages}\\` | Toggle agent message expansion |\n| \\`${expandEdits}\\` | Toggle edit diff expansion |\n| \\`${toggleThinking}\\` | Toggle thinking block visibility |\n| \\`${focusSubagents}\\` | Focus the subagent summary / open the scoped agents view |\n| \\`${manageHeartbeats}\\` | Manage heartbeats |\n| \\`${externalEditor}\\` | Edit message in external editor |\n| \\`${promptStash}\\` | Stash or restore draft prompt |\n| \\`${followUp}\\` | Queue follow-up message |\n| \\`${browseQueue}\\` | Browse and edit queued messages |\n| \\`${reorderQueue}\\` | Reorder the selected queued message |\n| \\`${pasteImage}\\` | Paste image from clipboard |\n| \\`/\\` | Slash commands |\n\n**Fullscreen mode (\\`/fullscreen\\`)**\n| Key | Action |\n|-----|--------|\n| \\`${viewportPageUp}\\` / \\`${viewportPageDown}\\` | Scroll transcript by page |\n| \\`${viewportTop}\\` | Scroll to top |\n| \\`${viewportFollow}\\` | Scroll to bottom and follow output |\n| mouse wheel | Scroll transcript |\n| mouse drag | Select and copy text |\n| mouse click on link | Open link in browser |\n`;\n\n\t\tconst shortcuts = this.bindLocalSessionExtensions\n\t\t\t? this.getLocalSessionHost().getExtensionRunner().getShortcuts(this.keybindings.getEffectiveConfig())\n\t\t\t: undefined;\n\t\tif (shortcuts && shortcuts.size > 0) {\n\t\t\thotkeys += `\n**Extensions**\n| Key | Action |\n|-----|--------|\n`;\n\t\t\tfor (const [key, shortcut] of shortcuts) {\n\t\t\t\tconst description = shortcut.description ?? shortcut.extensionPath;\n\t\t\t\thotkeys += `| \\`${formatKeyText(key)}\\` | ${description} |\\n`;\n\t\t\t}\n\t\t}\n\n\t\treturn hotkeys;\n\t}\n\n\tprivate showShortcutGuide(): void {\n\t\tconst hotkeys = this.getShortcutGuide();\n\n\t\tthis.shortcutGuideContainer.clear();\n\t\tthis.shortcutGuideContainer.addChild(new Spacer(1));\n\t\tthis.shortcutGuideContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, this.getMarkdownThemeWithSettings()));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleHotkeysCommand(): void {\n\t\tconst hotkeys = this.getHotkeysGuide();\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, this.getMarkdownThemeWithSettings()));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate clearShortcutGuide(): void {\n\t\tif (this.shortcutGuideContainer.children.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tthis.shortcutGuideContainer.clear();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async handleClearCommand(options: { name?: string; prompt?: string } = {}): Promise<void> {\n\t\tthis.stopWorkingLoader();\n\t\tconst retainedImages = options.prompt ? this.getPromptStashImages(options.prompt) : [];\n\t\tconst restorePrompt = () => {\n\t\t\tif (!options.prompt) return;\n\t\t\tfor (const [id, image] of retainedImages) this.pastedImages.set(id, image);\n\t\t\tthis.editor.setText(options.prompt);\n\t\t};\n\t\tlet created = false;\n\t\ttry {\n\t\t\tconst result = await this.agentConnection.newSession();\n\t\t\tif (result.cancelled) {\n\t\t\t\trestorePrompt();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcreated = true;\n\t\t\tawait this.renderCurrentSessionState();\n\t\t\tfor (const [id, image] of retainedImages) this.pastedImages.set(id, image);\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(`${theme.fg(\"accent\", \"✓ New session started\")}`, 1, 1));\n\t\t\tthis.ui.requestRender();\n\t\t\tconst images = options.prompt ? this.collectImagesFor(options.prompt) : undefined;\n\t\t\tif (options.name) await this.agentConnection.setSessionName(options.name);\n\t\t\tif (options.prompt) {\n\t\t\t\tthis.editor.addToHistory?.(options.prompt);\n\t\t\t\tawait this.agentConnection.prompt(options.prompt, { images });\n\t\t\t}\n\t\t} catch (error: unknown) {\n\t\t\tif (!created) {\n\t\t\t\tawait this.handleFatalRuntimeError(\"Failed to create session\", error);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\trestorePrompt();\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate async handleDebugCommand(): Promise<void> {\n\t\tconst width = this.ui.terminal.columns;\n\t\tconst height = this.ui.terminal.rows;\n\t\tconst allLines = this.ui.render(width);\n\t\ttry {\n\t\t\tconst messages = await this.agentConnection.getMessages();\n\t\t\tconst debugLogPath = getDebugLogPath();\n\t\t\tconst debugData = [\n\t\t\t\t`Debug output at ${new Date().toISOString()}`,\n\t\t\t\t`Terminal: ${width}x${height}`,\n\t\t\t\t`Total lines: ${allLines.length}`,\n\t\t\t\t\"\",\n\t\t\t\t\"=== All rendered lines with visible widths ===\",\n\t\t\t\t...allLines.map((line, idx) => {\n\t\t\t\t\tconst vw = visibleWidth(line);\n\t\t\t\t\tconst escaped = JSON.stringify(line);\n\t\t\t\t\treturn `[${idx}] (w=${vw}) ${escaped}`;\n\t\t\t\t}),\n\t\t\t\t\"\",\n\t\t\t\t\"=== Agent messages (JSONL) ===\",\n\t\t\t\t...messages.map((msg) => JSON.stringify(msg)),\n\t\t\t\t\"\",\n\t\t\t].join(\"\\n\");\n\n\t\t\tfs.mkdirSync(path.dirname(debugLogPath), { recursive: true });\n\t\t\tfs.writeFileSync(debugLogPath, debugData);\n\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(\n\t\t\t\tnew Text(`${theme.fg(\"accent\", \"✓ Debug log written\")}\\n${theme.fg(\"muted\", debugLogPath)}`, 1, 1),\n\t\t\t);\n\t\t\tthis.ui.requestRender();\n\t\t} catch (error: unknown) {\n\t\t\tthis.showError(`Failed to write debug log: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n\n\tprivate handleArminSaysHi(): void {\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new ArminComponent(this.ui));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleDementedDelves(): void {\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new EarendilAnnouncementComponent());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleDaxnuts(): void {\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new DaxnutsComponent(this.ui));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate checkDaxnutsEasterEgg(model: { provider: string; id: string }): void {\n\t\tif (model.provider === \"opencode\" && model.id.toLowerCase().includes(\"kimi-k2.5\")) {\n\t\t\tthis.handleDaxnuts();\n\t\t}\n\t}\n\n\tstop(options: { preserveAltScreen?: boolean } = {}): void {\n\t\tthis.unregisterSignalHandlers();\n\t\tthis.clearCtrlCExitHint({ render: false });\n\t\tthis.clearEscapeRepeat();\n\t\tif (this.settingsManager.getShowTerminalProgress()) {\n\t\t\tthis.ui.terminal.setProgress(false);\n\t\t}\n\t\tthis.stopWorkingLoader();\n\t\tthis.discardRefineLoader();\n\t\tthis.endFeatureHintRun();\n\t\tthis.stopWorkingPulse();\n\t\tthis.stopGoalTrayTimer();\n\t\tthis.closeHeartbeatManager();\n\t\tthis.clearExtensionTerminalInputListeners();\n\t\tthis.footer.dispose();\n\t\tthis.footerDataProvider.dispose();\n\t\tif (this.unsubscribe) {\n\t\t\tthis.unsubscribe();\n\t\t}\n\t\tif (this.isInitialized) {\n\t\t\tthis.ui.stop({\n\t\t\t\tpreserveAltScreen: options.preserveAltScreen,\n\t\t\t\tflushFullscreen: options.preserveAltScreen ? false : undefined,\n\t\t\t});\n\t\t\tthis.isInitialized = false;\n\t\t}\n\t}\n}\n"]}
|