mixdog 0.9.3 → 0.9.4

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 (329) hide show
  1. package/package.json +7 -3
  2. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  3. package/scripts/bench/lead-review-tasks.json +20 -0
  4. package/scripts/bench/r4-mixed-tasks.json +20 -0
  5. package/scripts/bench/review-tasks.json +20 -0
  6. package/scripts/bench/round-codex.json +114 -0
  7. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  8. package/scripts/bench/round-mixdog-lead.json +269 -0
  9. package/scripts/bench/round-mixdog.json +126 -0
  10. package/scripts/bench/round-r10-bigsample.json +679 -0
  11. package/scripts/bench/round-r11-codexalign.json +257 -0
  12. package/scripts/bench/round-r4-codex.json +114 -0
  13. package/scripts/bench/round-r4-mixed.json +225 -0
  14. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  15. package/scripts/bench/round-r6-codex.json +114 -0
  16. package/scripts/bench/round-r6-solo.json +257 -0
  17. package/scripts/bench/round-r7-full.json +254 -0
  18. package/scripts/bench/round-r8-fulldefault.json +255 -0
  19. package/scripts/bench-run.mjs +215 -29
  20. package/scripts/freevar-smoke.mjs +95 -0
  21. package/scripts/internal-comms-bench.mjs +1 -0
  22. package/scripts/internal-comms-smoke.mjs +10 -9
  23. package/scripts/mouse-probe.mjs +45 -0
  24. package/scripts/output-style-bench.mjs +13 -6
  25. package/scripts/output-style-smoke.mjs +4 -4
  26. package/scripts/provider-toolcall-test.mjs +7 -3
  27. package/scripts/recall-usecase-cases.json +18 -0
  28. package/scripts/recall-usecase-probe.json +6 -0
  29. package/scripts/session-bench.mjs +152 -6
  30. package/scripts/tool-smoke.mjs +23 -63
  31. package/scripts/tui-render-smoke.mjs +90 -0
  32. package/scripts/webhook-smoke.mjs +208 -0
  33. package/src/agents/debugger/AGENT.md +4 -1
  34. package/src/agents/heavy-worker/AGENT.md +6 -5
  35. package/src/agents/maintainer/AGENT.md +4 -0
  36. package/src/agents/reviewer/AGENT.md +2 -1
  37. package/src/agents/worker/AGENT.md +8 -4
  38. package/src/lib/rules-builder.cjs +4 -0
  39. package/src/mixdog-session-runtime.mjs +632 -2042
  40. package/src/output-styles/default.md +34 -9
  41. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  42. package/src/output-styles/minimal.md +4 -1
  43. package/src/output-styles/simple.md +22 -7
  44. package/src/rules/agent/00-common.md +2 -0
  45. package/src/rules/lead/lead-brief.md +12 -0
  46. package/src/rules/lead/lead-tool.md +0 -11
  47. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  49. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  50. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  51. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  52. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  53. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  54. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  55. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  56. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  57. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  58. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  59. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  60. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  62. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  63. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  65. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  66. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
  67. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  69. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  70. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
  76. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
  77. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
  80. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  82. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  83. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  84. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  85. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  86. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  87. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  88. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  89. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  90. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  91. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  93. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
  94. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  95. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  97. package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
  98. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  99. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  100. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  101. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  102. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  103. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  104. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  105. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  106. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  107. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  108. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  109. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +32 -41
  110. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  113. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  115. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  118. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  119. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  120. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  124. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  125. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  126. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  128. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  130. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  131. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  132. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  133. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  134. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  136. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  137. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  138. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  139. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  140. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  141. package/src/runtime/channels/backends/discord.mjs +12 -292
  142. package/src/runtime/channels/index.mjs +229 -663
  143. package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
  144. package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
  145. package/src/runtime/channels/lib/event-queue.mjs +63 -4
  146. package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
  147. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  148. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  149. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  150. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  151. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  152. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  153. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  154. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  155. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  156. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  157. package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
  158. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  159. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  160. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  161. package/src/runtime/channels/lib/webhook.mjs +36 -570
  162. package/src/runtime/channels/tool-defs.mjs +11 -130
  163. package/src/runtime/memory/index.mjs +201 -1948
  164. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  165. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  166. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  167. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  168. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  169. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  170. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  171. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  172. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  173. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  174. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  175. package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  177. package/src/runtime/memory/lib/query-handlers.mjs +780 -0
  178. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  179. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  180. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  181. package/src/runtime/memory/tool-defs.mjs +5 -13
  182. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  183. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  184. package/src/runtime/search/lib/web-tools.mjs +24 -602
  185. package/src/runtime/shared/atomic-file.mjs +26 -1
  186. package/src/runtime/shared/launcher-control.mjs +2 -2
  187. package/src/runtime/shared/tool-primitives.mjs +308 -0
  188. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  189. package/src/runtime/shared/tool-surface.mjs +80 -898
  190. package/src/runtime/shared/transcript-writer.mjs +23 -0
  191. package/src/runtime/shared/update-checker.mjs +7 -4
  192. package/src/session-runtime/config-helpers.mjs +84 -2
  193. package/src/session-runtime/config-lifecycle.mjs +232 -0
  194. package/src/session-runtime/cwd-plugins.mjs +226 -0
  195. package/src/session-runtime/mcp-glue.mjs +177 -0
  196. package/src/session-runtime/model-recency.mjs +111 -0
  197. package/src/session-runtime/native-search.mjs +247 -0
  198. package/src/session-runtime/output-styles.mjs +11 -9
  199. package/src/session-runtime/prewarm.mjs +142 -0
  200. package/src/session-runtime/provider-models.mjs +278 -0
  201. package/src/session-runtime/provider-usage.mjs +120 -0
  202. package/src/session-runtime/quick-model-rows.mjs +170 -0
  203. package/src/session-runtime/quick-search-models.mjs +46 -0
  204. package/src/session-runtime/session-hooks.mjs +93 -0
  205. package/src/session-runtime/settings-api.mjs +319 -0
  206. package/src/session-runtime/tool-catalog.mjs +29 -29
  207. package/src/session-runtime/tool-defs.mjs +84 -0
  208. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  209. package/src/standalone/agent-tool/helpers.mjs +237 -0
  210. package/src/standalone/agent-tool/notify.mjs +107 -0
  211. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  212. package/src/standalone/agent-tool/render.mjs +152 -0
  213. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  214. package/src/standalone/agent-tool.mjs +110 -671
  215. package/src/standalone/channel-worker.mjs +4 -7
  216. package/src/standalone/explore-tool.mjs +30 -9
  217. package/src/standalone/hook-bus/config.mjs +207 -0
  218. package/src/standalone/hook-bus/constants.mjs +90 -0
  219. package/src/standalone/hook-bus/handlers.mjs +481 -0
  220. package/src/standalone/hook-bus/payload.mjs +31 -0
  221. package/src/standalone/hook-bus/rules.mjs +77 -0
  222. package/src/standalone/hook-bus.mjs +77 -870
  223. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  224. package/src/standalone/opencode-go-login.mjs +5 -1
  225. package/src/standalone/provider-admin.mjs +1 -16
  226. package/src/standalone/usage-dashboard.mjs +3 -1
  227. package/src/tui/App.jsx +945 -8094
  228. package/src/tui/app/app-format.mjs +206 -0
  229. package/src/tui/app/channel-pickers.mjs +510 -0
  230. package/src/tui/app/clipboard.mjs +67 -0
  231. package/src/tui/app/core-memory-picker.mjs +210 -0
  232. package/src/tui/app/extension-pickers.mjs +506 -0
  233. package/src/tui/app/input-parsers.mjs +193 -0
  234. package/src/tui/app/maintenance-pickers.mjs +324 -0
  235. package/src/tui/app/model-options.mjs +330 -0
  236. package/src/tui/app/model-picker.mjs +365 -0
  237. package/src/tui/app/onboarding-steps.mjs +400 -0
  238. package/src/tui/app/project-picker.mjs +247 -0
  239. package/src/tui/app/provider-setup-picker.mjs +580 -0
  240. package/src/tui/app/resume-picker.mjs +55 -0
  241. package/src/tui/app/route-pickers.mjs +419 -0
  242. package/src/tui/app/settings-picker.mjs +490 -0
  243. package/src/tui/app/slash-commands.mjs +101 -0
  244. package/src/tui/app/slash-dispatch.mjs +427 -0
  245. package/src/tui/app/text-layout.mjs +46 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  247. package/src/tui/app/transcript-window.mjs +671 -0
  248. package/src/tui/app/use-mouse-input.mjs +460 -0
  249. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  250. package/src/tui/app/use-transcript-scroll.mjs +510 -0
  251. package/src/tui/app/use-transcript-window.mjs +589 -0
  252. package/src/tui/components/ConfirmBar.jsx +1 -1
  253. package/src/tui/components/Picker.jsx +32 -4
  254. package/src/tui/components/PromptInput.jsx +23 -101
  255. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  256. package/src/tui/components/StatusLine.jsx +63 -12
  257. package/src/tui/components/TextEntryPanel.jsx +11 -0
  258. package/src/tui/components/ToolExecution.jsx +52 -594
  259. package/src/tui/components/TranscriptItem.jsx +105 -0
  260. package/src/tui/components/UsagePanel.jsx +18 -4
  261. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  262. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  263. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  264. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  265. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  266. package/src/tui/display-width.mjs +20 -3
  267. package/src/tui/dist/index.mjs +19652 -18630
  268. package/src/tui/engine/agent-job-feed.mjs +133 -0
  269. package/src/tui/engine/notification-plan.mjs +76 -0
  270. package/src/tui/engine/render-timing.mjs +17 -0
  271. package/src/tui/engine/tool-approval.mjs +94 -0
  272. package/src/tui/engine/tool-card-results.mjs +234 -0
  273. package/src/tui/engine/tool-result-status.mjs +135 -0
  274. package/src/tui/engine.mjs +122 -562
  275. package/src/tui/figures.mjs +5 -0
  276. package/src/tui/index.jsx +105 -0
  277. package/src/tui/input-editing.mjs +2 -2
  278. package/src/tui/markdown/format-token.mjs +4 -1
  279. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  280. package/src/tui/theme.mjs +6 -0
  281. package/src/ui/statusline-agents.mjs +213 -0
  282. package/src/ui/statusline-format.mjs +146 -0
  283. package/src/ui/statusline-segments.mjs +148 -0
  284. package/src/ui/statusline.mjs +67 -501
  285. package/src/ui/tool-card.mjs +0 -1
  286. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  287. package/src/workflows/default/WORKFLOW.md +1 -1
  288. package/src/workflows/sequential/WORKFLOW.md +1 -1
  289. package/vendor/ink/build/display-width.js +19 -3
  290. package/vendor/ink/build/ink.js +103 -6
  291. package/vendor/ink/build/log-update.js +17 -3
  292. package/vendor/ink/build/wrap-text.js +125 -0
  293. package/scripts/_test-folder-dialog.mjs +0 -30
  294. package/scripts/fix-brief-fn.mjs +0 -35
  295. package/scripts/fix-format-tool-surface.mjs +0 -24
  296. package/scripts/fix-tool-exec-visible.mjs +0 -42
  297. package/scripts/patch-agent-brief.mjs +0 -48
  298. package/scripts/patch-app.mjs +0 -21
  299. package/scripts/patch-app2.mjs +0 -18
  300. package/scripts/patch-dist-brief.mjs +0 -96
  301. package/scripts/patch-tool-exec.mjs +0 -70
  302. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  303. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  304. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  305. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  306. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  307. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  308. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  309. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  310. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  311. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  312. package/src/standalone/agent-task-status.test.mjs +0 -76
  313. package/src/tui/components/tool-output-format.test.mjs +0 -399
  314. package/src/tui/display-width.test.mjs +0 -35
  315. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  316. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  317. package/src/tui/input-editing.selection.test.mjs +0 -75
  318. package/src/tui/markdown/format-token.test.mjs +0 -354
  319. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  320. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  321. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  322. package/src/tui/paste-fix.test.mjs +0 -119
  323. package/src/tui/prompt-history-store.test.mjs +0 -52
  324. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  325. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  326. package/src/ui/markdown.test.mjs +0 -70
  327. package/src/ui/statusline-context-label.test.mjs +0 -15
  328. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  329. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -0,0 +1,405 @@
