mixdog 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +205 -33
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1222 -90
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2212 -446
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +110 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +76 -28
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  114. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  115. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  116. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  117. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  118. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  119. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  120. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  121. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  122. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  123. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  124. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  125. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  126. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  127. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  128. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  129. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  130. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  137. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  138. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  140. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  141. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  142. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  144. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  145. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  148. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  149. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  150. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  151. package/src/runtime/channels/index.mjs +152 -24
  152. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  153. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  154. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  155. package/src/runtime/channels/lib/webhook.mjs +1 -1
  156. package/src/runtime/channels/tool-defs.mjs +29 -29
  157. package/src/runtime/memory/index.mjs +520 -98
  158. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  159. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  160. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  161. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  162. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  163. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  164. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  165. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  166. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  167. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  168. package/src/runtime/memory/lib/memory.mjs +88 -9
  169. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  170. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  171. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  172. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  173. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  174. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  175. package/src/runtime/memory/tool-defs.mjs +46 -37
  176. package/src/runtime/search/index.mjs +102 -466
  177. package/src/runtime/search/lib/web-tools.mjs +45 -25
  178. package/src/runtime/search/tool-defs.mjs +16 -23
  179. package/src/runtime/shared/abort-controller.mjs +1 -1
  180. package/src/runtime/shared/atomic-file.mjs +4 -3
  181. package/src/runtime/shared/background-tasks.mjs +122 -11
  182. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  183. package/src/runtime/shared/config.mjs +7 -4
  184. package/src/runtime/shared/err-text.mjs +131 -4
  185. package/src/runtime/shared/llm/cost.mjs +2 -2
  186. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  187. package/src/runtime/shared/llm/index.mjs +34 -11
  188. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  189. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  190. package/src/runtime/shared/singleton-owner.mjs +104 -0
  191. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  192. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  193. package/src/runtime/shared/tool-surface.mjs +624 -95
  194. package/src/runtime/shared/user-data-guard.mjs +0 -2
  195. package/src/standalone/agent-task-status.mjs +203 -0
  196. package/src/standalone/agent-task-status.test.mjs +76 -0
  197. package/src/standalone/agent-tool.mjs +1913 -0
  198. package/src/standalone/channel-worker.mjs +370 -14
  199. package/src/standalone/explore-tool.mjs +165 -70
  200. package/src/standalone/folder-dialog.mjs +314 -0
  201. package/src/standalone/hook-bus.mjs +898 -22
  202. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  203. package/src/standalone/projects.mjs +226 -0
  204. package/src/standalone/provider-admin.mjs +41 -24
  205. package/src/standalone/seeds.mjs +2 -69
  206. package/src/standalone/usage-dashboard.mjs +96 -8
  207. package/src/tui/App.jsx +4788 -2151
  208. package/src/tui/components/AnsiText.jsx +39 -28
  209. package/src/tui/components/ContextPanel.jsx +14 -4
  210. package/src/tui/components/Markdown.jsx +43 -77
  211. package/src/tui/components/MarkdownTable.jsx +9 -184
  212. package/src/tui/components/Message.jsx +28 -11
  213. package/src/tui/components/Picker.jsx +95 -56
  214. package/src/tui/components/PromptInput.jsx +367 -239
  215. package/src/tui/components/QueuedCommands.jsx +1 -1
  216. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  217. package/src/tui/components/Spinner.jsx +67 -38
  218. package/src/tui/components/StatusLine.jsx +606 -38
  219. package/src/tui/components/TextEntryPanel.jsx +128 -9
  220. package/src/tui/components/ToolExecution.jsx +592 -362
  221. package/src/tui/components/TurnDone.jsx +3 -3
  222. package/src/tui/components/UsagePanel.jsx +3 -5
  223. package/src/tui/components/tool-output-format.mjs +365 -0
  224. package/src/tui/components/tool-output-format.test.mjs +220 -0
  225. package/src/tui/dist/index.mjs +8826 -2390
  226. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  227. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  228. package/src/tui/engine.mjs +1455 -279
  229. package/src/tui/figures.mjs +21 -40
  230. package/src/tui/index.jsx +75 -31
  231. package/src/tui/input-editing.mjs +25 -0
  232. package/src/tui/markdown/format-token.mjs +511 -68
  233. package/src/tui/markdown/format-token.test.mjs +216 -0
  234. package/src/tui/markdown/render-ansi.mjs +94 -0
  235. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  236. package/src/tui/markdown/stream-fence.mjs +34 -0
  237. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  238. package/src/tui/markdown/table-layout.mjs +250 -0
  239. package/src/tui/paste-attachments.mjs +0 -7
  240. package/src/tui/spinner-verbs.mjs +1 -2
  241. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  242. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  243. package/src/tui/theme.mjs +746 -24
  244. package/src/tui/time-format.mjs +1 -1
  245. package/src/tui/transcript-tool-failures.mjs +67 -0
  246. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  247. package/src/ui/ansi.mjs +1 -2
  248. package/src/ui/markdown.mjs +85 -26
  249. package/src/ui/markdown.test.mjs +70 -0
  250. package/src/ui/model-display.mjs +121 -0
  251. package/src/ui/session-stats.mjs +44 -0
  252. package/src/ui/statusline-context-label.test.mjs +15 -0
  253. package/src/ui/statusline.mjs +386 -178
  254. package/src/ui/tool-card.mjs +2 -13
  255. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  256. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  257. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  258. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  259. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  260. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  261. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  262. package/src/workflows/default/WORKFLOW.md +12 -5
  263. package/src/workflows/default/workflow.json +0 -1
  264. package/src/workflows/solo/WORKFLOW.md +15 -0
  265. package/src/workflows/solo/workflow.json +7 -0
  266. package/vendor/ink/build/output.js +6 -1
  267. package/src/agents/scheduler-task.md +0 -3
  268. package/src/agents/web-researcher/AGENT.md +0 -3
  269. package/src/agents/web-researcher/agent.json +0 -6
  270. package/src/agents/webhook-handler.md +0 -3
  271. package/src/rules/bridge/00-common.md +0 -5
  272. package/src/rules/bridge/30-explorer.md +0 -4
  273. package/src/rules/lead/00-tool-lead.md +0 -5
  274. package/src/rules/shared/00-language.md +0 -3
  275. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  276. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  277. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  278. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  279. package/src/standalone/bridge-tool.mjs +0 -1414
  280. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -1,23 +1,23 @@
