@sns-myagent/cli 0.2.0 → 0.3.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 (61) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/README.md +7 -8
  3. package/package.json +3 -3
  4. package/scripts/apply-pi-natives-patch.js +82 -56
  5. package/src/adapters/telegram/bot.ts +41 -1
  6. package/src/adapters/telegram/bridge.ts +186 -0
  7. package/src/adapters/telegram/handler.ts +138 -13
  8. package/src/adapters/telegram/index.ts +12 -2
  9. package/src/agents/__tests__/config.test.ts +79 -0
  10. package/src/agents/__tests__/resilience.test.ts +119 -0
  11. package/src/agents/__tests__/strategies.test.ts +83 -0
  12. package/src/agents/config.ts +367 -0
  13. package/src/agents/ensemble.ts +224 -0
  14. package/src/agents/resilience.ts +332 -0
  15. package/src/agents/strategies/best-of-n.ts +108 -0
  16. package/src/agents/strategies/consensus.ts +105 -0
  17. package/src/agents/strategies/critic.ts +131 -0
  18. package/src/agents/strategies/types.ts +68 -0
  19. package/src/async/__tests__/task-runner.test.ts +162 -0
  20. package/src/async/__tests__/task-store.test.ts +146 -0
  21. package/src/async/index.ts +28 -1
  22. package/src/async/notifier.ts +133 -0
  23. package/src/async/task-runner.ts +175 -0
  24. package/src/async/task-store.ts +170 -0
  25. package/src/async/types.ts +70 -0
  26. package/src/cli/entry.ts +3 -1
  27. package/src/cli/index.ts +69 -54
  28. package/src/config/index.ts +1 -1
  29. package/src/debug/index.ts +1 -1
  30. package/src/modes/components/welcome.ts +13 -6
  31. package/src/modes/controllers/event-controller.ts +1 -1
  32. package/src/modes/setup-wizard/scenes/splash.ts +1 -1
  33. package/src/session/agent-session.ts +1 -1
  34. package/src/slash-commands/builtin-registry.ts +63 -0
  35. package/src/slash-commands/helpers/task.ts +181 -0
  36. package/src/tbm/__tests__/tbm.test.ts +660 -0
  37. package/src/tbm/comm-modes.ts +165 -0
  38. package/src/tbm/config.ts +136 -0
  39. package/src/tbm/context-delta.ts +146 -0
  40. package/src/tbm/context-pyramid.ts +202 -0
  41. package/src/tbm/dashboard.ts +131 -0
  42. package/src/tbm/index.ts +247 -0
  43. package/src/tbm/lazy-skills.ts +182 -0
  44. package/src/tbm/response-cache.ts +220 -0
  45. package/src/tbm/tombstone.ts +189 -0
  46. package/src/tbm/tool-compress.ts +230 -0
  47. package/src/tools/ask.ts +1 -1
  48. package/src/tui/chat-blocks.ts +205 -0
  49. package/src/tui/chat-ui.ts +270 -0
  50. package/src/tui/code-cell.ts +90 -1
  51. package/src/tui/command-palette.ts +189 -0
  52. package/src/tui/index.ts +4 -0
  53. package/src/tui/splash.ts +130 -0
  54. package/src/ui/banner.ts +69 -29
  55. package/src/ui/colors.ts +24 -0
  56. package/src/ui/error-display.ts +130 -0
  57. package/src/ui/gradient.ts +104 -0
  58. package/src/ui/index.ts +15 -0
  59. package/src/ui/memory-toast.ts +102 -0
  60. package/src/ui/status-bar.ts +36 -30
  61. package/bin/snscoder.js +0 -98
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Command palette component for SNS-MyAgent terminal UI.
3
+ *
4
+ * Fuzzy-searchable command overlay with gradient styling.
5
+ * Triggered by Ctrl+P or typing "/" in the input.
6
+ */
7
+ import chalk from "chalk";
8
+ import gradient from "gradient-string";
9
+ import { BRAND_GRADIENT, ACCENT_GRADIENT } from "../ui/colors.js";
10
+
11
+ export interface PaletteCommand {
12
+ /** Command name (e.g., "/help"). */
13
+ name: string;
14
+ /** Short description. */
15
+ description: string;
16
+ /** Category for grouping. */
17
+ category: string;
18
+ /** Keyboard shortcut (optional). */
19
+ shortcut?: string;
20
+ /** Whether command is enabled. */
21
+ enabled?: boolean;
22
+ }
23
+
24
+ export interface CommandPaletteOptions {
25
+ /** Available commands. */
26
+ commands: PaletteCommand[];
27
+ /** Search query filter. */
28
+ query?: string;
29
+ /** Currently highlighted index. */
30
+ highlighted?: number;
31
+ /** Width of the palette. */
32
+ width?: number;
33
+ }
34
+
35
+ // ── Pre-built command lists ──
36
+
37
+ export const CHAT_COMMANDS: PaletteCommand[] = [
38
+ { name: "/help", description: "Show available commands", category: "General", shortcut: "?" },
39
+ { name: "/clear", description: "Clear screen", category: "General", shortcut: "⌘K" },
40
+ { name: "/exit", description: "Quit the chat", category: "General", shortcut: "⌘Q" },
41
+ { name: "/model", description: "Show/switch model", category: "Config" },
42
+ { name: "/history", description: "Show conversation history", category: "General" },
43
+ { name: "/memory", description: "Recall from memory", category: "Memory" },
44
+ { name: "/skills", description: "List available skills", category: "Extensibility" },
45
+ { name: "/plugins", description: "List installed plugins", category: "Extensibility" },
46
+ { name: "/mcp", description: "MCP server management", category: "Extensibility" },
47
+ { name: "/theme", description: "Change terminal theme", category: "Config" },
48
+ { name: "/bench", description: "Run benchmarks", category: "Debug" },
49
+ { name: "/debug", description: "Debug info", category: "Debug" },
50
+ ];
51
+
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
+ export function renderCommandPalette(opts: CommandPaletteOptions): string {
85
+ const cols = process.stdout.columns ?? 80;
86
+ const width = Math.min(opts.width ?? cols - 4, 64);
87
+ const grad = gradient(BRAND_GRADIENT);
88
+ const accentGrad = gradient(ACCENT_GRADIENT);
89
+
90
+ // Filter and score commands
91
+ const query = opts.query ?? "";
92
+ const scored = opts.commands
93
+ .map(cmd => ({
94
+ cmd,
95
+ score: fuzzyMatch(query, cmd.name) + fuzzyMatch(query, cmd.description) * 0.5,
96
+ }))
97
+ .filter(s => s.score > 0)
98
+ .sort((a, b) => b.score - a.score)
99
+ .slice(0, 12);
100
+
101
+ const highlighted = opts.highlighted ?? 0;
102
+
103
+ // Group by category
104
+ const groups = new Map<string, typeof scored>();
105
+ for (const item of scored) {
106
+ const cat = item.cmd.category;
107
+ if (!groups.has(cat)) groups.set(cat, []);
108
+ groups.get(cat)!.push(item);
109
+ }
110
+
111
+ const lines: string[] = [];
112
+ const inner = width - 4;
113
+
114
+ // ── Header ──
115
+ const searchIcon = "🔍";
116
+ const headerText = `${searchIcon} ${accentGrad("Command Palette")}`;
117
+ const fill = "─".repeat(Math.max(0, inner - 22));
118
+ lines.push(grad("╭─") + headerText + grad(fill + "─╮"));
119
+
120
+ // ── Search bar ──
121
+ const searchDisplay = query ? chalk.white(query) + chalk.dim("█") : chalk.dim("Type to search...");
122
+ const searchFill = " ".repeat(Math.max(0, inner - searchDisplay.length + 8));
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
+ }
package/src/tui/index.ts CHANGED
@@ -11,3 +11,7 @@ export * from "./tree-list";
11
11
  export * from "./types";