1
+ /**
2
+ * components/tool-execution/surface-detail.mjs — agent/background-task/shell
3
+ * surface detection, title/summary/detail derivation, and status-color logic
4
+ * for the tool card. Pure helpers (no React). Extracted verbatim from
5
+ * ToolExecution.jsx — behavior unchanged.
6
+ */
7
+ import { theme } from '../../theme.mjs';
8
+ import { formatElapsed } from '../../time-format.mjs';
9
+ import { displayModelName } from '../../../runtime/shared/tool-surface.mjs';
10
+ import { backgroundTaskFailureStatusLabel, isBackgroundErrorOnlyBody } from '../../../runtime/shared/err-text.mjs';
11
+ import {
12
+ plural,
13
+ displayTerminalStatus,
14
+ normalizeTerminalStatus,
15
+ shellResultStatus,
16
+ } from './text-format.mjs';
17
+
18
+ export function isShellTool(normalizedName, label = '') {
19
+ const n = String(normalizedName || '').toLowerCase();
20
+ const l = String(label || '').toLowerCase();
21
+ return n === 'shell' || n === 'bash' || n === 'bash_session' || n === 'shell_command' || n === 'job_wait' || l === 'run';
22
+ }
23
+
24
+ export function shellDisplayStatus({ pending = false, failedCount = 0, isError = false, result = '' } = {}) {
25
+ const status = shellResultStatus(result);
26
+ if (pending || /^(running|pending|queued)$/.test(status)) return 'running';
27
+ if (/^cancel/.test(status)) return 'cancelled';
28
+ if (/^(failed|error|killed|timeout)$/.test(status) || isError || failedCount > 0) return 'failed';
29
+ return 'completed';
30
+ }
31
+
32
+ export function shellHeader(status, count = 1) {
33
+ const n = Math.max(1, Number(count) || 1);
34
+ const object = `${n} ${plural(n, 'command')}`;
35
+ if (status === 'running') return `Running ${object}`;
36
+ return `Ran ${object}`;
37
+ }
38
+
39
+ export function isAgentTool(normalizedName) {
40
+ return normalizedName === 'agent';
41
+ }
42
+
43
+ export const SKILL_SURFACE_NAMES = new Set([
44
+ 'skill', 'skill_execute', 'skill_view', 'skills_list', 'use_skill',
45
+ ]);
46
+
47
+ export function isBackgroundTaskTool(normalizedName) {
48
+ return new Set(['explore', 'search', 'shell', 'bash', 'bash_session', 'shell_command', 'task']).has(String(normalizedName || '').toLowerCase());
49
+ }
50
+
51
+ const AGENT_DISPLAY_NAMES = new Map([
52
+ ['explore', 'Explore'],
53
+ ['maintainer', 'Maintainer'],
54
+ ['worker', 'Worker'],
55
+ ['heavy-worker', 'Heavy Worker'],
56
+ ['reviewer', 'Reviewer'],
57
+ ['debugger', 'Debugger'],
58
+ ]);
59
+
60
+ export function titleizeAgentName(value) {
61
+ const text = String(value || '').trim();
62
+ if (!text) return '';
63
+ const key = text.toLowerCase().replace(/[\s_]+/g, '-');
64
+ if (AGENT_DISPLAY_NAMES.has(key)) return AGENT_DISPLAY_NAMES.get(key);
65
+ return text
66
+ .replace(/[_-]+/g, ' ')
67
+ .split(/\s+/)
68
+ .filter(Boolean)
69
+ .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1).toLowerCase()}`)
70
+ .join(' ');
71
+ }
72
+
73
+ export function agentModelLabel(args) {
74
+ const a = args && typeof args === 'object' ? args : {};
75
+ const provider = String(a.provider || a.providerId || a.provider_id || '').trim();
76
+ const model = String(a.model || '').trim();
77
+ const displayHint = String(a.modelDisplay || a.model_display || a.displayModel || '').trim();
78
+ return displayModelName(model, provider, displayHint);
79
+ }
80
+
81
+ export function agentTagLabel(args) {
82
+ // The real spawn tag (engine fills parsedArgs.tag from the envelope target).
83
+ // Never fall back to task_id — only the human-meaningful spawn tag belongs in
84
+ // the header parentheses.
85
+ return String(args?.tag || '').trim();
86
+ }
87
+
88
+ export function withModelAndTag(label, args) {
89
+ const model = agentModelLabel(args);
90
+ const tag = agentTagLabel(args);
91
+ const inner = [model, tag].filter(Boolean).join(', ');
92
+ return inner ? `${label} (${inner})` : label;
93
+ }
94
+
95
+ // Append an agent name to a base action word without leaving a trailing space
96
+ // when the agent is unknown (no generic "Agent" fallback).
97
+ export function joinActionAgent(action, agent) {
98
+ return agent ? `${action} ${agent}` : action;
99
+ }
100
+
101
+ export function agentResponseTitle(args) {
102
+ const name = titleizeAgentName(args?.agent || args?.subagent_type || args?.name || '');
103
+ // The agent + model identify the responder; the response summary itself
104
+ // is hidden in the collapsed card (ctrl+o expand still shows the full body).
105
+ // No generic "Agent" fallback — render just "Response" when the agent is empty.
106
+ return withModelAndTag(joinActionAgent('Response', name), args);
107
+ }
108
+
109
+ export function agentActionTitle(args) {
110
+ const name = titleizeAgentName(args?.agent || args?.subagent_type || args?.name || '');
111
+ // Runtime treats an omitted type/action as "spawn" (see agent-tool.mjs default),
112
+ // so mirror that contract here instead of falling through to the generic
113
+ // "Called agent" status copy.
114
+ const action = String(args?.type || args?.action || 'spawn').toLowerCase();
115
+ // Fixed action verbs regardless of running/completed status. No generic
116
+ // "Agent" fallback for the agent: when the agent is unknown render the action
117
+ // word alone ("Spawn") instead of "Spawn Agent".
118
+ if (action === 'spawn') return withModelAndTag(joinActionAgent('Spawn', name), args);
119
+ if (action === 'send') return withModelAndTag(joinActionAgent('Send', name), args);
120
+ if (action === 'list') return 'Agent status';
121
+ if (action === 'cancel') return withModelAndTag(joinActionAgent('Cancel', name), args);
122
+ if (action === 'close') return withModelAndTag(joinActionAgent('Close', name), args);
123
+ if (action === 'cleanup') return withModelAndTag(joinActionAgent('Cleanup', name), args);
124
+ if (action === 'read' || action === 'status') return withModelAndTag(joinActionAgent('Status', name), args);
125
+ return '';
126
+ }
127
+
128
+ export function agentActionSummary(args, summary) {
129
+ const text = String(summary || '').trim();
130
+ if (!text) return '';
131
+ const name = titleizeAgentName(args?.agent || args?.subagent_type || args?.name || '');
132
+ if (name && text === name) return '';
133
+ let rest = name && text.startsWith(`${name} · `) ? text.slice(name.length + 3).trim() : text;
134
+ // The agent/model/tag surface summary ("Heavy Worker · Opus 4.8") is now folded
135
+ // into the header label itself ("Spawn Heavy Worker (Opus 4.8, tag)"), so drop
136
+ // the model and tag tokens from the parenthesized summary to avoid showing
137
+ // them twice.
138
+ const model = agentModelLabel(args);
139
+ if (model && rest === model) return '';
140
+ const tag = agentTagLabel(args);
141
+ if (tag && rest === tag) return '';
142
+ return rest;
143
+ }
144
+
145
+ export function hasAgentResponseResult(value) {
146
+ const text = String(value || '').trim();
147
+ if (!text) return false;
148
+ if (/^(?:undefined|null)$/i.test(text)) return false;
149
+ if (/^status:\s*(?:running|pending|queued|completed|failed|cancelled|canceled)(?:\s*·\s*task_id:\s*\S+)?$/i.test(text)) return false;
150
+ const isBridgeEnvelope = /^(?:agent task:|background task\b|agent message queued\b|agent close:)/i.test(text)
151
+ || /^(?:agents|tasks):\s*\d/i.test(text)
152
+ || /^\(no agents or tasks\)$/i.test(text)
153
+ || (/^task_id:\s*\S+/mi.test(text) && /^(?:surface|operation|status):\s*/mi.test(text));
154
+ if (!isBridgeEnvelope) return true;
155
+ let sawBlank = false;
156
+ for (const line of text.split('\n')) {
157
+ const trimmed = line.trim();
158
+ if (!trimmed) {
159
+ sawBlank = true;
160
+ continue;
161
+ }
162
+ if (/^agent result\b/i.test(trimmed)) continue;
163
+ if (/^(?:undefined|null)$/i.test(trimmed)) continue;
164
+ if (/^<\/?(?:final-answer|task-notification|task-id|tool-use-id|output-file|result|status|summary|usage|total_tokens|tool_uses|duration_ms|worktree|worktreePath|worktreeBranch)[^>]*>$/i.test(trimmed)) continue;
165
+ if (!sawBlank && /^(?:agent task|background task|agent message queued\b|agent close:|task_id|surface|operation|label|status|type|target|agent|preset|model|effort|fast|limits|started|finished|error|notification|queueDepth):?\s*/i.test(trimmed)) continue;
166
+ if (!sawBlank && /^(?:agents|tasks):\s*/i.test(trimmed)) continue;
167
+ if (/^\(no agents or tasks\)$/i.test(trimmed)) continue;
168
+ if (!sawBlank && /^-\s+\S+/i.test(trimmed)) continue;
169
+ return true;
170
+ }
171
+ return false;
172
+ }
173
+
174
+ export function parseBackgroundTaskResult(value) {
175
+ const text = String(value || '').trim();
176
+ if (!text) return null;
177
+ const allLines = text.split('\n');
178
+ const start = allLines.findIndex((line) => line.trim() === 'background task');
179
+ if (start < 0) return null;
180
+ const rest = allLines.slice(start + 1);
181
+ const blank = rest.findIndex((line) => !line.trim());
182
+ const headLines = blank >= 0 ? rest.slice(0, blank) : rest;
183
+ const body = blank >= 0 ? rest.slice(blank + 1).join('\n').trim() : '';
184
+ const fields = {};
185
+ for (const line of headLines) {
186
+ const match = /^([a-zA-Z][\w-]*):\s*(.*)$/.exec(line.trim());
187
+ if (match) fields[match[1].toLowerCase()] = match[2].trim();
188
+ }
189
+ const status = String(fields.status || '').toLowerCase();
190
+ const error = fields.error || '';
191
+ return {
192
+ taskId: fields.task_id || fields.taskid || '',
193
+ surface: fields.surface || '',
194
+ operation: fields.operation || '',
195
+ label: fields.label || '',
196
+ status,
197
+ startedAt: fields.started || fields.startedat || '',
198
+ finishedAt: fields.finished || fields.finishedat || '',
199
+ body,
200
+ error,
201
+ hasResponse: Boolean(body) && !isBackgroundErrorOnlyBody(body, error) && !/^(running|pending|queued)$/i.test(status),
202
+ };
203
+ }
204
+
205
+ export function backgroundTaskMetaFromArgs(args = {}) {
206
+ const taskId = String(args.task_id || args.taskId || '').trim();
207
+ if (!taskId) return null;
208
+ return {
209
+ taskId,
210
+ surface: args.surface || '',
211
+ operation: args.operation || '',
212
+ label: args.label || '',
213
+ status: String(args.status || '').toLowerCase(),
214
+ startedAt: args.startedAt || args.started || '',
215
+ finishedAt: args.finishedAt || args.finished || '',
216
+ error: args.error || '',
217
+ body: '',
218
+ hasResponse: false,
219
+ };
220
+ }
221
+
222
+ export function resolveBackgroundTaskMeta(parsedArgs = {}, resultText = '') {
223
+ const parsed = parseBackgroundTaskResult(resultText);
224
+ if (parsed) {
225
+ if (!parsed.error && parsedArgs?.error) parsed.error = parsedArgs.error;
226
+ if (!parsed.status && parsedArgs?.status) parsed.status = String(parsedArgs.status).toLowerCase();
227
+ if (!parsed.surface && parsedArgs?.surface) parsed.surface = parsedArgs.surface;
228
+ return parsed;
229
+ }
230
+ return backgroundTaskMetaFromArgs(parsedArgs);
231
+ }
232
+
233
+ export function backgroundTaskElapsed(meta = {}, fallback = '') {
234
+ const startedMs = Date.parse(meta.startedAt || '');
235
+ const finishedMs = Date.parse(meta.finishedAt || '');
236
+ if (Number.isFinite(startedMs) && Number.isFinite(finishedMs) && finishedMs >= startedMs) {
237
+ const elapsedMs = finishedMs - startedMs;
238
+ return elapsedMs >= 1000 ? formatElapsed(elapsedMs) : '';
239
+ }
240
+ return fallback || '';
241
+ }
242
+
243
+ export function prefixElapsed(detail, elapsed = '') {
244
+ const text = String(detail || '').trim();
245
+ const time = String(elapsed || '').trim();
246
+ if (!time) return text;
247
+ // Unified convention: the elapsed time ALWAYS goes at the END, ` · ` separated.
248
+ // Guard against a double-append when the text already ends with the same time.
249
+ if (text && text.endsWith(`· ${time}`)) return text;
250
+ return text ? `${text} · ${time}` : time;
251
+ }
252
+
253
+ export function mergeTerminalDetail(status, detail = '') {
254
+ const label = displayTerminalStatus(status);
255
+ const text = String(detail || '').trim();
256
+ if (!label) return text;
257
+ if (label === 'Finished' && text) return text;
258
+ if (!text) return label;
259
+ if (text.toLowerCase().startsWith(label.toLowerCase())) return text;
260
+ return `${label} · ${text}`;
261
+ }
262
+
263
+ export function shouldPrefixSyncElapsed(normalizedName, label) {
264
+ const n = String(normalizedName || '').toLowerCase();
265
+ const l = String(label || '').toLowerCase();
266
+ return n === 'explore' || l === 'explore' || n === 'search' || l === 'search' || l === 'web search';
267
+ }
268
+
269
+ export function backgroundTaskDisplayName(normalizedName, meta = {}) {
270
+ const surface = String(meta.surface || normalizedName || '').toLowerCase();
271
+ if (surface === 'explore') return 'Explore';
272
+ if (surface === 'search') return 'Search';
273
+ if (surface === 'shell' || surface === 'bash' || surface === 'bash_session' || surface === 'shell_command' || surface === 'task') return 'Shell';
274
+ return titleizeAgentName(surface || normalizedName || 'Task');
275
+ }
276
+
277
+ export function backgroundTaskResultTitle(normalizedName, meta = {}) {
278
+ const display = backgroundTaskDisplayName(normalizedName, meta);
279
+ if (display === 'Shell') return 'Shell output';
280
+ if (display === 'Search') return 'Search results';
281
+ return `${display} response`;
282
+ }
283
+
284
+ export function backgroundTaskActionTitle(normalizedName, meta = {}) {
285
+ const display = backgroundTaskDisplayName(normalizedName, meta);
286
+ if (/^(running|pending|queued)$/i.test(meta.status || '')) return `Started ${display}`;
287
+ if (meta.hasResponse) return backgroundTaskResultTitle(normalizedName, meta);
288
+ return `${display} status`;
289
+ }
290
+
291
+ export function backgroundTaskFailureDetail(meta = {}, parsedArgs = {}) {
292
+ const status = meta.status || parsedArgs?.status;
293
+ const error = meta.error || parsedArgs?.error;
294
+ if (!error) return '';
295
+ const surface = meta.surface || parsedArgs?.surface || '';
296
+ return backgroundTaskFailureStatusLabel(status, error, { surface });
297
+ }
298
+
299
+ export function backgroundTaskDetail(meta = {}, elapsed = '', parsedArgs = {}) {
300
+ const parts = [];
301
+ const status = displayTerminalStatus(meta.status);
302
+ if (status) parts.push(status);
303
+ if (meta.taskId) parts.push(`task_id: ${meta.taskId}`);
304
+ const firstBodyLine = String(meta.body || '').split('\n').map((line) => line.trim()).find(Boolean) || '';
305
+ if (firstBodyLine && /^(running|pending|queued)$/i.test(meta.status || '')) parts.push(firstBodyLine);
306
+ return prefixElapsed(parts.join(' · '), elapsed);
307
+ }
308
+
309
+ export function isBackgroundTaskResponseArgs(normalizedName, args = {}) {
310
+ if (!isBackgroundTaskTool(normalizedName)) return false;
311
+ const type = String(args?.type || args?.action || '').toLowerCase();
312
+ const status = String(args?.status || '').toLowerCase();
313
+ return type === 'result' || type === 'completion' || (/^(completed|cancelled|canceled)$/i.test(status) && Boolean(args?.task_id));
314
+ }
315
+
316
+ export function isOutputDetailTool(normalizedName, label) {
317
+ const n = String(normalizedName || '').toLowerCase();
318
+ const l = String(label || '').toLowerCase();
319
+ return new Set([
320
+ 'shell', 'bash', 'bash_session', 'shell_command', 'job_wait',
321
+ 'read', 'view_image', 'read_mcp_resource',
322
+ 'grep', 'glob', 'search', 'search_query', 'image_query', 'web_search', 'web_search_call', 'explore', 'web_fetch', 'fetch',
323
+ 'list', 'ls', 'code_graph',
324
+ 'recall', 'recall_memory', 'search_memories', 'remember', 'save_memory', 'update_memory',
325
+ ]).has(n) || l === 'read' || l === 'search' || l === 'web search' || l === 'run';
326
+ }
327
+
328
+ export function genericCompletedDetail({ normalizedName, label, hasResult, firstResultLine, isError }) {
329
+ const n = String(normalizedName || '').toLowerCase();
330
+ const l = String(label || '').toLowerCase();
331
+ if (isError) return hasResult ? firstResultLine : 'Failed';
332
+ if (n === 'shell' || n === 'bash' || n === 'bash_session' || n === 'shell_command' || n === 'job_wait') {
333
+ return '';
334
+ }
335
+ if (isOutputDetailTool(n, l)) {
336
+ return hasResult ? firstResultLine : '';
337
+ }
338
+ return '';
339
+ }
340
+
341
+ export function toolSearchLoadedSummary(resultText) {
342
+ let parsed;
343
+ try {
344
+ parsed = JSON.parse(String(resultText || ''));
345
+ } catch {
346
+ const text = String(resultText || '');
347
+ const match = /^Loaded deferred tools:\s*(.+)$/m.exec(text);
348
+ if (!match) return '';
349
+ return [...new Set(match[1].split(',').map((name) => name.trim()).filter(Boolean))].join(', ');
350
+ }
351
+ const tools = parsed?.selected?.tools;
352
+ if (!tools || typeof tools !== 'object') return '';
353
+ const names = [
354
+ ...(Array.isArray(tools.added) ? tools.added : []),
355
+ ...(Array.isArray(tools.already) ? tools.already : []),
356
+ ]
357
+ .map((name) => String(name || '').trim())
358
+ .filter(Boolean);
359
+ return [...new Set(names)].join(', ');
360
+ }
361
+
362
+ export function agentTerminalDetail(status, isError, elapsed, error = '') {
363
+ const failureDetail = isError && error
364
+ ? backgroundTaskFailureStatusLabel(status, error, { surface: 'agent' })
365
+ : '';
366
+ if (failureDetail) return failureDetail;
367
+ const s = String(status || '').toLowerCase();
368
+ const word = /cancel/.test(s)
369
+ ? 'Cancelled'
370
+ : /error|fail|killed|timeout/.test(s) || isError
371
+ ? 'Failed'
372
+ : /done|success|complete|closed/.test(s)
373
+ ? 'Finished'
374
+ : '';
375
+ // Unified ` · <time>` convention (previously "Finished after 12s").
376
+ return word ? `${word}${elapsed ? ` · ${elapsed}` : ''}` : '';
377
+ }
378
+
379
+ export function clampFailureCount(errorCount, groupCount, isError) {
380
+ const explicit = Number(errorCount);
381
+ if (Number.isFinite(explicit)) return Math.max(0, Math.min(groupCount, Math.floor(explicit)));
382
+ return isError ? groupCount : 0;
383
+ }
384
+
385
+ // Single source of truth for the tool-card dot (●) color. Both the aggregate
386
+ // and normal (single-tool) render paths must call this with a resolved
387
+ // `terminalStatus` — do not recompute color inline elsewhere.
388
+ // running/pending -> theme.text (white; blink handled by caller)
389
+ // success -> theme.success
390
+ // partial failure -> mixdogOrange || warning (some, not all, of the group failed)
391
+ // all failed -> theme.error
392
+ // cancelled -> theme.warning
393
+ // Note: callers resolve terminalStatus to 'failed' whenever failedCount > 0
394
+ // (they cannot tell partial from total failure), so a 'failed' status must
395
+ // still fall through to the failedCount check below rather than short-circuit
396
+ // to error — otherwise a mixed-success batch (e.g. 1 of 3 failed) renders as
397
+ // full-failure red instead of partial orange.
398
+ export function toolStatusColor({ pending, groupCount, failedCount, terminalStatus = '' }) {
399
+ if (pending) return theme.text;
400
+ const status = normalizeTerminalStatus(terminalStatus);
401
+ if (status === 'cancelled') return theme.warning;
402
+ if (failedCount <= 0) return status === 'failed' ? theme.error : theme.success;
403
+ if (groupCount > 1 && failedCount < groupCount) return theme.mixdogOrange || theme.warning;
404
+ return theme.error;
405
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * components/tool-execution/text-format.mjs — pure text/formatting helpers for
3
+ * the tool card (no React). Inline-row sanitization, width fitting/truncation,
4
+ * count normalization, diff-token splitting, and terminal-status parsing.
5
+ * Extracted verbatim from ToolExecution.jsx — behavior unchanged.
6
+ */
7
+ import { displayWidth } from '../../display-width.mjs';
8
+ import { theme } from '../../theme.mjs';
9
+ import { formatElapsed } from '../../time-format.mjs';
10
+ import stripAnsi from 'strip-ansi';
11
+
12
+ export const MIN_RESULT_LINE_CHARS = 24;
13
+ // Hard cap for the collapsed result detail row (the second line under the ⎿
14
+ // gutter). Independent of terminal width so a wide terminal never lets a long
15
+ // line (e.g. an agent response brief) stretch the whole row — anything past
16
+ // this is truncated with an ellipsis. ctrl+o expand still shows the full body.
17
+ export const RESULT_LINE_HARD_MAX = 80;
18
+ // Hard cap for the parenthesized header arg summary so a long path/query does
19
+ // not eat the whole header line; anything longer is truncated with an ellipsis.
20
+ export const SUMMARY_MAX_CHARS = 48;
21
+ export const HEADER_FAILURE_STATUS_MAX = 32;
22
+
23
+ // Read `theme.subtle` at use-time (not captured here) so a live `/theme`
24
+ // switch re-tones the tool hints. `theme` is mutated in-place on switch.
25
+ // Collapsed tool headers/details are laid out as single terminal rows. Never let
26
+ // raw C0/control bytes (CR, tabs, cursor escapes, etc.) reach those rows: a
27
+ // terminal can apply them after Ink has already clipped/measured the row, which
28
+ // makes a scrolled tool card appear to write through the prompt/statusline.
29
+ const INLINE_CONTROL_RE = /[\u0000-\u001F\u007F]/g;
30
+
31
+ export function safeInlineText(value) {
32
+ return stripAnsi(String(value ?? ''))
33
+ .replace(/\r\n?/g, '\n')
34
+ .replace(/\t/g, ' ')
35
+ .replace(/\n+/g, ' ')
36
+ .replace(INLINE_CONTROL_RE, ' ')
37
+ .replace(/\s+/g, ' ')
38
+ .trim();
39
+ }
40
+
41
+ export function normalizeCount(value) {
42
+ const n = Number(value || 0);
43
+ return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
44
+ }
45
+
46
+ export function normalizeCountMap(value = {}) {
47
+ const out = {};
48
+ for (const [key, raw] of Object.entries(value || {})) {
49
+ if (raw && typeof raw === 'object') {
50
+ out[key] = { ...raw, count: normalizeCount(raw.count) };
51
+ } else {
52
+ out[key] = normalizeCount(raw);
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+
58
+ export function deltaColor(token) {
59
+ return String(token || '').startsWith('+') ? theme.success : theme.error;
60
+ }
61
+
62
+ export function deltaTextParts(text) {
63
+ const value = String(text ?? '');
64
+ const parts = [];
65
+ const re = /(^|[\s([,{·])([+-]\s*\d+)(?=\s+Lines?\b)/gi;
66
+ let last = 0;
67
+ let match;
68
+ while ((match = re.exec(value))) {
69
+ const prefix = match[1] || '';
70
+ const token = (match[2] || '').replace(/\s+/g, '');
71
+ const tokenStart = match.index + prefix.length;
72
+ if (match.index > last) parts.push({ text: value.slice(last, match.index) });
73
+ if (prefix) parts.push({ text: prefix });
74
+ if (token) parts.push({ text: token, color: deltaColor(token) });
75
+ last = tokenStart + (match[2] || '').length;
76
+ }
77
+ if (last < value.length) parts.push({ text: value.slice(last) });
78
+ return parts;
79
+ }
80
+
81
+ export function plural(count, singular, pluralText = `${singular}s`) {
82
+ return count === 1 ? singular : pluralText;
83
+ }
84
+
85
+ export function fitResultLine(line, columns) {
86
+ const max = Math.min(RESULT_LINE_HARD_MAX, Math.max(MIN_RESULT_LINE_CHARS, Number(columns || 80) - 7));
87
+ const text = safeInlineText(line);
88
+ return displayWidth(text) > max ? truncateToWidth(text, max) : text;
89
+ }
90
+
91
+ /** Trim text from the end (by display width) so it fits maxWidth, appending '…'. */
92
+ export function truncateToWidth(text, maxWidth) {
93
+ const str = safeInlineText(text);
94
+ if (maxWidth < 1) return '';
95
+ if (displayWidth(str) <= maxWidth) return str;
96
+ const chars = Array.from(str);
97
+ let out = '';
98
+ for (const ch of chars) {
99
+ if (displayWidth(out + ch + '…') > maxWidth) break;
100
+ out += ch;
101
+ }
102
+ return `${out}…`;
103
+ }
104
+
105
+ export function shellResultStatus(value) {
106
+ const match = String(value || '').match(/(?:^|\b)status:\s*(running|pending|queued|completed|failed|cancelled|canceled)\b/im);
107
+ return match ? String(match[1] || '').toLowerCase() : '';
108
+ }
109
+
110
+ export function normalizeTerminalStatus(value) {
111
+ const raw = String(value || '').trim().toLowerCase();
112
+ if (!raw) return '';
113
+ if (/^(running|pending|queued|in_progress|in-progress)$/.test(raw)) return 'running';
114
+ if (/^(completed|complete|done|success|succeeded|ok)$/.test(raw)) return 'completed';
115
+ if (/^(failed|fail|error|errored|timeout|timed_out|killed)$/.test(raw)) return 'failed';
116
+ if (/^(cancelled|canceled|cancel)$/.test(raw)) return 'cancelled';
117
+ return '';
118
+ }
119
+
120
+ export function displayTerminalStatus(value) {
121
+ const status = normalizeTerminalStatus(value);
122
+ if (status === 'running') return 'Running';
123
+ if (status === 'completed') return 'Finished';
124
+ if (status === 'failed') return 'Failed';
125
+ if (status === 'cancelled') return 'Cancelled';
126
+ return '';
127
+ }
128
+
129
+ export function resultTerminalStatus(value) {
130
+ const text = String(value || '');
131
+ const tagged = text.match(/<status[^>]*>([\s\S]*?)<\/status>/i)?.[1]?.trim();
132
+ if (tagged) return normalizeTerminalStatus(tagged);
133
+ const bracketed = text.match(/^\[status:\s*([^\]]*)\]/mi)?.[1]?.trim();
134
+ if (bracketed) return normalizeTerminalStatus(bracketed);
135
+ // Loose inline `status: x` / `state: x` matches are a last-resort fallback —
136
+ // prefer the engine-controlled `<status>` tag or `[status: …]` marker above.
137
+ // A loose match can false-positive on prose that happens to start with
138
+ // "status:" (rare, but shellResultStatus below already owns real shell
139
+ // output parsing; this fallback stays narrow and unchanged in behavior).
140
+ const inline = text.match(/^(?:status|state):\s*([^\s·,;]+)/mi)?.[1]?.trim();
141
+ return normalizeTerminalStatus(inline);
142
+ }
143
+
144
+ const LEADING_STATUS_MARKER_LINE_RE = /^\[status:\s*[^\]]*\]\s*$/i;
145
+
146
+ export function stripLeadingStatusMarkerLines(lines) {
147
+ const out = Array.isArray(lines) ? lines.slice() : [];
148
+ if (out.length > 0 && LEADING_STATUS_MARKER_LINE_RE.test(String(out[0] ?? '').trim())) out.shift();
149
+ return out;
150
+ }
151
+
152
+ export function stripLeadingStatusMarkerFromText(text) {
153
+ return stripLeadingStatusMarkerLines(String(text || '').split('\n')).join('\n');
154
+ }
155
+
156
+ export function shellResultElapsed(value) {
157
+ const match = String(value || '').match(/^\[elapsed:\s*(\d+)\s*ms\]/mi);
158
+ if (!match) return '';
159
+ const elapsedMs = Number(match[1]);
160
+ return Number.isFinite(elapsedMs) && elapsedMs >= 1000 ? formatElapsed(elapsedMs) : '';
161
+ }
@@ -23,6 +23,10 @@
23
23
  */
24
24
  import stringWidth from 'string-width';
25
25
 
26
+ // Grapheme segmenter for cluster-aware width math (e.g. `↔️` = U+2194 U+FE0F
27
+ // is one cluster, counted once). Kept identical to vendor/ink/build/display-width.js.
28
+ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
29
+
26
30
  /**
27
31
  * True for code points we treat as wide (2 cells) when the policy is ON:
28
32
  * - U+2460–U+24FF Enclosed Alphanumerics (① ② ③ …)
@@ -33,6 +37,12 @@ export function isProblemCodePoint(cp) {
33
37
  return (cp >= 0x2460 && cp <= 0x24ff) || (cp >= 0x2190 && cp <= 0x21ff);
34
38
  }
35
39
 
40
+ // Fast precheck for the problem ranges above. Lets the hot path bail before
41
+ // the per-grapheme segmenter loop when a string (the overwhelmingly common
42
+ // ASCII/status-text case) contains no widenable glyph. Kept identical to
43
+ // vendor/ink/build/display-width.js.
44
+ const PROBLEM_RE = /[\u2190-\u21ff\u2460-\u24ff]/;
45
+
36
46
  /** Resolve the wide policy from env. Override wins over the WT_SESSION default. */
37
47
  export function resolveAmbiguousWidePolicy(env = process.env) {
38
48
  const override = env?.MIXDOG_TUI_AMBIGUOUS_WIDE;
@@ -55,10 +65,17 @@ export function displayWidthWith(str, wide) {
55
65
  const s = String(str ?? '');
56
66
  const base = stringWidth(s);
57
67
  if (!wide) return base;
68
+ // Fast bail: no problem glyph ⇒ segmenting can only return `base`.
69
+ if (!PROBLEM_RE.test(s)) return base;
58
70
  let extra = 0;
59
- for (const ch of s) {
60
- const cp = ch.codePointAt(0);
61
- if (isProblemCodePoint(cp) && stringWidth(ch) === 1) extra += 1;
71
+ // Widen per GRAPHEME CLUSTER, not per code point: a clustered glyph like
72
+ // `↔️` (U+2194 U+FE0F) is one visible cell-group — count its problem base
73
+ // once. Only add +1 when the WHOLE cluster still measures 1 cell under
74
+ // string-width (so a cluster the terminal already draws as 2 isn't
75
+ // double-widened).
76
+ for (const { segment } of graphemeSegmenter.segment(s)) {
77
+ const cp = segment.codePointAt(0);
78
+ if (isProblemCodePoint(cp) && stringWidth(segment) === 1) extra += 1;
62
79
  }
63
80
  return base + extra;
64
81
  }