mixdog 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +205 -33
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1222 -90
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2212 -446
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +110 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +76 -28
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  114. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  115. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  116. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  117. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  118. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  119. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  120. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  121. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  122. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  123. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  124. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  125. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  126. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  127. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  128. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  129. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  130. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  137. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  138. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  140. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  141. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  142. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  144. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  145. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  148. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  149. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  150. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  151. package/src/runtime/channels/index.mjs +152 -24
  152. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  153. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  154. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  155. package/src/runtime/channels/lib/webhook.mjs +1 -1
  156. package/src/runtime/channels/tool-defs.mjs +29 -29
  157. package/src/runtime/memory/index.mjs +520 -98
  158. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  159. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  160. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  161. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  162. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  163. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  164. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  165. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  166. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  167. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  168. package/src/runtime/memory/lib/memory.mjs +88 -9
  169. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  170. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  171. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  172. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  173. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  174. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  175. package/src/runtime/memory/tool-defs.mjs +46 -37
  176. package/src/runtime/search/index.mjs +102 -466
  177. package/src/runtime/search/lib/web-tools.mjs +45 -25
  178. package/src/runtime/search/tool-defs.mjs +16 -23
  179. package/src/runtime/shared/abort-controller.mjs +1 -1
  180. package/src/runtime/shared/atomic-file.mjs +4 -3
  181. package/src/runtime/shared/background-tasks.mjs +122 -11
  182. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  183. package/src/runtime/shared/config.mjs +7 -4
  184. package/src/runtime/shared/err-text.mjs +131 -4
  185. package/src/runtime/shared/llm/cost.mjs +2 -2
  186. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  187. package/src/runtime/shared/llm/index.mjs +34 -11
  188. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  189. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  190. package/src/runtime/shared/singleton-owner.mjs +104 -0
  191. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  192. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  193. package/src/runtime/shared/tool-surface.mjs +624 -95
  194. package/src/runtime/shared/user-data-guard.mjs +0 -2
  195. package/src/standalone/agent-task-status.mjs +203 -0
  196. package/src/standalone/agent-task-status.test.mjs +76 -0
  197. package/src/standalone/agent-tool.mjs +1913 -0
  198. package/src/standalone/channel-worker.mjs +370 -14
  199. package/src/standalone/explore-tool.mjs +165 -70
  200. package/src/standalone/folder-dialog.mjs +314 -0
  201. package/src/standalone/hook-bus.mjs +898 -22
  202. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  203. package/src/standalone/projects.mjs +226 -0
  204. package/src/standalone/provider-admin.mjs +41 -24
  205. package/src/standalone/seeds.mjs +2 -69
  206. package/src/standalone/usage-dashboard.mjs +96 -8
  207. package/src/tui/App.jsx +4788 -2151
  208. package/src/tui/components/AnsiText.jsx +39 -28
  209. package/src/tui/components/ContextPanel.jsx +14 -4
  210. package/src/tui/components/Markdown.jsx +43 -77
  211. package/src/tui/components/MarkdownTable.jsx +9 -184
  212. package/src/tui/components/Message.jsx +28 -11
  213. package/src/tui/components/Picker.jsx +95 -56
  214. package/src/tui/components/PromptInput.jsx +367 -239
  215. package/src/tui/components/QueuedCommands.jsx +1 -1
  216. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  217. package/src/tui/components/Spinner.jsx +67 -38
  218. package/src/tui/components/StatusLine.jsx +606 -38
  219. package/src/tui/components/TextEntryPanel.jsx +128 -9
  220. package/src/tui/components/ToolExecution.jsx +592 -362
  221. package/src/tui/components/TurnDone.jsx +3 -3
  222. package/src/tui/components/UsagePanel.jsx +3 -5
  223. package/src/tui/components/tool-output-format.mjs +365 -0
  224. package/src/tui/components/tool-output-format.test.mjs +220 -0
  225. package/src/tui/dist/index.mjs +8826 -2390
  226. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  227. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  228. package/src/tui/engine.mjs +1455 -279
  229. package/src/tui/figures.mjs +21 -40
  230. package/src/tui/index.jsx +75 -31
  231. package/src/tui/input-editing.mjs +25 -0
  232. package/src/tui/markdown/format-token.mjs +511 -68
  233. package/src/tui/markdown/format-token.test.mjs +216 -0
  234. package/src/tui/markdown/render-ansi.mjs +94 -0
  235. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  236. package/src/tui/markdown/stream-fence.mjs +34 -0
  237. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  238. package/src/tui/markdown/table-layout.mjs +250 -0
  239. package/src/tui/paste-attachments.mjs +0 -7
  240. package/src/tui/spinner-verbs.mjs +1 -2
  241. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  242. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  243. package/src/tui/theme.mjs +746 -24
  244. package/src/tui/time-format.mjs +1 -1
  245. package/src/tui/transcript-tool-failures.mjs +67 -0
  246. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  247. package/src/ui/ansi.mjs +1 -2
  248. package/src/ui/markdown.mjs +85 -26
  249. package/src/ui/markdown.test.mjs +70 -0
  250. package/src/ui/model-display.mjs +121 -0
  251. package/src/ui/session-stats.mjs +44 -0
  252. package/src/ui/statusline-context-label.test.mjs +15 -0
  253. package/src/ui/statusline.mjs +386 -178
  254. package/src/ui/tool-card.mjs +2 -13
  255. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  256. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  257. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  258. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  259. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  260. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  261. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  262. package/src/workflows/default/WORKFLOW.md +12 -5
  263. package/src/workflows/default/workflow.json +0 -1
  264. package/src/workflows/solo/WORKFLOW.md +15 -0
  265. package/src/workflows/solo/workflow.json +7 -0
  266. package/vendor/ink/build/output.js +6 -1
  267. package/src/agents/scheduler-task.md +0 -3
  268. package/src/agents/web-researcher/AGENT.md +0 -3
  269. package/src/agents/web-researcher/agent.json +0 -6
  270. package/src/agents/webhook-handler.md +0 -3
  271. package/src/rules/bridge/00-common.md +0 -5
  272. package/src/rules/bridge/30-explorer.md +0 -4
  273. package/src/rules/lead/00-tool-lead.md +0 -5
  274. package/src/rules/shared/00-language.md +0 -3
  275. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  276. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  277. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  278. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  279. package/src/standalone/bridge-tool.mjs +0 -1414
  280. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -1,28 +1,29 @@