12
12
  export * from "./utils";
13
13
  export * from "./width-aware-text";
14
+ export * from "./splash";
15
+ export * from "./chat-blocks";
16
+ export * from "./chat-ui";
17
+ export { renderCommandPalette, filterCommands, CHAT_COMMANDS, type PaletteCommand, type CommandPaletteOptions } from "./command-palette";
@@ -0,0 +1,130 @@
1
+ /**
2
+ * SNS-MyAgent splash screen — branded gradient banner + system info blocks.
3
+ * Premium terminal UI with gradient logo, rounded box, and info panel.
4
+ */
5
+ import chalk from "chalk";
6
+ import gradient from "gradient-string";
7
+ import boxen from "boxen";
8
+ import { readFileSync } from "node:fs";
9
+ import { dirname, resolve } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ const BANNER_ART = `
13
+ ███╗ ██╗███████╗██╗ ██╗██╗ ██╗███████╗
14
+ ████╗ ██║██╔════╝╚██╗██╔╝██║ ██║██╔════╝
15
+ ██╔██╗ ██║█████╗ ╚███╔╝ ██║ ██║███████╗
16
+ ██║╚██╗██║██╔══╝ ██╔██╗ ██║ ██║╚════██║
17
+ ██║ ╚████║███████╗██╔╝ ╚██╗╚██████╔╝███████║
18
+ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝`;
19
+
20
+ const SUBTITLE = "My-Agent • SnsCoder CLI";
21
+
22
+ const SNY_GRADIENT = ["#00d2ff", "#7b2ff7", "#ff6b9d"];
23
+ const ACCENT_GRADIENT = ["#7b2ff7", "#00d2ff"];
24
+
25
+ function readVersion(): string {
26
+ try {
27
+ // Strategy 1: import.meta.url relative path (works in most bundler/ts-node setups)
28
+ const here = dirname(fileURLToPath(import.meta.url));
29
+ const candidates = [
30
+ resolve(here, "..", "..", "package.json"), // src/tui/ → root
31
+ resolve(here, "..", "package.json"), // dist/tui/ → dist/ (fallback)
32
+ resolve(here, "..", "..", "dist", "package.json"), // flat dist
33
+ ];
34
+
35
+ for (const pkgPath of candidates) {
36
+ try {
37
+ const raw = readFileSync(pkgPath, "utf8");
38
+ const pkg = JSON.parse(raw) as { version?: string };
39
+ if (pkg.version && pkg.version !== "0.0.0") return pkg.version;
40
+ } catch { /* try next */ }
41
+ }
42
+
43
+ // Strategy 2: Walk up from CWD looking for package.json with matching name
44
+ let dir = process.cwd();
45
+ for (let i = 0; i < 5; i++) {
46
+ try {
47
+ const raw = readFileSync(resolve(dir, "package.json"), "utf8");
48
+ const pkg = JSON.parse(raw) as { name?: string; version?: string };
49
+ if (pkg.name === "@sns-myagent/cli" && pkg.version) return pkg.version;
50
+ } catch { /* continue */ }
51
+ const parent = dirname(dir);
52
+ if (parent === dir) break;
53
+ dir = parent;
54
+ }
55
+
56
+ return "0.0.0";
57
+ } catch {
58
+ return "0.0.0";
59
+ }
60
+ }
61
+
62
+ export interface SplashInfo {
63
+ model?: string;
64
+ provider?: string;
65
+ cwd?: string;
66
+ platform?: string;
67
+ nodeVersion?: string;
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");
91
+ }
92
+
93
+ export function renderSplash(info: SplashInfo = {}): string {
94
+ const cols = process.stdout.columns ?? 80;
95
+ const bannerWidth = Math.min(cols - 2, 64);
96
+
97
+ // Gradient banner
98
+ const bannerLines = BANNER_ART.split("\n").filter(l => l.trim().length > 0);
99
+ const coloredBanner = bannerLines
100
+ .map(l => gradient(SNY_GRADIENT).multiline(l))
101
+ .join("\n");
102
+
103
+ // Subtitle
104
+ const subtitle = gradient(ACCENT_GRADIENT)(SUBTITLE);
105
+
106
+ // Info block
107
+ const infoBlock = makeInfoBlocks(info, bannerWidth);
108
+
109
+ // Wrap in boxen
110
+ const content = `${coloredBanner}\n${subtitle}\n${infoBlock}`;
111
+
112
+ const box = boxen(content, {
113
+ padding: { top: 1, bottom: 1, left: 2, right: 2 },
114
+ margin: { top: 0, bottom: 0, left: 1, right: 1 },
115
+ borderStyle: "round",
116
+ borderColor: "cyan",
117
+ width: bannerWidth,
118
+ textAlignment: "center",
119
+ });
120
+
121
+ return box;
122
+ }
123
+
124
+ export function renderInlineHeader(info: SplashInfo = {}): string {
125
+ const model = info.model
126
+ ? chalk.cyan(`${info.provider ?? "?"}/${info.model}`)
127
+ : chalk.dim("no model");
128
+ const ver = chalk.dim(`v${readVersion()}`);
129
+ return `\n ${gradient(SNY_GRADIENT)("SnsCoder")} ${ver} ${chalk.dim("│")} ${model}\n`;
130
+ }
package/src/ui/banner.ts CHANGED
@@ -1,41 +1,81 @@
1
1
  /**
2
- * ASCII art banner for SNS-MyAgent.
2
+ * Premium ASCII art banner for SNS-MyAgent.
3
3
  *
4
- * Renders the snscoder logo + runtime info on `snscoder chat`.
4
+ * Renders the branded logo + runtime info using chalk, gradient-string, and boxen.
5
+ * Called at the start of `snscoder launch`.
5
6
  */
