whale-code 6.4.0 → 6.5.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 (187) hide show
  1. package/bin/swagmanager-mcp.js +7 -0
  2. package/dist/cli/app.js +30 -2
  3. package/dist/cli/chat/ChatApp.d.ts +4 -4
  4. package/dist/cli/chat/ChatApp.js +114 -44
  5. package/dist/cli/chat/ChatInput.d.ts +13 -6
  6. package/dist/cli/chat/ChatInput.js +433 -89
  7. package/dist/cli/chat/MemoryManager.d.ts +15 -0
  8. package/dist/cli/chat/MemoryManager.js +61 -0
  9. package/dist/cli/chat/MessageList.d.ts +8 -0
  10. package/dist/cli/chat/MessageList.js +1 -1
  11. package/dist/cli/chat/NodeManager.d.ts +30 -0
  12. package/dist/cli/chat/NodeManager.js +89 -0
  13. package/dist/cli/chat/NodeSelector.d.ts +19 -0
  14. package/dist/cli/chat/NodeSelector.js +37 -0
  15. package/dist/cli/chat/PlanApproval.d.ts +17 -0
  16. package/dist/cli/chat/PlanApproval.js +82 -0
  17. package/dist/cli/chat/SessionManager.d.ts +16 -0
  18. package/dist/cli/chat/SessionManager.js +43 -0
  19. package/dist/cli/chat/SlashMenu.d.ts +38 -0
  20. package/dist/cli/chat/SlashMenu.js +208 -0
  21. package/dist/cli/chat/StatusBar.d.ts +16 -0
  22. package/dist/cli/chat/StatusBar.js +22 -0
  23. package/dist/cli/chat/ThemeSelector.d.ts +14 -0
  24. package/dist/cli/chat/ThemeSelector.js +29 -0
  25. package/dist/cli/chat/ToolIndicator.d.ts +8 -0
  26. package/dist/cli/chat/ToolIndicator.js +33 -9
  27. package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
  28. package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
  29. package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
  30. package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
  31. package/dist/cli/commands/config-cmd.js +4 -25
  32. package/dist/cli/commands/db.d.ts +13 -0
  33. package/dist/cli/commands/db.js +243 -0
  34. package/dist/cli/commands/doctor.js +6 -9
  35. package/dist/cli/commands/mcp.js +1 -20
  36. package/dist/cli/services/agent-events.d.ts +22 -1
  37. package/dist/cli/services/agent-events.js +9 -0
  38. package/dist/cli/services/agent-loop.js +66 -2
  39. package/dist/cli/services/agent-worker-base.js +21 -6
  40. package/dist/cli/services/api-retry.d.ts +25 -0
  41. package/dist/cli/services/api-retry.js +91 -0
  42. package/dist/cli/services/auth-service.d.ts +1 -1
  43. package/dist/cli/services/auth-service.js +40 -19
  44. package/dist/cli/services/background-processes.js +26 -2
  45. package/dist/cli/services/config-store.d.ts +13 -1
  46. package/dist/cli/services/config-store.js +116 -13
  47. package/dist/cli/services/format-server-response.js +12 -6
  48. package/dist/cli/services/ink-resize-fix.d.ts +18 -0
  49. package/dist/cli/services/ink-resize-fix.js +66 -0
  50. package/dist/cli/services/interactive-tools.d.ts +14 -0
  51. package/dist/cli/services/interactive-tools.js +47 -2
  52. package/dist/cli/services/keybinding-manager.js +1 -1
  53. package/dist/cli/services/local-tools.js +35 -2
  54. package/dist/cli/services/server-tools.js +175 -3
  55. package/dist/cli/services/subagent.js +15 -3
  56. package/dist/cli/services/system-prompt.js +5 -3
  57. package/dist/cli/services/task-decomposer.d.ts +35 -0
  58. package/dist/cli/services/task-decomposer.js +199 -0
  59. package/dist/cli/services/team-lead.d.ts +18 -0
  60. package/dist/cli/services/team-lead.js +80 -0
  61. package/dist/cli/services/teammate.js +5 -5
  62. package/dist/cli/services/telemetry.d.ts +8 -2
  63. package/dist/cli/services/telemetry.js +116 -92
  64. package/dist/cli/services/tools/agent-tools.d.ts +1 -0
  65. package/dist/cli/services/tools/agent-tools.js +50 -4
  66. package/dist/cli/services/tools/file-ops.d.ts +2 -0
  67. package/dist/cli/services/tools/file-ops.js +71 -19
  68. package/dist/cli/services/tools/shell-exec.js +22 -12
  69. package/dist/cli/shared/Theme.d.ts +1 -2
  70. package/dist/cli/shared/Theme.js +1 -1
  71. package/dist/cli/shared/WhaleBanner.d.ts +4 -1
  72. package/dist/cli/shared/WhaleBanner.js +12 -8
  73. package/dist/cli/shared/markdown.d.ts +5 -4
  74. package/dist/cli/shared/markdown.js +376 -334
  75. package/dist/cli/shared/theme-manager.d.ts +27 -0
  76. package/dist/cli/shared/theme-manager.js +178 -0
  77. package/dist/cli/shared/theme-presets.d.ts +16 -0
  78. package/dist/cli/shared/theme-presets.js +265 -0
  79. package/dist/index.js +0 -51
  80. package/dist/node/adapters/imessage.d.ts +10 -0
  81. package/dist/node/adapters/imessage.js +45 -6
  82. package/dist/node/cli.js +459 -8
  83. package/dist/node/config.d.ts +17 -0
  84. package/dist/node/gateway-client.d.ts +55 -0
  85. package/dist/node/gateway-client.js +201 -0
  86. package/dist/node/portal/clipboard.d.ts +28 -0
  87. package/dist/node/portal/clipboard.js +183 -0
  88. package/dist/node/portal/discovery.d.ts +29 -0
  89. package/dist/node/portal/discovery.js +61 -0
  90. package/dist/node/portal/forward.d.ts +30 -0
  91. package/dist/node/portal/forward.js +90 -0
  92. package/dist/node/portal/index.d.ts +47 -0
  93. package/dist/node/portal/index.js +250 -0
  94. package/dist/node/portal/multiplexer.d.ts +48 -0
  95. package/dist/node/portal/multiplexer.js +207 -0
  96. package/dist/node/portal/permissions.d.ts +36 -0
  97. package/dist/node/portal/permissions.js +131 -0
  98. package/dist/node/portal/protocol.d.ts +140 -0
  99. package/dist/node/portal/protocol.js +193 -0
  100. package/dist/node/portal/screen.d.ts +18 -0
  101. package/dist/node/portal/screen.js +93 -0
  102. package/dist/node/portal/session.d.ts +68 -0
  103. package/dist/node/portal/session.js +127 -0
  104. package/dist/node/portal/shell.d.ts +26 -0
  105. package/dist/node/portal/shell.js +142 -0
  106. package/dist/node/portal/stream.d.ts +43 -0
  107. package/dist/node/portal/stream.js +90 -0
  108. package/dist/node/portal/transfer.d.ts +33 -0
  109. package/dist/node/portal/transfer.js +231 -0
  110. package/dist/node/portal/ui.d.ts +16 -0
  111. package/dist/node/portal/ui.js +148 -0
  112. package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
  113. package/dist/node/remote-desktop/compile-helper.js +73 -0
  114. package/dist/node/remote-desktop/index.d.ts +67 -0
  115. package/dist/node/remote-desktop/index.js +220 -0
  116. package/dist/node/remote-desktop/protocol.d.ts +96 -0
  117. package/dist/node/remote-desktop/protocol.js +67 -0
  118. package/dist/node/runtime.d.ts +8 -1
  119. package/dist/node/runtime.js +117 -9
  120. package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
  121. package/dist/server/handlers/__test-utils__/test-db.js +128 -0
  122. package/dist/server/handlers/api-keys.js +26 -2
  123. package/dist/server/handlers/browser.d.ts +0 -4
  124. package/dist/server/handlers/browser.js +0 -46
  125. package/dist/server/handlers/catalog.js +37 -14
  126. package/dist/server/handlers/clickhouse.d.ts +10 -0
  127. package/dist/server/handlers/clickhouse.js +215 -0
  128. package/dist/server/handlers/comms.d.ts +308 -4
  129. package/dist/server/handlers/comms.js +444 -11
  130. package/dist/server/handlers/creations.js +1 -1
  131. package/dist/server/handlers/crm.d.ts +54 -8
  132. package/dist/server/handlers/crm.js +353 -68
  133. package/dist/server/handlers/embeddings.js +3 -3
  134. package/dist/server/handlers/enrichment.js +39 -55
  135. package/dist/server/handlers/inventory.js +1 -1
  136. package/dist/server/handlers/kali.d.ts +9 -1
  137. package/dist/server/handlers/kali.js +50 -1
  138. package/dist/server/handlers/media.d.ts +8 -0
  139. package/dist/server/handlers/media.js +902 -0
  140. package/dist/server/handlers/meta-ads.js +6 -3
  141. package/dist/server/handlers/nodes.d.ts +2 -0
  142. package/dist/server/handlers/nodes.js +331 -40
  143. package/dist/server/handlers/operations.d.ts +4 -6
  144. package/dist/server/handlers/operations.js +99 -38
  145. package/dist/server/handlers/platform.js +224 -107
  146. package/dist/server/handlers/remove-bg.d.ts +6 -0
  147. package/dist/server/handlers/remove-bg.js +96 -0
  148. package/dist/server/handlers/storefront.d.ts +6 -0
  149. package/dist/server/handlers/storefront.js +477 -0
  150. package/dist/server/handlers/supply-chain.js +21 -3
  151. package/dist/server/handlers/workflow-steps.js +87 -31
  152. package/dist/server/handlers/workflows.js +4 -1
  153. package/dist/server/index.js +334 -88
  154. package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
  155. package/dist/server/lib/clickhouse-buffer.js +175 -0
  156. package/dist/server/lib/clickhouse-client.d.ts +112 -0
  157. package/dist/server/lib/clickhouse-client.js +141 -0
  158. package/dist/server/lib/coa-renderer.d.ts +91 -0
  159. package/dist/server/lib/coa-renderer.js +411 -0
  160. package/dist/server/lib/compaction-service.js +45 -1
  161. package/dist/server/lib/pdf-renderer.d.ts +143 -0
  162. package/dist/server/lib/pdf-renderer.js +867 -0
  163. package/dist/server/lib/react-pdf-layout.d.ts +40 -0
  164. package/dist/server/lib/react-pdf-layout.js +437 -0
  165. package/dist/server/lib/server-agent-loop.d.ts +2 -0
  166. package/dist/server/lib/server-agent-loop.js +61 -15
  167. package/dist/server/lib/server-subagent.d.ts +3 -0
  168. package/dist/server/lib/server-subagent.js +7 -4
  169. package/dist/server/lib/supabase-client.js +51 -3
  170. package/dist/server/lib/template-resolver.js +14 -4
  171. package/dist/server/lib/utils.js +15 -0
  172. package/dist/server/local-agent-gateway.d.ts +44 -0
  173. package/dist/server/local-agent-gateway.js +389 -49
  174. package/dist/server/providers/anthropic.js +12 -2
  175. package/dist/server/providers/gemini.js +17 -2
  176. package/dist/server/proxy-handlers.js +151 -0
  177. package/dist/server/tool-router.d.ts +2 -2
  178. package/dist/server/tool-router.js +25 -35
  179. package/dist/shared/agent-core.d.ts +5 -2
  180. package/dist/shared/agent-core.js +30 -4
  181. package/dist/shared/api-client.js +54 -3
  182. package/dist/shared/sse-parser.d.ts +1 -1
  183. package/dist/shared/sse-parser.js +5 -2
  184. package/dist/shared/tool-dispatch.js +1 -1
  185. package/package.json +16 -10
  186. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
  187. package/dist/server/handlers/__test-utils__/mock-supabase.js +0 -393