1
- /**
2
- * components/Picker.jsx — selectable list picker for slash commands.
3
- *
4
- * Renders a bordered, scrollable list of items with up/down navigation,
1
+ /**
2
+ * components/Picker.jsx — selectable list picker for slash commands.
3
+ *
4
+ * Renders a bordered, scrollable list of items with up/down navigation,
5
5
  * Enter confirms and Escape backs out. Used by /model and /resume to let the
6
- * user pick from available presets or saved sessions.
7
- *
8
- * Keyboard:
6
+ * user pick from available presets or saved sessions.
7
+ *
8
+ * Keyboard:
9
9
  * ↑ / ↓ — move selection (wraps at ends)
10
10
  * ← / → — optional picker-specific adjustment
11
11
  * Tab — optional picker-specific toggle
12
12
  * Enter — choose/apply the selected row
13
13
  * Escape — back/cancel
14
14
  * Ctrl+C — handled globally for selection copy
15
- */
15
+ */
16
16
  import React, { useState, useCallback, useEffect } from 'react';
17
17
  import { Box, Text, useInput } from 'ink';
18
18
  import stringWidth from 'string-width';
19
19
  import { theme } from '../theme.mjs';
20
-
20
+
21
21
  /** Max items visible at once before scrolling kicks in. */
22
22
  const MAX_VISIBLE = 8;
23
23
  const DEFAULT_LABEL_WIDTH = 28;
