@gajae-code/coding-agent 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/dist/types/async/job-manager.d.ts +4 -1
  3. package/dist/types/autoresearch/dashboard.d.ts +29 -4
  4. package/dist/types/autoresearch/data-context.d.ts +25 -0
  5. package/dist/types/autoresearch/git.d.ts +65 -7
  6. package/dist/types/autoresearch/harness.d.ts +58 -0
  7. package/dist/types/autoresearch/prompts.d.ts +51 -0
  8. package/dist/types/autoresearch/runs.d.ts +173 -0
  9. package/dist/types/autoresearch/types.d.ts +9 -139
  10. package/dist/types/capability/types.d.ts +20 -0
  11. package/dist/types/cli/accounts-cli.d.ts +1 -0
  12. package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
  13. package/dist/types/config/autorouting-contract.d.ts +146 -0
  14. package/dist/types/config/autorouting-generator.d.ts +40 -0
  15. package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
  16. package/dist/types/config/autorouting.d.ts +35 -0
  17. package/dist/types/config/model-discovery-manager.d.ts +8 -1
  18. package/dist/types/config/model-registry.d.ts +30 -1
  19. package/dist/types/config/provider-selection-policy.d.ts +23 -0
  20. package/dist/types/config/settings-schema.d.ts +61 -3
  21. package/dist/types/config/settings.d.ts +3 -0
  22. package/dist/types/coordinator/contract.d.ts +1 -1
  23. package/dist/types/coordinator-mcp/durability.d.ts +29 -0
  24. package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
  26. package/dist/types/coordinator-mcp/server.d.ts +14 -0
  27. package/dist/types/crash/index-store.d.ts +3 -0
  28. package/dist/types/crash/record-loader.d.ts +2 -0
  29. package/dist/types/crash/upstream/relay.d.ts +5 -3
  30. package/dist/types/cursor.d.ts +12 -0
  31. package/dist/types/defaults/gjc-defaults.d.ts +35 -1
  32. package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
  33. package/dist/types/discovery/helpers.d.ts +1 -1
  34. package/dist/types/edit/index.d.ts +2 -1
  35. package/dist/types/eval/py/executor.d.ts +2 -0
  36. package/dist/types/extensibility/extensions/loader.d.ts +32 -0
  37. package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
  40. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  41. package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
  42. package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
  44. package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
  45. package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
  46. package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
  47. package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
  49. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
  50. package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
  51. package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
  52. package/dist/types/hooks/skill-keywords.d.ts +1 -1
  53. package/dist/types/main.d.ts +0 -10
  54. package/dist/types/modes/acp/acp-agent.d.ts +13 -6
  55. package/dist/types/modes/components/model-selector.d.ts +14 -0
  56. package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
  57. package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
  58. package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
  59. package/dist/types/modes/interactive-mode.d.ts +3 -0
  60. package/dist/types/modes/types.d.ts +9 -0
  61. package/dist/types/rlm/artifacts.d.ts +4 -4
  62. package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
  63. package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
  64. package/dist/types/runtime-mcp/config.d.ts +6 -0
  65. package/dist/types/runtime-mcp/manager.d.ts +10 -0
  66. package/dist/types/sdk/acp/adapter.d.ts +0 -4
  67. package/dist/types/sdk/broker/broker.d.ts +1 -1
  68. package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
  69. package/dist/types/sdk/broker/session-index.d.ts +15 -0
  70. package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
  71. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
  72. package/dist/types/sdk/bus/index.d.ts +8 -0
  73. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
  74. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
  75. package/dist/types/sdk/client/client.d.ts +39 -4
  76. package/dist/types/sdk/host/host.d.ts +1 -0
  77. package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
  78. package/dist/types/sdk/host/model-pin.d.ts +29 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +2 -1
  80. package/dist/types/sdk/lifecycle-session.d.ts +13 -0
  81. package/dist/types/sdk/router/session-router.d.ts +27 -1
  82. package/dist/types/sdk/session.d.ts +11 -1
  83. package/dist/types/session/agent-session.d.ts +36 -6
  84. package/dist/types/session/artifacts.d.ts +16 -1
  85. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  86. package/dist/types/session/session-manager.d.ts +84 -2
  87. package/dist/types/session/session-staging-paths.d.ts +3 -0
  88. package/dist/types/skill-state/canonical-skills.d.ts +1 -1
  89. package/dist/types/skill-state/workflow-hud.d.ts +12 -21
  90. package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
  91. package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
  92. package/dist/types/system-prompt.d.ts +23 -0
  93. package/dist/types/task/executor.d.ts +35 -4
  94. package/dist/types/task/index.d.ts +17 -3
  95. package/dist/types/task/receipt.d.ts +3 -1
  96. package/dist/types/task/types.d.ts +144 -0
  97. package/dist/types/tools/atomic-file-write.d.ts +72 -0
  98. package/dist/types/tools/descriptors.d.ts +1 -0
  99. package/dist/types/tools/index.d.ts +36 -1
  100. package/dist/types/tools/move-session.d.ts +40 -0
  101. package/dist/types/tools/python.d.ts +12 -0
  102. package/dist/types/tools/resource-gc.d.ts +0 -3
  103. package/dist/types/tools/search.d.ts +8 -1
  104. package/dist/types/tools/skill.d.ts +9 -4
  105. package/dist/types/tools/subagent.d.ts +33 -3
  106. package/dist/types/utils/xml-escape.d.ts +2 -0
  107. package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
  108. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
  109. package/package.json +11 -12
  110. package/scripts/build-sdk-package-smoke.ts +7 -5
  111. package/scripts/check-autorouting-tier-map.ts +183 -0
  112. package/scripts/generate-sdk-operation-inventory.ts +12 -0
  113. package/scripts/generate-tool-catalog.ts +40 -5
  114. package/src/async/job-manager.ts +5 -2
  115. package/src/autoresearch/dashboard.ts +134 -314
  116. package/src/autoresearch/data-context.ts +35 -0
  117. package/src/autoresearch/git.ts +186 -26
  118. package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
  119. package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
  120. package/src/autoresearch/prompts/prompt.md +103 -0
  121. package/src/autoresearch/prompts.ts +126 -0
  122. package/src/autoresearch/runs.ts +708 -0
  123. package/src/autoresearch/types.ts +9 -150
  124. package/src/capability/index.ts +4 -4
  125. package/src/capability/types.ts +20 -0
  126. package/src/cli/accounts-cli.ts +21 -5
  127. package/src/cli/config-cli.ts +25 -1
  128. package/src/cli/fast-help.ts +1 -2
  129. package/src/cli/list-models.ts +5 -8
  130. package/src/cli/setup-cli.ts +8 -0
  131. package/src/cli.ts +2 -21
  132. package/src/commands/autoresearch.ts +33 -0
  133. package/src/commands/coordinator.ts +15 -0
  134. package/src/commands/launch.ts +2 -4
  135. package/src/commands/quick-lane.ts +1 -1
  136. package/src/commands/sdk.ts +24 -0
  137. package/src/commands/state.ts +1 -1
  138. package/src/config/autorouting-contract.ts +474 -0
  139. package/src/config/autorouting-generator.ts +238 -0
  140. package/src/config/autorouting-tier-map.ts +6108 -0
  141. package/src/config/autorouting.ts +120 -0
  142. package/src/config/model-discovery-manager.ts +42 -11
  143. package/src/config/model-registry.ts +348 -68
  144. package/src/config/provider-selection-policy.ts +60 -9
  145. package/src/config/settings-schema.ts +195 -6
  146. package/src/config/settings.ts +62 -1
  147. package/src/config.ts +2 -3
  148. package/src/coordinator/contract.ts +0 -1
  149. package/src/coordinator-mcp/codex-handoff.ts +68 -42
  150. package/src/coordinator-mcp/durability.ts +200 -0
  151. package/src/coordinator-mcp/event-webhook.ts +278 -0
  152. package/src/coordinator-mcp/question-state.ts +109 -46
  153. package/src/coordinator-mcp/server.ts +932 -504
  154. package/src/crash/index-store.ts +126 -22
  155. package/src/crash/record-loader.ts +7 -0
  156. package/src/crash/upstream/relay.ts +82 -34
  157. package/src/cursor.ts +84 -1
  158. package/src/debug/index.ts +26 -0
  159. package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
  160. package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
  161. package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
  162. package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
  163. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
  164. package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
  165. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
  166. package/src/defaults/gjc-defaults.ts +89 -2
  167. package/src/defaults/gjc-skills.generated.ts +24 -17
  168. package/src/discovery/builtin.ts +18 -5
  169. package/src/discovery/helpers.ts +6 -6
  170. package/src/edit/index.ts +5 -2
  171. package/src/eval/py/executor.ts +8 -0
  172. package/src/extensibility/extensions/loader.ts +135 -13
  173. package/src/extensibility/extensions/wrapper.ts +1 -2
  174. package/src/extensibility/gjc-plugins/schema.ts +1 -1
  175. package/src/extensibility/runtime-skill-discovery.ts +2 -2
  176. package/src/extensibility/skill-management.ts +2 -3
  177. package/src/extensibility/skills.ts +16 -8
  178. package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
  179. package/src/gjc-runtime/gc-render.ts +0 -1
  180. package/src/gjc-runtime/gc-runtime.ts +1 -11
  181. package/src/gjc-runtime/launch-tmux.ts +3 -3
  182. package/src/gjc-runtime/psmux-detect.ts +2 -2
  183. package/src/gjc-runtime/python-transcript.ts +58 -0
  184. package/src/gjc-runtime/ralplan-runtime.ts +6 -18
  185. package/src/gjc-runtime/session-layout.ts +18 -7
  186. package/src/gjc-runtime/session-state-sidecar.ts +58 -9
  187. package/src/gjc-runtime/state-renderer.ts +0 -38
  188. package/src/gjc-runtime/state-runtime.ts +43 -22
  189. package/src/gjc-runtime/state-schema.ts +1 -1
  190. package/src/gjc-runtime/tmux-common.ts +7 -7
  191. package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
  192. package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
  193. package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
  194. package/src/gjc-runtime/workflow-command-ref.ts +15 -11
  195. package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
  196. package/src/gjc-runtime/workflow-manifest.ts +43 -83
  197. package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
  198. package/src/hooks/skill-keywords.ts +3 -3
  199. package/src/hooks/skill-state.ts +2 -2
  200. package/src/internal-urls/docs-index.generated.ts +26 -24
  201. package/src/lsp/index.ts +25 -5
  202. package/src/main.ts +2 -27
  203. package/src/modes/acp/acp-agent.ts +200 -75
  204. package/src/modes/components/model-selector.ts +216 -13
  205. package/src/modes/components/oauth-selector.ts +5 -5
  206. package/src/modes/components/skill-hud/render.ts +173 -35
  207. package/src/modes/components/smart-routing-panel.ts +531 -0
  208. package/src/modes/components/tool-status-header.ts +6 -3
  209. package/src/modes/controllers/command-controller.ts +17 -7
  210. package/src/modes/controllers/input-controller.ts +44 -9
  211. package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
  212. package/src/modes/controllers/selector-controller.ts +298 -65
  213. package/src/modes/interactive-mode.ts +8 -1
  214. package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
  215. package/src/modes/types.ts +9 -1
  216. package/src/prompts/agents/planner.md +1 -1
  217. package/src/prompts/system/system-prompt.md +1 -1
  218. package/src/prompts/system/volatile-project-context.md +2 -1
  219. package/src/prompts/tools/move-session.md +15 -0
  220. package/src/prompts/tools/python.md +12 -0
  221. package/src/prompts/tools/skill-discovery.md +1 -1
  222. package/src/prompts/tools/skill.md +2 -2
  223. package/src/prompts/tools/task-summary.md +1 -0
  224. package/src/prompts/tools/task.md +6 -0
  225. package/src/rlm/artifacts.ts +12 -10
  226. package/src/runtime/optional-runtime-services.ts +3 -2
  227. package/src/runtime/workspace-tree-service.ts +11 -3
  228. package/src/runtime-mcp/config.ts +8 -1
  229. package/src/runtime-mcp/manager.ts +33 -0
  230. package/src/sdk/acp/adapter.ts +48 -1
  231. package/src/sdk/broker/broker.ts +30 -2
  232. package/src/sdk/broker/discovery.ts +217 -2
  233. package/src/sdk/broker/lifecycle.ts +385 -59
  234. package/src/sdk/broker/session-index.ts +243 -48
  235. package/src/sdk/broker/transport.ts +1 -0
  236. package/src/sdk/bus/chat-daemon-control.ts +8 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
  238. package/src/sdk/bus/index.ts +208 -11
  239. package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
  240. package/src/sdk/bus/reconciliation-store.ts +27 -5
  241. package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
  242. package/src/sdk/client/client.ts +204 -22
  243. package/src/sdk/host/host.ts +16 -0
  244. package/src/sdk/host/internal-autorouting-state.ts +13 -0
  245. package/src/sdk/host/model-pin.ts +101 -0
  246. package/src/sdk/host/reverse-leases.ts +2 -1
  247. package/src/sdk/host/session-runtime.test.ts +224 -1
  248. package/src/sdk/host/session-runtime.ts +125 -37
  249. package/src/sdk/lifecycle/service.ts +3 -1
  250. package/src/sdk/lifecycle-session.ts +42 -1
  251. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  252. package/src/sdk/router/session-router.ts +375 -36
  253. package/src/sdk/session.ts +547 -47
  254. package/src/session/account-inventory.ts +1 -1
  255. package/src/session/agent-session.ts +765 -189
  256. package/src/session/artifacts.ts +234 -4
  257. package/src/session/internal/managed-session-storage.ts +91 -4
  258. package/src/session/provider-safety-stop-hint.ts +3 -2
  259. package/src/session/session-manager.ts +775 -16
  260. package/src/session/session-staging-paths.ts +8 -0
  261. package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
  262. package/src/setup/host-plugin-setup.ts +2 -2
  263. package/src/skill-state/active-state.ts +2 -2
  264. package/src/skill-state/canonical-skills.ts +1 -1
  265. package/src/skill-state/initial-phase.ts +1 -1
  266. package/src/skill-state/workflow-hud.ts +41 -41
  267. package/src/skill-state/workflow-mutation-guard.ts +26 -7
  268. package/src/slash-commands/builtin-registry.ts +77 -0
  269. package/src/slash-commands/helpers/autorouting-status.ts +55 -0
  270. package/src/system-prompt.ts +102 -5
  271. package/src/task/discovery.ts +2 -3
  272. package/src/task/executor.ts +617 -43
  273. package/src/task/index.ts +262 -15
  274. package/src/task/receipt.ts +30 -0
  275. package/src/task/render.ts +11 -1
  276. package/src/task/types.ts +316 -0
  277. package/src/tools/atomic-file-write.ts +594 -0
  278. package/src/tools/bisect.ts +3 -2
  279. package/src/tools/descriptors.test.ts +3 -0
  280. package/src/tools/descriptors.ts +24 -0
  281. package/src/tools/image-gen.ts +5 -1
  282. package/src/tools/index.ts +62 -5
  283. package/src/tools/move-session.ts +107 -0
  284. package/src/tools/python.ts +205 -0
  285. package/src/tools/read.ts +88 -15
  286. package/src/tools/renderers.ts +2 -0
  287. package/src/tools/resource-gc.ts +8 -177
  288. package/src/tools/search.ts +30 -6
  289. package/src/tools/skill-discovery.ts +1 -0
  290. package/src/tools/skill.ts +37 -7
  291. package/src/tools/subagent-render.ts +119 -13
  292. package/src/tools/subagent.ts +91 -74
  293. package/src/tools/tool-catalog.generated.ts +64 -2
  294. package/src/tools/write.ts +16 -12
  295. package/src/utils/xml-escape.ts +14 -0
  296. package/src/web/search/providers/codex.ts +2 -2
  297. package/src/workflow/workflow-intent-diff.ts +13 -12
  298. package/src/workflow/workflow-intent-report-consumer.ts +6 -6
  299. package/dist/types/autoresearch/helpers.d.ts +0 -24
  300. package/dist/types/autoresearch/index.d.ts +0 -2
  301. package/dist/types/autoresearch/state.d.ts +0 -17
  302. package/dist/types/autoresearch/storage.d.ts +0 -142
  303. package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
  304. package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
  305. package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
  306. package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
  307. package/dist/types/commands/team.d.ts +0 -24
  308. package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
  309. package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
  310. package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
  311. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
  312. package/dist/types/gjc-runtime/team-store.d.ts +0 -168
  313. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
  314. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
  315. package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
  316. package/dist/types/rlm/index.d.ts +0 -47
  317. package/dist/types/rlm/preset.d.ts +0 -23
  318. package/dist/types/rlm/python-tool.d.ts +0 -16
  319. package/src/autoresearch/command-resume.md +0 -14
  320. package/src/autoresearch/index.ts +0 -536
  321. package/src/autoresearch/prompt.md +0 -103
  322. package/src/autoresearch/resume-message.md +0 -10
  323. package/src/autoresearch/state.ts +0 -273
  324. package/src/autoresearch/storage.ts +0 -699
  325. package/src/autoresearch/tools/init-experiment.ts +0 -272
  326. package/src/autoresearch/tools/log-experiment.ts +0 -524
  327. package/src/autoresearch/tools/run-experiment.ts +0 -475
  328. package/src/autoresearch/tools/update-notes.ts +0 -109
  329. package/src/commands/rlm.ts +0 -19
  330. package/src/commands/team.ts +0 -238
  331. package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
  332. package/src/defaults/gjc/skills/team/SKILL.md +0 -467
  333. package/src/gjc-runtime/team-gc.ts +0 -49
  334. package/src/gjc-runtime/team-launch.ts +0 -404
  335. package/src/gjc-runtime/team-notify.ts +0 -326
  336. package/src/gjc-runtime/team-runtime.ts +0 -5923
  337. package/src/gjc-runtime/team-store.ts +0 -878
  338. package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
  339. package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
  340. package/src/gjc-runtime/team-workers.ts +0 -1123
  341. package/src/prompts/system/rlm-report-command.md +0 -1
  342. package/src/prompts/system/rlm-research.md +0 -23
  343. package/src/rlm/index.ts +0 -365
  344. package/src/rlm/preset.ts +0 -76
  345. package/src/rlm/python-tool.ts +0 -68