@@ -1,39 +1,42 @@
1
1
  /**
2
- * Markdown rendering — Apple-polished terminal output
2
+ * Markdown rendering — polished terminal output with shiki syntax highlighting
3
3
  *
4
- * Syntax theme: purples, blues, pinks no yellow.
5
- * Financials: green for gains, red for losses/deductions.
6
- * Uses marked + marked-terminal + cli-highlight.
4
+ * Uses shiki for VS Code-quality syntax highlighting.
5
+ * All chalk instances are rebuilt on theme switch via rebuildMarkdownRenderer().
6
+ * Uses marked + marked-terminal for markdown structure.
7
7
  */
8
8
  import { Marked } from "marked";
9
9
  import { markedTerminal } from "marked-terminal";
10
10
  import chalk from "chalk";
11
- import { createRequire } from "module";
12
11
  import { colors } from "./Theme.js";
12
+ import { highlightCode, setRebuildCallback } from "./theme-manager.js";
13
13
  import { diffWords } from "diff";
14
14
  // Note: chalk.level is auto-detected by supports-color.
15
15
  // Apple_Terminal → level 2 (256-color), iTerm.app v3+ → level 3 (24-bit).
16
16
  // Do NOT force level 3 — Terminal.app can't render 24-bit codes (shows gray).
17
- const require = createRequire(import.meta.url);
18
- const { highlight } = require("cli-highlight");
19
17
  // ============================================================================
20
- // Apple Dark palette derived from Theme.tsx (respects ~/.swagmanager/theme.json)
18
+ // Palette factoryrebuilt on theme switch
21
19
  // ============================================================================
22
- const systemBlue = chalk.hex(colors.brand);
23
- const systemCyan = chalk.hex(colors.info);
24
- const systemPink = chalk.hex(colors.pink);
25
- const systemPurple = chalk.hex(colors.purple);
26
- const systemIndigo = chalk.hex(colors.indigo);
27
- const systemGreen = chalk.hex(colors.success);
28
- const systemMint = chalk.hex(colors.mint);
29
- const systemRed = chalk.hex(colors.error);
30
- const systemOrange = chalk.hex(colors.warning);
31
- const text = chalk.hex(colors.text);
32
- const secondary = chalk.hex(colors.secondary);
33
- const tertiary = chalk.hex(colors.tertiary);
34
- const separator = chalk.hex(colors.separator);
35
- const lavender = chalk.hex(colors.lavender);
36
- const roseGold = chalk.hex(colors.roseGold);
20
+ function buildPalette() {
21
+ return {
22
+ systemBlue: chalk.hex(colors.brand),
23
+ systemCyan: chalk.hex(colors.info),
24
+ systemPink: chalk.hex(colors.pink),
25
+ systemPurple: chalk.hex(colors.purple),
26
+ systemIndigo: chalk.hex(colors.indigo),
27
+ systemGreen: chalk.hex(colors.success),
28
+ systemMint: chalk.hex(colors.mint),
29
+ systemRed: chalk.hex(colors.error),
30
+ systemOrange: chalk.hex(colors.warning),
31
+ text: chalk.hex(colors.text),
32
+ secondary: chalk.hex(colors.secondary),
33
+ tertiary: chalk.hex(colors.tertiary),
34
+ separator: chalk.hex(colors.separator),
35
+ lavender: chalk.hex(colors.lavender),
36
+ roseGold: chalk.hex(colors.roseGold),
37
+ };
38
+ }
39
+ let p = buildPalette();
37
40
  // ============================================================================