1
1
  /**
2
2
  * markdown/format-token.mjs — token → ANSI string renderer.
3
3
  *
4
- * Ported from Claude Code (refs/claude-code/src/utils/markdown.ts: formatToken
5
- * + helpers), adapted for this CLI:
4
+ * Token ANSI string renderer:
6
5
  * - chalk is forced to truecolor (level 3) so colors render regardless of the
7
6
  * ambient TTY detection (we control the surface).
8
7
  * - The `permission`/code accent and blockquote bar come from our theme.mjs.
9
- * - `code` (fenced) is emitted as plain text + EOL (no syntax highlighter
10
- * dependency); `codespan` (inline) gets the accent color, like CC.
8
+ * - `code` (fenced) is emitted as a block-colored plain text + EOL (no
9
+ * syntax highlighter dependency); `codespan` (inline) gets the accent color.
11
10
  * - `table` is NOT handled here — the React component (MarkdownTable.jsx)
12
- * renders tables with proper ink Box layout, mirroring CC's hybrid split.
11
+ * renders tables with proper ink Box layout (hybrid split).
13
12
  * - Hyperlinks/issue-ref linkify are dropped (no OSC-8 dependency); link text
14
13
  * is shown plainly with its URL.
15
14
  */
16
15
  import { Chalk } from 'chalk';
17
16
  import stripAnsi from 'strip-ansi';
18
- import { theme } from '../theme.mjs';
19
- import { BLOCKQUOTE_BAR } from '../figures.mjs';
17
+ import stringWidth from 'string-width';
18
+ import wrapAnsi from 'wrap-ansi';
19
+ import { theme, getThemeVersion } from '../theme.mjs';
20
+ import { BLOCKQUOTE_BAR, HR_LINE } from '../figures.mjs';
20
21
 
21
22
  // Force truecolor so chalk emits 24-bit SGR even when the ambient level is 0.
22
23
  // ink's <Text> passes these escapes through verbatim.
23
24
  const chalk = new Chalk({ level: 3 });
24
25
 
25
- // Use \n unconditionally (CC note: os.EOL's \r breaks segment mapping).
26
+ // Use \n unconditionally (os.EOL's \r breaks segment mapping).
26
27
  const EOL = '\n';
27
28
 
28
29
  /** Parse an `rgb(r,g,b)` theme string into a chalk colorizer. */
@@ -32,8 +33,77 @@ function rgbColor(str) {
32
33
  return chalk.rgb(Number(m[1]), Number(m[2]), Number(m[3]));
33
34
  }
34
35
 
35
- const accent = rgbColor(theme.code); // inline code / codespan accent (CC permission)
36
- const dim = rgbColor(theme.subtle);
36
+ /**
37
+ * Parse an `rgb(r,g,b)` theme string into a TRUECOLOR BACKGROUND wrapper. Emits
38
+ * `48;2;R;G;B` … `49` (bg reset) around the string so AnsiText (case 48) maps it
39
+ * to an ink backgroundColor, giving a code line a tinted band. Returns identity
40
+ * when the string is not a valid rgb() so a missing key never corrupts output.
41
+ */
42
+ function rgbBg(str) {
43
+ const m = /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/.exec(String(str || ''));
44
+ if (!m) return (s) => s;
45
+ const open = `\x1b[48;2;${+m[1]};${+m[2]};${+m[3]}m`;
46
+ return (s) => `${open}${s}\x1b[49m`;
47
+ }
48
+
49
+ // Colorizers are derived from the active theme's md* keys. They are cached and
50
+ // rebuilt only when the theme version changes, so a `/theme` switch takes
51
+ // effect on the next render without recomputing a chalk fn per token.
52
+ let _colorizerVersion = -1;
53
+ let _colorizers = null;
54
+ function colorizers() {
55
+ const version = getThemeVersion();
56
+ if (_colorizers && _colorizerVersion === version) return _colorizers;
57
+ _colorizerVersion = version;
58
+ _colorizers = {
59
+ accent: rgbColor(theme.mdCode), // inline codespan
60
+ codeBlock: rgbColor(theme.mdCodeBlock), // fenced code block body
61
+ headingAccent: rgbColor(theme.mdHeading),
62
+ quoteBorder: rgbColor(theme.mdQuoteBorder),
63
+ quoteText: rgbColor(theme.mdQuote),
64
+ hrLine: rgbColor(theme.mdHr),
65
+ listBullet: rgbColor(theme.mdListBullet),
66
+ };
67
+ return _colorizers;
68
+ }
69
+
70
+ // Extended colorizers (fence border, link, emphasis, diff + syntax classes).
71
+ // Cached on the same theme-version key so a /theme switch rebuilds both maps.
72
+ let _extraVersion = -1;
73
+ let _extra = null;
74
+ export function extraColorizers() {
75
+ const version = getThemeVersion();
76
+ if (_extra && _extraVersion === version) return _extra;
77
+ _extraVersion = version;
78
+ const fallbackBody = rgbColor(theme.mdCodeBlock);
79
+ _extra = {
80
+ fenceBorder: rgbColor(theme.mdCodeBlockBorder ?? theme.mdHr),
81
+ link: rgbColor(theme.mdLink ?? theme.code),
82
+ linkText: rgbColor(theme.mdLinkText ?? theme.mdCode),
83
+ strong: rgbColor(theme.mdStrong ?? theme.mdHeading),
84
+ emph: rgbColor(theme.mdEmph ?? theme.mdHeading),
85
+ // diff/patch
86
+ diffAdded: rgbColor(theme.mdDiffAdded ?? theme.success),
87
+ diffRemoved: rgbColor(theme.mdDiffRemoved ?? theme.error),
88
+ diffHunk: rgbColor(theme.mdDiffHunk ?? theme.code),
89
+ diffHeader: rgbColor(theme.mdDiffHeader ?? theme.mdHeading),
90
+ diffContext: rgbColor(theme.mdDiffContext ?? theme.subtle),
91
+ // syntax classes (fall back to plain code-block body when key missing)
92
+ synComment: rgbColor(theme.syntaxComment ?? theme.subtle),
93
+ synKeyword: rgbColor(theme.syntaxKeyword ?? theme.mdCodeBlock),
94
+ synFunction: rgbColor(theme.syntaxFunction ?? theme.mdCodeBlock),
95
+ synVariable: rgbColor(theme.syntaxVariable ?? theme.mdCodeBlock),
96
+ synString: rgbColor(theme.syntaxString ?? theme.mdCodeBlock),
97
+ synNumber: rgbColor(theme.syntaxNumber ?? theme.mdCodeBlock),
98
+ synType: rgbColor(theme.syntaxType ?? theme.mdCodeBlock),
99
+ synOperator: rgbColor(theme.syntaxOperator ?? theme.mdCodeBlock),
100
+ synPunct: rgbColor(theme.syntaxPunctuation ?? theme.mdCodeBlock),
101
+ body: fallbackBody,
102
+ // Truecolor background band for fenced code blocks (per-line wrap).
103
+ codeBg: rgbBg(theme.mdCodeBlockBg ?? theme.background),
104
+ };
105
+ return _extra;
106
+ }
37
107
 