6
7
 
8
+ import chalk from "chalk";
9
+ import gradient from "gradient-string";
10
+ import boxen from "boxen";
7
11
  import type { FullConfig } from "#src/config/index.js";
8
- import { accent, bold, muted, primary, success, warning } from "./colors.js";
9
12
  import { loadSnsConfig } from "#src/config/sns-config.js";
13
+ import { BRAND_GRADIENT, ACCENT_GRADIENT } from "./colors.js";
10
14
 
11
- const LOGO = [
12
- ` ___ _ ___ _`,
13
- ` / __| ___ __ _| |__ / __|___ ___ __ _| |___`,
14
- ` \\__ \\/ _ \\/ _\` | '_ \\| | / __|/ _ \\/ _\` | / __|`,
15
- ` |___/\\___/\\__,_|_.__/|_| \\___|\\___/\\__,_|_\\___|`,
16
- ].join("\n");
15
+ const LOGO = `
16
+ ███╗ ██╗███████╗██╗ ██╗██╗ ██╗███████╗
17
+ ████╗ ██║██╔════╝╚██╗██╔╝██║ ██║██╔════╝
18
+ ██╔██╗ ██║█████╗ ╚███╔╝ ██║ ██║███████╗
19
+ ██║╚██╗██║██╔══╝ ██╔██╗ ██║ ██║╚════██║
20
+ ██║ ╚████║███████╗██╔╝ ╚██╗╚██████╔╝███████║
21
+ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝`;
22
+
23
+ const SUBTITLE = "My-Agent • SnsCoder CLI";
17
24
 