38
41
  // Terminal width helpers — single source of truth for layout widths
39
42
  // ============================================================================
@@ -49,20 +52,6 @@ export function toolContentWidth() {
49
52
  return termWidth() - 6;
50
53
  }
51
54
  // ============================================================================
52
- // console.warn suppression — safe in single-threaded Node.js
53
- // ============================================================================
54
- /** Suppress console.warn during a synchronous function call. */
55
- function withSuppressedWarnings(fn) {
56
- const orig = console.warn;
57
- console.warn = () => { };
58
- try {
59
- return fn();
60
- }
61
- finally {
62
- console.warn = orig;
63
- }
64
- }
65
- // ============================================================================
66
55
  // OSC 8 hyperlinks — only in terminals that support them
67
56
  // ============================================================================
68
57
  /** Detect if terminal supports OSC 8 clickable hyperlinks */
@@ -83,37 +72,37 @@ function hyperlink(url, text) {
83
72
  // mailto: → clean email address, no protocol prefix, no OSC 8
84
73
  if (url.startsWith("mailto:")) {
85
74
  const email = text || url.slice(7);
86
- return systemCyan(email);
75
+ return p.systemCyan(email);
87
76
  }
88
77
  // tel: → clean phone number
89
78
  if (url.startsWith("tel:")) {
90
- return systemCyan(text || url.slice(4));
79
+ return p.systemCyan(text || url.slice(4));
91
80
  }
92
81
  const display = text || url;
93
82
  // OSC 8 clickable links — only where terminal supports them
94
83
  if (supportsOsc8) {
95
- return `\x1B]8;;${url}\x07${systemCyan.underline(display)}\x1B]8;;\x07`;
84
+ return `\x1B]8;;${url}\x07${p.systemCyan.underline(display)}\x1B]8;;\x07`;
96
85
  }
97
86
  // Fallback: colored underlined text (no escape sequences)
98
- return systemCyan.underline(display);
87
+ return p.systemCyan.underline(display);
99
88
  }
100
89
  // ============================================================================
101
90
  // Path helpers
102
91
  // ============================================================================
103
92
  /** Shorten a file path for code block headers */
