@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.
- package/CHANGELOG.md +12 -1
- package/README.md +7 -8
- package/package.json +3 -3
- package/scripts/apply-pi-natives-patch.js +82 -56
- package/src/adapters/telegram/bot.ts +41 -1
- package/src/adapters/telegram/bridge.ts +186 -0
- package/src/adapters/telegram/handler.ts +138 -13
- package/src/adapters/telegram/index.ts +12 -2
- package/src/agents/__tests__/config.test.ts +79 -0
- package/src/agents/__tests__/resilience.test.ts +119 -0
- package/src/agents/__tests__/strategies.test.ts +83 -0
- package/src/agents/config.ts +367 -0
- package/src/agents/ensemble.ts +224 -0
- package/src/agents/resilience.ts +332 -0
- package/src/agents/strategies/best-of-n.ts +108 -0
- package/src/agents/strategies/consensus.ts +105 -0
- package/src/agents/strategies/critic.ts +131 -0
- package/src/agents/strategies/types.ts +68 -0
- package/src/async/__tests__/task-runner.test.ts +162 -0
- package/src/async/__tests__/task-store.test.ts +146 -0
- package/src/async/index.ts +28 -1
- package/src/async/notifier.ts +133 -0
- package/src/async/task-runner.ts +175 -0
- package/src/async/task-store.ts +170 -0
- package/src/async/types.ts +70 -0
- package/src/cli/entry.ts +3 -1
- package/src/cli/index.ts +69 -54
- package/src/config/index.ts +1 -1
- package/src/debug/index.ts +1 -1
- package/src/modes/components/welcome.ts +13 -6
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/setup-wizard/scenes/splash.ts +1 -1
- package/src/session/agent-session.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +63 -0
- package/src/slash-commands/helpers/task.ts +181 -0
- package/src/tbm/__tests__/tbm.test.ts +660 -0
- package/src/tbm/comm-modes.ts +165 -0
- package/src/tbm/config.ts +136 -0
- package/src/tbm/context-delta.ts +146 -0
- package/src/tbm/context-pyramid.ts +202 -0
- package/src/tbm/dashboard.ts +131 -0
- package/src/tbm/index.ts +247 -0
- package/src/tbm/lazy-skills.ts +182 -0
- package/src/tbm/response-cache.ts +220 -0
- package/src/tbm/tombstone.ts +189 -0
- package/src/tbm/tool-compress.ts +230 -0
- package/src/tools/ask.ts +1 -1
- package/src/tui/chat-blocks.ts +205 -0
- package/src/tui/chat-ui.ts +270 -0
- package/src/tui/code-cell.ts +90 -1
- package/src/tui/command-palette.ts +189 -0
- package/src/tui/index.ts +4 -0
- package/src/tui/splash.ts +130 -0
- package/src/ui/banner.ts +69 -29
- package/src/ui/colors.ts +24 -0
- package/src/ui/error-display.ts +130 -0
- package/src/ui/gradient.ts +104 -0
- package/src/ui/index.ts +15 -0
- package/src/ui/memory-toast.ts +102 -0
- package/src/ui/status-bar.ts +36 -30
- 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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
}
|