18
25
  /**
19
- * Show the banner with config info.
20
- * Called at the start of `snscoder chat`.
26
+ * Show the premium banner with gradient logo + config info.
27
+ * Called at the start of `snscoder launch`.
21
28
  */
22
29
  export function showBanner(config: FullConfig): void {
23
- const line = muted("─".repeat(52));
24
-
25
- console.log();
26
- console.log(accent(LOGO));
27
- console.log(line);
28
-
29
- const version = bold(`v${config.version}`);
30
- const provider = primary(config.provider);
31
- const model = primary(config.model);
32
- const hasKey = Boolean(loadSnsConfig().apiKey);
33
- const memStatus = hasKey ? success("✓ API key set") : warning("⚠ no API key (BYOK)");
34
-
35
- console.log(` ${muted("version")} ${version}`);
36
- console.log(` ${muted("provider")} ${provider}`);
37
- console.log(` ${muted("model")} ${model}`);
38
- console.log(` ${muted("memory")} ${memStatus}`);
39
- console.log(line);
40
- console.log();
30
+ const cols = process.stdout.columns ?? 80;
31
+ const width = Math.min(cols - 2, 64);
32
+
33
+ // Gradient logo
34
+ const bannerLines = LOGO.split("\n").filter(l => l.trim().length > 0);
35
+ const coloredLogo = bannerLines
36
+ .map(l => gradient(BRAND_GRADIENT).multiline(l))
37
+ .join("\n");
38
+
39
+ // Subtitle
40
+ const subtitle = gradient(ACCENT_GRADIENT)(SUBTITLE);
41
+
42
+ // Info block
43
+ const version = chalk.bold(`v${config.version}`);
44
+ const provider = chalk.cyan(config.provider);
45
+ const model = chalk.cyan(config.model);
46
+ const hasKey = Boolean(loadSnsConfig().apiKey);
47
+ const memStatus = hasKey
48
+ ? chalk.green("✓ API key set")
49
+ : chalk.yellow("⚠ no API key (BYOK)");
50
+
51
+ const inner = Math.min(width - 4, 56);
52
+ const sep = chalk.dim("─".repeat(inner));
53
+ const kv = (k: string, v: string) =>
54
+ ` ${chalk.dim(k.padEnd(12))}${chalk.white(v)}`;
55
+
56
+ const info = [
57
+ sep,
58
+ kv("Version", version),
59
+ kv("Provider", provider),
60
+ kv("Model", model),
61
+ kv("Memory", memStatus),
62
+ sep,
63
+ chalk.dim(" Type your message to start chatting."),
64
+ chalk.dim(" /exit or Ctrl+C to quit."),
65
+ ].join("\n");
66
+
67
+ // Wrap in boxen
68
+ const content = `${coloredLogo}\n${subtitle}\n${info}`;
69
+
70
+ const box = boxen(content, {
71
+ padding: { top: 1, bottom: 1, left: 2, right: 2 },
72
+ margin: { top: 1, bottom: 0, left: 0, right: 0 },
73
+ borderStyle: "round",
74
+ borderColor: "cyan",
75
+ width,
76
+ textAlignment: "center",
77
+ });
78
+
79
+ console.log(box);
80
+ console.log();
41
81
  }
