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
@@ -0,0 +1,312 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ formatExpandedResult,
5
+ wrapExpandedResultLines,
6
+ expandedResultBodyWidth,
7
+ inferLangFamily,
8
+ stripUnderlineAnsi,
9
+ linkifyUrls,
10
+ tryFormatJson,
11
+ } from './tool-output-format.mjs';
12
+ import stringWidth from 'string-width';
13
+
14
+ const stripAnsi = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, '').replace(/\x1b\]8;;[^\x07]*\x07/g, '');
15
+
16
+ function withRenderLineCap(cap, fn) {
17
+ const prev = process.env.MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES;
18
+ const legacy = process.env.MIXDOG_TUI_EXPANDED_MAX_ROWS;
19
+ process.env.MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES = String(cap);
20
+ delete process.env.MIXDOG_TUI_EXPANDED_MAX_ROWS;
21
+ try {
22
+ return fn();
23
+ } finally {
24
+ if (prev === undefined) delete process.env.MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES;
25
+ else process.env.MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES = prev;
26
+ if (legacy === undefined) delete process.env.MIXDOG_TUI_EXPANDED_MAX_ROWS;
27
+ else process.env.MIXDOG_TUI_EXPANDED_MAX_ROWS = legacy;
28
+ }
29
+ }
30
+
31
+ test('inferLangFamily maps known extensions, null otherwise', () => {
32
+ assert.equal(inferLangFamily('src/x.mjs'), 'js');
33
+ assert.equal(inferLangFamily('a.py'), 'py');
34
+ assert.equal(inferLangFamily('x.json'), 'json');
35
+ assert.equal(inferLangFamily('run.sh'), 'sh');
36
+ assert.equal(inferLangFamily('readme'), null);
37
+ assert.equal(inferLangFamily(''), null);
38
+ });
39
+
40
+ test('read line-number gutter is split into a dim column and body is highlighted', () => {
41
+ const read = '900\u2192function f() {\n901\u2192 return 1;\n902\u2192}';
42
+ const out = formatExpandedResult(read, { pathArg: 'a.mjs' });
43
+ assert.equal(out.length, 3);
44
+ // gutter text survives, body keyword colored (output carries SGR escapes).
45
+ assert.ok(stripAnsi(out[0]).startsWith('900\u2192function f() {'));
46
+ assert.ok(out[0].includes('\x1b['), 'first line carries color escapes');
47
+ });
48
+
49
+ test('block syntax highlight skips tokenizer for over-long lines (index aligned)', () => {
50
+ // MAX_HIGHLIGHT_LINE_CHARS is 2000 in tool-output-format.mjs
51
+ const giant = 'x'.repeat(2001);
52
+ const read = `1\u2192const a = 1;\n2\u2192${giant}\n3\u2192const b = 2;`;
53
+ const out = formatExpandedResult(read, { pathArg: 'a.mjs' });
54
+ assert.equal(out.length, 3);
55
+ assert.ok(out[0].includes('\x1b['), 'short line before giant is highlighted');
56
+ assert.ok(out[2].includes('\x1b['), 'short line after giant is highlighted');
57
+ const plainMid = stripAnsi(out[1]);
58
+ assert.ok(plainMid.startsWith('2\u2192'), 'gutter preserved on giant line');
59
+ assert.ok(plainMid.endsWith(giant), 'giant body preserved');
60
+ const hljsSpans = (out[1].match(/\x1b\[[0-9;]*m/g) || []).length;
61
+ const shortSpans = (out[0].match(/\x1b\[[0-9;]*m/g) || []).length;
62
+ assert.ok(hljsSpans <= shortSpans + 2, 'giant line not fully tokenized like normal code');
63
+ });
64
+
65
+ test('grep file:line: gutter is split too', () => {
66
+ const grep = 'src/a.mjs:5581: value: foo,';
67
+ const out = formatExpandedResult(grep, { pathArg: 'src/a.mjs' });
68
+ assert.equal(out.length, 1);
69
+ assert.ok(stripAnsi(out[0]).startsWith('src/a.mjs:5581:'));
70
+ });
71
+
72
+ test('grep Windows absolute path:line: gutter is split', () => {
73
+ const grep = 'C:\\Project\\mixdog\\src\\App.jsx:12: const x = 1;';
74
+ const out = formatExpandedResult(grep, { pathArg: 'src/App.jsx' });
75
+ assert.equal(out.length, 1);
76
+ const plain = stripAnsi(out[0]);
77
+ assert.ok(plain.startsWith('C:\\Project\\mixdog\\src\\App.jsx:12:'));
78
+ assert.ok(plain.includes('const x = 1;'), 'body preserved after gutter');
79
+ assert.ok(out[0].includes('\x1b['), 'body carries highlight escapes');
80
+ });
81
+
82
+ test('grep bare line-number gutter still works', () => {
83
+ const grep = '12: return null;';
84
+ const out = formatExpandedResult(grep, { pathArg: 'a.mjs' });
85
+ assert.equal(out.length, 1);
86
+ assert.ok(stripAnsi(out[0]).startsWith('12:'));
87
+ });
88
+
89
+ test('https URLs are not treated as grep path:line: gutters', () => {
90
+ const line = 'see https://example.com:443/path for docs';
91
+ const out = formatExpandedResult(line, {});
92
+ assert.equal(out.length, 1);
93
+ assert.equal(stripAnsi(out[0]), line);
94
+ });
95
+
96
+ test('JSON result is auto pretty-printed (size-capped, precision-safe)', () => {
97
+ const out = formatExpandedResult('{"a":1,"b":[2,3]}', {});
98
+ assert.ok(out.length > 1, 'single-line JSON expands to multiple rows');
99
+ assert.equal(stripAnsi(out[0]), '{');
100
+ // precision loss → keep original line
101
+ const big = '{"id":123456789012345678901234567890}';
102
+ assert.equal(tryFormatJson(big), big);
103
+ });
104
+
105
+ test('JSON with markdown-like string values stays JSON (no markdown lexer)', () => {
106
+ const raw = '["**literal**","`code`"]';
107
+ const out = formatExpandedResult(raw, {});
108
+ const visible = out.map(stripAnsi).join('\n');
109
+ assert.ok(visible.includes('**literal**'), 'array string keeps ** delimiters');
110
+ assert.ok(visible.includes('`code`'), 'array string keeps backticks');
111
+ });
112
+
113
+ test('JSON object with markdown-like values is not rendered as markdown', () => {
114
+ const raw = '{"msg":"**bold**","x":"`y`"}';
115
+ const out = formatExpandedResult(raw, {});
116
+ const visible = out.map(stripAnsi).join('\n');
117
+ assert.ok(visible.includes('**bold**'));
118
+ assert.ok(visible.includes('`y`'));
119
+ });
120
+
121
+ test('shell output keeps color ANSI but strips underline and never highlights', () => {
122
+ const sh = 'PASS \x1b[32mok\x1b[0m\n\x1b[4mfile.js\x1b[24m done';
123
+ const out = formatExpandedResult(sh, { isShell: true });
124
+ assert.ok(out[0].includes('\x1b[32m'), 'color ANSI preserved');
125
+ assert.ok(!out[1].includes('\x1b[4m'), 'underline stripped');
126
+ assert.equal(stripAnsi(out[1]), 'file.js done');
127
+ });
128
+
129
+ test('unified diff is colored by line class', () => {
130
+ const diff = 'diff --git a/x b/x\n@@ -1 +1 @@\n-old\n+new\n ctx';
131
+ const out = formatExpandedResult(diff, {});
132
+ assert.equal(out.length, 5);
133
+ assert.ok(out.every((l) => l.includes('\x1b[')), 'every diff line colored');
134
+ });
135
+
136
+ test('oversize output is capped with a truncation marker', () => {
137
+ const many = Array.from({ length: 5000 }, (_, i) => `line${i}`).join('\n');
138
+ const out = formatExpandedResult(many, {});
139
+ assert.ok(out.length <= 4001, 'logical cap keeps at most MAX_EXPANDED_LINES plus marker');
140
+ assert.ok(/truncated/.test(stripAnsi(out[out.length - 1])));
141
+
142
+ const sixHundred = Array.from({ length: 600 }, (_, i) => `row${i}`).join('\n');
143
+ const out600 = formatExpandedResult(sixHundred, {});
144
+ assert.equal(out600.length, 600, 'normal expanded output is not truncated at 80 logical lines');
145
+
146
+ const huge = 'x'.repeat(300 * 1024);
147
+ const out2 = formatExpandedResult(huge, {});
148
+ assert.ok(/truncated/.test(stripAnsi(out2[out2.length - 1])));
149
+ });
150
+
151
+ test('stripUnderlineAnsi removes only underline SGR', () => {
152
+ assert.equal(stripUnderlineAnsi('a\x1b[4mu\x1b[24mb'), 'aub');
153
+ assert.equal(stripUnderlineAnsi('a\x1b[32mg\x1b[0mb'), 'a\x1b[32mg\x1b[0mb');
154
+ assert.equal(stripUnderlineAnsi('x\x1b[4;31my'), 'x\x1b[31my');
155
+ assert.equal(stripUnderlineAnsi('x\x1b[42my'), 'x\x1b[42my');
156
+ assert.equal(stripUnderlineAnsi('x\x1b[31;4my'), 'x\x1b[31my');
157
+ });
158
+
159
+ test('linkifyUrls wraps bare URLs in OSC 8', () => {
160
+ const out = linkifyUrls('see https://x.com/y here');
161
+ assert.ok(out.includes('\x1b]8;;https://x.com/y\x07'));
162
+ assert.equal(linkifyUrls('no url here'), 'no url here');
163
+ });
164
+
165
+ test('linkifyUrls keeps ANSI out of OSC 8 URL targets', () => {
166
+ const colored = 'go \x1b[31mhttps://example.com/a\x1b[0m end';
167
+ const out = linkifyUrls(colored);
168
+ const m = /\x1b]8;;([^\x07]+)\x07/.exec(out);
169
+ assert.ok(m, 'OSC 8 opener present');
170
+ assert.equal(m[1], 'https://example.com/a');
171
+ assert.ok(!m[1].includes('\x1b'), 'target URL has no escape bytes');
172
+ });
173
+
174
+ function osc8UrlTargets(text) {
175
+ const targets = [];
176
+ const bel = /\x1b]8;;([^\x07]+)\x07/g;
177
+ const st = /\x1b]8;;((?:[^\x1b]|\x1b(?![\\]))+)\x1b\\/g;
178
+ let m;
179
+ while ((m = bel.exec(text)) !== null) targets.push(m[1]);
180
+ while ((m = st.exec(text)) !== null) targets.push(m[1]);
181
+ return targets;
182
+ }
183
+
184
+ test('linkifyUrls does not re-linkify existing BEL OSC 8 hyperlinks', () => {
185
+ const belLink = '\x1b]8;;https://a.test\x07label\x1b]8;;\x07';
186
+ const out = linkifyUrls(belLink);
187
+ assert.equal(out, belLink);
188
+ assert.equal((out.match(/\x1b]8;;/g) || []).length, 2, 'single open/close pair');
189
+ for (const target of osc8UrlTargets(out)) {
190
+ assert.ok(!target.includes('\x1b'), 'target has no escape bytes');
191
+ assert.ok(!target.includes('\x1b]8;;'), 'no nested OSC in target');
192
+ }
193
+ });
194
+
195
+ test('linkifyUrls does not re-linkify existing ST OSC 8 hyperlinks', () => {
196
+ const stLink = '\x1b]8;;https://a.test\x1b\\label\x1b]8;;\x1b\\';
197
+ const out = linkifyUrls(stLink);
198
+ assert.equal(out, stLink);
199
+ assert.equal((out.match(/\x1b]8;;/g) || []).length, 2, 'single open/close pair');
200
+ for (const target of osc8UrlTargets(out)) {
201
+ assert.ok(!target.includes('\x1b'), 'target has no escape bytes');
202
+ assert.ok(!target.includes('\x1b]8;;'), 'no nested OSC in target');
203
+ }
204
+ });
205
+
206
+ test('empty input yields no lines', () => {
207
+ assert.deepEqual(formatExpandedResult('', {}), []);
208
+ });
209
+
210
+ test('wrapExpandedResultLines splits long logical rows to body width', () => {
211
+ const columns = 48;
212
+ const maxW = expandedResultBodyWidth(columns);
213
+ const longBody = 'alpha_beta_gamma_delta '.repeat(6).trimEnd();
214
+ const logical = formatExpandedResult(`900\u2192${longBody}`, { pathArg: 'a.mjs' });
215
+ assert.equal(logical.length, 1);
216
+ const physical = wrapExpandedResultLines(logical, columns);
217
+ assert.ok(physical.length > 1, 'long read line becomes multiple physical rows');
218
+ for (const row of physical) {
219
+ assert.ok(
220
+ stringWidth(stripAnsi(row)) <= maxW,
221
+ `row width ${stringWidth(stripAnsi(row))} exceeds budget ${maxW}`,
222
+ );
223
+ }
224
+ for (let i = 1; i < physical.length; i++) {
225
+ assert.ok(/^\s+/.test(stripAnsi(physical[i])), 'wrapped continuations are indented');
226
+ }
227
+ });
228
+
229
+ test('expanded markdown prose renders headings and emphasis (not raw #/**)', () => {
230
+ const md = '# Title\n\nSome **bold** and `inline`.\n';
231
+ const out = formatExpandedResult(md, {});
232
+ const visible = out.map(stripAnsi).join('\n');
233
+ assert.ok(!visible.includes('# Title'), 'ATX heading marker not shown raw');
234
+ assert.ok(visible.includes('Title'), 'heading text preserved');
235
+ assert.ok(visible.includes('bold'), 'strong text preserved');
236
+ assert.ok(!visible.includes('**'), 'emphasis delimiters stripped');
237
+ assert.ok(out.some((l) => l.includes('\x1b[')), 'markdown path applies theme colors');
238
+ });
239
+
240
+ test('expanded fenced code in markdown path is highlighted', () => {
241
+ const md = '```js\nconst x = 1;\n```\n';
242
+ const out = formatExpandedResult(md, {});
243
+ const visible = out.map(stripAnsi).join('\n');
244
+ assert.ok(visible.includes('const x = 1;'), 'fence body preserved');
245
+ assert.ok(out.some((l) => l.includes('\x1b[')), 'fenced block carries ANSI');
246
+ });
247
+
248
+ test('read gutter lines skip markdown mode and keep syntax highlight', () => {
249
+ const read = '1\u2192# not a heading\n2\u2192const y = 2;\n';
250
+ const out = formatExpandedResult(read, { pathArg: 'a.mjs' });
251
+ const visible = out.map(stripAnsi);
252
+ assert.ok(visible.some((l) => l.includes('1\u2192# not a heading')), 'source # kept on read lines');
253
+ assert.ok(visible.some((l) => l.includes('const y = 2;')), 'read body preserved');
254
+ assert.ok(out.some((l) => stripAnsi(l).includes('const y = 2;') && l.includes('\x1b[')), 'code line still highlighted');
255
+ });
256
+
257
+ test('shell physical cap keeps newest rows across logical lines', () => {
258
+ withRenderLineCap(5, () => {
259
+ const long = 'W'.repeat(240);
260
+ const logical = formatExpandedResult(`${long}\nNEWEST_TAIL_LINE`, { isShell: true });
261
+ const physical = wrapExpandedResultLines(logical, 32, { isShell: true });
262
+ const visible = physical.map(stripAnsi);
263
+ assert.ok(visible.some((l) => l.includes('NEWEST_TAIL_LINE')), 'newest logical line survives rolling cap');
264
+ assert.ok(physical.length <= 5);
265
+ assert.ok(visible.some((l) => /omitted above/i.test(l)));
266
+ });
267
+ });
268
+
269
+ test('exact physical cap row count is not truncated', () => {
270
+ withRenderLineCap(10, () => {
271
+ const logical = Array.from({ length: 10 }, (_, i) => `row-${i}`).map((l) => formatExpandedResult(l, { isShell: true })[0]);
272
+ const physical = wrapExpandedResultLines(logical, 80, { isShell: true });
273
+ assert.equal(physical.length, 10);
274
+ assert.ok(!physical.some((l) => /omitted above/i.test(stripAnsi(l))));
275
+ });
276
+ });
277
+
278
+ test('physical cap of 1 never returns more than one row', () => {
279
+ withRenderLineCap(1, () => {
280
+ const physical = wrapExpandedResultLines(formatExpandedResult('a\nb\nc', { isShell: true }), 80, { isShell: true });
281
+ assert.ok(physical.length <= 1);
282
+ const physicalNonShell = wrapExpandedResultLines(formatExpandedResult('a\nb\nc', {}), 80, { isShell: false });
283
+ assert.ok(physicalNonShell.length <= 1);
284
+ });
285
+ });
286
+
287
+ test('shell physical cap omitted count includes marker slot', () => {
288
+ withRenderLineCap(5, () => {
289
+ const logical = ['r0', 'r1', 'r2', 'r3', 'r4', 'r5'].map((l) => formatExpandedResult(l, { isShell: true })[0]);
290
+ const physical = wrapExpandedResultLines(logical, 80, { isShell: true });
291
+ const visible = physical.map(stripAnsi);
292
+ assert.equal(physical.length, 5);
293
+ assert.match(visible[0], /2 lines omitted above/);
294
+ assert.deepEqual(visible.slice(1), ['r2', 'r3', 'r4', 'r5']);
295
+ });
296
+ withRenderLineCap(1, () => {
297
+ const logical = Array.from({ length: 6 }, (_, i) => `row${i}`).map((l) => formatExpandedResult(l, { isShell: true })[0]);
298
+ const physical = wrapExpandedResultLines(logical, 80, { isShell: true });
299
+ assert.equal(physical.length, 1);
300
+ assert.match(stripAnsi(physical[0]), /6 lines omitted above/);
301
+ });
302
+ });
303
+
304
+ test('shell logical truncation marker precedes retained tail', () => {
305
+ const many = Array.from({ length: 5000 }, (_, i) => `line${i}`).join('\n');
306
+ const out = formatExpandedResult(many, { isShell: true });
307
+ const visible = out.map(stripAnsi);
308
+ assert.ok(visible.length <= 4001);
309
+ assert.ok(/omitted above/i.test(visible[0]), 'shell marker is first row');
310
+ assert.ok(visible[visible.length - 1].includes('line4999'), 'newest logical line is last');
311
+ assert.ok(!visible[visible.length - 1].includes('re-read a narrower range'));
312
+ });