@@ -14,6 +14,15 @@ import {
14
14
  truncateToWidth,
15
15
  } from "@gajae-code/tui";
16
16
  import { sanitizeText } from "@gajae-code/utils";
17
+ import {
18
+ type AutoroutingProviderOrderHint,
19
+ type AutoroutingSetup,
20
+ autoroutingProviderOrderHint,
21
+ evaluateAutoroutingProvenanceState,
22
+ normalizeTierMap,
23
+ validateAutoroutingSetup,
24
+ } from "../../config/autorouting-contract";
25
+
17
26
  import { isModelProfileProviderAvailable } from "../../config/model-profile-contract";
18
27
  import {
19
28
  getModelProfilePresentation,
@@ -46,6 +55,8 @@ import { formatModelOnboardingInlineHint } from "../../setup/model-onboarding-gu
46
55
  import { formatClampedModelSelector, getThinkingLevelMetadata, parseThinkingLevel } from "../../thinking";
47
56
  import { getTabBarTheme } from "../shared";
48
57
  import { DynamicBorder } from "./dynamic-border";
58
+ import type { SmartRoutingIntent, SmartRoutingPreview } from "./smart-routing-panel";
59
+ import { SmartRoutingPanelComponent } from "./smart-routing-panel";
49
60
 
50
61
  function makeInvertedBadge(label: string, color: ThemeColor): string {
51
62
  const fgAnsi = theme.getFgAnsi(color);
@@ -147,6 +158,10 @@ export type ModelSelectorSelection =
147
158
  | {
148
159
  kind: "deleteProfile";
149
160
  profileName: string;
161
+ }
162
+ | {
163
+ kind: "smartRouting";
164
+ intent: SmartRoutingIntent;
150
165
  };
151
166
 
152
167
  interface PendingThinkingChoice {
@@ -180,7 +195,7 @@ function createProviderTab(providerId: string): ProviderTabState {
180
195
  return { id: providerId, label: formatProviderTabLabel(providerId), providerId };
181
196
  }
182
197
 
183
- type ModelSelectorViewMode = "presets" | "models";
198
+ type ModelSelectorViewMode = "presets" | "models" | "smart-routing";
184
199
 
185
200
  interface PresetGroupRow {
186
201
  kind: "group";
@@ -216,6 +231,10 @@ interface PresetImageRoleRow {
216
231
  kind: "imageRole";
217
232
  }
218
233
 
234
+ interface PresetSmartRoutingRow {
235
+ kind: "smartRouting";
236
+ }
237
+
219
238
  type PresetLandingRow =
220
239
  | PresetGroupRow
221
240
  | PresetProfileRow
@@ -223,7 +242,8 @@ type PresetLandingRow =
223
242
  | PresetCreateUnavailableRow
224
243
  | PresetAlreadySavedRow
225
244
  | PresetBrowseRow
226
- | PresetImageRoleRow;
245
+ | PresetImageRoleRow
246
+ | PresetSmartRoutingRow;
227
247
 
228
248
  // Stable logical identity for a preset landing row, independent of its current
229
249
  // list position. Used to relocate the cursor after the expanded group changes so
@@ -244,6 +264,8 @@ function presetRowIdentity(row: PresetLandingRow): string {
244
264
  return `alreadySaved:${row.profile.name}`;
245
265
  case "imageRole":
246
266
  return "imageRole";
267
+ case "smartRouting":
268
+ return "smartRouting";
247
269
  }
248
270
  }
249
271
 
@@ -376,7 +398,10 @@ export class ModelSelectorComponent extends Container {
376
398
  #assignmentState: "idle" | "assigning" = "idle";
377
399
  #closeAfterAssignment = false;
378
400
  #unsubscribeCatalogChanged: () => void = () => {};
401
+ #unsubscribeProviderOrderChanged: () => void = () => {};
379
402
  #disposed = false;
403
+ /** Standalone smart-routing entry: cancel closes the selector instead of falling back to the preset landing. */
404
+ #smartRoutingOnly = false;
380
405
 
381
406
  // Preset landing state
382
407
  #viewMode: ModelSelectorViewMode = "presets";
@@ -391,6 +416,8 @@ export class ModelSelectorComponent extends Container {
391
416
  #presetLoginHint?: string;
392
417
  #authSessionId?: string;
393
418
  #imageRoleFilter: boolean = false;
419
+ #smartRoutingPanel?: SmartRoutingPanelComponent;
420
+ #smartRoutingPreviewBuilder?: (draft: AutoroutingSetup) => SmartRoutingPreview;
394
421
 
395
422
  // Tab state
396
423
  #providers: ProviderTabState[] = STATIC_PROVIDER_TABS;
@@ -414,6 +441,9 @@ export class ModelSelectorComponent extends Container {
414
441
  currentThinkingLevel?: ThinkingLevel;
415
442
  activeModelProfile?: string;
416
443
  configuredDefaultChain?: readonly string[];
444
+ smartRoutingPreview?: (draft: AutoroutingSetup) => SmartRoutingPreview;
445
+ /** Open the smart-routing panel directly instead of the preset landing. */
446
+ smartRoutingOnly?: boolean;
417
447
  },
418
448
  ) {
419
449
  super();
@@ -426,6 +456,7 @@ export class ModelSelectorComponent extends Container {
426
456
  this.#onCancelCallback = onCancel;
427
457
  this.#temporaryOnly = options?.temporaryOnly ?? false;
428
458
  this.#authSessionId = options?.sessionId;
459
+ this.#smartRoutingPreviewBuilder = options?.smartRoutingPreview;
429
460
  this.#currentModel = _currentModel;
430
461
  this.#currentThinkingLevel = options?.currentThinkingLevel;
431
462
  this.#activeModelProfile = options?.activeModelProfile;
@@ -442,7 +473,12 @@ export class ModelSelectorComponent extends Container {
442
473
  ? this.#isFastForProvider(this.#currentModel.provider, modelSupportsServiceTier(this.#currentModel))
443
474
  : false);
444
475
  const initialSearchInput = options?.initialSearchInput;
445
- this.#viewMode = this.#temporaryOnly || initialSearchInput || scopedModels.length > 0 ? "models" : "presets";
476
+ this.#smartRoutingOnly = options?.smartRoutingOnly === true;
477
+ this.#viewMode = this.#smartRoutingOnly
478
+ ? "smart-routing"
479
+ : this.#temporaryOnly || initialSearchInput || scopedModels.length > 0
480
+ ? "models"
481
+ : "presets";
446
482
 
447
483
  // Load current role assignments from settings
448
484
  this.#rebuildRoleModels();
@@ -492,9 +528,25 @@ export class ModelSelectorComponent extends Container {
492
528
  });
493
529
  }
494
530
 
531
+ // Advisory drift only, and only while the smart-routing panel is mounted. This
532
+ // must not call refreshState: that would discard the user's unsaved draft.
533
+ this.#unsubscribeProviderOrderChanged = this.#settings.onChanged?.(path => {
534
+ if (this.#disposed || path !== "modelProviderOrder") return;
535
+ if (this.#viewMode !== "smart-routing") return;
536
+ const panel = this.#smartRoutingPanel;
537
+ if (!panel) return;
538
+ panel.updateProviderOrderHint(this.#providerOrderHintFor(panel.getProviderOrder()));
539
+ this.#tui.requestRender();
540
+ });
541
+
495
542
  // Load models and do initial render
496
543
  this.#loadModels().then(() => {
497
544
  this.#buildProviderTabs();
545
+ if (this.#smartRoutingOnly) {
546
+ this.#enterSmartRoutingMode();
547
+ this.#tui.requestRender();
548
+ return;
549
+ }
498
550
  if (this.#viewMode === "presets" && (this.#modelRegistry.getModelProfiles?.().size ?? 0) === 0) {
499
551
  this.#viewMode = "models";
500
552
  }
@@ -521,6 +573,7 @@ export class ModelSelectorComponent extends Container {
521
573
  if (this.#disposed) return;
522
574
  this.#disposed = true;
523
575
  this.#unsubscribeCatalogChanged();
576
+ this.#unsubscribeProviderOrderChanged();
524
577
  super.dispose();
525
578
  }
526
579
 
@@ -805,17 +858,15 @@ export class ModelSelectorComponent extends Container {
805
858
 
806
859
  #materializeModels(models: ModelItem[]): MaterializedCatalog {
807
860
  const candidateModels = models.map(item => item.model);
808
- const canonicalRecords = this.#modelRegistry.getCanonicalModels({
861
+ const canonicalSelections = this.#modelRegistry.getCanonicalModelSelections({
809
862
  availableOnly: this.#scopedModels.length === 0,
810
863
  candidates: candidateModels,
811
864
  });
812
865
  const scopedThinkingBySelector = new Map(models.map(item => [item.selector, item.thinkingLevel]));
813
- const canonicalModels = canonicalRecords
814
- .map((record): CanonicalModelItem | undefined => {
815
- const selectedModel = this.#modelRegistry.resolveCanonicalModel(record.id, {
816
- availableOnly: this.#scopedModels.length === 0,
817
- candidates: candidateModels,
818
- });
866
+ const canonicalModels = canonicalSelections
867
+ .map((selection): CanonicalModelItem | undefined => {
868
+ const record = selection.record;
869
+ const selectedModel = selection.model;
819
870
  if (!selectedModel) return undefined;
820
871
  const selectedSelector = `${selectedModel.provider}/${selectedModel.id}`;
821
872
  const searchText = [
@@ -954,7 +1005,10 @@ export class ModelSelectorComponent extends Container {
954
1005
 
955
1006
  let refreshError: unknown;
956
1007
  try {
957
- await this.#modelRegistry.refreshProvider(providerId);
1008
+ // Cache-aware: a fresh discovery cache answers instantly instead of
1009
+ // forcing a provider round-trip (hundreds of ms on remote gateways)
1010
+ // on every provider-tab visit. Stale/missing cache still fetches.
1011
+ await this.#modelRegistry.refreshProvider(providerId, "online-if-uncached");
958
1012
  } catch (error) {
959
1013
  refreshError = error;
960
1014
  }
@@ -1229,6 +1283,7 @@ export class ModelSelectorComponent extends Container {
1229
1283
  rows.push({ kind: "createUnavailable", label: "Select a model before creating a custom preset" });
1230
1284
  }
1231
1285
  rows.push({ kind: "imageRole" });
1286
+ rows.push({ kind: "smartRouting" });
1232
1287
  rows.push({ kind: "browse" });
1233
1288
  return rows;
1234
1289
  }
@@ -1435,7 +1490,8 @@ export class ModelSelectorComponent extends Container {
1435
1490
  selected.kind === "create" ||
1436
1491
  selected.kind === "createUnavailable" ||
1437
1492
  selected.kind === "alreadySaved" ||
1438
- selected.kind === "imageRole"
1493
+ selected.kind === "imageRole" ||
1494
+ selected.kind === "smartRouting"
1439
1495
  )
1440
1496
  return;
1441
1497
  if (this.#expandedPresetProviderId === selected.groupId) return;
@@ -1452,7 +1508,8 @@ export class ModelSelectorComponent extends Container {
1452
1508
  selected.kind === "create" ||
1453
1509
  selected.kind === "createUnavailable" ||
1454
1510
  selected.kind === "alreadySaved" ||
1455
- selected.kind === "imageRole"
1511
+ selected.kind === "imageRole" ||
1512
+ selected.kind === "smartRouting"
1456
1513
  )
1457
1514
  return;
1458
1515
  if (this.#expandedPresetProviderId !== selected.groupId) return;
@@ -1518,6 +1575,128 @@ export class ModelSelectorComponent extends Container {
1518
1575
  return truncateToWidth(label, ROLE_BINDING_MAX_WIDTH);
1519
1576
  }
1520
1577
 
1578
+ /**
1579
+ * A recorded declaration always wins; otherwise seed from the deterministic
1580
+ * provider priority so the draft reflects the user's configured order instead of
1581
+ * raw catalog iteration. No hardcoded provider fallback: an empty catalog means
1582
+ * there is nothing to generate tiers from, and the caller refuses entry.
1583
+ */
1584
+ #smartRoutingSetup(): AutoroutingSetup {
1585
+ const stored = this.#settings.get("task.autorouting.setup");
1586
+ if (validateAutoroutingSetup(stored).length === 0 && stored !== undefined) return structuredClone(stored);
1587
+ return { schema: 1, providers: [...this.#modelRegistry.autoroutingProviderOrder()] };
1588
+ }
1589
+
1590
+ /**
1591
+ * Advisory hint input is the panel's *current draft*, not the persisted setup, so
1592
+ * a user mid-reorder sees drift for what they are actually editing.
1593
+ */
1594
+ #providerOrderHintFor(declared: readonly string[]): AutoroutingProviderOrderHint {
1595
+ return autoroutingProviderOrderHint(declared, this.#modelRegistry.autoroutingProviderOrder());
1596
+ }
1597
+
1598
+ #smartRoutingPreview(setup: AutoroutingSetup): SmartRoutingPreview {
1599
+ if (!this.#smartRoutingPreviewBuilder) {
1600
+ throw new Error("Smart-routing preview is unavailable in this selector context.");
1601
+ }
1602
+ return this.#smartRoutingPreviewBuilder(setup);
1603
+ }
1604
+
1605
+ #smartRoutingIsStale(): boolean {
1606
+ const provenance = this.#settings.get("task.autorouting.provenance");
1607
+ if (!provenance || validateAutoroutingSetup(this.#smartRoutingSetup()).length > 0) return false;
1608
+ try {
1609
+ const preview = this.#smartRoutingPreview(this.#smartRoutingSetup());
1610
+ const state = evaluateAutoroutingProvenanceState(provenance, {
1611
+ catalogFingerprint: preview.sourceIdentity.catalogFingerprint,
1612
+ mapFingerprint: preview.sourceIdentity.mapFingerprint,
1613
+ tiers: this.#settings.get("task.autorouting.tiers") ?? {},
1614
+ });
1615
+ return state.staleMap || state.staleCatalog || state.handEdited;
1616
+ } catch {
1617
+ return true;
1618
+ }
1619
+ }
1620
+
1621
+ #smartRoutingReadOnly(): boolean {
1622
+ return this.#temporaryOnly || this.#scopedModels.length > 0 || !this.#settings.canWriteDurableConfig();
1623
+ }
1624
+
1625
+ #enterSmartRoutingMode(): void {
1626
+ if (!this.#smartRoutingPreviewBuilder) {
1627
+ if (this.#smartRoutingOnly) {
1628
+ this.#onCancelCallback();
1629
+ return;
1630
+ }
1631
+ this.#presetLoginHint = "Smart-routing setup is unavailable in this selector context.";
1632
+ this.#renderPresetLanding();
1633
+ return;
1634
+ }
1635
+
1636
+ const setup = this.#smartRoutingSetup();
1637
+ if (setup.providers.length === 0) {
1638
+ // Nothing to generate tiers from; refuse entry instead of seeding a guess.
1639
+ if (this.#smartRoutingOnly) {
1640
+ this.#onCancelCallback();
1641
+ return;
1642
+ }
1643
+ this.#presetLoginHint = "No providers are available to generate routing tiers.";
1644
+ this.#renderPresetLanding();
1645
+ return;
1646
+ }
1647
+ const preview = this.#smartRoutingPreview(setup);
1648
+ const tiers = normalizeTierMap(this.#settings.get("task.autorouting.tiers"));
1649
+ const provenance = this.#settings.get("task.autorouting.provenance");
1650
+ this.#viewMode = "smart-routing";
1651
+ this.#smartRoutingPanel = new SmartRoutingPanelComponent({
1652
+ setup,
1653
+ tiers,
1654
+ provenance,
1655
+ enabled: this.#settings.get("task.autorouting.enabled") === true,
1656
+ providerOrderHint: this.#providerOrderHintFor(setup.providers),
1657
+ readOnly: this.#smartRoutingReadOnly(),
1658
+ stale: this.#smartRoutingIsStale(),
1659
+ preview,
1660
+ generatePreview: draft => this.#smartRoutingPreview(draft),
1661
+ onSelect: async intent => {
1662
+ await this.#onSelectCallback({ kind: "smartRouting", intent });
1663
+ return intent.kind === "apply" ? this.#smartRoutingPreview(intent.draft) : undefined;
1664
+ },
1665
+ onCancel: () => (this.#smartRoutingOnly ? this.#onCancelCallback() : this.#switchToPresetMode()),
1666
+ });
1667
+ this.#headerContainer.clear();
1668
+ this.#headerContainer.addChild(new Text(theme.fg("accent", "Smart routing"), 0, 0));
1669
+ this.#tabBar = null;
1670
+ this.#listContainer.clear();
1671
+ this.#listContainer.addChild(this.#smartRoutingPanel);
1672
+ this.#tui.requestRender();
1673
+ }
1674
+
1675
+ #switchToPresetMode(): void {
1676
+ this.#smartRoutingPanel = undefined;
1677
+ this.#viewMode = "presets";
1678
+ this.#presetCursor = Math.min(this.#presetCursor, Math.max(0, this.#getPresetRows().length - 1));
1679
+ this.#renderPresetLanding();
1680
+ this.#tui.requestRender();
1681
+ }
1682
+
1683
+ refreshSmartRoutingState(): void {
1684
+ const panel = this.#smartRoutingPanel;
1685
+ if (!panel || this.#viewMode !== "smart-routing") return;
1686
+ const setup = this.#smartRoutingSetup();
1687
+ const preview = this.#smartRoutingPreview(setup);
1688
+ panel.refreshState({
1689
+ setup,
1690
+ tiers: normalizeTierMap(this.#settings.get("task.autorouting.tiers")),
1691
+ provenance: this.#settings.get("task.autorouting.provenance"),
1692
+ enabled: this.#settings.get("task.autorouting.enabled") === true,
1693
+ providerOrderHint: this.#providerOrderHintFor(setup.providers),
1694
+ stale: this.#smartRoutingIsStale(),
1695
+ preview,
1696
+ });
1697
+ this.#tui.requestRender();
1698
+ }
1699
+
1521
1700
  #renderPresetLanding(): void {
1522
1701
  this.#headerContainer.clear();
1523
1702
  this.#tabBar = null;
@@ -1531,6 +1710,12 @@ export class ModelSelectorComponent extends Container {
1531
1710
  const row = rows[i];
1532
1711
  const selected = i === this.#presetCursor;
1533
1712
  const prefix = selected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
1713
+ if (row.kind === "smartRouting") {
1714
+ const stale = this.#smartRoutingIsStale();
1715
+ const label = stale ? "Smart routing (stale)" : "Smart routing setup";
1716
+ this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
1717
+ continue;
1718
+ }
1534
1719
  if (row.kind === "create") {
1535
1720
  const label = "Create custom preset";
1536
1721
  this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
@@ -1921,6 +2106,10 @@ export class ModelSelectorComponent extends Container {
1921
2106
  }
1922
2107
 
1923
2108
  handleInput(keyData: string): void {
2109
+ if (this.#viewMode === "smart-routing") {
2110
+ this.#smartRoutingPanel?.handleInput(keyData);
2111
+ return;
2112
+ }
1924
2113
  if (this.#assignmentState === "assigning") {
1925
2114
  if (getKeybindings().matches(keyData, "tui.select.cancel")) {
1926
2115
  this.#closeAfterAssignment = true;
@@ -2106,6 +2295,10 @@ export class ModelSelectorComponent extends Container {
2106
2295
  }
2107
2296
  const row = this.#getSelectedPresetRow();
2108
2297
  if (!row) return;
2298
+ if (row.kind === "smartRouting") {
2299
+ this.#enterSmartRoutingMode();
2300
+ return;
2301
+ }
2109
2302
  if (row.kind === "create") {
2110
2303
  this.#onSelectCallback({ kind: "createProfile", profile: this.#buildCustomModelProfileSnapshot() });
2111
2304
  return;
@@ -2431,6 +2624,16 @@ export class ModelSelectorComponent extends Container {
2431
2624
  const row = this.#getSelectedPresetRow();
2432
2625
  return row ? presetRowIdentity(row) : undefined;
2433
2626
  }
2627
+ __testGetSmartRoutingPanel(): SmartRoutingPanelComponent | undefined {
2628
+ return this.#smartRoutingPanel;
2629
+ }
2630
+
2631
+ __testViewMode(): ModelSelectorViewMode {
2632
+ return this.#viewMode;
2633
+ }
2634
+ __testOpenSmartRoutingPanel(): void {
2635
+ this.#enterSmartRoutingMode();
2636
+ }
2434
2637
  }
2435
2638
 
2436
2639
  function getSelectableThinkingLevels(model: Model): ThinkingLevel[] {
@@ -146,15 +146,15 @@ export class OAuthSelectorComponent extends Container {
146
146
  this.#authStorage.hasRuntimeApiKey(provider) ||
147
147
  this.#authStorage.hasConfigApiKey(provider) ||
148
148
  Boolean(getEnvApiKey(provider));
149
- const rows = inventory.filter(row => this.#mode === "login" || row.credentialKind === "oauth");
149
+ const rows = this.#mode === "logout" ? inventory : inventory.filter(row => row.credentialKind === "oauth");
150
150
  this.#accountEntries = rows.map(row => ({
151
151
  kind: "account" as const,
152
152
  row,
153
153
  target: targets.get(row.id),
154
154
  selectable:
155
- row.credentialKind === "oauth" &&
156
- !apiKeyOverrideActive &&
157
- (this.#mode === "login" ? !row.disabled : targets.has(row.id)),
155
+ this.#mode === "login"
156
+ ? row.credentialKind === "oauth" && !apiKeyOverrideActive && !row.disabled
157
+ : targets.has(row.id),
158
158
  }));
159
159
  if (this.#mode === "login") {
160
160
  this.#accountEntries.push(
@@ -441,8 +441,8 @@ export class OAuthSelectorComponent extends Container {
441
441
  this.#statusMessage = undefined;
442
442
  this.stopValidation();
443
443
  if (selected.kind === "account") {
444
- if (selected.row.credentialKind !== "oauth") return;
445
444
  if (this.#mode === "login") {
445
+ if (selected.row.credentialKind !== "oauth") return;
446
446
  void this.#onAccountSelect?.({ kind: "id", value: String(selected.row.id) });
447
447
  } else if (selected.target) {
448
448
  this.#confirmOrArmAccountRemoval([selected.target], "Remove this account?");
@@ -1,28 +1,24 @@
1
+ import { truncateToWidth, visibleWidth } from "@gajae-code/tui";
1
2
  import {
2
3
  collapsePlanningPipeline,
3
4
  type SkillActiveEntry,
4
5
  type WorkflowHudChip,
5
6
  } from "../../../skill-state/active-state";
6
7
  import { workflowReceiptStatus } from "../../../skill-state/workflow-state-contract";
8
+ import { theme } from "../../theme/theme";
7
9
 
8
- const ANSI_RESET_FG = "\x1b[39m";
9
- const ANSI_RESET_BOLD = "\x1b[22m";
10
- const ANSI_BORDER = "\x1b[90m";
11
- const ANSI_ACCENT = "\x1b[36m";
12
- const ANSI_DIM = "\x1b[2m";
13
- const ANSI_BOLD = "\x1b[1m";
14
10
  const ANSI_PATTERN = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
11
+ const MAX_HUD_ROWS = 2;
15
12
 
16
- function visibleWidth(text: string): number {
17
- return text.replace(ANSI_PATTERN, "").length;
13
+ type WidthTier = "wide" | "medium" | "tight";
14
+ type HudToken = { text: string; mandatory: boolean; startsEntry?: boolean; reserveAfter?: number };
15
+
16
+ function color(role: "border" | "accent" | "dim" | "muted" | "warning" | "error", text: string): string {
17
+ return theme?.fg(role, text) ?? text;
18
18
  }
19
19
 
20
- function truncateToWidth(text: string, maxWidth: number): string {
21
- if (maxWidth <= 0) return "";
22
- if (visibleWidth(text) <= maxWidth) return text;
23
- const plain = text.replace(ANSI_PATTERN, "");
24
- if (maxWidth === 1) return "…";
25
- return `${plain.slice(0, maxWidth - 1)}…`;
20
+ function statusSymbol(kind: "warning" | "error"): string {
21
+ return theme?.status[kind] ?? (kind === "error" ? "[!!]" : "[!]");
26
22
  }
27
23
 
28
24
  function sanitizeHudPart(value: string | undefined): string {
@@ -40,10 +36,21 @@ function compareChips(a: WorkflowHudChip, b: WorkflowHudChip): number {
40
36
  return (a.priority ?? 50) - (b.priority ?? 50) || a.label.localeCompare(b.label);
41
37
  }
42
38
 
43
- function chipPrefix(chip: WorkflowHudChip): string {
44
- if (chip.severity === "error") return "!";
45
- if (chip.severity === "blocked") return "block";
46
- if (chip.severity === "warning") return "warn";
39
+ function tierForWidth(width: number): WidthTier {
40
+ return width >= 100 ? "wide" : width >= 60 ? "medium" : "tight";
41
+ }
42
+
43
+ function severityOf(chip: WorkflowHudChip): "error" | "warning" | undefined {
44
+ return chip.severity === "error" || chip.severity === "blocked"
45
+ ? "error"
46
+ : chip.severity === "warning"
47
+ ? "warning"
48
+ : undefined;
49
+ }
50
+
51
+ function severityGlyph(severity: WorkflowHudChip["severity"]): string {
52
+ if (severity === "error" || severity === "blocked") return color("error", statusSymbol("error"));
53
+ if (severity === "warning") return color("warning", statusSymbol("warning"));
47
54
  return "";
48
55
  }
49
56
 
@@ -52,32 +59,163 @@ function formatChip(chip: WorkflowHudChip): string | null {
52
59
  const value = sanitizeHudPart(chip.value);
53
60
  if (!label) return null;
54
61
  const body = value ? `${label}=${value}` : label;
55
- const prefix = chipPrefix(chip);
56
- return prefix ? `${prefix}:${body}` : body;
62
+ const role = severityOf(chip);
63
+ return role ? color(role, body) : color("dim", body);
57
64
  }
58
65
 
59
- function formatEntry(entry: SkillActiveEntry): string {
66
+ function keyMetricChip(skill: string, chips: readonly WorkflowHudChip[]): WorkflowHudChip | undefined {
67
+ const preferredBySkill: Record<string, readonly string[]> = {
68
+ "deep-interview": ["ambiguity"],
69
+ ralplan: ["iter", "round", "stage"],
70
+ ultragoal: ["goals", "current"],
71
+ autoresearch: ["exp", "experiments"],
72
+ };
73
+ const preferred = preferredBySkill[skill] ?? [];
74
+ return (
75
+ preferred.map(label => chips.find(chip => chip.label === label && !severityOf(chip))).find(Boolean) ??
76
+ chips.find(chip => !severityOf(chip))
77
+ );
78
+ }
79
+
80
+ function buildEntryTokens(entry: SkillActiveEntry, tier: WidthTier, width: number): HudToken[] {
60
81
  const skill = sanitizeHudPart(entry.skill);
61
82
  const phase = sanitizeHudPart(entry.phase);
62
83
  const base = phase ? `${skill}:${phase}` : skill;
63
- const chips = [...(entry.hud?.chips ?? [])]
64
- .sort(compareChips)
65
- .map(formatChip)
66
- .filter((chip): chip is string => Boolean(chip));
67
- if (entry.stale === true) chips.unshift("warn:stale");
68
- const receiptStatus = workflowReceiptStatus(entry.receipt);
69
- if (receiptStatus === "stale") chips.unshift("warn:receipt=stale");
70
- if (receiptStatus === "fresh") chips.push("receipt=fresh");
84
+ const chips = [...(entry.hud?.chips ?? [])].sort(compareChips);
85
+ if (entry.stale === true) chips.unshift({ label: "stale", priority: 0, severity: "warning" });
86
+ if (workflowReceiptStatus(entry.receipt) === "stale") {
87
+ chips.unshift({ label: "receipt", value: "stale", priority: 1, severity: "warning" });
88
+ }
89
+
90
+ const severity =
91
+ chips.find(chip => chip.severity === "error" || chip.severity === "blocked")?.severity ??
92
+ chips.find(chip => chip.severity === "warning")?.severity;
93
+ const glyph = severityGlyph(severity);
94
+ const baseText = color("accent", tier === "tight" ? skill : base);
95
+ if (
96
+ tier === "tight" &&
97
+ glyph &&
98
+ visibleWidth(glyph) <= width &&
99
+ visibleWidth(baseText) + 1 + visibleWidth(glyph) > width
100
+ ) {
101
+ return [{ text: glyph, mandatory: true, startsEntry: true }];
102
+ }
103
+ const tokens: HudToken[] = [
104
+ {
105
+ text: baseText,
106
+ mandatory: true,
107
+ startsEntry: true,
108
+ reserveAfter: glyph ? visibleWidth(` ${glyph}`) : 0,
109
+ },
110
+ ];
111
+ if (glyph) tokens.push({ text: glyph, mandatory: true });
112
+
113
+ if (tier === "medium") {
114
+ const metric = keyMetricChip(skill, chips);
115
+ const metricText = metric ? formatChip(metric) : null;
116
+ if (metricText) tokens.push({ text: metricText, mandatory: false });
117
+ return tokens;
118
+ }
119
+
71
120
  const summary = sanitizeHudPart(entry.hud?.summary);
72
- return [base, summary, ...chips].filter(Boolean).join(" ");
121
+ if (summary) tokens.push({ text: color("muted", summary), mandatory: false });
122
+ for (const chip of chips) {
123
+ const formatted = formatChip(chip);
124
+ if (formatted) tokens.push({ text: formatted, mandatory: false });
125
+ }
126
+ return tokens;
127
+ }
128
+
129
+ function appendOverflowMarker(row: string, width: number, protectedToken?: string): string {
130
+ if (!row || width <= 0) return row;
131
+ if (protectedToken) {
132
+ const tokenIndex = row.lastIndexOf(protectedToken);
133
+ if (tokenIndex >= 0) {
134
+ const prefix = row.slice(0, tokenIndex);
135
+ const tokenWidth = visibleWidth(protectedToken);
136
+ const plainPrefix = Bun.stripANSI(prefix).trimEnd();
137
+ const needsMarker = !plainPrefix.endsWith("…");
138
+ const prefixWidth = Math.max(0, width - tokenWidth - (needsMarker ? 1 : 0));
139
+ return `${truncateToWidth(prefix, prefixWidth)}${needsMarker ? "…" : ""}${protectedToken}`;
140
+ }
141
+ }
142
+ return truncateToWidth(`${row}…`, width);
73
143
  }
74
144
 
75
- export function renderSkillHudBar(entries: readonly SkillActiveEntry[], width: number): string | null {
145
+ /**
146
+ * Render the HUD as physical rows rather than embedding a newline in one row.
147
+ * The bottom-pinned TUI layout counts array elements as rows; a newline inside
148
+ * one element corrupts that accounting and can overwrite the editor.
149
+ */
150
+ export function renderSkillHudBar(entries: readonly SkillActiveEntry[], width: number): string[] | null {
76
151
  const visible = collapsePlanningPipeline(entries.filter(entry => entry.active !== false));
77
152
  const active = visible.filter(entry => sanitizeHudPart(entry.skill)).sort(compareEntries);
78
153
  if (active.length === 0 || width <= 0) return null;
79
- const body = active.map(formatEntry).join(" + ");
80
- const prefix = `${ANSI_BORDER}◆${ANSI_RESET_FG} ${ANSI_BOLD}${ANSI_ACCENT}hud${ANSI_RESET_FG}${ANSI_RESET_BOLD} `;
81
- const budget = Math.max(1, width - visibleWidth(prefix));
82
- return truncateToWidth(`${prefix}${ANSI_DIM}${truncateToWidth(body, budget)}${ANSI_RESET_BOLD}`, width);
154
+
155
+ const tier = tierForWidth(width);
156
+ const rail = color("border", "◆");
157
+ const separator = color("dim", " + ");
158
+ const firstPrefix = visibleWidth(rail) + 1 <= width ? `${rail} ` : "";
159
+ const continuationPrefix = firstPrefix ? " ".repeat(visibleWidth(firstPrefix)) : "";
160
+ const rows: string[] = [];
161
+ let row = firstPrefix;
162
+ let rowIndex = 0;
163
+ let hasToken = false;
164
+ let omitted = false;
165
+ let lastSeverityToken: string | undefined;
166
+
167
+ const tryAppend = (token: HudToken, allowTruncate: boolean): boolean => {
168
+ let joiner = hasToken ? (token.startsEntry ? separator : " ") : "";
169
+ let available = width - visibleWidth(row) - visibleWidth(joiner);
170
+ if (!hasToken && visibleWidth(row) > 0 && visibleWidth(token.text) > available) {
171
+ row = "";
172
+ joiner = "";
173
+ available = width;
174
+ }
175
+ if (available <= 0) return false;
176
+ const textAvailable = Math.max(0, available - (allowTruncate ? (token.reserveAfter ?? 0) : 0));
177
+ const text =
178
+ visibleWidth(token.text) <= textAvailable
179
+ ? token.text
180
+ : allowTruncate
181
+ ? truncateToWidth(token.text, textAvailable)
182
+ : "";
183
+ if (!text || visibleWidth(text) <= 0) return false;
184
+ row = `${row}${joiner}${text}`;
185
+ hasToken = true;
186
+ return true;
187
+ };
188
+
189
+ const startNextRow = (): boolean => {
190
+ if (rowIndex >= MAX_HUD_ROWS - 1) return false;
191
+ if (hasToken) rows.push(truncateToWidth(row, width));
192
+ rowIndex += 1;
193
+ row = continuationPrefix;
194
+ hasToken = false;
195
+ return true;
196
+ };
197
+
198
+ for (const entry of active) {
199
+ for (const token of buildEntryTokens(entry, tier, width)) {
200
+ const append = (allowTruncate: boolean): boolean => {
201
+ const appended = tryAppend(token, allowTruncate);
202
+ if (appended && token.mandatory && !token.startsEntry) lastSeverityToken = token.text;
203
+ return appended;
204
+ };
205
+ if (append(false)) continue;
206
+ if (startNextRow() && append(false)) continue;
207
+ if (append(true)) continue;
208
+ if (token.mandatory && startNextRow() && append(true)) continue;
209
+ omitted = true;
210
+ break;
211
+ }
212
+ if (omitted) break;
213
+ }
214
+
215
+ if (hasToken) rows.push(truncateToWidth(row, width));
216
+ if (omitted && rows.length > 0) {
217
+ const last = rows.length - 1;
218
+ rows[last] = appendOverflowMarker(rows[last] ?? "", width, lastSeverityToken);
219
+ }
220
+ return rows.length > 0 ? rows.slice(0, MAX_HUD_ROWS) : null;
83
221
  }