mixdog 0.9.0 → 0.9.2

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 (240) hide show
  1. package/package.json +10 -3
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/background-task-meta-smoke.mjs +1 -1
  6. package/scripts/bench-run.mjs +262 -0
  7. package/scripts/compact-smoke.mjs +12 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  9. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  10. package/scripts/internal-comms-bench.mjs +727 -0
  11. package/scripts/internal-comms-smoke.mjs +75 -0
  12. package/scripts/lead-workflow-smoke.mjs +4 -4
  13. package/scripts/live-worker-smoke.mjs +9 -9
  14. package/scripts/output-style-bench.mjs +285 -0
  15. package/scripts/output-style-smoke.mjs +13 -10
  16. package/scripts/patch-replay.mjs +90 -0
  17. package/scripts/provider-stream-stall-test.mjs +276 -0
  18. package/scripts/provider-toolcall-test.mjs +599 -1
  19. package/scripts/routing-corpus.mjs +281 -0
  20. package/scripts/session-bench.mjs +1526 -0
  21. package/scripts/session-diag.mjs +595 -0
  22. package/scripts/session-ingest-smoke.mjs +2 -2
  23. package/scripts/task-bench.mjs +207 -0
  24. package/scripts/tool-failures.mjs +6 -6
  25. package/scripts/tool-smoke.mjs +306 -66
  26. package/scripts/toolcall-args-test.mjs +81 -0
  27. package/src/agents/debugger/AGENT.md +4 -4
  28. package/src/agents/heavy-worker/AGENT.md +4 -2
  29. package/src/agents/reviewer/AGENT.md +4 -4
  30. package/src/agents/worker/AGENT.md +4 -2
  31. package/src/app.mjs +10 -6
  32. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  33. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  34. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  35. package/src/headless-role.mjs +14 -14
  36. package/src/help.mjs +1 -0
  37. package/src/lib/mixdog-debug.cjs +0 -22
  38. package/src/lib/plugin-paths.cjs +1 -7
  39. package/src/lib/rules-builder.cjs +34 -56
  40. package/src/mixdog-session-runtime.mjs +710 -319
  41. package/src/output-styles/default.md +12 -7
  42. package/src/output-styles/minimal.md +25 -0
  43. package/src/output-styles/oneline.md +21 -0
  44. package/src/output-styles/simple.md +10 -9
  45. package/src/repl.mjs +12 -4
  46. package/src/rules/agent/00-common.md +7 -5
  47. package/src/rules/agent/30-explorer.md +7 -8
  48. package/src/rules/lead/01-general.md +3 -1
  49. package/src/rules/lead/lead-tool.md +7 -0
  50. package/src/rules/shared/01-tool.md +17 -12
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  52. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  54. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  55. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  56. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  57. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  59. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  60. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  61. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  62. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  65. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
  66. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
  67. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
  75. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  76. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  77. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  78. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
  79. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  80. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  81. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  82. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  83. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  84. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  85. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  86. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  87. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  92. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  99. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  104. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  105. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  106. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  107. package/src/runtime/channels/backends/discord.mjs +99 -9
  108. package/src/runtime/channels/backends/telegram.mjs +501 -0
  109. package/src/runtime/channels/index.mjs +441 -1254
  110. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  111. package/src/runtime/channels/lib/config.mjs +54 -3
  112. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  113. package/src/runtime/channels/lib/executor.mjs +0 -3
  114. package/src/runtime/channels/lib/format.mjs +4 -2
  115. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  116. package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
  117. package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
  118. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  119. package/src/runtime/channels/lib/session-discovery.mjs +0 -4
  120. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  121. package/src/runtime/channels/lib/tool-format.mjs +1 -2
  122. package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
  123. package/src/runtime/channels/lib/webhook.mjs +59 -31
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  126. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  127. package/src/runtime/memory/index.mjs +187 -43
  128. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  129. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  130. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  131. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  132. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  133. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  134. package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
  135. package/src/runtime/memory/lib/memory.mjs +101 -4
  136. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  137. package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
  138. package/src/runtime/memory/lib/session-ingest.mjs +116 -7
  139. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  140. package/src/runtime/memory/tool-defs.mjs +6 -3
  141. package/src/runtime/search/index.mjs +2 -7
  142. package/src/runtime/search/lib/config.mjs +0 -4
  143. package/src/runtime/search/lib/state.mjs +1 -15
  144. package/src/runtime/search/lib/web-tools.mjs +0 -1
  145. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  146. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  147. package/src/runtime/shared/child-spawn-gate.mjs +0 -6
  148. package/src/runtime/shared/config.mjs +9 -0
  149. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  150. package/src/runtime/shared/schedules-store.mjs +21 -19
  151. package/src/runtime/shared/tool-surface.mjs +98 -13
  152. package/src/runtime/shared/transcript-writer.mjs +129 -0
  153. package/src/runtime/shared/update-checker.mjs +214 -0
  154. package/src/standalone/agent-tool.mjs +255 -109
  155. package/src/standalone/channel-admin.mjs +133 -40
  156. package/src/standalone/channel-worker.mjs +8 -291
  157. package/src/standalone/explore-tool.mjs +2 -2
  158. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  159. package/src/standalone/provider-admin.mjs +11 -0
  160. package/src/standalone/seeds.mjs +1 -11
  161. package/src/standalone/usage-dashboard.mjs +1 -1
  162. package/src/tui/App.jsx +2137 -750
  163. package/src/tui/components/ConfirmBar.jsx +47 -0
  164. package/src/tui/components/ContextPanel.jsx +5 -3
  165. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  166. package/src/tui/components/Markdown.jsx +22 -98
  167. package/src/tui/components/Message.jsx +14 -35
  168. package/src/tui/components/Picker.jsx +87 -12
  169. package/src/tui/components/PromptInput.jsx +146 -9
  170. package/src/tui/components/QueuedCommands.jsx +1 -1
  171. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  172. package/src/tui/components/Spinner.jsx +7 -7
  173. package/src/tui/components/StatusLine.jsx +40 -21
  174. package/src/tui/components/TextEntryPanel.jsx +51 -7
  175. package/src/tui/components/ToolExecution.jsx +177 -100
  176. package/src/tui/components/TurnDone.jsx +4 -4
  177. package/src/tui/components/UsagePanel.jsx +1 -1
  178. package/src/tui/components/tool-output-format.mjs +312 -40
  179. package/src/tui/components/tool-output-format.test.mjs +180 -1
  180. package/src/tui/display-width.mjs +69 -0
  181. package/src/tui/display-width.test.mjs +35 -0
  182. package/src/tui/dist/index.mjs +7324 -2393
  183. package/src/tui/engine.mjs +287 -126
  184. package/src/tui/index.jsx +117 -7
  185. package/src/tui/keyboard-protocol.mjs +42 -0
  186. package/src/tui/lib/voice-recorder.mjs +453 -0
  187. package/src/tui/markdown/format-token.mjs +354 -142
  188. package/src/tui/markdown/format-token.test.mjs +155 -17
  189. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  190. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  191. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  192. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  193. package/src/tui/markdown/table-layout.mjs +9 -9
  194. package/src/tui/paste-attachments.mjs +0 -11
  195. package/src/tui/prompt-history-store.mjs +129 -0
  196. package/src/tui/prompt-history-store.test.mjs +52 -0
  197. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  198. package/src/tui/theme.mjs +41 -647
  199. package/src/tui/themes/base.mjs +86 -0
  200. package/src/tui/themes/basic.mjs +85 -0
  201. package/src/tui/themes/catppuccin.mjs +72 -0
  202. package/src/tui/themes/dracula.mjs +70 -0
  203. package/src/tui/themes/everforest.mjs +71 -0
  204. package/src/tui/themes/gruvbox.mjs +71 -0
  205. package/src/tui/themes/index.mjs +71 -0
  206. package/src/tui/themes/indigo.mjs +78 -0
  207. package/src/tui/themes/kanagawa.mjs +80 -0
  208. package/src/tui/themes/light.mjs +81 -0
  209. package/src/tui/themes/nord.mjs +72 -0
  210. package/src/tui/themes/onedark.mjs +16 -0
  211. package/src/tui/themes/rosepine.mjs +70 -0
  212. package/src/tui/themes/teal.mjs +81 -0
  213. package/src/tui/themes/tokyonight.mjs +79 -0
  214. package/src/tui/themes/utils.mjs +106 -0
  215. package/src/tui/themes/warm.mjs +79 -0
  216. package/src/tui/transcript-tool-failures.mjs +13 -2
  217. package/src/ui/markdown.mjs +1 -1
  218. package/src/ui/model-display.mjs +2 -2
  219. package/src/ui/statusline.mjs +26 -27
  220. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
  221. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  222. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  223. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  224. package/src/workflows/default/WORKFLOW.md +39 -12
  225. package/src/workflows/sequential/WORKFLOW.md +46 -0
  226. package/src/workflows/solo/WORKFLOW.md +7 -0
  227. package/vendor/ink/build/display-width.js +62 -0
  228. package/vendor/ink/build/ink.js +154 -20
  229. package/vendor/ink/build/measure-text.js +4 -1
  230. package/vendor/ink/build/output.js +115 -9
  231. package/vendor/ink/build/render-node-to-output.js +4 -1
  232. package/vendor/ink/build/render.js +4 -0
  233. package/src/hooks/lib/permission-rules.cjs +0 -170
  234. package/src/hooks/lib/settings-loader.cjs +0 -112
  235. package/src/lib/hook-pipe-path.cjs +0 -10
  236. package/src/output-styles/extreme-simple.md +0 -20
  237. package/src/rules/lead/04-workflow.md +0 -51
  238. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  239. package/src/workflows/default/workflow.json +0 -13
  240. package/src/workflows/solo/workflow.json +0 -7