38
108
  // marked 14 HTML-encodes token.text / codespan.text (`"` → `&quot;`, `&` →
39
109
  // `&amp;`, `<`→`&lt;`, `>`→`&gt;`, `'`→`&#39;`), but we render to a terminal,
@@ -53,80 +123,419 @@ function decodeEntities(s) {
53
123
  .replace(/&amp;/g, '&');
54
124
  }
55
125
 
126
+ // ── Fenced code block rendering ─────────────────────────────────────────────
127
+ // Compact language label (no ``` fences), two-space-indented wrapped body.
128
+ // Diff/patch languages
129
+ // (and code text that clearly looks like a unified diff) get colored +/-/@@
130
+ // lines; other known languages get a conservative regex highlighter; unknown
131
+ // languages fall back to the flat `mdCodeBlock` body color.
132
+ const CODE_BLOCK_INDENT = ' ';
133
+ const DIFF_LANGS = new Set(['diff', 'patch', 'udiff', 'git-diff', 'gitdiff']);
134
+
135
+ /** Wrap text to width, ANSI-aware (lockstep with table-layout hard wrap). */
136
+ function wrapTextToWidth(text, width, options) {
137
+ if (width <= 0) return [text];
138
+ const trimmedText = String(text).trimEnd();
139
+ const wrapped = wrapAnsi(trimmedText, width, {
140
+ hard: options?.hard ?? false,
141
+ trim: false,
142
+ wordWrap: true,
143
+ });
144
+ const lines = wrapped.split('\n').filter((line) => line.length > 0);
145
+ return lines.length > 0 ? lines : [''];
146
+ }
147
+
148
+ /** Hard-wrap so every line satisfies stringWidth(line) <= width. */
149
+ function hardWrapAnsiLines(text, width) {
150
+ const max = Math.max(1, Math.floor(Number(width) || 1));
151
+ const input = String(text ?? '');
152
+ if (!input) return [''];
153
+ const out = [];
154
+ for (const softLine of wrapTextToWidth(input, max, { hard: true })) {
155
+ let rest = softLine;
156
+ while (rest.length > 0 && stringWidth(rest) > max) {
157
+ let take = 1;
158
+ for (let i = 1; i <= rest.length; i++) {
159
+ if (stringWidth(rest.slice(0, i)) <= max) take = i;
160
+ else break;
161
+ }
162
+ out.push(rest.slice(0, take));
163
+ rest = rest.slice(take);
164
+ }
165
+ if (rest.length > 0) out.push(rest);
166
+ }
167
+ return out.length > 0 ? out : [''];
168
+ }
169
+
170
+ /** Wrap one logical code line (ANSI content, no indent) and prefix each segment. */
171
+ function wrapIndentedCodeLine(ansiContent, maxLineWidth) {
172
+ const indentW = stringWidth(CODE_BLOCK_INDENT);
173
+ const contentMax = Math.max(1, maxLineWidth - indentW);
174
+ const segments = hardWrapAnsiLines(ansiContent, contentMax);
175
+ return segments.map((seg) => `${CODE_BLOCK_INDENT}${seg}`);
176
+ }
177
+
178
+ /** Tint code lines without padding to terminal width (no trailing bg band). */
179
+ function tintCodeLines(lines, codeBg) {
180
+ return lines.map((line) => codeBg(line)).join(EOL);
181
+ }
182
+
183
+ /** Reduce render width by a visible prefix (list marker, blockquote bar, etc.). */
184
+ function contentWidthAfterPrefix(width, prefix) {
185
+ const base = Number(width) || 0;
186
+ if (base <= 0) return 0;
187
+ return Math.max(8, base - stringWidth(String(prefix ?? '')));
188
+ }
189
+
190
+ /** Normalize a fenced info-string to a bare lowercase language token. */
191
+ function normalizeLang(lang) {
192
+ return String(lang || '')
193
+ .trim()
194
+ .split(/\s+/)[0]
195
+ .toLowerCase();
196
+ }
197
+
198
+ /**
199
+ * Heuristic: does this code body look like a unified diff/patch? Requires at
200
+ * least one +/- body line AND a hunk header or file header, so ordinary code
201
+ * with leading +/- (rare) is not misclassified.
202
+ */
203
+ export function looksLikeUnifiedDiff(text) {
204
+ const lines = String(text ?? '').split(EOL);
205
+ let hasHunk = false;
206
+ let hasFileHeader = false;
207
+ let hasSign = false;
208
+ let hasStat = false;
209
+ for (const line of lines) {
210
+ if (/^@@ .* @@/.test(line) || /^@@ /.test(line)) hasHunk = true;
211
+ if (/^(\+\+\+ |--- |diff --git |index [0-9a-f]+)/.test(line)) hasFileHeader = true;
212
+ if (/^[+-](?![+-])/.test(line)) hasSign = true;
213
+ // `git diff --stat` summary rows: `path | 4 +-` and the trailer
214
+ // `N files changed, M insertions(+), K deletions(-)`.
215
+ if (DIFF_STAT_FILE_RE.test(line) || DIFF_STAT_SUMMARY_RE.test(line)) hasStat = true;
216
+ }
217
+ return ((hasHunk || hasFileHeader) && hasSign) || hasStat;
218
+ }
219
+
220
+ // `git diff --stat` rows. FILE: `<path> | <count> <+/-/ graph>` (count may be
221
+ // `Bin`); SUMMARY: `N file(s) changed[, M insertion…][, K deletion…]`.
222
+ const DIFF_STAT_FILE_RE = /^(\s*)(.+?)(\s+\|\s+)(Bin\b.*|\d+\s*[+\-]*)\s*$/;
223
+ const DIFF_STAT_SUMMARY_RE = /^\s*\d+\s+files?\s+changed\b/;
224
+
225
+ /** Color a `git diff --stat` file row: dim path/sep, green `+`, red `-`. */
226
+ function colorizeDiffStatLine(line, c) {
227
+ const m = DIFF_STAT_FILE_RE.exec(line);
228
+ if (m) {
229
+ const [, indent, path, sep, tail] = m;
230
+ // The tail is either `Bin …` or `<count> <graph>` where graph is +/-.
231
+ const countMatch = /^(\d+)(\s*)([+\-]*)\s*$/.exec(tail);
232
+ let coloredTail;
233
+ if (countMatch) {
234
+ const [, count, gap, bars] = countMatch;
235
+ const pluses = c.diffAdded('+'.repeat((bars.match(/\+/g) || []).length));
236
+ const minuses = c.diffRemoved('-'.repeat((bars.match(/-/g) || []).length));
237
+ coloredTail = `${c.diffContext(count)}${gap}${pluses}${minuses}`;
238
+ } else {
239
+ coloredTail = c.diffContext(tail);
240
+ }
241
+ return `${indent}${c.diffContext(path)}${c.diffContext(sep)}${coloredTail}`;
242
+ }
243
+ return c.diffContext(line);
244
+ }
245
+
246
+ /** Classify and color a single unified-diff line. */
247
+ export function colorizeDiffLine(line, c) {
248
+ // File/section headers first so +++/--- are never treated as add/remove.
249
+ if (/^(\+\+\+|---)(\s|$)/.test(line)) return c.diffHeader(line);
250
+ if (/^(diff --git |index [0-9a-f]|new file|deleted file|rename |similarity |old mode|new mode)/.test(line)) {
251
+ return c.diffHeader(line);
252
+ }
253
+ if (/^@@/.test(line)) return c.diffHunk(line);
254
+ if (/^\+/.test(line)) return c.diffAdded(line);
255
+ if (/^-/.test(line)) return c.diffRemoved(line);
256
+ if (/^\\ No newline/.test(line)) return c.diffContext(line);
257
+ // `git diff --stat` summary trailer + per-file rows.
258
+ if (DIFF_STAT_SUMMARY_RE.test(line)) return colorizeDiffStatTrailer(line, c);
259
+ if (DIFF_STAT_FILE_RE.test(line)) return colorizeDiffStatLine(line, c);
260
+ return c.diffContext(line);
261
+ }
262
+
263
+ /** Color the `N files changed, M insertions(+), K deletions(-)` trailer. */
264
+ function colorizeDiffStatTrailer(line, c) {
265
+ return line.replace(/(\d+)(\s+insertions?\(\+\))/g, (_, n, rest) => `${c.diffAdded(n)}${c.diffContext(rest)}`)
266
+ .replace(/(\d+)(\s+deletions?\(-\))/g, (_, n, rest) => `${c.diffRemoved(n)}${c.diffContext(rest)}`);
267
+ }
268
+
269
+ function renderDiffBody(text, c, bandWidth) {
270
+ const lines = [];
271
+ for (const line of String(text ?? '').split(EOL)) {
272
+ const colored = colorizeDiffLine(line, c);
273
+ lines.push(...wrapIndentedCodeLine(colored, bandWidth));
274
+ }
275
+ return tintCodeLines(lines, c.codeBg);
276
+ }
277
+
278
+ // ── Lightweight syntax highlighting (regex token classes, no parser) ────────
279
+ const KEYWORDS = {
280
+ js: ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'new', 'class', 'extends', 'super', 'this', 'import', 'export', 'from', 'default', 'async', 'await', 'yield', 'try', 'catch', 'finally', 'throw', 'typeof', 'instanceof', 'in', 'of', 'void', 'delete', 'null', 'undefined', 'true', 'false'],
281
+ py: ['def', 'class', 'return', 'if', 'elif', 'else', 'for', 'while', 'import', 'from', 'as', 'with', 'try', 'except', 'finally', 'raise', 'pass', 'break', 'continue', 'lambda', 'yield', 'global', 'nonlocal', 'async', 'await', 'and', 'or', 'not', 'in', 'is', 'None', 'True', 'False', 'self'],
282
+ sh: ['if', 'then', 'else', 'elif', 'fi', 'for', 'while', 'do', 'done', 'case', 'esac', 'function', 'return', 'in', 'export', 'local', 'echo', 'cd', 'set', 'unset', 'read', 'source'],
283
+ css: [],
284
+ go: ['package', 'import', 'func', 'return', 'if', 'else', 'for', 'range', 'switch', 'case', 'default', 'break', 'continue', 'fallthrough', 'goto', 'defer', 'go', 'select', 'chan', 'map', 'struct', 'interface', 'type', 'var', 'const', 'nil', 'true', 'false', 'iota', 'string', 'int', 'int64', 'float64', 'bool', 'byte', 'rune', 'error'],
285
+ rust: ['fn', 'let', 'mut', 'const', 'static', 'return', 'if', 'else', 'match', 'for', 'while', 'loop', 'break', 'continue', 'struct', 'enum', 'trait', 'impl', 'mod', 'use', 'pub', 'crate', 'self', 'super', 'where', 'as', 'dyn', 'move', 'ref', 'unsafe', 'async', 'await', 'type', 'true', 'false', 'Some', 'None', 'Ok', 'Err', 'Box', 'Vec', 'String', 'usize', 'isize', 'i32', 'u32', 'i64', 'u64', 'f64', 'bool'],
286
+ java: ['public', 'private', 'protected', 'class', 'interface', 'enum', 'extends', 'implements', 'abstract', 'final', 'static', 'void', 'new', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'default', 'break', 'continue', 'try', 'catch', 'finally', 'throw', 'throws', 'import', 'package', 'this', 'super', 'instanceof', 'synchronized', 'volatile', 'transient', 'native', 'int', 'long', 'short', 'byte', 'char', 'float', 'double', 'boolean', 'true', 'false', 'null'],
287
+ c: ['auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do', 'double', 'else', 'enum', 'extern', 'float', 'for', 'goto', 'if', 'inline', 'int', 'long', 'register', 'return', 'short', 'signed', 'sizeof', 'static', 'struct', 'switch', 'typedef', 'union', 'unsigned', 'void', 'volatile', 'while', 'class', 'namespace', 'template', 'public', 'private', 'protected', 'virtual', 'new', 'delete', 'using', 'nullptr', 'true', 'false', 'bool'],
288
+ ruby: ['def', 'end', 'class', 'module', 'if', 'elsif', 'else', 'unless', 'case', 'when', 'then', 'while', 'until', 'for', 'in', 'do', 'begin', 'rescue', 'ensure', 'raise', 'return', 'yield', 'break', 'next', 'redo', 'retry', 'self', 'nil', 'true', 'false', 'and', 'or', 'not', 'require', 'require_relative', 'attr_accessor', 'attr_reader', 'attr_writer', 'puts', 'lambda', 'proc'],
289
+ sql: ['select', 'from', 'where', 'insert', 'into', 'values', 'update', 'set', 'delete', 'create', 'alter', 'drop', 'table', 'view', 'index', 'join', 'inner', 'left', 'right', 'outer', 'full', 'on', 'group', 'by', 'order', 'having', 'limit', 'offset', 'distinct', 'as', 'and', 'or', 'not', 'null', 'is', 'in', 'between', 'like', 'union', 'all', 'primary', 'key', 'foreign', 'references', 'default', 'constraint', 'unique', 'count', 'sum', 'avg', 'min', 'max'],
290
+ yaml: [],
291
+ toml: [],
292
+ };
293
+
294
+ export const LANG_FAMILY = {
295
+ js: 'js', javascript: 'js', mjs: 'js', cjs: 'js',
296
+ ts: 'js', typescript: 'js', jsx: 'js', tsx: 'js',
297
+ json: 'json', json5: 'json',
298
+ bash: 'sh', sh: 'sh', shell: 'sh', zsh: 'sh',
299
+ python: 'py', py: 'py',
300
+ css: 'css', scss: 'css', less: 'css',
301
+ html: 'html', xml: 'html',
302
+ md: 'md', markdown: 'md',
303
+ go: 'go', golang: 'go',
304
+ rust: 'rust', rs: 'rust',
305
+ java: 'java',
306
+ c: 'c', cpp: 'c', 'c++': 'c', cc: 'c', h: 'c', hpp: 'c',
307
+ ruby: 'ruby', rb: 'ruby',
308
+ sql: 'sql',
309
+ yaml: 'yaml', yml: 'yaml',
310
+ toml: 'toml',
311
+ };
312
+
313
+ // Families whose comment lines / inline comments start with `#`.
314
+ const HASH_COMMENT_FAMILIES = new Set(['sh', 'py', 'yaml', 'toml']);
315
+
316
+ /** Highlight a single line for a c-like / scripting family (token scan). */
317
+ export function highlightCodeLine(line, family, c) {
318
+ const kw = new Set(KEYWORDS[family === 'json' ? 'js' : family] ?? []);
319
+ // Comment lines (whole-line) for the common families.
320
+ if (family === 'js' && /^\s*\/\//.test(line)) return c.synComment(line);
321
+ if (HASH_COMMENT_FAMILIES.has(family) && /^\s*#/.test(line)) return c.synComment(line);
322
+ if (family === 'html' && /^\s*<!--/.test(line)) return c.synComment(line);
323
+
324
+ // Token regex: strings, numbers, comments, identifiers, punctuation.
325
+ const TOKEN_RE = /(`(?:\\.|[^`\\])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|(\/\/[^\n]*|#[^\n]*)|(\b\d+(?:\.\d+)?\b)|([A-Za-z_$][A-Za-z0-9_$]*)|([{}()[\].,;:=+\-*/%<>!&|^~?]+)/g;
326
+ let out = '';
327
+ let last = 0;
328
+ let m;
329
+ TOKEN_RE.lastIndex = 0;
330
+ while ((m = TOKEN_RE.exec(line)) !== null) {
331
+ if (m.index > last) out += c.body(line.slice(last, m.index));
332
+ const [, str, comment, num, ident, punct] = m;
333
+ if (str !== undefined) {
334
+ out += c.synString(str);
335
+ } else if (comment !== undefined) {
336
+ // `#` is only a comment for sh/py; for js treat as body.
337
+ if (comment.startsWith('//') ? family === 'js' : HASH_COMMENT_FAMILIES.has(family)) {
338
+ out += c.synComment(comment);
339
+ } else {
340
+ out += c.body(comment);
341
+ }
342
+ } else if (num !== undefined) {
343
+ out += c.synNumber(num);
344
+ } else if (ident !== undefined) {
345
+ if (kw.has(ident)) out += c.synKeyword(ident);
346
+ else if (line[TOKEN_RE.lastIndex] === '(') out += c.synFunction(ident);
347
+ else out += c.body(ident);
348
+ } else if (punct !== undefined) {
349
+ out += c.synOperator(punct);
350
+ }
351
+ last = TOKEN_RE.lastIndex;
352
+ }
353
+ if (last < line.length) out += c.body(line.slice(last));
354
+ return out;
355
+ }
356
+
357
+ function renderHighlightedBody(text, family, c, bandWidth) {
358
+ const lines = [];
359
+ for (const line of String(text ?? '').split(EOL)) {
360
+ const colored = line ? highlightCodeLine(line, family, c) : '';
361
+ lines.push(...wrapIndentedCodeLine(colored, bandWidth));
362
+ }
363
+ return tintCodeLines(lines, c.codeBg);
364
+ }
365
+
366
+ /**
367
+ * Render a fenced `code` token: compact language label, wrapped indented body,
368
+ * and language-aware coloring (no visible ``` fence lines).
369
+ */
370
+ function renderCodeBlock(token, width = 0) {
371
+ const { codeBlock } = colorizers();
372
+ const c = extraColorizers();
373
+ const lang = normalizeLang(token.lang);
374
+ const text = decodeEntities(token.text ?? '');
375
+ const bandWidth = Math.max(8, Number(width) || 80);
376
+
377
+ let body;
378
+ if (DIFF_LANGS.has(lang) || (!lang && looksLikeUnifiedDiff(text))) {
379
+ body = renderDiffBody(text, c, bandWidth);
380
+ } else {
381
+ const family = LANG_FAMILY[lang];
382
+ // Any known family except markdown-in-markdown routes through the regex
383
+ // highlighter (html/json/yaml/toml included). 'md' stays flat (no nested
384
+ // markdown highlighter); unknown langs fall back to the flat body color.
385
+ if (family && family !== 'md') {
386
+ body = renderHighlightedBody(text, family, c, bandWidth);
387
+ } else {
388
+ // Unknown/plain language: flat code-block body color, still indented.
389
+ const lines = [];
390
+ for (const line of text.split(EOL)) {
391
+ const colored = codeBlock(line);
392
+ lines.push(...wrapIndentedCodeLine(colored, bandWidth));
393
+ }
394
+ body = tintCodeLines(lines, c.codeBg);
395
+ }
396
+ }
397
+ const langLine = lang
398
+ ? `${c.codeBg(c.fenceBorder(lang))}${EOL}`
399
+ : '';
400
+ return `${langLine}${body}${body ? EOL : ''}`;
401
+ }
402
+
403
+ function numberToLetter(n) {
404
+ let result = '';
405
+ while (n > 0) {
406
+ n--;
407
+ result = String.fromCharCode(97 + (n % 26)) + result;
408
+ n = Math.floor(n / 26);
409
+ }
410
+ return result;
411
+ }
412
+
413
+ const ROMAN_VALUES = [
414
+ [1000, 'm'], [900, 'cm'], [500, 'd'], [400, 'cd'], [100, 'c'], [90, 'xc'],
415
+ [50, 'l'], [40, 'xl'], [10, 'x'], [9, 'ix'], [5, 'v'], [4, 'iv'], [1, 'i'],
416
+ ];
417
+
418
+ function numberToRoman(n) {
419
+ let result = '';
420
+ for (const [value, numeral] of ROMAN_VALUES) {
421
+ while (n >= value) {
422
+ result += numeral;
423
+ n -= value;
424
+ }
425
+ }
426
+ return result;
427
+ }
428
+
429
+ function getListNumber(depth, orderedListNumber) {
430
+ switch (depth) {
431
+ case 0:
432
+ case 1:
433
+ return String(orderedListNumber);
434
+ case 2:
435
+ return numberToLetter(orderedListNumber);
436
+ case 3:
437
+ return numberToRoman(orderedListNumber);
438
+ default:
439
+ return String(orderedListNumber);
440
+ }
441
+ }
442
+
56
443
  /**
57
444
  * Render a single marked token to an ANSI string.
58
- * Mirrors CC's formatToken switch verbatim (minus table / hyperlink deps).
445
+ * marked token switch (minus table / hyperlink deps).
59
446
  */
60
- export function formatToken(token, listDepth = 0, orderedListNumber = null, parent = null) {
447
+ export function formatToken(token, listBaseIndent = 0, orderedListNumber = null, parent = null, width = 0, depth = 0) {
448
+ const { accent, codeBlock, headingAccent, quoteBorder, quoteText, hrLine } = colorizers();
449
+ const ex = extraColorizers();
61
450
  switch (token.type) {
62
451
  case 'blockquote': {
63
- const inner = (token.tokens ?? []).map((t) => formatToken(t)).join('');
64
- const bar = dim(BLOCKQUOTE_BAR);
452
+ const bar = quoteBorder(BLOCKQUOTE_BAR);
453
+ const quotePrefix = `${BLOCKQUOTE_BAR} `;
454
+ const innerWidth = contentWidthAfterPrefix(width, quotePrefix);
455
+ const inner = (token.tokens ?? []).map((t) => formatToken(t, 0, null, null, innerWidth)).join('');
65
456
  return inner
66
457
  .split(EOL)
67
- .map((line) => (stripAnsi(line).trim() ? `${bar} ${chalk.italic(line)}` : line))
458
+ .map((line) =>
459
+ stripAnsi(line).trim()
460
+ ? `${bar} ${quoteText(chalk.italic(line))}`
461
+ : line,
462
+ )
68
463
  .join(EOL);
69
464
  }
70
465
  case 'code':
71
- // No syntax highlighter emit the code text as-is.
72
- return token.text + EOL;
466
+ // Fenced block: compact lang label + wrapped indented, colored body.
467
+ return renderCodeBlock(token, width);
73
468
  case 'codespan':
74
469
  // inline code
75
470
  return accent(decodeEntities(token.text));
76
471
  case 'em':
77
- return chalk.italic((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join(''));
472
+ return ex.emph(chalk.italic((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join('')));
78
473
  case 'strong':
79
- return chalk.bold((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join(''));
474
+ return ex.strong(chalk.bold((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join('')));
80
475
  case 'heading':
81
476
  switch (token.depth) {
82
477
  case 1:
83
478
  return (
84
- chalk.bold.italic.underline(
85
- (token.tokens ?? []).map((t) => formatToken(t)).join(''),
86
- ) + EOL + EOL
479
+ chalk.bold.italic.underline(headingAccent((token.tokens ?? []).map((t) => formatToken(t)).join(''))) + EOL + EOL
87
480
  );
88
481
  default: // h2+
89
- return chalk.bold((token.tokens ?? []).map((t) => formatToken(t)).join('')) + EOL + EOL;
482
+ return chalk.bold(headingAccent((token.tokens ?? []).map((t) => formatToken(t)).join(''))) + EOL + EOL;
90
483
  }
91
- case 'hr':
92
- return '---';
484
+ case 'hr': {
485
+ // Span the available content width with a box-drawing rule. width is only
486
+ // threaded from the top-level token loop; recursive calls pass 0 and fall
487
+ // back to a sane 80-col rule.
488
+ const w = Math.max(3, Number(width) || 80);
489
+ return hrLine(HR_LINE.repeat(w)) + EOL;
490
+ }
93
491
  case 'image':
94
492
  return token.href;
95
493
  case 'link': {
96
494
  if (token.href.startsWith('mailto:')) {
97
- return token.href.replace(/^mailto:/, '');
495
+ return ex.linkText(token.href.replace(/^mailto:/, ''));
98
496
  }
99
497
  const linkText = (token.tokens ?? []).map((t) => formatToken(t, 0, null, token)).join('');
100
498
  const plain = stripAnsi(linkText);
101
- if (plain && plain !== token.href) {
102
- return `${linkText} (${token.href})`;
499
+ const href = token.href;
500
+ // OSC 8 hyperlink: clickable label, URL hidden in supporting terminals
501
+ // (Windows Terminal / iTerm); other terminals show the label text as-is.
502
+ // AnsiText only strips `\x1b[...m` SGR, so the OSC 8 `\x1b]8;;…\x07`
503
+ // sequences pass through untouched for the terminal to interpret.
504
+ const OSC8_OPEN = (url) => `\x1b]8;;${url}\x07`;
505
+ const OSC8_CLOSE = '\x1b]8;;\x07';
506
+ if (plain && plain !== href) {
507
+ const styledLabel = ex.linkText(chalk.underline(linkText));
508
+ return `${OSC8_OPEN(href)}${styledLabel}${OSC8_CLOSE}`;
103
509
  }
104
- return token.href;
510
+ // No distinct label (empty or equal to href): show the URL itself as the
511
+ // clickable, visible text.
512
+ return `${OSC8_OPEN(href)}${ex.link(chalk.underline(href))}${OSC8_CLOSE}`;
105
513
  }
106
514
  case 'list':
107
515
  return token.items
108
516
  .map((item, index) =>
109
- formatToken(item, listDepth, token.ordered ? token.start + index : null, token),
517
+ formatToken(
518
+ item,
519
+ listBaseIndent,
520
+ token.ordered ? Number(token.start || 1) + index : null,
521
+ token,
522
+ width,
523
+ depth,
524
+ ),
110
525
  )
111
526
  .join('');
112
527
  case 'list_item':
113
- return (token.tokens ?? [])
114
- .map((t) => `${' '.repeat(listDepth)}${formatToken(t, listDepth + 1, orderedListNumber, token)}`)
115
- .join('');
528
+ return formatListItem(token, listBaseIndent, orderedListNumber, parent, depth, width);
116
529
  case 'paragraph':
117
- return (token.tokens ?? []).map((t) => formatToken(t)).join('') + EOL;
530
+ return (token.tokens ?? []).map((t) => formatToken(t, 0, null, null, width)).join('') + EOL;
118
531
  case 'space':
119
532
  return EOL;
120
533
  case 'br':
121
534
  return EOL;
122
535
  case 'text':
123
536
  if (parent?.type === 'link') return decodeEntities(token.text);
124
- if (parent?.type === 'list_item') {
125
- const marker = orderedListNumber === null ? '-' : `${getListNumber(listDepth, orderedListNumber)}.`;
126
- const body = token.tokens
127
- ? token.tokens.map((t) => formatToken(t, listDepth, orderedListNumber, token)).join('')
128
- : decodeEntities(token.text);
129
- return `${marker} ${body}${EOL}`;
537
+ if (token.tokens) {
538
+ return token.tokens.map((t) => formatToken(t, listBaseIndent, orderedListNumber, token, width, depth)).join('');
130
539
  }
131
540
  return decodeEntities(token.text);
132
541
  case 'escape':
@@ -140,44 +549,78 @@ export function formatToken(token, listDepth = 0, orderedListNumber = null, pare
140
549
  }
141
550
  }
142
551
 
143
- /* --- ordered-list numbering (CC: numberToLetter / numberToRoman) ---------- */
144
-
145
- function numberToLetter(n) {
146
- let result = '';
147
- while (n > 0) {
148
- n--;
149
- result = String.fromCharCode(97 + (n % 26)) + result;
150
- n = Math.floor(n / 26);
151
- }
152
- return result;
552
+ function trimTrailingEol(value) {
553
+ return String(value ?? '').replace(/\n+$/g, '');
153
554
  }
154
555
 
155
- const ROMAN_VALUES = [
156
- [1000, 'm'], [900, 'cm'], [500, 'd'], [400, 'cd'], [100, 'c'], [90, 'xc'],
157
- [50, 'l'], [40, 'xl'], [10, 'x'], [9, 'ix'], [5, 'v'], [4, 'iv'], [1, 'i'],
158
- ];
556
+ function prefixLines(value, prefix) {
557
+ return String(value ?? '')
558
+ .split(EOL)
559
+ .map((line) => `${prefix}${line}`)
560
+ .join(EOL);
561
+ }
159
562
 
160
- function numberToRoman(n) {
161
- let result = '';
162
- for (const [value, numeral] of ROMAN_VALUES) {
163
- while (n >= value) { result += numeral; n -= value; }
164
- }
165
- return result;
563
+ function prefixFirstAndRest(value, firstPrefix, restPrefix) {
564
+ const lines = String(value ?? '').split(EOL);
565
+ if (lines.length === 0) return '';
566
+ return [
567
+ `${firstPrefix}${lines[0] ?? ''}`,
568
+ ...lines.slice(1).map((line) => `${restPrefix}${line}`),
569
+ ].join(EOL);
166
570
  }
167
571
 
168
- function getListNumber(listDepth, orderedListNumber) {
169
- switch (listDepth) {
170
- case 0:
171
- case 1: return String(orderedListNumber);
172
- case 2: return numberToLetter(orderedListNumber);
173
- case 3: return numberToRoman(orderedListNumber);
174
- default: return String(orderedListNumber);
572
+ function formatListItem(token, listBaseIndent, orderedListNumber, parent, depth = 0, width = 0) {
573
+ const { listBullet } = colorizers();
574
+ const markerPlain = orderedListNumber === null
575
+ ? '-'
576
+ : `${getListNumber(depth, orderedListNumber)}.`;
577
+ const marker = listBullet(markerPlain);
578
+ const markerPrefix = `${' '.repeat(listBaseIndent)}${marker} `;
579
+ const continuationPrefix = ' '.repeat(stripAnsi(markerPrefix).length);
580
+ const nestedListIndent = continuationPrefix.length;
581
+ const childWidth = contentWidthAfterPrefix(width, continuationPrefix);
582
+ const children = token.tokens ?? [];
583
+ let out = '';
584
+ let firstBlock = true;
585
+
586
+ for (const child of children) {
587
+ if (!child) continue;
588
+
589
+ if (child.type === 'space') {
590
+ if (!firstBlock) {
591
+ out += `${continuationPrefix}${EOL}`;
592
+ }
593
+ continue;
594
+ }
595
+
596
+ if (child.type === 'list') {
597
+ if (firstBlock) {
598
+ out += `${markerPrefix.trimEnd()}${EOL}`;
599
+ firstBlock = false;
600
+ }
601
+ out += formatToken(child, nestedListIndent, null, token, childWidth, depth + 1);
602
+ continue;
603
+ }
604
+
605
+ const rendered = formatToken(child, listBaseIndent, orderedListNumber, token, childWidth, depth);
606
+ const body = trimTrailingEol(rendered);
607
+ if (!body) continue;
608
+
609
+ if (firstBlock) {
610
+ out += `${prefixFirstAndRest(body, markerPrefix, continuationPrefix)}${EOL}`;
611
+ firstBlock = false;
612
+ } else {
613
+ out += `${prefixLines(body, continuationPrefix)}${EOL}`;
614
+ }
175
615
  }
616
+
617
+ if (firstBlock) return `${markerPrefix.trimEnd()}${EOL}`;
618
+ return out;
176
619
  }
177
620
 
178
621
  /**
179
622
  * Pad `content` to `targetWidth` per alignment. `displayWidth` is the visible
180
- * width of `content` (ANSI codes excluded). Ported verbatim from CC padAligned.
623
+ * width of `content` (ANSI codes excluded).
181
624
  */
182
625
  export function padAligned(content, displayWidth, targetWidth, align) {
183
626
  const padding = Math.max(0, targetWidth - displayWidth);