@@ -78,6 +78,8 @@ export function Picker({
78
78
  onLeft,
79
79
  onRight,
80
80
  onTab,
81
+ onKey,
82
+ onHighlight,
81
83
  title,
82
84
  description = '',
83
85
  footer = '',
@@ -85,10 +87,15 @@ export function Picker({
85
87
  columns = 80,
86
88
  labelWidth: labelWidthOverride = null,
87
89
  metaWidth: metaWidthOverride = null,
90
+ footerGapRows = 1,
88
91
  initialIndex = 0,
89
92
  indexMode = 'auto',
90
93
  fillHeight = false,
91
94
  visibleCount = MAX_VISIBLE,
95
+ // Memo-busting epoch: ItemRow is React.memo and reads theme.* directly, so a
96
+ // live /theme switch (or picker preview) must re-render every row. Threading
97
+ // the epoch into each ItemRow breaks its shallow-equality on a theme change.
98
+ themeEpoch = 0,
92
99
  }) {
93
100
  const visibleLimit = Math.max(1, Math.floor(Number(visibleCount) || MAX_VISIBLE));
94
101
  const [selectedIndex, setSelectedIndex] = useState(() => Math.max(0, Math.min(Number(initialIndex) || 0, Math.max(0, items.length - 1))));
@@ -101,9 +108,20 @@ export function Picker({
101
108
  setSelectedIndex(Math.max(0, Math.min(Number(initialIndex) || 0, Math.max(0, items.length - 1))));
102
109
  }, [initialIndex, items.length]);
103
110
 
111
+ // Live-preview hook: notify the owner whenever the highlighted row changes
112
+ // (arrow keys, paging, initial mount). The /theme picker uses this to apply a
113
+ // non-persisted palette preview as the selection moves. Kept side-effect-free
114
+ // for pickers that do not pass onHighlight.
115
+ useEffect(() => {
116
+ if (typeof onHighlight !== 'function') return;
117
+ const item = items[selectedIndex];
118
+ if (item) onHighlight(item.value, item, selectedIndex);
119
+ }, [onHighlight, items, selectedIndex]);
120
+
104
121
  const activeFooter = typeof footer === 'function' ? footer(items[selectedIndex], selectedIndex) : footer;
105
122
  const footerLines = normalizeFooterLines(activeFooter, columns);
106
- const footerReserveRows = footerLines.length > 0 ? footerLines.length + 1 : 0;
123
+ const footerGap = footerLines.length > 0 ? Math.max(0, Math.floor(Number(footerGapRows) || 0)) : 0;
124
+ const footerReserveRows = footerLines.length > 0 ? footerLines.length + footerGap : 0;
107
125
  const effectiveVisibleLimit = Math.max(1, visibleLimit - footerReserveRows);
108
126
  const helpText = help || (onLeft || onRight || onTab ? ADJUST_HELP : SELECT_HELP);
109
127
 
@@ -157,18 +175,25 @@ export function Picker({
157
175
  if (selected && onSelect) onSelect(selected.value, selected);
158
176
  return;
159
177
  }
160
- if (key.escape) {
161
- onCancel();
162
- return;
163
- }
164
- },
165
- [items, selectedIndex, onSelect, onCancel, onLeft, onRight, onTab, effectiveVisibleLimit],
178
+ if (key.escape) {
179
+ onCancel();
180
+ return;
181
+ }
182
+ if (key.ctrl && (input === 'c' || input === 'C')) {
183
+ return;
184
+ }
185
+ if (onKey) {
186
+ onKey(input, key, items[selectedIndex], selectedIndex);
187
+ return;
188
+ }
189
+ },
190
+ [items, selectedIndex, onSelect, onCancel, onLeft, onRight, onTab, onKey, effectiveVisibleLimit],
166
191
  ),
167
192
  );