@@ -5,19 +5,20 @@
5
5
  * - chalk is forced to truecolor (level 3) so colors render regardless of the
6
6
  * ambient TTY detection (we control the surface).
7
7
  * - The `permission`/code accent and blockquote bar come from our theme.mjs.
8
- * - `code` (fenced) is emitted as a block-colored plain text + EOL (no
9
- * syntax highlighter dependency); `codespan` (inline) gets the accent color.
8
+ * - `code` (fenced) uses cli-highlight + theme syntax palette; `codespan`
9
+ * (inline) gets the accent color.
10
10
  * - `table` is NOT handled here — the React component (MarkdownTable.jsx)
11
11
  * renders tables with proper ink Box layout (hybrid split).
12
12
  * - Hyperlinks/issue-ref linkify are dropped (no OSC-8 dependency); link text
13
13
  * is shown plainly with its URL.
14
14
  */
15
+ import { createRequire } from 'node:module';
15
16
  import { Chalk } from 'chalk';
16
17
  import stripAnsi from 'strip-ansi';
17
- import stringWidth from 'string-width';
18
18
  import wrapAnsi from 'wrap-ansi';
19
19
  import { theme, getThemeVersion } from '../theme.mjs';
20
20
  import { BLOCKQUOTE_BAR, HR_LINE } from '../figures.mjs';