104
93
  function shortenPathForHeader(fullPath, maxLen = 40) {
105
- let p = fullPath;
94
+ let fp = fullPath;
106
95
  const cwd = process.cwd();
107
96
  const home = process.env.HOME || "";
108
- if (p.startsWith(cwd + "/"))
109
- p = p.slice(cwd.length + 1);
110
- else if (p.startsWith(cwd))
111
- p = p.slice(cwd.length);
112
- else if (home && p.startsWith(home))
113
- p = "~" + p.slice(home.length);
114
- if (p.length <= maxLen)
115
- return p;
116
- const parts = p.split("/");
97
+ if (fp.startsWith(cwd + "/"))
98
+ fp = fp.slice(cwd.length + 1);
99
+ else if (fp.startsWith(cwd))
100
+ fp = fp.slice(cwd.length);
101
+ else if (home && fp.startsWith(home))
102
+ fp = "~" + fp.slice(home.length);
103
+ if (fp.length <= maxLen)
104
+ return fp;
105
+ const parts = fp.split("/");
117
106
  const file = parts.pop();
118
107
  if (file.length >= maxLen - 4)
119
108
  return "…/" + file.slice(-(maxLen - 4));
@@ -121,64 +110,6 @@ function shortenPathForHeader(fullPath, maxLen = 40) {
121
110
  return parent ? "…/" + parent + "/" + file : "…/" + file;
122
111
  }
123
112
  // ============================================================================
124
- // Syntax highlighting — purples / blues / pinks
125
- // ============================================================================
126
- const appleTheme = {
127
- // Keywords — pink bold
128
- keyword: systemPink.bold,
129
- built_in: systemPurple,
130
- type: systemCyan,
131
- literal: systemIndigo,
132
- number: systemMint,
133
- regexp: systemPink,
134
- // Strings — lavender
135
- string: lavender,
136
- subst: systemCyan,
137
- symbol: systemPurple,
138
- // Functions & classes — blue
139
- class: systemCyan.bold,
140
- function: systemBlue,
141
- title: systemBlue.bold,
142
- params: roseGold,
143
- // Comments — tertiary italic
144
- comment: tertiary.italic,
145
- doctag: secondary.italic,
146
- meta: systemIndigo,
147
- "meta-keyword": systemPink,
148
- "meta-string": lavender,
149
- // Tags (HTML/JSX)
150
- tag: systemPink,
151
- name: systemCyan,
152
- attr: systemPurple,
153
- attribute: systemPurple,
154
- // Variables & properties
155
- variable: systemCyan,
156
- property: systemBlue,
157
- // Diff
158
- addition: systemGreen,
159
- deletion: systemRed,
160
- // Lists & markup
161
- bullet: systemPurple,
162
- code: systemPink,
163
- emphasis: chalk.italic,
164
- strong: chalk.bold,
165
- link: systemCyan.underline,
166
- quote: secondary.italic,
167
- // Selectors (CSS)
168
- "selector-tag": systemPink,
169
- "selector-id": systemBlue,
170
- "selector-class": systemPurple,
171
- "selector-pseudo": systemCyan,
172
- "selector-attr": lavender,
173
- // Template
174
- "template-tag": systemPink,
175
- "template-variable": systemCyan,
176
- // JSON property names — purple
177
- section: systemPurple,
178
- // Fallback
179
- default: (s) => s,
180
- };
181
- // ============================================================================
182
113
  // Financial coloring
183
114
  // ============================================================================
184
115
  function colorizeFinancials(str) {
@@ -186,72 +117,150 @@ function colorizeFinancials(str) {
186
117
  // NOTE: URL hyperlinking is handled by marked's GFM autolink + link/href handlers.
187
118
  // Do NOT add URL patterns here — it causes links to render 3-5x.
188
119
  // Negative dollar amounts → red (-$1,234.56) — require digit after $
189
- .replace(/(-\$\d[\d,]*\.?\d*)/g, (m) => systemRed(m))
120
+ .replace(/(-\$\d[\d,]*\.?\d*)/g, (m) => p.systemRed(m))
190
121
  // Positive dollar amounts → green ($1,234.56) — require digit after $
191
- .replace(/((?:^|[^-])\$\d[\d,]*\.?\d*)/g, (m) => systemGreen(m))
122
+ .replace(/((?:^|[^-])\$\d[\d,]*\.?\d*)/g, (m) => p.systemGreen(m))
192
123
  // Negative percentages → red
193
- .replace(/(-\d+\.?\d*%)/g, (m) => systemRed(m))
124
+ .replace(/(-\d+\.?\d*%)/g, (m) => p.systemRed(m))
194
125
  // Positive percentages → cyan
195
- .replace(/((?:^|[^-])\d+\.?\d*%)/g, (m) => systemCyan(m))
126
+ .replace(/((?:^|[^-])\d+\.?\d*%)/g, (m) => p.systemCyan(m))
196
127
  // Explicit positive → green
197
- .replace(/(\+\$?[\d,]+\.?\d*)/g, (m) => systemGreen(m))
128
+ .replace(/(\+\$?[\d,]+\.?\d*)/g, (m) => p.systemGreen(m))
198
129
  // Financial-specific words → green (no generic words like running/ready/started)
199
- .replace(/\b(profit|revenue|gain|in stock|available)\b/gi, (m) => systemGreen(m))
130
+ .replace(/\b(profit|revenue|gain|in stock|available)\b/gi, (m) => p.systemGreen(m))
200
131
  // Financial/status negatives → red
201
- .replace(/\b(loss|deduction|deficit|expense|out of stock|low stock|overdue|expired|cancelled|failed|error|crashed|EADDRINUSE|ENOENT)\b/gi, (m) => systemRed(m));
132
+ .replace(/\b(loss|deduction|deficit|expense|out of stock|low stock|overdue|expired|cancelled|failed|error|crashed|EADDRINUSE|ENOENT)\b/gi, (m) => p.systemRed(m));
202
133
  }
203
134
  // ============================================================================
204
- // Markdown renderer
135
+ // Markdown renderer factory — rebuilt on theme switch
205
136
  // ============================================================================
206
- // ── Isolated marked instance (no global side effects) ──
207
- const md = new Marked();
208
- md.use(markedTerminal({
209
- // Headings — bold blue
210
- firstHeading: systemBlue.bold,
211
- heading: systemBlue.bold,
212
- // Inline
213
- codespan: systemPink,
214
- strong: text.bold,
215
- em: lavender.italic,
216
- // Blocks
217
- blockquote: secondary.italic,
218
- paragraph: (body) => colorizeFinancials(body),
219
- hr: () => separator("─".repeat(50)),
220
- // Links — OSC 8 clickable (single source of truth for URL rendering)
221
- // NOTE: Only use `link` handler, NOT `href` — having both causes double-hyperlinking
222
- link: (href, _title, text) => hyperlink(href, text !== href ? text : undefined),
223
- // Lists — purple bullets, financial-aware
224
- list: (body, ordered) => {
225
- if (ordered) {
226
- let n = 0;
227
- return body.replace(/^\* /gm, () => {
228
- n++;
229
- return `${systemIndigo(String(n) + ".")} `;
230
- });
231
- }
232
- return body.replace(/^\* /gm, `${systemPurple("●")} `);
233
- },
234
- listitem: (itemText) => {
235
- return colorizeFinancials(itemText);
236
- },
237
- // Layout — adapt to terminal width
238
- reflowText: false,
239
- showSectionPrefix: false,
240
- width: Math.min(120, contentWidth()),
241
- tab: 2,
242
- }, {
243
- // cli-highlight purple/blue/pink syntax theme
244
- theme: appleTheme,
245
- ignoreIllegals: true,
246
- }));
137
+ function buildMarkedInstance() {
138
+ const md = new Marked();
139
+ md.use(markedTerminal({
140
+ // Headings — bold blue
141
+ firstHeading: p.systemBlue.bold,
142
+ heading: p.systemBlue.bold,
143
+ // Inline
144
+ codespan: p.systemPink,
145
+ strong: p.text.bold,
146
+ em: p.lavender.italic,
147
+ // Blocks
148
+ blockquote: p.secondary.italic,
149
+ paragraph: (body) => colorizeFinancials(body),
150
+ hr: () => p.separator("─".repeat(50)),
151
+ // Links — OSC 8 clickable (single source of truth for URL rendering)
152
+ // NOTE: Only use `link` handler, NOT `href` — having both causes double-hyperlinking
153
+ link: (href, _title, text) => hyperlink(href, text !== href ? text : undefined),
154
+ // Lists — purple bullets, financial-aware
155
+ list: (body, ordered) => {
156
+ if (ordered) {
157
+ let n = 0;
158
+ return body.replace(/^\* /gm, () => {
159
+ n++;
160
+ return `${p.systemIndigo(String(n) + ".")} `;
161
+ });
162
+ }
163
+ return body.replace(/^\* /gm, `${p.systemPurple("●")} `);
164
+ },
165
+ listitem: (itemText) => {
166
+ return colorizeFinancials(itemText);
167
+ },
168
+ // Layout — adapt to terminal width
169
+ reflowText: false,
170
+ showSectionPrefix: false,
171
+ width: Math.min(120, contentWidth()),
172
+ tab: 2,
173
+ }));
174
+ // Register chart + table + code block extensions
175
+ md.use({
176
+ renderer: {
177
+ code(token) {
178
+ const rawLang = token.lang || "";
179
+ const code = token.text;
180
+ // Parse lang:subtitle (e.g. "typescript:src/foo.ts")
181
+ const colonIdx = rawLang.indexOf(":");
182
+ const lang = colonIdx > 0 ? rawLang.slice(0, colonIdx) : rawLang;
183
+ const subtitle = colonIdx > 0 ? rawLang.slice(colonIdx + 1) : "";
184
+ if (lang === "chart" || lang === "bar") {
185
+ return renderBarChart(code);
186
+ }
187
+ if (lang === "diff") {
188
+ return renderDiff(code);
189
+ }
190
+ {
191
+ // Command output mode: bash without subtitle = run_command output
192
+ // → no line numbers, wider content area, clean indent
193
+ const isCommandOutput = (lang === "bash" || lang === "terminal") && !subtitle;
194
+ const highlightLang = lang === "terminal" ? "bash" : lang;
195
+ // Build header: ── lang ── subtitle ──────
196
+ const cw = contentWidth();
197
+ const headerWidth = Math.max(20, cw - 6);
198
+ const displayLang = isCommandOutput ? "bash" : lang;
199
+ let header;
200
+ if (displayLang && subtitle) {
201
+ const shortSub = shortenPathForHeader(subtitle, headerWidth - displayLang.length - 10);
202
+ const pad = Math.max(2, headerWidth - displayLang.length - shortSub.length - 6);
203
+ header = p.separator(" ── ") + p.tertiary(displayLang) + p.separator(" ── ") + p.secondary(shortSub) + p.separator(` ${"─".repeat(pad)}`);
204
+ }
205
+ else if (displayLang) {
206
+ const pad = Math.max(2, headerWidth - displayLang.length - 3);
207
+ header = p.separator(" ── ") + p.tertiary(displayLang) + p.separator(` ${"─".repeat(pad)}`);
208
+ }
209
+ else {
210
+ header = p.separator(" ──" + "─".repeat(headerWidth - 2));
211
+ }
212
+ // Calculate max line width to prevent wrapping
213
+ const lineCount = code.split("\n").length;
214
+ const gutterW = isCommandOutput ? 0 : String(lineCount).length;
215
+ const gutterOverhead = isCommandOutput ? 4 : (2 + gutterW + 3); // " " or " 123 │ "
216
+ const maxLineWidth = Math.max(20, cw - gutterOverhead - 2);
217
+ // Pre-truncate lines BEFORE highlighting (avoids cutting ANSI codes)
218
+ const truncatedCode = code.split("\n").map((line) => {
219
+ if (line.length > maxLineWidth) {
220
+ return line.slice(0, maxLineWidth - 1) + "…";
221
+ }
222
+ return line;
223
+ }).join("\n");
224
+ // Highlight with shiki
225
+ const highlighted = highlightLang
226
+ ? highlightCode(truncatedCode, highlightLang)
227
+ : truncatedCode;
228
+ const hLines = highlighted.split("\n");
229
+ if (isCommandOutput) {
230
+ // Command output: no line numbers, 4-space indent
231
+ const body = hLines.map(l => " " + l).join("\n");
232
+ return "\n" + header + "\n" + body + "\n";
233
+ }
234
+ else {
235
+ // Code with line numbers + gutter
236
+ const numbered = hLines.map((l, i) => {
237
+ const num = p.tertiary(String(i + 1).padStart(gutterW));
238
+ return " " + num + p.separator(" │ ") + l;
239
+ }).join("\n");
240
+ return "\n" + header + "\n" + numbered + "\n";
241
+ }
242
+ }
243
+ },
244
+ table(token) {
245
+ return renderTable(token);
246
+ },
247
+ },
248
+ });
249
+ return md;
250
+ }
251
+ let md = buildMarkedInstance();
247
252
  // ============================================================================
248
253
  // Diff renderer — background colors + word-level diff (Claude Code parity)
249
254
  // ============================================================================
250
- // Background colors for diff lines — derived from Theme (256-color safe cube values)
251
- const diffAddedBg = chalk.bgHex(colors.diffAddedBg).white;
252
- const diffRemovedBg = chalk.bgHex(colors.diffRemovedBg).white;
253
- const diffWordAdded = chalk.bgHex(colors.diffWordAdded).whiteBright.bold;
254
- const diffWordRemoved = chalk.bgHex(colors.diffWordRemoved).whiteBright.bold;
255
+ function buildDiffStyles() {
256
+ return {
257
+ diffAddedBg: chalk.bgHex(colors.diffAddedBg).white,
258
+ diffRemovedBg: chalk.bgHex(colors.diffRemovedBg).white,
259
+ diffWordAdded: chalk.bgHex(colors.diffWordAdded).whiteBright.bold,
260
+ diffWordRemoved: chalk.bgHex(colors.diffWordRemoved).whiteBright.bold,
261
+ };
262
+ }
263
+ let d = buildDiffStyles();
255
264
  /** Compute word-level diff between two lines using the `diff` library. */
256
265
  function wordDiff(oldLine, newLine) {
257
266
  const changes = diffWords(oldLine, newLine);
@@ -353,16 +362,16 @@ function renderDiff(code) {
353
362
  const wd = wordDiff(r.content, a.content);
354
363
  // Removed line with word highlights
355
364
  const rPad = Math.max(0, tw - rPrefix.length - r.content.length);
356
- out.push(diffRemovedBg(rPrefix) + renderSegments(wd.old, diffWordRemoved, diffRemovedBg) + diffRemovedBg(" ".repeat(rPad)));
365
+ out.push(d.diffRemovedBg(rPrefix) + renderSegments(wd.old, d.diffWordRemoved, d.diffRemovedBg) + d.diffRemovedBg(" ".repeat(rPad)));
357
366
  // Added line with word highlights
358
367
  const aPad = Math.max(0, tw - aPrefix.length - a.content.length);
359
- out.push(diffAddedBg(aPrefix) + renderSegments(wd.new, diffWordAdded, diffAddedBg) + diffAddedBg(" ".repeat(aPad)));
368
+ out.push(d.diffAddedBg(aPrefix) + renderSegments(wd.new, d.diffWordAdded, d.diffAddedBg) + d.diffAddedBg(" ".repeat(aPad)));
360
369
  }
361
370
  else {
362
371
  // Unpaired remove
363
372
  const raw = rPrefix + r.content;
364
373
  const pad = Math.max(0, tw - raw.length);
365
- out.push(diffRemovedBg(raw + " ".repeat(pad)));
374
+ out.push(d.diffRemovedBg(raw + " ".repeat(pad)));
366
375
  }
367
376
  }
368
377
  // Unpaired adds
@@ -371,7 +380,7 @@ function renderDiff(code) {
371
380
  const prefix = `${String(a.lineNo).padStart(gutterW)} + `;
372
381
  const raw = prefix + a.content;
373
382
  const pad = Math.max(0, tw - raw.length);
374
- out.push(diffAddedBg(raw + " ".repeat(pad)));
383
+ out.push(d.diffAddedBg(raw + " ".repeat(pad)));
375
384
  }
376
385
  continue;
377
386
  }
@@ -380,12 +389,12 @@ function renderDiff(code) {
380
389
  const prefix = `${String(seg.lineNo).padStart(gutterW)} + `;
381
390
  const raw = prefix + seg.content;
382
391
  const pad = Math.max(0, tw - raw.length);
383
- out.push(diffAddedBg(raw + " ".repeat(pad)));
392
+ out.push(d.diffAddedBg(raw + " ".repeat(pad)));
384
393
  i++;
385
394
  continue;
386
395
  }
387
396
  // Context line — dim line number, plain content, no background
388
- const prefix = tertiary(`${String(seg.lineNo).padStart(gutterW)} `);
397
+ const prefix = p.tertiary(`${String(seg.lineNo).padStart(gutterW)} `);
389
398
  out.push(prefix + seg.content);
390
399
  i++;
391
400
  }