168
-
169
- // Clamp selected index when items change length.
193
+
194
+ // Clamp selected index when items change length.
170
195
  if (items.length === 0) {
171
- const emptyDescription = truncateText(description, Math.max(0, columns - 4));
196
+ const emptyLine = truncateText(String(description || '').replace(/\s+/g, ' ').trim(), Math.max(0, columns - 4));
172
197
  return (
173
198
  <Box flexDirection="column" flexShrink={0} height={fillHeight ? '100%' : undefined}>
174
199
  <Box
@@ -179,27 +204,21 @@ export function Picker({
179
204
  height={fillHeight ? '100%' : undefined}
180
205
  width="100%"
181
206
  >
182
- <Box flexDirection="row" justifyContent="space-between" marginBottom={0}>
207
+ <Box flexDirection="row" justifyContent="space-between">
183
208
  <Text color={theme.panelTitle}>{title || 'Picker'}</Text>
184
209
  <Text color={theme.subtle}>{helpText}</Text>
185
210
  </Box>
186
- {emptyDescription ? (
187
- <>
188
- <Text> </Text>
189
- <Text color={theme.text}>{emptyDescription}</Text>
190
- <Text> </Text>
191
- </>
192
- ) : (
193
- <>
194
- <Text> </Text>
195
- <Text color={theme.inactive}>(empty)</Text>
196
- </>
197
- )}
211
+ {/* Standard rhythm: title, blank, description/hint (blank if none),
212
+ blank, then the (empty) content row. */}
213
+ <Text> </Text>
214
+ <Text color={theme.text}>{emptyLine || ' '}</Text>
215
+ <Text> </Text>
216
+ <Text color={theme.inactive}>(empty)</Text>
198
217
  </Box>
199
218
  </Box>
200
219
  );
201
220
  }
202
-
221
+
203
222
  // Scroll window centered on the selected item.
204
223
  const total = items.length;
205
224
  const half = Math.floor(effectiveVisibleLimit / 2);
@@ -220,10 +239,17 @@ export function Picker({
220
239
  // Keep the label column fixed across menus. Per-picker overrides are still
221
240
  // allowed for intentionally compact surfaces such as providers/resume.
222
241
  const labelWidth = clampLabelWidth(labelWidthOverride ?? DEFAULT_LABEL_WIDTH, columns);
223
- const hasMeta = metaWidthOverride != null || visible.some((item) => item.meta || item.modelProfile || item.metaParts);
242
+ const hasMarker = items.some((item) => item.marker || item.checked === true || item.checked === false);
243
+ const markerWidth = hasMarker ? 2 : 0;
244
+ const hasMeta = metaWidthOverride != null || items.some((item) => item.meta || item.modelProfile || item.metaParts);
224
245
  const metaWidth = hasMeta ? clampMetaWidth(metaWidthOverride, columns, labelWidth) : 0;
225
- const descriptionWidth = Math.max(0, columns - indexOffset - labelWidth - (hasMeta ? metaWidth + 14 : 12));
226
- const panelDescription = truncateText(description, Math.max(0, columns - 4));
246
+ const descriptionWidth = Math.max(0, columns - indexOffset - markerWidth - labelWidth - (hasMeta ? metaWidth + 14 : 12));
247
+ // Standard panel rhythm: title row, blank, description/hint row, blank,
248
+ // content. Description newlines are collapsed and width-truncated to a single
249
+ // line so a multi-line description (e.g. ToolApproval) cannot push the title
250
+ // off the top. The slot is always reserved so panel chrome is a constant 6
251
+ // rows (title + blank + desc + blank + 2 border), matching PICKER_CHROME_ROWS.
252
+ const panelDescription = truncateText(String(description || '').replace(/\s+/g, ' ').trim(), Math.max(0, columns - 4));
227
253
 
228
254
  return (
229
255
  <Box flexDirection="column" flexShrink={0} width="100%" height={fillHeight ? '100%' : undefined}>
@@ -235,26 +261,27 @@ export function Picker({
235
261
  width="100%"
236
262
  height={fillHeight ? '100%' : undefined}
237
263
  >
238
- <Box flexDirection="row" justifyContent="space-between" marginBottom={panelDescription ? 0 : 1}>
264
+ <Box flexDirection="row" justifyContent="space-between">
239
265
  <Text color={theme.panelTitle}>{title}</Text>
240
266
  <Text color={theme.subtle}>{helpText}</Text>
241
267
  </Box>
242
- {panelDescription ? (
243
- <>
244
- <Text> </Text>
245
- <Text color={theme.text}>{panelDescription}</Text>
246
- <Text> </Text>
247
- </>
248
- ) : null}
268
+ <Text> </Text>
269
+ <Text color={theme.text}>{panelDescription || ' '}</Text>
270
+ <Text> </Text>
249
271
  {visible.map((item, i) => {
250
- const idx = start + i;
251
- const isSelected = idx === selectedIndex;
252
- return (
253
- <ItemRow
272
+ const idx = start + i;
273
+ const isSelected = idx === selectedIndex;
274
+ return (
275
+ <ItemRow
254
276
  key={item.value}
255
277
  indexText={showIndex ? `${idx + 1}.` : ''}
256
278
  indexWidth={indexWidth}
279
+ marker={item.marker || (item.checked === true ? '✓' : item.checked === false ? ' ' : '')}
280
+ markerColor={item.markerColor}
281
+ markerWidth={markerWidth}
257
282
  label={item.label}
283
+ labelSuffix={item.labelSuffix}
284
+ labelSuffixColor={item.labelSuffixColor}
258
285
  meta={item.meta || item.modelProfile || ''}
259
286
  metaParts={item.metaParts}
260
287
  description={item.description}
@@ -263,6 +290,7 @@ export function Picker({
263
290
  descriptionWidth={descriptionWidth}
264
291
  showMeta={hasMeta}
265
292
  isSelected={isSelected}
293
+ themeEpoch={themeEpoch}
266
294
  />
267
295
  );
268
296
  })}
@@ -282,8 +310,14 @@ export function Picker({
282
310
  );
283
311
  }
284
312
 
285
- const ItemRow = React.memo(function ItemRow({ indexText, indexWidth, label, meta, metaParts, description, labelWidth, metaWidth, descriptionWidth, showMeta, isSelected }) {
286
- const displayLabel = truncateText(label, labelWidth);
313
+ const ItemRow = React.memo(function ItemRow({ indexText, indexWidth, marker, markerColor, markerWidth, label, labelSuffix, labelSuffixColor, meta, metaParts, description, labelWidth, metaWidth, descriptionWidth, showMeta, isSelected, themeEpoch = 0 }) {
314
+ const rawSuffix = String(labelSuffix || '');
315
+ const suffix = rawSuffix ? truncateText(rawSuffix, labelWidth) : '';
316
+ const suffixGap = suffix && stringWidth(suffix) < labelWidth ? ' ' : '';
317
+ const suffixWidth = suffix ? stringWidth(suffixGap) + stringWidth(suffix) : 0;
318
+ const displayMarker = truncateText(marker, markerWidth);
319
+ const displayLabel = truncateText(label, Math.max(0, labelWidth - suffixWidth));
320
+ const labelPadding = ' '.repeat(Math.max(0, labelWidth - stringWidth(displayLabel) - suffixWidth));
287
321
  const displayMeta = truncateText(meta, metaWidth);
288
322
  const displayDescription = truncateText(description, descriptionWidth);
289
323
  const parts = Array.isArray(metaParts) ? metaParts : null;
@@ -295,9 +329,14 @@ const ItemRow = React.memo(function ItemRow({ indexText, indexWidth, label, meta
295
329
  {padCells(indexText, indexWidth)}{' '}
296
330
  </Text>
297
331
  ) : null}
298
- <Text color={theme.text}>
299
- {padCells(displayLabel, labelWidth)}
300
- </Text>
332
+ {markerWidth > 0 ? (
333
+ <Text color={marker ? (markerColor || theme.success) : theme.text}>
334
+ {padCells(displayMarker, markerWidth)}
335
+ </Text>
336
+ ) : null}
337
+ <Text color={theme.text}>{displayLabel}</Text>
338
+ {suffix ? <Text color={labelSuffixColor || theme.success}>{suffixGap}{suffix}</Text> : null}
339
+ <Text color={theme.text}>{labelPadding}</Text>
301
340
  {showMeta ? (
302
341
  <Text color={theme.text}>
303
342
  {' '}
@@ -312,6 +351,6 @@ const ItemRow = React.memo(function ItemRow({ indexText, indexWidth, label, meta
312
351
  {displayDescription}
313
352
  </Text>
314
353
  ) : null}
315
- </Box>
316
- );
317
- });
354
+ </Box>
355
+ );
356
+ });