21
+ import { displayWidth } from '../display-width.mjs';
21
22
 
22
23
  // Force truecolor so chalk emits 24-bit SGR even when the ambient level is 0.
23
24
  // ink's <Text> passes these escapes through verbatim.
@@ -26,26 +27,12 @@ const chalk = new Chalk({ level: 3 });
26
27
  // Use \n unconditionally (os.EOL's \r breaks segment mapping).
27
28
  const EOL = '\n';
28
29
 
29
- /** Parse an `rgb(r,g,b)` theme string into a chalk colorizer. */
30
30
  function rgbColor(str) {
31
31
  const m = /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/.exec(String(str || ''));
32
32
  if (!m) return (s) => s;
33
33
  return chalk.rgb(Number(m[1]), Number(m[2]), Number(m[3]));
34
34
  }
35
35
 
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
36
  // Colorizers are derived from the active theme's md* keys. They are cached and
50
37
  // rebuilt only when the theme version changes, so a `/theme` switch takes
51
38
  // effect on the next render without recomputing a chalk fn per token.
@@ -80,8 +67,6 @@ export function extraColorizers() {
80
67
  fenceBorder: rgbColor(theme.mdCodeBlockBorder ?? theme.mdHr),
81
68
  link: rgbColor(theme.mdLink ?? theme.code),
82
69
  linkText: rgbColor(theme.mdLinkText ?? theme.mdCode),
83
- strong: rgbColor(theme.mdStrong ?? theme.mdHeading),
84
- emph: rgbColor(theme.mdEmph ?? theme.mdHeading),
85
70
  // diff/patch
86
71
  diffAdded: rgbColor(theme.mdDiffAdded ?? theme.success),
87
72
  diffRemoved: rgbColor(theme.mdDiffRemoved ?? theme.error),
@@ -99,8 +84,6 @@ export function extraColorizers() {
99
84
  synOperator: rgbColor(theme.syntaxOperator ?? theme.mdCodeBlock),
100
85
  synPunct: rgbColor(theme.syntaxPunctuation ?? theme.mdCodeBlock),
101
86
  body: fallbackBody,
102
- // Truecolor background band for fenced code blocks (per-line wrap).
103
- codeBg: rgbBg(theme.mdCodeBlockBg ?? theme.background),
104
87
  };
105
88
  return _extra;
106
89
  }
@@ -124,14 +107,65 @@ function decodeEntities(s) {
124
107
  }
125
108
 
126
109
  // ── 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 = ' ';
110
+ // Gutter-indented, syntax-highlighted body with NO background band and no ```
111
+ // fences (codex/claude-code convention). Diff/patch languages (and bodies that
112
+ // look like unified diffs) keep the diff highlighter; other languages use
113
+ // cli-highlight (highlight.js) themed from our syntax* palette; unknown
114
+ // languages fall back to flat `mdCodeBlock` body color.
133
115
  const DIFF_LANGS = new Set(['diff', 'patch', 'udiff', 'git-diff', 'gitdiff']);
134
116
 
