mixdog 0.8.1 → 0.9.1

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 (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  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 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Pinned into the transcript right after a turn's output (an `item.kind ===
8
8
  * 'turndone'` entry), so it scrolls up with the answer and stays in the
9
- * scrollback mirroring Claude Code's post-think summary line. It leaves a
9
+ * scrollback as a post-think summary line. It leaves a
10
10
  * quiet, dim record of how long the turn took next to the answer it belongs to.
11
11
  */
12
12
  import React from 'react';
@@ -45,7 +45,7 @@ export function TurnDone({ elapsedMs = 0, status = 'done', verb = 'Thought', rig
45
45
  </Text>
46
46
  </Box>
47
47
  {rightText ? (
48
- <Box flexShrink={0} width={rightWidth} marginLeft={1} justifyContent="flex-end" overflow="hidden">
48
+ <Box flexShrink={0} width={rightWidth} marginLeft={1} marginRight={1} justifyContent="flex-end" overflow="hidden">
49
49
  <Text color={statusMessageColor(rightTone)} wrap="truncate">{rightText}</Text>
50
50
  </Box>
51
51
  ) : null}
@@ -69,7 +69,7 @@ export function StatusDone({ label = 'Complete', detail = '', rightMessage = '',
69
69
  </Text>
70
70
  </Box>
71
71
  {rightText ? (
72
- <Box flexShrink={0} width={rightWidth} marginLeft={1} justifyContent="flex-end" overflow="hidden">
72
+ <Box flexShrink={0} width={rightWidth} marginLeft={1} marginRight={1} justifyContent="flex-end" overflow="hidden">
73
73
  <Text color={statusMessageColor(rightTone)} wrap="truncate">{rightText}</Text>
74
74
  </Box>
75
75
  ) : null}
@@ -148,10 +148,11 @@ function windowSegmentParts(w) {
148
148
  }
149
149
 
150
150
  function creditSegmentParts(value) {
151
+ const creditColor = Number(value) === 0 ? theme.text : remainingUsdColor(value);
151
152
  return [
152
153
  { text: CREDIT_LABEL, color: theme.subtle },
153
154
  { text: ' ', color: theme.inactive },
154
- { text: money(value), color: remainingUsdColor(value) },
155
+ { text: money(value), color: creditColor },
155
156
  ];
156
157
  }
157
158
 
@@ -185,13 +186,10 @@ function rowStatusParts(row, columns = 80, statusWidth = 0) {
185
186
  case 'checking':
186
187
  return [{ text: 'checking...', color: theme.inactive }];
187
188
  case 'hidden':
188
- return row?.detail ? [{ text: row.detail, color: theme.inactive }] : [];
189
189
  case 'missing':
190
- return row?.primary && row.primary !== 'not configured' ? [{ text: row.primary, color: theme.inactive }] : [];
191
190
  case 'local':
192
- return row?.primary ? [{ text: row.primary, color: theme.inactive }] : [];
193
191
  case 'error':
194
- return row?.primary ? [{ text: row.primary, color: theme.error }] : [];
192
+ return [];
195
193
  default:
196
194
  return row?.primary ? [{ text: row.primary, color: theme.text }] : [];
197
195
  }
@@ -0,0 +1,499 @@
1
+ /**
2
+ * components/tool-output-format.mjs — shared post-processing for EXPANDED tool
3
+ * result bodies (ctrl+o). Mirrors the Claude Code output pipeline so every tool
4
+ * surface renders the same way instead of a flat run of plain lines:
5
+ *
6
+ * - language inference from the read/grep path argument (file extension)
7
+ * - JSON auto pretty-print (precision-safe, size-capped)
8
+ * - URL → OSC 8 hyperlink
9
+ * - stray underline-ANSI removal (shell output leaks these)
10
+ * - oversized-output guard (never wrap/scan a 64 MB dump line-by-line)
11
+ * - per-line render: split the `<n>→` / `<file>:<n>:` line-number gutter into a
12
+ * dim column, then syntax-highlight the body with the shared markdown
13
+ * code-block highlighter so colors track the active theme.
14
+ *
15
+ * This module returns ANSI STRINGS (one per logical line). ToolExecution
16
+ * runs `wrapExpandedResultLines` so each physical terminal row maps 1:1 to the
17
+ * left result rail before ink draws the body (no ink `wrap` on expanded rows).
18
+ */
19
+ import stringWidth from 'string-width';
20
+ import stripAnsi from 'strip-ansi';
21
+ import {
22
+ extraColorizers,
23
+ highlightCodeLine,
24
+ highlightCodeBlockToLines,
25
+ colorizeDiffLine,
26
+ looksLikeUnifiedDiff,
27
+ LANG_FAMILY,
28
+ } from '../markdown/format-token.mjs';
29
+ import { wrapText } from '../markdown/table-layout.mjs';
30
+ import { RESULT_GUTTER } from '../theme.mjs';
31
+ import { hasMarkdownSyntax, renderTokenAnsiSegments } from '../markdown/render-ansi.mjs';
32
+ import { buildTableRender } from '../markdown/table-layout.mjs';
33
+
34
+ const DEFAULT_MARKDOWN_WIDTH = 80;
35
+
36
+ // Hard ceilings so a pathological tool result can never lock the render loop.
37
+ // CC uses ~MAX_LINES*width*4 for the collapsed fold; for the EXPANDED body we
38
+ // cap total characters processed and total lines kept, with an explicit marker.
39
+ export const MAX_EXPANDED_CHARS = 256 * 1024; // 256 KB of text gets per-line processing
40
+ const MAX_EXPANDED_LINES = 4000; // logical-line safety ceiling; physical mount cap is separate
41
+ const MAX_JSON_FORMAT_LENGTH = 10_000; // mirror CC's tryJsonFormatContent cap
42
+ const MAX_HIGHLIGHT_LINE_CHARS = 2000; // skip token-scan on absurdly long lines
43
+ // Lockstep with ToolExecution collapsed fit budget (MIN_RESULT_LINE_CHARS).
44
+ const MIN_EXPANDED_BODY_COLS = 24;
45
+
46
+ // Hard cap on the number of PHYSICAL (wrapped) rows a single expanded tool /
47
+ // script body may mount. MAX_EXPANDED_LINES bounds LOGICAL lines, but a single
48
+ // very long line with no newlines (minified JSON, a 256 KB single-line blob)
49
+ // wraps into thousands of physical rows — and EACH physical row is a mounted
50
+ // Ink <Text> node that ink re-serializes every frame even while clipped off-
51
+ // screen (clipping only trims write coords, not the serialize pass). So a lone
52
+ // huge expanded item could mount thousands of nodes and stall typing/drag even
53
+ // though it is the only visible transcript item. This cap bounds the mounted
54
+ // node count regardless of line shape; the omitted tail gets a clear marker.
55
+ // It is set generously above what any viewport needs so normal multi-line
56
+ // output (already capped at MAX_EXPANDED_LINES logical lines) is byte-for-byte
57
+ // unchanged — only pathological wide/unwrapped blobs are trimmed. Env-tunable
58
+ // via MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES (0 disables); legacy
59
+ // MIXDOG_TUI_EXPANDED_MAX_ROWS is still honored when the primary var is unset.
60
+ export function resolveToolOutputMaxRenderLines() {
61
+ const raw = process.env.MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES;
62
+ if (raw !== undefined && String(raw).trim() !== '') {
63
+ const v = Number(raw);
64
+ if (Number.isFinite(v) && v <= 0) return 0;
65
+ if (Number.isFinite(v) && v > 0) return Math.floor(v);
66
+ }
67
+ const legacy = process.env.MIXDOG_TUI_EXPANDED_MAX_ROWS;
68
+ if (legacy !== undefined && String(legacy).trim() !== '') {
69
+ const v = Number(legacy);
70
+ if (Number.isFinite(v) && v > 0) return Math.floor(v);
71
+ }
72
+ return 600;
73
+ }
74
+
75
+ function omittedPhysicalRowsMarker(omitted, isShell) {
76
+ if (isShell) {
77
+ const n = Math.max(1, Math.floor(Number(omitted) || 0));
78
+ return `\u2026 [${n} line${n === 1 ? '' : 's'} omitted above \u2014 showing newest output below]`;
79
+ }
80
+ return '\u2026 [output truncated for display \u2014 collapse (ctrl+o) or re-read a narrower range]';
81
+ }
82
+
83
+ function shellLogicalOmittedMarker({ omittedLines = 0, omittedChars = false }, c) {
84
+ if (omittedLines > 0) {
85
+ return c.synComment(omittedPhysicalRowsMarker(omittedLines, true));
86
+ }
87
+ if (omittedChars) {
88
+ return c.synComment('\u2026 [earlier output omitted above \u2014 showing newest output below]');
89
+ }
90
+ return null;
91
+ }
92
+
93
+ function finalizeShellPhysicalCap(buffer, omitted, maxRows) {
94
+ const bodySlots = Math.max(0, maxRows - 1);
95
+ if (omitted <= 0) {
96
+ return buffer.length > 0 ? buffer : [' '];
97
+ }
98
+ const tail = bodySlots > 0 ? buffer.slice(-bodySlots) : [];
99
+ const extraHidden = Math.max(0, buffer.length - bodySlots);
100
+ const totalOmitted = omitted + extraHidden;
101
+ const out = [omittedPhysicalRowsMarker(totalOmitted, true), ...tail];
102
+ return out.length > 0 ? out.slice(0, maxRows) : [' '];
103
+ }
104
+
105
+ // `<n>→<content>` (read) OR `<n>:<content>` / `<path>:<n>:<content>` (grep).
106
+ const READ_LINE_RE = /^(\s*)(\d+)(\u2192)(.*)$/;
107
+ // Gutter ends with `:<line>:`. Windows absolute paths use `X:\...` so the drive
108
+ // colon must not terminate the path prefix (unlike the old `[^:\n]*:` rule).
109
+ const GREP_LINE_RE = /^(\s*)((?:[A-Za-z]:[\\/](?:[^\n:])*|[^\n:]*):\d+:|\d+:)(\s?)(.*)$/;
110
+
111
+ // http(s) URLs not wrapped in quotes/brackets/whitespace (conservative).
112
+ const URL_RE = /https?:\/\/[^\s"'<>\x1b\\)\]]+/g;
113
+ // CSI SGR and OSC sequences (BEL- or ST-terminated) — linkify plain text only.
114
+ // eslint-disable-next-line no-control-regex
115
+ const ANSI_ESCAPE_RE = /\x1b(?:\[[0-9;]*m|\][\s\S]*?(?:\x07|\x1b\\))/g;
116
+
117
+
118
+ /** Infer a highlighter family from a read/grep path arg's extension. */
119
+ export function inferLangFamily(pathArg) {
120
+ const p = String(pathArg || '').trim().toLowerCase();
121
+ if (!p) return null;
122
+ const m = /\.([a-z0-9]+)$/.exec(p);
123
+ if (!m) return null;
124
+ return LANG_FAMILY[m[1]] || null;
125
+ }
126
+
127
+ /** Strip ONLY underline SGR (4 / 24) — other styles/colors are preserved. */
128
+ export function stripUnderlineAnsi(text) {
129
+ return String(text ?? '').replace(
130
+ // eslint-disable-next-line no-control-regex
131
+ /\x1b\[([0-9;]*)m/g,
132
+ (seq, params) => {
133
+ if (!params) return seq;
134
+ const kept = params.split(';').filter((p) => p !== '' && p !== '4' && p !== '24');
135
+ if (kept.length === 0) return '';
136
+ return `\x1b[${kept.join(';')}m`;
137
+ },
138
+ );
139
+ }
140
+
141
+ /** Wrap bare URLs in OSC 8 hyperlinks (terminals that ignore it show the URL). */
142
+ export function linkifyUrls(text) {
143
+ const src = String(text ?? '');
144
+ if (!/https?:\/\//.test(src)) return src;
145
+ const chunks = [];
146
+ let last = 0;
147
+ for (const match of src.matchAll(ANSI_ESCAPE_RE)) {
148
+ if (match.index > last) chunks.push(src.slice(last, match.index));
149
+ chunks.push(match[0]);
150
+ last = match.index + match[0].length;
151
+ }
152
+ if (last < src.length) chunks.push(src.slice(last));
153
+ return chunks
154
+ .map((part) => (part.startsWith('\x1b')
155
+ ? part
156
+ : part.replace(URL_RE, (url) => `\x1b]8;;${url}\x07${url}\x1b]8;;\x07`)))
157
+ .join('');
158
+ }
159
+
160
+ /** Precision-safe JSON pretty-print for a single line; returns input on miss. */
161
+ function tryFormatJsonLine(line) {
162
+ const t = String(line ?? '').trim();
163
+ if (!t || (t[0] !== '{' && t[0] !== '[')) return line;
164
+ try {
165
+ const parsed = JSON.parse(t);
166
+ if (parsed === null || typeof parsed !== 'object') return line;
167
+ const pretty = JSON.stringify(parsed, null, 2);
168
+ // Bail if a big-int lost precision on the round-trip.
169
+ const a = t.replace(/\\\//g, '/').replace(/\s+/g, '');
170
+ const b = JSON.stringify(parsed).replace(/\s+/g, '');
171
+ return a === b ? pretty : line;
172
+ } catch {
173
+ return line;
174
+ }
175
+ }
176
+
177
+ /** Auto pretty-print whole-result JSON (size-capped, per-line). */
178
+ export function tryFormatJson(text) {
179
+ const src = String(text ?? '');
180
+ if (src.length > MAX_JSON_FORMAT_LENGTH) return src;
181
+ return src.split('\n').map(tryFormatJsonLine).join('\n');
182
+ }
183
+
184
+ /** True when the whole body is a JSON object/array (not markdown prose). */
185
+ function isJsonDocument(text) {
186
+ const t = String(text ?? '').trim();
187
+ if (!t || (t[0] !== '{' && t[0] !== '[')) return false;
188
+ try {
189
+ const parsed = JSON.parse(t);
190
+ return parsed !== null && typeof parsed === 'object';
191
+ } catch {
192
+ return false;
193
+ }
194
+ }
195
+
196
+ /** True when text already carries SGR escapes (e.g. shell color output). */
197
+ function hasAnsi(text) {
198
+ return /\x1b\[/.test(String(text ?? ''));
199
+ }
200
+
201
+ /** True when a line carries a read/grep tool line-number gutter. */
202
+ function lineHasToolGutter(line) {
203
+ return READ_LINE_RE.test(line) || GREP_LINE_RE.test(line);
204
+ }
205
+
206
+ /** Read/grep bodies are source lines — never whole-document markdown. */
207
+ function contentHasToolGutters(lines) {
208
+ for (const line of lines) {
209
+ if (lineHasToolGutter(line)) return true;
210
+ }
211
+ return false;
212
+ }
213
+
214
+ /**
215
+ * Whole-result markdown render for prose-y tool output (headings, emphasis,
216
+ * fences). Skipped for shell/ANSI/diff, read/grep gutters, and non-md path
217
+ * language inference (those stay per-line syntax highlight).
218
+ */
219
+ function shouldRenderExpandedMarkdown({ src, lines, carriesAnsi, isShell, diffMode, family }) {
220
+ if (carriesAnsi || isShell || diffMode) return false;
221
+ if (isJsonDocument(src)) return false;
222
+ if (contentHasToolGutters(lines)) return false;
223
+ if (family && family !== 'md') return false;
224
+ if (family === 'md') return true;
225
+ return hasMarkdownSyntax(src);
226
+ }
227
+
228
+ /** Markdown lexer → one ANSI string per terminal row (tables via table-layout). */
229
+ function formatExpandedMarkdownLines(src, { width = DEFAULT_MARKDOWN_WIDTH } = {}) {
230
+ const segments = renderTokenAnsiSegments(src, { width });
231
+ const out = [];
232
+ for (const seg of segments) {
233
+ if (seg.type === 'table') {
234
+ const { lines } = buildTableRender(seg.token, width);
235
+ for (const line of lines) out.push(linkifyUrls(line));
236
+ continue;
237
+ }
238
+ if (seg.type !== 'ansi' || !seg.ansi) continue;
239
+ for (const line of String(seg.ansi).split('\n')) {
240
+ out.push(linkifyUrls(line));
241
+ }
242
+ }
243
+ return out;
244
+ }
245
+
246
+ /**
247
+ * Process an expanded tool-result body into an array of ANSI line strings.
248
+ *
249
+ * @param {string} text raw tool result
250
+ * @param {object} opts
251
+ * @param {string} [opts.pathArg] read/grep path for language inference
252
+ * @param {boolean} [opts.isShell] shell surface (preserve existing ANSI, no highlight)
253
+ * @returns {string[]} one ANSI string per visible line
254
+ */
255
+ export function formatExpandedResult(text, { pathArg = '', isShell = false } = {}) {
256
+ let src = String(text ?? '');
257
+ if (!src) return [];
258
+
259
+ // Oversized guard: slice before any O(n) per-line work, append a marker.
260
+ let truncatedChars = false;
261
+ let shellOmittedLines = 0;
262
+ let shellOmittedChars = false;
263
+ if (src.length > MAX_EXPANDED_CHARS) {
264
+ src = isShell ? src.slice(-MAX_EXPANDED_CHARS) : src.slice(0, MAX_EXPANDED_CHARS);
265
+ if (isShell) shellOmittedChars = true;
266
+ else truncatedChars = true;
267
+ }
268
+
269
+ const carriesAnsi = hasAnsi(src);
270
+ // JSON pretty only when the text is not already colored (don't reflow ANSI).
271
+ if (!carriesAnsi) src = tryFormatJson(src);
272
+
273
+ let lines = src.split('\n');
274
+ let truncatedLines = false;
275
+ if (lines.length > MAX_EXPANDED_LINES) {
276
+ if (isShell) shellOmittedLines = lines.length - MAX_EXPANDED_LINES;
277
+ lines = isShell ? lines.slice(-MAX_EXPANDED_LINES) : lines.slice(0, MAX_EXPANDED_LINES);
278
+ if (!isShell) truncatedLines = true;
279
+ }
280
+
281
+ const c = extraColorizers();
282
+ // Diff bodies get diff coloring regardless of file family.
283
+ const diffMode = !carriesAnsi && !isShell && looksLikeUnifiedDiff(src);
284
+ const family = isShell || carriesAnsi ? null : inferLangFamily(pathArg);
285
+
286
+ let out;
287
+ if (shouldRenderExpandedMarkdown({ src, lines, carriesAnsi, isShell, diffMode, family })) {
288
+ out = formatExpandedMarkdownLines(src);
289
+ if (out.length > MAX_EXPANDED_LINES) {
290
+ if (isShell) shellOmittedLines += out.length - MAX_EXPANDED_LINES;
291
+ out = isShell ? out.slice(-MAX_EXPANDED_LINES) : out.slice(0, MAX_EXPANDED_LINES);
292
+ if (!isShell) truncatedLines = true;
293
+ }
294
+ } else {
295
+ if (family && family !== 'md' && !diffMode && !carriesAnsi && !isShell) {
296
+ out = formatSyntaxBlock(lines, { c, family });
297
+ } else {
298
+ out = lines.map((line) => formatLine(line, { c, family, diffMode, carriesAnsi, isShell }));
299
+ }
300
+ }
301
+
302
+ if (isShell) {
303
+ const marker = shellLogicalOmittedMarker({ omittedLines: shellOmittedLines, omittedChars: shellOmittedChars }, c);
304
+ if (marker) return [marker, ...out];
305
+ return out;
306
+ }
307
+ if (truncatedChars || truncatedLines) {
308
+ out.push(c.synComment('… [output truncated for display — re-read a narrower range]'));
309
+ }
310
+ return out;
311
+ }
312
+
313
+ /** Split read/grep line-number gutter from body (shared by per-line and block highlight). */
314
+ function splitGutter(line) {
315
+ let indent = '';
316
+ let gutter = '';
317
+ let body = line;
318
+ const rm = READ_LINE_RE.exec(line);
319
+ if (rm) {
320
+ indent = rm[1];
321
+ gutter = `${rm[2]}${rm[3]}`;
322
+ body = rm[4];
323
+ } else {
324
+ const gm = GREP_LINE_RE.exec(line);
325
+ if (gm) {
326
+ indent = gm[1];
327
+ gutter = `${gm[2]}${gm[3]}`;
328
+ body = gm[4];
329
+ }
330
+ }
331
+ return { indent, gutter, body };
332
+ }
333
+
334
+ /** Block syntax highlight for non-diff code bodies (multi-line tokens color correctly). */
335
+ function formatSyntaxBlock(lines, { c, family }) {
336
+ const parts = lines.map(splitGutter);
337
+ const bodies = parts.map((p) =>
338
+ p.body.length > MAX_HIGHLIGHT_LINE_CHARS ? '' : p.body,
339
+ );
340
+ const highlighted = highlightCodeBlockToLines(bodies.join('\n'), family, c);
341
+ return parts.map((p, i) => {
342
+ const rawBody = p.body;
343
+ const bodyOut = rawBody.length > MAX_HIGHLIGHT_LINE_CHARS
344
+ ? c.body(rawBody)
345
+ : (highlighted[i] ?? '');
346
+ const linked = linkifyUrls(bodyOut);
347
+ return p.gutter ? `${p.indent}${c.synComment(p.gutter)}${linked}` : linked;
348
+ });
349
+ }
350
+
351
+ /** Format ONE line: split line-number gutter, then highlight/linkify the body. */
352
+ function formatLine(line, { c, family, diffMode, carriesAnsi, isShell }) {
353
+ // Shell / already-colored output: keep ANSI verbatim, only fix underline leaks
354
+ // and linkify URLs. No gutter split (shell has no <n>→ prefix).
355
+ if (carriesAnsi || isShell) {
356
+ return linkifyUrls(stripUnderlineAnsi(line));
357
+ }
358
+
359
+ const { indent, gutter, body } = splitGutter(line);
360
+
361
+ const coloredBody = highlightBody(body, { c, family, diffMode });
362
+ const linked = linkifyUrls(coloredBody);
363
+ return gutter ? `${indent}${c.synComment(gutter)}${linked}` : linked;
364
+ }
365
+
366
+ /** Color a body string per the active mode (diff / language / plain). */
367
+ function highlightBody(body, { c, family, diffMode }) {
368
+ if (!body) return '';
369
+ if (body.length > MAX_HIGHLIGHT_LINE_CHARS) return c.body(body);
370
+ if (diffMode) return colorizeDiffLine(body, c);
371
+ if (family && family !== 'md') return highlightCodeLine(body, family, c);
372
+ return c.body(body);
373
+ }
374
+
375
+ /** Body text width for expanded tool results (terminal cols minus the ⎿ rail). */
376
+ export function expandedResultBodyWidth(columns = 80) {
377
+ const cols = Math.max(1, Number(columns) || 80);
378
+ const budget = cols - stringWidth(RESULT_GUTTER);
379
+ return Math.max(MIN_EXPANDED_BODY_COLS, budget);
380
+ }
381
+
382
+ function padDisplaySpaces(width) {
383
+ const w = Math.max(0, Math.floor(Number(width) || 0));
384
+ return w ? ' '.repeat(w) : '';
385
+ }
386
+
387
+ /** Split an ANSI string after `plainTarget` display columns of visible text. */
388
+ function splitAnsiByPlainWidth(text, plainTarget) {
389
+ const src = String(text ?? '');
390
+ const target = Math.max(0, Math.floor(Number(plainTarget) || 0));
391
+ if (!src) return ['', ''];
392
+ if (target <= 0) return ['', src];
393
+ let i = 0;
394
+ let plain = 0;
395
+ while (i < src.length) {
396
+ if (plain >= target) break;
397
+ if (src[i] === '\x1b') {
398
+ const rest = src.slice(i);
399
+ const sgr = rest.match(/^\x1b\[[0-9;]*m/);
400
+ if (sgr) {
401
+ i += sgr[0].length;
402
+ continue;
403
+ }
404
+ const osc = rest.match(/^\x1b\]8;;[^\x07]*\x07/);
405
+ if (osc) {
406
+ i += osc[0].length;
407
+ continue;
408
+ }
409
+ }
410
+ const cp = src.codePointAt(i);
411
+ const ch = String.fromCodePoint(cp);
412
+ plain += stringWidth(ch);
413
+ i += cp > 0xffff ? 2 : 1;
414
+ }
415
+ return [src.slice(0, i), src.slice(i)];
416
+ }
417
+
418
+ function leadingPrefixPlainWidth(plainLine) {
419
+ const rm = READ_LINE_RE.exec(plainLine);
420
+ if (rm) return stringWidth(rm[1] + rm[2] + rm[3]);
421
+ const gm = GREP_LINE_RE.exec(plainLine);
422
+ if (gm) return stringWidth(gm[1] + gm[2] + gm[3]);
423
+ return 0;
424
+ }
425
+
426
+ function wrapOneExpandedLogicalLine(line, maxWidth) {
427
+ const src = String(line ?? '');
428
+ if (!src) return [' '];
429
+ if (stringWidth(src) <= maxWidth) return [src];
430
+
431
+ const prefixPlainW = leadingPrefixPlainWidth(stripAnsi(src));
432
+ const [prefix, body] = prefixPlainW > 0
433
+ ? splitAnsiByPlainWidth(src, prefixPlainW)
434
+ : ['', src];
435
+ const prefixW = stringWidth(prefix);
436
+ const bodyBudget = Math.max(1, maxWidth - prefixW);
437
+ const bodyPieces = wrapText(body, bodyBudget, { hard: true });
438
+ if (bodyPieces.length <= 1) return [src];
439
+
440
+ const out = [];
441
+ for (let i = 0; i < bodyPieces.length; i++) {
442
+ if (i === 0) out.push(`${prefix}${bodyPieces[i]}`);
443
+ else out.push(`${padDisplaySpaces(prefixW)}${bodyPieces[i]}`);
444
+ }
445
+ return out;
446
+ }
447
+
448
+ /**
449
+ * Turn logical expanded lines into physical rows that fit the body column.
450
+ * One output row per left-rail row in ToolExecution (lockstep with App row est.).
451
+ */
452
+ export function wrapExpandedResultLines(logicalLines, columns = 80, { isShell = false } = {}) {
453
+ const maxRows = resolveToolOutputMaxRenderLines();
454
+ const capOn = maxRows > 0;
455
+ const maxWidth = expandedResultBodyWidth(columns);
456
+ const lines = Array.isArray(logicalLines) ? logicalLines : [];
457
+ const out = [];
458
+
459
+ if (!capOn) {
460
+ for (const line of lines) {
461
+ for (const row of wrapOneExpandedLogicalLine(line, maxWidth)) out.push(row);
462
+ }
463
+ return out.length > 0 ? out : [' '];
464
+ }
465
+
466
+ if (isShell) {
467
+ let omitted = 0;
468
+ for (const line of lines) {
469
+ for (const row of wrapOneExpandedLogicalLine(line, maxWidth)) {
470
+ out.push(row);
471
+ if (out.length > maxRows) {
472
+ out.shift();
473
+ omitted += 1;
474
+ }
475
+ }
476
+ }
477
+ return finalizeShellPhysicalCap(out, omitted, maxRows);
478
+ }
479
+
480
+ // Non-shell surfaces keep the head of the expanded body (read/grep/json).
481
+ let truncated = false;
482
+ outer: for (const line of lines) {
483
+ for (const row of wrapOneExpandedLogicalLine(line, maxWidth)) {
484
+ if (out.length < maxRows) {
485
+ out.push(row);
486
+ } else {
487
+ truncated = true;
488
+ break outer;
489
+ }
490
+ }
491
+ }
492
+ if (truncated) {
493
+ const bodySlots = Math.max(0, maxRows - 1);
494
+ if (out.length > bodySlots) out.length = bodySlots;
495
+ if (maxRows > 0) out.push(omittedPhysicalRowsMarker(1, false));
496
+ return out.length > 0 ? out.slice(0, maxRows) : [' '];
497
+ }
498
+ return out.length > 0 ? out : [' '];
499
+ }