package/src/ui/colors.ts CHANGED
@@ -2,10 +2,13 @@
2
2
  * Brand color palette for SNS-MyAgent terminal UI.
3
3
  *
4
4
  * Uses picocolors for zero-dependency color output.
5
+ * Brand gradient: #00d2ff → #7b2ff7 → #ff6b9d
5
6
  */
6
7
 
7
8
  import pc from "picocolors";
8
9
 
10
+ // ── Brand identity colors ──
11
+
9
12
  /** Primary brand color — used for headers and highlights. */
10
13
  export const primary = pc.cyan;
11
14
 
@@ -35,3 +38,24 @@ export const muted = pc.dim;
35
38
 
36
39
  /** Bold alias for emphasis. */
37
40
  export const bold = pc.bold;
41
+
42
+ // ── Brand gradient constants (for gradient-string) ──
43
+
44
+ /** Full brand gradient: cyan → purple → pink. */
45
+ export const BRAND_GRADIENT = ["#00d2ff", "#7b2ff7", "#ff6b9d"];
46
+
47
+ /** Accent gradient: purple → cyan. */
48
+ export const ACCENT_GRADIENT = ["#7b2ff7", "#00d2ff"];
49
+
50
+ /** Subtle gradient: cyan → purple. */
51
+ export const SUBTLE_GRADIENT = ["#00d2ff", "#7b2ff7"];
52
+
53
+ // ── Role-specific border colors (hex for gradient-string) ──
54
+
55
+ export const ROLE_HEX = {
56
+ user: "#00d2ff",
57
+ assistant: "#7b2ff7",
58
+ tool: "#ffd700",
59
+ system: "#666666",
60
+ error: "#ff4444",
61
+ } as const;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Premium error display component for SNS-MyAgent terminal UI.
3
+ *
4
+ * Renders errors in styled bordered blocks with gradient accents.
5
+ * Supports error hierarchy: main error + optional cause chain.
6
+ */
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
+
12
+ const ERROR_GRADIENT = [ROLE_HEX.error, "#cc0000"] as const;
13
+ const WARN_GRADIENT = ["#ffd700", "#ff8c00"] as const;
14
+
15
+ export type ErrorSeverity = "error" | "warning" | "info";
16
+
17
+ export interface ErrorDisplayOptions {
18
+ /** Error title / short description. */
19
+ title: string;
20
+ /** Detailed error message. */
21
+ message: string;
22
+ /** Error code (e.g., "E001", "HTTP_404"). */
23
+ code?: string;
24
+ /** Severity level. */
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[];
32
+ }
33
+
34
+ function getSeverityColors(severity: ErrorSeverity) {
35
+ switch (severity) {
36
+ case "error":
37
+ return { border: "red" as const, gradient: [...ERROR_GRADIENT], icon: "✗", label: "ERROR" };
38
+ case "warning":
39
+ return { border: "yellow" as const, gradient: [...WARN_GRADIENT], icon: "⚠", label: "WARNING" };
40
+ case "info":
41
+ return { border: "cyan" as const, gradient: ["#00d2ff", "#7b2ff7"] as [string, string], icon: "ℹ", label: "INFO" };
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Render an error display block.
47
+ */
48
+ export function renderErrorDisplay(opts: ErrorDisplayOptions): string {
49
+ const severity = opts.severity ?? "error";
50
+ const colors = getSeverityColors(severity);
51
+ const grad = gradient(colors.gradient);
52
+ const cols = process.stdout.columns ?? 80;
53
+ const width = Math.min(cols - 2, 72);
54
+
55
+ const lines: string[] = [];
56
+
57
+ // ── Header ──
58
+ const headerIcon = severity === "error"
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}`);
66
+
67
+ // ── Title ──
68
+ lines.push("");
69
+ lines.push(chalk.bold.white(opts.title));
70
+
71
+ // ── Message ──
72
+ lines.push("");
73
+ const msgLines = opts.message.split("\n");
74
+ for (const line of msgLines) {
75
+ lines.push(chalk.white(line));
76
+ }
77
+
78
+ // ── Context lines ──
79
+ if (opts.context && opts.context.length > 0) {
80
+ lines.push("");
81
+ lines.push(chalk.dim("Context:"));
82
+ for (const ctx of opts.context.slice(0, 5)) {
83
+ lines.push(chalk.dim(` ${ctx}`));
84
+ }
85
+ if (opts.context.length > 5) {
86
+ lines.push(chalk.dim(` ... +${opts.context.length - 5} more`));
87
+ }
88
+ }
89
+
90
+ // ── Suggestion ──
91
+ if (opts.suggestion) {
92
+ lines.push("");
93
+ lines.push(`${chalk.green("💡")} ${chalk.green.bold("Suggestion:")}`);
94
+ lines.push(chalk.green(` ${opts.suggestion}`));
95
+ }
96
+
97
+ // ── Stack trace (collapsed preview) ──
98
+ if (opts.stack) {
99
+ lines.push("");
100
+ const stackLines = opts.stack.split("\n").slice(0, 3);
101
+ lines.push(chalk.dim("Stack trace (first 3 lines):"));
102
+ for (const sl of stackLines) {
103
+ lines.push(chalk.dim(` ${sl.trim()}`));
104
+ }
105
+ }
106
+
107
+ // ── Render in boxen ──
108
+ const content = lines.join("\n");
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
+ });
116
+ }
117
+
118
+ /**
119
+ * Quick error render — wraps a simple error message in a styled block.
120
+ */
121
+ export function renderQuickError(title: string, message: string): string {
122
+ return renderErrorDisplay({ title, message, severity: "error" });
123
+ }
124
+
125
+ /**
126
+ * Render a warning display.
127
+ */
128
+ export function renderWarning(title: string, message: string, suggestion?: string): string {
129
+ return renderErrorDisplay({ title, message, severity: "warning", suggestion });
130
+ }