@@ -394,16 +403,19 @@ function renderDiff(code) {
394
403
  // ============================================================================
395
404
  // Bar chart renderer — ```chart code blocks
396
405
  // ============================================================================
397
- const barGradient = [
398
- chalk.hex("#BF5AF2"), // purple
399
- chalk.hex("#5E5CE6"), // indigo
400
- chalk.hex("#0A84FF"), // blue
401
- chalk.hex("#64D2FF"), // cyan
402
- chalk.hex("#6AC4DC"), // teal
403
- chalk.hex("#30D158"), // green
404
- chalk.hex("#FF9F0A"), // orange
405
- chalk.hex("#FF375F"), // pink
406
- ];
406
+ function buildBarGradient() {
407
+ return [
408
+ chalk.hex(colors.purple),
409
+ chalk.hex(colors.indigo),
410
+ chalk.hex(colors.brand),
411
+ chalk.hex(colors.info),
412
+ chalk.hex(colors.teal),
413
+ chalk.hex(colors.success),
414
+ chalk.hex(colors.warning),
415
+ chalk.hex(colors.pink),
416
+ ];
417
+ }
418
+ let barGradient = buildBarGradient();
407
419
  function renderBarChart(code) {
408
420
  const lines = code.trim().split("\n").filter(l => l.trim());
409
421
  // Optional title — first line without "label: number" pattern
@@ -428,41 +440,64 @@ function renderBarChart(code) {
428
440
  if (entries.length === 0)
429
441
  return code;
430
442
  const maxVal = Math.max(...entries.map(e => e.value));
431
- const maxLabel = Math.max(...entries.map(e => e.label.length));
432
443
  const maxRaw = Math.max(...entries.map(e => e.raw.length));
433
444
  const cw = contentWidth();
434
- const barWidth = Math.min(36, Math.max(12, cw - 8 - maxLabel - maxRaw));
445
+ // Responsive label width truncate labels to fit
446
+ const idealLabelW = Math.max(...entries.map(e => e.label.length));
447
+ // Fixed overhead: indent(2) + gap(2) + gap(2) + value(maxRaw) + safety(2)
448
+ const fixedOverhead = 2 + 2 + 2 + maxRaw + 2;
449
+ const spaceForLabelAndBar = cw - fixedOverhead;
450
+ // Allocate: label gets up to half, bar gets the rest (min 4)
451
+ const maxLabelW = Math.min(idealLabelW, Math.max(6, Math.floor(spaceForLabelAndBar * 0.4)));
452
+ const barWidth = Math.max(4, spaceForLabelAndBar - maxLabelW);
453
+ // If even 4-char bars don't fit, fall back to compact list (no bars)
454
+ const compact = spaceForLabelAndBar < 12;
435
455
  const out = [];
436
456
  if (title) {
437
- out.push(` ${systemBlue.bold(title)}`);
457
+ const displayTitle = title.length > cw - 4 ? title.slice(0, cw - 5) + "…" : title;
458
+ out.push(` ${p.systemBlue.bold(displayTitle)}`);
438
459
  out.push("");
439
460
  }
440
- for (let i = 0; i < entries.length; i++) {
441
- const e = entries[i];
442
- const ratio = maxVal > 0 ? e.value / maxVal : 0;
443
- const filled = Math.round(ratio * barWidth);
444
- const color = barGradient[i % barGradient.length];
445
- const label = secondary(e.label.padStart(maxLabel));
446
- const bar = color("".repeat(filled)) + chalk.hex("#2C2C2E")("░".repeat(barWidth - filled));
447
- const val = e.raw.includes("$")
448
- ? systemGreen(e.raw.padStart(maxRaw))
449
- : e.raw.includes("%")
450
- ? systemCyan(e.raw.padStart(maxRaw))
451
- : systemMint(e.raw.padStart(maxRaw));
452
- out.push(` ${label} ${bar} ${val}`);
461
+ function colorVal(raw) {
462
+ return raw.includes("$") ? p.systemGreen(raw)
463
+ : raw.includes("%") ? p.systemCyan(raw)
464
+ : p.systemMint(raw);
465
+ }
466
+ if (compact) {
467
+ // Compact mode: just "label value" with color dot
468
+ for (let i = 0; i < entries.length; i++) {
469
+ const e = entries[i];
470
+ const color = barGradient[i % barGradient.length];
471
+ const labelStr = e.label.length > cw - maxRaw - 8
472
+ ? e.label.slice(0, cw - maxRaw - 9) + "…"
473
+ : e.label;
474
+ out.push(` ${color("●")} ${p.secondary(labelStr)} ${colorVal(e.raw)}`);
475
+ }
476
+ }
477
+ else {
478
+ for (let i = 0; i < entries.length; i++) {
479
+ const e = entries[i];
480
+ const ratio = maxVal > 0 ? e.value / maxVal : 0;
481
+ const filled = Math.round(ratio * barWidth);
482
+ const color = barGradient[i % barGradient.length];
483
+ const labelStr = e.label.length > maxLabelW
484
+ ? e.label.slice(0, maxLabelW - 1) + "…"
485
+ : e.label;
486
+ const label = p.secondary(labelStr.padStart(maxLabelW));
487
+ const bar = color("█".repeat(filled)) + chalk.hex("#2C2C2E")("░".repeat(barWidth - filled));
488
+ const val = colorVal(e.raw.padStart(maxRaw));
489
+ out.push(` ${label} ${bar} ${val}`);
490
+ }
453
491
  }
454
492
  return "\n" + out.join("\n") + "\n";
455
493
  }
456
494
  function getRowTone(cells) {
457
495
  for (const cell of cells) {
458
496
  const t = cell.trim();
459
- // Negative financial → red row
460
497
  if (/^-\$[\d,]+/.test(t) || /^-\d+\.?\d*%/.test(t))
461
498
  return "negative";
462
- // Explicit positive delta → green row
463
499
  if (/^\+\d/.test(t) || /^\+\$/.test(t))
464
500
  return "positive";
465
- // Status badges
466
501
  if (/^`?[✕✗]/.test(t) || /cancelled|failed|rejected|error|out of stock|low stock/i.test(t))
467
502
  return "negative";
468
503
  if (/^`?[✓●]/.test(t) || /completed|received|approved|active|success|paid|published/i.test(t))
@@ -470,72 +505,90 @@ function getRowTone(cells) {
470
505
  }
471
506
  return "neutral";
472
507
  }
473
- // Background tints for row-level coloring
474
- const rowBgPositive = chalk.bgHex("#0d1f14"); // subtle green tint
475
- const rowBgNegative = chalk.bgHex("#1f0d10"); // subtle red tint
476
- function colorizeCell(val, isHeader, rowTone = "neutral") {
508
+ /** Detect if a column is numeric (for right-alignment) */
509
+ function isNumericColumn(rows, colIndex) {
510
+ let numericCount = 0;
511
+ let total = 0;
512
+ for (const row of rows) {
513
+ const val = (row[colIndex] || "").trim();
514
+ if (!val)
515
+ continue;
516
+ total++;
517
+ if (/^[+\-]?\$?[\d,]+\.?\d*%?$/.test(val))
518
+ numericCount++;
519
+ }
520
+ return total > 0 && numericCount / total > 0.5;
521
+ }
522
+ /** Blend two hex colors by ratio (0 = colorA, 1 = colorB) */
523
+ function blendHex(a, b, ratio) {
524
+ const parse = (h) => {
525
+ const c = h.replace("#", "");
526
+ return [parseInt(c.slice(0, 2), 16), parseInt(c.slice(2, 4), 16), parseInt(c.slice(4, 6), 16)];
527
+ };
528
+ const [ar, ag, ab] = parse(a);
529
+ const [br, bg, bb] = parse(b);
530
+ const r = Math.round(ar + (br - ar) * ratio);
531
+ const g = Math.round(ag + (bg - ag) * ratio);
532
+ const bv = Math.round(ab + (bb - ab) * ratio);
533
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${bv.toString(16).padStart(2, "0")}`;
534
+ }
535
+ function colorizeCell(val, isHeader) {
477
536
  const trimmed = val.trim();
478
537
  if (!trimmed)
479
- return text("");
538
+ return p.text("");
480
539
  if (isHeader)
481
- return systemIndigo.bold(trimmed);
540
+ return p.systemBlue.bold(trimmed);
482
541
  // Badge format: `✓ status` or `◆ status` or `○ status` or `✕ status`
483
542
  const badgeMatch = trimmed.match(/^`([✓●◆○✕◦])\s+(.+)`$/);
484
543
  if (badgeMatch) {
485
544
  const [, icon, label] = badgeMatch;
486
545
  if (icon === "✓" || icon === "●")
487
- return systemGreen(`${icon} ${label}`);
546
+ return p.systemGreen(`${icon} ${label}`);
488
547
  if (icon === "◆")
489
- return systemCyan(`${icon} ${label}`);
548
+ return p.systemCyan(`${icon} ${label}`);
490
549
  if (icon === "○")
491
- return systemOrange(`${icon} ${label}`);
550
+ return p.systemOrange(`${icon} ${label}`);
492
551
  if (icon === "✕")
493
- return systemRed(`${icon} ${label}`);
494
- return secondary(`${icon} ${label}`);
552
+ return p.systemRed(`${icon} ${label}`);
553
+ return p.secondary(`${icon} ${label}`);
495
554
  }
496
- // Inline code (UUID, SKU, transfer number) — subtle style
555
+ // Inline code (UUID, SKU, transfer number)
497
556
  if (trimmed.startsWith("`") && trimmed.endsWith("`")) {
498
- return systemPurple(trimmed.slice(1, -1));
557
+ return p.systemPurple(trimmed.slice(1, -1));
499
558
  }
500
559
  // Bold text
501
560
  if (trimmed.startsWith("**") && trimmed.endsWith("**")) {
502
- return text.bold(trimmed.slice(2, -2));
561
+ return p.text.bold(trimmed.slice(2, -2));
503
562
  }
504
563
  // Negative values → red
505
564
  if (/^-\$?[\d,]+\.?\d*$/.test(trimmed) || /^-\d+\.?\d*%$/.test(trimmed)) {
506
- return systemRed(trimmed);
565
+ return p.systemRed(trimmed);
507
566
  }
508
567
  // Positive financial → green
509
568
  if (/^\+?\$[\d,]+\.?\d*$/.test(trimmed) || /^\$[\d,]+\.?\d*$/.test(trimmed)) {
510
- return systemGreen(trimmed);
569
+ return p.systemGreen(trimmed);
511
570
  }
512
571
  // Percentages → cyan
513
572
  if (/^\d+\.?\d*%$/.test(trimmed)) {
514
- return systemCyan(trimmed);
573
+ return p.systemCyan(trimmed);
515
574
  }
516
575
  // Plain numbers → mint
517
576
  if (/^[\d,]+\.?\d*$/.test(trimmed)) {
518
- return systemMint(trimmed);
577
+ return p.systemMint(trimmed);
519
578
  }
520
579
  // Status words
521
580
  if (/^(active|success|complete|approved|in stock|available)/i.test(trimmed)) {
522
- return systemGreen(trimmed);
581
+ return p.systemGreen(trimmed);
523
582
  }
524
583
  if (/^(inactive|error|failed|cancelled|out of stock|low|overdue|expired)/i.test(trimmed)) {
525
- return systemRed(trimmed);
584
+ return p.systemRed(trimmed);
526
585
  }
527
586
  if (/^(pending|draft|processing)/i.test(trimmed)) {
528
- return systemOrange(trimmed);
529
- }
530
- // Apply row tone tint to text cells
531
- if (rowTone === "positive")
532
- return rowBgPositive(text(trimmed));
533
- if (rowTone === "negative")
534
- return rowBgNegative(text(trimmed));
535
- return text(trimmed);
587
+ return p.systemOrange(trimmed);
588
+ }
589
+ return p.text(trimmed);
536
590
  }
537
591
  function renderTable(token) {
538
- // Extract cell text from token — handles both inline tokens and plain text
539
592
  function getCellText(cell) {
540
593
  if (!cell)
541
594
  return "";
@@ -552,136 +605,125 @@ function renderTable(token) {
552
605
  const rows = (token.rows || []).map((row) => row.map((cell) => getCellText(cell)));
553
606
  if (headers.length === 0)
554
607
  return "";
555
- // Responsive column widths based on terminal width
556
608
  const cw = contentWidth();
557
609
  const N = headers.length;
558
- // Overhead: " ╭" (3) + N+1 border chars + N*2 cell padding + "╮"
559
- const overhead = 3 + (N + 1) + (N * 2);
610
+ // ── Check if box table can fit ──
611
+ // Minimum box table width: indent(2) + borders(N+1) + N * (minCol + 2 padding)
612
+ const MIN_COL = 3;
613
+ const minBoxWidth = 2 + (N + 1) + N * (MIN_COL + 2);
614
+ if (minBoxWidth > cw) {
615
+ // ── Card layout — graceful fallback for narrow terminals ──
616
+ return renderTableCards(headers, rows, cw);
617
+ }
618
+ // ── Box table layout ──
619
+ const overhead = 2 + 1 + (N - 1) + 1 + (N * 2);
560
620
  const availableForContent = cw - overhead;
561
- // Scale minimum column width down for narrow terminals
562
- const minCol = cw < 70 ? 3 : cw < 90 ? 4 : 6;
621
+ const minCol = cw < 70 ? MIN_COL : cw < 90 ? 5 : 6;
563
622
  const maxPerCol = Math.max(minCol, Math.floor(availableForContent / N));
623
+ // Detect numeric columns for right-alignment
624
+ const numericCols = headers.map((_, i) => isNumericColumn(rows, i));
625
+ // Size columns: fit content up to maxPerCol, respect minCol
564
626
  const colWidths = headers.map((h, i) => {
565
627
  const dataMax = rows.reduce((max, row) => Math.max(max, (row[i] || "").length), 0);
566
628
  return Math.min(maxPerCol, Math.max(minCol, h.length, dataMax) + 2);
567
629
  });
568
- const border = chalk.hex("#48484A");
630
+ // Double-check total width won't overflow (defensive)
631
+ const totalWidth = 2 + 1 + colWidths.reduce((s, w) => s + w + 2, 0) + (N - 1) + 1;
632
+ if (totalWidth > cw) {
633
+ // Shrink largest columns until we fit
634
+ let excess = totalWidth - cw;
635
+ while (excess > 0) {
636
+ const maxIdx = colWidths.indexOf(Math.max(...colWidths));
637
+ if (colWidths[maxIdx] <= MIN_COL)
638
+ break; // can't shrink further
639
+ colWidths[maxIdx]--;
640
+ excess--;
641
+ }
642
+ }
643
+ // ── Colors ──
644
+ const borderColor = blendHex(colors.separator, colors.brand, 0.25);
645
+ const border = chalk.hex(borderColor);
646
+ const headerBg = chalk.bgHex(blendHex(colors.panel, colors.brand, 0.15));
647
+ const evenRowBg = chalk.bgHex(blendHex(colors.panel, colors.text, 0.04));
648
+ const oddRowBg = chalk.bgHex(colors.panel);
649
+ const positiveBg = chalk.bgHex(blendHex(colors.panel, colors.success, 0.12));
650
+ const negativeBg = chalk.bgHex(blendHex(colors.panel, colors.error, 0.12));
569
651
  const out = [];
570
- // Top border: ╭──────┬──────╮
652
+ // Top border
571
653
  out.push(border(" ╭" + colWidths.map((w) => "─".repeat(w + 2)).join("┬") + "╮"));
572
- // Header row (truncate headers to fit)
573
- const hdrLine = headers.map((h, i) => {
654
+ // Header row
655
+ const hdrCells = headers.map((h, i) => {
574
656
  const display = h.length > colWidths[i] ? h.slice(0, colWidths[i] - 1) + "…" : h;
575
- return " " + systemIndigo.bold(display.padEnd(colWidths[i])) + " ";
657
+ const padded = numericCols[i] ? display.padStart(colWidths[i]) : display.padEnd(colWidths[i]);
658
+ return headerBg(" " + chalk.hex(colors.brand).bold(padded) + " ");
576
659
  }).join(border("│"));
577
- out.push(border(" │") + hdrLine + border("│"));
578
- // Header/body divider: ├──────┼──────┤
579
- out.push(border(" ├" + colWidths.map((w) => "".repeat(w + 2)).join("") + "┤"));
580
- // Data rows (truncate values to fit, with row-level background tinting)
581
- for (const row of rows) {
660
+ out.push(border(" │") + hdrCells + border("│"));
661
+ // Header divider
662
+ out.push(border(" ├" + colWidths.map((w) => "".repeat(w + 2)).join("") + "┤"));
663
+ // Data rows
664
+ for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
665
+ const row = rows[rowIdx];
582
666
  const tone = getRowTone(row);
667
+ const rowBg = tone === "positive" ? positiveBg
668
+ : tone === "negative" ? negativeBg
669
+ : rowIdx % 2 === 0 ? evenRowBg : oddRowBg;
583
670
  const cells = headers.map((_, i) => {
584
671
  const raw = row[i] || "";
585
672
  const display = raw.length > colWidths[i] ? raw.slice(0, colWidths[i] - 1) + "…" : raw;
586
- const colored = colorizeCell(display, false, tone);
673
+ const colored = colorizeCell(display, false);
587
674
  const extraPad = Math.max(0, colWidths[i] - display.length);
588
- const cellContent = " " + colored + " ".repeat(extraPad) + " ";
589
- // Apply subtle background tint to the entire cell for positive/negative rows
590
- if (tone === "positive")
591
- return rowBgPositive(cellContent);
592
- if (tone === "negative")
593
- return rowBgNegative(cellContent);
594
- return cellContent;
675
+ if (numericCols[i]) {
676
+ return rowBg(" ".repeat(extraPad) + " " + colored + " ");
677
+ }
678
+ return rowBg(" " + colored + " ".repeat(extraPad) + " ");
595
679
  }).join(border("│"));
596
680
  out.push(border(" │") + cells + border("│"));
597
681
  }
598
- // Bottom border: ╰──────┴──────╯
682
+ // Bottom border
599
683
  out.push(border(" ╰" + colWidths.map((w) => "─".repeat(w + 2)).join("┴") + "╯"));
600
684
  return "\n" + out.join("\n") + "\n";
601
685
  }
602
- // Register chart + table extensions intercepts before markedTerminal
603
- md.use({
604
- renderer: {
605
- code(token) {
606
- const rawLang = token.lang || "";
607
- const code = token.text;
608
- // Parse lang:subtitle (e.g. "typescript:src/foo.ts")
609
- const colonIdx = rawLang.indexOf(":");
610
- const lang = colonIdx > 0 ? rawLang.slice(0, colonIdx) : rawLang;
611
- const subtitle = colonIdx > 0 ? rawLang.slice(colonIdx + 1) : "";
612
- if (lang === "chart" || lang === "bar") {
613
- return renderBarChart(code);
614
- }
615
- if (lang === "diff") {
616
- return renderDiff(code);
617
- }
618
- {
619
- // Command output mode: bash without subtitle = run_command output
620
- // no line numbers, wider content area, clean indent
621
- const isCommandOutput = (lang === "bash" || lang === "terminal") && !subtitle;
622
- const highlightLang = lang === "terminal" ? "bash" : lang;
623
- // Build header: ── lang ── subtitle ──────
624
- const cw = contentWidth();
625
- const headerWidth = Math.max(20, cw - 6);
626
- const displayLang = isCommandOutput ? "bash" : lang;
627
- let header;
628
- if (displayLang && subtitle) {
629
- const shortSub = shortenPathForHeader(subtitle, headerWidth - displayLang.length - 10);
630
- const pad = Math.max(2, headerWidth - displayLang.length - shortSub.length - 6);
631
- header = separator(" ── ") + tertiary(displayLang) + separator(" ── ") + secondary(shortSub) + separator(` ${"─".repeat(pad)}`);
632
- }
633
- else if (displayLang) {
634
- const pad = Math.max(2, headerWidth - displayLang.length - 3);
635
- header = separator(" ── ") + tertiary(displayLang) + separator(` ${"─".repeat(pad)}`);
636
- }
637
- else {
638
- header = separator(" ──" + "─".repeat(headerWidth - 2));
639
- }
640
- // Calculate max line width to prevent wrapping
641
- const lineCount = code.split("\n").length;
642
- const gutterW = isCommandOutput ? 0 : String(lineCount).length;
643
- const gutterOverhead = isCommandOutput ? 4 : (2 + gutterW + 3); // " " or " 123 │ "
644
- const maxLineWidth = Math.max(20, cw - gutterOverhead - 2);
645
- // Pre-truncate lines BEFORE highlighting (avoids cutting ANSI codes)
646
- const truncatedCode = code.split("\n").map((line) => {
647
- if (line.length > maxLineWidth) {
648
- return line.slice(0, maxLineWidth - 1) + "…";
649
- }
650
- return line;
651
- }).join("\n");
652
- let highlighted;
653
- if (highlightLang) {
654
- try {
655
- highlighted = withSuppressedWarnings(() => highlight(truncatedCode, { language: highlightLang, ignoreIllegals: true, theme: appleTheme }));
656
- }
657
- catch {
658
- highlighted = truncatedCode;
659
- }
660
- }
661
- else {
662
- highlighted = truncatedCode;
663
- }
664
- const hLines = highlighted.split("\n");
665
- if (isCommandOutput) {
666
- // Command output: no line numbers, 4-space indent
667
- const body = hLines.map(l => " " + l).join("\n");
668
- return "\n" + header + "\n" + body + "\n";
669
- }
670
- else {
671
- // Code with line numbers + gutter
672
- const numbered = hLines.map((l, i) => {
673
- const num = tertiary(String(i + 1).padStart(gutterW));
674
- return " " + num + separator(" │ ") + l;
675
- }).join("\n");
676
- return "\n" + header + "\n" + numbered + "\n";
677
- }
678
- }
679
- },
680
- table(token) {
681
- return renderTable(token);
682
- },
683
- },
684
- });
686
+ /** Card layout fallback renders each row as a key: value block. Never overflows. */
687
+ function renderTableCards(headers, rows, cw) {
688
+ const border = chalk.hex(blendHex(colors.separator, colors.brand, 0.25));
689
+ const out = [];
690
+ const innerW = Math.max(8, cw - 6); // indent(2) + border(2) + padding(2)
691
+ const divider = border(" " + "─".repeat(innerW + 2));
692
+ // Find longest header for label alignment
693
+ const maxHdrLen = Math.min(Math.max(...headers.map(h => h.length)), Math.floor(innerW * 0.4));
694
+ out.push(divider);
695
+ for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
696
+ const row = rows[rowIdx];
697
+ if (rowIdx > 0)
698
+ out.push(divider);
699
+ for (let i = 0; i < headers.length; i++) {
700
+ const hdr = headers[i].length > maxHdrLen
701
+ ? headers[i].slice(0, maxHdrLen - 1) + "…"
702
+ : headers[i];
703
+ const val = row[i] || "";
704
+ const maxValLen = innerW - maxHdrLen - 3; // " label value"
705
+ const displayVal = val.length > maxValLen
706
+ ? val.slice(0, maxValLen - 1) + ""
707
+ : val;
708
+ const labelStr = p.tertiary(hdr.padEnd(maxHdrLen));
709
+ const valStr = colorizeCell(displayVal, false);
710
+ out.push(` ${labelStr} ${valStr}`);
711
+ }
712
+ }
713
+ out.push(divider);
714
+ return "\n" + out.join("\n") + "\n";
715
+ }
716
+ // ============================================================================
717
+ // Rebuild called on theme switch to refresh all chalk instances
718
+ // ============================================================================
719
+ export function rebuildMarkdownRenderer() {
720
+ p = buildPalette();
721
+ d = buildDiffStyles();
722
+ barGradient = buildBarGradient();
723
+ md = buildMarkedInstance();
724
+ }
725
+ // Register rebuild callback with theme manager
726
+ setRebuildCallback(rebuildMarkdownRenderer);
685
727
  // ============================================================================
686
728
  // Streaming fence closure — state-tracking approach
687
729
  // ============================================================================