whale-code 6.4.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.
- package/README.md +95 -0
- package/bin/swag-agent.js +9 -0
- package/bin/swagmanager-mcp.js +321 -0
- package/dist/cli/app.d.ts +26 -0
- package/dist/cli/app.js +64 -0
- package/dist/cli/chat/AgentSelector.d.ts +14 -0
- package/dist/cli/chat/AgentSelector.js +14 -0
- package/dist/cli/chat/ChatApp.d.ts +9 -0
- package/dist/cli/chat/ChatApp.js +267 -0
- package/dist/cli/chat/ChatInput.d.ts +39 -0
- package/dist/cli/chat/ChatInput.js +509 -0
- package/dist/cli/chat/MarkdownText.d.ts +10 -0
- package/dist/cli/chat/MarkdownText.js +20 -0
- package/dist/cli/chat/MessageList.d.ts +37 -0
- package/dist/cli/chat/MessageList.js +80 -0
- package/dist/cli/chat/ModelSelector.d.ts +20 -0
- package/dist/cli/chat/ModelSelector.js +73 -0
- package/dist/cli/chat/RewindViewer.d.ts +26 -0
- package/dist/cli/chat/RewindViewer.js +185 -0
- package/dist/cli/chat/StoreSelector.d.ts +14 -0
- package/dist/cli/chat/StoreSelector.js +24 -0
- package/dist/cli/chat/StreamingText.d.ts +12 -0
- package/dist/cli/chat/StreamingText.js +12 -0
- package/dist/cli/chat/SubagentPanel.d.ts +45 -0
- package/dist/cli/chat/SubagentPanel.js +110 -0
- package/dist/cli/chat/TeamPanel.d.ts +21 -0
- package/dist/cli/chat/TeamPanel.js +42 -0
- package/dist/cli/chat/ToolIndicator.d.ts +25 -0
- package/dist/cli/chat/ToolIndicator.js +436 -0
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +39 -0
- package/dist/cli/chat/hooks/useAgentLoop.js +382 -0
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +37 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +387 -0
- package/dist/cli/commands/config-cmd.d.ts +10 -0
- package/dist/cli/commands/config-cmd.js +99 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +172 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.js +278 -0
- package/dist/cli/commands/mcp.d.ts +12 -0
- package/dist/cli/commands/mcp.js +162 -0
- package/dist/cli/login/LoginApp.d.ts +7 -0
- package/dist/cli/login/LoginApp.js +157 -0
- package/dist/cli/print-mode.d.ts +31 -0
- package/dist/cli/print-mode.js +202 -0
- package/dist/cli/serve-mode.d.ts +37 -0
- package/dist/cli/serve-mode.js +636 -0
- package/dist/cli/services/agent-definitions.d.ts +25 -0
- package/dist/cli/services/agent-definitions.js +91 -0
- package/dist/cli/services/agent-events.d.ts +178 -0
- package/dist/cli/services/agent-events.js +175 -0
- package/dist/cli/services/agent-loop.d.ts +90 -0
- package/dist/cli/services/agent-loop.js +762 -0
- package/dist/cli/services/agent-worker-base.d.ts +97 -0
- package/dist/cli/services/agent-worker-base.js +220 -0
- package/dist/cli/services/auth-service.d.ts +30 -0
- package/dist/cli/services/auth-service.js +160 -0
- package/dist/cli/services/background-processes.d.ts +126 -0
- package/dist/cli/services/background-processes.js +318 -0
- package/dist/cli/services/browser-auth.d.ts +24 -0
- package/dist/cli/services/browser-auth.js +180 -0
- package/dist/cli/services/claude-md-loader.d.ts +16 -0
- package/dist/cli/services/claude-md-loader.js +58 -0
- package/dist/cli/services/config-store.d.ts +47 -0
- package/dist/cli/services/config-store.js +79 -0
- package/dist/cli/services/debug-log.d.ts +10 -0
- package/dist/cli/services/debug-log.js +52 -0
- package/dist/cli/services/error-logger.d.ts +58 -0
- package/dist/cli/services/error-logger.js +269 -0
- package/dist/cli/services/file-history.d.ts +21 -0
- package/dist/cli/services/file-history.js +83 -0
- package/dist/cli/services/format-server-response.d.ts +16 -0
- package/dist/cli/services/format-server-response.js +440 -0
- package/dist/cli/services/git-context.d.ts +11 -0
- package/dist/cli/services/git-context.js +66 -0
- package/dist/cli/services/hooks.d.ts +85 -0
- package/dist/cli/services/hooks.js +258 -0
- package/dist/cli/services/interactive-tools.d.ts +125 -0
- package/dist/cli/services/interactive-tools.js +260 -0
- package/dist/cli/services/keybinding-manager.d.ts +52 -0
- package/dist/cli/services/keybinding-manager.js +115 -0
- package/dist/cli/services/local-tools.d.ts +22 -0
- package/dist/cli/services/local-tools.js +697 -0
- package/dist/cli/services/lsp-manager.d.ts +18 -0
- package/dist/cli/services/lsp-manager.js +717 -0
- package/dist/cli/services/mcp-client.d.ts +48 -0
- package/dist/cli/services/mcp-client.js +157 -0
- package/dist/cli/services/memory-manager.d.ts +16 -0
- package/dist/cli/services/memory-manager.js +57 -0
- package/dist/cli/services/model-manager.d.ts +18 -0
- package/dist/cli/services/model-manager.js +71 -0
- package/dist/cli/services/model-router.d.ts +26 -0
- package/dist/cli/services/model-router.js +149 -0
- package/dist/cli/services/permission-modes.d.ts +13 -0
- package/dist/cli/services/permission-modes.js +43 -0
- package/dist/cli/services/rewind.d.ts +84 -0
- package/dist/cli/services/rewind.js +194 -0
- package/dist/cli/services/ripgrep.d.ts +28 -0
- package/dist/cli/services/ripgrep.js +138 -0
- package/dist/cli/services/sandbox.d.ts +29 -0
- package/dist/cli/services/sandbox.js +97 -0
- package/dist/cli/services/server-tools.d.ts +61 -0
- package/dist/cli/services/server-tools.js +543 -0
- package/dist/cli/services/session-persistence.d.ts +23 -0
- package/dist/cli/services/session-persistence.js +99 -0
- package/dist/cli/services/subagent-worker.d.ts +19 -0
- package/dist/cli/services/subagent-worker.js +41 -0
- package/dist/cli/services/subagent.d.ts +47 -0
- package/dist/cli/services/subagent.js +647 -0
- package/dist/cli/services/system-prompt.d.ts +7 -0
- package/dist/cli/services/system-prompt.js +198 -0
- package/dist/cli/services/team-lead.d.ts +73 -0
- package/dist/cli/services/team-lead.js +512 -0
- package/dist/cli/services/team-state.d.ts +77 -0
- package/dist/cli/services/team-state.js +398 -0
- package/dist/cli/services/teammate.d.ts +31 -0
- package/dist/cli/services/teammate.js +689 -0
- package/dist/cli/services/telemetry.d.ts +61 -0
- package/dist/cli/services/telemetry.js +209 -0
- package/dist/cli/services/tools/agent-tools.d.ts +14 -0
- package/dist/cli/services/tools/agent-tools.js +347 -0
- package/dist/cli/services/tools/file-ops.d.ts +15 -0
- package/dist/cli/services/tools/file-ops.js +487 -0
- package/dist/cli/services/tools/search-tools.d.ts +8 -0
- package/dist/cli/services/tools/search-tools.js +186 -0
- package/dist/cli/services/tools/shell-exec.d.ts +10 -0
- package/dist/cli/services/tools/shell-exec.js +168 -0
- package/dist/cli/services/tools/task-manager.d.ts +28 -0
- package/dist/cli/services/tools/task-manager.js +209 -0
- package/dist/cli/services/tools/web-tools.d.ts +11 -0
- package/dist/cli/services/tools/web-tools.js +395 -0
- package/dist/cli/setup/SetupApp.d.ts +9 -0
- package/dist/cli/setup/SetupApp.js +191 -0
- package/dist/cli/shared/MatrixIntro.d.ts +4 -0
- package/dist/cli/shared/MatrixIntro.js +83 -0
- package/dist/cli/shared/Theme.d.ts +74 -0
- package/dist/cli/shared/Theme.js +127 -0
- package/dist/cli/shared/WhaleBanner.d.ts +10 -0
- package/dist/cli/shared/WhaleBanner.js +12 -0
- package/dist/cli/shared/markdown.d.ts +21 -0
- package/dist/cli/shared/markdown.js +756 -0
- package/dist/cli/status/StatusApp.d.ts +4 -0
- package/dist/cli/status/StatusApp.js +105 -0
- package/dist/cli/stores/StoreApp.d.ts +7 -0
- package/dist/cli/stores/StoreApp.js +81 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +538 -0
- package/dist/local-agent/connection.d.ts +48 -0
- package/dist/local-agent/connection.js +332 -0
- package/dist/local-agent/discovery.d.ts +18 -0
- package/dist/local-agent/discovery.js +146 -0
- package/dist/local-agent/executor.d.ts +34 -0
- package/dist/local-agent/executor.js +241 -0
- package/dist/local-agent/index.d.ts +14 -0
- package/dist/local-agent/index.js +198 -0
- package/dist/node/adapters/base.d.ts +35 -0
- package/dist/node/adapters/base.js +10 -0
- package/dist/node/adapters/discord.d.ts +29 -0
- package/dist/node/adapters/discord.js +299 -0
- package/dist/node/adapters/email.d.ts +23 -0
- package/dist/node/adapters/email.js +218 -0
- package/dist/node/adapters/imessage.d.ts +17 -0
- package/dist/node/adapters/imessage.js +118 -0
- package/dist/node/adapters/slack.d.ts +26 -0
- package/dist/node/adapters/slack.js +259 -0
- package/dist/node/adapters/sms.d.ts +23 -0
- package/dist/node/adapters/sms.js +161 -0
- package/dist/node/adapters/telegram.d.ts +17 -0
- package/dist/node/adapters/telegram.js +101 -0
- package/dist/node/adapters/webchat.d.ts +27 -0
- package/dist/node/adapters/webchat.js +160 -0
- package/dist/node/adapters/whatsapp.d.ts +28 -0
- package/dist/node/adapters/whatsapp.js +230 -0
- package/dist/node/cli.d.ts +2 -0
- package/dist/node/cli.js +325 -0
- package/dist/node/config.d.ts +17 -0
- package/dist/node/config.js +31 -0
- package/dist/node/runtime.d.ts +50 -0
- package/dist/node/runtime.js +351 -0
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +11 -0
- package/dist/server/handlers/__test-utils__/mock-supabase.js +393 -0
- package/dist/server/handlers/analytics.d.ts +17 -0
- package/dist/server/handlers/analytics.js +266 -0
- package/dist/server/handlers/api-keys.d.ts +6 -0
- package/dist/server/handlers/api-keys.js +221 -0
- package/dist/server/handlers/billing.d.ts +33 -0
- package/dist/server/handlers/billing.js +272 -0
- package/dist/server/handlers/browser.d.ts +10 -0
- package/dist/server/handlers/browser.js +517 -0
- package/dist/server/handlers/catalog.d.ts +99 -0
- package/dist/server/handlers/catalog.js +976 -0
- package/dist/server/handlers/comms.d.ts +254 -0
- package/dist/server/handlers/comms.js +588 -0
- package/dist/server/handlers/creations.d.ts +6 -0
- package/dist/server/handlers/creations.js +479 -0
- package/dist/server/handlers/crm.d.ts +89 -0
- package/dist/server/handlers/crm.js +538 -0
- package/dist/server/handlers/discovery.d.ts +6 -0
- package/dist/server/handlers/discovery.js +288 -0
- package/dist/server/handlers/embeddings.d.ts +92 -0
- package/dist/server/handlers/embeddings.js +197 -0
- package/dist/server/handlers/enrichment.d.ts +8 -0
- package/dist/server/handlers/enrichment.js +768 -0
- package/dist/server/handlers/image-gen.d.ts +6 -0
- package/dist/server/handlers/image-gen.js +409 -0
- package/dist/server/handlers/inventory.d.ts +319 -0
- package/dist/server/handlers/inventory.js +447 -0
- package/dist/server/handlers/kali.d.ts +10 -0
- package/dist/server/handlers/kali.js +210 -0
- package/dist/server/handlers/llm-providers.d.ts +6 -0
- package/dist/server/handlers/llm-providers.js +673 -0
- package/dist/server/handlers/local-agent.d.ts +6 -0
- package/dist/server/handlers/local-agent.js +118 -0
- package/dist/server/handlers/meta-ads.d.ts +111 -0
- package/dist/server/handlers/meta-ads.js +2279 -0
- package/dist/server/handlers/nodes.d.ts +33 -0
- package/dist/server/handlers/nodes.js +699 -0
- package/dist/server/handlers/operations.d.ts +138 -0
- package/dist/server/handlers/operations.js +131 -0
- package/dist/server/handlers/platform.d.ts +23 -0
- package/dist/server/handlers/platform.js +227 -0
- package/dist/server/handlers/supply-chain.d.ts +19 -0
- package/dist/server/handlers/supply-chain.js +327 -0
- package/dist/server/handlers/transcription.d.ts +17 -0
- package/dist/server/handlers/transcription.js +121 -0
- package/dist/server/handlers/video-gen.d.ts +6 -0
- package/dist/server/handlers/video-gen.js +466 -0
- package/dist/server/handlers/voice.d.ts +8 -0
- package/dist/server/handlers/voice.js +1146 -0
- package/dist/server/handlers/workflow-steps.d.ts +86 -0
- package/dist/server/handlers/workflow-steps.js +2349 -0
- package/dist/server/handlers/workflows.d.ts +7 -0
- package/dist/server/handlers/workflows.js +989 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2427 -0
- package/dist/server/lib/batch-client.d.ts +80 -0
- package/dist/server/lib/batch-client.js +467 -0
- package/dist/server/lib/code-worker-pool.d.ts +31 -0
- package/dist/server/lib/code-worker-pool.js +224 -0
- package/dist/server/lib/code-worker.d.ts +1 -0
- package/dist/server/lib/code-worker.js +188 -0
- package/dist/server/lib/compaction-service.d.ts +32 -0
- package/dist/server/lib/compaction-service.js +162 -0
- package/dist/server/lib/logger.d.ts +19 -0
- package/dist/server/lib/logger.js +46 -0
- package/dist/server/lib/otel.d.ts +38 -0
- package/dist/server/lib/otel.js +126 -0
- package/dist/server/lib/pg-rate-limiter.d.ts +21 -0
- package/dist/server/lib/pg-rate-limiter.js +86 -0
- package/dist/server/lib/prompt-sanitizer.d.ts +37 -0
- package/dist/server/lib/prompt-sanitizer.js +177 -0
- package/dist/server/lib/provider-capabilities.d.ts +85 -0
- package/dist/server/lib/provider-capabilities.js +190 -0
- package/dist/server/lib/provider-failover.d.ts +74 -0
- package/dist/server/lib/provider-failover.js +210 -0
- package/dist/server/lib/rate-limiter.d.ts +39 -0
- package/dist/server/lib/rate-limiter.js +147 -0
- package/dist/server/lib/server-agent-loop.d.ts +107 -0
- package/dist/server/lib/server-agent-loop.js +667 -0
- package/dist/server/lib/server-subagent.d.ts +78 -0
- package/dist/server/lib/server-subagent.js +203 -0
- package/dist/server/lib/session-checkpoint.d.ts +51 -0
- package/dist/server/lib/session-checkpoint.js +145 -0
- package/dist/server/lib/ssrf-guard.d.ts +13 -0
- package/dist/server/lib/ssrf-guard.js +240 -0
- package/dist/server/lib/supabase-client.d.ts +7 -0
- package/dist/server/lib/supabase-client.js +78 -0
- package/dist/server/lib/template-resolver.d.ts +31 -0
- package/dist/server/lib/template-resolver.js +215 -0
- package/dist/server/lib/utils.d.ts +16 -0
- package/dist/server/lib/utils.js +147 -0
- package/dist/server/local-agent-gateway.d.ts +82 -0
- package/dist/server/local-agent-gateway.js +426 -0
- package/dist/server/providers/anthropic.d.ts +20 -0
- package/dist/server/providers/anthropic.js +199 -0
- package/dist/server/providers/bedrock.d.ts +20 -0
- package/dist/server/providers/bedrock.js +194 -0
- package/dist/server/providers/gemini.d.ts +24 -0
- package/dist/server/providers/gemini.js +486 -0
- package/dist/server/providers/openai.d.ts +24 -0
- package/dist/server/providers/openai.js +522 -0
- package/dist/server/providers/registry.d.ts +32 -0
- package/dist/server/providers/registry.js +58 -0
- package/dist/server/providers/shared.d.ts +32 -0
- package/dist/server/providers/shared.js +124 -0
- package/dist/server/providers/types.d.ts +92 -0
- package/dist/server/providers/types.js +12 -0
- package/dist/server/proxy-handlers.d.ts +6 -0
- package/dist/server/proxy-handlers.js +89 -0
- package/dist/server/tool-router.d.ts +149 -0
- package/dist/server/tool-router.js +803 -0
- package/dist/server/validation.d.ts +24 -0
- package/dist/server/validation.js +301 -0
- package/dist/server/worker.d.ts +19 -0
- package/dist/server/worker.js +201 -0
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +181 -0
- package/dist/shared/agent-core.d.ts +157 -0
- package/dist/shared/agent-core.js +534 -0
- package/dist/shared/anthropic-types.d.ts +105 -0
- package/dist/shared/anthropic-types.js +7 -0
- package/dist/shared/api-client.d.ts +90 -0
- package/dist/shared/api-client.js +379 -0
- package/dist/shared/constants.d.ts +33 -0
- package/dist/shared/constants.js +80 -0
- package/dist/shared/sse-parser.d.ts +26 -0
- package/dist/shared/sse-parser.js +259 -0
- package/dist/shared/tool-dispatch.d.ts +52 -0
- package/dist/shared/tool-dispatch.js +191 -0
- package/dist/shared/types.d.ts +72 -0
- package/dist/shared/types.js +7 -0
- package/dist/updater.d.ts +25 -0
- package/dist/updater.js +140 -0
- package/dist/webchat/widget.d.ts +0 -0
- package/dist/webchat/widget.js +397 -0
- package/package.json +95 -0
- package/src/cli/services/builtin-skills/commit.md +19 -0
- package/src/cli/services/builtin-skills/review-pr.md +21 -0
- package/src/cli/services/builtin-skills/review.md +18 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown rendering — Apple-polished terminal output
|
|
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.
|
|
7
|
+
*/
|
|
8
|
+
import { Marked } from "marked";
|
|
9
|
+
import { markedTerminal } from "marked-terminal";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import { createRequire } from "module";
|
|
12
|
+
import { colors } from "./Theme.js";
|
|
13
|
+
import { diffWords } from "diff";
|
|
14
|
+
// Note: chalk.level is auto-detected by supports-color.
|
|
15
|
+
// Apple_Terminal → level 2 (256-color), iTerm.app v3+ → level 3 (24-bit).
|
|
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
|
+
// ============================================================================
|
|
20
|
+
// Apple Dark palette — derived from Theme.tsx (respects ~/.swagmanager/theme.json)
|
|
21
|
+
// ============================================================================
|
|
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);
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Terminal width helpers — single source of truth for layout widths
|
|
39
|
+
// ============================================================================
|
|
40
|
+
function termWidth() {
|
|
41
|
+
return process.stdout.columns || 80;
|
|
42
|
+
}
|
|
43
|
+
/** Width for top-level assistant text (accounts for MessageList marginLeft=2) */
|
|
44
|
+
export function contentWidth() {
|
|
45
|
+
return termWidth() - 2;
|
|
46
|
+
}
|
|
47
|
+
/** Width for content nested inside tool results (MessageList=2 + ToolIndicator=2 + safety=2) */
|
|
48
|
+
export function toolContentWidth() {
|
|
49
|
+
return termWidth() - 6;
|
|
50
|
+
}
|
|
51
|
+
// ============================================================================
|
|
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
|
+
// OSC 8 hyperlinks — only in terminals that support them
|
|
67
|
+
// ============================================================================
|
|
68
|
+
/** Detect if terminal supports OSC 8 clickable hyperlinks */
|
|
69
|
+
const supportsOsc8 = (() => {
|
|
70
|
+
const tp = process.env.TERM_PROGRAM || "";
|
|
71
|
+
if (/iterm|wezterm|kitty|hyper|warp|foot|alacritty/i.test(tp))
|
|
72
|
+
return true;
|
|
73
|
+
if (process.env.VTE_VERSION)
|
|
74
|
+
return true; // GNOME Terminal, Tilix
|
|
75
|
+
if (process.env.WT_SESSION)
|
|
76
|
+
return true; // Windows Terminal
|
|
77
|
+
if (process.env.KONSOLE_VERSION)
|
|
78
|
+
return true; // Konsole
|
|
79
|
+
// Apple Terminal.app does NOT support OSC 8
|
|
80
|
+
return false;
|
|
81
|
+
})();
|
|
82
|
+
function hyperlink(url, text) {
|
|
83
|
+
// mailto: → clean email address, no protocol prefix, no OSC 8
|
|
84
|
+
if (url.startsWith("mailto:")) {
|
|
85
|
+
const email = text || url.slice(7);
|
|
86
|
+
return systemCyan(email);
|
|
87
|
+
}
|
|
88
|
+
// tel: → clean phone number
|
|
89
|
+
if (url.startsWith("tel:")) {
|
|
90
|
+
return systemCyan(text || url.slice(4));
|
|
91
|
+
}
|
|
92
|
+
const display = text || url;
|
|
93
|
+
// OSC 8 clickable links — only where terminal supports them
|
|
94
|
+
if (supportsOsc8) {
|
|
95
|
+
return `\x1B]8;;${url}\x07${systemCyan.underline(display)}\x1B]8;;\x07`;
|
|
96
|
+
}
|
|
97
|
+
// Fallback: colored underlined text (no escape sequences)
|
|
98
|
+
return systemCyan.underline(display);
|
|
99
|
+
}
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// Path helpers
|
|
102
|
+
// ============================================================================
|
|
103
|
+
/** Shorten a file path for code block headers */
|
|
104
|
+
function shortenPathForHeader(fullPath, maxLen = 40) {
|
|
105
|
+
let p = fullPath;
|
|
106
|
+
const cwd = process.cwd();
|
|
107
|
+
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("/");
|
|
117
|
+
const file = parts.pop();
|
|
118
|
+
if (file.length >= maxLen - 4)
|
|
119
|
+
return "…/" + file.slice(-(maxLen - 4));
|
|
120
|
+
const parent = parts.pop();
|
|
121
|
+
return parent ? "…/" + parent + "/" + file : "…/" + file;
|
|
122
|
+
}
|
|
123
|
+
// ============================================================================
|
|
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
|
+
// Financial coloring
|
|
183
|
+
// ============================================================================
|
|
184
|
+
function colorizeFinancials(str) {
|
|
185
|
+
return str
|
|
186
|
+
// NOTE: URL hyperlinking is handled by marked's GFM autolink + link/href handlers.
|
|
187
|
+
// Do NOT add URL patterns here — it causes links to render 3-5x.
|
|
188
|
+
// Negative dollar amounts → red (-$1,234.56) — require digit after $
|
|
189
|
+
.replace(/(-\$\d[\d,]*\.?\d*)/g, (m) => systemRed(m))
|
|
190
|
+
// Positive dollar amounts → green ($1,234.56) — require digit after $
|
|
191
|
+
.replace(/((?:^|[^-])\$\d[\d,]*\.?\d*)/g, (m) => systemGreen(m))
|
|
192
|
+
// Negative percentages → red
|
|
193
|
+
.replace(/(-\d+\.?\d*%)/g, (m) => systemRed(m))
|
|
194
|
+
// Positive percentages → cyan
|
|
195
|
+
.replace(/((?:^|[^-])\d+\.?\d*%)/g, (m) => systemCyan(m))
|
|
196
|
+
// Explicit positive → green
|
|
197
|
+
.replace(/(\+\$?[\d,]+\.?\d*)/g, (m) => systemGreen(m))
|
|
198
|
+
// 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))
|
|
200
|
+
// 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));
|
|
202
|
+
}
|
|
203
|
+
// ============================================================================
|
|
204
|
+
// Markdown renderer
|
|
205
|
+
// ============================================================================
|
|
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
|
+
}));
|
|
247
|
+
// ============================================================================
|
|
248
|
+
// Diff renderer — background colors + word-level diff (Claude Code parity)
|
|
249
|
+
// ============================================================================
|
|
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
|
+
/** Compute word-level diff between two lines using the `diff` library. */
|
|
256
|
+
function wordDiff(oldLine, newLine) {
|
|
257
|
+
const changes = diffWords(oldLine, newLine);
|
|
258
|
+
const oldSegs = [];
|
|
259
|
+
const newSegs = [];
|
|
260
|
+
for (const change of changes) {
|
|
261
|
+
if (change.added) {
|
|
262
|
+
newSegs.push({ text: change.value, changed: true });
|
|
263
|
+
}
|
|
264
|
+
else if (change.removed) {
|
|
265
|
+
oldSegs.push({ text: change.value, changed: true });
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
oldSegs.push({ text: change.value, changed: false });
|
|
269
|
+
newSegs.push({ text: change.value, changed: false });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// If >60% of either line changed, fall back to full-line highlight
|
|
273
|
+
const oldChanged = oldSegs.filter(s => s.changed).reduce((n, s) => n + s.text.length, 0);
|
|
274
|
+
const newChanged = newSegs.filter(s => s.changed).reduce((n, s) => n + s.text.length, 0);
|
|
275
|
+
if ((oldLine.length > 0 && oldChanged / oldLine.length > 0.6) ||
|
|
276
|
+
(newLine.length > 0 && newChanged / newLine.length > 0.6)) {
|
|
277
|
+
return { old: [{ text: oldLine, changed: true }], new: [{ text: newLine, changed: true }] };
|
|
278
|
+
}
|
|
279
|
+
return { old: oldSegs, new: newSegs };
|
|
280
|
+
}
|
|
281
|
+
/** Render segments with word-level highlighting */
|
|
282
|
+
function renderSegments(segs, wordStyle, lineStyle) {
|
|
283
|
+
return segs.map(s => s.changed ? wordStyle(s.text) : lineStyle(s.text)).join("");
|
|
284
|
+
}
|
|
285
|
+
function renderDiff(code) {
|
|
286
|
+
const lines = code.split("\n");
|
|
287
|
+
const tw = toolContentWidth();
|
|
288
|
+
const segments = [];
|
|
289
|
+
let oldLineNo = 1, newLineNo = 1;
|
|
290
|
+
let seenDiff = false;
|
|
291
|
+
for (const line of lines) {
|
|
292
|
+
// Skip file headers (--- a/file, +++ b/file)
|
|
293
|
+
if (line.startsWith("---") || line.startsWith("+++"))
|
|
294
|
+
continue;
|
|
295
|
+
// Hunk header — extract line numbers
|
|
296
|
+
const hunkMatch = line.match(/^@@\s*-(\d+)(?:,\d+)?\s*\+(\d+)(?:,\d+)?\s*@@/);
|
|
297
|
+
if (hunkMatch) {
|
|
298
|
+
oldLineNo = parseInt(hunkMatch[1]);
|
|
299
|
+
newLineNo = parseInt(hunkMatch[2]);
|
|
300
|
+
seenDiff = true;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
// Skip non-diff header lines (e.g., "File edited:", "Applied N edits")
|
|
304
|
+
if (!seenDiff && !line.startsWith("-") && !line.startsWith("+") && !line.startsWith(" ")) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
seenDiff = true;
|
|
308
|
+
if (line.startsWith("-")) {
|
|
309
|
+
segments.push({ type: "remove", content: line.slice(1), lineNo: oldLineNo });
|
|
310
|
+
oldLineNo++;
|
|
311
|
+
}
|
|
312
|
+
else if (line.startsWith("+")) {
|
|
313
|
+
segments.push({ type: "add", content: line.slice(1), lineNo: newLineNo });
|
|
314
|
+
newLineNo++;
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
const content = line.startsWith(" ") ? line.slice(1) : line;
|
|
318
|
+
segments.push({ type: "context", content, lineNo: newLineNo });
|
|
319
|
+
oldLineNo++;
|
|
320
|
+
newLineNo++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (segments.length === 0)
|
|
324
|
+
return code; // fallback
|
|
325
|
+
// Gutter width from max line number
|
|
326
|
+
const maxLineNo = segments.reduce((max, s) => Math.max(max, s.lineNo), 0);
|
|
327
|
+
const gutterW = Math.max(3, String(maxLineNo).length);
|
|
328
|
+
const out = [];
|
|
329
|
+
let i = 0;
|
|
330
|
+
while (i < segments.length) {
|
|
331
|
+
const seg = segments[i];
|
|
332
|
+
if (seg.type === "remove") {
|
|
333
|
+
// Collect consecutive removes
|
|
334
|
+
const removes = [];
|
|
335
|
+
while (i < segments.length && segments[i].type === "remove") {
|
|
336
|
+
removes.push(segments[i]);
|
|
337
|
+
i++;
|
|
338
|
+
}
|
|
339
|
+
// Collect consecutive adds
|
|
340
|
+
const adds = [];
|
|
341
|
+
while (i < segments.length && segments[i].type === "add") {
|
|
342
|
+
adds.push(segments[i]);
|
|
343
|
+
i++;
|
|
344
|
+
}
|
|
345
|
+
// Pair for word-level diff
|
|
346
|
+
const pairCount = Math.min(removes.length, adds.length);
|
|
347
|
+
for (let j = 0; j < removes.length; j++) {
|
|
348
|
+
const r = removes[j];
|
|
349
|
+
const rPrefix = `${String(r.lineNo).padStart(gutterW)} - `;
|
|
350
|
+
if (j < pairCount) {
|
|
351
|
+
const a = adds[j];
|
|
352
|
+
const aPrefix = `${String(a.lineNo).padStart(gutterW)} + `;
|
|
353
|
+
const wd = wordDiff(r.content, a.content);
|
|
354
|
+
// Removed line with word highlights
|
|
355
|
+
const rPad = Math.max(0, tw - rPrefix.length - r.content.length);
|
|
356
|
+
out.push(diffRemovedBg(rPrefix) + renderSegments(wd.old, diffWordRemoved, diffRemovedBg) + diffRemovedBg(" ".repeat(rPad)));
|
|
357
|
+
// Added line with word highlights
|
|
358
|
+
const aPad = Math.max(0, tw - aPrefix.length - a.content.length);
|
|
359
|
+
out.push(diffAddedBg(aPrefix) + renderSegments(wd.new, diffWordAdded, diffAddedBg) + diffAddedBg(" ".repeat(aPad)));
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
// Unpaired remove
|
|
363
|
+
const raw = rPrefix + r.content;
|
|
364
|
+
const pad = Math.max(0, tw - raw.length);
|
|
365
|
+
out.push(diffRemovedBg(raw + " ".repeat(pad)));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Unpaired adds
|
|
369
|
+
for (let j = pairCount; j < adds.length; j++) {
|
|
370
|
+
const a = adds[j];
|
|
371
|
+
const prefix = `${String(a.lineNo).padStart(gutterW)} + `;
|
|
372
|
+
const raw = prefix + a.content;
|
|
373
|
+
const pad = Math.max(0, tw - raw.length);
|
|
374
|
+
out.push(diffAddedBg(raw + " ".repeat(pad)));
|
|
375
|
+
}
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (seg.type === "add") {
|
|
379
|
+
// Standalone add (no preceding remove)
|
|
380
|
+
const prefix = `${String(seg.lineNo).padStart(gutterW)} + `;
|
|
381
|
+
const raw = prefix + seg.content;
|
|
382
|
+
const pad = Math.max(0, tw - raw.length);
|
|
383
|
+
out.push(diffAddedBg(raw + " ".repeat(pad)));
|
|
384
|
+
i++;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
// Context line — dim line number, plain content, no background
|
|
388
|
+
const prefix = tertiary(`${String(seg.lineNo).padStart(gutterW)} `);
|
|
389
|
+
out.push(prefix + seg.content);
|
|
390
|
+
i++;
|
|
391
|
+
}
|
|
392
|
+
return out.join("\n") + "\n";
|
|
393
|
+
}
|
|
394
|
+
// ============================================================================
|
|
395
|
+
// Bar chart renderer — ```chart code blocks
|
|
396
|
+
// ============================================================================
|
|
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
|
+
];
|
|
407
|
+
function renderBarChart(code) {
|
|
408
|
+
const lines = code.trim().split("\n").filter(l => l.trim());
|
|
409
|
+
// Optional title — first line without "label: number" pattern
|
|
410
|
+
let title = "";
|
|
411
|
+
let dataLines = lines;
|
|
412
|
+
if (lines.length > 1 && !/:\s*[$\-+]?[\d,]+/.test(lines[0])) {
|
|
413
|
+
title = lines[0].trim();
|
|
414
|
+
dataLines = lines.slice(1);
|
|
415
|
+
}
|
|
416
|
+
// Parse "Label: $1,234.56" or "Label: 42%" or "Label: 1000"
|
|
417
|
+
const entries = [];
|
|
418
|
+
for (const line of dataLines) {
|
|
419
|
+
const m = line.match(/^(.+?):\s*([+\-]?\$?[\d,]+\.?\d*%?)\s*$/);
|
|
420
|
+
if (!m)
|
|
421
|
+
continue;
|
|
422
|
+
const label = m[1].trim();
|
|
423
|
+
const raw = m[2].trim();
|
|
424
|
+
const value = Math.abs(parseFloat(raw.replace(/[$,%]/g, "")));
|
|
425
|
+
if (!isNaN(value))
|
|
426
|
+
entries.push({ label, value, raw });
|
|
427
|
+
}
|
|
428
|
+
if (entries.length === 0)
|
|
429
|
+
return code;
|
|
430
|
+
const maxVal = Math.max(...entries.map(e => e.value));
|
|
431
|
+
const maxLabel = Math.max(...entries.map(e => e.label.length));
|
|
432
|
+
const maxRaw = Math.max(...entries.map(e => e.raw.length));
|
|
433
|
+
const cw = contentWidth();
|
|
434
|
+
const barWidth = Math.min(36, Math.max(12, cw - 8 - maxLabel - maxRaw));
|
|
435
|
+
const out = [];
|
|
436
|
+
if (title) {
|
|
437
|
+
out.push(` ${systemBlue.bold(title)}`);
|
|
438
|
+
out.push("");
|
|
439
|
+
}
|
|
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}`);
|
|
453
|
+
}
|
|
454
|
+
return "\n" + out.join("\n") + "\n";
|
|
455
|
+
}
|
|
456
|
+
function getRowTone(cells) {
|
|
457
|
+
for (const cell of cells) {
|
|
458
|
+
const t = cell.trim();
|
|
459
|
+
// Negative financial → red row
|
|
460
|
+
if (/^-\$[\d,]+/.test(t) || /^-\d+\.?\d*%/.test(t))
|
|
461
|
+
return "negative";
|
|
462
|
+
// Explicit positive delta → green row
|
|
463
|
+
if (/^\+\d/.test(t) || /^\+\$/.test(t))
|
|
464
|
+
return "positive";
|
|
465
|
+
// Status badges
|
|
466
|
+
if (/^`?[✕✗]/.test(t) || /cancelled|failed|rejected|error|out of stock|low stock/i.test(t))
|
|
467
|
+
return "negative";
|
|
468
|
+
if (/^`?[✓●]/.test(t) || /completed|received|approved|active|success|paid|published/i.test(t))
|
|
469
|
+
return "positive";
|
|
470
|
+
}
|
|
471
|
+
return "neutral";
|
|
472
|
+
}
|
|
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") {
|
|
477
|
+
const trimmed = val.trim();
|
|
478
|
+
if (!trimmed)
|
|
479
|
+
return text("");
|
|
480
|
+
if (isHeader)
|
|
481
|
+
return systemIndigo.bold(trimmed);
|
|
482
|
+
// Badge format: `✓ status` or `◆ status` or `○ status` or `✕ status`
|
|
483
|
+
const badgeMatch = trimmed.match(/^`([✓●◆○✕◦])\s+(.+)`$/);
|
|
484
|
+
if (badgeMatch) {
|
|
485
|
+
const [, icon, label] = badgeMatch;
|
|
486
|
+
if (icon === "✓" || icon === "●")
|
|
487
|
+
return systemGreen(`${icon} ${label}`);
|
|
488
|
+
if (icon === "◆")
|
|
489
|
+
return systemCyan(`${icon} ${label}`);
|
|
490
|
+
if (icon === "○")
|
|
491
|
+
return systemOrange(`${icon} ${label}`);
|
|
492
|
+
if (icon === "✕")
|
|
493
|
+
return systemRed(`${icon} ${label}`);
|
|
494
|
+
return secondary(`${icon} ${label}`);
|
|
495
|
+
}
|
|
496
|
+
// Inline code (UUID, SKU, transfer number) — subtle style
|
|
497
|
+
if (trimmed.startsWith("`") && trimmed.endsWith("`")) {
|
|
498
|
+
return systemPurple(trimmed.slice(1, -1));
|
|
499
|
+
}
|
|
500
|
+
// Bold text
|
|
501
|
+
if (trimmed.startsWith("**") && trimmed.endsWith("**")) {
|
|
502
|
+
return text.bold(trimmed.slice(2, -2));
|
|
503
|
+
}
|
|
504
|
+
// Negative values → red
|
|
505
|
+
if (/^-\$?[\d,]+\.?\d*$/.test(trimmed) || /^-\d+\.?\d*%$/.test(trimmed)) {
|
|
506
|
+
return systemRed(trimmed);
|
|
507
|
+
}
|
|
508
|
+
// Positive financial → green
|
|
509
|
+
if (/^\+?\$[\d,]+\.?\d*$/.test(trimmed) || /^\$[\d,]+\.?\d*$/.test(trimmed)) {
|
|
510
|
+
return systemGreen(trimmed);
|
|
511
|
+
}
|
|
512
|
+
// Percentages → cyan
|
|
513
|
+
if (/^\d+\.?\d*%$/.test(trimmed)) {
|
|
514
|
+
return systemCyan(trimmed);
|
|
515
|
+
}
|
|
516
|
+
// Plain numbers → mint
|
|
517
|
+
if (/^[\d,]+\.?\d*$/.test(trimmed)) {
|
|
518
|
+
return systemMint(trimmed);
|
|
519
|
+
}
|
|
520
|
+
// Status words
|
|
521
|
+
if (/^(active|success|complete|approved|in stock|available)/i.test(trimmed)) {
|
|
522
|
+
return systemGreen(trimmed);
|
|
523
|
+
}
|
|
524
|
+
if (/^(inactive|error|failed|cancelled|out of stock|low|overdue|expired)/i.test(trimmed)) {
|
|
525
|
+
return systemRed(trimmed);
|
|
526
|
+
}
|
|
527
|
+
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);
|
|
536
|
+
}
|
|
537
|
+
function renderTable(token) {
|
|
538
|
+
// Extract cell text from token — handles both inline tokens and plain text
|
|
539
|
+
function getCellText(cell) {
|
|
540
|
+
if (!cell)
|
|
541
|
+
return "";
|
|
542
|
+
if (typeof cell === "string")
|
|
543
|
+
return cell;
|
|
544
|
+
if (cell.text !== undefined)
|
|
545
|
+
return String(cell.text);
|
|
546
|
+
if (cell.tokens) {
|
|
547
|
+
return cell.tokens.map((t) => t.raw || t.text || "").join("");
|
|
548
|
+
}
|
|
549
|
+
return String(cell);
|
|
550
|
+
}
|
|
551
|
+
const headers = (token.header || []).map((h) => getCellText(h));
|
|
552
|
+
const rows = (token.rows || []).map((row) => row.map((cell) => getCellText(cell)));
|
|
553
|
+
if (headers.length === 0)
|
|
554
|
+
return "";
|
|
555
|
+
// Responsive column widths based on terminal width
|
|
556
|
+
const cw = contentWidth();
|
|
557
|
+
const N = headers.length;
|
|
558
|
+
// Overhead: " ╭" (3) + N+1 border chars + N*2 cell padding + "╮"
|
|
559
|
+
const overhead = 3 + (N + 1) + (N * 2);
|
|
560
|
+
const availableForContent = cw - overhead;
|
|
561
|
+
// Scale minimum column width down for narrow terminals
|
|
562
|
+
const minCol = cw < 70 ? 3 : cw < 90 ? 4 : 6;
|
|
563
|
+
const maxPerCol = Math.max(minCol, Math.floor(availableForContent / N));
|
|
564
|
+
const colWidths = headers.map((h, i) => {
|
|
565
|
+
const dataMax = rows.reduce((max, row) => Math.max(max, (row[i] || "").length), 0);
|
|
566
|
+
return Math.min(maxPerCol, Math.max(minCol, h.length, dataMax) + 2);
|
|
567
|
+
});
|
|
568
|
+
const border = chalk.hex("#48484A");
|
|
569
|
+
const out = [];
|
|
570
|
+
// Top border: ╭──────┬──────╮
|
|
571
|
+
out.push(border(" ╭" + colWidths.map((w) => "─".repeat(w + 2)).join("┬") + "╮"));
|
|
572
|
+
// Header row (truncate headers to fit)
|
|
573
|
+
const hdrLine = headers.map((h, i) => {
|
|
574
|
+
const display = h.length > colWidths[i] ? h.slice(0, colWidths[i] - 1) + "…" : h;
|
|
575
|
+
return " " + systemIndigo.bold(display.padEnd(colWidths[i])) + " ";
|
|
576
|
+
}).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) {
|
|
582
|
+
const tone = getRowTone(row);
|
|
583
|
+
const cells = headers.map((_, i) => {
|
|
584
|
+
const raw = row[i] || "";
|
|
585
|
+
const display = raw.length > colWidths[i] ? raw.slice(0, colWidths[i] - 1) + "…" : raw;
|
|
586
|
+
const colored = colorizeCell(display, false, tone);
|
|
587
|
+
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;
|
|
595
|
+
}).join(border("│"));
|
|
596
|
+
out.push(border(" │") + cells + border("│"));
|
|
597
|
+
}
|
|
598
|
+
// Bottom border: ╰──────┴──────╯
|
|
599
|
+
out.push(border(" ╰" + colWidths.map((w) => "─".repeat(w + 2)).join("┴") + "╯"));
|
|
600
|
+
return "\n" + out.join("\n") + "\n";
|
|
601
|
+
}
|
|
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
|
+
});
|
|
685
|
+
// ============================================================================
|
|
686
|
+
// Streaming fence closure — state-tracking approach
|
|
687
|
+
// ============================================================================
|
|
688
|
+
/** Extract text outside fenced code blocks */
|
|
689
|
+
function getNonFencedText(input) {
|
|
690
|
+
const lines = input.split("\n");
|
|
691
|
+
const parts = [];
|
|
692
|
+
let inFence = false;
|
|
693
|
+
for (const line of lines) {
|
|
694
|
+
if (line.trimStart().startsWith("```")) {
|
|
695
|
+
inFence = !inFence;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (!inFence)
|
|
699
|
+
parts.push(line);
|
|
700
|
+
}
|
|
701
|
+
return parts.join("\n");
|
|
702
|
+
}
|
|
703
|
+
/** Strip complete inline code spans from text */
|
|
704
|
+
function getNonCodeText(t) {
|
|
705
|
+
return t.replace(/``[^`]*``/g, "").replace(/`[^`\n]*`/g, "");
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Close incomplete markdown fences for safe streaming rendering.
|
|
709
|
+
* State-tracking approach: handles nested fences, escaped markers, double-backtick spans.
|
|
710
|
+
*/
|
|
711
|
+
export function closeIncompleteFences(input) {
|
|
712
|
+
let result = input;
|
|
713
|
+
// 1. Code fences — walk lines, track open/close state
|
|
714
|
+
let inFence = false;
|
|
715
|
+
for (const line of result.split("\n")) {
|
|
716
|
+
const trimmed = line.trimStart();
|
|
717
|
+
if (trimmed.startsWith("```")) {
|
|
718
|
+
inFence = !inFence;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (inFence) {
|
|
722
|
+
result += "\n```";
|
|
723
|
+
}
|
|
724
|
+
// 2. For inline markers, only examine text OUTSIDE fenced code blocks
|
|
725
|
+
const outside = getNonFencedText(result);
|
|
726
|
+
// 3. Inline backticks — remove escaped, remove complete spans, count remainder
|
|
727
|
+
let forBacktick = outside.replace(/\\`/g, "");
|
|
728
|
+
forBacktick = forBacktick.replace(/``[^`]*``/g, ""); // double-backtick spans
|
|
729
|
+
forBacktick = forBacktick.replace(/`[^`\n]*`/g, ""); // single-backtick spans
|
|
730
|
+
const unmatched = (forBacktick.match(/`/g) || []).length;
|
|
731
|
+
if (unmatched % 2 !== 0) {
|
|
732
|
+
result += "`";
|
|
733
|
+
}
|
|
734
|
+
// 4. Bold — count ** in non-code text
|
|
735
|
+
const forBold = getNonCodeText(outside);
|
|
736
|
+
const boldCount = (forBold.match(/\*\*/g) || []).length;
|
|
737
|
+
if (boldCount % 2 !== 0) {
|
|
738
|
+
result += "**";
|
|
739
|
+
}
|
|
740
|
+
// 5. Italic — count standalone * (not part of **) with escape awareness
|
|
741
|
+
const forItalic = forBold.replace(/\*\*/g, "");
|
|
742
|
+
const italicCount = (forItalic.match(/(?<!\\)\*/g) || []).length;
|
|
743
|
+
if (italicCount % 2 !== 0) {
|
|
744
|
+
result += "*";
|
|
745
|
+
}
|
|
746
|
+
return result;
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Render markdown to ANSI-styled terminal string.
|
|
750
|
+
* Optionally applies streaming-safe fence closing.
|
|
751
|
+
*/
|
|
752
|
+
export function renderMarkdown(input, streaming = false) {
|
|
753
|
+
const safe = streaming ? closeIncompleteFences(input) : input;
|
|
754
|
+
const result = md.parse(safe);
|
|
755
|
+
return result.replace(/\n+$/, "");
|
|
756
|
+
}
|