@sns-myagent/cli 0.3.5 → 0.3.6
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/package.json +1 -1
- package/scripts/smoke-tui.ts +18 -22
- package/src/tui/chat-blocks.ts +117 -174
- package/src/tui/code-cell.ts +9 -9
- package/src/tui/command-palette.ts +62 -175
- package/src/tui/index.ts +1 -1
- package/src/tui/splash.ts +67 -111
- package/src/ui/error-display.ts +67 -103
- package/src/ui/gradient.ts +15 -95
- package/src/ui/index.ts +3 -9
- package/src/ui/memory-toast.ts +51 -77
- package/src/ui/status-bar.ts +26 -47
|
@@ -1,189 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Command palette
|
|
3
|
-
*
|
|
4
|
-
* Fuzzy-searchable command overlay with gradient styling.
|
|
5
|
-
* Triggered by Ctrl+P or typing "/" in the input.
|
|
2
|
+
* Command palette — fuzzy command overlay.
|
|
3
|
+
* Minimal: cyan highlight, dim for secondary. No gradient.
|
|
6
4
|
*/
|
|
5
|
+
|
|
7
6
|
import chalk from "chalk";
|
|
8
|
-
import gradient from "gradient-string";
|
|
9
|
-
import { BRAND_GRADIENT, ACCENT_GRADIENT } from "../ui/colors.js";
|
|
10
7
|
|
|
11
8
|
export interface PaletteCommand {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
category: string;
|
|
18
|
-
/** Keyboard shortcut (optional). */
|
|
19
|
-
shortcut?: string;
|
|
20
|
-
/** Whether command is enabled. */
|
|
21
|
-
enabled?: boolean;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
category: string;
|
|
12
|
+
shortcut?: string;
|
|
13
|
+
enabled?: boolean;
|
|
22
14
|
}
|
|
23
15
|
|
|
24
16
|
export interface CommandPaletteOptions {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/** Currently highlighted index. */
|
|
30
|
-
highlighted?: number;
|
|
31
|
-
/** Width of the palette. */
|
|
32
|
-
width?: number;
|
|
17
|
+
commands: PaletteCommand[];
|
|
18
|
+
query?: string;
|
|
19
|
+
highlighted?: number;
|
|
20
|
+
width?: number;
|
|
33
21
|
}
|
|
34
22
|
|
|
35
|
-
// ── Pre-built command lists ──
|
|
36
|
-
|
|
37
23
|
export const CHAT_COMMANDS: PaletteCommand[] = [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
24
|
+
{ name: "/help", description: "Show available commands", category: "General", shortcut: "?" },
|
|
25
|
+
{ name: "/clear", description: "Clear screen", category: "General", shortcut: "⌘K" },
|
|
26
|
+
{ name: "/exit", description: "Quit the chat", category: "General", shortcut: "⌘Q" },
|
|
27
|
+
{ name: "/model", description: "Show/switch model", category: "Config" },
|
|
28
|
+
{ name: "/history", description: "Show conversation history", category: "General" },
|
|
29
|
+
{ name: "/memory", description: "Recall from memory", category: "Memory" },
|
|
30
|
+
{ name: "/skills", description: "List available skills", category: "Extensibility" },
|
|
31
|
+
{ name: "/plugins", description: "List installed plugins", category: "Extensibility" },
|
|
32
|
+
{ name: "/mcp", description: "MCP server management", category: "Extensibility" },
|
|
33
|
+
{ name: "/theme", description: "Change terminal theme", category: "Config" },
|
|
34
|
+
{ name: "/bench", description: "Run benchmarks", category: "Debug" },
|
|
35
|
+
{ name: "/debug", description: "Debug info", category: "Debug" },
|
|
50
36
|
];
|
|
51
37
|
|
|
52
|
-
// ── Fuzzy match ──
|
|
53
|
-
|
|
54
|
-
function fuzzyMatch(query: string, text: string): number {
|
|
55
|
-
if (!query) return 1;
|
|
56
|
-
const q = query.toLowerCase();
|
|
57
|
-
const t = text.toLowerCase();
|
|
58
|
-
|
|
59
|
-
// Exact prefix match — highest score
|
|
60
|
-
if (t.startsWith(q)) return 1;
|
|
61
|
-
|
|
62
|
-
// Subsequence match
|
|
63
|
-
let qi = 0;
|
|
64
|
-
let score = 0;
|
|
65
|
-
let consecutive = 0;
|
|
66
|
-
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
|
|
67
|
-
if (t[ti] === q[qi]) {
|
|
68
|
-
qi++;
|
|
69
|
-
consecutive++;
|
|
70
|
-
score += consecutive; // bonus for consecutive chars
|
|
71
|
-
} else {
|
|
72
|
-
consecutive = 0;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return qi === q.length ? score / (q.length * q.length) : 0;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ── Renderer ──
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Render the command palette as a bordered overlay.
|
|
83
|
-
*/
|
|
84
38
|
export function renderCommandPalette(opts: CommandPaletteOptions): string {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
lines.push(grad("│") + ` ${searchDisplay}${searchFill}` + grad("│"));
|
|
124
|
-
|
|
125
|
-
// ── Separator ──
|
|
126
|
-
lines.push(grad("├") + "─".repeat(inner) + grad("┤"));
|
|
127
|
-
|
|
128
|
-
// ── Commands ──
|
|
129
|
-
let globalIdx = 0;
|
|
130
|
-
for (const [category, items] of groups) {
|
|
131
|
-
// Category header
|
|
132
|
-
const catText = chalk.dim(` ${category.toUpperCase()} `);
|
|
133
|
-
const catFill = " ".repeat(Math.max(0, inner - category.length - 2));
|
|
134
|
-
lines.push(grad("│") + catText + catFill + grad("│"));
|
|
135
|
-
|
|
136
|
-
for (const { cmd } of items) {
|
|
137
|
-
const isSelected = globalIdx === highlighted;
|
|
138
|
-
const nameStr = isSelected
|
|
139
|
-
? chalk.bgCyan.black(` ${cmd.name} `)
|
|
140
|
-
: chalk.cyan(cmd.name);
|
|
141
|
-
const descStr = chalk.dim(cmd.description);
|
|
142
|
-
const shortcutStr = cmd.shortcut ? chalk.dim(` [${cmd.shortcut}]`) : "";
|
|
143
|
-
|
|
144
|
-
const line = ` ${nameStr} ${descStr}${shortcutStr}`;
|
|
145
|
-
const padding = " ".repeat(Math.max(0, inner - visibleLen(line) + 4));
|
|
146
|
-
|
|
147
|
-
if (isSelected) {
|
|
148
|
-
lines.push(grad("│") + chalk.bgHex("#1a1a2e")(line) + padding + grad("│"));
|
|
149
|
-
} else {
|
|
150
|
-
lines.push(grad("│") + line + padding + grad("│"));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
globalIdx++;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ── Footer ──
|
|
158
|
-
const footer = chalk.dim(" ↑↓ navigate ⏎ select esc close ");
|
|
159
|
-
const footerFill = " ".repeat(Math.max(0, inner - footer.length + 4));
|
|
160
|
-
lines.push(grad("├") + "─".repeat(inner) + grad("┤"));
|
|
161
|
-
lines.push(grad("│") + footer + footerFill + grad("│"));
|
|
162
|
-
lines.push(grad("╰") + "─".repeat(inner) + grad("╯"));
|
|
163
|
-
|
|
164
|
-
return lines.join("\n");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Get filtered commands for a query.
|
|
169
|
-
*/
|
|
170
|
-
export function filterCommands(
|
|
171
|
-
commands: PaletteCommand[],
|
|
172
|
-
query: string,
|
|
173
|
-
): PaletteCommand[] {
|
|
174
|
-
return commands
|
|
175
|
-
.map(cmd => ({
|
|
176
|
-
cmd,
|
|
177
|
-
score: fuzzyMatch(query, cmd.name) + fuzzyMatch(query, cmd.description) * 0.5,
|
|
178
|
-
}))
|
|
179
|
-
.filter(s => s.score > 0)
|
|
180
|
-
.sort((a, b) => b.score - a.score)
|
|
181
|
-
.map(s => s.cmd);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ── Helpers ──
|
|
185
|
-
|
|
186
|
-
function visibleLen(str: string): number {
|
|
187
|
-
// Strip ANSI escape sequences for length calculation
|
|
188
|
-
return str.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
189
|
-
}
|
|
39
|
+
const { commands, query = "", highlighted = 0, width = 60 } = opts;
|
|
40
|
+
const filtered = commands.filter(
|
|
41
|
+
(c) => c.enabled !== false &&
|
|
42
|
+
(c.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
43
|
+
c.description.toLowerCase().includes(query.toLowerCase()) ||
|
|
44
|
+
c.category.toLowerCase().includes(query.toLowerCase()))
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const cols = process.stdout.columns ?? 80;
|
|
48
|
+
const width2 = Math.min(opts.width ?? cols, cols) - 4;
|
|
49
|
+
|
|
50
|
+
const lines: string[] = [];
|
|
51
|
+
|
|
52
|
+
// Header
|
|
53
|
+
const search = chalk.cyan("? ") + query + chalk.dim(" (↑↓ navigate, Enter select, Esc cancel)");
|
|
54
|
+
lines.push(chalk.dim("─".repeat(60)));
|
|
55
|
+
lines.push(` ${chalk.cyan("?")} ${query}${chalk.dim(" (↑↓ Enter Esc)")}`);
|
|
56
|
+
lines.push("");
|
|
57
|
+
|
|
58
|
+
if (filtered.length === 0) {
|
|
59
|
+
lines.push(chalk.dim(" No commands match"));
|
|
60
|
+
} else {
|
|
61
|
+
filtered.forEach((cmd, idx) => {
|
|
62
|
+
const isActive = idx === highlighted;
|
|
63
|
+
const prefix = isActive ? chalk.cyan("► ") : " ";
|
|
64
|
+
const name = isActive ? chalk.cyan.bold(cmd.name) : chalk.cyan(cmd.name);
|
|
65
|
+
const desc = chalk.dim(cmd.description);
|
|
66
|
+
const cat = chalk.dim(` [${cmd.category}]`);
|
|
67
|
+
const shortcut = cmd.shortcut ? chalk.dim(` ${cmd.shortcut}`) : "";
|
|
68
|
+
lines.push(`${prefix}${name}${cat}${shortcut}`);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
lines.push("");
|
|
73
|
+
lines.push(chalk.dim("─".repeat(60)));
|
|
74
|
+
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
package/src/tui/index.ts
CHANGED
|
@@ -14,4 +14,4 @@ export * from "./width-aware-text";
|
|
|
14
14
|
export * from "./splash";
|
|
15
15
|
export * from "./chat-blocks";
|
|
16
16
|
export * from "./chat-ui";
|
|
17
|
-
export { renderCommandPalette,
|
|
17
|
+
export { renderCommandPalette, CHAT_COMMANDS, type PaletteCommand, type CommandPaletteOptions } from "./command-palette";
|
package/src/tui/splash.ts
CHANGED
|
@@ -1,130 +1,86 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SNS-MyAgent splash
|
|
3
|
-
*
|
|
2
|
+
* SNS-MyAgent splash — minimal banner + info block.
|
|
3
|
+
* Single accent (cyan) for brand, default terminal text everywhere else.
|
|
4
4
|
*/
|
|
5
5
|
import chalk from "chalk";
|
|
6
|
-
import gradient from "gradient-string";
|
|
7
|
-
import boxen from "boxen";
|
|
8
6
|
import { readFileSync } from "node:fs";
|
|
9
7
|
import { dirname, resolve } from "node:path";
|
|
10
8
|
import { fileURLToPath } from "node:url";
|
|
11
9
|
|
|
12
|
-
const
|
|
13
|
-
███╗ ██╗███████╗██╗ ██╗██╗ ██╗███████╗
|
|
14
|
-
████╗ ██║██╔════╝╚██╗██╔╝██║ ██║██╔════╝
|
|
15
|
-
██╔██╗ ██║█████╗ ╚███╔╝ ██║ ██║███████╗
|
|
16
|
-
██║╚██╗██║██╔══╝ ██╔██╗ ██║ ██║╚════██║
|
|
17
|
-
██║ ╚████║███████╗██╔╝ ╚██╗╚██████╔╝███████║
|
|
18
|
-
╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝`;
|
|
19
|
-
|
|
20
|
-
const SUBTITLE = "My-Agent • SnsAgent CLI";
|
|
21
|
-
|
|
22
|
-
const SNY_GRADIENT = ["#00d2ff", "#7b2ff7", "#ff6b9d"];
|
|
23
|
-
const ACCENT_GRADIENT = ["#7b2ff7", "#00d2ff"];
|
|
10
|
+
const SUBTITLE = "snsagent — coding agent CLI";
|
|
24
11
|
|
|
25
12
|
function readVersion(): string {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return "0.0.0";
|
|
57
|
-
} catch {
|
|
58
|
-
return "0.0.0";
|
|
59
|
-
}
|
|
13
|
+
try {
|
|
14
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const candidates = [
|
|
16
|
+
resolve(here, "..", "..", "package.json"),
|
|
17
|
+
resolve(here, "..", "package.json"),
|
|
18
|
+
resolve(here, "..", "..", "dist", "package.json"),
|
|
19
|
+
];
|
|
20
|
+
for (const pkgPath of candidates) {
|
|
21
|
+
try {
|
|
22
|
+
const raw = readFileSync(pkgPath, "utf8");
|
|
23
|
+
const pkg = JSON.parse(raw) as { version?: string };
|
|
24
|
+
if (pkg.version && pkg.version !== "0.0.0") return pkg.version;
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
let dir = process.cwd();
|
|
28
|
+
for (let i = 0; i < 5; i++) {
|
|
29
|
+
try {
|
|
30
|
+
const raw = readFileSync(resolve(dir, "package.json"), "utf8");
|
|
31
|
+
const pkg = JSON.parse(raw) as { name?: string; version?: string };
|
|
32
|
+
if (pkg.name === "@sns-myagent/cli" && pkg.version) return pkg.version;
|
|
33
|
+
} catch {}
|
|
34
|
+
const parent = dirname(dir);
|
|
35
|
+
if (parent === dir) break;
|
|
36
|
+
dir = parent;
|
|
37
|
+
}
|
|
38
|
+
return "0.0.0";
|
|
39
|
+
} catch {
|
|
40
|
+
return "0.0.0";
|
|
41
|
+
}
|
|
60
42
|
}
|
|
61
43
|
|
|
62
44
|
export interface SplashInfo {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function makeInfoBlocks(info: SplashInfo, width: number): string {
|
|
71
|
-
const inner = Math.min(width - 4, 56);
|
|
72
|
-
const sep = chalk.dim("─".repeat(inner));
|
|
73
|
-
|
|
74
|
-
const rows: string[] = [];
|
|
75
|
-
const kv = (k: string, v: string) =>
|
|
76
|
-
` ${chalk.dim(k.padEnd(12))}${chalk.white(v)}`;
|
|
77
|
-
|
|
78
|
-
rows.push(sep);
|
|
79
|
-
if (info.model) rows.push(kv("Model", `${info.provider ?? "unknown"}/${info.model}`));
|
|
80
|
-
if (info.cwd) rows.push(kv("Working Dir", info.cwd));
|
|
81
|
-
if (info.platform) rows.push(kv("Platform", info.platform));
|
|
82
|
-
rows.push(kv("Version", readVersion()));
|
|
83
|
-
rows.push(sep);
|
|
84
|
-
|
|
85
|
-
const hint = chalk.dim(" Type your message to start chatting.");
|
|
86
|
-
const exit = chalk.dim(" /exit or Ctrl+C to quit.");
|
|
87
|
-
rows.push(hint);
|
|
88
|
-
rows.push(exit);
|
|
89
|
-
|
|
90
|
-
return rows.join("\n");
|
|
45
|
+
model?: string;
|
|
46
|
+
provider?: string;
|
|
47
|
+
cwd?: string;
|
|
48
|
+
platform?: string;
|
|
49
|
+
nodeVersion?: string;
|
|
91
50
|
}
|
|
92
51
|
|
|
93
52
|
export function renderSplash(info: SplashInfo = {}): string {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
return box;
|
|
53
|
+
const cols = process.stdout.columns ?? 80;
|
|
54
|
+
const width = Math.min(cols - 4, 72);
|
|
55
|
+
|
|
56
|
+
// Single brand mark, no rainbow
|
|
57
|
+
const brandLine = `\n ${chalk.cyan.bold("MY")} ${chalk.bold("snsagent")} ${chalk.dim(`v${readVersion()}`)}\n`;
|
|
58
|
+
const subLine = ` ${chalk.dim(SUBTITLE)}\n`;
|
|
59
|
+
|
|
60
|
+
// Separator
|
|
61
|
+
const sep = chalk.dim("─".repeat(Math.min(width - 4, 60)));
|
|
62
|
+
|
|
63
|
+
// Info rows: key dim, value default
|
|
64
|
+
const rows: string[] = [sep];
|
|
65
|
+
const kv = (k: string, v: string) =>
|
|
66
|
+
` ${chalk.dim(k.padEnd(12))}${v}`;
|
|
67
|
+
if (info.model) rows.push(kv("Model", `${info.provider ?? "unknown"}/${info.model}`));
|
|
68
|
+
if (info.cwd) rows.push(kv("Working Dir", info.cwd));
|
|
69
|
+
if (info.platform) rows.push(kv("Platform", info.platform));
|
|
70
|
+
rows.push(kv("Version", readVersion()));
|
|
71
|
+
rows.push(sep);
|
|
72
|
+
|
|
73
|
+
// Hints
|
|
74
|
+
rows.push(` ${chalk.dim("Type your message to start chatting.")}`);
|
|
75
|
+
rows.push(` ${chalk.dim("/exit or Ctrl+C to quit.")}`);
|
|
76
|
+
|
|
77
|
+
return brandLine + subLine + "\n" + rows.join("\n") + "\n";
|
|
122
78
|
}
|
|
123
79
|
|
|
124
80
|
export function renderInlineHeader(info: SplashInfo = {}): string {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
81
|
+
const model = info.model
|
|
82
|
+
? chalk.cyan(`${info.provider ?? "?"}/${info.model}`)
|
|
83
|
+
: chalk.dim("no model");
|
|
84
|
+
const ver = chalk.dim(`v${readVersion()}`);
|
|
85
|
+
return `\n ${chalk.cyan.bold("MY")} ${chalk.bold("snsagent")} ${ver} ${chalk.dim("│")} ${model}\n`;
|
|
130
86
|
}
|
package/src/ui/error-display.ts
CHANGED
|
@@ -1,130 +1,94 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Supports error hierarchy: main error + optional cause chain.
|
|
2
|
+
* Error display — minimal, single accent, no gradient.
|
|
3
|
+
* Severity controls the icon color (red/yellow/cyan) but everything else
|
|
4
|
+
* stays dim + default text.
|
|
6
5
|
*/
|
|
7
|
-
import chalk from "chalk";
|
|
8
|
-
import gradient from "gradient-string";
|
|
9
|
-
import boxen from "boxen";
|
|
10
|
-
import { BRAND_GRADIENT, ROLE_HEX } from "./colors.js";
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
const WARN_GRADIENT = ["#ffd700", "#ff8c00"] as const;
|
|
7
|
+
import chalk from "chalk";
|
|
14
8
|
|
|
15
9
|
export type ErrorSeverity = "error" | "warning" | "info";
|
|
16
10
|
|
|
17
11
|
export interface ErrorDisplayOptions {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
severity?: ErrorSeverity;
|
|
26
|
-
/** Stack trace (shown collapsed). */
|
|
27
|
-
stack?: string;
|
|
28
|
-
/** Suggestion for fixing the error. */
|
|
29
|
-
suggestion?: string;
|
|
30
|
-
/** Additional context lines. */
|
|
31
|
-
context?: string[];
|
|
12
|
+
title: string;
|
|
13
|
+
message: string;
|
|
14
|
+
code?: string;
|
|
15
|
+
severity?: ErrorSeverity;
|
|
16
|
+
stack?: string;
|
|
17
|
+
suggestion?: string;
|
|
18
|
+
context?: string[];
|
|
32
19
|
}
|
|
33
20
|
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
21
|
+
function getSeverityMeta(severity: ErrorSeverity) {
|
|
22
|
+
switch (severity) {
|
|
23
|
+
case "error":
|
|
24
|
+
return { icon: "✗", label: "ERROR", color: chalk.red.bold };
|
|
25
|
+
case "warning":
|
|
26
|
+
return { icon: "⚠", label: "WARNING", color: chalk.yellow.bold };
|
|
27
|
+
case "info":
|
|
28
|
+
return { icon: "ℹ", label: "INFO", color: chalk.cyan.bold };
|
|
29
|
+
}
|
|
43
30
|
}
|
|
44
31
|
|
|
45
|
-
/**
|
|
46
|
-
* Render an error display block.
|
|
47
|
-
*/
|
|
48
32
|
export function renderErrorDisplay(opts: ErrorDisplayOptions): string {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const width = Math.min(cols - 2, 72);
|
|
33
|
+
const severity = opts.severity ?? "error";
|
|
34
|
+
const meta = getSeverityMeta(severity);
|
|
35
|
+
const cols = process.stdout.columns ?? 80;
|
|
36
|
+
const width = Math.min(cols - 2, 72);
|
|
54
37
|
|
|
55
|
-
|
|
38
|
+
const border = "─".repeat(width - 2);
|
|
56
39
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
? chalk.red.bold(colors.icon)
|
|
60
|
-
: severity === "warning"
|
|
61
|
-
? chalk.yellow.bold(colors.icon)
|
|
62
|
-
: chalk.cyan(colors.icon);
|
|
63
|
-
const headerLabel = grad(` ${colors.label} `);
|
|
64
|
-
const codeStr = opts.code ? chalk.dim(` [${opts.code}]`) : "";
|
|
65
|
-
lines.push(`${headerIcon} ${headerLabel}${codeStr}`);
|
|
40
|
+
const lines: string[] = [];
|
|
41
|
+
lines.push(chalk.dim(border));
|
|
66
42
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
43
|
+
// Header
|
|
44
|
+
const codeStr = opts.code ? chalk.dim(` [${opts.code}]`) : "";
|
|
45
|
+
lines.push(`${meta.color(meta.icon)} ${meta.color(meta.label)}${codeStr}`);
|
|
46
|
+
lines.push("");
|
|
47
|
+
lines.push(chalk.bold(opts.title));
|
|
48
|
+
lines.push("");
|
|
70
49
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
lines.push(chalk.white(line));
|
|
76
|
-
}
|
|
50
|
+
// Message
|
|
51
|
+
for (const line of opts.message.split("\n")) {
|
|
52
|
+
lines.push(line);
|
|
53
|
+
}
|
|
77
54
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
55
|
+
// Context
|
|
56
|
+
if (opts.context && opts.context.length > 0) {
|
|
57
|
+
lines.push("");
|
|
58
|
+
lines.push(chalk.dim("Context:"));
|
|
59
|
+
for (const ctx of opts.context.slice(0, 5)) {
|
|
60
|
+
lines.push(chalk.dim(` ${ctx}`));
|
|
61
|
+
}
|
|
62
|
+
if (opts.context.length > 5) {
|
|
63
|
+
lines.push(chalk.dim(` ... +${opts.context.length - 5} more`));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
89
66
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
67
|
+
// Suggestion
|
|
68
|
+
if (opts.suggestion) {
|
|
69
|
+
lines.push("");
|
|
70
|
+
lines.push(`${chalk.cyan("→")} ${chalk.bold("Suggestion:")}`);
|
|
71
|
+
lines.push(` ${opts.suggestion}`);
|
|
72
|
+
}
|
|
96
73
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
74
|
+
// Stack preview
|
|
75
|
+
if (opts.stack) {
|
|
76
|
+
lines.push("");
|
|
77
|
+
const stackLines = opts.stack.split("\n").slice(0, 3);
|
|
78
|
+
lines.push(chalk.dim("Stack (first 3):"));
|
|
79
|
+
for (const sl of stackLines) {
|
|
80
|
+
lines.push(chalk.dim(` ${sl.trim()}`));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
106
83
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return boxen(content, {
|
|
110
|
-
padding: { top: 1, bottom: 1, left: 2, right: 2 },
|
|
111
|
-
margin: { top: 0, bottom: 1, left: 0, right: 0 },
|
|
112
|
-
borderStyle: "round",
|
|
113
|
-
borderColor: colors.border,
|
|
114
|
-
width,
|
|
115
|
-
});
|
|
84
|
+
lines.push(chalk.dim(border));
|
|
85
|
+
return lines.join("\n");
|
|
116
86
|
}
|
|
117
87
|
|
|
118
|
-
/**
|
|
119
|
-
* Quick error render — wraps a simple error message in a styled block.
|
|
120
|
-
*/
|
|
121
88
|
export function renderQuickError(title: string, message: string): string {
|
|
122
|
-
|
|
89
|
+
return renderErrorDisplay({ title, message, severity: "error" });
|
|
123
90
|
}
|
|
124
91
|
|
|
125
|
-
/**
|
|
126
|
-
* Render a warning display.
|
|
127
|
-
*/
|
|
128
92
|
export function renderWarning(title: string, message: string, suggestion?: string): string {
|
|
129
|
-
|
|
93
|
+
return renderErrorDisplay({ title, message, severity: "warning", suggestion });
|
|
130
94
|
}
|