@iowarp/clio-coder 0.3.0 → 0.3.2
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 +123 -2
- package/CONTRIBUTING.md +23 -23
- package/README.md +295 -627
- package/dist/acp-BIYHVZIM.js +128 -0
- package/dist/{agents-Q4MYPMUW.js → agents-YT6SSRIT.js} +52 -27
- package/dist/assets/codewiki.json +1 -0
- package/dist/assets/grammars/notices/@vscode__tree-sitter-wasm/LICENSE +21 -0
- package/dist/assets/grammars/notices/@vscode__tree-sitter-wasm/cgmanifest.json +16 -0
- package/dist/assets/grammars/notices/tree-sitter-wasms/LICENSE +24 -0
- package/dist/assets/grammars/tree-sitter-c-sharp.wasm +0 -0
- package/dist/assets/grammars/tree-sitter-c.wasm +0 -0
- package/dist/assets/grammars/tree-sitter-cpp.wasm +0 -0
- package/dist/assets/grammars/tree-sitter-go.wasm +0 -0
- package/dist/assets/grammars/tree-sitter-java.wasm +0 -0
- package/dist/assets/grammars/tree-sitter-javascript.wasm +0 -0
- package/dist/assets/grammars/tree-sitter-python.wasm +0 -0
- package/dist/assets/grammars/tree-sitter-ruby.wasm +0 -0
- package/dist/assets/grammars/tree-sitter-rust.wasm +0 -0
- package/dist/assets/grammars/tree-sitter-tsx.wasm +0 -0
- package/dist/assets/grammars/tree-sitter-typescript.wasm +0 -0
- package/dist/assets/grammars/tree-sitter.wasm +0 -0
- package/dist/{auth-O6HYIJ6J.js → auth-5TWEIYDN.js} +30 -15
- package/dist/chunk-2EHAIA3X.js +481 -0
- package/dist/{chunk-ME6DNWIU.js → chunk-2IR2NMPA.js} +12 -4
- package/dist/chunk-2SFS6XQE.js +122 -0
- package/dist/{chunk-KKKPTZLM.js → chunk-2STGEDFV.js} +8 -1
- package/dist/chunk-2VTFPG5O.js +48 -0
- package/dist/chunk-3I7MS7N2.js +40 -0
- package/dist/chunk-3R73A4XB.js +69 -0
- package/dist/{chunk-JPOWPFCU.js → chunk-3ZXDFGR5.js} +52 -23
- package/dist/chunk-4BJ5BYCE.js +61 -0
- package/dist/{chunk-26BZQOAD.js → chunk-4BPJXDWC.js} +161 -260
- package/dist/chunk-4KLWL3UC.js +18 -0
- package/dist/{chunk-5LDRNKX2.js → chunk-4VP4KH3K.js} +161 -262
- package/dist/chunk-4ZG3XFUR.js +77 -0
- package/dist/chunk-5B2AEOW5.js +5407 -0
- package/dist/chunk-5TSRNF4G.js +510 -0
- package/dist/{chunk-WD6AJM35.js → chunk-5UUP6MWO.js} +48 -69
- package/dist/{chunk-X3BR7HWV.js → chunk-5WIGXA4T.js} +7 -1
- package/dist/chunk-65DEGPJ6.js +52 -0
- package/dist/chunk-6EJMN2Y3.js +17 -0
- package/dist/chunk-6EJV5X2W.js +16405 -0
- package/dist/chunk-6N5PTWMY.js +136 -0
- package/dist/chunk-6XLNIQDB.js +27 -0
- package/dist/{chunk-XNISANGE.js → chunk-77VKQEHF.js} +171 -74
- package/dist/chunk-7CR24IG7.js +242 -0
- package/dist/{chunk-UWIJNAOB.js → chunk-7EYHLWU7.js} +907 -638
- package/dist/chunk-7MNJORFF.js +22 -0
- package/dist/{chunk-JWHCJDCI.js → chunk-A3CYT5EX.js} +257 -79
- package/dist/chunk-AGYYIBLL.js +1069 -0
- package/dist/chunk-AO4RKG4M.js +277 -0
- package/dist/chunk-APJ265NV.js +7410 -0
- package/dist/chunk-ARBGF5F7.js +174 -0
- package/dist/{chunk-TFSSEXL6.js → chunk-BMEMKKIT.js} +14 -4
- package/dist/{chunk-X3NE4WVW.js → chunk-BNAZZHFG.js} +7 -1
- package/dist/chunk-CBCAPZAA.js +229 -0
- package/dist/{chunk-QV47YRF4.js → chunk-CGKSTWHD.js} +7 -1
- package/dist/chunk-CMZWFGD2.js +352 -0
- package/dist/chunk-ECH6PKUQ.js +39 -0
- package/dist/{chunk-67MTHP2E.js → chunk-ED4KHGC3.js} +13 -5
- package/dist/chunk-EKMEHE4H.js +340 -0
- package/dist/{chunk-O4EJEDHO.js → chunk-EPVUXGXG.js} +50 -35
- package/dist/chunk-FCSXB6T2.js +338 -0
- package/dist/chunk-FJ3H4MN5.js +48 -0
- package/dist/chunk-FQ4SKYE4.js +29 -0
- package/dist/chunk-G2DE3C7R.js +644 -0
- package/dist/chunk-G4BMMOKF.js +182 -0
- package/dist/{chunk-7BHLZB3A.js → chunk-GGXXDWE4.js} +190 -1218
- package/dist/chunk-HC4CLZ2Y.js +68 -0
- package/dist/{chunk-E3NYWENM.js → chunk-HFSBBKSQ.js} +55 -61
- package/dist/chunk-HKIYEGME.js +30 -0
- package/dist/chunk-I4HZDVNP.js +73 -0
- package/dist/chunk-IGLFWIYI.js +161 -0
- package/dist/chunk-IHXBNWMM.js +137 -0
- package/dist/chunk-IKCO5N3L.js +162 -0
- package/dist/chunk-IR4CFBFN.js +56 -0
- package/dist/{chunk-DXQNI4PC.js → chunk-IWHMRKLL.js} +8 -1
- package/dist/{chunk-VOO7NYPP.js → chunk-J5HN4RYU.js} +27 -9
- package/dist/{chunk-4UQA7NCT.js → chunk-J5Q24KAG.js} +7 -2
- package/dist/{chunk-KBR4MZZR.js → chunk-JVCV3ICN.js} +7 -1
- package/dist/chunk-KJ5LWLOE.js +1077 -0
- package/dist/chunk-LBMZMYH2.js +285 -0
- package/dist/{chunk-3DANZDGR.js → chunk-LBNRH5WM.js} +100 -161
- package/dist/{chunk-TKHQ4DGZ.js → chunk-LZSJBIVT.js} +7358 -7414
- package/dist/{chunk-UGYAX5YI.js → chunk-M6SHUN7Q.js} +13 -8
- package/dist/chunk-MAW544W2.js +1882 -0
- package/dist/chunk-MBS4V7ZP.js +217 -0
- package/dist/{chunk-2J63S4SF.js → chunk-MFFY33HR.js} +167 -140
- package/dist/chunk-MNA4JGU4.js +255 -0
- package/dist/chunk-MQSRRFWA.js +3428 -0
- package/dist/{chunk-RQDWMVRB.js → chunk-MVVUPGPW.js} +138 -137
- package/dist/chunk-OAO4GE4M.js +619 -0
- package/dist/chunk-OHHN2SO4.js +5135 -0
- package/dist/chunk-OKGUZO2U.js +34 -0
- package/dist/{chunk-BBUH4VAA.js → chunk-OOJYHWRB.js} +60 -361
- package/dist/{chunk-D5YD55AR.js → chunk-OQ33BKR3.js} +12 -1
- package/dist/chunk-OQE5J4C6.js +73 -0
- package/dist/{chunk-AMFR5YA3.js → chunk-ORBHGJC5.js} +26 -12
- package/dist/chunk-POHLU5DW.js +1186 -0
- package/dist/chunk-QKMUKYO7.js +4961 -0
- package/dist/{chunk-7RBKDI66.js → chunk-QTYWRVRA.js} +104 -12
- package/dist/{chunk-5N2FG33Q.js → chunk-R346GLFC.js} +7 -1
- package/dist/chunk-SRF2PJNW.js +184 -0
- package/dist/chunk-SST6Z5JA.js +80 -0
- package/dist/{chunk-XV4ZJ6ZM.js → chunk-STBPMHSX.js} +473 -1194
- package/dist/{chunk-HBU5MTAM.js → chunk-SVNXH4L2.js} +7 -1
- package/dist/chunk-T6YILFSB.js +80 -0
- package/dist/chunk-TXOTCRLG.js +15 -0
- package/dist/chunk-TZTZS7QK.js +227 -0
- package/dist/chunk-UOV2BYIW.js +107 -0
- package/dist/{chunk-527KG6XR.js → chunk-V4RXGQ5Q.js} +72 -185
- package/dist/chunk-VAKQQHWR.js +434 -0
- package/dist/chunk-VG7TBQIY.js +128 -0
- package/dist/chunk-VJWL6YS5.js +244 -0
- package/dist/chunk-WEH5XRJQ.js +32 -0
- package/dist/{chunk-VPAWTYLY.js → chunk-WEPFGWHJ.js} +7 -1
- package/dist/chunk-WMSVI4G2.js +2095 -0
- package/dist/chunk-WVO7V2QY.js +797 -0
- package/dist/chunk-X4RCMKVQ.js +641 -0
- package/dist/chunk-X75S7HFS.js +374 -0
- package/dist/chunk-XN3L4EYL.js +46 -0
- package/dist/{chunk-HMYNFFY4.js → chunk-XPLRXC72.js} +7 -2
- package/dist/{chunk-PIDUD6M2.js → chunk-YCWGATWI.js} +12 -5
- package/dist/chunk-YHZX5GEU.js +193 -0
- package/dist/chunk-YXLYO42X.js +91 -0
- package/dist/{chunk-UHTSULZS.js → chunk-ZDOOVTXZ.js} +38 -73
- package/dist/chunk-ZI647VB5.js +37 -0
- package/dist/{chunk-CLJ5HLUD.js → chunk-ZWLZP4ZT.js} +25 -6
- package/dist/cli/index.js +70 -56
- package/dist/clio-4LY5K2AC.js +25 -0
- package/dist/code-nav-7AX6FYE6.js +600 -0
- package/dist/codewiki/build-worker.js +66 -0
- package/dist/compile-cache-CVJMMODC.js +18 -0
- package/dist/{components-JVHMUBEB.js → components-KELWS457.js} +31 -9
- package/dist/{config-ZFCDBMDC.js → config-GTLUW2PR.js} +104 -56
- package/dist/configure-R6A64DHX.js +42 -0
- package/dist/context-5VKGUVJJ.js +866 -0
- package/dist/{context-CDXTP2MP.js → context-JFZEJ7W5.js} +21 -13
- package/dist/{context-E3KIFVXI.js → context-RW5HC47S.js} +77 -36
- package/dist/{context-clear-3F4PLXOS.js → context-clear-6ZHBAZZT.js} +64 -30
- package/dist/{context-index-Q7YSYTR3.js → context-index-BZ4UYMTC.js} +36 -24
- package/dist/dispatch-runner-VKBRCWQC.js +1997 -0
- package/dist/{docs-YIETIWZI.js → docs-2C2LTVT2.js} +32 -13
- package/dist/doctor-KI767GSN.js +87 -0
- package/dist/{eval-B3KZZESM.js → eval-XSSNATB4.js} +93 -27
- package/dist/{evidence-V67CHM35.js → evidence-UA6AWDQQ.js} +55 -27
- package/dist/{evolve-YDZSUQYA.js → evolve-QNTFGV6Z.js} +59 -26
- package/dist/{extensions-SRG7XCAH.js → extensions-QVDOHDGJ.js} +23 -8
- package/dist/{fleet-CA2CRTVG.js → fleet-Q7UOMUSG.js} +173 -56
- package/dist/fleet-preflight-DDN536IT.js +26 -0
- package/dist/{init-2OZDJE2D.js → init-WBB65ZHQ.js} +79 -34
- package/dist/{memory-3PIQQAKX.js → memory-MD3O64RI.js} +57 -28
- package/dist/{models-DY35XI7Y.js → models-BZU34YWD.js} +49 -24
- package/dist/monitor-MEQA5C3I.js +661 -0
- package/dist/{chunk-PS4PFJQP.js → orchestrator-CGFKEP27.js} +22837 -18564
- package/dist/{paths-5OMXW7Z4.js → paths-UXLN5YYZ.js} +17 -6
- package/dist/preload-P6DGH2PZ.js +13 -0
- package/dist/{reset-PIFYNOS3.js → reset-L2FQEE3E.js} +25 -13
- package/dist/{run-3VSPP24F.js → run-IV4Q6RLN.js} +115 -63
- package/dist/{share-D36RQCXM.js → share-S5BZQC5I.js} +23 -10
- package/dist/{skills-F2MRLELY.js → skills-LQEKRDTN.js} +42 -15
- package/dist/{skills-eval-E2ZTW4PL.js → skills-eval-3DC4HEWS.js} +90 -45
- package/dist/steer-GGWFUJUD.js +77 -0
- package/dist/{targets-DZMEZAH4.js → targets-C4SSGQOB.js} +78 -60
- package/dist/terminal-lease-IT5JW2NR.js +395 -0
- package/dist/{trace-7NYCUI2J.js → trace-PNCASAXC.js} +48 -17
- package/dist/tree-sitter-HGKH6LG4.js +4673 -0
- package/dist/{uninstall-AD3JWHBB.js → uninstall-FZCQCDKC.js} +19 -8
- package/dist/{upgrade-WYYBKGDY.js → upgrade-7TT7SQ3G.js} +66 -24
- package/dist/{usage-ULIDAGFF.js → usage-GV4PKT3M.js} +71 -32
- package/dist/verify-G6V4D2G7.js +716 -0
- package/dist/version-KGEG7EPO.js +21 -0
- package/dist/web-fetch-2YHJ3KTG.js +638 -0
- package/dist/{wiki-generate-PKFIX6OB.js → wiki-generate-DQF6Z66B.js} +98 -48
- package/dist/worker/entry.js +297 -54
- package/dist/workspace-G4ZWUIPR.js +22 -0
- package/docs/README.md +29 -21
- package/docs/acp.md +191 -16
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +157 -7
- package/docs/artifact-placement.md +101 -0
- package/docs/artifact-versions.md +1 -1
- package/docs/built-in-agents.md +4 -2
- package/docs/capacity-and-scheduling.md +1 -1
- package/docs/commands-and-modes.md +224 -97
- package/docs/config-knobs-audit.md +1 -3
- package/docs/configuration-and-targets.md +236 -38
- package/docs/context-engine.md +34 -7
- package/docs/development-pipeline.md +102 -0
- package/docs/dispatch-architecture-rationale.md +1 -1
- package/docs/documentation-coverage.md +4 -4
- package/docs/documentation-guide.md +3 -3
- package/docs/environment-variables.md +15 -6
- package/docs/eval-runner.md +3 -3
- package/docs/evals-internal.md +2 -2
- package/docs/evidence-and-memory.md +10 -2
- package/docs/evolution.md +2 -2
- package/docs/exit-codes-and-output.md +15 -9
- package/docs/extensions-and-sharing.md +25 -18
- package/docs/fleet-demo-runbook.md +22 -23
- package/docs/fleet-dispatch.md +117 -16
- package/docs/git-commit-provenance.md +120 -0
- package/docs/glossary.md +107 -15
- package/docs/installation-and-lifecycle.md +75 -17
- package/docs/middleware-and-components.md +1 -1
- package/docs/model-catalog.md +46 -15
- package/docs/observability.md +8 -5
- package/docs/performance-methodology.md +491 -0
- package/docs/pi-boundary.md +72 -0
- package/docs/proactive-memory.md +14 -14
- package/docs/prompt-envelope-and-tools.md +25 -4
- package/docs/provider-adapter-cookbook.md +57 -4
- package/docs/release-cut-checklist.md +129 -111
- package/docs/safety-model.md +28 -8
- package/docs/scientific-validation.md +3 -3
- package/docs/session-lifecycle.md +55 -12
- package/docs/skills-marketplace.md +12 -8
- package/docs/time-conventions.md +101 -0
- package/docs/tool-usage.md +6 -5
- package/docs/trace-store.md +1 -1
- package/docs/troubleshooting.md +10 -7
- package/docs/tui-design.md +121 -28
- package/docs/worker-dispatch-mechanics.md +3 -3
- package/package.json +33 -36
- package/skills/README.md +5 -6
- package/skills/coding/ast-grep/SKILL.md +136 -0
- package/skills/coding/ast-grep/evals.md +56 -0
- package/skills/coding/ast-grep/references/rule_reference.md +297 -0
- package/skills/coding/coding-standards/SKILL.md +113 -0
- package/skills/coding/coding-standards/evals.md +34 -0
- package/skills/coding/prototype/SKILL.md +86 -0
- package/skills/coding/prototype/evals.md +42 -0
- package/skills/coding/prototype/references/LOGIC.md +67 -0
- package/skills/coding/prototype/references/UI.md +112 -0
- package/skills/coding/tdd/SKILL.md +101 -0
- package/skills/coding/tdd/evals.md +41 -0
- package/skills/coding/tdd/references/mocking.md +59 -0
- package/skills/coding/tdd/references/tests.md +77 -0
- package/skills/context/context-handoff/SKILL.md +126 -0
- package/skills/context/context-handoff/evals.md +57 -0
- package/skills/context/context-handoff/scripts/new-handoff.sh +26 -0
- package/skills/context/context-prime/SKILL.md +95 -0
- package/skills/context/context-prime/evals.md +54 -0
- package/skills/git/file-ticket/SKILL.md +107 -0
- package/skills/git/file-ticket/evals.md +54 -0
- package/skills/git/fix-issue/SKILL.md +106 -0
- package/skills/git/fix-issue/evals.md +71 -0
- package/skills/git/ship/SKILL.md +92 -0
- package/skills/git/ship/evals.md +72 -0
- package/skills/meta/clio-dev/SKILL.md +91 -0
- package/skills/meta/clio-dev/evals.md +45 -0
- package/skills/meta/clio-test/SKILL.md +130 -0
- package/skills/meta/clio-test/evals.md +43 -0
- package/skills/meta/clio-test/references/harness.md +97 -0
- package/skills/meta/clio-test/references/test-map.md +59 -0
- package/skills/meta/credentials/SKILL.md +125 -0
- package/skills/meta/credentials/evals.md +104 -0
- package/skills/meta/find-skills/SKILL.md +72 -0
- package/skills/meta/find-skills/evals.md +47 -0
- package/skills/meta/herdr/SKILL.md +127 -0
- package/skills/meta/herdr/evals.md +38 -0
- package/skills/meta/skill-craft/SKILL.md +102 -0
- package/skills/meta/skill-craft/evals.md +41 -0
- package/skills/planning/architecture/SKILL.md +129 -0
- package/skills/planning/architecture/evals.md +36 -0
- package/skills/planning/backlog/SKILL.md +90 -0
- package/skills/planning/backlog/evals.md +43 -0
- package/skills/planning/prd/SKILL.md +82 -0
- package/skills/planning/prd/evals.md +49 -0
- package/skills/planning/product-intent/SKILL.md +112 -0
- package/skills/planning/product-intent/evals.md +36 -0
- package/skills/planning/tech-spec/SKILL.md +115 -0
- package/skills/planning/tech-spec/evals.md +47 -0
- package/skills/registry.yaml +136 -0
- package/skills/research/arxiv-literature/SKILL.md +104 -0
- package/skills/research/arxiv-literature/evals.md +58 -0
- package/skills/research/experiment-protocol/SKILL.md +122 -0
- package/skills/research/experiment-protocol/evals.md +91 -0
- package/skills/research/scientific-debugging/SKILL.md +119 -0
- package/skills/research/scientific-debugging/evals.md +138 -0
- package/skills/research/scientific-modernization/SKILL.md +138 -0
- package/skills/research/scientific-modernization/evals.md +84 -0
- package/skills/skill-marketplace.json +12 -20
- package/skills/workflow/design-council/SKILL.md +139 -0
- package/skills/workflow/design-council/evals.md +97 -0
- package/skills/workflow/grill-me/SKILL.md +186 -0
- package/skills/workflow/grill-me/evals.md +78 -0
- package/skills/workflow/workflow-distiller/SKILL.md +136 -0
- package/skills/workflow/workflow-distiller/evals.md +107 -0
- package/src/cli/acp.ts +115 -0
- package/src/cli/agents.ts +60 -0
- package/src/cli/args.ts +280 -0
- package/src/cli/argv.ts +170 -0
- package/src/cli/ask.ts +34 -0
- package/src/cli/auth.ts +468 -0
- package/src/cli/bootstrap-generate.ts +498 -0
- package/src/cli/clio.ts +96 -0
- package/src/cli/components.ts +195 -0
- package/src/cli/config-inspect.ts +336 -0
- package/src/cli/config.ts +72 -0
- package/src/cli/configure-interop.ts +67 -0
- package/src/cli/configure-layout.ts +172 -0
- package/src/cli/configure.ts +1525 -0
- package/src/cli/context-clear.ts +76 -0
- package/src/cli/context-index.ts +100 -0
- package/src/cli/context.ts +321 -0
- package/src/cli/default-target.ts +55 -0
- package/src/cli/docs.ts +346 -0
- package/src/cli/doctor.ts +54 -0
- package/src/cli/eval.ts +315 -0
- package/src/cli/evidence.ts +225 -0
- package/src/cli/evolve.ts +219 -0
- package/src/cli/extensions.ts +214 -0
- package/src/cli/fleet.ts +868 -0
- package/src/cli/index.ts +338 -0
- package/src/cli/init.ts +219 -0
- package/src/cli/initial-message.ts +47 -0
- package/src/cli/internal-dispatch.ts +55 -0
- package/src/cli/memory.ts +182 -0
- package/src/cli/models.ts +257 -0
- package/src/cli/modes/json-stream.ts +121 -0
- package/src/cli/modes/jsonl.ts +9 -0
- package/src/cli/modes/print.ts +604 -0
- package/src/cli/oauth-manual-input.ts +72 -0
- package/src/cli/oauth-select.ts +36 -0
- package/src/cli/output-guard.ts +48 -0
- package/src/cli/paths.ts +44 -0
- package/src/cli/provider-target.ts +113 -0
- package/src/cli/removal.ts +72 -0
- package/src/cli/reset.ts +253 -0
- package/src/cli/run.ts +497 -0
- package/src/cli/share.ts +243 -0
- package/src/cli/shared.ts +60 -0
- package/src/cli/skills-eval.ts +1240 -0
- package/src/cli/skills.ts +485 -0
- package/src/cli/steer-channel.ts +86 -0
- package/src/cli/targets.ts +1154 -0
- package/src/cli/text-layout.ts +112 -0
- package/src/cli/trace.ts +322 -0
- package/src/cli/uninstall.ts +405 -0
- package/src/cli/upgrade.ts +286 -0
- package/src/cli/usage.ts +876 -0
- package/src/cli/validate-model.ts +28 -0
- package/src/cli/version.ts +7 -0
- package/src/cli/wiki-generate.ts +510 -0
- package/src/core/agent-environment.ts +7 -0
- package/src/core/artifact-paths.ts +43 -0
- package/src/core/bash-exec.ts +313 -0
- package/src/core/boot-trace.ts +42 -0
- package/src/core/bus-events.ts +744 -0
- package/src/core/bus-trace.ts +53 -0
- package/src/core/c-header-language.ts +265 -0
- package/src/core/clio-repo.ts +78 -0
- package/src/core/commit-attribution.ts +157 -0
- package/src/core/compile-cache.ts +159 -0
- package/src/core/config.ts +1722 -0
- package/src/core/context-floor.ts +33 -0
- package/src/core/defaults.ts +659 -0
- package/src/core/domain-loader.ts +177 -0
- package/src/core/event-bus.ts +94 -0
- package/src/core/file-references.ts +282 -0
- package/src/core/git-commit-attribution.ts +362 -0
- package/src/core/guardrails.ts +185 -0
- package/src/core/headless-permission.ts +21 -0
- package/src/core/incomplete-installation.ts +45 -0
- package/src/core/init.ts +223 -0
- package/src/core/json-payload.ts +58 -0
- package/src/core/package-root.ts +54 -0
- package/src/core/path-canonical.ts +37 -0
- package/src/core/process-identity.ts +41 -0
- package/src/core/recent-models.ts +91 -0
- package/src/core/residency-protection.ts +56 -0
- package/src/core/resolve-config-value.ts +88 -0
- package/src/core/response-schema.ts +279 -0
- package/src/core/run-overrides.ts +77 -0
- package/src/core/safe-exec.ts +205 -0
- package/src/core/safe-id.ts +37 -0
- package/src/core/safe-resource-write.ts +162 -0
- package/src/core/session-routing.ts +339 -0
- package/src/core/settings-layers.ts +354 -0
- package/src/core/shared-bus.ts +8 -0
- package/src/core/shell-quote.ts +26 -0
- package/src/core/skill-activation.ts +190 -0
- package/src/core/startup-timer.ts +37 -0
- package/src/core/state-file-lock.ts +219 -0
- package/src/core/termination.ts +275 -0
- package/src/core/timers.ts +38 -0
- package/src/core/tool-names.ts +65 -0
- package/src/core/verification-scripts.ts +13 -0
- package/src/core/workspace-files.ts +492 -0
- package/src/core/xdg.ts +177 -0
- package/src/domains/agents/builtins/architect.md +4 -4
- package/src/domains/agents/builtins/coder.md +3 -2
- package/src/domains/agents/builtins/debugger.md +1 -1
- package/src/domains/agents/builtins/documenter.md +1 -1
- package/src/domains/agents/builtins/git-master.md +3 -3
- package/src/domains/agents/builtins/provenance.md +1 -1
- package/src/domains/agents/builtins/researcher.md +1 -1
- package/src/domains/agents/builtins/scout.md +1 -1
- package/src/domains/agents/builtins/tester.md +1 -1
- package/src/domains/agents/builtins/verifier.md +1 -1
- package/src/domains/agents/builtins/wiki-writer.md +1 -1
- package/src/domains/agents/catalog.ts +172 -0
- package/src/domains/agents/contract.ts +16 -0
- package/src/domains/agents/extension.ts +110 -0
- package/src/domains/agents/fleet-commands.ts +154 -0
- package/src/domains/agents/fleet-contract.ts +797 -0
- package/src/domains/agents/frontmatter.ts +40 -0
- package/src/domains/agents/index.ts +102 -0
- package/src/domains/agents/manifest.ts +6 -0
- package/src/domains/agents/recipe-schema.ts +202 -0
- package/src/domains/agents/recipe.ts +125 -0
- package/src/domains/agents/registry.ts +111 -0
- package/src/domains/agents/result-contract.ts +1265 -0
- package/src/domains/agents/spec.ts +371 -0
- package/src/domains/agents/write-boundary.ts +109 -0
- package/src/domains/components/diff.ts +99 -0
- package/src/domains/components/hash.ts +21 -0
- package/src/domains/components/index.ts +3 -0
- package/src/domains/components/scan.ts +304 -0
- package/src/domains/components/snapshot.ts +108 -0
- package/src/domains/components/types.ts +82 -0
- package/src/domains/config/agent-namespace.ts +13 -0
- package/src/domains/config/classify.ts +106 -0
- package/src/domains/config/contract.ts +22 -0
- package/src/domains/config/extension.ts +142 -0
- package/src/domains/config/index.ts +23 -0
- package/src/domains/config/keybindings.ts +200 -0
- package/src/domains/config/manifest.ts +6 -0
- package/src/domains/config/watcher.ts +41 -0
- package/src/domains/context/adoption.ts +880 -0
- package/src/domains/context/bootstrap-prompt.ts +273 -0
- package/src/domains/context/bootstrap.ts +1502 -0
- package/src/domains/context/clear.ts +91 -0
- package/src/domains/context/clio-md.ts +429 -0
- package/src/domains/context/codewiki/artifact.ts +400 -0
- package/src/domains/context/codewiki/build-worker-protocol.ts +24 -0
- package/src/domains/context/codewiki/build-worker.ts +54 -0
- package/src/domains/context/codewiki/cmake.ts +356 -0
- package/src/domains/context/codewiki/cooperative.ts +46 -0
- package/src/domains/context/codewiki/coordinator.ts +182 -0
- package/src/domains/context/codewiki/digest.ts +135 -0
- package/src/domains/context/codewiki/grammar-assets.ts +33 -0
- package/src/domains/context/codewiki/indexer.ts +1529 -0
- package/src/domains/context/codewiki/paths.ts +67 -0
- package/src/domains/context/codewiki/schema.ts +80 -0
- package/src/domains/context/codewiki/tree-sitter.ts +771 -0
- package/src/domains/context/contract.ts +68 -0
- package/src/domains/context/excluded-dirs.ts +1 -0
- package/src/domains/context/extension.ts +358 -0
- package/src/domains/context/fingerprint.ts +198 -0
- package/src/domains/context/index.ts +154 -0
- package/src/domains/context/init-options.ts +69 -0
- package/src/domains/context/manifest.ts +6 -0
- package/src/domains/context/operator-profile.ts +157 -0
- package/src/domains/context/project-metadata.ts +529 -0
- package/src/domains/context/project-rules.ts +227 -0
- package/src/domains/context/prompt-context.ts +61 -0
- package/src/domains/context/refresh.ts +178 -0
- package/src/domains/context/runtime.ts +17 -0
- package/src/domains/context/sibling-files.ts +10 -0
- package/src/domains/context/state.ts +237 -0
- package/src/domains/context/wiki/assemble.ts +303 -0
- package/src/domains/context/wiki/frontmatter.ts +210 -0
- package/src/domains/context/wiki/generate.ts +595 -0
- package/src/domains/context/wiki/layout.ts +111 -0
- package/src/domains/context/wiki/meta.ts +221 -0
- package/src/domains/context/wiki/plan-store.ts +217 -0
- package/src/domains/context/wiki/plan.ts +250 -0
- package/src/domains/context/wiki/prompts.ts +283 -0
- package/src/domains/context/wiki/staleness.ts +198 -0
- package/src/domains/dispatch/active-route-planner.ts +168 -0
- package/src/domains/dispatch/admission-queue.ts +150 -0
- package/src/domains/dispatch/admission.ts +334 -0
- package/src/domains/dispatch/agent-candidates.ts +307 -0
- package/src/domains/dispatch/agent-ledger-hub.ts +71 -0
- package/src/domains/dispatch/agent-ledger-store.ts +257 -0
- package/src/domains/dispatch/agent-ledger.ts +212 -0
- package/src/domains/dispatch/agent-plan-adapter.ts +85 -0
- package/src/domains/dispatch/assignment-events.ts +132 -0
- package/src/domains/dispatch/assignment-reconcile.ts +76 -0
- package/src/domains/dispatch/assignment-store.ts +162 -0
- package/src/domains/dispatch/assignment.ts +278 -0
- package/src/domains/dispatch/backoff.ts +59 -0
- package/src/domains/dispatch/batch-store.ts +134 -0
- package/src/domains/dispatch/batch-tracker.ts +66 -0
- package/src/domains/dispatch/capability-match.ts +155 -0
- package/src/domains/dispatch/capacity-lease.ts +413 -0
- package/src/domains/dispatch/code-step-store.ts +52 -0
- package/src/domains/dispatch/code-step.ts +387 -0
- package/src/domains/dispatch/contract.ts +278 -0
- package/src/domains/dispatch/dedup.ts +133 -0
- package/src/domains/dispatch/event-pump.ts +251 -0
- package/src/domains/dispatch/execution-handoff.ts +29 -0
- package/src/domains/dispatch/execution-plan.ts +328 -0
- package/src/domains/dispatch/execution-role.ts +277 -0
- package/src/domains/dispatch/execution-scheduler.ts +583 -0
- package/src/domains/dispatch/extension.ts +6006 -0
- package/src/domains/dispatch/failure-classification.ts +147 -0
- package/src/domains/dispatch/fleet-commit-attribution.ts +56 -0
- package/src/domains/dispatch/fleet-plan.ts +225 -0
- package/src/domains/dispatch/fleet-preflight.ts +394 -0
- package/src/domains/dispatch/gate-decisions.ts +1040 -0
- package/src/domains/dispatch/gate-role-prompts.ts +58 -0
- package/src/domains/dispatch/heartbeat.ts +27 -0
- package/src/domains/dispatch/index.ts +137 -0
- package/src/domains/dispatch/joint-route-adapter.ts +114 -0
- package/src/domains/dispatch/joint-route-resolver.ts +283 -0
- package/src/domains/dispatch/manifest.ts +6 -0
- package/src/domains/dispatch/orphan-recovery.ts +266 -0
- package/src/domains/dispatch/outcome.ts +109 -0
- package/src/domains/dispatch/phase-timing.ts +47 -0
- package/src/domains/dispatch/placement.ts +202 -0
- package/src/domains/dispatch/receipt-findings.ts +255 -0
- package/src/domains/dispatch/receipt-integrity.ts +383 -0
- package/src/domains/dispatch/recovery-candidates.ts +74 -0
- package/src/domains/dispatch/reproducibility.ts +48 -0
- package/src/domains/dispatch/reservation-store.ts +501 -0
- package/src/domains/dispatch/route-approval.ts +177 -0
- package/src/domains/dispatch/route-candidates.ts +57 -0
- package/src/domains/dispatch/route-decision.ts +1079 -0
- package/src/domains/dispatch/route-facts.ts +187 -0
- package/src/domains/dispatch/route-history.ts +238 -0
- package/src/domains/dispatch/route-observer.ts +488 -0
- package/src/domains/dispatch/route-policy.ts +327 -0
- package/src/domains/dispatch/route-quality.ts +256 -0
- package/src/domains/dispatch/route-readiness.ts +120 -0
- package/src/domains/dispatch/routing-intent.ts +287 -0
- package/src/domains/dispatch/run-identity.ts +56 -0
- package/src/domains/dispatch/scout-transition.ts +130 -0
- package/src/domains/dispatch/state.ts +302 -0
- package/src/domains/dispatch/tool-stats.ts +132 -0
- package/src/domains/dispatch/transport.ts +210 -0
- package/src/domains/dispatch/types.ts +799 -0
- package/src/domains/dispatch/validation-grounding.ts +154 -0
- package/src/domains/dispatch/validation.ts +721 -0
- package/src/domains/dispatch/worker-protocol.ts +275 -0
- package/src/domains/dispatch/worker-spawn.ts +585 -0
- package/src/domains/dispatch/write-boundary-enforcer.ts +97 -0
- package/src/domains/dispatch/write-boundary.ts +426 -0
- package/src/domains/eval/artifacts/redact.ts +42 -0
- package/src/domains/eval/artifacts/store.ts +173 -0
- package/src/domains/eval/compare/compare.ts +33 -0
- package/src/domains/eval/compare/gates.ts +66 -0
- package/src/domains/eval/compare/thresholds.ts +92 -0
- package/src/domains/eval/harness-metrics.ts +62 -0
- package/src/domains/eval/index.ts +66 -0
- package/src/domains/eval/metrics/chaos-stream.ts +93 -0
- package/src/domains/eval/metrics/context.ts +34 -0
- package/src/domains/eval/metrics/coverage.ts +21 -0
- package/src/domains/eval/metrics/evidence.ts +182 -0
- package/src/domains/eval/metrics/fleet-loop-stream.ts +196 -0
- package/src/domains/eval/metrics/invariants.ts +736 -0
- package/src/domains/eval/metrics/latency.ts +4 -0
- package/src/domains/eval/metrics/token-stream.ts +139 -0
- package/src/domains/eval/metrics/tokens.ts +51 -0
- package/src/domains/eval/metrics/tool-calls.ts +7 -0
- package/src/domains/eval/proactive-memory.ts +279 -0
- package/src/domains/eval/provenance.ts +35 -0
- package/src/domains/eval/report.ts +234 -0
- package/src/domains/eval/reports/json.ts +5 -0
- package/src/domains/eval/reports/junit.ts +23 -0
- package/src/domains/eval/reports/markdown.ts +20 -0
- package/src/domains/eval/reports/swe-jsonl.ts +16 -0
- package/src/domains/eval/reports/text.ts +25 -0
- package/src/domains/eval/run-compare.ts +274 -0
- package/src/domains/eval/runner.ts +244 -0
- package/src/domains/eval/runners/clio-run.ts +148 -0
- package/src/domains/eval/runners/context-index.ts +56 -0
- package/src/domains/eval/runners/context-init.ts +203 -0
- package/src/domains/eval/runners/external-command.ts +350 -0
- package/src/domains/eval/runtime-paths.ts +171 -0
- package/src/domains/eval/schema/artifact.ts +75 -0
- package/src/domains/eval/schema/suite.ts +235 -0
- package/src/domains/eval/schema/validate.ts +339 -0
- package/src/domains/eval/store.ts +392 -0
- package/src/domains/eval/suites/load.ts +74 -0
- package/src/domains/eval/suites/matrix.ts +19 -0
- package/src/domains/eval/suites/resolve.ts +44 -0
- package/src/domains/eval/suites/run.ts +325 -0
- package/src/domains/eval/task-file.ts +186 -0
- package/src/domains/eval/types.ts +140 -0
- package/src/domains/eval/verifiers/command.ts +27 -0
- package/src/domains/eval/verifiers/file-exists.ts +6 -0
- package/src/domains/eval/verifiers/patch.ts +22 -0
- package/src/domains/eval/workspaces/git.ts +41 -0
- package/src/domains/eval/workspaces/local.ts +17 -0
- package/src/domains/eval/workspaces/temp-copy.ts +29 -0
- package/src/domains/evidence/build.ts +1754 -0
- package/src/domains/evidence/eval.ts +605 -0
- package/src/domains/evidence/failure-attribution.ts +54 -0
- package/src/domains/evidence/finish-contract-map.ts +42 -0
- package/src/domains/evidence/index.ts +55 -0
- package/src/domains/evidence/provenance.ts +200 -0
- package/src/domains/evidence/redact.ts +81 -0
- package/src/domains/evidence/store.ts +291 -0
- package/src/domains/evidence/types.ts +325 -0
- package/src/domains/evolution/SELF_EDIT_GATE.md +91 -0
- package/src/domains/evolution/index.ts +26 -0
- package/src/domains/evolution/manifest.ts +154 -0
- package/src/domains/evolution/validate.ts +304 -0
- package/src/domains/extensions/contract.ts +22 -0
- package/src/domains/extensions/discovery.ts +193 -0
- package/src/domains/extensions/extension.ts +46 -0
- package/src/domains/extensions/index.ts +34 -0
- package/src/domains/extensions/manager.ts +30 -0
- package/src/domains/extensions/manifest.ts +6 -0
- package/src/domains/extensions/resources.ts +38 -0
- package/src/domains/extensions/state.ts +224 -0
- package/src/domains/extensions/types.ts +83 -0
- package/src/domains/interop/consent.ts +185 -0
- package/src/domains/interop/contract.ts +29 -0
- package/src/domains/interop/detect.ts +173 -0
- package/src/domains/interop/extension.ts +59 -0
- package/src/domains/interop/index.ts +34 -0
- package/src/domains/interop/manifest.ts +6 -0
- package/src/domains/interop/registry.ts +139 -0
- package/src/domains/interop/state.ts +92 -0
- package/src/domains/interop/types.ts +109 -0
- package/src/domains/lifecycle/contract.ts +11 -0
- package/src/domains/lifecycle/doctor.ts +536 -0
- package/src/domains/lifecycle/extension.ts +23 -0
- package/src/domains/lifecycle/index.ts +21 -0
- package/src/domains/lifecycle/install-method.ts +51 -0
- package/src/domains/lifecycle/manifest.ts +6 -0
- package/src/domains/lifecycle/migrations/2026-08-18-lmstudio-runtime-id.ts +52 -0
- package/src/domains/lifecycle/migrations/index.ts +114 -0
- package/src/domains/lifecycle/state.ts +93 -0
- package/src/domains/lifecycle/upgrade-notice.ts +35 -0
- package/src/domains/lifecycle/version.ts +35 -0
- package/src/domains/memory/index.ts +128 -0
- package/src/domains/memory/operations.ts +143 -0
- package/src/domains/memory/prompt-section.ts +86 -0
- package/src/domains/memory/proposal.ts +180 -0
- package/src/domains/memory/store.ts +163 -0
- package/src/domains/memory/task-bank.ts +229 -0
- package/src/domains/memory/task-memory-handoff.ts +222 -0
- package/src/domains/memory/task-memory-policy.ts +602 -0
- package/src/domains/memory/task-memory-status.ts +58 -0
- package/src/domains/memory/task-memory-telemetry.ts +346 -0
- package/src/domains/memory/task-memory-trace.ts +89 -0
- package/src/domains/memory/types.ts +74 -0
- package/src/domains/memory/validate.ts +297 -0
- package/src/domains/middleware/budget.ts +216 -0
- package/src/domains/middleware/contract.ts +35 -0
- package/src/domains/middleware/dispatch-nudge.ts +469 -0
- package/src/domains/middleware/extension.ts +129 -0
- package/src/domains/middleware/hook-receipts.ts +70 -0
- package/src/domains/middleware/hooks-io.ts +186 -0
- package/src/domains/middleware/hooks.ts +432 -0
- package/src/domains/middleware/index.ts +154 -0
- package/src/domains/middleware/manifest.ts +6 -0
- package/src/domains/middleware/memory-intervention.ts +701 -0
- package/src/domains/middleware/rules.ts +42 -0
- package/src/domains/middleware/runtime.ts +333 -0
- package/src/domains/middleware/skills-reminder.ts +202 -0
- package/src/domains/middleware/snapshot.ts +71 -0
- package/src/domains/middleware/stalled-turn.ts +71 -0
- package/src/domains/middleware/task-board-reminder.ts +87 -0
- package/src/domains/middleware/task-nudge.ts +77 -0
- package/src/domains/middleware/tool-choice-control.ts +42 -0
- package/src/domains/middleware/types.ts +131 -0
- package/src/domains/middleware/validate.ts +329 -0
- package/src/domains/observability/accountability.ts +67 -0
- package/src/domains/observability/contract.ts +168 -0
- package/src/domains/observability/cost.ts +206 -0
- package/src/domains/observability/evidence-index.ts +123 -0
- package/src/domains/observability/extension.ts +281 -0
- package/src/domains/observability/index.ts +78 -0
- package/src/domains/observability/manifest.ts +10 -0
- package/src/domains/observability/metrics.ts +37 -0
- package/src/domains/observability/projection.ts +477 -0
- package/src/domains/observability/telemetry.ts +89 -0
- package/src/domains/observability/trace-store.ts +1454 -0
- package/src/domains/prompts/compiler.ts +614 -0
- package/src/domains/prompts/contract.ts +46 -0
- package/src/domains/prompts/extension.ts +336 -0
- package/src/domains/prompts/fragment-loader.ts +138 -0
- package/src/domains/prompts/fragments/identity/clio.md +5 -6
- package/src/domains/prompts/fragments/identity/docs-routing.md +10 -0
- package/src/domains/prompts/fragments/identity/self-awareness.md +19 -0
- package/src/domains/prompts/fragments/operating/contract.md +4 -41
- package/src/domains/prompts/fragments/operating/delegation.md +42 -0
- package/src/domains/prompts/fragments/operating/skills.md +26 -0
- package/src/domains/prompts/fragments/operating/worker.md +16 -0
- package/src/domains/prompts/fragments/safety/auto-edit.md +5 -5
- package/src/domains/prompts/fragments/safety/full-auto.md +3 -3
- package/src/domains/prompts/fragments/safety/read-only.md +4 -4
- package/src/domains/prompts/fragments/safety/suggest.md +2 -2
- package/src/domains/prompts/fragments/wiki/page.md +10 -0
- package/src/domains/prompts/fragments/wiki/plan.md +10 -0
- package/src/domains/prompts/hash.ts +73 -0
- package/src/domains/prompts/index.ts +32 -0
- package/src/domains/prompts/manifest.ts +8 -0
- package/src/domains/prompts/memory-intervention.ts +57 -0
- package/src/domains/prompts/preload.ts +82 -0
- package/src/domains/providers/auth/api-key.ts +47 -0
- package/src/domains/providers/auth/backend-file.ts +79 -0
- package/src/domains/providers/auth/backend-memory.ts +72 -0
- package/src/domains/providers/auth/boot-status.ts +65 -0
- package/src/domains/providers/auth/index.ts +37 -0
- package/src/domains/providers/auth/oauth.ts +37 -0
- package/src/domains/providers/auth/storage.ts +726 -0
- package/src/domains/providers/capabilities.ts +58 -0
- package/src/domains/providers/catalog.ts +162 -0
- package/src/domains/providers/contract.ts +162 -0
- package/src/domains/providers/credentials.ts +20 -0
- package/src/domains/providers/eligibility.ts +24 -0
- package/src/domains/providers/errors.ts +64 -0
- package/src/domains/providers/extension.ts +548 -0
- package/src/domains/providers/index.ts +142 -0
- package/src/domains/providers/knowledge-base-path.ts +102 -0
- package/src/domains/providers/manifest.ts +6 -0
- package/src/domains/providers/model-capabilities.ts +136 -0
- package/src/domains/providers/model-discovery.ts +144 -0
- package/src/domains/providers/model-family.ts +32 -0
- package/src/domains/providers/model-runtime-capabilities.ts +723 -0
- package/src/domains/providers/models/local-models/clio-local-coding-targets.yaml +120 -37
- package/src/domains/providers/plugins.ts +61 -0
- package/src/domains/providers/probe/fingerprint.ts +64 -0
- package/src/domains/providers/probe/http.ts +114 -0
- package/src/domains/providers/probe/reasoning.ts +111 -0
- package/src/domains/providers/registry.ts +187 -0
- package/src/domains/providers/resolver.ts +156 -0
- package/src/domains/providers/runtime-resolution.ts +720 -0
- package/src/domains/providers/runtimes/antigravity/antigravity-code.ts +90 -0
- package/src/domains/providers/runtimes/boot-manifest.ts +55 -0
- package/src/domains/providers/runtimes/builtins.ts +78 -0
- package/src/domains/providers/runtimes/claude/claude-code.ts +38 -0
- package/src/domains/providers/runtimes/claude/claude-sdk.ts +38 -0
- package/src/domains/providers/runtimes/claude/common.ts +55 -0
- package/src/domains/providers/runtimes/cloud/alcf.ts +171 -0
- package/src/domains/providers/runtimes/cloud/anthropic-max.ts +71 -0
- package/src/domains/providers/runtimes/cloud/anthropic.ts +43 -0
- package/src/domains/providers/runtimes/cloud/bedrock.ts +35 -0
- package/src/domains/providers/runtimes/cloud/deepseek.ts +50 -0
- package/src/domains/providers/runtimes/cloud/google.ts +41 -0
- package/src/domains/providers/runtimes/cloud/groq.ts +46 -0
- package/src/domains/providers/runtimes/cloud/mistral.ts +46 -0
- package/src/domains/providers/runtimes/cloud/openai-codex.ts +49 -0
- package/src/domains/providers/runtimes/cloud/openai.ts +46 -0
- package/src/domains/providers/runtimes/cloud/openrouter.ts +111 -0
- package/src/domains/providers/runtimes/common/lmstudio-http.ts +423 -0
- package/src/domains/providers/runtimes/common/local-synth.ts +164 -0
- package/src/domains/providers/runtimes/common/probe-helpers.ts +474 -0
- package/src/domains/providers/runtimes/local-native/lemonade-anthropic.ts +27 -0
- package/src/domains/providers/runtimes/local-native/lemonade-openai.ts +26 -0
- package/src/domains/providers/runtimes/local-native/llamacpp-anthropic.ts +88 -0
- package/src/domains/providers/runtimes/local-native/llamacpp-completion.ts +207 -0
- package/src/domains/providers/runtimes/local-native/llamacpp-embed.ts +180 -0
- package/src/domains/providers/runtimes/local-native/llamacpp-rerank.ts +126 -0
- package/src/domains/providers/runtimes/local-native/llamacpp.ts +98 -0
- package/src/domains/providers/runtimes/local-native/lmstudio.ts +241 -0
- package/src/domains/providers/runtimes/local-native/ollama-native.ts +117 -0
- package/src/domains/providers/runtimes/local-native/sglang.ts +26 -0
- package/src/domains/providers/runtimes/local-native/vllm.ts +27 -0
- package/src/domains/providers/runtimes/protocol/anthropic-compat.ts +104 -0
- package/src/domains/providers/runtimes/protocol/anthropic-messages.ts +28 -0
- package/src/domains/providers/runtimes/protocol/bedrock.ts +26 -0
- package/src/domains/providers/runtimes/protocol/google.ts +27 -0
- package/src/domains/providers/runtimes/protocol/openai-compat.ts +145 -0
- package/src/domains/providers/support.ts +241 -0
- package/src/domains/providers/thinking-control-policy.ts +85 -0
- package/src/domains/providers/types/capability-flags.ts +114 -0
- package/src/domains/providers/types/cost-provenance.ts +7 -0
- package/src/domains/providers/types/inference.ts +41 -0
- package/src/domains/providers/types/knowledge-base.ts +156 -0
- package/src/domains/providers/types/local-model-quirks.ts +216 -0
- package/src/domains/providers/types/runtime-descriptor.ts +181 -0
- package/src/domains/providers/types/target-descriptor.ts +60 -0
- package/src/domains/resources/collision.ts +92 -0
- package/src/domains/resources/common-loader.ts +140 -0
- package/src/domains/resources/contract.ts +24 -0
- package/src/domains/resources/extension.ts +59 -0
- package/src/domains/resources/index.ts +74 -0
- package/src/domains/resources/loader.ts +85 -0
- package/src/domains/resources/manifest.ts +6 -0
- package/src/domains/resources/prompts/loader.ts +246 -0
- package/src/domains/resources/prompts/substitute.ts +1 -0
- package/src/domains/resources/skills/content-hash.ts +107 -0
- package/src/domains/resources/skills/evals.ts +175 -0
- package/src/domains/resources/skills/install.ts +427 -0
- package/src/domains/resources/skills/loader.ts +985 -0
- package/src/domains/resources/skills/marketplace.ts +301 -0
- package/src/domains/resources/skills/provenance-pin.ts +115 -0
- package/src/domains/safety/action-classifier.ts +270 -0
- package/src/domains/safety/approval-axis.ts +23 -0
- package/src/domains/safety/audit.ts +493 -0
- package/src/domains/safety/autonomy.ts +174 -0
- package/src/domains/safety/call-target.ts +59 -0
- package/src/domains/safety/contract.ts +65 -0
- package/src/domains/safety/damage-control.ts +44 -0
- package/src/domains/safety/default-path-policy.ts +117 -0
- package/src/domains/safety/extension.ts +383 -0
- package/src/domains/safety/finish-contract-registration.ts +128 -0
- package/src/domains/safety/finish-contract.ts +554 -0
- package/src/domains/safety/index.ts +44 -0
- package/src/domains/safety/loop-detector.ts +143 -0
- package/src/domains/safety/manifest.ts +6 -0
- package/src/domains/safety/path-policy.ts +164 -0
- package/src/domains/safety/policy-engine.ts +994 -0
- package/src/domains/safety/project-policy.ts +391 -0
- package/src/domains/safety/protected-artifacts-registration.ts +189 -0
- package/src/domains/safety/protected-artifacts.ts +993 -0
- package/src/domains/safety/rejection-feedback.ts +102 -0
- package/src/domains/safety/rigor.ts +72 -0
- package/src/domains/safety/rule-compiler.ts +48 -0
- package/src/domains/safety/rule-pack-loader.ts +92 -0
- package/src/domains/safety/run-effects.ts +197 -0
- package/src/domains/safety/scope.ts +61 -0
- package/src/domains/scheduling/budget.ts +35 -0
- package/src/domains/scheduling/cluster.ts +175 -0
- package/src/domains/scheduling/contract.ts +30 -0
- package/src/domains/scheduling/extension.ts +91 -0
- package/src/domains/scheduling/index.ts +12 -0
- package/src/domains/scheduling/manifest.ts +6 -0
- package/src/domains/session/archive-readers.ts +208 -0
- package/src/domains/session/checkpoint.ts +38 -0
- package/src/domains/session/compaction/auto.ts +89 -0
- package/src/domains/session/compaction/branch-summary.ts +187 -0
- package/src/domains/session/compaction/compact.ts +493 -0
- package/src/domains/session/compaction/cut-point.ts +172 -0
- package/src/domains/session/compaction/defaults.ts +34 -0
- package/src/domains/session/compaction/mask-observations.ts +265 -0
- package/src/domains/session/compaction/session-entries.ts +18 -0
- package/src/domains/session/compaction/tokens.ts +148 -0
- package/src/domains/session/context-accounting.ts +583 -0
- package/src/domains/session/context-ledger.ts +362 -0
- package/src/domains/session/contract.ts +159 -0
- package/src/domains/session/cwd-fallback.ts +65 -0
- package/src/domains/session/decision-board.ts +190 -0
- package/src/domains/session/entries.ts +572 -0
- package/src/domains/session/extension.ts +403 -0
- package/src/domains/session/history.ts +172 -0
- package/src/domains/session/index.ts +84 -0
- package/src/domains/session/manager.ts +128 -0
- package/src/domains/session/manifest.ts +6 -0
- package/src/domains/session/migrations/index.ts +27 -0
- package/src/domains/session/prompt-manifest.ts +173 -0
- package/src/domains/session/protected-artifact-journal.ts +241 -0
- package/src/domains/session/protected-artifacts.ts +90 -0
- package/src/domains/session/retry.ts +230 -0
- package/src/domains/session/session-artifacts.ts +107 -0
- package/src/domains/session/task-board.ts +569 -0
- package/src/domains/session/tree/active-path.ts +137 -0
- package/src/domains/session/tree/fork.ts +159 -0
- package/src/domains/session/tree/manager.ts +128 -0
- package/src/domains/session/tree/navigator.ts +177 -0
- package/src/domains/session/tree/preview.ts +259 -0
- package/src/domains/session/usage.ts +148 -0
- package/src/domains/session/workspace/git-probe.ts +167 -0
- package/src/domains/session/workspace/index.ts +4 -0
- package/src/domains/session/workspace/project-type.ts +217 -0
- package/src/domains/session/workspace/snapshot.ts +74 -0
- package/src/domains/share/archive.ts +684 -0
- package/src/domains/share/contract.ts +9 -0
- package/src/domains/share/extension.ts +26 -0
- package/src/domains/share/index.ts +28 -0
- package/src/domains/share/manifest.ts +6 -0
- package/src/domains/user-tasks/store.ts +297 -0
- package/src/engine/acp/adapter.ts +397 -0
- package/src/engine/acp/errors.ts +128 -0
- package/src/engine/acp/event-mapper.ts +154 -0
- package/src/engine/acp/server.ts +2238 -0
- package/src/engine/acp/tool-mediator.ts +686 -0
- package/src/engine/acp/transport.ts +775 -0
- package/src/engine/acp/types.ts +196 -0
- package/src/engine/agent.ts +29 -0
- package/src/engine/ai.ts +207 -0
- package/src/engine/alcf-oauth.ts +205 -0
- package/src/engine/antigravity/subprocess-runtime.ts +205 -0
- package/src/engine/api-registry.ts +223 -0
- package/src/engine/apis/degraded-inference.ts +95 -0
- package/src/engine/apis/index.ts +16 -0
- package/src/engine/apis/llamacpp-residency.ts +292 -0
- package/src/engine/apis/lmstudio-residency.ts +76 -0
- package/src/engine/apis/lmstudio.ts +243 -0
- package/src/engine/apis/ollama-native.ts +528 -0
- package/src/engine/apis/openai-completions.ts +795 -0
- package/src/engine/apis/output-budget.ts +99 -0
- package/src/engine/apis/residency-lock.ts +29 -0
- package/src/engine/apis/residency.ts +776 -0
- package/src/engine/apis/resident-models.ts +42 -0
- package/src/engine/apis/sampling-overrides.ts +38 -0
- package/src/engine/apis/types.ts +16 -0
- package/src/engine/claude/sdk-runtime.ts +685 -0
- package/src/engine/claude/session-id.ts +5 -0
- package/src/engine/claude/subprocess-runtime.ts +316 -0
- package/src/engine/claude/tool-safety.ts +400 -0
- package/src/engine/env-api-keys.ts +98 -0
- package/src/engine/external-subprocess.ts +23 -0
- package/src/engine/gemma-channel-filter.ts +223 -0
- package/src/engine/harmony-response.ts +165 -0
- package/src/engine/instrumented-tui.ts +192 -0
- package/src/engine/loop-guard.ts +1127 -0
- package/src/engine/messages.ts +14 -0
- package/src/engine/models.ts +42 -0
- package/src/engine/oauth.ts +196 -0
- package/src/engine/pi-mono-names.ts +13 -0
- package/src/engine/prompt-templates.ts +1 -0
- package/src/engine/provider-payload.ts +272 -0
- package/src/engine/select-list.ts +53 -0
- package/src/engine/session.ts +834 -0
- package/src/engine/strip-tokenizer-sentinels.ts +125 -0
- package/src/engine/truncate.ts +9 -0
- package/src/engine/tui.ts +111 -0
- package/src/engine/types.ts +63 -0
- package/src/engine/worker-events.ts +100 -0
- package/src/engine/worker-runtime-capabilities.ts +5 -0
- package/src/engine/worker-runtime.ts +1022 -0
- package/src/engine/worker-tools.ts +208 -0
- package/src/entry/boot-options.ts +50 -0
- package/src/entry/orchestrator.ts +2011 -0
- package/src/interactive/application-controller.ts +433 -0
- package/src/interactive/bus-notices.ts +265 -0
- package/src/interactive/chat-loop-messages.ts +626 -0
- package/src/interactive/chat-loop-policy.ts +18 -0
- package/src/interactive/chat-loop.ts +1090 -0
- package/src/interactive/chat-panel.ts +1820 -0
- package/src/interactive/chat-renderer.ts +1228 -0
- package/src/interactive/clio-editor.ts +161 -0
- package/src/interactive/command-fallbacks.ts +23 -0
- package/src/interactive/command-output.ts +85 -0
- package/src/interactive/context-activity.ts +203 -0
- package/src/interactive/context-meter.ts +223 -0
- package/src/interactive/context-overlay.ts +246 -0
- package/src/interactive/cost-overlay.ts +279 -0
- package/src/interactive/dispatch-board.ts +1319 -0
- package/src/interactive/dispatch-steering.ts +86 -0
- package/src/interactive/editor-bash.ts +46 -0
- package/src/interactive/editor-steer.ts +51 -0
- package/src/interactive/editor-submit.ts +507 -0
- package/src/interactive/export-html/ansi-to-html.ts +161 -0
- package/src/interactive/export-html/index.ts +51 -0
- package/src/interactive/export-html/template.ts +45 -0
- package/src/interactive/export-html/tool-renderer.ts +54 -0
- package/src/interactive/external-editor.ts +67 -0
- package/src/interactive/follow-up-queue-panel.ts +75 -0
- package/src/interactive/footer/dashboard.ts +538 -0
- package/src/interactive/footer/notifications.ts +275 -0
- package/src/interactive/footer/widgets.ts +1264 -0
- package/src/interactive/footer-panel.ts +187 -0
- package/src/interactive/format-time.ts +97 -0
- package/src/interactive/index.ts +11 -0
- package/src/interactive/interactive-application.ts +939 -0
- package/src/interactive/interactive-event-projection.ts +385 -0
- package/src/interactive/interactive-input-runtime.ts +307 -0
- package/src/interactive/interactive-presentation.ts +584 -0
- package/src/interactive/interactive-shell.ts +388 -0
- package/src/interactive/interactive-slash-runtime.ts +554 -0
- package/src/interactive/interactive-subscriptions.ts +111 -0
- package/src/interactive/interactive-tickers.ts +183 -0
- package/src/interactive/keybinding-manager.ts +496 -0
- package/src/interactive/layout.ts +57 -0
- package/src/interactive/leader-key.ts +143 -0
- package/src/interactive/loop-guard-interrupt.ts +101 -0
- package/src/interactive/memory-overlay.ts +280 -0
- package/src/interactive/overlay-ask-user-lifecycle.ts +110 -0
- package/src/interactive/overlay-auth-lifecycle.ts +308 -0
- package/src/interactive/overlay-frame.ts +502 -0
- package/src/interactive/overlay-general-openers.ts +274 -0
- package/src/interactive/overlay-key-routing.ts +216 -0
- package/src/interactive/overlay-lifecycle.ts +426 -0
- package/src/interactive/overlay-model-selectors.ts +127 -0
- package/src/interactive/overlay-permission-lifecycle.ts +345 -0
- package/src/interactive/overlay-resource-openers.ts +112 -0
- package/src/interactive/overlay-session-lifecycle.ts +327 -0
- package/src/interactive/overlay-transitions.ts +57 -0
- package/src/interactive/overlays/agents.ts +69 -0
- package/src/interactive/overlays/ask-user.ts +921 -0
- package/src/interactive/overlays/auth-dialog.ts +221 -0
- package/src/interactive/overlays/context-reset.ts +108 -0
- package/src/interactive/overlays/cwd-fallback.ts +141 -0
- package/src/interactive/overlays/decisions.ts +300 -0
- package/src/interactive/overlays/extensions.ts +54 -0
- package/src/interactive/overlays/help-reference.ts +145 -0
- package/src/interactive/overlays/interop.ts +139 -0
- package/src/interactive/overlays/keybinding-detail.ts +90 -0
- package/src/interactive/overlays/list-overlay.ts +606 -0
- package/src/interactive/overlays/message-picker.ts +155 -0
- package/src/interactive/overlays/model-selector.ts +1109 -0
- package/src/interactive/overlays/prompts.ts +72 -0
- package/src/interactive/overlays/session-selector-search.ts +31 -0
- package/src/interactive/overlays/session-selector.ts +284 -0
- package/src/interactive/overlays/settings.ts +3451 -0
- package/src/interactive/overlays/skills-hub.ts +237 -0
- package/src/interactive/overlays/tree-selector.ts +422 -0
- package/src/interactive/permission-overlay.ts +170 -0
- package/src/interactive/render-trace.ts +534 -0
- package/src/interactive/renderers/branch-summary.ts +86 -0
- package/src/interactive/renderers/code-ink.ts +420 -0
- package/src/interactive/renderers/compaction-summary.ts +112 -0
- package/src/interactive/renderers/diff.ts +137 -0
- package/src/interactive/renderers/mermaid.ts +53 -0
- package/src/interactive/renderers/notice.ts +28 -0
- package/src/interactive/renderers/retry-status.ts +31 -0
- package/src/interactive/renderers/structured.ts +89 -0
- package/src/interactive/renderers/tool-execution.ts +1357 -0
- package/src/interactive/renderers/worker-entry.ts +280 -0
- package/src/interactive/session-last-turn.ts +186 -0
- package/src/interactive/session-switch-settlement.ts +10 -0
- package/src/interactive/session-transcript.ts +105 -0
- package/src/interactive/session-usage-reseed.ts +74 -0
- package/src/interactive/slash-autocomplete.ts +256 -0
- package/src/interactive/slash-commands.ts +1407 -0
- package/src/interactive/slash-spec.ts +424 -0
- package/src/interactive/status/controller.ts +260 -0
- package/src/interactive/status/index.ts +23 -0
- package/src/interactive/status/state-machine.ts +530 -0
- package/src/interactive/status/summary.ts +170 -0
- package/src/interactive/status/types.ts +121 -0
- package/src/interactive/status/verbs.ts +163 -0
- package/src/interactive/status/watchdog.ts +24 -0
- package/src/interactive/stdout-backpressure.ts +99 -0
- package/src/interactive/stream-pacer.ts +530 -0
- package/src/interactive/stream-pacing-policy.ts +66 -0
- package/src/interactive/tasks-overlay.ts +540 -0
- package/src/interactive/terminal-lease.ts +485 -0
- package/src/interactive/theme/components.ts +55 -0
- package/src/interactive/theme/glyphs.ts +70 -0
- package/src/interactive/theme/index.ts +15 -0
- package/src/interactive/theme/labels.ts +88 -0
- package/src/interactive/theme/rules.ts +138 -0
- package/src/interactive/theme/segments.ts +88 -0
- package/src/interactive/theme/tokens.ts +121 -0
- package/src/interactive/thinking-level-policy.ts +6 -0
- package/src/interactive/tool-prose-loop.ts +122 -0
- package/src/interactive/tool-prose-registration.ts +48 -0
- package/src/interactive/turn-context.ts +855 -0
- package/src/interactive/turn-middleware.ts +283 -0
- package/src/interactive/turn-persistence.ts +458 -0
- package/src/interactive/turn-queues.ts +181 -0
- package/src/interactive/turn-recovery.ts +323 -0
- package/src/interactive/turn-runtime.ts +869 -0
- package/src/interactive/turn-state.ts +133 -0
- package/src/interactive/view/artifacts.ts +1452 -0
- package/src/interactive/view/view-overlay.ts +735 -0
- package/src/interactive/welcome-dashboard.ts +537 -0
- package/src/interactive/worker-receipts.ts +143 -0
- package/src/interactive/worker-replay.ts +111 -0
- package/src/interactive/worker-share.ts +100 -0
- package/src/interactive/worker-stream.ts +446 -0
- package/src/interactive/workspace-facts.ts +141 -0
- package/src/tools/agent-tools.ts +402 -0
- package/src/tools/artifact.ts +103 -0
- package/src/tools/ask-user.ts +666 -0
- package/src/tools/bash.ts +204 -0
- package/src/tools/bootstrap.ts +77 -0
- package/src/tools/builtin-tool-catalog.ts +265 -0
- package/src/tools/codewiki/code-nav-surface.ts +29 -0
- package/src/tools/codewiki/code-nav.ts +549 -0
- package/src/tools/codewiki/shared.ts +118 -0
- package/src/tools/compete-worktrees.ts +898 -0
- package/src/tools/context/docs-engine.ts +630 -0
- package/src/tools/context/index.ts +514 -0
- package/src/tools/context/surface.ts +19 -0
- package/src/tools/core-bootstrap.ts +168 -0
- package/src/tools/credential-present.ts +151 -0
- package/src/tools/dispatch-admission.ts +533 -0
- package/src/tools/dispatch-arguments.ts +228 -0
- package/src/tools/dispatch-background.ts +54 -0
- package/src/tools/dispatch-event-text.ts +6 -0
- package/src/tools/dispatch-plan.ts +746 -0
- package/src/tools/dispatch-run-events.ts +238 -0
- package/src/tools/dispatch-runner.ts +2370 -0
- package/src/tools/dispatch-scout-admission.ts +295 -0
- package/src/tools/dispatch-scout.ts +461 -0
- package/src/tools/dispatch-types.ts +77 -0
- package/src/tools/dispatch.ts +244 -0
- package/src/tools/edit-diff.ts +534 -0
- package/src/tools/edit.ts +128 -0
- package/src/tools/executables.ts +22 -0
- package/src/tools/file-mutation-queue.ts +35 -0
- package/src/tools/find.ts +294 -0
- package/src/tools/grep.ts +495 -0
- package/src/tools/ignore-policy.ts +169 -0
- package/src/tools/lazy-tool.ts +60 -0
- package/src/tools/ledger.ts +206 -0
- package/src/tools/ls.ts +110 -0
- package/src/tools/monitor-surface.ts +36 -0
- package/src/tools/monitor.ts +767 -0
- package/src/tools/network-policy.ts +29 -0
- package/src/tools/observation.ts +421 -0
- package/src/tools/observers.ts +99 -0
- package/src/tools/path-utils.ts +79 -0
- package/src/tools/policy.ts +154 -0
- package/src/tools/profiles.ts +108 -0
- package/src/tools/read.ts +173 -0
- package/src/tools/registry.ts +1169 -0
- package/src/tools/result-shaping.ts +204 -0
- package/src/tools/safe-exec.ts +128 -0
- package/src/tools/spawn-hygiene.ts +182 -0
- package/src/tools/steer-surface.ts +17 -0
- package/src/tools/steer.ts +77 -0
- package/src/tools/tasks.ts +265 -0
- package/src/tools/truncate-utf8.ts +17 -0
- package/src/tools/truncate.ts +43 -0
- package/src/tools/verify/frontend.ts +687 -0
- package/src/tools/verify/index.ts +31 -0
- package/src/tools/verify/scripts.ts +108 -0
- package/src/tools/verify/surface.ts +46 -0
- package/src/tools/web-fetch-surface.ts +23 -0
- package/src/tools/web-fetch.ts +759 -0
- package/src/tools/worker-evidence.ts +215 -0
- package/src/tools/write.ts +57 -0
- package/src/utils/exif-orientation.ts +192 -0
- package/src/utils/git.ts +24 -0
- package/src/utils/image-resize.ts +148 -0
- package/src/utils/photon.ts +103 -0
- package/src/worker/control-lane.ts +22 -0
- package/src/worker/entry.ts +294 -0
- package/src/worker/event-projection.ts +56 -0
- package/src/worker/heartbeat.ts +24 -0
- package/src/worker/ledger-mirror.ts +95 -0
- package/src/worker/ndjson.ts +40 -0
- package/src/worker/protocol.ts +529 -0
- package/src/worker/resource-facts.ts +79 -0
- package/src/worker/runtime-registry.ts +12 -0
- package/src/worker/spec-contract.ts +675 -0
- package/src/worker/stdin-demux.ts +316 -0
- package/dist/acp-UMLFVA3F.js +0 -92
- package/dist/chunk-262G75JS.js +0 -35
- package/dist/chunk-6CWDTGUC.js +0 -20
- package/dist/chunk-BYEU76JP.js +0 -899
- package/dist/chunk-GNGDQYDU.js +0 -34688
- package/dist/chunk-GOTUR54M.js +0 -9
- package/dist/chunk-NI4DEJMC.js +0 -88
- package/dist/chunk-TPOCL34A.js +0 -2876
- package/dist/chunk-UU3R62TT.js +0 -128
- package/dist/clio-KIQ5SNDS.js +0 -53
- package/dist/configure-G4E3A2PG.js +0 -27
- package/dist/doctor-M5HJJZOL.js +0 -61
- package/dist/domains/agents/builtins/architect.md +0 -33
- package/dist/domains/agents/builtins/coder.md +0 -31
- package/dist/domains/agents/builtins/context-bootstrap.md +0 -38
- package/dist/domains/agents/builtins/debugger.md +0 -30
- package/dist/domains/agents/builtins/documenter.md +0 -31
- package/dist/domains/agents/builtins/git-master.md +0 -30
- package/dist/domains/agents/builtins/provenance.md +0 -30
- package/dist/domains/agents/builtins/researcher.md +0 -71
- package/dist/domains/agents/builtins/scout.md +0 -42
- package/dist/domains/agents/builtins/tester.md +0 -31
- package/dist/domains/agents/builtins/verifier.md +0 -30
- package/dist/domains/agents/builtins/wiki-writer.md +0 -41
- package/dist/fleet-preflight-CLIAX7YR.js +0 -21
- package/dist/preload-KZVHET2B.js +0 -11
- package/dist/version-ROZ6CZKH.js +0 -16
- package/docs/html/agents_blueprint.html +0 -936
- package/docs/html/alcf_blueprint.html +0 -324
- package/docs/html/architecture_blueprint.html +0 -850
- package/docs/html/commands_blueprint.html +0 -794
- package/docs/html/config_knobs_audit_blueprint.html +0 -178
- package/docs/html/configuration_blueprint.html +0 -1080
- package/docs/html/context_blueprint.html +0 -603
- package/docs/html/documentation_blueprint.html +0 -832
- package/docs/html/environment_blueprint.html +0 -404
- package/docs/html/eval_blueprint.html +0 -743
- package/docs/html/evals_internal_blueprint.html +0 -190
- package/docs/html/evolution_blueprint.html +0 -674
- package/docs/html/extensions_blueprint.html +0 -2065
- package/docs/html/fleet_dispatch_blueprint.html +0 -286
- package/docs/html/index.html +0 -919
- package/docs/html/lifecycle_blueprint.html +0 -723
- package/docs/html/memory_blueprint.html +0 -699
- package/docs/html/middleware_blueprint.html +0 -664
- package/docs/html/models_blueprint.html +0 -2366
- package/docs/html/observability_blueprint.html +0 -683
- package/docs/html/provider_adapter_blueprint.html +0 -245
- package/docs/html/safety_blueprint.html +0 -1386
- package/docs/html/shared.css +0 -571
- package/docs/html/shared.js +0 -143
- package/docs/html/skills_blueprint.html +0 -671
- package/docs/html/soak_blueprint.html +0 -182
- package/docs/html/tool_usage_blueprint.html +0 -350
- package/docs/html/tools_blueprint.html +0 -2249
- package/docs/html/trace_blueprint.html +0 -235
- package/docs/html/tui_design_blueprint.html +0 -314
- package/docs/html/validation_blueprint.html +0 -961
- package/docs/html/worker_dispatch_blueprint.html +0 -231
- package/skills/git/commit-crafting/SKILL.md +0 -79
- package/skills/git/commit-crafting/evals.md +0 -92
- package/skills/git/create-pr/SKILL.md +0 -116
- package/skills/git/create-pr/evals.md +0 -114
- package/skills/git/investigate-issue/SKILL.md +0 -139
- package/skills/git/investigate-issue/evals.md +0 -94
- package/skills/git/review-changes/SKILL.md +0 -103
- package/skills/git/review-changes/evals.md +0 -85
|
@@ -0,0 +1,3451 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bindAgentProfileInSettings,
|
|
3
|
+
type ClioSettings,
|
|
4
|
+
removeFleetProfileFromSettings,
|
|
5
|
+
removeTargetFromSettings,
|
|
6
|
+
setFleetProfileInSettings,
|
|
7
|
+
useTargetInSettings,
|
|
8
|
+
} from "../../core/config.js";
|
|
9
|
+
import { DEFAULT_SETTINGS, THINKING_LEVELS } from "../../core/defaults.js";
|
|
10
|
+
import { getAtPath, isRoutingPath } from "../../core/session-routing.js";
|
|
11
|
+
import { MAX_TIMER_DELAY_MS } from "../../core/timers.js";
|
|
12
|
+
import {
|
|
13
|
+
delegationEntryForKind,
|
|
14
|
+
type InteropAgentId,
|
|
15
|
+
type InteropProposal,
|
|
16
|
+
interopAgentKind,
|
|
17
|
+
} from "../../domains/interop/index.js";
|
|
18
|
+
import {
|
|
19
|
+
type CapabilityFlags,
|
|
20
|
+
isDispatchEligibleRuntime,
|
|
21
|
+
isOrchestratorEligibleRuntime,
|
|
22
|
+
type ProvidersContract,
|
|
23
|
+
resolveModelRuntimeCapabilitiesForProviders,
|
|
24
|
+
type TargetHealth,
|
|
25
|
+
type TargetStatus,
|
|
26
|
+
thinkingLevelChoiceLabel,
|
|
27
|
+
thinkingLevelFromChoiceLabel,
|
|
28
|
+
} from "../../domains/providers/index.js";
|
|
29
|
+
import type { FleetNodeSnapshot } from "../../domains/scheduling/cluster.js";
|
|
30
|
+
import {
|
|
31
|
+
type Component,
|
|
32
|
+
getKeybindings,
|
|
33
|
+
Input,
|
|
34
|
+
isKeyRelease,
|
|
35
|
+
matchesKey,
|
|
36
|
+
type OverlayHandle,
|
|
37
|
+
SelectList,
|
|
38
|
+
type SettingItem,
|
|
39
|
+
type TUI,
|
|
40
|
+
truncateToWidth,
|
|
41
|
+
visibleWidth,
|
|
42
|
+
wrapTextWithAnsi,
|
|
43
|
+
} from "../../engine/tui.js";
|
|
44
|
+
import { clockLocal } from "../format-time.js";
|
|
45
|
+
import { buildHint, DEFAULT_SELECT_THEME, showClioOverlayFrame } from "../overlay-frame.js";
|
|
46
|
+
import { barSep, clioTheme, GLYPH, padAnsi, rule, screenTitle } from "../theme/index.js";
|
|
47
|
+
import { modelsForTarget } from "./model-selector.js";
|
|
48
|
+
|
|
49
|
+
export const SETTINGS_OVERLAY_WIDTH = "100%";
|
|
50
|
+
export const SETTINGS_OVERLAY_MAX_HEIGHT = "100%";
|
|
51
|
+
export const SETTINGS_OVERLAY_MARGIN = {
|
|
52
|
+
top: 1,
|
|
53
|
+
right: 2,
|
|
54
|
+
bottom: 1,
|
|
55
|
+
left: 2,
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
const SECTION_LANE_WIDTH = 24;
|
|
59
|
+
/**
|
|
60
|
+
* The width at which the live description earns its own column instead of a
|
|
61
|
+
* footer strip. Same arithmetic as WIDE_LAYOUT_MIN_WIDTH below: two nested
|
|
62
|
+
* frames sit above this body, each costing a border and a pad, so the body is
|
|
63
|
+
* the terminal width less eight. A 120-column terminal renders at 112 and a
|
|
64
|
+
* 119-column one at 111, so 112 is the floor that makes three columns start at
|
|
65
|
+
* a real 120-column terminal.
|
|
66
|
+
*/
|
|
67
|
+
const ULTRAWIDE_LAYOUT_MIN_WIDTH = 112;
|
|
68
|
+
/**
|
|
69
|
+
* The design's degradation matrix wants the two-column layout at an 80-column
|
|
70
|
+
* terminal. Two nested frames sit above this body (the application frame and
|
|
71
|
+
* the overlay's own), each costing a border and a pad, so the body is the
|
|
72
|
+
* terminal width less eight: 72 at an 80-column terminal, 68 at a 76-column
|
|
73
|
+
* one. 72 therefore keeps an 80-column terminal two-column (left lane 24 +
|
|
74
|
+
* divider + rows ~47, with the key-path column dropped below
|
|
75
|
+
* DROP_PATH_COLUMN_WIDTH) while anything narrower, where the value column
|
|
76
|
+
* would collapse into the labels, stays stacked.
|
|
77
|
+
*/
|
|
78
|
+
const WIDE_LAYOUT_MIN_WIDTH = 72;
|
|
79
|
+
const DROP_PATH_COLUMN_WIDTH = 52;
|
|
80
|
+
/**
|
|
81
|
+
* Terminal width below which Settings drops its two-cell side margins.
|
|
82
|
+
*
|
|
83
|
+
* The engine composites an overlay only across the columns its region covers,
|
|
84
|
+
* and a left/right margin shrinks that region, so at 40 columns four of them
|
|
85
|
+
* kept showing the transcript beside a modal that owns the keyboard. Above this
|
|
86
|
+
* width the margins are cheap and the box reads better inset; below it the
|
|
87
|
+
* overlay claims every terminal column instead.
|
|
88
|
+
*/
|
|
89
|
+
const ULTRA_NARROW_TERMINAL_WIDTH = 60;
|
|
90
|
+
/** Shown when no runtime is resolvable, so it offers the full vocabulary. */
|
|
91
|
+
const FALLBACK_THINKING_VALUES: ReadonlyArray<string> = THINKING_LEVELS;
|
|
92
|
+
const ROW_GAP = " ";
|
|
93
|
+
/**
|
|
94
|
+
* The product's cut marker, as the help center and every list overlay use it.
|
|
95
|
+
*
|
|
96
|
+
* A setting key is an identifier the operator types into settings.yaml, and the
|
|
97
|
+
* panel rendered `delegation.defaults.toolGovernanc` for
|
|
98
|
+
* `delegation.defaults.toolGovernance` with nothing to say it was short. An
|
|
99
|
+
* unmarked cut presents a fragment as the whole value, so every cell, row, and
|
|
100
|
+
* explanation line in this overlay carries the marker.
|
|
101
|
+
*/
|
|
102
|
+
const ELLIPSIS = "…";
|
|
103
|
+
const SELECT_UP = "\u001b[A";
|
|
104
|
+
const SELECT_DOWN = "\u001b[B";
|
|
105
|
+
|
|
106
|
+
/** Settings pickers accept the same j/k navigation as the rows that open them. */
|
|
107
|
+
class SettingsSelectList extends SelectList {
|
|
108
|
+
override handleInput(data: string): void {
|
|
109
|
+
super.handleInput(data === "j" ? SELECT_DOWN : data === "k" ? SELECT_UP : data);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Scope tells the operator where an edit lands. Derived from the config-change
|
|
115
|
+
* classification (src/domains/config/classify.ts): hotReload/nextTurn knobs can
|
|
116
|
+
* apply to the live session immediately, so they offer "this session only" vs
|
|
117
|
+
* "save as the global default". restartRequired knobs cannot apply live, so the
|
|
118
|
+
* overlay only offers a global save that a restart picks up.
|
|
119
|
+
*/
|
|
120
|
+
type SettingScope = "live" | "restart";
|
|
121
|
+
const RESTART_REQUIRED_IDS = new Set<string>([
|
|
122
|
+
"budget.concurrency",
|
|
123
|
+
"runtimePlugins",
|
|
124
|
+
"terminal.tuiMode",
|
|
125
|
+
"terminal.fullscreenScrollbar",
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
export const SETTINGS_SECTIONS = [
|
|
129
|
+
{ id: "safety", label: "Autonomy & Safety", group: "CORE" },
|
|
130
|
+
{ id: "orchestrator", label: "Orchestrator", group: "CORE" },
|
|
131
|
+
{ id: "fleet", label: "Fleet", group: "ROUTING" },
|
|
132
|
+
{ id: "targets", label: "Targets", group: "ROUTING" },
|
|
133
|
+
{ id: "models", label: "Models", group: "ROUTING" },
|
|
134
|
+
{ id: "budget", label: "Budget", group: "RUNTIME" },
|
|
135
|
+
{ id: "compaction", label: "Compaction", group: "RUNTIME" },
|
|
136
|
+
{ id: "retry", label: "Retry", group: "RUNTIME" },
|
|
137
|
+
{ id: "terminal", label: "Terminal", group: "EXPERIENCE" },
|
|
138
|
+
{ id: "advanced", label: "Advanced", group: "EXPERIENCE" },
|
|
139
|
+
] as const;
|
|
140
|
+
|
|
141
|
+
export type SettingsSectionId = (typeof SETTINGS_SECTIONS)[number]["id"];
|
|
142
|
+
|
|
143
|
+
const SETTINGS_SECTION_DESCRIPTIONS = {
|
|
144
|
+
safety: "How freely Clio acts, and how delegated agents' tools are governed.",
|
|
145
|
+
orchestrator: "Interactive chat routing and the optional proactive-memory model plane.",
|
|
146
|
+
fleet: "Defaults, profiles, and agent bindings applied to dispatched workers, and where they run.",
|
|
147
|
+
targets: "Configured inference targets: which one chat and the fleet use, and whether each answers.",
|
|
148
|
+
models: "The /model picker, favorites, and Alt+J / Alt+K cycling.",
|
|
149
|
+
budget: "Cost ceiling, per-turn output budget, and worker concurrency.",
|
|
150
|
+
compaction: "When and how the context window is summarized under pressure.",
|
|
151
|
+
retry: "Automatic recovery from transient provider and network errors.",
|
|
152
|
+
terminal: "Terminal integration and the Clio color palette.",
|
|
153
|
+
advanced: "Commit provenance, runtime plugins, delegation timeouts, and links to other surfaces.",
|
|
154
|
+
} as const satisfies Record<SettingsSectionId, string>;
|
|
155
|
+
|
|
156
|
+
export const SETTINGS_LABELS_BY_ID = {
|
|
157
|
+
autonomy: "Autonomy level",
|
|
158
|
+
// Labels follow the CLI's post-rename vocabulary: the config surface is the
|
|
159
|
+
// fleet, and `worker` is the runtime entity the descriptions still name. A
|
|
160
|
+
// section headed Fleet whose rows read "Worker profiles" and "Worker
|
|
161
|
+
// retries" made one setting look like two subsystems.
|
|
162
|
+
"workers.onPermission": "Fleet approvals routing",
|
|
163
|
+
"delegation.defaults.toolGovernance": "Delegation governance",
|
|
164
|
+
"skills.trustProjectCompatRoots": "Trust project skill roots",
|
|
165
|
+
"attribution.gitCommits": "Clio commit provenance",
|
|
166
|
+
safetyNet: "Safety net",
|
|
167
|
+
"orchestrator.thinkingLevel": "Thinking level",
|
|
168
|
+
"orchestrator.target": "Target",
|
|
169
|
+
"orchestrator.model": "Model",
|
|
170
|
+
"background.target": "Memory target",
|
|
171
|
+
"background.model": "Memory model",
|
|
172
|
+
"background.thinkingLevel": "Memory thinking level",
|
|
173
|
+
"memory.intervention.enabled": "Proactive memory",
|
|
174
|
+
"memory.intervention.everyNTools": "Memory cadence (tools)",
|
|
175
|
+
"memory.intervention.windowSteps": "Memory trajectory steps",
|
|
176
|
+
"memory.intervention.maxTokens": "Memory reminder tokens",
|
|
177
|
+
"memory.intervention.timeoutMs": "Memory timeout (ms)",
|
|
178
|
+
"workers.default.target": "Default target",
|
|
179
|
+
"workers.default.model": "Default model",
|
|
180
|
+
"workers.default.thinkingLevel": "Default thinking level",
|
|
181
|
+
"workers.profiles": "Add profile",
|
|
182
|
+
"workers.agentBindings": "Add agent route",
|
|
183
|
+
"workers.maxRetries": "Fleet retries",
|
|
184
|
+
scope: "Model cycle set",
|
|
185
|
+
"modelSelector.recentLimit": "Recent models kept",
|
|
186
|
+
"modelSelector.favorites": "Pinned favorites",
|
|
187
|
+
"budget.sessionCeilingUsd": "Session ceiling (USD)",
|
|
188
|
+
"defaults.maxTokens": "Output budget (tokens)",
|
|
189
|
+
"budget.concurrency": "Fleet concurrency",
|
|
190
|
+
"compaction.auto": "Auto-compact",
|
|
191
|
+
"compaction.excludeLastTurns": "Protected recent turns",
|
|
192
|
+
"compaction.threshold": "Compaction threshold",
|
|
193
|
+
"retry.enabled": "Retry transient errors",
|
|
194
|
+
"retry.maxRetries": "Max retries",
|
|
195
|
+
"retry.baseDelayMs": "Base delay (ms)",
|
|
196
|
+
"retry.maxDelayMs": "Max delay (ms)",
|
|
197
|
+
"terminal.showTerminalProgress": "Terminal progress badges",
|
|
198
|
+
"terminal.outputVerbosity": "Output detail",
|
|
199
|
+
"terminal.tuiMode": "TUI mode",
|
|
200
|
+
"terminal.fullscreenScrollbar": "Fullscreen scrollbar",
|
|
201
|
+
"terminal.smoothStreaming": "Smooth streaming",
|
|
202
|
+
theme: "Theme",
|
|
203
|
+
runtimePlugins: "Runtime plugins",
|
|
204
|
+
"compaction.model": "Compaction model",
|
|
205
|
+
"compaction.systemPrompt": "Compaction prompt",
|
|
206
|
+
"delegation.defaults.connectTimeoutMs": "Delegate connect (ms)",
|
|
207
|
+
"delegation.defaults.turnTimeoutMs": "Delegate turn (ms)",
|
|
208
|
+
"delegation.defaults.permissionTimeoutMs": "Delegate permission (ms)",
|
|
209
|
+
targets: "Configured targets",
|
|
210
|
+
keybindings: "Keybinding overrides",
|
|
211
|
+
"delegation.agents": "Delegation agents",
|
|
212
|
+
} as const;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Entry rows (one per fleet profile field, agent binding, target, fleet node)
|
|
216
|
+
* are keyed by the config path they edit; fleet node rows are read-only status.
|
|
217
|
+
*/
|
|
218
|
+
type EntrySettingId =
|
|
219
|
+
| `workers.profiles.${string}`
|
|
220
|
+
| `workers.agentBindings.${string}`
|
|
221
|
+
| `targets.${string}`
|
|
222
|
+
| `fleet.nodes.${string}`;
|
|
223
|
+
export type EditableSettingId = keyof typeof SETTINGS_LABELS_BY_ID | EntrySettingId;
|
|
224
|
+
type FleetGroupHeaderId = `fleet.group.${"defaults" | "profiles" | "agent-routes" | "placement"}`;
|
|
225
|
+
type TargetsCtaId = "targets.add-cta";
|
|
226
|
+
export type SettingsCenterRowId = EditableSettingId | FleetGroupHeaderId | TargetsCtaId;
|
|
227
|
+
const REMOVE_PROFILE_CHOICE = "(remove profile)";
|
|
228
|
+
const UNBIND_CHOICE = "(unbind)";
|
|
229
|
+
const AUTO_PLACEMENT_CHOICE = "(auto placement)";
|
|
230
|
+
const PROFILE_FIELD_SEPARATOR = " -> ";
|
|
231
|
+
const PROFILE_SUMMARY_VALUE_BUDGET = 26;
|
|
232
|
+
|
|
233
|
+
export const SETTINGS_SECTION_ROWS = {
|
|
234
|
+
safety: [
|
|
235
|
+
"autonomy",
|
|
236
|
+
"workers.onPermission",
|
|
237
|
+
"delegation.defaults.toolGovernance",
|
|
238
|
+
"skills.trustProjectCompatRoots",
|
|
239
|
+
"safetyNet",
|
|
240
|
+
],
|
|
241
|
+
orchestrator: [
|
|
242
|
+
"orchestrator.thinkingLevel",
|
|
243
|
+
"orchestrator.target",
|
|
244
|
+
"orchestrator.model",
|
|
245
|
+
"background.target",
|
|
246
|
+
"background.model",
|
|
247
|
+
"background.thinkingLevel",
|
|
248
|
+
"memory.intervention.enabled",
|
|
249
|
+
"memory.intervention.everyNTools",
|
|
250
|
+
"memory.intervention.windowSteps",
|
|
251
|
+
"memory.intervention.maxTokens",
|
|
252
|
+
"memory.intervention.timeoutMs",
|
|
253
|
+
],
|
|
254
|
+
fleet: [
|
|
255
|
+
"workers.default.target",
|
|
256
|
+
"workers.default.model",
|
|
257
|
+
"workers.default.thinkingLevel",
|
|
258
|
+
"workers.maxRetries",
|
|
259
|
+
"workers.profiles",
|
|
260
|
+
"workers.agentBindings",
|
|
261
|
+
],
|
|
262
|
+
models: ["scope", "modelSelector.recentLimit", "modelSelector.favorites"],
|
|
263
|
+
budget: ["budget.sessionCeilingUsd", "defaults.maxTokens", "budget.concurrency"],
|
|
264
|
+
compaction: ["compaction.auto", "compaction.threshold", "compaction.excludeLastTurns"],
|
|
265
|
+
retry: ["retry.enabled", "retry.maxRetries", "retry.baseDelayMs", "retry.maxDelayMs"],
|
|
266
|
+
terminal: [
|
|
267
|
+
"terminal.showTerminalProgress",
|
|
268
|
+
"terminal.outputVerbosity",
|
|
269
|
+
"terminal.tuiMode",
|
|
270
|
+
"terminal.fullscreenScrollbar",
|
|
271
|
+
"terminal.smoothStreaming",
|
|
272
|
+
"theme",
|
|
273
|
+
],
|
|
274
|
+
advanced: [
|
|
275
|
+
"runtimePlugins",
|
|
276
|
+
"attribution.gitCommits",
|
|
277
|
+
"compaction.model",
|
|
278
|
+
"compaction.systemPrompt",
|
|
279
|
+
"delegation.defaults.connectTimeoutMs",
|
|
280
|
+
"delegation.defaults.turnTimeoutMs",
|
|
281
|
+
"delegation.defaults.permissionTimeoutMs",
|
|
282
|
+
"keybindings",
|
|
283
|
+
"delegation.agents",
|
|
284
|
+
],
|
|
285
|
+
targets: ["targets"],
|
|
286
|
+
} as const satisfies Record<SettingsSectionId, readonly EditableSettingId[]>;
|
|
287
|
+
|
|
288
|
+
const SETTINGS_DESCRIPTIONS_BY_ID = {
|
|
289
|
+
autonomy: "How freely Clio acts; the safety net always applies.",
|
|
290
|
+
"workers.onPermission":
|
|
291
|
+
"How a worker resolves an approval ask: deny the call, fail the run, or escalate to this session.",
|
|
292
|
+
"delegation.defaults.toolGovernance": "Tool policy for delegated external agents.",
|
|
293
|
+
"skills.trustProjectCompatRoots": "Whether third-party project skill roots are loaded.",
|
|
294
|
+
"attribution.gitCommits":
|
|
295
|
+
"Add evidence-backed assistance, testing, review, and contributor trailers to commits created through Clio.",
|
|
296
|
+
safetyNet: "Always-on rails; tuned in .clio-coder/safety.yaml.",
|
|
297
|
+
"orchestrator.thinkingLevel": "Reasoning budget for the chat loop.",
|
|
298
|
+
"orchestrator.target": "Active chat target id.",
|
|
299
|
+
"orchestrator.model": "Active chat wire model id.",
|
|
300
|
+
"background.target": "Optional target for LLM memory steps; unset keeps rules-only memory.",
|
|
301
|
+
"background.model": "Small non-reasoning model used only for task memory steps.",
|
|
302
|
+
"background.thinkingLevel": "Unused: memory steps always request thinking off.",
|
|
303
|
+
"memory.intervention.enabled": "Master switch for rules-only and model-backed task memory.",
|
|
304
|
+
"memory.intervention.everyNTools": "Maximum tool executions between prompted memory steps.",
|
|
305
|
+
"memory.intervention.windowSteps": "Recent completed tool steps visible to the memory policy.",
|
|
306
|
+
"memory.intervention.maxTokens": "Hard cap for one visible memory reminder.",
|
|
307
|
+
"memory.intervention.timeoutMs": "Hard deadline for one background-model memory call.",
|
|
308
|
+
"workers.default.target": "Default /run target id.",
|
|
309
|
+
"workers.default.model": "Default /run wire model id.",
|
|
310
|
+
"workers.default.thinkingLevel": "Reasoning budget for dispatched workers.",
|
|
311
|
+
"workers.profiles": "Named target/model/thinking choices that native workers can use. Enter adds one.",
|
|
312
|
+
"workers.agentBindings": "Pins native Clio agents, including shadow agents, to worker profiles. Enter adds one.",
|
|
313
|
+
"workers.maxRetries": "Automatic retries for a retryable worker outcome.",
|
|
314
|
+
scope: "Alt+J and Alt+K model cycle set.",
|
|
315
|
+
"modelSelector.recentLimit": "How many recently used models /model remembers.",
|
|
316
|
+
"modelSelector.favorites": "Exact target/model refs pinned in /model.",
|
|
317
|
+
"budget.sessionCeilingUsd": "Per-session cost cap.",
|
|
318
|
+
"defaults.maxTokens": "Output tokens requested per turn, applied to every target.",
|
|
319
|
+
"budget.concurrency": "Parallel workers allowed during dispatch.",
|
|
320
|
+
"compaction.auto": "Auto-compact before a turn when context crosses the threshold.",
|
|
321
|
+
"compaction.excludeLastTurns": "Recent user turns protected from observation masking.",
|
|
322
|
+
"compaction.threshold": "Pressure at which compaction masks stale observations, then summarizes.",
|
|
323
|
+
"retry.enabled": "Retry transient provider errors on the next submit.",
|
|
324
|
+
"retry.maxRetries": "Retry attempts after the initial failure.",
|
|
325
|
+
"retry.baseDelayMs": "Initial retry delay in milliseconds.",
|
|
326
|
+
"retry.maxDelayMs": "Maximum retry delay in milliseconds.",
|
|
327
|
+
"terminal.showTerminalProgress": "Emit OSC 9;4 progress badges during agent turns.",
|
|
328
|
+
"terminal.outputVerbosity": "How much reasoning, tool input, and live tool output appears in the transcript.",
|
|
329
|
+
"terminal.tuiMode": "Use regular terminal scrollback or a fullscreen transcript with a sticky composer and footer.",
|
|
330
|
+
"terminal.fullscreenScrollbar": "When the draggable transcript scrollbar is visible in fullscreen mode.",
|
|
331
|
+
"terminal.smoothStreaming": "Presentation-only pacing for streamed assistant text and thinking.",
|
|
332
|
+
theme: "Color palette. Clio ships a single tuned palette.",
|
|
333
|
+
runtimePlugins: "npm packages exporting clioRuntimes: RuntimeDescriptor[].",
|
|
334
|
+
"compaction.model": "Dedicated summarization model; blank uses the orchestrator.",
|
|
335
|
+
"compaction.systemPrompt": "Path to a compaction prompt override; blank uses the built-in.",
|
|
336
|
+
"delegation.defaults.connectTimeoutMs": "How long to wait for a delegated agent to connect.",
|
|
337
|
+
"delegation.defaults.turnTimeoutMs": "How long a single delegated turn may run.",
|
|
338
|
+
"delegation.defaults.permissionTimeoutMs": "How long a delegated permission ask may wait.",
|
|
339
|
+
targets: "Inference targets available for chat and workers. Add one with `clio-coder targets add`.",
|
|
340
|
+
keybindings: "Custom key overrides layered on the defaults.",
|
|
341
|
+
"delegation.agents": "External ACP agents available to /delegate.",
|
|
342
|
+
} as const satisfies Record<EditableSettingId, string>;
|
|
343
|
+
|
|
344
|
+
/** Longer, optional guidance shown beneath the one-line description when there is room. */
|
|
345
|
+
const SETTINGS_HELP_BY_ID: Partial<Record<EditableSettingId, string>> = {
|
|
346
|
+
autonomy:
|
|
347
|
+
"read-only observes; suggest parks non-read calls; auto-edit edits, dispatches, and runs recognized commands; full-auto runs except command substitution and system-level changes. A confirmation marked exposure=outward parks for you at suggest and auto-edit.",
|
|
348
|
+
"defaults.maxTokens":
|
|
349
|
+
"Clamped down to each model's max-output cap and the remaining context window. Set 0 to use per-model caps only.",
|
|
350
|
+
"compaction.threshold":
|
|
351
|
+
"pressure = estimated tokens ÷ context window. Higher keeps more history but risks overflow before a summary runs.",
|
|
352
|
+
"budget.concurrency": "auto sizes to your machine. A fixed number caps how many workers run at once.",
|
|
353
|
+
"skills.trustProjectCompatRoots":
|
|
354
|
+
"Project roots like .claude/skills and .codex/skills are untrusted by default; enabling exposes them to the model.",
|
|
355
|
+
"attribution.gitCommits":
|
|
356
|
+
"Role trailers are added only when Clio has trusted evidence for that role. Disabling leaves subsequent commit messages entirely unchanged.",
|
|
357
|
+
"workers.onPermission":
|
|
358
|
+
"deny turns the ask into a tool denial and the run continues; fail stops the run as permission_required; escalate forwards the ask to you and falls back per workers.escalation on timeout.",
|
|
359
|
+
"workers.agentBindings":
|
|
360
|
+
"Bind base, custom, and shadow native agents such as scout, researcher, and provenance to profiles. ACP delegation agents cannot be bound.",
|
|
361
|
+
"delegation.defaults.toolGovernance":
|
|
362
|
+
"clio-policy gates the agent through Clio's safety net; agent-managed trusts the agent; deny-all blocks every tool.",
|
|
363
|
+
scope: "Choose target-level or exact target/model refs. Alt+J / Alt+K step the chat target through this list.",
|
|
364
|
+
runtimePlugins: "Comma-separated package names, loaded at startup. Restart Clio after changing.",
|
|
365
|
+
keybindings:
|
|
366
|
+
"Renderer controls: Alt+O newest tool or worker details, Ctrl+Alt+O or Alt+Shift+O all of them, Alt+P live tool output, Alt+R latest reasoning, Ctrl+Alt+R or Alt+Shift+R all reasoning. Override these in settings.yaml or use /help.",
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
/** Per-value meaning, surfaced for the current value of an enum knob. */
|
|
370
|
+
const SETTINGS_VALUE_HELP_BY_ID: Partial<Record<EditableSettingId, Record<string, string>>> = {
|
|
371
|
+
autonomy: {
|
|
372
|
+
"read-only": "observe and answer only; never edits files or runs commands",
|
|
373
|
+
suggest:
|
|
374
|
+
"propose every edit and command for your approval; confirmations marked exposure=outward (filing an issue or PR, pushing, releasing) park here as well",
|
|
375
|
+
"auto-edit":
|
|
376
|
+
"edits and dispatches run; recognized commands (tests, lint, build, .clio-coder/safety.yaml entries) run; other commands ask, as do confirmations marked exposure=outward (filing an issue or PR, pushing, releasing)",
|
|
377
|
+
"full-auto":
|
|
378
|
+
"runs without approval prompts, outward-facing confirmations included, except command substitution and system-level changes; hard blocks always apply",
|
|
379
|
+
},
|
|
380
|
+
"workers.onPermission": {
|
|
381
|
+
deny: "a worker permission ask becomes a tool denial; the run continues",
|
|
382
|
+
fail: "the run ends immediately as permission_required",
|
|
383
|
+
escalate:
|
|
384
|
+
"the ask is forwarded to this session's operator; on timeout it falls back to deny or fail per workers.escalation",
|
|
385
|
+
},
|
|
386
|
+
"delegation.defaults.toolGovernance": {
|
|
387
|
+
"clio-policy": "Clio's safety policy gates the delegated agent's tools",
|
|
388
|
+
"agent-managed": "the external agent governs its own tools",
|
|
389
|
+
"deny-all": "block every tool the delegated agent requests",
|
|
390
|
+
},
|
|
391
|
+
"compaction.auto": {
|
|
392
|
+
true: "compact automatically before a turn crosses the threshold",
|
|
393
|
+
false: "context is only compacted when you run /context compact",
|
|
394
|
+
},
|
|
395
|
+
"retry.enabled": {
|
|
396
|
+
true: "retry transient provider errors automatically",
|
|
397
|
+
false: "surface transient errors immediately without retrying",
|
|
398
|
+
},
|
|
399
|
+
"skills.trustProjectCompatRoots": {
|
|
400
|
+
true: "load skills from .claude/.codex/.github/etc. project roots",
|
|
401
|
+
false: "ignore third-party project skill roots",
|
|
402
|
+
},
|
|
403
|
+
"attribution.gitCommits": {
|
|
404
|
+
enabled: "add only the Clio role trailers justified by trusted evidence",
|
|
405
|
+
disabled: "leave every subsequent commit message byte-for-byte unchanged",
|
|
406
|
+
},
|
|
407
|
+
"terminal.showTerminalProgress": {
|
|
408
|
+
true: "emit OSC 9;4 taskbar/tab progress badges during turns",
|
|
409
|
+
false: "no terminal progress badges",
|
|
410
|
+
},
|
|
411
|
+
"terminal.outputVerbosity": {
|
|
412
|
+
minimal: "quiet transcript; tools stay to one-line outcomes and reasoning stays folded",
|
|
413
|
+
default: "balanced transcript; unfold the latest tool, worker, or reasoning block on demand",
|
|
414
|
+
verbose: "transparent transcript; reasoning, arguments, and live tool output stay visible",
|
|
415
|
+
},
|
|
416
|
+
"terminal.tuiMode": {
|
|
417
|
+
regular: "preserve terminal scrollback and render the composer below the transcript",
|
|
418
|
+
fullscreen: "use the alternate screen with an independently scrollable transcript and sticky composer/footer",
|
|
419
|
+
},
|
|
420
|
+
"terminal.fullscreenScrollbar": {
|
|
421
|
+
hidden: "never draw the fullscreen transcript scrollbar",
|
|
422
|
+
auto: "show the scrollbar while scrolling or dragging",
|
|
423
|
+
always: "reserve the rightmost column for the scrollbar",
|
|
424
|
+
},
|
|
425
|
+
"terminal.smoothStreaming": {
|
|
426
|
+
off: "preserve the current immediate 16ms-coalesced streaming behavior",
|
|
427
|
+
auto: "pace only on a capable local TTY without accessibility or backpressure risk",
|
|
428
|
+
on: "request grapheme-safe pacing; stdout backpressure still pauses presentation",
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
export type SettingSubmenuBuilder = NonNullable<SettingItem["submenu"]>;
|
|
433
|
+
type SettingsCenterLane = "sections" | "rows";
|
|
434
|
+
/**
|
|
435
|
+
* Where the operator is in the Settings stack, independent of width. Narrow
|
|
436
|
+
* terminals render one level at a time and wide ones render adjacent context,
|
|
437
|
+
* but both share this state so Esc walks the same chain everywhere.
|
|
438
|
+
*/
|
|
439
|
+
export type SettingsNavigationDepth = "sections" | "rows" | "detail";
|
|
440
|
+
|
|
441
|
+
export type SettingsPresentationKind =
|
|
442
|
+
| "setting"
|
|
443
|
+
| "status"
|
|
444
|
+
| "action"
|
|
445
|
+
| "group-header"
|
|
446
|
+
| "read-only-fact"
|
|
447
|
+
| "destructive-action";
|
|
448
|
+
|
|
449
|
+
type SettingsValueTone = "neutral" | "healthy" | "degraded" | "unhealthy" | "unknown" | "activity";
|
|
450
|
+
|
|
451
|
+
export interface SettingsValueSegment {
|
|
452
|
+
text: string;
|
|
453
|
+
tone: SettingsValueTone;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export interface SettingsCenterItem extends SettingItem {
|
|
457
|
+
id: SettingsCenterRowId;
|
|
458
|
+
label: string;
|
|
459
|
+
description: string;
|
|
460
|
+
section: SettingsSectionId;
|
|
461
|
+
configPath: SettingsCenterRowId;
|
|
462
|
+
affordance: string;
|
|
463
|
+
scope: SettingScope;
|
|
464
|
+
readOnly: boolean;
|
|
465
|
+
presentationKind: SettingsPresentationKind;
|
|
466
|
+
valueSegments: readonly SettingsValueSegment[];
|
|
467
|
+
targetConsole?: {
|
|
468
|
+
health: SettingsValueSegment;
|
|
469
|
+
id: string;
|
|
470
|
+
roles: string;
|
|
471
|
+
runtime: string;
|
|
472
|
+
latency: string;
|
|
473
|
+
url: string;
|
|
474
|
+
defaultModel: string;
|
|
475
|
+
lastProbe: string;
|
|
476
|
+
failureReason: string;
|
|
477
|
+
};
|
|
478
|
+
help?: string;
|
|
479
|
+
valueHelp?: Record<string, string>;
|
|
480
|
+
defaultValue?: string;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export interface SettingsCenterSection {
|
|
484
|
+
id: SettingsSectionId;
|
|
485
|
+
label: string;
|
|
486
|
+
items: SettingsCenterItem[];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export interface SettingsCenterSelection {
|
|
490
|
+
lane: SettingsCenterLane;
|
|
491
|
+
depth: SettingsNavigationDepth;
|
|
492
|
+
section: SettingsSectionId;
|
|
493
|
+
rowIndex: number;
|
|
494
|
+
rowId: SettingsCenterRowId | null;
|
|
495
|
+
submenuOpen: boolean;
|
|
496
|
+
filter: string;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
interface BuildSettingItemsOptions {
|
|
500
|
+
providers?: ProvidersContract;
|
|
501
|
+
/**
|
|
502
|
+
* Live settings source for submenus. The static `settings` snapshot is
|
|
503
|
+
* captured when the overlay opens; submenus must read through this so
|
|
504
|
+
* changing target, then picking model lists models for the new target.
|
|
505
|
+
*/
|
|
506
|
+
getSettings?: () => Readonly<ClioSettings>;
|
|
507
|
+
/** Re-derive rows after an action that changes what they show without a commit (a health probe). */
|
|
508
|
+
requestRefresh?: () => void;
|
|
509
|
+
/** Live fleet node snapshots (scheduling.fleet.list()); absent hides the node rows. */
|
|
510
|
+
getFleetNodes?: () => ReadonlyArray<FleetNodeSnapshot>;
|
|
511
|
+
/** Run the API-key / OAuth connect flow for a target; absent hides the action. */
|
|
512
|
+
connectTarget?: (targetId: string) => Promise<void> | void;
|
|
513
|
+
/** The connect/probe operation currently acting on a target, if any. */
|
|
514
|
+
getTargetOperation?: (targetId: string) => "connect" | "probe" | null;
|
|
515
|
+
/** Keeps the row grammar synchronized with the lifetime of connect/probe work. */
|
|
516
|
+
onTargetOperationChange?: (targetId: string, operation: "connect" | "probe" | null, operationToken: object) => void;
|
|
517
|
+
/** Detected agents not yet wired; absent or empty hides the add action on the delegation.agents row. */
|
|
518
|
+
getInteropProposals?: () => ReadonlyArray<InteropProposal>;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const DELEGATION_ADD_PREFIX = "add ";
|
|
522
|
+
const DELEGATION_REMOVE_PREFIX = "remove ";
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* The delegation.agents row is read-only until interop has something to offer.
|
|
526
|
+
* Adding writes the same entry `configure --interop` would, and removing names
|
|
527
|
+
* the `/delegate` id that stops resolving, which is the only user-visible
|
|
528
|
+
* consequence a change plan cannot show from a settings diff.
|
|
529
|
+
*/
|
|
530
|
+
function delegationAgentsAffordance(
|
|
531
|
+
agents: ReadonlyArray<{ id: string }>,
|
|
532
|
+
proposals: ReadonlyArray<InteropProposal>,
|
|
533
|
+
): { affordance: string; readOnly?: boolean; submenu?: SettingSubmenuBuilder } {
|
|
534
|
+
const choices = [
|
|
535
|
+
...proposals.map((proposal) => ({
|
|
536
|
+
value: `${DELEGATION_ADD_PREFIX}${proposal.kind}`,
|
|
537
|
+
label: `Add detected agent ${proposal.entry.id} (${[proposal.entry.command, ...proposal.entry.args].join(" ")})`,
|
|
538
|
+
presentationKind: "action" as const,
|
|
539
|
+
})),
|
|
540
|
+
...agents.map((agent) => ({
|
|
541
|
+
value: `${DELEGATION_REMOVE_PREFIX}${agent.id}`,
|
|
542
|
+
label: `Remove ${agent.id}; /delegate ${agent.id} stops resolving`,
|
|
543
|
+
presentationKind: "destructive-action" as const,
|
|
544
|
+
})),
|
|
545
|
+
];
|
|
546
|
+
if (choices.length === 0) return { affordance: "edit settings.yaml", readOnly: true };
|
|
547
|
+
return {
|
|
548
|
+
affordance: "opens picker",
|
|
549
|
+
submenu: selectListSubmenu(
|
|
550
|
+
"Delegation agents",
|
|
551
|
+
choices,
|
|
552
|
+
"An added peer runs under clio-policy governance and inherits projectContext: none.",
|
|
553
|
+
),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
type SubmenuTitle = string | ((width: number) => string);
|
|
558
|
+
|
|
559
|
+
export class SubmenuWrapper implements Component {
|
|
560
|
+
constructor(
|
|
561
|
+
private readonly title: SubmenuTitle,
|
|
562
|
+
private readonly child: Component,
|
|
563
|
+
private readonly hint: string = buildHint([{ key: "Enter", verb: "confirm" }], "back"),
|
|
564
|
+
private readonly note?: string,
|
|
565
|
+
) {}
|
|
566
|
+
|
|
567
|
+
render(width: number): string[] {
|
|
568
|
+
const theme = clioTheme();
|
|
569
|
+
const lines: string[] = [];
|
|
570
|
+
const titleWidth = Math.max(1, width - 2);
|
|
571
|
+
const title = typeof this.title === "function" ? this.title(titleWidth) : this.title;
|
|
572
|
+
lines.push(screenTitle(theme, ` ${title}`));
|
|
573
|
+
if (this.note) {
|
|
574
|
+
for (const line of wrapTextWithAnsi(this.note, Math.max(1, width - 2))) {
|
|
575
|
+
lines.push(theme.fg("dim", ` ${line}`));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
lines.push("");
|
|
579
|
+
// The two-column indent is this wrapper's to spend, so the child is told
|
|
580
|
+
// what is left. Handing it the full width made every row that pads itself
|
|
581
|
+
// arrive two columns over budget: the checklist's entries came back at
|
|
582
|
+
// `width`, the indent pushed them to `width + 2`, and the panel then cut
|
|
583
|
+
// two columns off each one and marked the cut, so every entry wore a
|
|
584
|
+
// trailing … it had not earned.
|
|
585
|
+
lines.push(...this.child.render(Math.max(1, width - 2)).map((line) => ` ${line}`));
|
|
586
|
+
lines.push("");
|
|
587
|
+
lines.push(theme.fg("dim", ` ${this.hint}`));
|
|
588
|
+
return lines;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
handleInput(data: string): void {
|
|
592
|
+
this.child.handleInput?.(data);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
invalidate(): void {
|
|
596
|
+
this.child.invalidate?.();
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function textInputSubmenu(title: string, note?: string): SettingSubmenuBuilder {
|
|
601
|
+
return (currentValue: string, done: (val?: string) => void) => {
|
|
602
|
+
const input = new Input();
|
|
603
|
+
input.setValue(currentValue);
|
|
604
|
+
input.focused = true;
|
|
605
|
+
input.onSubmit = (val) => done(val);
|
|
606
|
+
input.onEscape = () => done();
|
|
607
|
+
return new SubmenuWrapper(title, input, buildHint([{ key: "Enter", verb: "confirm" }], "back"), note);
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function selectTargetSubmenu(providers: ProvidersContract): SettingSubmenuBuilder {
|
|
612
|
+
return (currentValue: string, done: (val?: string) => void) => {
|
|
613
|
+
const statuses = providers.list();
|
|
614
|
+
if (statuses.length === 0) {
|
|
615
|
+
return textInputSubmenu("Type target id")(currentValue, done);
|
|
616
|
+
}
|
|
617
|
+
const items = statuses.map((status) => ({
|
|
618
|
+
value: status.target.id,
|
|
619
|
+
label: `${status.target.id} (${status.target.url ?? "no url"})`,
|
|
620
|
+
}));
|
|
621
|
+
return selectListSubmenu("Select target", items)(currentValue, done);
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function selectOptionalBackgroundTargetSubmenu(providers: ProvidersContract): SettingSubmenuBuilder {
|
|
626
|
+
return (currentValue: string, done: (val?: string) => void) => {
|
|
627
|
+
const statuses = providers.list();
|
|
628
|
+
const items = [
|
|
629
|
+
{ value: "(unset)", label: "(unset — rules-only)" },
|
|
630
|
+
...statuses.map((status) => ({
|
|
631
|
+
value: status.target.id,
|
|
632
|
+
label: `${status.target.id} (${status.target.url ?? "no url"})`,
|
|
633
|
+
})),
|
|
634
|
+
];
|
|
635
|
+
const note = "Unset keeps the zero-cost rules-only tier.";
|
|
636
|
+
return selectListSubmenu("Select memory target", items, note)(currentValue, done);
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function selectModelSubmenu(
|
|
641
|
+
providers: ProvidersContract,
|
|
642
|
+
getActiveTarget: () => string | undefined,
|
|
643
|
+
): SettingSubmenuBuilder {
|
|
644
|
+
return (currentValue: string, done: (val?: string) => void) => {
|
|
645
|
+
const targetId = getActiveTarget();
|
|
646
|
+
const status = providers.list().find((s) => s.target.id === targetId);
|
|
647
|
+
const models = status ? modelsForTarget(status) : [];
|
|
648
|
+
if (models.length === 0) {
|
|
649
|
+
return textInputSubmenu("Type model name")(currentValue, done);
|
|
650
|
+
}
|
|
651
|
+
const items = models.map((m) => ({ value: m, label: m }));
|
|
652
|
+
return selectListSubmenu(`Select model for ${targetId}`, items)(currentValue, done);
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function editTextSubmenu(title: string, note?: string): SettingSubmenuBuilder {
|
|
657
|
+
return textInputSubmenu(title, note);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function editNumberSubmenu(title: string): SettingSubmenuBuilder {
|
|
661
|
+
return (currentValue: string, done: (val?: string) => void) => {
|
|
662
|
+
const input = new Input();
|
|
663
|
+
input.setValue(currentValue);
|
|
664
|
+
input.focused = true;
|
|
665
|
+
input.onSubmit = (val) => {
|
|
666
|
+
const num = Number(val);
|
|
667
|
+
if (Number.isFinite(num) && num >= 0) {
|
|
668
|
+
done(val);
|
|
669
|
+
} else {
|
|
670
|
+
done();
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
input.onEscape = () => done();
|
|
674
|
+
return new SubmenuWrapper(
|
|
675
|
+
title,
|
|
676
|
+
input,
|
|
677
|
+
buildHint([{ key: "Enter", verb: "confirm" }], "back"),
|
|
678
|
+
"Use a non-negative number.",
|
|
679
|
+
);
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function selectListSubmenu(
|
|
684
|
+
title: string,
|
|
685
|
+
items: ReadonlyArray<{
|
|
686
|
+
value: string;
|
|
687
|
+
label: string;
|
|
688
|
+
presentationKind?: SettingsPresentationKind;
|
|
689
|
+
}>,
|
|
690
|
+
note?: string,
|
|
691
|
+
): SettingSubmenuBuilder {
|
|
692
|
+
return (currentValue: string, done: (val?: string) => void) => {
|
|
693
|
+
const theme = clioTheme();
|
|
694
|
+
const presented = items.map((item) => ({
|
|
695
|
+
value: item.value,
|
|
696
|
+
label:
|
|
697
|
+
item.presentationKind === "destructive-action"
|
|
698
|
+
? theme.fg("error", `${GLYPH.error} ${item.label}`)
|
|
699
|
+
: item.presentationKind === "action"
|
|
700
|
+
? `${GLYPH.active} ${item.label}`
|
|
701
|
+
: item.label,
|
|
702
|
+
}));
|
|
703
|
+
const list = new SettingsSelectList(presented, Math.min(10, Math.max(1, items.length)), DEFAULT_SELECT_THEME);
|
|
704
|
+
const currentIndex = items.findIndex((item) => item.value === currentValue);
|
|
705
|
+
if (currentIndex >= 0) list.setSelectedIndex(currentIndex);
|
|
706
|
+
list.onSelect = (item) => done(item.value);
|
|
707
|
+
list.onCancel = () => done();
|
|
708
|
+
return new SubmenuWrapper(title, list, undefined, note);
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
interface ScopedModelChecklistEntry {
|
|
713
|
+
kind: "entry";
|
|
714
|
+
key: string;
|
|
715
|
+
ref: string;
|
|
716
|
+
label: string;
|
|
717
|
+
detail: string;
|
|
718
|
+
available: boolean;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
interface ScopedModelChecklistGroup {
|
|
722
|
+
kind: "group";
|
|
723
|
+
label: string;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
type ScopedModelChecklistRow = ScopedModelChecklistEntry | ScopedModelChecklistGroup;
|
|
727
|
+
const SCOPED_MODEL_SELECTION_PREFIX = "__clio_scope_v1__:";
|
|
728
|
+
|
|
729
|
+
function serializeScopedModelSelection(refs: readonly string[]): string {
|
|
730
|
+
return `${SCOPED_MODEL_SELECTION_PREFIX}${JSON.stringify(refs)}`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function parseScopedModelSelection(value: string): string[] | null {
|
|
734
|
+
if (!value.startsWith(SCOPED_MODEL_SELECTION_PREFIX)) return null;
|
|
735
|
+
try {
|
|
736
|
+
const parsed: unknown = JSON.parse(value.slice(SCOPED_MODEL_SELECTION_PREFIX.length));
|
|
737
|
+
return Array.isArray(parsed) && parsed.every((entry) => typeof entry === "string") ? parsed : null;
|
|
738
|
+
} catch {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function compactCapabilityCount(value: number): string {
|
|
744
|
+
if (!Number.isFinite(value) || value <= 0) return "unknown";
|
|
745
|
+
if (value >= 1_000_000) return `${Math.round(value / 100_000) / 10}m`;
|
|
746
|
+
if (value >= 1000) return `${Math.round(value / 1000)}k`;
|
|
747
|
+
return String(Math.round(value));
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function scopedModelCapabilityDetail(caps: CapabilityFlags, thinking?: string): string {
|
|
751
|
+
const enabled = [
|
|
752
|
+
caps.chat ? "chat" : null,
|
|
753
|
+
caps.tools ? "tools" : null,
|
|
754
|
+
caps.reasoning ? "reasoning" : null,
|
|
755
|
+
caps.vision ? "vision" : null,
|
|
756
|
+
caps.audio ? "audio" : null,
|
|
757
|
+
caps.embeddings ? "embeddings" : null,
|
|
758
|
+
caps.rerank ? "rerank" : null,
|
|
759
|
+
caps.fim ? "fim" : null,
|
|
760
|
+
].filter((value): value is string => value !== null);
|
|
761
|
+
return `Capabilities: ${enabled.length > 0 ? enabled.join(", ") : "none reported"} · context ${compactCapabilityCount(caps.contextWindow)} · max output ${compactCapabilityCount(caps.maxTokens)}${thinking ? ` · thinking ${thinking}` : ""}`;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function scopedModelEntryDetail(providers: ProvidersContract, status: TargetStatus, model: string | null): string {
|
|
765
|
+
if (!model) {
|
|
766
|
+
return `${scopedModelCapabilityDetail(status.capabilities)} · target-level scope uses this target's default model when switching targets`;
|
|
767
|
+
}
|
|
768
|
+
const resolved = resolveModelRuntimeCapabilitiesForProviders(providers, status.target.id, model);
|
|
769
|
+
return resolved
|
|
770
|
+
? scopedModelCapabilityDetail(resolved.capabilities, resolved.thinking.display)
|
|
771
|
+
: `${scopedModelCapabilityDetail(status.capabilities)} · model details unresolved`;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function buildScopedModelChecklistRows(
|
|
775
|
+
selectedRefs: readonly string[],
|
|
776
|
+
providers: ProvidersContract | undefined,
|
|
777
|
+
): { rows: ScopedModelChecklistRow[]; availableRefs: ReadonlySet<string> } {
|
|
778
|
+
const rows: ScopedModelChecklistRow[] = [];
|
|
779
|
+
const availableRefs = new Set<string>();
|
|
780
|
+
if (providers) {
|
|
781
|
+
for (const status of providers.list()) {
|
|
782
|
+
const targetRef = status.target.id;
|
|
783
|
+
rows.push({ kind: "group", label: `Target · ${targetRef}` });
|
|
784
|
+
availableRefs.add(targetRef);
|
|
785
|
+
rows.push({
|
|
786
|
+
kind: "entry",
|
|
787
|
+
key: `available:${targetRef}`,
|
|
788
|
+
ref: targetRef,
|
|
789
|
+
label: `${targetRef} (all models)`,
|
|
790
|
+
detail: scopedModelEntryDetail(providers, status, null),
|
|
791
|
+
available: true,
|
|
792
|
+
});
|
|
793
|
+
for (const model of modelsForTarget(status)) {
|
|
794
|
+
const ref = `${targetRef}/${model}`;
|
|
795
|
+
if (availableRefs.has(ref)) continue;
|
|
796
|
+
availableRefs.add(ref);
|
|
797
|
+
rows.push({
|
|
798
|
+
kind: "entry",
|
|
799
|
+
key: `available:${ref}`,
|
|
800
|
+
ref,
|
|
801
|
+
label: ref,
|
|
802
|
+
detail: scopedModelEntryDetail(providers, status, model),
|
|
803
|
+
available: true,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const unavailable = [...new Set(selectedRefs.filter((ref) => !availableRefs.has(ref.trim())))];
|
|
809
|
+
if (unavailable.length > 0) {
|
|
810
|
+
rows.push({ kind: "group", label: "Unavailable" });
|
|
811
|
+
for (const ref of unavailable) {
|
|
812
|
+
rows.push({
|
|
813
|
+
kind: "entry",
|
|
814
|
+
key: `unavailable:${ref}`,
|
|
815
|
+
ref,
|
|
816
|
+
label: ref,
|
|
817
|
+
detail:
|
|
818
|
+
"Not present in the current provider catalog. It remains selected and will be preserved unchanged unless you uncheck it.",
|
|
819
|
+
available: false,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (rows.length === 0) rows.push({ kind: "group", label: "No configured targets or scoped references" });
|
|
824
|
+
return { rows, availableRefs };
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
class ScopedModelChecklist implements Component {
|
|
828
|
+
private readonly rows: ScopedModelChecklistRow[];
|
|
829
|
+
private readonly availableRefs: ReadonlySet<string>;
|
|
830
|
+
private readonly selectedKeys = new Set<string>();
|
|
831
|
+
private selectedRow = 0;
|
|
832
|
+
|
|
833
|
+
constructor(
|
|
834
|
+
private readonly originalRefs: readonly string[],
|
|
835
|
+
providers: ProvidersContract | undefined,
|
|
836
|
+
private readonly done: (value?: string) => void,
|
|
837
|
+
) {
|
|
838
|
+
const built = buildScopedModelChecklistRows(originalRefs, providers);
|
|
839
|
+
this.rows = built.rows;
|
|
840
|
+
this.availableRefs = built.availableRefs;
|
|
841
|
+
for (const ref of originalRefs) {
|
|
842
|
+
const trimmed = ref.trim();
|
|
843
|
+
this.selectedKeys.add(this.availableRefs.has(trimmed) ? `available:${trimmed}` : `unavailable:${ref}`);
|
|
844
|
+
}
|
|
845
|
+
this.selectedRow = this.nextEntryIndex(0, 1);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
render(width: number): string[] {
|
|
849
|
+
const theme = clioTheme();
|
|
850
|
+
if (!this.rows.some((row) => row.kind === "entry")) {
|
|
851
|
+
return [theme.fg("dim", "No models are available to select.")];
|
|
852
|
+
}
|
|
853
|
+
const visibleRows = Math.min(10, this.rows.length);
|
|
854
|
+
const [start, end] = scrollWindow(this.rows.length, this.selectedRow, visibleRows);
|
|
855
|
+
const lines = this.rows.slice(start, end).map((row, offset) => {
|
|
856
|
+
if (row.kind === "group") return theme.style("dim", row.label, { bold: true });
|
|
857
|
+
const selected = start + offset === this.selectedRow;
|
|
858
|
+
const checked = this.selectedKeys.has(row.key);
|
|
859
|
+
const pointer = selected ? theme.fg("accent", `${GLYPH.cursor} `) : " ";
|
|
860
|
+
const check = theme.fg(checked ? "accent" : "dim", checked ? "[x]" : "[ ]");
|
|
861
|
+
const label = selected ? theme.style("accent", row.label, { bold: true }) : theme.fg("muted", row.label);
|
|
862
|
+
return truncateToWidth(`${pointer}${check} ${label}`, Math.max(1, width), ELLIPSIS, true);
|
|
863
|
+
});
|
|
864
|
+
const selected = this.rows[this.selectedRow];
|
|
865
|
+
if (selected?.kind === "entry") {
|
|
866
|
+
const detail = selected.available ? selected.detail : `Unavailable · ${selected.detail}`;
|
|
867
|
+
const wrapped = wrapTextWithAnsi(theme.fg("dim", detail), Math.max(1, width));
|
|
868
|
+
const kept = wrapped.slice(0, 3);
|
|
869
|
+
// A capability sentence that stops at "context 131" reads as the whole
|
|
870
|
+
// fact, so the line that survives the clip says it is not.
|
|
871
|
+
const last = kept.at(-1);
|
|
872
|
+
const marked =
|
|
873
|
+
wrapped.length > kept.length && last !== undefined ? [...kept.slice(0, -1), `${last}${ELLIPSIS}`] : kept;
|
|
874
|
+
lines.push("", ...marked);
|
|
875
|
+
}
|
|
876
|
+
return lines;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
handleInput(data: string): void {
|
|
880
|
+
const kb = getKeybindings();
|
|
881
|
+
if (kb.matches(data, "tui.select.up") || data === "k") {
|
|
882
|
+
this.selectedRow = this.nextEntryIndex(this.selectedRow - 1, -1);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (kb.matches(data, "tui.select.down") || data === "j") {
|
|
886
|
+
this.selectedRow = this.nextEntryIndex(this.selectedRow + 1, 1);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (data === " ") {
|
|
890
|
+
const row = this.rows[this.selectedRow];
|
|
891
|
+
if (row?.kind !== "entry") return;
|
|
892
|
+
if (this.selectedKeys.has(row.key)) this.selectedKeys.delete(row.key);
|
|
893
|
+
else this.selectedKeys.add(row.key);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (kb.matches(data, "tui.select.confirm") || matchesKey(data, "enter")) {
|
|
897
|
+
this.done(serializeScopedModelSelection(this.selectedRefs()));
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
if (kb.matches(data, "tui.select.cancel")) this.done();
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
invalidate(): void {}
|
|
904
|
+
|
|
905
|
+
private nextEntryIndex(from: number, direction: -1 | 1): number {
|
|
906
|
+
if (!this.rows.some((row) => row.kind === "entry")) return 0;
|
|
907
|
+
let index = from;
|
|
908
|
+
for (let attempts = 0; attempts < this.rows.length; attempts += 1) {
|
|
909
|
+
if (index < 0) index = this.rows.length - 1;
|
|
910
|
+
if (index >= this.rows.length) index = 0;
|
|
911
|
+
if (this.rows[index]?.kind === "entry") return index;
|
|
912
|
+
index += direction;
|
|
913
|
+
}
|
|
914
|
+
return 0;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
private selectedRefs(): string[] {
|
|
918
|
+
const preserved = this.originalRefs.filter((ref) => {
|
|
919
|
+
const trimmed = ref.trim();
|
|
920
|
+
const key = this.availableRefs.has(trimmed) ? `available:${trimmed}` : `unavailable:${ref}`;
|
|
921
|
+
return this.selectedKeys.has(key);
|
|
922
|
+
});
|
|
923
|
+
const selectedOriginalRefs = new Set(this.originalRefs.map((ref) => ref.trim()));
|
|
924
|
+
const additions = this.rows
|
|
925
|
+
.filter((row): row is ScopedModelChecklistEntry => row.kind === "entry" && row.available)
|
|
926
|
+
.filter((row) => this.selectedKeys.has(row.key) && !selectedOriginalRefs.has(row.ref))
|
|
927
|
+
.map((row) => row.ref);
|
|
928
|
+
return [...preserved, ...additions];
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function scopedModelsSubmenu(
|
|
933
|
+
selectedRefs: readonly string[],
|
|
934
|
+
providers: ProvidersContract | undefined,
|
|
935
|
+
): SettingSubmenuBuilder {
|
|
936
|
+
return (_currentValue, done) =>
|
|
937
|
+
new SubmenuWrapper(
|
|
938
|
+
"Choose the model cycle set",
|
|
939
|
+
new ScopedModelChecklist(selectedRefs, providers, done),
|
|
940
|
+
buildHint(
|
|
941
|
+
[
|
|
942
|
+
{ key: "Space", verb: "toggle" },
|
|
943
|
+
{ key: "Enter", verb: "continue" },
|
|
944
|
+
],
|
|
945
|
+
"back",
|
|
946
|
+
),
|
|
947
|
+
"Target entries cycle by target, using its default model when switching targets. Model entries pin one exact target/model reference.",
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/** Two pickers in sequence joined into one committed value; Esc at either step cancels. */
|
|
952
|
+
function chainSubmenus(
|
|
953
|
+
first: SettingSubmenuBuilder,
|
|
954
|
+
second: (firstValue: string) => SettingSubmenuBuilder,
|
|
955
|
+
join: (first: string, second: string) => string,
|
|
956
|
+
): SettingSubmenuBuilder {
|
|
957
|
+
return (_currentValue: string, done: (val?: string) => void) => {
|
|
958
|
+
let active: Component = first("", (head) => {
|
|
959
|
+
const trimmed = head?.trim() ?? "";
|
|
960
|
+
if (!trimmed) return done();
|
|
961
|
+
active = second(trimmed)("", (tail) => done(tail?.trim() ? join(trimmed, tail.trim()) : undefined));
|
|
962
|
+
});
|
|
963
|
+
return {
|
|
964
|
+
render: (width: number) => active.render(width),
|
|
965
|
+
handleInput: (data: string) => active.handleInput?.(data),
|
|
966
|
+
invalidate: () => active.invalidate?.(),
|
|
967
|
+
};
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** Dispatch-eligible targets for a fleet profile; every configured target when no provider domain is wired. */
|
|
972
|
+
function profileTargetChoices(
|
|
973
|
+
live: () => Readonly<ClioSettings>,
|
|
974
|
+
providers: ProvidersContract | undefined,
|
|
975
|
+
): Array<{ value: string; label: string }> {
|
|
976
|
+
if (!providers)
|
|
977
|
+
return live().targets.map((target) => ({
|
|
978
|
+
value: target.id,
|
|
979
|
+
label: target.id,
|
|
980
|
+
}));
|
|
981
|
+
return providers
|
|
982
|
+
.list()
|
|
983
|
+
.filter((status) => status.runtime !== null && isDispatchEligibleRuntime(status.runtime))
|
|
984
|
+
.map((status) => ({
|
|
985
|
+
value: status.target.id,
|
|
986
|
+
label: `${status.target.id} (${status.target.url ?? "no url"})`,
|
|
987
|
+
}));
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function profileNameChoices(live: () => Readonly<ClioSettings>): Array<{ value: string; label: string }> {
|
|
991
|
+
const names = Object.keys(live().workers.profiles).sort();
|
|
992
|
+
return names.map((name) => ({ value: name, label: name }));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Per-target actions. Probe and connect are not settings changes: they run,
|
|
997
|
+
* then the rows refresh; only use/remove commit.
|
|
998
|
+
*/
|
|
999
|
+
function targetActionsSubmenu(targetId: string, options: BuildSettingItemsOptions | undefined): SettingSubmenuBuilder {
|
|
1000
|
+
const providers = options?.providers;
|
|
1001
|
+
const requestRefresh = options?.requestRefresh;
|
|
1002
|
+
const connectTarget = options?.connectTarget;
|
|
1003
|
+
return (currentValue: string, done: (val?: string) => void) => {
|
|
1004
|
+
const status = providers?.list().find((entry) => entry.target.id === targetId);
|
|
1005
|
+
const target = status?.target ?? options?.getSettings?.().targets.find((entry) => entry.id === targetId);
|
|
1006
|
+
const runtime = status?.runtime ?? null;
|
|
1007
|
+
const chatEligible = !providers || (runtime !== null && isOrchestratorEligibleRuntime(runtime));
|
|
1008
|
+
const items = [
|
|
1009
|
+
...(chatEligible
|
|
1010
|
+
? [
|
|
1011
|
+
{
|
|
1012
|
+
value: "use",
|
|
1013
|
+
label: "Use for chat and fleet dispatch",
|
|
1014
|
+
presentationKind: "action" as const,
|
|
1015
|
+
},
|
|
1016
|
+
]
|
|
1017
|
+
: []),
|
|
1018
|
+
...(connectTarget
|
|
1019
|
+
? [
|
|
1020
|
+
{
|
|
1021
|
+
value: "connect",
|
|
1022
|
+
label: "Connect (API key or OAuth), then probe",
|
|
1023
|
+
presentationKind: "action" as const,
|
|
1024
|
+
},
|
|
1025
|
+
]
|
|
1026
|
+
: []),
|
|
1027
|
+
...(providers
|
|
1028
|
+
? [
|
|
1029
|
+
{
|
|
1030
|
+
value: "probe",
|
|
1031
|
+
label: "Probe health now",
|
|
1032
|
+
presentationKind: "action" as const,
|
|
1033
|
+
},
|
|
1034
|
+
]
|
|
1035
|
+
: []),
|
|
1036
|
+
{
|
|
1037
|
+
value: "remove",
|
|
1038
|
+
label: "Remove target",
|
|
1039
|
+
presentationKind: "destructive-action" as const,
|
|
1040
|
+
},
|
|
1041
|
+
];
|
|
1042
|
+
const details = `URL: ${target?.url ?? "(none)"} · Default model: ${target?.defaultModel ?? "(none)"} · Last probe: ${status?.health.lastCheckAt ? clockLocal(status.health.lastCheckAt) : "never"} · Failure reason: ${status?.health.lastError ?? (!status?.available && status?.reason ? status.reason : "none")}`;
|
|
1043
|
+
const note = chatEligible ? details : `Not chat-eligible: its runtime is not HTTP/native. · ${details}`;
|
|
1044
|
+
return selectListSubmenu(
|
|
1045
|
+
`Target ${targetId}`,
|
|
1046
|
+
items,
|
|
1047
|
+
note,
|
|
1048
|
+
)(currentValue, (value) => {
|
|
1049
|
+
const refresh = (): void => requestRefresh?.();
|
|
1050
|
+
if (value === "connect" && connectTarget) {
|
|
1051
|
+
done();
|
|
1052
|
+
const operationToken = {};
|
|
1053
|
+
options?.onTargetOperationChange?.(targetId, "connect", operationToken);
|
|
1054
|
+
refresh();
|
|
1055
|
+
const settle = (): void => {
|
|
1056
|
+
options?.onTargetOperationChange?.(targetId, null, operationToken);
|
|
1057
|
+
refresh();
|
|
1058
|
+
};
|
|
1059
|
+
try {
|
|
1060
|
+
void Promise.resolve(connectTarget(targetId)).then(settle, settle);
|
|
1061
|
+
} catch {
|
|
1062
|
+
settle();
|
|
1063
|
+
}
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (value !== "probe" || !providers) return done(value);
|
|
1067
|
+
done();
|
|
1068
|
+
const operationToken = {};
|
|
1069
|
+
options?.onTargetOperationChange?.(targetId, "probe", operationToken);
|
|
1070
|
+
refresh();
|
|
1071
|
+
const settle = (): void => {
|
|
1072
|
+
options?.onTargetOperationChange?.(targetId, null, operationToken);
|
|
1073
|
+
refresh();
|
|
1074
|
+
};
|
|
1075
|
+
try {
|
|
1076
|
+
void Promise.resolve(providers.probeTarget(targetId)).then(settle, settle);
|
|
1077
|
+
} catch {
|
|
1078
|
+
settle();
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function sectionForSetting(id: EditableSettingId): SettingsSectionId {
|
|
1085
|
+
if (id.startsWith("workers.profiles.") || id.startsWith("workers.agentBindings.") || id.startsWith("fleet.nodes.")) {
|
|
1086
|
+
return "fleet";
|
|
1087
|
+
}
|
|
1088
|
+
if (id.startsWith("targets.")) return "targets";
|
|
1089
|
+
for (const section of SETTINGS_SECTIONS) {
|
|
1090
|
+
if ((SETTINGS_SECTION_ROWS[section.id] as readonly EditableSettingId[]).includes(id)) return section.id;
|
|
1091
|
+
}
|
|
1092
|
+
return "safety";
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function cycleAffordance(values: readonly string[]): string {
|
|
1096
|
+
return `cycles: ${values.join(", ")}`;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function scopeForId(id: EditableSettingId): SettingScope {
|
|
1100
|
+
return RESTART_REQUIRED_IDS.has(id) ? "restart" : "live";
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/** Shipped default as a display string, for the "default: X" hint and the modified marker. */
|
|
1104
|
+
function defaultValueFor(id: EditableSettingId): string | undefined {
|
|
1105
|
+
if (isRoutingPath(id)) return undefined;
|
|
1106
|
+
const raw = getAtPath(DEFAULT_SETTINGS, id);
|
|
1107
|
+
if (raw === null || raw === undefined || typeof raw === "object") return undefined;
|
|
1108
|
+
if (id === "attribution.gitCommits") return raw === true ? "enabled" : "disabled";
|
|
1109
|
+
return String(raw);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function settingItem(
|
|
1113
|
+
id: EditableSettingId,
|
|
1114
|
+
currentValue: string,
|
|
1115
|
+
options: {
|
|
1116
|
+
values?: readonly string[];
|
|
1117
|
+
submenu?: SettingSubmenuBuilder;
|
|
1118
|
+
affordance?: string;
|
|
1119
|
+
readOnly?: boolean;
|
|
1120
|
+
presentationKind?: SettingsPresentationKind;
|
|
1121
|
+
valueSegments?: readonly SettingsValueSegment[];
|
|
1122
|
+
label?: string;
|
|
1123
|
+
description?: string;
|
|
1124
|
+
help?: string;
|
|
1125
|
+
},
|
|
1126
|
+
): SettingsCenterItem {
|
|
1127
|
+
const item: SettingsCenterItem = {
|
|
1128
|
+
id,
|
|
1129
|
+
label: options.label ?? SETTINGS_LABELS_BY_ID[id as keyof typeof SETTINGS_LABELS_BY_ID],
|
|
1130
|
+
currentValue,
|
|
1131
|
+
description: options.description ?? SETTINGS_DESCRIPTIONS_BY_ID[id as keyof typeof SETTINGS_LABELS_BY_ID],
|
|
1132
|
+
section: sectionForSetting(id),
|
|
1133
|
+
configPath: id,
|
|
1134
|
+
affordance: options.affordance ?? (options.values ? cycleAffordance(options.values) : "opens picker"),
|
|
1135
|
+
scope: scopeForId(id),
|
|
1136
|
+
readOnly: options.readOnly ?? false,
|
|
1137
|
+
presentationKind: options.presentationKind ?? (options.readOnly ? "read-only-fact" : "setting"),
|
|
1138
|
+
valueSegments: options.valueSegments ?? [{ text: currentValue, tone: "neutral" }],
|
|
1139
|
+
};
|
|
1140
|
+
const help = options.help ?? SETTINGS_HELP_BY_ID[id];
|
|
1141
|
+
if (help) item.help = help;
|
|
1142
|
+
const valueHelp = SETTINGS_VALUE_HELP_BY_ID[id];
|
|
1143
|
+
if (valueHelp) item.valueHelp = valueHelp;
|
|
1144
|
+
const def = defaultValueFor(id);
|
|
1145
|
+
if (def !== undefined) item.defaultValue = def;
|
|
1146
|
+
if (options.values) item.values = [...options.values];
|
|
1147
|
+
if (options.submenu) item.submenu = options.submenu;
|
|
1148
|
+
return item;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function fleetGroupHeader(id: FleetGroupHeaderId, label: string): SettingsCenterItem {
|
|
1152
|
+
return {
|
|
1153
|
+
id,
|
|
1154
|
+
label,
|
|
1155
|
+
currentValue: "",
|
|
1156
|
+
description: `${label} in the Fleet workbench.`,
|
|
1157
|
+
section: "fleet",
|
|
1158
|
+
configPath: id,
|
|
1159
|
+
affordance: "group heading",
|
|
1160
|
+
scope: "live",
|
|
1161
|
+
readOnly: true,
|
|
1162
|
+
presentationKind: "group-header",
|
|
1163
|
+
valueSegments: [],
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function targetAddCta(): SettingsCenterItem {
|
|
1168
|
+
return {
|
|
1169
|
+
id: "targets.add-cta",
|
|
1170
|
+
label: "Add target",
|
|
1171
|
+
currentValue: "`clio-coder targets add`",
|
|
1172
|
+
description: "Launch the accepted target setup wizard from your shell.",
|
|
1173
|
+
section: "targets",
|
|
1174
|
+
configPath: "targets.add-cta",
|
|
1175
|
+
affordance: "accepted CLI wizard",
|
|
1176
|
+
scope: "live",
|
|
1177
|
+
readOnly: true,
|
|
1178
|
+
presentationKind: "action",
|
|
1179
|
+
valueSegments: [{ text: "`clio-coder targets add`", tone: "neutral" }],
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function thinkingChoices(
|
|
1184
|
+
providers: ProvidersContract | undefined,
|
|
1185
|
+
target: string | null,
|
|
1186
|
+
model: string | null,
|
|
1187
|
+
level: ClioSettings["orchestrator"]["thinkingLevel"],
|
|
1188
|
+
): { display: string; values: readonly string[] } {
|
|
1189
|
+
const resolved = providers
|
|
1190
|
+
? resolveModelRuntimeCapabilitiesForProviders(providers, target, model, level ?? "off")?.thinking
|
|
1191
|
+
: null;
|
|
1192
|
+
const display = resolved?.display ?? level ?? "off";
|
|
1193
|
+
const values = resolved
|
|
1194
|
+
? resolved.supportedLevels.map((entry) => thinkingLevelChoiceLabel(resolved.mechanism, entry))
|
|
1195
|
+
: FALLBACK_THINKING_VALUES;
|
|
1196
|
+
return { display, values };
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* Surface every configurable knob. Editable knobs carry values/submenus;
|
|
1201
|
+
* read-only pointer rows (targets, keybindings, favorites, safety net) name the
|
|
1202
|
+
* surface that owns them so the Center has no dead-but-tappable rows.
|
|
1203
|
+
*/
|
|
1204
|
+
export function buildSettingItems(
|
|
1205
|
+
settings: Readonly<ClioSettings>,
|
|
1206
|
+
options?: BuildSettingItemsOptions,
|
|
1207
|
+
): SettingsCenterItem[] {
|
|
1208
|
+
const live = options?.getSettings ?? ((): Readonly<ClioSettings> => settings);
|
|
1209
|
+
const scopeList = settings.scope ?? [];
|
|
1210
|
+
const scopeText = scopeList.length > 0 ? scopeList.join(", ") : "(empty)";
|
|
1211
|
+
const compaction = settings.compaction;
|
|
1212
|
+
const retry = settings.retry;
|
|
1213
|
+
const terminal = settings.terminal;
|
|
1214
|
+
const orchestratorThinking = thinkingChoices(
|
|
1215
|
+
options?.providers,
|
|
1216
|
+
settings.orchestrator.target,
|
|
1217
|
+
settings.orchestrator.model,
|
|
1218
|
+
settings.orchestrator.thinkingLevel,
|
|
1219
|
+
);
|
|
1220
|
+
const workerThinking = thinkingChoices(
|
|
1221
|
+
options?.providers,
|
|
1222
|
+
settings.workers.default.target,
|
|
1223
|
+
settings.workers.default.model,
|
|
1224
|
+
settings.workers.default.thinkingLevel,
|
|
1225
|
+
);
|
|
1226
|
+
const backgroundThinking = thinkingChoices(
|
|
1227
|
+
options?.providers,
|
|
1228
|
+
settings.background.target,
|
|
1229
|
+
settings.background.model,
|
|
1230
|
+
settings.background.thinkingLevel,
|
|
1231
|
+
);
|
|
1232
|
+
const profileCount = Object.keys(settings.workers.profiles ?? {}).length;
|
|
1233
|
+
const addProfileSubmenu = chainSubmenus(
|
|
1234
|
+
textInputSubmenu("New profile name"),
|
|
1235
|
+
() => selectListSubmenu("Select the profile's target", profileTargetChoices(live, options?.providers)),
|
|
1236
|
+
(name, target) => `${name} -> ${target}`,
|
|
1237
|
+
);
|
|
1238
|
+
const addBindingSubmenu = chainSubmenus(
|
|
1239
|
+
textInputSubmenu("Agent id to bind", "Native agents only, such as scout, researcher, or provenance."),
|
|
1240
|
+
(agentId) => selectListSubmenu(`Select the profile for ${agentId}`, profileNameChoices(live)),
|
|
1241
|
+
(agentId, profile) => `${agentId} -> ${profile}`,
|
|
1242
|
+
);
|
|
1243
|
+
const targetSubmenu = options?.providers ? selectTargetSubmenu(options.providers) : editTextSubmenu("Type target id");
|
|
1244
|
+
const orchestratorModelSubmenu = options?.providers
|
|
1245
|
+
? selectModelSubmenu(options.providers, () => live().orchestrator.target ?? undefined)
|
|
1246
|
+
: editTextSubmenu("Type model name");
|
|
1247
|
+
const workerModelSubmenu = options?.providers
|
|
1248
|
+
? selectModelSubmenu(options.providers, () => live().workers.default.target ?? undefined)
|
|
1249
|
+
: editTextSubmenu("Type model name");
|
|
1250
|
+
const backgroundTargetSubmenu = options?.providers
|
|
1251
|
+
? selectOptionalBackgroundTargetSubmenu(options.providers)
|
|
1252
|
+
: editTextSubmenu("Type memory target id", "Leave blank for rules-only memory.");
|
|
1253
|
+
const backgroundModelSubmenu = options?.providers
|
|
1254
|
+
? selectModelSubmenu(options.providers, () => live().background.target ?? undefined)
|
|
1255
|
+
: editTextSubmenu("Type memory model name");
|
|
1256
|
+
const favorites = settings.modelSelector?.favorites ?? [];
|
|
1257
|
+
const agents = settings.delegation?.agents ?? [];
|
|
1258
|
+
const keybindingCount = Object.keys(settings.keybindings ?? {}).length;
|
|
1259
|
+
return [
|
|
1260
|
+
settingItem("autonomy", settings.autonomy, {
|
|
1261
|
+
values: ["read-only", "suggest", "auto-edit", "full-auto"],
|
|
1262
|
+
}),
|
|
1263
|
+
settingItem("workers.onPermission", settings.workers.onPermission ?? "deny", {
|
|
1264
|
+
values: ["deny", "fail", "escalate"],
|
|
1265
|
+
}),
|
|
1266
|
+
settingItem("delegation.defaults.toolGovernance", settings.delegation.defaults.toolGovernance, {
|
|
1267
|
+
values: ["clio-policy", "agent-managed", "deny-all"],
|
|
1268
|
+
}),
|
|
1269
|
+
settingItem("skills.trustProjectCompatRoots", String(settings.skills.trustProjectCompatRoots), {
|
|
1270
|
+
values: ["false", "true"],
|
|
1271
|
+
}),
|
|
1272
|
+
settingItem("safetyNet", "always on", {
|
|
1273
|
+
affordance: "tuned in .clio-coder/safety.yaml",
|
|
1274
|
+
readOnly: true,
|
|
1275
|
+
}),
|
|
1276
|
+
settingItem("orchestrator.thinkingLevel", orchestratorThinking.display, {
|
|
1277
|
+
values: orchestratorThinking.values,
|
|
1278
|
+
}),
|
|
1279
|
+
settingItem("orchestrator.target", settings.orchestrator.target ?? "(unset)", {
|
|
1280
|
+
submenu: targetSubmenu,
|
|
1281
|
+
affordance: options?.providers ? "opens picker" : "free text",
|
|
1282
|
+
}),
|
|
1283
|
+
settingItem("orchestrator.model", settings.orchestrator.model ?? "(unset)", {
|
|
1284
|
+
submenu: orchestratorModelSubmenu,
|
|
1285
|
+
affordance: options?.providers ? "opens picker" : "free text",
|
|
1286
|
+
}),
|
|
1287
|
+
settingItem("background.target", settings.background.target ?? "(unset — rules-only)", {
|
|
1288
|
+
submenu: backgroundTargetSubmenu,
|
|
1289
|
+
affordance: options?.providers ? "opens picker" : "free text",
|
|
1290
|
+
}),
|
|
1291
|
+
settingItem("background.model", settings.background.model ?? "(unset)", {
|
|
1292
|
+
submenu: backgroundModelSubmenu,
|
|
1293
|
+
affordance: options?.providers ? "opens picker" : "free text",
|
|
1294
|
+
}),
|
|
1295
|
+
settingItem("background.thinkingLevel", backgroundThinking.display, {
|
|
1296
|
+
values: backgroundThinking.values,
|
|
1297
|
+
}),
|
|
1298
|
+
settingItem("memory.intervention.enabled", String(settings.memory.intervention.enabled), {
|
|
1299
|
+
values: ["true", "false"],
|
|
1300
|
+
}),
|
|
1301
|
+
settingItem("memory.intervention.everyNTools", String(settings.memory.intervention.everyNTools), {
|
|
1302
|
+
values: ["5", "10", "20", "30"],
|
|
1303
|
+
}),
|
|
1304
|
+
settingItem("memory.intervention.windowSteps", String(settings.memory.intervention.windowSteps), {
|
|
1305
|
+
values: ["4", "8", "12", "20"],
|
|
1306
|
+
}),
|
|
1307
|
+
settingItem("memory.intervention.maxTokens", String(settings.memory.intervention.maxTokens), {
|
|
1308
|
+
values: ["100", "200", "400", "800"],
|
|
1309
|
+
}),
|
|
1310
|
+
settingItem("memory.intervention.timeoutMs", String(settings.memory.intervention.timeoutMs), {
|
|
1311
|
+
values: ["5000", "10000", "20000", "30000", "60000"],
|
|
1312
|
+
}),
|
|
1313
|
+
fleetGroupHeader("fleet.group.defaults", "Defaults"),
|
|
1314
|
+
settingItem("workers.default.target", settings.workers.default.target ?? "(unset)", {
|
|
1315
|
+
submenu: targetSubmenu,
|
|
1316
|
+
affordance: options?.providers ? "opens picker" : "free text",
|
|
1317
|
+
}),
|
|
1318
|
+
settingItem("workers.default.model", settings.workers.default.model ?? "(unset)", {
|
|
1319
|
+
submenu: workerModelSubmenu,
|
|
1320
|
+
affordance: options?.providers ? "opens picker" : "free text",
|
|
1321
|
+
}),
|
|
1322
|
+
settingItem("workers.default.thinkingLevel", workerThinking.display, {
|
|
1323
|
+
values: workerThinking.values,
|
|
1324
|
+
}),
|
|
1325
|
+
settingItem("workers.maxRetries", String(settings.workers.maxRetries), {
|
|
1326
|
+
values: ["0", "1", "2", "3", "5", "8"],
|
|
1327
|
+
}),
|
|
1328
|
+
fleetGroupHeader("fleet.group.profiles", "Profiles"),
|
|
1329
|
+
...fleetProfileRows(settings, live, options),
|
|
1330
|
+
settingItem("workers.profiles", "", {
|
|
1331
|
+
label: "Add profile",
|
|
1332
|
+
submenu: addProfileSubmenu,
|
|
1333
|
+
affordance: "Enter: name and route a new profile",
|
|
1334
|
+
presentationKind: "action",
|
|
1335
|
+
valueSegments: [],
|
|
1336
|
+
}),
|
|
1337
|
+
fleetGroupHeader("fleet.group.agent-routes", "Agent routes"),
|
|
1338
|
+
...agentBindingRows(settings, live),
|
|
1339
|
+
settingItem("workers.agentBindings", "", {
|
|
1340
|
+
label: "Add agent route",
|
|
1341
|
+
...(profileCount > 0 ? { submenu: addBindingSubmenu } : { readOnly: true }),
|
|
1342
|
+
affordance: profileCount > 0 ? "Enter binds an agent" : "create a profile first",
|
|
1343
|
+
presentationKind: profileCount > 0 ? "action" : "read-only-fact",
|
|
1344
|
+
valueSegments: [],
|
|
1345
|
+
}),
|
|
1346
|
+
fleetGroupHeader("fleet.group.placement", "Placement"),
|
|
1347
|
+
...fleetNodeRows(options?.getFleetNodes?.() ?? []),
|
|
1348
|
+
settingItem("targets", "", {
|
|
1349
|
+
description: "Live inference target inventory and routing roles.",
|
|
1350
|
+
affordance: "column heading",
|
|
1351
|
+
readOnly: true,
|
|
1352
|
+
presentationKind: "group-header",
|
|
1353
|
+
valueSegments: [],
|
|
1354
|
+
}),
|
|
1355
|
+
...targetRows(settings, options),
|
|
1356
|
+
targetAddCta(),
|
|
1357
|
+
settingItem("scope", scopeText, {
|
|
1358
|
+
submenu: scopedModelsSubmenu(scopeList, options?.providers),
|
|
1359
|
+
affordance: "opens provider-backed checklist",
|
|
1360
|
+
}),
|
|
1361
|
+
settingItem("modelSelector.recentLimit", String(settings.modelSelector.recentLimit), {
|
|
1362
|
+
values: ["6", "12", "20", "50"],
|
|
1363
|
+
}),
|
|
1364
|
+
settingItem("modelSelector.favorites", favorites.length > 0 ? `${favorites.length} pinned` : "(none)", {
|
|
1365
|
+
affordance: "manage in /model",
|
|
1366
|
+
readOnly: true,
|
|
1367
|
+
}),
|
|
1368
|
+
settingItem("budget.sessionCeilingUsd", String(settings.budget.sessionCeilingUsd), {
|
|
1369
|
+
submenu: editNumberSubmenu("Edit session cost ceiling USD"),
|
|
1370
|
+
affordance: "free text",
|
|
1371
|
+
}),
|
|
1372
|
+
settingItem("defaults.maxTokens", String(settings.defaults.maxTokens), {
|
|
1373
|
+
values: ["0", "4096", "8192", "16384", "32768", "65536", "131072"],
|
|
1374
|
+
}),
|
|
1375
|
+
settingItem("budget.concurrency", String(settings.budget.concurrency), {
|
|
1376
|
+
values: ["auto", "1", "2", "4", "8"],
|
|
1377
|
+
}),
|
|
1378
|
+
settingItem("compaction.auto", String(compaction.auto), {
|
|
1379
|
+
values: ["true", "false"],
|
|
1380
|
+
}),
|
|
1381
|
+
settingItem("compaction.threshold", formatThreshold(compaction.threshold), {
|
|
1382
|
+
values: ["0.7", "0.8", "0.85", "0.9"],
|
|
1383
|
+
}),
|
|
1384
|
+
settingItem("compaction.excludeLastTurns", String(compaction.excludeLastTurns), {
|
|
1385
|
+
values: ["3", "6", "10", "15"],
|
|
1386
|
+
}),
|
|
1387
|
+
settingItem("retry.enabled", String(retry.enabled), {
|
|
1388
|
+
values: ["true", "false"],
|
|
1389
|
+
}),
|
|
1390
|
+
settingItem("retry.maxRetries", String(retry.maxRetries), {
|
|
1391
|
+
values: ["0", "1", "2", "3", "5", "8"],
|
|
1392
|
+
}),
|
|
1393
|
+
settingItem("retry.baseDelayMs", String(retry.baseDelayMs), {
|
|
1394
|
+
values: ["500", "1000", "2000", "5000", "10000"],
|
|
1395
|
+
}),
|
|
1396
|
+
settingItem("retry.maxDelayMs", String(retry.maxDelayMs), {
|
|
1397
|
+
values: ["10000", "30000", "60000", "120000", "300000"],
|
|
1398
|
+
}),
|
|
1399
|
+
settingItem("terminal.showTerminalProgress", String(terminal.showTerminalProgress), {
|
|
1400
|
+
values: ["false", "true"],
|
|
1401
|
+
}),
|
|
1402
|
+
settingItem("terminal.outputVerbosity", terminal.outputVerbosity, {
|
|
1403
|
+
values: ["minimal", "default", "verbose"],
|
|
1404
|
+
}),
|
|
1405
|
+
settingItem("terminal.tuiMode", terminal.tuiMode, {
|
|
1406
|
+
values: ["regular", "fullscreen"],
|
|
1407
|
+
}),
|
|
1408
|
+
settingItem("terminal.fullscreenScrollbar", terminal.fullscreenScrollbar, {
|
|
1409
|
+
values: ["hidden", "auto", "always"],
|
|
1410
|
+
}),
|
|
1411
|
+
settingItem("terminal.smoothStreaming", terminal.smoothStreaming, {
|
|
1412
|
+
values: ["off", "auto", "on"],
|
|
1413
|
+
}),
|
|
1414
|
+
settingItem("theme", settings.theme, {
|
|
1415
|
+
affordance: "single clio-coder palette",
|
|
1416
|
+
readOnly: true,
|
|
1417
|
+
}),
|
|
1418
|
+
settingItem("runtimePlugins", settings.runtimePlugins.length > 0 ? settings.runtimePlugins.join(", ") : "(none)", {
|
|
1419
|
+
submenu: editTextSubmenu("Edit runtime plugins comma-separated list", "Restart Clio to load changes."),
|
|
1420
|
+
affordance: "free text",
|
|
1421
|
+
}),
|
|
1422
|
+
settingItem("attribution.gitCommits", settings.attribution.gitCommits ? "enabled" : "disabled", {
|
|
1423
|
+
values: ["enabled", "disabled"],
|
|
1424
|
+
}),
|
|
1425
|
+
settingItem("compaction.model", compaction.model ?? "(orchestrator target)", {
|
|
1426
|
+
submenu: editTextSubmenu("Edit compaction model; blank uses the orchestrator"),
|
|
1427
|
+
affordance: "free text",
|
|
1428
|
+
}),
|
|
1429
|
+
settingItem("compaction.systemPrompt", compaction.systemPrompt ?? "(built-in)", {
|
|
1430
|
+
submenu: editTextSubmenu("Edit compaction prompt path; blank uses the built-in"),
|
|
1431
|
+
affordance: "free text",
|
|
1432
|
+
}),
|
|
1433
|
+
settingItem("delegation.defaults.connectTimeoutMs", String(settings.delegation.defaults.connectTimeoutMs), {
|
|
1434
|
+
submenu: editNumberSubmenu("Edit delegate connect timeout (ms)"),
|
|
1435
|
+
affordance: "free text",
|
|
1436
|
+
}),
|
|
1437
|
+
settingItem("delegation.defaults.turnTimeoutMs", String(settings.delegation.defaults.turnTimeoutMs), {
|
|
1438
|
+
submenu: editNumberSubmenu("Edit delegate turn timeout (ms)"),
|
|
1439
|
+
affordance: "free text",
|
|
1440
|
+
}),
|
|
1441
|
+
settingItem("delegation.defaults.permissionTimeoutMs", String(settings.delegation.defaults.permissionTimeoutMs), {
|
|
1442
|
+
submenu: editNumberSubmenu("Edit delegate permission timeout (ms)"),
|
|
1443
|
+
affordance: "free text",
|
|
1444
|
+
}),
|
|
1445
|
+
settingItem("keybindings", keybindingCount > 0 ? `${keybindingCount} override(s)` : "(defaults)", {
|
|
1446
|
+
affordance: "edit settings.yaml",
|
|
1447
|
+
readOnly: true,
|
|
1448
|
+
}),
|
|
1449
|
+
settingItem("delegation.agents", agents.length > 0 ? `${agents.length} agent(s)` : "(none)", {
|
|
1450
|
+
...delegationAgentsAffordance(agents, options?.getInteropProposals?.() ?? []),
|
|
1451
|
+
}),
|
|
1452
|
+
];
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function fleetProfileRows(
|
|
1456
|
+
settings: Readonly<ClioSettings>,
|
|
1457
|
+
live: () => Readonly<ClioSettings>,
|
|
1458
|
+
options: BuildSettingItemsOptions | undefined,
|
|
1459
|
+
): SettingsCenterItem[] {
|
|
1460
|
+
const providers = options?.providers;
|
|
1461
|
+
return Object.entries(settings.workers.profiles)
|
|
1462
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
1463
|
+
.map(([name, profile]) => {
|
|
1464
|
+
const thinking = thinkingChoices(providers, profile.target, profile.model, profile.thinkingLevel);
|
|
1465
|
+
const target = profile.target ?? "(unset)";
|
|
1466
|
+
const model = profile.model ?? "(unset)";
|
|
1467
|
+
const placement = profile.node ?? "auto";
|
|
1468
|
+
const summary = `${target}/${model} ${thinking.display} ${placement}`;
|
|
1469
|
+
const compactThinking = truncateProfileFact(thinking.display, 7);
|
|
1470
|
+
const compactPlacement = truncateProfileFact(placement, 7);
|
|
1471
|
+
const route = target === settings.workers.default.target ? model : `${target}/${model}`;
|
|
1472
|
+
const routeBudget = Math.max(
|
|
1473
|
+
8,
|
|
1474
|
+
PROFILE_SUMMARY_VALUE_BUDGET -
|
|
1475
|
+
visibleWidth(compactThinking) -
|
|
1476
|
+
visibleWidth(compactPlacement) -
|
|
1477
|
+
visibleWidth(ROW_GAP) * 2,
|
|
1478
|
+
);
|
|
1479
|
+
const compactRoute = truncateProfileFact(route, routeBudget);
|
|
1480
|
+
return settingItem(`workers.profiles.${name}`, summary, {
|
|
1481
|
+
label: name,
|
|
1482
|
+
description: `Profile ${name}. Enter to edit its target, model, thinking level, placement, or remove it.`,
|
|
1483
|
+
help: ["target", "model", "thinkingLevel", "node"].map((field) => `workers.profiles.${name}.${field}`).join(" · "),
|
|
1484
|
+
submenu: profileWorkbenchSubmenu(name, settings, live, options),
|
|
1485
|
+
affordance: "Enter: drill into profile fields",
|
|
1486
|
+
valueSegments: [
|
|
1487
|
+
{ text: compactRoute, tone: "neutral" },
|
|
1488
|
+
{ text: `${ROW_GAP}${compactThinking}`, tone: "neutral" },
|
|
1489
|
+
{ text: `${ROW_GAP}${compactPlacement}`, tone: "neutral" },
|
|
1490
|
+
],
|
|
1491
|
+
});
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function profileWorkbenchSubmenu(
|
|
1496
|
+
name: string,
|
|
1497
|
+
settings: Readonly<ClioSettings>,
|
|
1498
|
+
live: () => Readonly<ClioSettings>,
|
|
1499
|
+
options: BuildSettingItemsOptions | undefined,
|
|
1500
|
+
): SettingSubmenuBuilder {
|
|
1501
|
+
return (_currentValue: string, done: (value?: string) => void): Component => {
|
|
1502
|
+
const fields = [
|
|
1503
|
+
{ value: "target", label: "Edit target", presentationKind: "action" as const },
|
|
1504
|
+
{ value: "model", label: "Edit model", presentationKind: "action" as const },
|
|
1505
|
+
{ value: "thinkingLevel", label: "Edit thinking level", presentationKind: "action" as const },
|
|
1506
|
+
{ value: "node", label: "Edit placement", presentationKind: "action" as const },
|
|
1507
|
+
{
|
|
1508
|
+
value: "remove",
|
|
1509
|
+
label: "Remove profile",
|
|
1510
|
+
presentationKind: "destructive-action" as const,
|
|
1511
|
+
},
|
|
1512
|
+
];
|
|
1513
|
+
let active: Component;
|
|
1514
|
+
const openField = (field?: string): void => {
|
|
1515
|
+
if (!field) {
|
|
1516
|
+
done();
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
const profile = live().workers.profiles[name];
|
|
1520
|
+
if (!profile) {
|
|
1521
|
+
done();
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
if (field === "remove") {
|
|
1525
|
+
done(REMOVE_PROFILE_CHOICE);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
const finish = (value?: string): void =>
|
|
1529
|
+
done(value === undefined ? undefined : `${field}${PROFILE_FIELD_SEPARATOR}${value}`);
|
|
1530
|
+
if (field === "target") {
|
|
1531
|
+
active = selectListSubmenu(
|
|
1532
|
+
`Target for profile ${name}`,
|
|
1533
|
+
profileTargetChoices(live, options?.providers),
|
|
1534
|
+
"Changing the target rebases the model on that target's default.",
|
|
1535
|
+
)(profile.target ?? "(unset)", finish);
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
if (field === "model") {
|
|
1539
|
+
const submenu = options?.providers
|
|
1540
|
+
? selectModelSubmenu(options.providers, () => live().workers.profiles[name]?.target ?? undefined)
|
|
1541
|
+
: editTextSubmenu("Type model name");
|
|
1542
|
+
active = submenu(profile.model ?? "(unset)", finish);
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
if (field === "thinkingLevel") {
|
|
1546
|
+
const thinking = thinkingChoices(options?.providers, profile.target, profile.model, profile.thinkingLevel);
|
|
1547
|
+
active = selectListSubmenu(
|
|
1548
|
+
`Thinking level for profile ${name}`,
|
|
1549
|
+
thinking.values.map((value) => ({ value, label: value })),
|
|
1550
|
+
)(thinking.display, finish);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
active = selectListSubmenu(`Placement for profile ${name}`, profileNodeChoices(settings, options))(
|
|
1554
|
+
profile.node ?? AUTO_PLACEMENT_CHOICE,
|
|
1555
|
+
finish,
|
|
1556
|
+
);
|
|
1557
|
+
};
|
|
1558
|
+
const picker = selectListSubmenu(`Profile ${name}`, fields)("", openField);
|
|
1559
|
+
active = picker;
|
|
1560
|
+
return {
|
|
1561
|
+
render: (width: number) => active.render(width),
|
|
1562
|
+
handleInput: (data: string) => active.handleInput?.(data),
|
|
1563
|
+
invalidate: () => active.invalidate?.(),
|
|
1564
|
+
};
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/** Pin choices: auto placement, never-remote local, then every declared node (with live state when known). */
|
|
1569
|
+
function profileNodeChoices(
|
|
1570
|
+
settings: Readonly<ClioSettings>,
|
|
1571
|
+
options: BuildSettingItemsOptions | undefined,
|
|
1572
|
+
): Array<{ value: string; label: string }> {
|
|
1573
|
+
const live = new Map(options?.getFleetNodes?.().map((node) => [node.id, node] as const) ?? []);
|
|
1574
|
+
return [
|
|
1575
|
+
{ value: AUTO_PLACEMENT_CHOICE, label: AUTO_PLACEMENT_CHOICE },
|
|
1576
|
+
{ value: "local", label: "local (never remote)" },
|
|
1577
|
+
...settings.fleet.nodes.map((node) => {
|
|
1578
|
+
const state = live.get(node.id);
|
|
1579
|
+
return {
|
|
1580
|
+
value: node.id,
|
|
1581
|
+
label: `${node.id} (${node.host}${state ? `, ${state.state}` : ""})`,
|
|
1582
|
+
};
|
|
1583
|
+
}),
|
|
1584
|
+
];
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function agentBindingRows(settings: Readonly<ClioSettings>, live: () => Readonly<ClioSettings>): SettingsCenterItem[] {
|
|
1588
|
+
return Object.entries(settings.workers.agentBindings)
|
|
1589
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
1590
|
+
.map(([agentId, profileName]) => {
|
|
1591
|
+
const missing = !settings.workers.profiles[profileName];
|
|
1592
|
+
return settingItem(`workers.agentBindings.${agentId}`, profileName, {
|
|
1593
|
+
label: `${agentId} · profile`,
|
|
1594
|
+
description: missing
|
|
1595
|
+
? `Agent ${agentId} is bound to profile ${profileName}, which does not exist; it dispatches on the fleet default until the profile is created.`
|
|
1596
|
+
: `Profile agent ${agentId} dispatches with.`,
|
|
1597
|
+
submenu: selectListSubmenu(`Profile for ${agentId}`, [
|
|
1598
|
+
...profileNameChoices(live),
|
|
1599
|
+
{ value: UNBIND_CHOICE, label: UNBIND_CHOICE },
|
|
1600
|
+
]),
|
|
1601
|
+
});
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function targetRows(
|
|
1606
|
+
settings: Readonly<ClioSettings>,
|
|
1607
|
+
options: BuildSettingItemsOptions | undefined,
|
|
1608
|
+
): SettingsCenterItem[] {
|
|
1609
|
+
const providers = options?.providers;
|
|
1610
|
+
const statuses = new Map(providers?.list().map((status) => [status.target.id, status] as const) ?? []);
|
|
1611
|
+
return settings.targets.map((target) => {
|
|
1612
|
+
const status = statuses.get(target.id);
|
|
1613
|
+
const roles = [
|
|
1614
|
+
settings.orchestrator.target === target.id ? "chat" : null,
|
|
1615
|
+
settings.workers.default.target === target.id ? "fleet" : null,
|
|
1616
|
+
settings.background.target === target.id ? "memory" : null,
|
|
1617
|
+
].filter((role) => role !== null);
|
|
1618
|
+
const health = status?.health.status ?? "unknown";
|
|
1619
|
+
const operation = options?.getTargetOperation?.(target.id) ?? null;
|
|
1620
|
+
const roleText = roles.length > 0 ? roles.join("+") : "—";
|
|
1621
|
+
const healthSegment = targetHealthSegment(health);
|
|
1622
|
+
const activitySegment: SettingsValueSegment | null = operation
|
|
1623
|
+
? {
|
|
1624
|
+
text: `${GLYPH.running} ${operation === "connect" ? "connecting" : "probing"}`,
|
|
1625
|
+
tone: "activity",
|
|
1626
|
+
}
|
|
1627
|
+
: null;
|
|
1628
|
+
const liveHealth = activitySegment ?? healthSegment;
|
|
1629
|
+
const runtime = status?.runtime?.id ?? target.runtime;
|
|
1630
|
+
const latency =
|
|
1631
|
+
status?.health.latencyMs === null || status?.health.latencyMs === undefined ? "—" : `${status.health.latencyMs} ms`;
|
|
1632
|
+
const lastProbe = status?.health.lastCheckAt ? clockLocal(status.health.lastCheckAt) : "never";
|
|
1633
|
+
const failureReason = status?.health.lastError ?? (!status?.available && status?.reason ? status.reason : "none");
|
|
1634
|
+
const valueSegments = [
|
|
1635
|
+
liveHealth,
|
|
1636
|
+
{ text: ` ${target.id}`, tone: "neutral" as const },
|
|
1637
|
+
{ text: ` ${roleText}`, tone: "neutral" as const },
|
|
1638
|
+
{ text: ` ${runtime}`, tone: "neutral" as const },
|
|
1639
|
+
{ text: ` ${latency}`, tone: "neutral" as const },
|
|
1640
|
+
];
|
|
1641
|
+
const value = `${health} · ${target.id} · ${roleText} · ${runtime} · ${latency}`;
|
|
1642
|
+
const item = settingItem(`targets.${target.id}`, value, {
|
|
1643
|
+
label: target.id,
|
|
1644
|
+
description: `URL: ${target.url ?? "(none)"} · Default model: ${target.defaultModel ?? "(none)"}`,
|
|
1645
|
+
help: `Last probe: ${lastProbe} · Failure reason: ${failureReason}`,
|
|
1646
|
+
submenu: targetActionsSubmenu(target.id, options),
|
|
1647
|
+
affordance: options?.connectTarget ? "Enter: use, connect, probe, remove" : "Enter: use, probe, remove",
|
|
1648
|
+
presentationKind: "status",
|
|
1649
|
+
valueSegments,
|
|
1650
|
+
});
|
|
1651
|
+
item.targetConsole = {
|
|
1652
|
+
health: liveHealth,
|
|
1653
|
+
id: target.id,
|
|
1654
|
+
roles: roleText,
|
|
1655
|
+
runtime,
|
|
1656
|
+
latency,
|
|
1657
|
+
url: target.url ?? "(none)",
|
|
1658
|
+
defaultModel: target.defaultModel ?? "(none)",
|
|
1659
|
+
lastProbe,
|
|
1660
|
+
failureReason,
|
|
1661
|
+
};
|
|
1662
|
+
return item;
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function targetHealthSegment(status: TargetHealth["status"]): SettingsValueSegment {
|
|
1667
|
+
switch (status) {
|
|
1668
|
+
case "healthy":
|
|
1669
|
+
return { text: `${GLYPH.running} healthy`, tone: "healthy" };
|
|
1670
|
+
case "degraded":
|
|
1671
|
+
return { text: "◐ degraded", tone: "degraded" };
|
|
1672
|
+
case "down":
|
|
1673
|
+
return { text: "○ down", tone: "unhealthy" };
|
|
1674
|
+
case "unknown":
|
|
1675
|
+
return { text: "? unknown", tone: "unknown" };
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/** Read-only placement rows: where dispatched workers run, from the live scheduler snapshot. */
|
|
1680
|
+
function fleetNodeRows(nodes: ReadonlyArray<FleetNodeSnapshot>): SettingsCenterItem[] {
|
|
1681
|
+
return nodes.map((node) => {
|
|
1682
|
+
const busy = node.maxWorkers > 0 ? `${node.activeWorkers}/${node.maxWorkers} busy` : `${node.activeWorkers} busy`;
|
|
1683
|
+
return settingItem(`fleet.nodes.${node.id}`, `${node.state} · ${busy}`, {
|
|
1684
|
+
label: `node ${node.id}`,
|
|
1685
|
+
description: `${node.kind} · ${node.host}${node.stateReason ? ` · ${node.stateReason}` : ""}${node.lastSeenAt ? ` · seen ${clockLocal(node.lastSeenAt)}` : ""}`,
|
|
1686
|
+
affordance: "declared as fleet.nodes in settings.yaml; `clio-coder doctor` preflights them",
|
|
1687
|
+
readOnly: true,
|
|
1688
|
+
presentationKind: "status",
|
|
1689
|
+
valueSegments: [
|
|
1690
|
+
{
|
|
1691
|
+
text: `${node.state === "online" ? GLYPH.running : "○"} ${node.state}`,
|
|
1692
|
+
tone: node.state === "online" ? "healthy" : "unhealthy",
|
|
1693
|
+
},
|
|
1694
|
+
{ text: ` · ${busy}`, tone: "neutral" },
|
|
1695
|
+
],
|
|
1696
|
+
});
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
export function buildSettingsSections(items: readonly SettingsCenterItem[]): SettingsCenterSection[] {
|
|
1701
|
+
return SETTINGS_SECTIONS.map((section) => ({
|
|
1702
|
+
id: section.id,
|
|
1703
|
+
label: section.label,
|
|
1704
|
+
items: items.filter((item) => item.section === section.id),
|
|
1705
|
+
}));
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
function refreshSettingItemsInPlace(items: SettingsCenterItem[], next: readonly SettingsCenterItem[]): void {
|
|
1709
|
+
const byId = new Map(next.map((item) => [item.id, item] as const));
|
|
1710
|
+
// Entry rows come and go with their profiles, bindings, and targets; surviving row objects keep their identity.
|
|
1711
|
+
const existing = new Map(items.map((item) => [item.id, item] as const));
|
|
1712
|
+
items.splice(0, items.length, ...next.map((item) => existing.get(item.id) ?? item));
|
|
1713
|
+
for (const item of items) {
|
|
1714
|
+
const updated = byId.get(item.id);
|
|
1715
|
+
if (!updated) continue;
|
|
1716
|
+
item.label = updated.label;
|
|
1717
|
+
item.currentValue = updated.currentValue;
|
|
1718
|
+
item.description = updated.description;
|
|
1719
|
+
item.section = updated.section;
|
|
1720
|
+
item.configPath = updated.configPath;
|
|
1721
|
+
item.affordance = updated.affordance;
|
|
1722
|
+
item.scope = updated.scope;
|
|
1723
|
+
item.readOnly = updated.readOnly;
|
|
1724
|
+
item.presentationKind = updated.presentationKind;
|
|
1725
|
+
item.valueSegments = updated.valueSegments;
|
|
1726
|
+
if (updated.targetConsole) item.targetConsole = updated.targetConsole;
|
|
1727
|
+
else delete item.targetConsole;
|
|
1728
|
+
if (updated.help) item.help = updated.help;
|
|
1729
|
+
else delete item.help;
|
|
1730
|
+
if (updated.valueHelp) item.valueHelp = updated.valueHelp;
|
|
1731
|
+
else delete item.valueHelp;
|
|
1732
|
+
if (updated.defaultValue !== undefined) item.defaultValue = updated.defaultValue;
|
|
1733
|
+
else delete item.defaultValue;
|
|
1734
|
+
if (updated.values) item.values = updated.values;
|
|
1735
|
+
else delete item.values;
|
|
1736
|
+
if (updated.submenu) item.submenu = updated.submenu;
|
|
1737
|
+
else delete item.submenu;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
function formatThreshold(value: number): string {
|
|
1742
|
+
return Number.isFinite(value) ? String(value) : "0.8";
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function applyNonNegativeInteger(value: string, set: (next: number) => void): void {
|
|
1746
|
+
const parsed = Number(value);
|
|
1747
|
+
if (Number.isFinite(parsed) && parsed >= 0) set(Math.floor(parsed));
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function applyPositiveInteger(value: string, set: (next: number) => void): void {
|
|
1751
|
+
const parsed = Number(value);
|
|
1752
|
+
if (Number.isInteger(parsed) && parsed >= 1 && parsed <= MAX_TIMER_DELAY_MS) set(parsed);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/**
|
|
1756
|
+
* Pure mutation applied in place for Settings Center editable rows.
|
|
1757
|
+
*/
|
|
1758
|
+
function applyDelegationAgentChange(settings: ClioSettings, value: string): void {
|
|
1759
|
+
if (value.startsWith(DELEGATION_REMOVE_PREFIX)) {
|
|
1760
|
+
const id = value.slice(DELEGATION_REMOVE_PREFIX.length);
|
|
1761
|
+
settings.delegation.agents = settings.delegation.agents.filter((agent) => agent.id !== id);
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
if (!value.startsWith(DELEGATION_ADD_PREFIX)) return;
|
|
1765
|
+
const kind = interopAgentKind(value.slice(DELEGATION_ADD_PREFIX.length) as InteropAgentId);
|
|
1766
|
+
if (kind?.acp === undefined) return;
|
|
1767
|
+
const entry = delegationEntryForKind(kind, settings.delegation.defaults);
|
|
1768
|
+
if (settings.delegation.agents.some((agent) => agent.id === entry.id)) return;
|
|
1769
|
+
settings.delegation.agents.push(entry);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
export function applySettingChange(settings: ClioSettings, id: string, value: string): void {
|
|
1773
|
+
if (applyEntrySettingChange(settings, id, value)) return;
|
|
1774
|
+
switch (id) {
|
|
1775
|
+
case "autonomy":
|
|
1776
|
+
if (value === "read-only" || value === "suggest" || value === "auto-edit" || value === "full-auto")
|
|
1777
|
+
settings.autonomy = value;
|
|
1778
|
+
return;
|
|
1779
|
+
case "workers.onPermission":
|
|
1780
|
+
if (value === "deny" || value === "fail" || value === "escalate") settings.workers.onPermission = value;
|
|
1781
|
+
return;
|
|
1782
|
+
case "delegation.defaults.toolGovernance":
|
|
1783
|
+
if (value === "clio-policy" || value === "agent-managed" || value === "deny-all")
|
|
1784
|
+
settings.delegation.defaults.toolGovernance = value;
|
|
1785
|
+
return;
|
|
1786
|
+
case "delegation.agents":
|
|
1787
|
+
applyDelegationAgentChange(settings, value);
|
|
1788
|
+
return;
|
|
1789
|
+
case "skills.trustProjectCompatRoots":
|
|
1790
|
+
if (value === "true" || value === "false") settings.skills.trustProjectCompatRoots = value === "true";
|
|
1791
|
+
return;
|
|
1792
|
+
case "attribution.gitCommits":
|
|
1793
|
+
if (value === "enabled" || value === "disabled") settings.attribution.gitCommits = value === "enabled";
|
|
1794
|
+
return;
|
|
1795
|
+
case "orchestrator.thinkingLevel":
|
|
1796
|
+
settings.orchestrator.thinkingLevel = thinkingLevelFromChoiceLabel(value) ?? settings.orchestrator.thinkingLevel;
|
|
1797
|
+
return;
|
|
1798
|
+
case "background.thinkingLevel":
|
|
1799
|
+
settings.background.thinkingLevel = thinkingLevelFromChoiceLabel(value) ?? settings.background.thinkingLevel;
|
|
1800
|
+
return;
|
|
1801
|
+
case "memory.intervention.enabled":
|
|
1802
|
+
if (value === "true" || value === "false") settings.memory.intervention.enabled = value === "true";
|
|
1803
|
+
return;
|
|
1804
|
+
case "memory.intervention.everyNTools":
|
|
1805
|
+
case "memory.intervention.windowSteps":
|
|
1806
|
+
case "memory.intervention.maxTokens":
|
|
1807
|
+
case "memory.intervention.timeoutMs":
|
|
1808
|
+
applyNonNegativeInteger(value, (next) => {
|
|
1809
|
+
if (next >= 1) {
|
|
1810
|
+
const key = id.slice("memory.intervention.".length) as "everyNTools" | "windowSteps" | "maxTokens" | "timeoutMs";
|
|
1811
|
+
settings.memory.intervention[key] = next;
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
return;
|
|
1815
|
+
case "workers.default.thinkingLevel":
|
|
1816
|
+
settings.workers.default.thinkingLevel =
|
|
1817
|
+
thinkingLevelFromChoiceLabel(value) ?? settings.workers.default.thinkingLevel;
|
|
1818
|
+
return;
|
|
1819
|
+
case "workers.maxRetries":
|
|
1820
|
+
applyNonNegativeInteger(value, (next) => {
|
|
1821
|
+
settings.workers.maxRetries = next;
|
|
1822
|
+
});
|
|
1823
|
+
return;
|
|
1824
|
+
case "modelSelector.recentLimit":
|
|
1825
|
+
applyNonNegativeInteger(value, (next) => {
|
|
1826
|
+
if (next >= 1) settings.modelSelector.recentLimit = next;
|
|
1827
|
+
});
|
|
1828
|
+
return;
|
|
1829
|
+
case "defaults.maxTokens":
|
|
1830
|
+
applyNonNegativeInteger(value, (next) => {
|
|
1831
|
+
settings.defaults.maxTokens = next;
|
|
1832
|
+
});
|
|
1833
|
+
return;
|
|
1834
|
+
case "budget.concurrency": {
|
|
1835
|
+
if (value === "auto") {
|
|
1836
|
+
settings.budget.concurrency = "auto";
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
applyNonNegativeInteger(value, (next) => {
|
|
1840
|
+
if (next >= 1) settings.budget.concurrency = next;
|
|
1841
|
+
});
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
case "compaction.auto":
|
|
1845
|
+
if (value === "true" || value === "false") settings.compaction.auto = value === "true";
|
|
1846
|
+
return;
|
|
1847
|
+
case "compaction.excludeLastTurns":
|
|
1848
|
+
applyNonNegativeInteger(value, (next) => {
|
|
1849
|
+
if (next > 0) settings.compaction.excludeLastTurns = next;
|
|
1850
|
+
});
|
|
1851
|
+
return;
|
|
1852
|
+
case "compaction.threshold": {
|
|
1853
|
+
const parsed = Number(value);
|
|
1854
|
+
if (Number.isFinite(parsed) && parsed >= 0 && parsed <= 1) settings.compaction.threshold = parsed;
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
case "compaction.model": {
|
|
1858
|
+
const trimmed = value.trim();
|
|
1859
|
+
if (trimmed) settings.compaction.model = trimmed;
|
|
1860
|
+
else delete settings.compaction.model;
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
case "compaction.systemPrompt": {
|
|
1864
|
+
const trimmed = value.trim();
|
|
1865
|
+
if (trimmed) settings.compaction.systemPrompt = trimmed;
|
|
1866
|
+
else delete settings.compaction.systemPrompt;
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
case "retry.enabled":
|
|
1870
|
+
if (value === "true" || value === "false") settings.retry.enabled = value === "true";
|
|
1871
|
+
return;
|
|
1872
|
+
case "retry.maxRetries":
|
|
1873
|
+
applyNonNegativeInteger(value, (next) => {
|
|
1874
|
+
settings.retry.maxRetries = next;
|
|
1875
|
+
});
|
|
1876
|
+
return;
|
|
1877
|
+
case "retry.baseDelayMs":
|
|
1878
|
+
applyNonNegativeInteger(value, (next) => {
|
|
1879
|
+
settings.retry.baseDelayMs = next;
|
|
1880
|
+
});
|
|
1881
|
+
return;
|
|
1882
|
+
case "retry.maxDelayMs":
|
|
1883
|
+
applyNonNegativeInteger(value, (next) => {
|
|
1884
|
+
settings.retry.maxDelayMs = next;
|
|
1885
|
+
});
|
|
1886
|
+
return;
|
|
1887
|
+
case "terminal.showTerminalProgress":
|
|
1888
|
+
if (value === "true" || value === "false") settings.terminal.showTerminalProgress = value === "true";
|
|
1889
|
+
return;
|
|
1890
|
+
case "terminal.outputVerbosity":
|
|
1891
|
+
if (value === "minimal" || value === "default" || value === "verbose") settings.terminal.outputVerbosity = value;
|
|
1892
|
+
return;
|
|
1893
|
+
case "terminal.tuiMode":
|
|
1894
|
+
if (value === "regular" || value === "fullscreen") settings.terminal.tuiMode = value;
|
|
1895
|
+
return;
|
|
1896
|
+
case "terminal.fullscreenScrollbar":
|
|
1897
|
+
if (value === "hidden" || value === "auto" || value === "always") {
|
|
1898
|
+
settings.terminal.fullscreenScrollbar = value;
|
|
1899
|
+
}
|
|
1900
|
+
return;
|
|
1901
|
+
case "terminal.smoothStreaming":
|
|
1902
|
+
if (value === "off" || value === "auto" || value === "on") settings.terminal.smoothStreaming = value;
|
|
1903
|
+
return;
|
|
1904
|
+
case "runtimePlugins":
|
|
1905
|
+
settings.runtimePlugins = value
|
|
1906
|
+
.split(",")
|
|
1907
|
+
.map((entry) => entry.trim())
|
|
1908
|
+
.filter(Boolean);
|
|
1909
|
+
return;
|
|
1910
|
+
case "delegation.defaults.connectTimeoutMs":
|
|
1911
|
+
applyPositiveInteger(value, (next) => {
|
|
1912
|
+
settings.delegation.defaults.connectTimeoutMs = next;
|
|
1913
|
+
});
|
|
1914
|
+
return;
|
|
1915
|
+
case "delegation.defaults.turnTimeoutMs":
|
|
1916
|
+
applyPositiveInteger(value, (next) => {
|
|
1917
|
+
settings.delegation.defaults.turnTimeoutMs = next;
|
|
1918
|
+
});
|
|
1919
|
+
return;
|
|
1920
|
+
case "delegation.defaults.permissionTimeoutMs":
|
|
1921
|
+
applyPositiveInteger(value, (next) => {
|
|
1922
|
+
settings.delegation.defaults.permissionTimeoutMs = next;
|
|
1923
|
+
});
|
|
1924
|
+
return;
|
|
1925
|
+
case "orchestrator.target": {
|
|
1926
|
+
const target = value === "(unset)" || value === "" ? null : value;
|
|
1927
|
+
// Switching targets re-bases the model on the new target default.
|
|
1928
|
+
if (target !== settings.orchestrator.target) {
|
|
1929
|
+
settings.orchestrator.model = target
|
|
1930
|
+
? (settings.targets.find((entry) => entry.id === target)?.defaultModel ?? null)
|
|
1931
|
+
: null;
|
|
1932
|
+
}
|
|
1933
|
+
settings.orchestrator.target = target;
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
case "orchestrator.model":
|
|
1937
|
+
settings.orchestrator.model = value === "(unset)" || value === "" ? null : value;
|
|
1938
|
+
return;
|
|
1939
|
+
case "background.target": {
|
|
1940
|
+
const target = value.startsWith("(unset") || value === "" ? null : value;
|
|
1941
|
+
if (target !== settings.background.target) {
|
|
1942
|
+
settings.background.model = target
|
|
1943
|
+
? (settings.targets.find((entry) => entry.id === target)?.defaultModel ?? null)
|
|
1944
|
+
: null;
|
|
1945
|
+
}
|
|
1946
|
+
settings.background.target = target;
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
case "background.model":
|
|
1950
|
+
settings.background.model = value === "(unset)" || value === "" ? null : value;
|
|
1951
|
+
return;
|
|
1952
|
+
case "workers.default.target": {
|
|
1953
|
+
const target = value === "(unset)" || value === "" ? null : value;
|
|
1954
|
+
if (target !== settings.workers.default.target) {
|
|
1955
|
+
settings.workers.default.model = target
|
|
1956
|
+
? (settings.targets.find((entry) => entry.id === target)?.defaultModel ?? null)
|
|
1957
|
+
: null;
|
|
1958
|
+
}
|
|
1959
|
+
settings.workers.default.target = target;
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
case "workers.default.model":
|
|
1963
|
+
settings.workers.default.model = value === "(unset)" || value === "" ? null : value;
|
|
1964
|
+
return;
|
|
1965
|
+
case "scope":
|
|
1966
|
+
settings.scope =
|
|
1967
|
+
parseScopedModelSelection(value) ??
|
|
1968
|
+
value
|
|
1969
|
+
.split(",")
|
|
1970
|
+
.map((v) => v.trim())
|
|
1971
|
+
.filter(Boolean);
|
|
1972
|
+
return;
|
|
1973
|
+
case "budget.sessionCeilingUsd": {
|
|
1974
|
+
const parsed = Number(value);
|
|
1975
|
+
if (Number.isFinite(parsed) && parsed >= 0) settings.budget.sessionCeilingUsd = parsed;
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
/** Per-entry rows route to the same mutations the `clio-coder targets` CLI applies. */
|
|
1982
|
+
function applyEntrySettingChange(settings: ClioSettings, id: string, value: string): boolean {
|
|
1983
|
+
if (id === "workers.profiles" || id === "workers.agentBindings") {
|
|
1984
|
+
// `name -> target` / `agent -> profile`: the encoding the chained add pickers produce.
|
|
1985
|
+
const [head = "", tail = ""] = value.split(" -> ").map((part) => part.trim());
|
|
1986
|
+
if (!head || !tail) return true;
|
|
1987
|
+
if (id === "workers.profiles") setFleetProfileInSettings(settings, head, tail);
|
|
1988
|
+
else bindAgentProfileInSettings(settings, head, tail);
|
|
1989
|
+
return true;
|
|
1990
|
+
}
|
|
1991
|
+
if (id.startsWith("workers.profiles.")) {
|
|
1992
|
+
const encoded = value.split(PROFILE_FIELD_SEPARATOR);
|
|
1993
|
+
const summaryName = id.slice("workers.profiles.".length);
|
|
1994
|
+
const legacySegments = id.split(".");
|
|
1995
|
+
const legacyField = legacySegments.at(-1);
|
|
1996
|
+
const usesLegacyField =
|
|
1997
|
+
!settings.workers.profiles[summaryName] && ["target", "model", "thinkingLevel", "node"].includes(legacyField ?? "");
|
|
1998
|
+
const field = usesLegacyField ? legacyField : encoded.shift();
|
|
1999
|
+
const name = usesLegacyField ? legacySegments.slice(2, -1).join(".") : summaryName;
|
|
2000
|
+
const fieldValue = usesLegacyField ? value : encoded.join(PROFILE_FIELD_SEPARATOR);
|
|
2001
|
+
const profile = settings.workers.profiles[name];
|
|
2002
|
+
if (!profile) return true;
|
|
2003
|
+
if (value === REMOVE_PROFILE_CHOICE) {
|
|
2004
|
+
removeFleetProfileFromSettings(settings, name);
|
|
2005
|
+
} else if (field === "target") {
|
|
2006
|
+
if (fieldValue !== profile.target) setFleetProfileInSettings(settings, name, fieldValue);
|
|
2007
|
+
} else if (field === "model") {
|
|
2008
|
+
profile.model = fieldValue === "(unset)" || fieldValue === "" ? null : fieldValue;
|
|
2009
|
+
} else if (field === "thinkingLevel") {
|
|
2010
|
+
profile.thinkingLevel = thinkingLevelFromChoiceLabel(fieldValue) ?? profile.thinkingLevel;
|
|
2011
|
+
} else if (field === "node") {
|
|
2012
|
+
if (fieldValue === AUTO_PLACEMENT_CHOICE || fieldValue === "") delete profile.node;
|
|
2013
|
+
else profile.node = fieldValue;
|
|
2014
|
+
}
|
|
2015
|
+
return true;
|
|
2016
|
+
}
|
|
2017
|
+
if (id.startsWith("workers.agentBindings.")) {
|
|
2018
|
+
const agentId = id.slice("workers.agentBindings.".length);
|
|
2019
|
+
if (value === UNBIND_CHOICE) delete settings.workers.agentBindings[agentId];
|
|
2020
|
+
else bindAgentProfileInSettings(settings, agentId, value);
|
|
2021
|
+
return true;
|
|
2022
|
+
}
|
|
2023
|
+
if (id.startsWith("targets.")) {
|
|
2024
|
+
const targetId = id.slice("targets.".length);
|
|
2025
|
+
if (value === "use") useTargetInSettings(settings, targetId);
|
|
2026
|
+
else if (value === "remove") removeTargetFromSettings(settings, targetId);
|
|
2027
|
+
return true;
|
|
2028
|
+
}
|
|
2029
|
+
return false;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
/**
|
|
2033
|
+
* The scoped commit persists one dotted leaf per call and an entry action can
|
|
2034
|
+
* touch several (`use` moves chat and fleet routing; removing a profile drops
|
|
2035
|
+
* its bindings), so diff the blobs. Profile objects and arrays are leaves.
|
|
2036
|
+
*/
|
|
2037
|
+
function changedLeafPaths(before: Readonly<ClioSettings>, after: Readonly<ClioSettings>): string[] {
|
|
2038
|
+
const out: string[] = [];
|
|
2039
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
2040
|
+
v !== null && typeof v === "object" && !Array.isArray(v);
|
|
2041
|
+
const visit = (a: unknown, b: unknown, path: string): void => {
|
|
2042
|
+
if (a === b) return;
|
|
2043
|
+
const profileLeaf = path.startsWith("workers.profiles.") && path.split(".").length === 3;
|
|
2044
|
+
if (!isRecord(a) || !isRecord(b) || profileLeaf) {
|
|
2045
|
+
if (JSON.stringify(a) !== JSON.stringify(b)) out.push(path);
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
2049
|
+
visit(a[key], b[key], path ? `${path}.${key}` : key);
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
visit(before, after, "");
|
|
2053
|
+
return out;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
type DeepReadonly<T> = T extends (...args: never[]) => unknown
|
|
2057
|
+
? T
|
|
2058
|
+
: T extends readonly (infer U)[]
|
|
2059
|
+
? readonly DeepReadonly<U>[]
|
|
2060
|
+
: T extends object
|
|
2061
|
+
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
|
|
2062
|
+
: T;
|
|
2063
|
+
|
|
2064
|
+
export type SettingsPropagationTiming = "now" | "next-dispatch" | "next-session";
|
|
2065
|
+
|
|
2066
|
+
export interface SettingsChangePlan {
|
|
2067
|
+
readonly rowId: EditableSettingId;
|
|
2068
|
+
readonly label: string;
|
|
2069
|
+
readonly originalValue: string;
|
|
2070
|
+
readonly selectedValue: string;
|
|
2071
|
+
readonly original: DeepReadonly<ClioSettings>;
|
|
2072
|
+
readonly proposed: DeepReadonly<ClioSettings>;
|
|
2073
|
+
readonly leaves: readonly {
|
|
2074
|
+
readonly path: string;
|
|
2075
|
+
readonly before: unknown;
|
|
2076
|
+
readonly after: unknown;
|
|
2077
|
+
}[];
|
|
2078
|
+
readonly propagation: readonly {
|
|
2079
|
+
readonly path: string;
|
|
2080
|
+
readonly timing: SettingsPropagationTiming;
|
|
2081
|
+
}[];
|
|
2082
|
+
readonly impact: string;
|
|
2083
|
+
readonly sessionCapable: boolean;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
function deepFreeze<T>(value: T): DeepReadonly<T> {
|
|
2087
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
2088
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
2089
|
+
Object.freeze(value);
|
|
2090
|
+
}
|
|
2091
|
+
return value as DeepReadonly<T>;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
function propagationTiming(
|
|
2095
|
+
id: string,
|
|
2096
|
+
selectedValue: string,
|
|
2097
|
+
path: string,
|
|
2098
|
+
restartRequired: boolean,
|
|
2099
|
+
): SettingsPropagationTiming {
|
|
2100
|
+
if (restartRequired) return "next-session";
|
|
2101
|
+
if (id.startsWith("targets.")) {
|
|
2102
|
+
if (selectedValue === "remove") return "next-dispatch";
|
|
2103
|
+
return path.startsWith("orchestrator.") ? "now" : "next-dispatch";
|
|
2104
|
+
}
|
|
2105
|
+
if (path.startsWith("workers.")) return "next-dispatch";
|
|
2106
|
+
return "now";
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
function impactFor(propagation: SettingsChangePlan["propagation"]): string {
|
|
2110
|
+
const timings = new Set(propagation.map((entry) => entry.timing));
|
|
2111
|
+
if (timings.has("now") && timings.has("next-dispatch"))
|
|
2112
|
+
return "takes effect for chat now and fleet routing at the next dispatch";
|
|
2113
|
+
if (timings.has("next-session")) return "takes effect next session";
|
|
2114
|
+
if (timings.has("next-dispatch")) return "takes effect at the next dispatch";
|
|
2115
|
+
return "takes effect now";
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
export function createSettingsChangePlan(
|
|
2119
|
+
settings: Readonly<ClioSettings>,
|
|
2120
|
+
item: Pick<SettingsCenterItem, "id" | "label" | "currentValue" | "scope">,
|
|
2121
|
+
selectedValue: string,
|
|
2122
|
+
sessionDestinationAvailable = true,
|
|
2123
|
+
): SettingsChangePlan | null {
|
|
2124
|
+
if (item.id.startsWith("fleet.group.")) return null;
|
|
2125
|
+
const rowId = item.id as EditableSettingId;
|
|
2126
|
+
const original = structuredClone(settings);
|
|
2127
|
+
const proposed = structuredClone(original);
|
|
2128
|
+
applySettingChange(proposed, rowId, selectedValue);
|
|
2129
|
+
const paths = changedLeafPaths(original, proposed);
|
|
2130
|
+
if (paths.length === 0) return null;
|
|
2131
|
+
const restartRequired = item.scope === "restart";
|
|
2132
|
+
const sessionCapable = !restartRequired && sessionDestinationAvailable;
|
|
2133
|
+
const leaves = paths.map((path) => ({
|
|
2134
|
+
path,
|
|
2135
|
+
before: getAtPath(original, path),
|
|
2136
|
+
after: getAtPath(proposed, path),
|
|
2137
|
+
}));
|
|
2138
|
+
const propagation = paths.map((path) => ({
|
|
2139
|
+
path,
|
|
2140
|
+
timing: propagationTiming(rowId, selectedValue, path, restartRequired),
|
|
2141
|
+
}));
|
|
2142
|
+
return deepFreeze({
|
|
2143
|
+
rowId,
|
|
2144
|
+
label: item.label,
|
|
2145
|
+
originalValue: item.currentValue,
|
|
2146
|
+
selectedValue,
|
|
2147
|
+
original,
|
|
2148
|
+
proposed,
|
|
2149
|
+
leaves,
|
|
2150
|
+
propagation,
|
|
2151
|
+
impact: impactFor(propagation),
|
|
2152
|
+
sessionCapable,
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
/**
|
|
2157
|
+
* A selection of several model references is a set, not a destination.
|
|
2158
|
+
* Spelling it out produced `…-coder-model, node-b/example-…`, a left-chopped ref dump
|
|
2159
|
+
* with the row label pushed off the front, so a multi-reference selection is
|
|
2160
|
+
* summarized by count and the note beneath the title carries the leaves.
|
|
2161
|
+
*/
|
|
2162
|
+
function scopedSelectionSummary(plan: SettingsChangePlan): string | null {
|
|
2163
|
+
if (plan.rowId !== "scope") return null;
|
|
2164
|
+
const refs = parseScopedModelSelection(plan.selectedValue);
|
|
2165
|
+
return refs && refs.length >= 2 ? `${refs.length} changes` : null;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
/** Keep the destination—the value the operator is about to commit—visible as confirmation titles narrow. */
|
|
2169
|
+
function formatScopeConfirmTitle(plan: SettingsChangePlan, width: number): string {
|
|
2170
|
+
const summary = scopedSelectionSummary(plan);
|
|
2171
|
+
if (summary !== null) {
|
|
2172
|
+
const title = `${plan.label}: ${summary}`;
|
|
2173
|
+
return visibleWidth(title) <= width ? title : ellipsizeFromLeft(title, width);
|
|
2174
|
+
}
|
|
2175
|
+
const selectedValue = humanizeChangePlanValue(plan);
|
|
2176
|
+
const full = plan.originalValue.trim()
|
|
2177
|
+
? `${plan.label}: ${plan.originalValue} → ${selectedValue}`
|
|
2178
|
+
: `${plan.label}: ${selectedValue}`;
|
|
2179
|
+
if (visibleWidth(full) <= width) return full;
|
|
2180
|
+
const destinationFirst = plan.originalValue.trim()
|
|
2181
|
+
? `${plan.label}: → ${selectedValue}`
|
|
2182
|
+
: `${plan.label}: ${selectedValue}`;
|
|
2183
|
+
if (visibleWidth(destinationFirst) <= width) return destinationFirst;
|
|
2184
|
+
return ellipsizeFromLeft(destinationFirst, width);
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
function humanizeChangePlanValue(plan: SettingsChangePlan): string {
|
|
2188
|
+
if (plan.rowId === "scope") {
|
|
2189
|
+
const summary = scopedSelectionSummary(plan);
|
|
2190
|
+
if (summary !== null) return summary;
|
|
2191
|
+
const refs = parseScopedModelSelection(plan.selectedValue);
|
|
2192
|
+
if (refs) return refs.length > 0 ? refs.join(", ") : "(empty)";
|
|
2193
|
+
}
|
|
2194
|
+
const separatorIndex = plan.selectedValue.indexOf(PROFILE_FIELD_SEPARATOR);
|
|
2195
|
+
if (separatorIndex < 0) return plan.selectedValue;
|
|
2196
|
+
const head = plan.selectedValue.slice(0, separatorIndex).trim();
|
|
2197
|
+
const tail = plan.selectedValue.slice(separatorIndex + PROFILE_FIELD_SEPARATOR.length).trim();
|
|
2198
|
+
if (plan.rowId === "workers.profiles" || plan.rowId === "workers.agentBindings") return `${head} → ${tail}`;
|
|
2199
|
+
if (!plan.rowId.startsWith("workers.profiles.")) return plan.selectedValue;
|
|
2200
|
+
const label =
|
|
2201
|
+
head === "thinkingLevel" ? "thinking" : head === "node" ? "placement" : head === "target" ? "target" : "model";
|
|
2202
|
+
return `${label} → ${tail}`;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
function truncateProfileFact(text: string, width: number): string {
|
|
2206
|
+
const safeWidth = Math.max(1, width);
|
|
2207
|
+
if (visibleWidth(text) <= safeWidth) return text;
|
|
2208
|
+
if (safeWidth <= visibleWidth(ELLIPSIS)) return ELLIPSIS;
|
|
2209
|
+
const characters: string[] = [];
|
|
2210
|
+
let used = 0;
|
|
2211
|
+
const contentWidth = safeWidth - visibleWidth(ELLIPSIS);
|
|
2212
|
+
for (const character of Array.from(text)) {
|
|
2213
|
+
const characterWidth = visibleWidth(character);
|
|
2214
|
+
if (used + characterWidth > contentWidth) break;
|
|
2215
|
+
characters.push(character);
|
|
2216
|
+
used += characterWidth;
|
|
2217
|
+
}
|
|
2218
|
+
return `${characters.join("")}${ELLIPSIS}`;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
function ellipsizeFromLeft(text: string, width: number): string {
|
|
2222
|
+
const safeWidth = Math.max(1, width);
|
|
2223
|
+
if (visibleWidth(text) <= safeWidth) return text;
|
|
2224
|
+
if (safeWidth <= visibleWidth(ELLIPSIS)) return ELLIPSIS;
|
|
2225
|
+
const suffixWidth = safeWidth - visibleWidth(ELLIPSIS);
|
|
2226
|
+
const suffix: string[] = [];
|
|
2227
|
+
let used = 0;
|
|
2228
|
+
for (const character of Array.from(text).reverse()) {
|
|
2229
|
+
const characterWidth = visibleWidth(character);
|
|
2230
|
+
if (used + characterWidth > suffixWidth) break;
|
|
2231
|
+
suffix.unshift(character);
|
|
2232
|
+
used += characterWidth;
|
|
2233
|
+
}
|
|
2234
|
+
return `${ELLIPSIS}${suffix.join("")}`;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
interface RowColumns {
|
|
2238
|
+
label: number;
|
|
2239
|
+
path: number;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
/** When a change reaches the running system, in the vocabulary of the config-change classification. */
|
|
2243
|
+
function propagationFor(id: string): string | null {
|
|
2244
|
+
if (RESTART_REQUIRED_IDS.has(id)) return "takes effect next session";
|
|
2245
|
+
if (id !== "targets.add-cta" && id.startsWith("targets."))
|
|
2246
|
+
return "use: chat now, workers at the next dispatch · remove: next dispatch";
|
|
2247
|
+
if (id.startsWith("workers.")) return "takes effect at the next dispatch";
|
|
2248
|
+
return null;
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
function fixedLines(lines: readonly string[], width: number, height: number): string[] {
|
|
2252
|
+
const out = lines.slice(0, height).map((line) => padAnsi(line, width, ELLIPSIS));
|
|
2253
|
+
while (out.length < height) out.push(" ".repeat(Math.max(0, width)));
|
|
2254
|
+
return out;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
/** Typed text, as opposed to an escape sequence or a control byte. */
|
|
2258
|
+
function isPrintableInput(data: string): boolean {
|
|
2259
|
+
if (data.length === 0 || data.startsWith("\u001b")) return false;
|
|
2260
|
+
return Array.from(data).every((character) => character >= " " && character !== "\u007f");
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
function scrollWindow(total: number, selected: number, height: number): [number, number] {
|
|
2264
|
+
if (height <= 0 || total <= height) return [0, total];
|
|
2265
|
+
const clamped = Math.max(0, Math.min(selected, total - 1));
|
|
2266
|
+
const start = Math.max(0, Math.min(clamped - Math.floor(height / 2), total - height));
|
|
2267
|
+
return [start, Math.min(total, start + height)];
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
function rowColumns(items: readonly SettingsCenterItem[], width: number, indentWidth: number): RowColumns {
|
|
2271
|
+
const safeWidth = Math.max(1, width);
|
|
2272
|
+
const prefixWidth = indentWidth + 2;
|
|
2273
|
+
// Drop the dotted config-path column entirely on very narrow terminals so the
|
|
2274
|
+
// value never gets squeezed out of view.
|
|
2275
|
+
if (safeWidth < DROP_PATH_COLUMN_WIDTH) {
|
|
2276
|
+
const available = Math.max(1, safeWidth - prefixWidth - visibleWidth(ROW_GAP));
|
|
2277
|
+
const label = Math.min(20, Math.max(6, Math.floor(available * 0.5)));
|
|
2278
|
+
return { label, path: 0 };
|
|
2279
|
+
}
|
|
2280
|
+
const available = Math.max(1, safeWidth - prefixWidth - visibleWidth(ROW_GAP) * 2);
|
|
2281
|
+
const labelNatural = Math.max(8, ...items.map((item) => visibleWidth(item.label)));
|
|
2282
|
+
const pathNatural = Math.max(10, ...items.map((item) => visibleWidth(item.configPath)));
|
|
2283
|
+
let label = Math.min(labelNatural, 26, Math.max(8, Math.floor(available * 0.34)));
|
|
2284
|
+
let path = Math.min(pathNatural, 36, Math.max(8, Math.floor(available * 0.42)));
|
|
2285
|
+
while (available - label - path < 8 && path > 8) path -= 1;
|
|
2286
|
+
while (available - label - path < 8 && label > 8) label -= 1;
|
|
2287
|
+
if (available - label - path < 4) {
|
|
2288
|
+
path = Math.max(4, available - label - 4);
|
|
2289
|
+
}
|
|
2290
|
+
return { label, path };
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
function formatSettingRow(
|
|
2294
|
+
item: SettingsCenterItem,
|
|
2295
|
+
width: number,
|
|
2296
|
+
selected: boolean,
|
|
2297
|
+
columns: RowColumns,
|
|
2298
|
+
indentWidth: number,
|
|
2299
|
+
displayValue: string,
|
|
2300
|
+
pending: boolean,
|
|
2301
|
+
): string {
|
|
2302
|
+
const theme = clioTheme();
|
|
2303
|
+
const indent = " ".repeat(Math.max(0, indentWidth));
|
|
2304
|
+
if (item.id === "targets") return formatTargetConsoleHeader(width, indentWidth);
|
|
2305
|
+
if (item.targetConsole) return formatTargetConsoleRow(item, width, selected, indentWidth);
|
|
2306
|
+
if (item.id === "targets.add-cta") {
|
|
2307
|
+
const prefix = selected ? theme.fg("accent", `${GLYPH.cursor} `) : " ";
|
|
2308
|
+
const label = theme.style("accentDeep", item.label, { bold: true });
|
|
2309
|
+
return truncateToWidth(
|
|
2310
|
+
`${indent}${prefix}${label}${ROW_GAP}${theme.fg(selected ? "accent" : "muted", item.currentValue)}`,
|
|
2311
|
+
width,
|
|
2312
|
+
ELLIPSIS,
|
|
2313
|
+
true,
|
|
2314
|
+
);
|
|
2315
|
+
}
|
|
2316
|
+
const prefix = selected ? theme.fg("accent", `${GLYPH.cursor} `) : " ";
|
|
2317
|
+
const labelText = padAnsi(item.label, columns.label, ELLIPSIS);
|
|
2318
|
+
const label = selected
|
|
2319
|
+
? theme.style("accent", labelText, { bold: true })
|
|
2320
|
+
: item.presentationKind === "group-header"
|
|
2321
|
+
? theme.style("dim", labelText, { bold: true })
|
|
2322
|
+
: item.presentationKind === "action"
|
|
2323
|
+
? theme.style("accentDeep", labelText, { bold: true })
|
|
2324
|
+
: labelText;
|
|
2325
|
+
if (item.presentationKind === "group-header") {
|
|
2326
|
+
return truncateToWidth(`${indent} ${label}`, width, ELLIPSIS, true);
|
|
2327
|
+
}
|
|
2328
|
+
const modified = !item.readOnly && item.defaultValue !== undefined && item.currentValue !== item.defaultValue;
|
|
2329
|
+
const marker = pending
|
|
2330
|
+
? theme.fg("accent", `${GLYPH.scoped} `)
|
|
2331
|
+
: modified
|
|
2332
|
+
? theme.fg("accent", `${GLYPH.scoped} `)
|
|
2333
|
+
: " ";
|
|
2334
|
+
let used = visibleWidth(indent) + 2 + columns.label + visibleWidth(ROW_GAP);
|
|
2335
|
+
let pathSegment = "";
|
|
2336
|
+
// Status rows need enough room for both their role/fact and their semantic
|
|
2337
|
+
// health text. At the ultrawide center-column floor, the dotted config path
|
|
2338
|
+
// is the expendable metadata; keeping it would collapse `chat+fleet` to
|
|
2339
|
+
// `cha…` beside an otherwise readable health state.
|
|
2340
|
+
if (columns.path > 0 && !(item.presentationKind === "status" && width < 64)) {
|
|
2341
|
+
pathSegment = `${theme.fg("dim", padAnsi(item.configPath, columns.path, ELLIPSIS))}${ROW_GAP}`;
|
|
2342
|
+
used += columns.path + visibleWidth(ROW_GAP);
|
|
2343
|
+
}
|
|
2344
|
+
const valueWidth = Math.max(1, width - used - 2);
|
|
2345
|
+
const valueSegments = pending
|
|
2346
|
+
? [{ text: displayValue, tone: "neutral" as const }]
|
|
2347
|
+
: item.presentationKind === "read-only-fact"
|
|
2348
|
+
? [{ text: "— ", tone: "neutral" as const }, ...item.valueSegments]
|
|
2349
|
+
: item.valueSegments;
|
|
2350
|
+
const value = renderSettingValue(valueSegments, valueWidth, selected, item.readOnly);
|
|
2351
|
+
return truncateToWidth(`${indent}${prefix}${label}${ROW_GAP}${pathSegment}${marker}${value}`, width, ELLIPSIS, true);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
function targetConsoleColumns(
|
|
2355
|
+
width: number,
|
|
2356
|
+
): Array<{ key: "health" | "id" | "roles" | "runtime" | "latency"; width: number }> {
|
|
2357
|
+
const safeWidth = Math.max(1, width);
|
|
2358
|
+
if (safeWidth < 34) {
|
|
2359
|
+
const health = Math.min(11, Math.max(6, Math.floor(safeWidth * 0.42)));
|
|
2360
|
+
return [
|
|
2361
|
+
{ key: "health", width: health },
|
|
2362
|
+
{ key: "id", width: Math.max(1, safeWidth - health - 2) },
|
|
2363
|
+
];
|
|
2364
|
+
}
|
|
2365
|
+
if (safeWidth < 47) {
|
|
2366
|
+
const latency = 7;
|
|
2367
|
+
const health = 10;
|
|
2368
|
+
return [
|
|
2369
|
+
{ key: "health", width: health },
|
|
2370
|
+
{ key: "id", width: Math.max(6, safeWidth - health - latency - 4) },
|
|
2371
|
+
{ key: "latency", width: latency },
|
|
2372
|
+
];
|
|
2373
|
+
}
|
|
2374
|
+
if (safeWidth < 56) {
|
|
2375
|
+
const gaps = 6;
|
|
2376
|
+
const health = 12;
|
|
2377
|
+
const latency = 7;
|
|
2378
|
+
const roles = 6;
|
|
2379
|
+
return [
|
|
2380
|
+
{ key: "health", width: health },
|
|
2381
|
+
{ key: "id", width: safeWidth - gaps - health - latency - roles },
|
|
2382
|
+
{ key: "roles", width: roles },
|
|
2383
|
+
{ key: "latency", width: latency },
|
|
2384
|
+
];
|
|
2385
|
+
}
|
|
2386
|
+
const gaps = 8;
|
|
2387
|
+
const cells = safeWidth - gaps;
|
|
2388
|
+
const health = 12;
|
|
2389
|
+
const latency = 7;
|
|
2390
|
+
const extra = cells - 48;
|
|
2391
|
+
const roles = Math.min(13, 6 + Math.floor(extra * 0.25));
|
|
2392
|
+
const id = Math.min(18, 8 + Math.floor(extra * 0.5));
|
|
2393
|
+
return [
|
|
2394
|
+
{ key: "health", width: health },
|
|
2395
|
+
{ key: "id", width: id },
|
|
2396
|
+
{ key: "roles", width: roles },
|
|
2397
|
+
{ key: "runtime", width: cells - health - latency - roles - id },
|
|
2398
|
+
{ key: "latency", width: latency },
|
|
2399
|
+
];
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
function formatTargetConsoleHeader(width: number, indentWidth: number): string {
|
|
2403
|
+
const theme = clioTheme();
|
|
2404
|
+
const indent = " ".repeat(Math.max(0, indentWidth));
|
|
2405
|
+
const available = Math.max(1, width - visibleWidth(indent) - 2);
|
|
2406
|
+
const labels = { health: "HEALTH", id: "TARGET", roles: "ROLES", runtime: "RUNTIME", latency: "LATENCY" } as const;
|
|
2407
|
+
const cells = targetConsoleColumns(available).map((column) => padAnsi(labels[column.key], column.width, ELLIPSIS));
|
|
2408
|
+
return truncateToWidth(`${indent} ${theme.style("dim", cells.join(ROW_GAP), { bold: true })}`, width, ELLIPSIS, true);
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
function formatTargetConsoleRow(
|
|
2412
|
+
item: SettingsCenterItem,
|
|
2413
|
+
width: number,
|
|
2414
|
+
selected: boolean,
|
|
2415
|
+
indentWidth: number,
|
|
2416
|
+
): string {
|
|
2417
|
+
const console = item.targetConsole;
|
|
2418
|
+
if (!console) return "";
|
|
2419
|
+
const theme = clioTheme();
|
|
2420
|
+
const indent = " ".repeat(Math.max(0, indentWidth));
|
|
2421
|
+
const prefix = selected ? theme.fg("accent", `${GLYPH.cursor} `) : " ";
|
|
2422
|
+
const available = Math.max(1, width - visibleWidth(indent) - 2);
|
|
2423
|
+
const cells = targetConsoleColumns(available).map((column) => {
|
|
2424
|
+
const text = column.key === "health" ? console.health.text : console[column.key];
|
|
2425
|
+
const padded = padAnsi(text, column.width, ELLIPSIS);
|
|
2426
|
+
if (column.key === "health")
|
|
2427
|
+
return renderSettingValue([{ ...console.health, text: padded }], column.width, selected, false);
|
|
2428
|
+
if (column.key === "id" && selected) return theme.style("accent", padded, { bold: true });
|
|
2429
|
+
return theme.fg(selected ? "accent" : "muted", padded);
|
|
2430
|
+
});
|
|
2431
|
+
return truncateToWidth(`${indent}${prefix}${cells.join(ROW_GAP)}`, width, ELLIPSIS, true);
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
function renderSettingValue(
|
|
2435
|
+
segments: readonly SettingsValueSegment[],
|
|
2436
|
+
width: number,
|
|
2437
|
+
selected: boolean,
|
|
2438
|
+
readOnly: boolean,
|
|
2439
|
+
): string {
|
|
2440
|
+
const theme = clioTheme();
|
|
2441
|
+
let semantic: SettingsValueSegment | null = null;
|
|
2442
|
+
for (let index = segments.length - 1; index >= 0; index -= 1) {
|
|
2443
|
+
const candidate = segments[index];
|
|
2444
|
+
if (candidate && candidate.tone !== "neutral") {
|
|
2445
|
+
semantic = candidate;
|
|
2446
|
+
break;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
let visible = segments;
|
|
2450
|
+
if (semantic && visibleWidth(segments.map((segment) => segment.text).join("")) > width) {
|
|
2451
|
+
const semanticWidth = Math.min(width, visibleWidth(semantic.text));
|
|
2452
|
+
const prefixWidth = Math.max(0, width - semanticWidth);
|
|
2453
|
+
const prefix = segments
|
|
2454
|
+
.slice(0, segments.indexOf(semantic))
|
|
2455
|
+
.map((segment) => segment.text)
|
|
2456
|
+
.join("")
|
|
2457
|
+
.replace(/ · $/, "·");
|
|
2458
|
+
visible = [
|
|
2459
|
+
...(prefixWidth > 0
|
|
2460
|
+
? [
|
|
2461
|
+
{
|
|
2462
|
+
text: visibleWidth(prefix) <= prefixWidth ? prefix : truncateToWidth(prefix, prefixWidth, ELLIPSIS, true),
|
|
2463
|
+
tone: "neutral" as const,
|
|
2464
|
+
},
|
|
2465
|
+
]
|
|
2466
|
+
: []),
|
|
2467
|
+
{
|
|
2468
|
+
...semantic,
|
|
2469
|
+
text: truncateToWidth(semantic.text, semanticWidth, ELLIPSIS, true),
|
|
2470
|
+
},
|
|
2471
|
+
];
|
|
2472
|
+
}
|
|
2473
|
+
let remaining = width;
|
|
2474
|
+
const rendered: string[] = [];
|
|
2475
|
+
for (const segment of visible) {
|
|
2476
|
+
if (remaining <= 0) break;
|
|
2477
|
+
const text = truncateToWidth(segment.text, remaining, ELLIPSIS);
|
|
2478
|
+
remaining -= visibleWidth(text);
|
|
2479
|
+
const token =
|
|
2480
|
+
segment.tone === "healthy"
|
|
2481
|
+
? "success"
|
|
2482
|
+
: segment.tone === "degraded"
|
|
2483
|
+
? "warning"
|
|
2484
|
+
: segment.tone === "unhealthy"
|
|
2485
|
+
? "error"
|
|
2486
|
+
: segment.tone === "activity"
|
|
2487
|
+
? "action"
|
|
2488
|
+
: segment.tone === "unknown"
|
|
2489
|
+
? "dim"
|
|
2490
|
+
: readOnly
|
|
2491
|
+
? "dim"
|
|
2492
|
+
: selected
|
|
2493
|
+
? "accent"
|
|
2494
|
+
: "muted";
|
|
2495
|
+
rendered.push(theme.fg(token, text));
|
|
2496
|
+
}
|
|
2497
|
+
return rendered.join("");
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
export interface SettingsCenterOptions {
|
|
2501
|
+
getBodyHeight: () => number;
|
|
2502
|
+
prepareChange: (item: SettingsCenterItem, newValue: string) => SettingsChangePlan | null;
|
|
2503
|
+
onApply: (plan: SettingsChangePlan, scope: "session" | "global") => void;
|
|
2504
|
+
onCancel: () => void;
|
|
2505
|
+
requestRender?: () => void;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
export class SettingsCenter implements Component {
|
|
2509
|
+
/** The stack level that owns input; "detail" is implied by an open submenu. */
|
|
2510
|
+
private level: SettingsCenterLane = "rows";
|
|
2511
|
+
private selectedSectionId: SettingsSectionId = SETTINGS_SECTIONS[0].id;
|
|
2512
|
+
private readonly rowIndexBySection = new Map<SettingsSectionId, number>();
|
|
2513
|
+
/** Semantic anchors survive rows being inserted, removed, or reordered during refresh. */
|
|
2514
|
+
private readonly rowIdBySection = new Map<SettingsSectionId, SettingsCenterRowId>();
|
|
2515
|
+
private submenuComponent: Component | null = null;
|
|
2516
|
+
/** Committed catalog filter; empty means unfiltered. */
|
|
2517
|
+
private filterQuery = "";
|
|
2518
|
+
/**
|
|
2519
|
+
* Draft while the filter editor owns input; null when it is closed. The
|
|
2520
|
+
* draft narrows the catalog live per keystroke, matching /model and /resume;
|
|
2521
|
+
* Enter commits it and Esc restores the committed query.
|
|
2522
|
+
*/
|
|
2523
|
+
private filterDraft: string | null = null;
|
|
2524
|
+
/** Local cycle preview for the selected row; committed on Enter. */
|
|
2525
|
+
private pendingValue: string | null = null;
|
|
2526
|
+
|
|
2527
|
+
constructor(
|
|
2528
|
+
private readonly items: SettingsCenterItem[],
|
|
2529
|
+
private readonly options: SettingsCenterOptions,
|
|
2530
|
+
) {}
|
|
2531
|
+
|
|
2532
|
+
getSelection(): SettingsCenterSelection {
|
|
2533
|
+
const section = this.currentSection();
|
|
2534
|
+
const rowIndex = section ? this.rowIndex(section.id) : 0;
|
|
2535
|
+
const row = section?.items[rowIndex] ?? null;
|
|
2536
|
+
return {
|
|
2537
|
+
lane: this.level,
|
|
2538
|
+
depth: this.depth(),
|
|
2539
|
+
section: section?.id ?? this.selectedSectionId,
|
|
2540
|
+
rowIndex,
|
|
2541
|
+
rowId: row?.id ?? null,
|
|
2542
|
+
submenuOpen: this.submenuComponent !== null,
|
|
2543
|
+
filter: this.filterQuery,
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
setSelection(sectionId: SettingsSectionId, rowIndex: number, lane: SettingsCenterLane = "rows"): void {
|
|
2548
|
+
if (this.sections().some((section) => section.id === sectionId)) this.selectedSectionId = sectionId;
|
|
2549
|
+
const section = this.currentSection();
|
|
2550
|
+
if (section) this.setRowIndex(section, this.selectableRowIndex(section, rowIndex));
|
|
2551
|
+
this.level = lane;
|
|
2552
|
+
this.submenuComponent = null;
|
|
2553
|
+
this.pendingValue = null;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
refreshItems(): void {
|
|
2557
|
+
this.normalizeSelection();
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
render(width: number): string[] {
|
|
2561
|
+
const bodyHeight = Math.max(1, this.options.getBodyHeight());
|
|
2562
|
+
this.normalizeSelection();
|
|
2563
|
+
const lines =
|
|
2564
|
+
width < WIDE_LAYOUT_MIN_WIDTH
|
|
2565
|
+
? this.renderNarrow(width, bodyHeight)
|
|
2566
|
+
: width >= ULTRAWIDE_LAYOUT_MIN_WIDTH
|
|
2567
|
+
? this.renderUltraWide(width, bodyHeight)
|
|
2568
|
+
: this.renderWide(width, bodyHeight);
|
|
2569
|
+
return fixedLines(lines, width, bodyHeight);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
/**
|
|
2573
|
+
* Settings is component-owned for Esc: the application router forwards it
|
|
2574
|
+
* here instead of closing the overlay, so one press moves up exactly one
|
|
2575
|
+
* level. Every physical encoding (raw, Kitty CSI-u, modifyOtherKeys) is
|
|
2576
|
+
* recognized before any delegation, which is what lets the parent pop a
|
|
2577
|
+
* submenu whose own cancel binding has been customized. Key releases are not
|
|
2578
|
+
* presses and do nothing.
|
|
2579
|
+
*/
|
|
2580
|
+
handleInput(data: string): void {
|
|
2581
|
+
if (isKeyRelease(data)) return;
|
|
2582
|
+
if (matchesKey(data, "escape")) {
|
|
2583
|
+
this.back();
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2586
|
+
if (this.filterDraft !== null) {
|
|
2587
|
+
this.handleFilterKey(data);
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
if (this.submenuComponent) {
|
|
2591
|
+
this.submenuComponent.handleInput?.(data);
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
const kb = getKeybindings();
|
|
2595
|
+
if (data === "/") {
|
|
2596
|
+
this.filterDraft = this.filterQuery;
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
if (matchesKey(data, "tab")) {
|
|
2600
|
+
this.toggleLevel();
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
if (matchesKey(data, "left")) {
|
|
2604
|
+
this.level = "sections";
|
|
2605
|
+
this.pendingValue = null;
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
if (matchesKey(data, "right")) {
|
|
2609
|
+
this.level = "rows";
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
if (kb.matches(data, "tui.select.up") || data === "k") {
|
|
2613
|
+
this.moveSelection(-1);
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
if (kb.matches(data, "tui.select.down") || data === "j") {
|
|
2617
|
+
this.moveSelection(1);
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
if (data === " " && this.level === "rows") {
|
|
2621
|
+
this.cyclePreview();
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
if (kb.matches(data, "tui.select.confirm") || matchesKey(data, "enter")) {
|
|
2625
|
+
if (this.level === "sections") {
|
|
2626
|
+
this.level = "rows";
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
this.activateSelectedItem();
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
if (kb.matches(data, "tui.select.cancel")) this.back();
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
invalidate(): void {
|
|
2636
|
+
this.submenuComponent?.invalidate?.();
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
/**
|
|
2640
|
+
* One level up, at every width: cancel the filter editor, then the open
|
|
2641
|
+
* submenu, then the rows page, and only from the section list does Esc close
|
|
2642
|
+
* Settings. A pending Space preview is part of the row context, so leaving
|
|
2643
|
+
* rows discards it rather than costing an extra press.
|
|
2644
|
+
*/
|
|
2645
|
+
private back(): void {
|
|
2646
|
+
if (this.filterDraft !== null) {
|
|
2647
|
+
this.filterDraft = null;
|
|
2648
|
+
this.options.requestRender?.();
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
if (this.submenuComponent) {
|
|
2652
|
+
this.submenuComponent = null;
|
|
2653
|
+
this.pendingValue = null;
|
|
2654
|
+
this.options.requestRender?.();
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
if (this.level === "rows" && this.sections().length > 0) {
|
|
2658
|
+
this.level = "sections";
|
|
2659
|
+
this.pendingValue = null;
|
|
2660
|
+
this.options.requestRender?.();
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
this.options.onCancel();
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
private depth(): SettingsNavigationDepth {
|
|
2667
|
+
return this.submenuComponent ? "detail" : this.level;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
private handleFilterKey(data: string): void {
|
|
2671
|
+
const draft = this.filterDraft ?? "";
|
|
2672
|
+
const kb = getKeybindings();
|
|
2673
|
+
if (kb.matches(data, "tui.select.confirm") || matchesKey(data, "enter")) {
|
|
2674
|
+
this.filterQuery = draft.trim();
|
|
2675
|
+
this.filterDraft = null;
|
|
2676
|
+
this.normalizeSelection();
|
|
2677
|
+
this.options.requestRender?.();
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
if (data === "\x7f" || data === "\b") {
|
|
2681
|
+
this.filterDraft = draft.slice(0, -1);
|
|
2682
|
+
this.normalizeSelection();
|
|
2683
|
+
this.options.requestRender?.();
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
if (isPrintableInput(data)) {
|
|
2687
|
+
this.filterDraft = draft + data;
|
|
2688
|
+
this.normalizeSelection();
|
|
2689
|
+
this.options.requestRender?.();
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
/** The query the catalog is narrowed by right now: the live draft while the
|
|
2694
|
+
* filter editor is open, the committed query otherwise. */
|
|
2695
|
+
private effectiveFilterQuery(): string {
|
|
2696
|
+
return (this.filterDraft ?? this.filterQuery).trim();
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
private matchesFilter(item: SettingsCenterItem): boolean {
|
|
2700
|
+
const query = this.effectiveFilterQuery().toLowerCase();
|
|
2701
|
+
if (query.length === 0) return true;
|
|
2702
|
+
return (
|
|
2703
|
+
item.label.toLowerCase().includes(query) ||
|
|
2704
|
+
item.configPath.toLowerCase().includes(query) ||
|
|
2705
|
+
item.description.toLowerCase().includes(query)
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
/**
|
|
2710
|
+
* The catalog the operator can currently reach. Group headers survive only as
|
|
2711
|
+
* context for a matching row beneath them, never as results or stops, and a
|
|
2712
|
+
* section with no matching row disappears from the list entirely.
|
|
2713
|
+
*/
|
|
2714
|
+
private sections(): SettingsCenterSection[] {
|
|
2715
|
+
const all = buildSettingsSections(this.items);
|
|
2716
|
+
if (this.effectiveFilterQuery().length === 0) return all;
|
|
2717
|
+
return all
|
|
2718
|
+
.map((section) => ({ ...section, items: this.filterSectionItems(section.items) }))
|
|
2719
|
+
.filter((section) => section.items.some((item) => this.isSelectableRow(item)));
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
private filterSectionItems(items: readonly SettingsCenterItem[]): SettingsCenterItem[] {
|
|
2723
|
+
const kept: SettingsCenterItem[] = [];
|
|
2724
|
+
let pendingHeader: SettingsCenterItem | null = null;
|
|
2725
|
+
for (const item of items) {
|
|
2726
|
+
if (!this.isSelectableRow(item)) {
|
|
2727
|
+
pendingHeader = item;
|
|
2728
|
+
continue;
|
|
2729
|
+
}
|
|
2730
|
+
if (!this.matchesFilter(item)) continue;
|
|
2731
|
+
if (pendingHeader) {
|
|
2732
|
+
kept.push(pendingHeader);
|
|
2733
|
+
pendingHeader = null;
|
|
2734
|
+
}
|
|
2735
|
+
kept.push(item);
|
|
2736
|
+
}
|
|
2737
|
+
return kept;
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
private currentSection(): SettingsCenterSection | null {
|
|
2741
|
+
const sections = this.sections();
|
|
2742
|
+
return sections.find((section) => section.id === this.selectedSectionId) ?? sections[0] ?? null;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
private rowIndex(sectionId: SettingsSectionId): number {
|
|
2746
|
+
const section = this.sections().find((entry) => entry.id === sectionId);
|
|
2747
|
+
if (!section) return 0;
|
|
2748
|
+
const rowId = this.rowIdBySection.get(sectionId);
|
|
2749
|
+
const identityIndex = rowId ? section.items.findIndex((item) => item.id === rowId) : -1;
|
|
2750
|
+
if (identityIndex >= 0 && this.isSelectableRow(section.items[identityIndex])) return identityIndex;
|
|
2751
|
+
return this.selectableRowIndex(section, this.rowIndexBySection.get(sectionId) ?? 0);
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
private isSelectableRow(item: SettingsCenterItem | undefined): boolean {
|
|
2755
|
+
return item !== undefined && item.presentationKind !== "group-header";
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
private selectableRowIndex(section: SettingsCenterSection, rowIndex: number): number {
|
|
2759
|
+
if (section.items.length === 0) return 0;
|
|
2760
|
+
const clamped = Math.max(0, Math.min(rowIndex, section.items.length - 1));
|
|
2761
|
+
if (this.isSelectableRow(section.items[clamped])) return clamped;
|
|
2762
|
+
for (let distance = 1; distance < section.items.length; distance += 1) {
|
|
2763
|
+
const after = clamped + distance;
|
|
2764
|
+
if (this.isSelectableRow(section.items[after])) return after;
|
|
2765
|
+
const before = clamped - distance;
|
|
2766
|
+
if (this.isSelectableRow(section.items[before])) return before;
|
|
2767
|
+
}
|
|
2768
|
+
return 0;
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
private setRowIndex(section: SettingsCenterSection, rowIndex: number): void {
|
|
2772
|
+
const next = this.selectableRowIndex(section, rowIndex);
|
|
2773
|
+
this.rowIndexBySection.set(section.id, next);
|
|
2774
|
+
const item = section.items[next];
|
|
2775
|
+
if (item && this.isSelectableRow(item)) this.rowIdBySection.set(section.id, item.id);
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
private normalizeSelection(): void {
|
|
2779
|
+
const sections = this.sections();
|
|
2780
|
+
// A committed filter that hides everything leaves nowhere to drill into, so
|
|
2781
|
+
// the stack sits at its top level and one Esc closes Settings from the
|
|
2782
|
+
// empty state. A live draft narrows per keystroke and may pass through the
|
|
2783
|
+
// empty state on its way to a match, so it must not disturb the level:
|
|
2784
|
+
// Esc's first press cancels the draft and lands back where editing began.
|
|
2785
|
+
if (sections.length === 0) {
|
|
2786
|
+
if (this.filterDraft === null) this.level = "sections";
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
if (!sections.some((section) => section.id === this.selectedSectionId)) {
|
|
2790
|
+
this.selectedSectionId = sections[0]?.id ?? this.selectedSectionId;
|
|
2791
|
+
}
|
|
2792
|
+
for (const section of sections) {
|
|
2793
|
+
this.setRowIndex(section, this.rowIndex(section.id));
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
private selectedItem(): SettingsCenterItem | null {
|
|
2798
|
+
const section = this.currentSection();
|
|
2799
|
+
if (!section) return null;
|
|
2800
|
+
return section.items[this.rowIndex(section.id)] ?? null;
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
private toggleLevel(): void {
|
|
2804
|
+
this.level = this.level === "sections" ? "rows" : "sections";
|
|
2805
|
+
this.pendingValue = null;
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
private moveSelection(delta: -1 | 1): void {
|
|
2809
|
+
this.pendingValue = null;
|
|
2810
|
+
if (this.level === "sections") {
|
|
2811
|
+
this.moveSection(delta);
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
// Rows are the section's own rows at every width. Flattening the catalog
|
|
2815
|
+
// on narrow terminals meant an operator scrolling Retry fell into Terminal
|
|
2816
|
+
// with nothing in the frame saying they had left the section they opened.
|
|
2817
|
+
const section = this.currentSection();
|
|
2818
|
+
if (!section) return;
|
|
2819
|
+
const selectable = section.items
|
|
2820
|
+
.map((item, index) => (this.isSelectableRow(item) ? index : -1))
|
|
2821
|
+
.filter((index) => index >= 0);
|
|
2822
|
+
if (selectable.length === 0) return;
|
|
2823
|
+
const current = selectable.indexOf(this.rowIndex(section.id));
|
|
2824
|
+
const next = selectable[(Math.max(0, current) + delta + selectable.length) % selectable.length];
|
|
2825
|
+
if (next !== undefined) this.setRowIndex(section, next);
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
private moveSection(delta: -1 | 1): void {
|
|
2829
|
+
const sections = this.sections();
|
|
2830
|
+
if (sections.length === 0) return;
|
|
2831
|
+
const current = Math.max(
|
|
2832
|
+
0,
|
|
2833
|
+
sections.findIndex((section) => section.id === this.selectedSectionId),
|
|
2834
|
+
);
|
|
2835
|
+
const next = sections[(current + delta + sections.length) % sections.length];
|
|
2836
|
+
if (next) this.selectedSectionId = next.id;
|
|
2837
|
+
this.normalizeSelection();
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
/** Space cycles a local preview of an enum/bool row without committing. */
|
|
2841
|
+
private cyclePreview(): void {
|
|
2842
|
+
const item = this.selectedItem();
|
|
2843
|
+
if (!item || item.readOnly || !item.values || item.values.length === 0) return;
|
|
2844
|
+
const base = this.pendingValue ?? item.currentValue;
|
|
2845
|
+
const currentIndex = item.values.indexOf(base);
|
|
2846
|
+
const nextIndex = (currentIndex + 1) % item.values.length;
|
|
2847
|
+
this.pendingValue = item.values[nextIndex] ?? base;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
private activateSelectedItem(): void {
|
|
2851
|
+
const item = this.selectedItem();
|
|
2852
|
+
if (!item || item.readOnly) return;
|
|
2853
|
+
if (item.submenu) {
|
|
2854
|
+
this.submenuComponent = item.submenu(item.currentValue, (selectedValue) => {
|
|
2855
|
+
this.submenuComponent = null;
|
|
2856
|
+
if (selectedValue !== undefined) this.prepareScopeConfirm(item, selectedValue);
|
|
2857
|
+
this.options.requestRender?.();
|
|
2858
|
+
});
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
if (item.values && item.values.length > 0) {
|
|
2862
|
+
const selectedValue = this.pendingValue ?? item.currentValue;
|
|
2863
|
+
const choices = item.values.map((value) => ({ value, label: value }));
|
|
2864
|
+
const list = new SettingsSelectList(choices, Math.min(10, choices.length), DEFAULT_SELECT_THEME);
|
|
2865
|
+
list.setSelectedIndex(Math.max(0, item.values.indexOf(selectedValue)));
|
|
2866
|
+
list.onSelect = (choice) => this.prepareScopeConfirm(item, choice.value);
|
|
2867
|
+
list.onCancel = () => {
|
|
2868
|
+
this.submenuComponent = null;
|
|
2869
|
+
this.options.requestRender?.();
|
|
2870
|
+
};
|
|
2871
|
+
this.submenuComponent = new SubmenuWrapper(
|
|
2872
|
+
`Select ${item.label}`,
|
|
2873
|
+
list,
|
|
2874
|
+
buildHint([{ key: "Enter", verb: "choose" }], "back"),
|
|
2875
|
+
);
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
private prepareScopeConfirm(item: SettingsCenterItem, value: string): void {
|
|
2880
|
+
const plan = this.options.prepareChange(item, value);
|
|
2881
|
+
if (!plan) {
|
|
2882
|
+
this.submenuComponent = null;
|
|
2883
|
+
this.pendingValue = null;
|
|
2884
|
+
this.options.requestRender?.();
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
this.openScopeConfirm(plan);
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
/** Hold one immutable plan while the operator chooses its destination. */
|
|
2891
|
+
private openScopeConfirm(plan: SettingsChangePlan): void {
|
|
2892
|
+
const options = plan.sessionCapable
|
|
2893
|
+
? [
|
|
2894
|
+
{ value: "session", label: "Apply this session" },
|
|
2895
|
+
{ value: "global", label: "Apply and save globally" },
|
|
2896
|
+
{ value: "cancel", label: "Cancel" },
|
|
2897
|
+
]
|
|
2898
|
+
: [
|
|
2899
|
+
{ value: "global", label: "Apply and save globally" },
|
|
2900
|
+
{ value: "cancel", label: "Cancel" },
|
|
2901
|
+
];
|
|
2902
|
+
const list = new SettingsSelectList(options, options.length, DEFAULT_SELECT_THEME);
|
|
2903
|
+
const finish = (chosen: "session" | "global" | "cancel"): void => {
|
|
2904
|
+
if (chosen === "session" || chosen === "global") this.options.onApply(plan, chosen);
|
|
2905
|
+
this.submenuComponent = null;
|
|
2906
|
+
this.pendingValue = null;
|
|
2907
|
+
this.options.requestRender?.();
|
|
2908
|
+
};
|
|
2909
|
+
list.onSelect = (opt) => finish(opt.value as "session" | "global" | "cancel");
|
|
2910
|
+
list.onCancel = () => finish("cancel");
|
|
2911
|
+
const title = (width: number): string => formatScopeConfirmTitle(plan, width);
|
|
2912
|
+
const affectedBindings = plan.leaves
|
|
2913
|
+
.filter((leaf) => leaf.path.startsWith("workers.agentBindings."))
|
|
2914
|
+
.map((leaf) => leaf.path.slice("workers.agentBindings.".length));
|
|
2915
|
+
const bindingPreflight =
|
|
2916
|
+
plan.selectedValue === REMOVE_PROFILE_CHOICE
|
|
2917
|
+
? `Affected agent routes: ${affectedBindings.length > 0 ? affectedBindings.join(", ") : "none"} · `
|
|
2918
|
+
: "";
|
|
2919
|
+
const targetRemovalPreflight = (() => {
|
|
2920
|
+
if (!plan.rowId.startsWith("targets.") || plan.selectedValue !== "remove") return "";
|
|
2921
|
+
const paths = (prefix: string): string[] =>
|
|
2922
|
+
plan.leaves.filter((leaf) => leaf.path.startsWith(prefix)).map((leaf) => leaf.path);
|
|
2923
|
+
const profiles = paths("workers.profiles.").map((path) => path.slice("workers.profiles.".length));
|
|
2924
|
+
const describe = (label: string, affected: readonly string[]): string =>
|
|
2925
|
+
`Affected ${label}: ${affected.length > 0 ? affected.join(", ") : "none"}`;
|
|
2926
|
+
return `${describe("chat route", paths("orchestrator."))} · ${describe("fleet route", paths("workers.default."))} · ${describe("memory route", paths("background."))} · ${describe("profiles", profiles)} · `;
|
|
2927
|
+
})();
|
|
2928
|
+
const note = `${bindingPreflight}${targetRemovalPreflight}Affects ${plan.leaves.map((leaf) => leaf.path).join(", ")} · ${plan.impact}`;
|
|
2929
|
+
this.submenuComponent = new SubmenuWrapper(title, list, buildHint([{ key: "Enter", verb: "choose" }], "back"), note);
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
private displayValueFor(item: SettingsCenterItem, selected: boolean): { value: string; pending: boolean } {
|
|
2933
|
+
if (selected && this.pendingValue !== null && this.pendingValue !== item.currentValue) {
|
|
2934
|
+
return { value: this.pendingValue, pending: true };
|
|
2935
|
+
}
|
|
2936
|
+
return { value: item.currentValue, pending: false };
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
/**
|
|
2940
|
+
* Section 2.6's ultrawide row: categories left, settings center, and the live
|
|
2941
|
+
* description as its own right column.
|
|
2942
|
+
*
|
|
2943
|
+
* Below this width the description is a footer under both lanes, which is the
|
|
2944
|
+
* only place it fits. At 120 columns that footer was spending five or six rows
|
|
2945
|
+
* of height on prose while 40-odd columns sat empty to the right of the
|
|
2946
|
+
* settings rows, so the panel scrolled a list it had the room to show. The
|
|
2947
|
+
* description lane is a readout, not a third focus target: Tab still moves
|
|
2948
|
+
* between sections and rows.
|
|
2949
|
+
*/
|
|
2950
|
+
private renderUltraWide(width: number, bodyHeight: number): string[] {
|
|
2951
|
+
const head = this.filterEditorLines(width);
|
|
2952
|
+
const bodyRows = Math.max(1, bodyHeight - head.length);
|
|
2953
|
+
if (this.sections().length === 0) return [...head, ...this.emptyFilterLines(width, bodyRows)];
|
|
2954
|
+
const theme = clioTheme();
|
|
2955
|
+
const separator = barSep(theme);
|
|
2956
|
+
const separatorWidth = visibleWidth(" │ ");
|
|
2957
|
+
const leftWidth = Math.min(SECTION_LANE_WIDTH, Math.max(16, Math.floor(width * 0.28)));
|
|
2958
|
+
const left = this.renderSectionLane(leftWidth, bodyRows);
|
|
2959
|
+
// A detail page owns the work area: the description column would be a second
|
|
2960
|
+
// readout beside a submenu that already carries its own title and note.
|
|
2961
|
+
if (this.submenuComponent) {
|
|
2962
|
+
const workWidth = Math.max(1, width - leftWidth - separatorWidth);
|
|
2963
|
+
const work = this.renderRightLane(workWidth, bodyRows);
|
|
2964
|
+
return [
|
|
2965
|
+
...head,
|
|
2966
|
+
...Array.from(
|
|
2967
|
+
{ length: bodyRows },
|
|
2968
|
+
(_, index) =>
|
|
2969
|
+
`${padAnsi(left[index] ?? "", leftWidth, ELLIPSIS)}${separator}${padAnsi(work[index] ?? "", workWidth, ELLIPSIS)}`,
|
|
2970
|
+
),
|
|
2971
|
+
];
|
|
2972
|
+
}
|
|
2973
|
+
const detailWidth = Math.max(28, Math.min(44, Math.floor(width * 0.3)));
|
|
2974
|
+
const centerWidth = Math.max(1, width - leftWidth - detailWidth - separatorWidth * 2);
|
|
2975
|
+
const center = this.renderRightLane(centerWidth, bodyRows);
|
|
2976
|
+
const right = this.renderDetailLane(detailWidth, bodyRows);
|
|
2977
|
+
return [
|
|
2978
|
+
...head,
|
|
2979
|
+
...Array.from({ length: bodyRows }, (_, index) =>
|
|
2980
|
+
[
|
|
2981
|
+
padAnsi(left[index] ?? "", leftWidth, ELLIPSIS),
|
|
2982
|
+
padAnsi(center[index] ?? "", centerWidth, ELLIPSIS),
|
|
2983
|
+
padAnsi(right[index] ?? "", detailWidth, ELLIPSIS),
|
|
2984
|
+
].join(separator),
|
|
2985
|
+
),
|
|
2986
|
+
];
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
/** The one-line filter editor, shown at every width while it owns input. */
|
|
2990
|
+
private filterEditorLines(width: number): string[] {
|
|
2991
|
+
if (this.filterDraft === null) return [];
|
|
2992
|
+
const theme = clioTheme();
|
|
2993
|
+
return [truncateToWidth(theme.fg("accent", `Filter settings: ${this.filterDraft}_`), width, ELLIPSIS, true)];
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
private emptyFilterLines(width: number, height: number): string[] {
|
|
2997
|
+
const theme = clioTheme();
|
|
2998
|
+
return fixedLines(
|
|
2999
|
+
[
|
|
3000
|
+
theme.fg("muted", truncateToWidth(`No settings match “${this.effectiveFilterQuery()}”`, width, ELLIPSIS, true)),
|
|
3001
|
+
theme.fg("dim", truncateToWidth("/ edit filter · empty Enter clears", width, ELLIPSIS, true)),
|
|
3002
|
+
],
|
|
3003
|
+
width,
|
|
3004
|
+
height,
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
/**
|
|
3009
|
+
* The right column: what the selected row is, what it means, and where a
|
|
3010
|
+
* change to it lands. Same content the footer carries below 120 columns,
|
|
3011
|
+
* wrapped to a narrow column instead of a wide strip, with the scope note
|
|
3012
|
+
* pinned to the bottom so it survives a long explanation.
|
|
3013
|
+
*/
|
|
3014
|
+
private renderDetailLane(width: number, height: number): string[] {
|
|
3015
|
+
const theme = clioTheme();
|
|
3016
|
+
const section = this.currentSection();
|
|
3017
|
+
const item = this.selectedItem();
|
|
3018
|
+
if (!section) return fixedLines([], width, height);
|
|
3019
|
+
if (this.level === "sections" || !item) {
|
|
3020
|
+
const rows = [
|
|
3021
|
+
screenTitle(theme, section.label),
|
|
3022
|
+
"",
|
|
3023
|
+
...wrapTextWithAnsi(theme.fg("muted", SETTINGS_SECTION_DESCRIPTIONS[section.id]), width),
|
|
3024
|
+
"",
|
|
3025
|
+
theme.fg("dim", "Tab or → to edit its settings"),
|
|
3026
|
+
];
|
|
3027
|
+
return fixedLines(rows, width, height);
|
|
3028
|
+
}
|
|
3029
|
+
const body: string[] = [
|
|
3030
|
+
screenTitle(theme, item.label),
|
|
3031
|
+
"",
|
|
3032
|
+
...wrapTextWithAnsi(theme.fg("muted", item.description), width),
|
|
3033
|
+
];
|
|
3034
|
+
if (item.help) body.push("", ...wrapTextWithAnsi(theme.fg("dim", item.help), width));
|
|
3035
|
+
const detail = this.footerDetail(item, theme);
|
|
3036
|
+
if (detail) body.push("", ...wrapTextWithAnsi(detail, width));
|
|
3037
|
+
const note = wrapTextWithAnsi(theme.fg("dim", this.footerScopeNote(item)), width);
|
|
3038
|
+
// The note is the answer to "where does this land", so it keeps its rows
|
|
3039
|
+
// and the explanation above it is what gets cut, with a marker.
|
|
3040
|
+
const bodyBudget = Math.max(0, height - note.length - 1);
|
|
3041
|
+
const kept = body.slice(0, bodyBudget);
|
|
3042
|
+
// A cut that lands on one of the blank spacer rows would otherwise mark it,
|
|
3043
|
+
// leaving a lone `…` under the last full sentence. The marker belongs on the
|
|
3044
|
+
// last line that actually carries text.
|
|
3045
|
+
while (kept.length > 0 && (kept.at(-1) ?? "").trim().length === 0) kept.pop();
|
|
3046
|
+
const last = kept.at(-1);
|
|
3047
|
+
const marked = body.length > kept.length && last !== undefined ? [...kept.slice(0, -1), `${last}${ELLIPSIS}`] : kept;
|
|
3048
|
+
const filler = Array.from({ length: Math.max(0, height - marked.length - note.length) }, () => "");
|
|
3049
|
+
return fixedLines([...marked, ...filler, ...note], width, height);
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
private renderWide(width: number, bodyHeight: number): string[] {
|
|
3053
|
+
const head = this.filterEditorLines(width);
|
|
3054
|
+
const available = Math.max(1, bodyHeight - head.length);
|
|
3055
|
+
if (this.sections().length === 0) return [...head, ...this.emptyFilterLines(width, available)];
|
|
3056
|
+
// A detail page owns the work area, and the footer describes the row the
|
|
3057
|
+
// operator has already left, so it is suppressed while a submenu is open.
|
|
3058
|
+
const footer = this.submenuComponent ? [] : this.renderFooter(width, this.footerBudget(available));
|
|
3059
|
+
const contentHeight = Math.max(1, available - footer.length);
|
|
3060
|
+
const leftWidth = Math.min(SECTION_LANE_WIDTH, Math.max(16, Math.floor(width * 0.28)));
|
|
3061
|
+
const separator = barSep(clioTheme());
|
|
3062
|
+
const separatorWidth = visibleWidth(" │ ");
|
|
3063
|
+
const rightWidth = Math.max(1, width - leftWidth - separatorWidth);
|
|
3064
|
+
const left = this.renderSectionLane(leftWidth, contentHeight);
|
|
3065
|
+
const right = this.renderRightLane(rightWidth, contentHeight);
|
|
3066
|
+
const body = Array.from(
|
|
3067
|
+
{ length: contentHeight },
|
|
3068
|
+
(_, index) =>
|
|
3069
|
+
`${padAnsi(left[index] ?? "", leftWidth, ELLIPSIS)}${separator}${padAnsi(right[index] ?? "", rightWidth, ELLIPSIS)}`,
|
|
3070
|
+
);
|
|
3071
|
+
return [...head, ...body, ...footer];
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
/**
|
|
3075
|
+
* The footer is help, and the list is the work. A fixed six-line ceiling spent
|
|
3076
|
+
* a third of a 20-row body on prose, so the budget is what the body can spare
|
|
3077
|
+
* once the list keeps six rows.
|
|
3078
|
+
*/
|
|
3079
|
+
private footerBudget(bodyHeight: number): number {
|
|
3080
|
+
return Math.min(4, Math.max(0, bodyHeight - 6));
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
private renderSectionLane(width: number, height: number): string[] {
|
|
3084
|
+
const theme = clioTheme();
|
|
3085
|
+
const rows = [{ line: theme.fg("dim", "Sections"), sectionId: null as SettingsSectionId | null }];
|
|
3086
|
+
rows.push(...this.sectionCatalogRows());
|
|
3087
|
+
const selectedLine = Math.max(
|
|
3088
|
+
0,
|
|
3089
|
+
rows.findIndex((row) => row.sectionId === this.selectedSectionId),
|
|
3090
|
+
);
|
|
3091
|
+
const [start, end] = scrollWindow(rows.length, selectedLine, height);
|
|
3092
|
+
return fixedLines(
|
|
3093
|
+
rows.slice(start, end).map((row) => row.line),
|
|
3094
|
+
width,
|
|
3095
|
+
height,
|
|
3096
|
+
);
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
/**
|
|
3100
|
+
* The section catalog, shared by the wide lane and the narrow sections page.
|
|
3101
|
+
* Group tags stay visual separators: they carry no cursor and are never a stop.
|
|
3102
|
+
*/
|
|
3103
|
+
private sectionCatalogRows(): Array<{ line: string; sectionId: SettingsSectionId | null }> {
|
|
3104
|
+
const theme = clioTheme();
|
|
3105
|
+
const filtering = this.effectiveFilterQuery().length > 0;
|
|
3106
|
+
const rows: Array<{ line: string; sectionId: SettingsSectionId | null }> = [];
|
|
3107
|
+
let previousGroup: string | null = null;
|
|
3108
|
+
for (const section of this.sections()) {
|
|
3109
|
+
const group = SETTINGS_SECTIONS.find((entry) => entry.id === section.id)?.group;
|
|
3110
|
+
if (group && group !== previousGroup) {
|
|
3111
|
+
rows.push({ line: theme.style("accentDeep", group, { bold: true }), sectionId: null });
|
|
3112
|
+
previousGroup = group;
|
|
3113
|
+
}
|
|
3114
|
+
const selected = section.id === this.selectedSectionId;
|
|
3115
|
+
const cursor = selected && this.level === "sections" ? theme.fg("accent", `${GLYPH.cursor} `) : " ";
|
|
3116
|
+
const matchCount = section.items.filter((item) => this.isSelectableRow(item)).length;
|
|
3117
|
+
const modifiedCount = section.items.filter(
|
|
3118
|
+
(item) => !item.readOnly && item.defaultValue !== undefined && item.currentValue !== item.defaultValue,
|
|
3119
|
+
).length;
|
|
3120
|
+
const badge = filtering
|
|
3121
|
+
? theme.fg("accent", ` ${matchCount}`)
|
|
3122
|
+
: modifiedCount > 0
|
|
3123
|
+
? theme.fg("accent", ` ${GLYPH.scoped}${modifiedCount}`)
|
|
3124
|
+
: "";
|
|
3125
|
+
const label = selected ? theme.style("accent", section.label, { bold: true }) : section.label;
|
|
3126
|
+
rows.push({ line: `${cursor}${label}${badge}`, sectionId: section.id });
|
|
3127
|
+
}
|
|
3128
|
+
return rows;
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
private renderRightLane(width: number, height: number): string[] {
|
|
3132
|
+
if (this.submenuComponent) {
|
|
3133
|
+
const lines = this.submenuComponent.render(width);
|
|
3134
|
+
return fixedLines(lines, width, height);
|
|
3135
|
+
}
|
|
3136
|
+
const theme = clioTheme();
|
|
3137
|
+
const section = this.currentSection();
|
|
3138
|
+
if (!section) return fixedLines([], width, height);
|
|
3139
|
+
const rowBudget = Math.max(0, height - 1);
|
|
3140
|
+
const selected = this.rowIndex(section.id);
|
|
3141
|
+
const [start, end] = scrollWindow(section.items.length, selected, rowBudget);
|
|
3142
|
+
const columns = rowColumns(section.items, width, 0);
|
|
3143
|
+
const rows = section.items.slice(start, end).map((item, offset) => {
|
|
3144
|
+
const isSelected = start + offset === selected && this.level === "rows";
|
|
3145
|
+
const display = this.displayValueFor(item, isSelected);
|
|
3146
|
+
return formatSettingRow(item, width, isSelected, columns, 0, display.value, display.pending);
|
|
3147
|
+
});
|
|
3148
|
+
return fixedLines([screenTitle(theme, section.label), ...rows], width, height);
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
/**
|
|
3152
|
+
* The narrow stack: one page at a time under a breadcrumb. The flattened
|
|
3153
|
+
* catalog it replaces rendered every section and every row into one scroll,
|
|
3154
|
+
* so a 40-column terminal showed six sections' worth of context and no way to
|
|
3155
|
+
* tell which one owned the cursor.
|
|
3156
|
+
*/
|
|
3157
|
+
private renderNarrow(width: number, bodyHeight: number): string[] {
|
|
3158
|
+
const head = [this.breadcrumbLine(width), ...this.filterEditorLines(width)];
|
|
3159
|
+
const available = Math.max(1, bodyHeight - head.length);
|
|
3160
|
+
if (this.sections().length === 0) return [...head, ...this.emptyFilterLines(width, available)];
|
|
3161
|
+
if (this.submenuComponent) {
|
|
3162
|
+
return [...head, ...fixedLines(this.submenuComponent.render(width), width, available)];
|
|
3163
|
+
}
|
|
3164
|
+
const inspector = this.narrowInspector(width, bodyHeight);
|
|
3165
|
+
const listHeight = Math.max(1, available - inspector.length);
|
|
3166
|
+
const list =
|
|
3167
|
+
this.level === "sections" ? this.renderSectionsPage(width, listHeight) : this.renderRowsPage(width, listHeight);
|
|
3168
|
+
return [...head, ...list, ...inspector];
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
private breadcrumbLine(width: number): string {
|
|
3172
|
+
const theme = clioTheme();
|
|
3173
|
+
const section = this.currentSection();
|
|
3174
|
+
const trail: string[] = ["Settings"];
|
|
3175
|
+
if (this.level === "sections" && !this.submenuComponent) trail.push("Sections");
|
|
3176
|
+
else if (section) trail.push(section.label);
|
|
3177
|
+
if (this.submenuComponent) {
|
|
3178
|
+
const item = this.selectedItem();
|
|
3179
|
+
if (item) trail.push(item.label);
|
|
3180
|
+
}
|
|
3181
|
+
const query = this.filterQuery.trim().length > 0 ? theme.fg("accent", ` /${this.filterQuery}`) : "";
|
|
3182
|
+
return truncateToWidth(`${screenTitle(theme, trail.join(" › "))}${query}`, width, ELLIPSIS, true);
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
/**
|
|
3186
|
+
* Prose is the first thing a short terminal can spare. Sixteen body rows keep
|
|
3187
|
+
* two inspector rows, ten keep one, and anything shorter gives every row to
|
|
3188
|
+
* the list; the description stays reachable at wider widths and through the
|
|
3189
|
+
* filter.
|
|
3190
|
+
*/
|
|
3191
|
+
private narrowInspector(width: number, bodyHeight: number): string[] {
|
|
3192
|
+
const budget = bodyHeight >= 16 ? 2 : bodyHeight >= 10 ? 1 : 0;
|
|
3193
|
+
if (budget === 0) return [];
|
|
3194
|
+
const theme = clioTheme();
|
|
3195
|
+
const section = this.currentSection();
|
|
3196
|
+
if (!section) return [];
|
|
3197
|
+
const item = this.level === "rows" ? this.selectedItem() : null;
|
|
3198
|
+
const text = item ? item.description : SETTINGS_SECTION_DESCRIPTIONS[section.id];
|
|
3199
|
+
const wrapped = wrapTextWithAnsi(theme.fg("muted", text), Math.max(1, width));
|
|
3200
|
+
const kept = wrapped.slice(0, budget);
|
|
3201
|
+
const last = kept.at(-1);
|
|
3202
|
+
return wrapped.length > kept.length && last !== undefined ? [...kept.slice(0, -1), `${last}${ELLIPSIS}`] : kept;
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
private renderSectionsPage(width: number, height: number): string[] {
|
|
3206
|
+
const rows = this.sectionCatalogRows();
|
|
3207
|
+
const selectedLine = Math.max(
|
|
3208
|
+
0,
|
|
3209
|
+
rows.findIndex((row) => row.sectionId === this.selectedSectionId),
|
|
3210
|
+
);
|
|
3211
|
+
const [start, end] = scrollWindow(rows.length, selectedLine, height);
|
|
3212
|
+
return fixedLines(
|
|
3213
|
+
rows.slice(start, end).map((row) => row.line),
|
|
3214
|
+
width,
|
|
3215
|
+
height,
|
|
3216
|
+
);
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
private renderRowsPage(width: number, height: number): string[] {
|
|
3220
|
+
const section = this.currentSection();
|
|
3221
|
+
if (!section) return fixedLines([], width, height);
|
|
3222
|
+
const selected = this.rowIndex(section.id);
|
|
3223
|
+
const [start, end] = scrollWindow(section.items.length, selected, height);
|
|
3224
|
+
const columns = rowColumns(section.items, width, 0);
|
|
3225
|
+
const rows = section.items.slice(start, end).map((item, offset) => {
|
|
3226
|
+
const isSelected = start + offset === selected;
|
|
3227
|
+
const display = this.displayValueFor(item, isSelected);
|
|
3228
|
+
return formatSettingRow(item, width, isSelected, columns, 0, display.value, display.pending);
|
|
3229
|
+
});
|
|
3230
|
+
return fixedLines(rows, width, height);
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
private renderFooter(width: number, maxFooterLines: number): string[] {
|
|
3234
|
+
const theme = clioTheme();
|
|
3235
|
+
if (maxFooterLines <= 0) return [];
|
|
3236
|
+
const safeWidth = Math.max(1, width);
|
|
3237
|
+
const separator = rule(theme, safeWidth);
|
|
3238
|
+
const sections = this.sections();
|
|
3239
|
+
const section = this.currentSection();
|
|
3240
|
+
const item = this.selectedItem();
|
|
3241
|
+
if (!section) return [];
|
|
3242
|
+
const position = sections.findIndex((entry) => entry.id === section.id) + 1;
|
|
3243
|
+
const query = this.filterQuery.trim().length > 0 ? ` ${theme.fg("accent", `/${this.filterQuery}`)}` : "";
|
|
3244
|
+
const positionText = `${theme.fg("dim", `section ${position}/${sections.length}`)}${query}`;
|
|
3245
|
+
|
|
3246
|
+
if (this.level === "sections") {
|
|
3247
|
+
const breadcrumb = `${screenTitle(theme, section.label)} ${theme.fg("dim", "·")} ${positionText}`;
|
|
3248
|
+
const body = wrapTextWithAnsi(theme.fg("muted", SETTINGS_SECTION_DESCRIPTIONS[section.id]), safeWidth);
|
|
3249
|
+
const note = theme.fg("dim", "Tab or → to edit its settings");
|
|
3250
|
+
return this.assembleFooter([separator, breadcrumb], body, note, maxFooterLines, safeWidth);
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
if (!item) {
|
|
3254
|
+
return this.assembleFooter([separator], [], theme.fg("muted", "No setting selected."), maxFooterLines, safeWidth);
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
const breadcrumb = `${screenTitle(theme, section.label)} ${theme.fg("dim", "›")} ${theme.style("accent", item.label, { bold: true })} ${theme.fg("dim", "·")} ${positionText}`;
|
|
3258
|
+
const contentLines: string[] = [];
|
|
3259
|
+
contentLines.push(...wrapTextWithAnsi(theme.fg("muted", item.description), safeWidth));
|
|
3260
|
+
if (item.help) contentLines.push(...wrapTextWithAnsi(theme.fg("dim", item.help), safeWidth));
|
|
3261
|
+
const detail = this.footerDetail(item, theme);
|
|
3262
|
+
if (detail) contentLines.push(...wrapTextWithAnsi(detail, safeWidth));
|
|
3263
|
+
const note = theme.fg("dim", truncateToWidth(this.footerScopeNote(item), safeWidth, ELLIPSIS, true));
|
|
3264
|
+
return this.assembleFooter([separator, breadcrumb], contentLines, note, maxFooterLines, safeWidth);
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
private footerDetail(item: SettingsCenterItem, theme: ReturnType<typeof clioTheme>): string {
|
|
3268
|
+
const parts: string[] = [theme.fg("dim", item.affordance)];
|
|
3269
|
+
if (!item.readOnly && item.defaultValue !== undefined) {
|
|
3270
|
+
const modified = item.currentValue !== item.defaultValue;
|
|
3271
|
+
parts.push(
|
|
3272
|
+
modified
|
|
3273
|
+
? theme.fg("accent", `${GLYPH.scoped} changed (default: ${item.defaultValue})`)
|
|
3274
|
+
: theme.fg("dim", `default: ${item.defaultValue}`),
|
|
3275
|
+
);
|
|
3276
|
+
}
|
|
3277
|
+
const valueMeaning = item.valueHelp?.[item.currentValue];
|
|
3278
|
+
if (!item.readOnly && valueMeaning) parts.push(theme.fg("muted", valueMeaning));
|
|
3279
|
+
const propagation = propagationFor(item.id);
|
|
3280
|
+
if (propagation) parts.push(theme.fg("dim", propagation));
|
|
3281
|
+
return parts.join(theme.fg("frame", " · "));
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
private footerScopeNote(item: SettingsCenterItem): string {
|
|
3285
|
+
if (item.id === "targets.add-cta") return "Run the command shown to open the accepted add wizard";
|
|
3286
|
+
if (item.submenu && (item.presentationKind === "status" || item.presentationKind === "action"))
|
|
3287
|
+
return "Enter opens actions · nothing changes until an action is confirmed";
|
|
3288
|
+
if (item.readOnly) return "Read-only here · managed on the surface above";
|
|
3289
|
+
if (item.scope === "restart") return "Saved to settings.yaml · restart Clio to apply";
|
|
3290
|
+
return "Enter chooses a value · then choose session, global, or cancel before anything changes";
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
/**
|
|
3294
|
+
* Lay out the footer so the breadcrumb (top) and the scope note (bottom)
|
|
3295
|
+
* always survive; the middle help fills whatever rows remain. This keeps the
|
|
3296
|
+
* "where does this land" guidance visible even on short terminals.
|
|
3297
|
+
*/
|
|
3298
|
+
private assembleFooter(
|
|
3299
|
+
top: readonly string[],
|
|
3300
|
+
middle: readonly string[],
|
|
3301
|
+
note: string,
|
|
3302
|
+
maxFooterLines: number,
|
|
3303
|
+
width: number,
|
|
3304
|
+
): string[] {
|
|
3305
|
+
const fit = (line: string): string => truncateToWidth(line, width, ELLIPSIS, true);
|
|
3306
|
+
let out: string[];
|
|
3307
|
+
if (maxFooterLines <= top.length) {
|
|
3308
|
+
out = top.slice(0, maxFooterLines).map(fit);
|
|
3309
|
+
} else {
|
|
3310
|
+
const middleBudget = Math.max(0, maxFooterLines - top.length - 1);
|
|
3311
|
+
// The explanation is wrapped, so a short terminal drops whole lines off
|
|
3312
|
+
// its end rather than cutting one. At 40 columns the autonomy help
|
|
3313
|
+
// stopped at "read-only observes; suggest" and read as the whole
|
|
3314
|
+
// sentence, so the last line it keeps says that it is not.
|
|
3315
|
+
const kept = middle.slice(0, middleBudget);
|
|
3316
|
+
const last = kept.at(-1);
|
|
3317
|
+
const marked =
|
|
3318
|
+
middle.length > kept.length && last !== undefined ? [...kept.slice(0, -1), `${last}${ELLIPSIS}`] : kept;
|
|
3319
|
+
out = [...top.map(fit), ...marked.map(fit), fit(note)];
|
|
3320
|
+
}
|
|
3321
|
+
while (out.length < maxFooterLines) out.push("");
|
|
3322
|
+
return out.slice(0, maxFooterLines);
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
export type SettingsNoticeLevel = "info" | "success" | "warning" | "error";
|
|
3327
|
+
|
|
3328
|
+
export interface OpenSettingsOverlayDeps {
|
|
3329
|
+
getSettings: () => Readonly<ClioSettings>;
|
|
3330
|
+
providers?: ProvidersContract;
|
|
3331
|
+
writeSettings: (next: ClioSettings) => void;
|
|
3332
|
+
/**
|
|
3333
|
+
* Scoped commit for a single edit. When present, the overlay routes session
|
|
3334
|
+
* and global saves through it; when absent it falls back to writeSettings
|
|
3335
|
+
* (every edit goes global, the legacy behavior).
|
|
3336
|
+
*/
|
|
3337
|
+
commitSetting?: (id: string, next: ClioSettings, scope: "session" | "global") => void;
|
|
3338
|
+
notice?: (level: SettingsNoticeLevel, text: string, key?: string) => void;
|
|
3339
|
+
onClose: () => void;
|
|
3340
|
+
/** Open focused on this section (deep link from `/settings <section>`). */
|
|
3341
|
+
section?: SettingsSectionId;
|
|
3342
|
+
/** Optional semantic row anchor for command deep links. */
|
|
3343
|
+
rowId?: SettingsCenterRowId;
|
|
3344
|
+
getFleetNodes?: BuildSettingItemsOptions["getFleetNodes"];
|
|
3345
|
+
connectTarget?: BuildSettingItemsOptions["connectTarget"];
|
|
3346
|
+
getInteropProposals?: BuildSettingItemsOptions["getInteropProposals"];
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
function formatSettingChangeNotice(id: string, value: string, scope: "session" | "global"): string {
|
|
3350
|
+
const scopedRefs = id === "scope" ? parseScopedModelSelection(value) : null;
|
|
3351
|
+
const displayValue = scopedRefs ? (scopedRefs.length > 0 ? scopedRefs.join(", ") : "(empty)") : value;
|
|
3352
|
+
return `${id} set to ${displayValue} (${scope === "global" ? "saved globally" : "this session"})`;
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
export interface SettingsOverlayHandle extends OverlayHandle {
|
|
3356
|
+
/**
|
|
3357
|
+
* Re-derive every row from the live effective settings. Called after each
|
|
3358
|
+
* committed edit and on config change events while the overlay is open, so
|
|
3359
|
+
* dependent rows never go stale.
|
|
3360
|
+
*/
|
|
3361
|
+
refreshRows(): void;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
function settingsBodyHeight(tui: TUI): number {
|
|
3365
|
+
return Math.max(1, tui.terminal.rows - SETTINGS_OVERLAY_MARGIN.top - SETTINGS_OVERLAY_MARGIN.bottom - 2);
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
export function openSettingsOverlay(tui: TUI, deps: OpenSettingsOverlayDeps): SettingsOverlayHandle {
|
|
3369
|
+
const targetOperations = new Map<string, { operation: "connect" | "probe"; token: object }>();
|
|
3370
|
+
const buildOptions: BuildSettingItemsOptions = {
|
|
3371
|
+
getSettings: deps.getSettings,
|
|
3372
|
+
requestRefresh: () => refreshRows(),
|
|
3373
|
+
getTargetOperation: (targetId) => {
|
|
3374
|
+
const keyboardOwningOperation = targetOperations.entries().next().value as
|
|
3375
|
+
| [string, { operation: "connect" | "probe"; token: object }]
|
|
3376
|
+
| undefined;
|
|
3377
|
+
return keyboardOwningOperation?.[0] === targetId ? keyboardOwningOperation[1].operation : null;
|
|
3378
|
+
},
|
|
3379
|
+
onTargetOperationChange: (targetId, operation, operationToken) => {
|
|
3380
|
+
if (operation) targetOperations.set(targetId, { operation, token: operationToken });
|
|
3381
|
+
else if (targetOperations.get(targetId)?.token === operationToken) targetOperations.delete(targetId);
|
|
3382
|
+
},
|
|
3383
|
+
};
|
|
3384
|
+
if (deps.providers) buildOptions.providers = deps.providers;
|
|
3385
|
+
if (deps.getFleetNodes) buildOptions.getFleetNodes = deps.getFleetNodes;
|
|
3386
|
+
if (deps.connectTarget) buildOptions.connectTarget = deps.connectTarget;
|
|
3387
|
+
if (deps.getInteropProposals) buildOptions.getInteropProposals = deps.getInteropProposals;
|
|
3388
|
+
const items = buildSettingItems(deps.getSettings(), buildOptions);
|
|
3389
|
+
const center = new SettingsCenter(items, {
|
|
3390
|
+
getBodyHeight: () => settingsBodyHeight(tui),
|
|
3391
|
+
prepareChange: (item, value) =>
|
|
3392
|
+
createSettingsChangePlan(deps.getSettings(), item, value, Boolean(deps.commitSetting)),
|
|
3393
|
+
onApply: (plan, scope) => {
|
|
3394
|
+
if (deps.commitSetting) {
|
|
3395
|
+
for (const leaf of plan.leaves) deps.commitSetting(leaf.path, plan.proposed as ClioSettings, scope);
|
|
3396
|
+
} else deps.writeSettings(plan.proposed as ClioSettings);
|
|
3397
|
+
deps.notice?.("success", formatSettingChangeNotice(plan.rowId, plan.selectedValue, scope), `settings:${plan.rowId}`);
|
|
3398
|
+
refreshRows();
|
|
3399
|
+
},
|
|
3400
|
+
onCancel: () => deps.onClose(),
|
|
3401
|
+
requestRender: () => tui.requestRender(),
|
|
3402
|
+
});
|
|
3403
|
+
if (deps.section) {
|
|
3404
|
+
const sectionItems = buildSettingsSections(items).find((section) => section.id === deps.section)?.items ?? [];
|
|
3405
|
+
const rowIndex = deps.rowId ? sectionItems.findIndex((item) => item.id === deps.rowId) : -1;
|
|
3406
|
+
center.setSelection(deps.section, rowIndex >= 0 ? rowIndex : 0);
|
|
3407
|
+
}
|
|
3408
|
+
const refreshRows = (): void => {
|
|
3409
|
+
refreshSettingItemsInPlace(items, buildSettingItems(deps.getSettings(), buildOptions));
|
|
3410
|
+
center.refreshItems();
|
|
3411
|
+
tui.requestRender();
|
|
3412
|
+
};
|
|
3413
|
+
// The engine composites an overlay across the columns its region covers, and
|
|
3414
|
+
// side margins shrink that region, so this margin is re-read every frame and
|
|
3415
|
+
// drops to zero on an ultra-narrow terminal rather than leaving transcript
|
|
3416
|
+
// columns beside a modal that owns the keyboard.
|
|
3417
|
+
const margin: { top: number; right: number; bottom: number; left: number } = { ...SETTINGS_OVERLAY_MARGIN };
|
|
3418
|
+
const handle = showClioOverlayFrame(tui, center, {
|
|
3419
|
+
anchor: "top-left",
|
|
3420
|
+
width: SETTINGS_OVERLAY_WIDTH,
|
|
3421
|
+
maxHeight: SETTINGS_OVERLAY_MAX_HEIGHT,
|
|
3422
|
+
margin,
|
|
3423
|
+
visible: (terminalWidth) => {
|
|
3424
|
+
const side = terminalWidth < ULTRA_NARROW_TERMINAL_WIDTH ? 0 : SETTINGS_OVERLAY_MARGIN.left;
|
|
3425
|
+
margin.left = side;
|
|
3426
|
+
margin.right = side;
|
|
3427
|
+
return true;
|
|
3428
|
+
},
|
|
3429
|
+
title: "Settings",
|
|
3430
|
+
footerHint: (innerWidth) =>
|
|
3431
|
+
innerWidth < WIDE_LAYOUT_MIN_WIDTH
|
|
3432
|
+
? buildHint(
|
|
3433
|
+
[
|
|
3434
|
+
{ key: "↑↓", verb: "move" },
|
|
3435
|
+
{ key: "Enter", verb: "open" },
|
|
3436
|
+
{ key: "/", verb: "filter" },
|
|
3437
|
+
],
|
|
3438
|
+
"back",
|
|
3439
|
+
)
|
|
3440
|
+
: buildHint(
|
|
3441
|
+
[
|
|
3442
|
+
{ key: "Tab", verb: "switch level" },
|
|
3443
|
+
{ key: "Space", verb: "preview" },
|
|
3444
|
+
{ key: "Enter", verb: "open" },
|
|
3445
|
+
{ key: "/", verb: "filter" },
|
|
3446
|
+
],
|
|
3447
|
+
"back",
|
|
3448
|
+
),
|
|
3449
|
+
});
|
|
3450
|
+
return Object.assign(handle, { refreshRows });
|
|
3451
|
+
}
|