117
+ // Fixed tab size for fenced-code rendering. The exact value is cosmetic; what
118
+ // matters for correctness is that NO raw \t byte survives into the rendered
119
+ // string. string-width counts a tab as ZERO cells ("Tabs are ignored by
120
+ // design"; \p{Control} is a zero-width cluster), so our wrap/row math believes
121
+ // a tab-bearing code line is narrower than it is. ink clips the transcript
122
+ // viewport at the measured height, but the terminal then EXPANDS the surviving
123
+ // tab to the next tab stop — adding physical columns/rows AFTER the clip, which
124
+ // makes a code line near the bottom edge bleed THROUGH into the prompt box on
125
+ // scroll-up. Expanding tabs to literal spaces here makes the measurement and
126
+ // the terminal agree, so the clip holds.
127
+ const CODE_TAB_SIZE = 2;
128
+
129
+ /**
130
+ * Normalize raw fenced-code source for terminal rendering:
131
+ * - CRLF / lone CR → LF (so wrap sees real logical lines);
132
+ * - TAB → spaces, column-aware to the next CODE_TAB_SIZE stop, so width math
133
+ * matches what the terminal draws and the viewport clip is not bypassed;
134
+ * - other C0 control chars (and DEL) except LF → single space, so stray
135
+ * control bytes cannot trigger terminal-side cursor moves / extra rows.
136
+ */
137
+ function normalizeCodeText(text) {
138
+ const input = String(text ?? '');
139
+ if (input.length === 0) return input;
140
+ const normalizedEol = input.replace(/\r\n?/g, '\n');
141
+ let out = '';
142
+ let col = 0;
143
+ for (const ch of normalizedEol) {
144
+ if (ch === '\n') {
145
+ out += ch;
146
+ col = 0;
147
+ continue;
148
+ }
149
+ if (ch === '\t') {
150
+ const advance = CODE_TAB_SIZE - (col % CODE_TAB_SIZE);
151
+ out += ' '.repeat(advance);
152
+ col += advance;
153
+ continue;
154
+ }
155
+ const cp = ch.codePointAt(0);
156
+ if (cp != null && (cp <= 0x1f || cp === 0x7f)) {
157
+ // Remaining C0 control / DEL: replace with a space so it cannot move the
158
+ // terminal cursor after ink has already accounted for the row.
159
+ out += ' ';
160
+ col += 1;
161
+ continue;
162
+ }
163
+ out += ch;
164
+ col += displayWidth(ch);
165
+ }
166
+ return out;
167
+ }
168
+
135
169
  /** Wrap text to width, ANSI-aware (lockstep with table-layout hard wrap). */
