mixdog 0.8.0 → 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 (285) 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 +344 -0
  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 +1223 -95
  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 +2325 -450
  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 +118 -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 +77 -29
  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/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * components/MarkdownTable.jsx — GFM table → ink Box layout.
3
3
  *
4
- * Ported from Claude Code (refs/claude-code/src/components/MarkdownTable.tsx),
5
- * adapted for this CLI:
4
+ * GFM table layout for ink:
6
5
  * - <Ansi> → <Text> (ink 7 has no <Ansi>; <Text> passes ANSI escapes through).
7
6
  * - useTerminalSize() → ink useStdout().stdout.columns.
8
- * - stringWidth / wrapAnsi from the npm packages (same libs CC vendors).
7
+ * - stringWidth / wrapAnsi from npm packages.
9
8
  * - formatCell uses our format-token.formatToken (no highlight arg).
10
9
  *
11
10
  * The width-fitting algorithm (ideal vs min widths, proportional shrink, hard
@@ -13,29 +12,7 @@
13
12
  */
14
13
  import React from 'react';
15
14
  import { Text, useStdout } from 'ink';
16
- import stripAnsi from 'strip-ansi';
17
- import stringWidth from 'string-width';
18
- import wrapAnsi from 'wrap-ansi';
19
- import { formatToken, padAligned } from '../markdown/format-token.mjs';
20
-
21
- const SAFETY_MARGIN = 4;
22
- const MIN_COLUMN_WIDTH = 3;
23
- const MAX_ROW_LINES = 4;
24
- const ANSI_BOLD_START = '\x1b[1m';
25
- const ANSI_BOLD_END = '\x1b[22m';
26
-
27
- /** Wrap text to width, ANSI-aware, returning lines (CC wrapText). */
28
- function wrapText(text, width, options) {
29
- if (width <= 0) return [text];
30
- const trimmedText = String(text).trimEnd();
31
- const wrapped = wrapAnsi(trimmedText, width, {
32
- hard: options?.hard ?? false,
33
- trim: false,
34
- wordWrap: true,
35
- });
36
- const lines = wrapped.split('\n').filter((line) => line.length > 0);
37
- return lines.length > 0 ? lines : [''];
38
- }
15
+ import { buildTableRender } from '../markdown/table-layout.mjs';
39
16
 
40
17
  export function MarkdownTable({ token, forceWidth }) {
41
18
  // App owns resize reflow and there is no <Static> transcript anymore, so table
@@ -43,162 +20,10 @@ export function MarkdownTable({ token, forceWidth }) {
43
20
  const actualTerminalWidth = useStdout()?.stdout?.columns ?? 80;
44
21
  const terminalWidth = forceWidth ?? actualTerminalWidth;
45
22
 
46
- const formatCell = (tokens) => tokens?.map((t) => formatToken(t)).join('') ?? '';
47
- const getPlainText = (tokens) => stripAnsi(formatCell(tokens));
48
-
49
- const getMinWidth = (tokens) => {
50
- const text = getPlainText(tokens);
51
- const words = text.split(/\s+/).filter((w) => w.length > 0);
52
- if (words.length === 0) return MIN_COLUMN_WIDTH;
53
- return Math.max(...words.map((w) => stringWidth(w)), MIN_COLUMN_WIDTH);
54
- };
55
- const getIdealWidth = (tokens) =>
56
- Math.max(stringWidth(getPlainText(tokens)), MIN_COLUMN_WIDTH);
57
-
58
- // Step 1: min (longest word) and ideal (full content) widths per column.
59
- const minWidths = token.header.map((header, colIndex) => {
60
- let maxMinWidth = getMinWidth(header.tokens);
61
- for (const row of token.rows) maxMinWidth = Math.max(maxMinWidth, getMinWidth(row[colIndex]?.tokens));
62
- return maxMinWidth;
63
- });
64
- const idealWidths = token.header.map((header, colIndex) => {
65
- let maxIdeal = getIdealWidth(header.tokens);
66
- for (const row of token.rows) maxIdeal = Math.max(maxIdeal, getIdealWidth(row[colIndex]?.tokens));
67
- return maxIdeal;
68
- });
69
-
70
- // Step 2: available space.
71
- const numCols = token.header.length;
72
- const borderOverhead = 1 + numCols * 3;
73
- const availableWidth = Math.max(terminalWidth - borderOverhead - SAFETY_MARGIN, numCols * MIN_COLUMN_WIDTH);
74
-
75
- // Step 3: fit column widths into available space.
76
- const totalMin = minWidths.reduce((s, w) => s + w, 0);
77
- const totalIdeal = idealWidths.reduce((s, w) => s + w, 0);
78
- let needsHardWrap = false;
79
- let columnWidths;
80
- if (totalIdeal <= availableWidth) {
81
- columnWidths = idealWidths;
82
- } else if (totalMin <= availableWidth) {
83
- const extraSpace = availableWidth - totalMin;
84
- const overflows = idealWidths.map((ideal, i) => ideal - minWidths[i]);
85
- const totalOverflow = overflows.reduce((s, o) => s + o, 0);
86
- columnWidths = minWidths.map((min, i) => {
87
- if (totalOverflow === 0) return min;
88
- return min + Math.floor((overflows[i] / totalOverflow) * extraSpace);
89
- });
90
- } else {
91
- needsHardWrap = true;
92
- const scaleFactor = availableWidth / totalMin;
93
- columnWidths = minWidths.map((w) => Math.max(Math.floor(w * scaleFactor), MIN_COLUMN_WIDTH));
94
- }
95
-
96
- // Step 4: max row lines → decide vertical fallback.
97
- const calculateMaxRowLines = () => {
98
- let maxLines = 1;
99
- for (let i = 0; i < token.header.length; i++) {
100
- const wrapped = wrapText(formatCell(token.header[i].tokens), columnWidths[i], { hard: needsHardWrap });
101
- maxLines = Math.max(maxLines, wrapped.length);
102
- }
103
- for (const row of token.rows) {
104
- for (let i = 0; i < row.length; i++) {
105
- const wrapped = wrapText(formatCell(row[i]?.tokens), columnWidths[i], { hard: needsHardWrap });
106
- maxLines = Math.max(maxLines, wrapped.length);
107
- }
108
- }
109
- return maxLines;
110
- };
111
- const useVerticalFormat = calculateMaxRowLines() > MAX_ROW_LINES;
112
-
113
- const renderRowLines = (cells, isHeader) => {
114
- const cellLines = cells.map((cell, colIndex) =>
115
- wrapText(formatCell(cell.tokens), columnWidths[colIndex], { hard: needsHardWrap }),
116
- );
117
- const maxLines = Math.max(...cellLines.map((l) => l.length), 1);
118
- const verticalOffsets = cellLines.map((l) => Math.floor((maxLines - l.length) / 2));
119
- const result = [];
120
- for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
121
- let line = '│';
122
- for (let colIndex = 0; colIndex < cells.length; colIndex++) {
123
- const lines = cellLines[colIndex];
124
- const offset = verticalOffsets[colIndex];
125
- const contentLineIdx = lineIdx - offset;
126
- const lineText = contentLineIdx >= 0 && contentLineIdx < lines.length ? lines[contentLineIdx] : '';
127
- const width = columnWidths[colIndex];
128
- const align = isHeader ? 'center' : token.align?.[colIndex] ?? 'left';
129
- line += ' ' + padAligned(lineText, stringWidth(lineText), width, align) + ' │';
130
- }
131
- result.push(line);
132
- }
133
- return result;
134
- };
135
-
136
- const renderBorderLine = (type) => {
137
- const [left, mid, cross, right] = {
138
- top: ['┌', '─', '┬', '┐'],
139
- middle: ['├', '─', '┼', '┤'],
140
- bottom: ['└', '─', '┴', '┘'],
141
- }[type];
142
- let line = left;
143
- columnWidths.forEach((width, colIndex) => {
144
- line += mid.repeat(width + 2);
145
- line += colIndex < columnWidths.length - 1 ? cross : right;
146
- });
147
- return line;
148
- };
149
-
150
- const renderVerticalFormat = () => {
151
- const lines = [];
152
- const headers = token.header.map((h) => getPlainText(h.tokens));
153
- const separatorWidth = Math.min(terminalWidth - 1, 40);
154
- const separator = '─'.repeat(separatorWidth);
155
- const wrapIndent = ' ';
156
- token.rows.forEach((row, rowIndex) => {
157
- if (rowIndex > 0) lines.push(separator);
158
- row.forEach((cell, colIndex) => {
159
- const label = headers[colIndex] || `Column ${colIndex + 1}`;
160
- const rawValue = formatCell(cell.tokens).trimEnd();
161
- const value = rawValue.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim();
162
- const firstLineWidth = terminalWidth - stringWidth(label) - 3;
163
- const subsequentLineWidth = terminalWidth - wrapIndent.length - 1;
164
- const firstPassLines = wrapText(value, Math.max(firstLineWidth, 10));
165
- const firstLine = firstPassLines[0] || '';
166
- let wrappedValue;
167
- if (firstPassLines.length <= 1 || subsequentLineWidth <= firstLineWidth) {
168
- wrappedValue = firstPassLines;
169
- } else {
170
- const remainingText = firstPassLines.slice(1).map((l) => l.trim()).join(' ');
171
- wrappedValue = [firstLine, ...wrapText(remainingText, subsequentLineWidth)];
172
- }
173
- lines.push(`${ANSI_BOLD_START}${label}:${ANSI_BOLD_END} ${wrappedValue[0] || ''}`);
174
- for (let i = 1; i < wrappedValue.length; i++) {
175
- if (!wrappedValue[i].trim()) continue;
176
- lines.push(`${wrapIndent}${wrappedValue[i]}`);
177
- }
178
- });
179
- });
180
- return lines.join('\n');
181
- };
182
-
183
- if (useVerticalFormat) {
184
- return <Text>{renderVerticalFormat()}</Text>;
185
- }
186
-
187
- const tableLines = [];
188
- tableLines.push(renderBorderLine('top'));
189
- tableLines.push(...renderRowLines(token.header, true));
190
- tableLines.push(renderBorderLine('middle'));
191
- token.rows.forEach((row, rowIndex) => {
192
- tableLines.push(...renderRowLines(row, false));
193
- if (rowIndex < token.rows.length - 1) tableLines.push(renderBorderLine('middle'));
194
- });
195
- tableLines.push(renderBorderLine('bottom'));
196
-
197
- // Safety: if any line would overflow (resize race), fall back to vertical.
198
- const maxLineWidth = Math.max(...tableLines.map((l) => stringWidth(stripAnsi(l))));
199
- if (maxLineWidth > terminalWidth - SAFETY_MARGIN) {
200
- return <Text>{renderVerticalFormat()}</Text>;
201
- }
202
-
203
- return <Text>{tableLines.join('\n')}</Text>;
23
+ // The entire layout (column fit, vertical fallback, bordered box, overflow
24
+ // re-fallback) lives in the pure markdown/table-layout.mjs module so the
25
+ // renderer and the App.jsx row-height estimator share one source of truth and
26
+ // can never drift. The component just draws the lines it returns verbatim.
27
+ const { lines } = buildTableRender(token, terminalWidth);
28
+ return <Text>{lines.join('\n')}</Text>;
204
29
  }
@@ -1,42 +1,59 @@
1
1
  /**
2
2
  * components/Message.jsx — transcript message rows.
3
3
  *
4
- * Ported from Claude Code:
5
- * - AssistantTextMessage.tsx: `● <markdown>` where the dot is `BLACK_CIRCLE`
6
- * in the `text` color (white, NOT claude-orange), in a 2-wide gutter, and
4
+ * Transcript message rows:
5
+ * - Assistant: `● <markdown>` where the dot is `BLACK_CIRCLE`
6
+ * in the `text` color (not the orange title accent), in a 2-wide gutter, and
7
7
  * the markdown sits in a column box beside it (flexDirection row).
8
- * - UserPromptMessage.tsx: text on a `userMessageBackground` background with
9
- * `paddingRight={1}` (no prompt glyph — CC uses the bg to distinguish input
8
+ * - User: text on a `userMessageBackground` background with
9
+ * `paddingRight={1}` (no prompt glyph — the bg distinguishes input
10
10
  * from assistant/tool rows).
11
11
  *
12
12
  * The dot uses `minWidth={2}` so wrapped markdown lines align under the text,
13
- * not under the dot (CC's NoSelect minWidth={2} behavior).
13
+ * not under the dot (2-wide gutter alignment).
14
14
  */
15
15
  import React from 'react';
16
16
  import { Box, Text, useAnimation } from 'ink';
17
17
  import { theme, TURN_MARKER } from '../theme.mjs';
18
18
  import { Markdown, StreamingMarkdown } from './Markdown.jsx';
19
+ import { assistantBodyWidth } from '../markdown/table-layout.mjs';
19
20
  import { THEREFORE } from '../figures.mjs';
20
21
  import { formatDuration } from '../time-format.mjs';
21
22
 
22
- export const AssistantMessage = React.memo(function AssistantMessage({ text, streaming = false }) {
23
+ // `themeEpoch` is a memo-busting prop (threaded from App): the active theme
24
+ // mutates `theme` in-place, so a /theme switch must re-render this memoized row
25
+ // and recompute its markdown colors. It is forwarded into Markdown so the
26
+ // token/AnsiText caches include it as a dep.
27
+ export const AssistantMessage = React.memo(function AssistantMessage({ text, streaming = false, columns = 80, themeEpoch = 0 }) {
28
+ // The body column needs an EXPLICIT numeric width. Without it, ink/Yoga
29
+ // measures the wrapped markdown body before the row's available width is
30
+ // resolved and caches its height as a single row — so a multi-line assistant
31
+ // message renders only its LAST wrapped line (the head lines are clipped) and
32
+ // streaming height jitters as the measure flips. Reserve the 2-col gutter
33
+ // (BLACK CIRCLE in minWidth={2}) plus one right-edge safety cell. Letting an
34
+ // assistant line reach the terminal's last column can auto-wrap/scroll on
35
+ // Windows Terminal/conhost and make the next redraw appear to lose leading
36
+ // CJK characters even though the backing transcript is intact.
37
+ const bodyWidth = assistantBodyWidth(columns);
23
38
  return (
24
39
  <Box flexDirection="row" marginTop={1}>
25
40
  <Box flexShrink={0} minWidth={2}>
26
41
  <Text color={theme.text}>{TURN_MARKER}</Text>
27
42
  </Box>
28
- <Box flexDirection="column" flexGrow={1}>
29
- {streaming ? <StreamingMarkdown>{text}</StreamingMarkdown> : <Markdown>{text}</Markdown>}
43
+ <Box flexDirection="column" flexShrink={0} width={bodyWidth}>
44
+ {streaming
45
+ ? <StreamingMarkdown themeEpoch={themeEpoch} columns={bodyWidth}>{text}</StreamingMarkdown>
46
+ : <Markdown themeEpoch={themeEpoch} columns={bodyWidth}>{text}</Markdown>}
30
47
  </Box>
31
48
  </Box>
32
49
  );
33
50
  });
34
51
 
35
- export const UserMessage = React.memo(function UserMessage({ text, attached = false, columns }) {
52
+ export const UserMessage = React.memo(function UserMessage({ text, attached = false, columns, themeEpoch = 0 }) {
36
53
  // `attached` = the previous transcript row is also a user message (consecutive
37
54
  // steering prompts). Those stack flush together; a user message that follows
38
55
  // an assistant/tool row gets a one-row gap above it.
39
- // The background band fills the row edge-to-edge (CC's user bubble reads as a
56
+ // The background band fills the row edge-to-edge (user bubble reads as a
40
57
  // full-width band, not a text-hugging tag). An explicit numeric width is more
41
58
  // robust than "100%" here — it guarantees the band even if a parent's width
42
59
  // context is ambiguous. paddingLeft aligns the body under the 2-col gutter.
@@ -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
+ });