136
170
  function wrapTextToWidth(text, width, options) {
137
171
  if (width <= 0) return [text];
@@ -153,10 +187,10 @@ function hardWrapAnsiLines(text, width) {
153
187
  const out = [];
154
188
  for (const softLine of wrapTextToWidth(input, max, { hard: true })) {
155
189
  let rest = softLine;
156
- while (rest.length > 0 && stringWidth(rest) > max) {
190
+ while (rest.length > 0 && displayWidth(rest) > max) {
157
191
  let take = 1;
158
192
  for (let i = 1; i <= rest.length; i++) {
159
- if (stringWidth(rest.slice(0, i)) <= max) take = i;
193
+ if (displayWidth(rest.slice(0, i)) <= max) take = i;
160
194
  else break;
161
195
  }
162
196
  out.push(rest.slice(0, take));
@@ -167,24 +201,17 @@ function hardWrapAnsiLines(text, width) {
167
201
  return out.length > 0 ? out : [''];
168
202
  }
169
203
 
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);
204
+ /** Wrap one logical code line (ANSI content) to max visible width. */
205
+ function wrapCodeLine(ansiContent, maxLineWidth) {
206
+ const contentMax = Math.max(1, maxLineWidth);
207
+ return hardWrapAnsiLines(ansiContent, contentMax);
181
208
  }
182
209
 
183
210
  /** Reduce render width by a visible prefix (list marker, blockquote bar, etc.). */
184
211
  function contentWidthAfterPrefix(width, prefix) {
185
212
  const base = Number(width) || 0;
186
213
  if (base <= 0) return 0;
187
- return Math.max(8, base - stringWidth(String(prefix ?? '')));
214
+ return Math.max(8, base - displayWidth(String(prefix ?? '')));
188
215
  }
189
216
 
190
217
  /** Normalize a fenced info-string to a bare lowercase language token. */
@@ -266,34 +293,81 @@ function colorizeDiffStatTrailer(line, c) {
266
293
  .replace(/(\d+)(\s+deletions?\(-\))/g, (_, n, rest) => `${c.diffRemoved(n)}${c.diffContext(rest)}`);
267
294
  }
268
295
 
269
- function renderDiffBody(text, c, bandWidth) {
296
+ function collectDiffBodyLines(text, c, bandWidth) {
270
297
  const lines = [];
271
298
  for (const line of String(text ?? '').split(EOL)) {
272
299
  const colored = colorizeDiffLine(line, c);
273
- lines.push(...wrapIndentedCodeLine(colored, bandWidth));
300
+ lines.push(...wrapCodeLine(colored, bandWidth));
301
+ }
302
+ return lines;
303
+ }
304
+
305
+ // ── cli-highlight (highlight.js) ────────────────────────────────────────────
306
+ const requireCliHighlight = createRequire(import.meta.url);
307
+ /** @type {{ highlight: typeof import('cli-highlight').highlight, supportsLanguage: typeof import('cli-highlight').supportsLanguage } | null} */
308
+ let cliHighlightModule = null;
309
+
310
+ function getCliHighlight() {
311
+ if (cliHighlightModule) return cliHighlightModule;
312
+ try {
313
+ const clihl = requireCliHighlight('cli-highlight');
314
+ cliHighlightModule = {
315
+ highlight: clihl.highlight,
316
+ supportsLanguage: clihl.supportsLanguage,
317
+ };
318
+ } catch {
319
+ cliHighlightModule = {
320
+ highlight: null,
321
+ supportsLanguage: () => false,
322
+ };
274
323
  }
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: [],
324
+ return cliHighlightModule;
325
+ }
326
+
327
+ /** Internal family highlight.js language id (when alias alone is insufficient). */
328
+ const FAMILY_TO_HLJS = {
329
+ js: 'javascript',
330
+ ts: 'typescript',
331
+ tsx: 'tsx',
332
+ jsx: 'jsx',
333
+ json: 'json',
334
+ sh: 'bash',
335
+ py: 'python',
336
+ css: 'css',
337
+ html: 'xml',
338
+ go: 'go',
339
+ rust: 'rust',
340
+ java: 'java',
341
+ c: 'cpp',
342
+ ruby: 'ruby',
343
+ sql: 'sql',
344
+ yaml: 'yaml',
345
+ toml: 'ini',
346
+ kotlin: 'kotlin',
347
+ swift: 'swift',
348
+ php: 'php',
349
+ csharp: 'csharp',
350
+ dockerfile: 'dockerfile',
351
+ protobuf: 'protobuf',
352
+ scala: 'scala',
353
+ dart: 'dart',
354
+ lua: 'lua',
355
+ perl: 'perl',
356
+ r: 'r',
357
+ objc: 'objectivec',
358
+ powershell: 'powershell',
359
+ makefile: 'makefile',
360
+ nginx: 'nginx',
361
+ ini: 'ini',
362
+ vim: 'vim',
363
+ haskell: 'haskell',
364
+ elixir: 'elixir',
365
+ clojure: 'clojure',
292
366
  };
293
367
 
294
368
  export const LANG_FAMILY = {
295
369
  js: 'js', javascript: 'js', mjs: 'js', cjs: 'js',
296
- ts: 'js', typescript: 'js', jsx: 'js', tsx: 'js',
370
+ ts: 'ts', typescript: 'ts', jsx: 'jsx', tsx: 'tsx',
297
371
  json: 'json', json5: 'json',
298
372
  bash: 'sh', sh: 'sh', shell: 'sh', zsh: 'sh',
299
373
  python: 'py', py: 'py',
@@ -308,96 +382,226 @@ export const LANG_FAMILY = {
308
382
  sql: 'sql',
309
383
  yaml: 'yaml', yml: 'yaml',
310
384
  toml: 'toml',
385
+ kotlin: 'kotlin', kt: 'kotlin', kts: 'kotlin',
386
+ swift: 'swift',
387
+ php: 'php',
388
+ csharp: 'csharp', cs: 'csharp', 'c#': 'csharp',
389
+ dockerfile: 'dockerfile', docker: 'dockerfile',
390
+ graphql: 'graphql', gql: 'graphql',
391
+ protobuf: 'protobuf', proto: 'protobuf',
392
+ scala: 'scala',
393
+ dart: 'dart',
394
+ lua: 'lua',
395
+ perl: 'perl', pl: 'perl',
396
+ r: 'r', rl: 'r',
397
+ objc: 'objc', 'objective-c': 'objc', 'obj-c': 'objc',
398
+ powershell: 'powershell', ps1: 'powershell', ps: 'powershell', pwsh: 'powershell',
399
+ makefile: 'makefile', make: 'makefile',
400
+ nginx: 'nginx',
401
+ ini: 'ini',
402
+ vim: 'vim',
403
+ haskell: 'haskell', hs: 'haskell',
404
+ elixir: 'elixir', ex: 'elixir', exs: 'elixir',
405
+ clojure: 'clojure', clj: 'clojure',
311
406
  };
312
407
 
313
- // Families whose comment lines / inline comments start with `#`.
314
- const HASH_COMMENT_FAMILIES = new Set(['sh', 'py', 'yaml', 'toml']);
408
+ const HIGHLIGHT_CACHE_MAX = 300;
409
+ const highlightCache = new Map();
315
410
 
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);
411
+ /** @internal Test-only introspection for highlight LRU cache. */
412
+ export function _highlightCacheSizeForTests() {
413
+ return highlightCache.size;
414
+ }
415
+
416
+ let _hljsThemeVersion = -1;
417
+ let _hljsTheme = null;
418
+
419
+ /** Map highlight.js token classes to chalk truecolor fns from the active theme. */
420
+ function buildCliHighlightTheme(c) {
421
+ const plain = (s) => c.body(s);
422
+ return {
423
+ default: plain,
424
+ keyword: c.synKeyword,
425
+ built_in: c.synType,
426
+ type: c.synType,
427
+ literal: c.synKeyword,
428
+ number: c.synNumber,
429
+ regexp: c.synString,
430
+ string: c.synString,
431
+ subst: plain,
432
+ symbol: plain,
433
+ class: c.synType,
434
+ function: c.synFunction,
435
+ title: c.synFunction,
436
+ params: c.synVariable,
437
+ comment: c.synComment,
438
+ doctag: c.synComment,
439
+ meta: c.synComment,
440
+ section: c.synType,
441
+ tag: c.synPunct,
442
+ name: c.synFunction,
443
+ builtin: c.synType,
444
+ attr: c.synType,
445
+ attribute: c.synType,
446
+ variable: c.synVariable,
447
+ selector: c.synKeyword,
448
+ template: c.synVariable,
449
+ bullet: plain,
450
+ code: plain,
451
+ emphasis: plain,
452
+ strong: plain,
453
+ formula: plain,
454
+ link: plain,
455
+ quote: plain,
456
+ addition: c.synString,
457
+ deletion: c.synString,
458
+ };
459
+ }
460
+
461
+ function getCliHighlightTheme() {
462
+ const version = getThemeVersion();
463
+ if (_hljsTheme && _hljsThemeVersion === version) return _hljsTheme;
464
+ _hljsThemeVersion = version;
465
+ _hljsTheme = buildCliHighlightTheme(extraColorizers());
466
+ return _hljsTheme;
467
+ }
468
+
469
+ function hljsLanguageFromFamily(family) {
470
+ if (!family || family === 'md') return null;
471
+ const { supportsLanguage } = getCliHighlight();
472
+ const mapped = FAMILY_TO_HLJS[family];
473
+ if (mapped && supportsLanguage(mapped)) return mapped;
474
+ if (supportsLanguage(family)) return family;
475
+ return null;
476
+ }
477
+
478
+ /** Resolve a fenced info-string to a highlight.js language id. */
479
+ function resolveHljsLanguage(lang) {
480
+ const normalized = normalizeLang(lang);
481
+ if (!normalized) return null;
482
+ const viaFamily = hljsLanguageFromFamily(LANG_FAMILY[normalized]);
483
+ if (viaFamily) return viaFamily;
484
+ const { supportsLanguage } = getCliHighlight();
485
+ return supportsLanguage(normalized) ? normalized : null;
486
+ }
487
+
488
+ /**
489
+ * Highlight source with cli-highlight; returns null when language is unsupported
490
+ * or highlighting fails (caller falls back to flat body color).
491
+ */
492
+ function highlightCodeText(text, hljsLang) {
493
+ const { highlight, supportsLanguage } = getCliHighlight();
494
+ if (!hljsLang || !supportsLanguage(hljsLang) || typeof highlight !== 'function') return null;
495
+ const src = String(text ?? '');
496
+ if (!src.trim()) return null;
497
+ if (!/[A-Za-z0-9]/.test(src)) return null;
498
+ const cacheKey = `${hljsLang}|${getThemeVersion()}|${src}`;
499
+ const cached = highlightCache.get(cacheKey);
500
+ if (cached !== undefined) {
501
+ highlightCache.delete(cacheKey);
502
+ highlightCache.set(cacheKey, cached);
503
+ return cached;
504
+ }
505
+ try {
506
+ const out = highlight(src, {
507
+ language: hljsLang,
508
+ theme: getCliHighlightTheme(),
509
+ ignoreIllegals: true,
510
+ });
511
+ if (highlightCache.size >= HIGHLIGHT_CACHE_MAX) {
512
+ const first = highlightCache.keys().next().value;
513
+ if (first !== undefined) highlightCache.delete(first);
350
514
  }
351
- last = TOKEN_RE.lastIndex;
515
+ highlightCache.set(cacheKey, out);
516
+ return out;
517
+ } catch {
518
+ return null;
352
519
  }
353
- if (last < line.length) out += c.body(line.slice(last));
354
- return out;
355
520
  }
356
521
 
357
- function renderHighlightedBody(text, family, c, bandWidth) {
522
+ /** Highlight a single line (tool expanded output); uses the same theme map. */
523
+ export function highlightCodeLine(line, family, c) {
524
+ const hljsLang = hljsLanguageFromFamily(family);
525
+ const highlighted = highlightCodeText(line, hljsLang);
526
+ if (highlighted != null) return highlighted;
527
+ return line ? c.body(line) : '';
528
+ }
529
+
530
+ /**
531
+ * Highlight a multi-line block in one cli-highlight call; returns one ANSI string per line.
532
+ * On miss, each line is flat-colored with `c.body`.
533
+ */
534
+ export function highlightCodeBlockToLines(text, family, c) {
535
+ const hljsLang = hljsLanguageFromFamily(family);
536
+ const raw = String(text ?? '');
537
+ const highlighted = hljsLang ? highlightCodeText(raw, hljsLang) : null;
538
+ if (highlighted != null) {
539
+ return highlighted.split(EOL);
540
+ }
541
+ return raw.split(EOL).map((line) => (line ? c.body(line) : ''));
542
+ }
543
+
544
+ function collectFlatBodyLines(text, codeBlock, bandWidth) {
358
545
  const lines = [];
359
546
  for (const line of String(text ?? '').split(EOL)) {
360
- const colored = line ? highlightCodeLine(line, family, c) : '';
361
- lines.push(...wrapIndentedCodeLine(colored, bandWidth));
547
+ const colored = codeBlock(line);
548
+ lines.push(...wrapCodeLine(colored, bandWidth));
362
549
  }
363
- return tintCodeLines(lines, c.codeBg);
550
+ return lines;
364
551
  }
365
552
 
553
+ function collectHighlightedBodyLines(text, hljsLang, bandWidth) {
554
+ const highlighted = highlightCodeText(text, hljsLang);
555
+ const { codeBlock } = colorizers();
556
+ const source = highlighted != null
557
+ ? highlighted
558
+ : String(text ?? '').split(EOL).map(codeBlock).join(EOL);
559
+ const lines = [];
560
+ for (const line of source.split(EOL)) {
561
+ lines.push(...wrapCodeLine(line, bandWidth));
562
+ }
563
+ return lines;
564
+ }
565
+
566
+ /** Left gutter that keeps code visually distinct from prose (no bg band). */
567
+ const CODE_GUTTER = ' ';
568
+
366
569
  /**
367
- * Render a fenced `code` token: compact language label, wrapped indented body,
368
- * and language-aware coloring (no visible ``` fence lines).
570
+ * Render a fenced `code` token (codex/claude-code convention): an optional
571
+ * subtle language label row, then the wrapped, syntax/diff/flat-highlighted
572
+ * body. Every row is left-indented by a 2-col gutter. NO background band, no
573
+ * full-width padding, and no visible ``` fence lines.
369
574
  */
370
575
  function renderCodeBlock(token, width = 0) {
371
576
  const { codeBlock } = colorizers();
372
577
  const c = extraColorizers();
373
578
  const lang = normalizeLang(token.lang);
374
- const text = decodeEntities(token.text ?? '');
375
- const bandWidth = Math.max(8, Number(width) || 80);
579
+ const text = normalizeCodeText(decodeEntities(token.text ?? ''));
580
+ const renderWidth = Math.max(8, Number(width) || 80);
581
+ // Wrap content to the render width minus the left gutter so `gutter + content`
582
+ // never overruns the available width.
583
+ const contentWidth = Math.max(1, renderWidth - CODE_GUTTER.length);
376
584
 
377
- let body;
585
+ let bodyLines;
378
586
  if (DIFF_LANGS.has(lang) || (!lang && looksLikeUnifiedDiff(text))) {
379
- body = renderDiffBody(text, c, bandWidth);
587
+ bodyLines = collectDiffBodyLines(text, c, contentWidth);
380
588
  } else {
589
+ const hljsLang = resolveHljsLanguage(lang);
381
590
  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);
591
+ if (hljsLang && family !== 'md') {
592
+ bodyLines = collectHighlightedBodyLines(text, hljsLang, contentWidth);
387
593
  } 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);
594
+ bodyLines = collectFlatBodyLines(text, codeBlock, contentWidth);
395
595
  }
396
596
  }
397
- const langLine = lang
398
- ? `${c.codeBg(c.fenceBorder(lang))}${EOL}`
399
- : '';
400
- return `${langLine}${body}${body ? EOL : ''}`;
597
+ // Indent every body row by the gutter (no bg band).
598
+ const indentedBody = bodyLines.map((line) => `${CODE_GUTTER}${line ?? ''}`);
599
+ // Language label: a subtle (syntax-comment colored) metadata row above the
600
+ // body. Its trimmed visible text is exactly the bare lang token, so consumers
601
+ // can detect the label by trimmed equality.
602
+ const langLine = lang ? `${CODE_GUTTER}${c.synComment(lang)}` : null;
603
+ const rows = [...(langLine ? [langLine] : []), ...indentedBody].join(EOL);
604
+ return `${rows}${rows ? EOL : ''}`;
401
605
  }
402
606
 
403
607
  function numberToLetter(n) {
@@ -445,41 +649,48 @@ function getListNumber(depth, orderedListNumber) {
445
649
  * marked token switch (minus table / hyperlink deps).
446
650
  */
447
651
  export function formatToken(token, listBaseIndent = 0, orderedListNumber = null, parent = null, width = 0, depth = 0) {
448
- const { accent, codeBlock, headingAccent, quoteBorder, quoteText, hrLine } = colorizers();
652
+ const { accent, codeBlock, hrLine } = colorizers();
449
653
  const ex = extraColorizers();
450
654
  switch (token.type) {
451
655
  case 'blockquote': {
452
- const bar = quoteBorder(BLOCKQUOTE_BAR);
656
+ // Block structure carries no color (claude-code policy): a dim bar plus
657
+ // body-colored italic text. Color is reserved for inline codespan/link.
658
+ const bar = chalk.dim(BLOCKQUOTE_BAR);
453
659
  const quotePrefix = `${BLOCKQUOTE_BAR} `;
454
660
  const innerWidth = contentWidthAfterPrefix(width, quotePrefix);
455
661
  const inner = (token.tokens ?? []).map((t) => formatToken(t, 0, null, null, innerWidth)).join('');
456
662
  return inner
457
663
  .split(EOL)
458
- .map((line) =>
459
- stripAnsi(line).trim()
460
- ? `${bar} ${quoteText(chalk.italic(line))}`
461
- : line,
462
- )
664
+ .map((line) => {
665
+ // Padded fenced-code blank rows are space-only; trim() would drop the quote bar.
666
+ if (displayWidth(stripAnsi(line)) === 0) return line;
667
+ return `${bar} ${chalk.italic(line)}`;
668
+ })
463
669
  .join(EOL);
464
670
  }
465
671
  case 'code':
466
- // Fenced block: compact lang label + wrapped indented, colored body.
672
+ // Fenced block: flush-left wrapped body with syntax highlighting.
467
673
  return renderCodeBlock(token, width);
468
674
  case 'codespan':
469
- // inline code
675
+ // inline code — accent color only (no background tint; a bg box behind
676
+ // inline spans reads as awkward against body text, matches claude-code's
677
+ // codespan = color-only treatment).
470
678
  return accent(decodeEntities(token.text));
471
679
  case 'em':
472
- return ex.emph(chalk.italic((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join('')));
680
+ // Italic only no color tint (matches codex/claude-code; a colored em
681
+ // clashes per-theme and reads loud against body prose).
682
+ return chalk.italic((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join(''));
473
683
  case 'strong':
474
- return ex.strong(chalk.bold((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join('')));
684
+ // Bold only no color tint (stays body-colored, just heavier weight).
685
+ return chalk.bold((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join(''));
475
686
  case 'heading':
476
687
  switch (token.depth) {
477
688
  case 1:
478
689
  return (
479
- chalk.bold.italic.underline(headingAccent((token.tokens ?? []).map((t) => formatToken(t)).join(''))) + EOL + EOL
690
+ chalk.bold.italic.underline((token.tokens ?? []).map((t) => formatToken(t)).join('')) + EOL + EOL
480
691
  );
481
692
  default: // h2+
482
- return chalk.bold(headingAccent((token.tokens ?? []).map((t) => formatToken(t)).join(''))) + EOL + EOL;
693
+ return chalk.bold((token.tokens ?? []).map((t) => formatToken(t)).join('')) + EOL + EOL;
483
694
  }
484
695
  case 'hr': {
485
696
  // Span the available content width with a box-drawing rule. width is only
@@ -504,12 +715,12 @@ export function formatToken(token, listBaseIndent = 0, orderedListNumber = null,
504
715
  const OSC8_OPEN = (url) => `\x1b]8;;${url}\x07`;
505
716
  const OSC8_CLOSE = '\x1b]8;;\x07';
506
717
  if (plain && plain !== href) {
507
- const styledLabel = ex.linkText(chalk.underline(linkText));
718
+ const styledLabel = ex.linkText(linkText);
508
719
  return `${OSC8_OPEN(href)}${styledLabel}${OSC8_CLOSE}`;
509
720
  }
510
721
  // No distinct label (empty or equal to href): show the URL itself as the
511
722
  // clickable, visible text.
512
- return `${OSC8_OPEN(href)}${ex.link(chalk.underline(href))}${OSC8_CLOSE}`;
723
+ return `${OSC8_OPEN(href)}${ex.link(href)}${OSC8_CLOSE}`;
513
724
  }
514
725
  case 'list':
515
726
  return token.items
@@ -598,7 +809,8 @@ function formatListItem(token, listBaseIndent, orderedListNumber, parent, depth
598
809
  out += `${markerPrefix.trimEnd()}${EOL}`;
599
810
  firstBlock = false;
600
811
  }
601
- out += formatToken(child, nestedListIndent, null, token, childWidth, depth + 1);
812
+ // Pass outer `width` so the nested list subtracts only its own marker prefix once.
813
+ out += formatToken(child, nestedListIndent, null, token, width, depth + 1);
602
814
  continue;
603
815
  }
604
816