@sns-myagent/cli 0.3.4 → 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.
Files changed (43) hide show
  1. package/package.json +6 -4
  2. package/scripts/smoke-tui.ts +110 -0
  3. package/src/cli/agents-cli.ts +1 -1
  4. package/src/cli/commands/init-xdg.ts +1 -1
  5. package/src/cli/profile-alias.ts +6 -6
  6. package/src/cli/ssh-cli.ts +7 -7
  7. package/src/commands/read.ts +1 -1
  8. package/src/config/settings-schema.ts +1 -1
  9. package/src/config/settings.ts +1 -1
  10. package/src/dap/session.ts +2 -2
  11. package/src/hindsight/bank.ts +1 -1
  12. package/src/hindsight/config.ts +1 -1
  13. package/src/internal-urls/docs-index.ts +1 -1
  14. package/src/internal-urls/index.ts +1 -1
  15. package/src/internal-urls/local-protocol.ts +2 -2
  16. package/src/internal-urls/router.ts +3 -3
  17. package/src/internal-urls/{omp-protocol.ts → snsagent-protocol.ts} +12 -12
  18. package/src/internal-urls/types.ts +1 -1
  19. package/src/modes/acp/acp-agent.ts +1 -1
  20. package/src/modes/components/welcome.ts +9 -7
  21. package/src/modes/internal-url-autocomplete.ts +1 -1
  22. package/src/modes/setup-wizard/scenes/splash.ts +1 -1
  23. package/src/task/omp-command.ts +1 -1
  24. package/src/tools/image-gen.ts +1 -1
  25. package/src/tools/read.ts +2 -2
  26. package/src/tools/report-tool-issue.ts +1 -1
  27. package/src/tools/search.ts +4 -4
  28. package/src/tui/chat-blocks.ts +117 -174
  29. package/src/tui/code-cell.ts +9 -9
  30. package/src/tui/command-palette.ts +62 -175
  31. package/src/tui/hyperlink.ts +1 -1
  32. package/src/tui/index.ts +1 -1
  33. package/src/tui/splash.ts +67 -111
  34. package/src/ui/banner.ts +1 -1
  35. package/src/ui/error-display.ts +67 -103
  36. package/src/ui/gradient.ts +15 -95
  37. package/src/ui/index.ts +3 -9
  38. package/src/ui/memory-toast.ts +51 -77
  39. package/src/ui/status-bar.ts +26 -47
  40. package/src/web/scrapers/crates-io.ts +1 -1
  41. package/src/web/scrapers/docs-rs.ts +1 -1
  42. package/src/web/scrapers/github.ts +1 -1
  43. package/src/web/search/providers/codex.ts +2 -2
@@ -1,205 +1,148 @@
1
1
  /**
2
- * Chat message block renderers — bordered terminal blocks for each message role.
3
- * User, assistant, tool, system messages each get distinct gradient border treatment.
4
- * Premium branded terminal UI for SNS-MyAgent.
2
+ * Chat message block renderers — single-accent bordered blocks.
3
+ * One border style (rounded), one accent color (cyan). No role-specific
4
+ * rainbow. Per-role distinction via bold label only.
5
5
  */
6
6
  import chalk from "chalk";
7
- import gradient from "gradient-string";
8
7
  import { visibleWidth } from "@oh-my-pi/pi-tui";
9
- import { BRAND_GRADIENT, ROLE_HEX } from "#src/ui/colors.js";
10
8
 
11
- // ── Box-drawing chars (rounded) ──
12
9
  const BOX = {
13
- topLeft: "╭",
14
- topRight: "╮",
15
- bottomLeft: "╰",
16
- bottomRight: "╯",
17
- horizontal: "─",
18
- vertical: "│",
19
- teeRight: "├",
20
- teeLeft: "┤",
10
+ topLeft: "╭",
11
+ topRight: "╮",
12
+ bottomLeft: "╰",
13
+ bottomRight: "╯",
14
+ horizontal: "─",
15
+ vertical: "│",
21
16
  } as const;
22
17
 
23
- // ── Color themes per role ──
24
- const ROLE_COLORS = {
25
- user: { border: chalk.cyan, label: chalk.cyan.bold, gradient: [ROLE_HEX.user, "#0099cc"] },
26
- assistant: { border: chalk.magenta, label: chalk.magenta.bold, gradient: [ROLE_HEX.assistant, "#9b59b6"] },
27
- tool: { border: chalk.yellow, label: chalk.yellow, gradient: [ROLE_HEX.tool, "#e6a700"] },
28
- system: { border: chalk.dim, label: chalk.dim, gradient: [ROLE_HEX.system, "#555555"] },
29
- error: { border: chalk.red, label: chalk.red.bold, gradient: [ROLE_HEX.error, "#cc0000"] },
30
- } as const;
18
+ const ACCENT = chalk.cyan;
19
+
20
+ export type MessageRole = "user" | "assistant" | "tool" | "system" | "error";
31
21
 
32
- export type MessageRole = keyof typeof ROLE_COLORS;
22
+ const ROLE_LABEL: Record<MessageRole, string> = {
23
+ user: "you",
24
+ assistant: "snsagent",
25
+ tool: "tool",
26
+ system: "system",
27
+ error: "error",
28
+ };
33
29
 
34
30
  export interface ChatBlockOptions {
35
- role: MessageRole;
36
- label?: string;
37
- content: string;
38
- width?: number;
39
- meta?: string;
40
- streaming?: boolean;
31
+ role: MessageRole;
32
+ label?: string;
33
+ content: string;
34
+ width?: number;
35
+ meta?: string;
36
+ streaming?: boolean;
41
37
  }
42
38
 
43
- /**
44
- * Word-wrap text respecting ANSI escape sequences.
45
- * Returns array of lines fitting within `maxWidth` visible columns.
46
- */
47
39
  function wrapAnsi(text: string, maxWidth: number): string[] {
48
- const lines: string[] = [];
49
- for (const rawLine of text.split("\n")) {
50
- if (visibleWidth(rawLine) <= maxWidth) {
51
- lines.push(rawLine);
52
- continue;
53
- }
54
- // Simple word-wrap
55
- let current = "";
56
- let currentWidth = 0;
57
- const words = rawLine.split(/(\s+)/);
58
- for (const word of words) {
59
- const wWidth = visibleWidth(word);
60
- if (currentWidth + wWidth > maxWidth && current.length > 0) {
61
- lines.push(current);
62
- current = word.trimStart();
63
- currentWidth = visibleWidth(current);
64
- } else {
65
- current += word;
66
- currentWidth += wWidth;
67
- }
68
- }
69
- if (current.length > 0) lines.push(current);
70
- }
71
- return lines;
40
+ const lines: string[] = [];
41
+ for (const rawLine of text.split("\n")) {
42
+ if (visibleWidth(rawLine) <= maxWidth) {
43
+ lines.push(rawLine);
44
+ continue;
45
+ }
46
+ let current = "";
47
+ let currentWidth = 0;
48
+ const words = rawLine.split(/(\s+)/);
49
+ for (const word of words) {
50
+ const wWidth = visibleWidth(word);
51
+ if (currentWidth + wWidth > maxWidth && current.length > 0) {
52
+ lines.push(current);
53
+ current = word.trimStart();
54
+ currentWidth = visibleWidth(current);
55
+ } else {
56
+ current += word;
57
+ currentWidth += wWidth;
58
+ }
59
+ }
60
+ if (current.length > 0) lines.push(current);
61
+ }
62
+ return lines;
72
63
  }
73
64
 
74
- /**
75
- * Render a single chat message as a bordered block with gradient border.
76
- */
77
65
  export function renderChatBlock(opts: ChatBlockOptions): string {
78
- const cols = process.stdout.columns ?? 80;
79
- const width = Math.min(opts.width ?? cols, cols) - 2; // margin
80
- const colors = ROLE_COLORS[opts.role];
81
- const border = colors.border;
82
-
83
- const pad = 1;
84
- const innerWidth = width - 2 - pad * 2; // borders + padding
85
-
86
- const lines: string[] = [];
87
-
88
- // ── Top bar with gradient label ──
89
- const label = opts.label ?? opts.role.toUpperCase();
90
- const labelText = ` ${label} `;
91
- const topFill = BOX.horizontal.repeat(
92
- Math.max(0, innerWidth - visibleWidth(labelText) + pad * 2)
93
- );
94
- const gradBorder = gradient(colors.gradient as [string, string]);
95
- lines.push(
96
- gradBorder(BOX.topLeft + BOX.horizontal.repeat(2)) +
97
- colors.label(labelText) +
98
- gradBorder(topFill + BOX.topRight)
99
- );
100
-
101
- // ── Content lines with gradient side borders ──
102
- const contentLines = wrapAnsi(opts.content, innerWidth);
103
- const padStr = " ".repeat(pad);
104
- for (const line of contentLines) {
105
- const vis = visibleWidth(line);
106
- const fill = " ".repeat(Math.max(0, innerWidth - vis));
107
- lines.push(
108
- gradBorder(BOX.vertical) + padStr + line + fill + padStr + gradBorder(BOX.vertical)
109
- );
110
- }
111
-
112
- // ── Meta line (timestamp, tokens, etc.) ──
113
- if (opts.meta) {
114
- const metaText = chalk.dim(opts.meta);
115
- const metaFill = " ".repeat(Math.max(0, innerWidth - visibleWidth(opts.meta)));
116
- lines.push(
117
- gradBorder(BOX.vertical) + padStr + metaText + metaFill + padStr + gradBorder(BOX.vertical)
118
- );
119
- }
120
-
121
- // ── Streaming indicator (animated) ──
122
- if (opts.streaming) {
123
- const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
124
- const frameIdx = Date.now() % (frames.length * 80);
125
- const frame = frames[Math.floor(frameIdx / 80)];
126
- const spinnerText = `${chalk.cyan(frame)} ${chalk.dim("thinking...")}`;
127
- const spinFill = " ".repeat(Math.max(0, innerWidth - visibleWidth(spinnerText)));
128
- lines.push(
129
- gradBorder(BOX.vertical) + padStr + spinnerText + spinFill + padStr + gradBorder(BOX.vertical)
130
- );
131
- }
132
-
133
- // ── Bottom bar ──
134
- lines.push(
135
- gradBorder(BOX.bottomLeft + BOX.horizontal.repeat(width - 2) + BOX.bottomRight)
136
- );
137
-
138
- return lines.join("\n");
66
+ const cols = process.stdout.columns ?? 80;
67
+ const width = Math.min(opts.width ?? cols, cols) - 2;
68
+ const pad = 1;
69
+ const innerWidth = width - 2 - pad * 2;
70
+
71
+ const lines: string[] = [];
72
+
73
+ // Top bar: accent line + label
74
+ const label = opts.label ?? ROLE_LABEL[opts.role];
75
+ const labelText = ` ${label} `;
76
+ const topFill = BOX.horizontal.repeat(Math.max(0, innerWidth - visibleWidth(labelText) + pad * 2));
77
+ lines.push(ACCENT(BOX.topLeft + BOX.horizontal.repeat(2)) + chalk.bold(labelText) + ACCENT(topFill + BOX.topRight));
78
+
79
+ // Content
80
+ const contentLines = wrapAnsi(opts.content, innerWidth);
81
+ const padStr = " ".repeat(pad);
82
+ for (const line of contentLines) {
83
+ const vis = visibleWidth(line);
84
+ const fill = " ".repeat(Math.max(0, innerWidth - vis));
85
+ lines.push(ACCENT(BOX.vertical) + padStr + line + fill + padStr + ACCENT(BOX.vertical));
86
+ }
87
+
88
+ // Meta
89
+ if (opts.meta) {
90
+ const metaText = chalk.dim(opts.meta);
91
+ const metaFill = " ".repeat(Math.max(0, innerWidth - visibleWidth(opts.meta)));
92
+ lines.push(ACCENT(BOX.vertical) + padStr + metaText + metaFill + padStr + ACCENT(BOX.vertical));
93
+ }
94
+
95
+ // Streaming
96
+ if (opts.streaming) {
97
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
98
+ const frameIdx = Date.now() % (frames.length * 80);
99
+ const frame = frames[Math.floor(frameIdx / 80)];
100
+ const spinnerText = `${ACCENT(frame)} ${chalk.dim("thinking...")}`;
101
+ const spinFill = " ".repeat(Math.max(0, innerWidth - visibleWidth(spinnerText)));
102
+ lines.push(ACCENT(BOX.vertical) + padStr + spinnerText + spinFill + padStr + ACCENT(BOX.vertical));
103
+ }
104
+
105
+ // Bottom bar
106
+ lines.push(ACCENT(BOX.bottomLeft + BOX.horizontal.repeat(width - 2) + BOX.bottomRight));
107
+
108
+ return lines.join("\n");
139
109
  }
140
110
 
141
- /**
142
- * Render a compact inline message (no box, just colored prefix).
143
- */
111
+ /** Compact inline message — no box. */
144
112
  export function renderInline(role: MessageRole, text: string): string {
145
- const colors = ROLE_COLORS[role];
146
- const prefix = colors.label(`[${role}]`);
147
- return `${prefix} ${text}`;
113
+ const label = chalk.bold(`[${ROLE_LABEL[role]}]`);
114
+ return `${label} ${text}`;
148
115
  }
149
116
 
150
- /**
151
- * Render a separator/divider line with gradient accent.
152
- */
117
+ /** Separator line, optional label. */
153
118
  export function renderDivider(label?: string): string {
154
- const cols = process.stdout.columns ?? 80;
155
- const grad = gradient(BRAND_GRADIENT);
156
- if (!label) {
157
- return grad("─".repeat(cols - 2));
158
- }
159
- const labelText = ` ${label} `;
160
- const fill = BOX.horizontal.repeat(Math.max(0, cols - 4 - visibleWidth(labelText)));
161
- return grad(`${fill.slice(0, Math.floor(fill.length / 2))}`) +
162
- chalk.dim(labelText) +
163
- grad(`${fill.slice(Math.floor(fill.length / 2))}`);
119
+ const cols = process.stdout.columns ?? 80;
120
+ if (!label) {
121
+ return chalk.dim("─".repeat(cols - 2));
122
+ }
123
+ const labelText = ` ${label} `;
124
+ const fill = "─".repeat(Math.max(0, cols - 2 - visibleWidth(labelText)));
125
+ return chalk.dim(fill.slice(0, Math.floor(fill.length / 2))) + chalk.dim(labelText) + chalk.dim(fill.slice(Math.floor(fill.length / 2)));
164
126
  }
165
127
 
166
- /**
167
- * Render a tool-call status block (compact, with gradient border).
168
- */
128
+ /** Tool-call status line. */
169
129
  export function renderToolBlock(
170
- toolName: string,
171
- status: "running" | "done" | "error",
172
- detail?: string,
130
+ toolName: string,
131
+ status: "running" | "done" | "error",
132
+ detail?: string,
173
133
  ): string {
174
- const cols = process.stdout.columns ?? 80;
175
- const width = Math.min(cols - 2, 72);
176
- const gradColors = status === "error"
177
- ? ["#ff4444", "#cc0000"]
178
- : status === "running"
179
- ? ["#ffd700", "#e6a700"]
180
- : ["#00d2ff", "#7b2ff7"];
181
- const grad = gradient(gradColors as [string, string]);
182
-
183
- const icon = status === "running" ? chalk.yellow("⚙") : status === "done" ? chalk.green("✓") : chalk.red("✗");
184
- const label = ` ${icon} tool:${toolName} `;
185
- const detailText = detail ? chalk.dim(` ${detail}`) : "";
186
- const fill = BOX.horizontal.repeat(
187
- Math.max(0, width - 2 - visibleWidth(` ⚙ tool:${toolName} `) - visibleWidth(detail ?? ""))
188
- );
189
-
190
- return grad(BOX.topLeft + BOX.horizontal) + label + detailText + grad(fill + BOX.topRight);
134
+ const icon = status === "running" ? chalk.cyan("⚙") : status === "done" ? chalk.green("✓") : chalk.red("✗");
135
+ const label = ` ${icon} tool:${toolName} `;
136
+ const detailText = detail ? chalk.dim(` ${detail}`) : "";
137
+ return label + detailText;
191
138
  }
192
139
 
193
- /**
194
- * Build a gradient header line for the top of the session.
195
- */
140
+ /** Session header. */
196
141
  export function renderSessionHeader(model: string, version: string): string {
197
- const cols = process.stdout.columns ?? 80;
198
- const left = gradient(["#00d2ff", "#7b2ff7"])(" SnsCoder");
199
- const ver = chalk.dim(` v${version}`);
200
- const modelStr = chalk.cyan(` ${model}`);
201
- const sep = chalk.dim(" ");
202
- const line = `${left}${ver}${sep}${modelStr}`;
203
- const fill = chalk.dim("─".repeat(Math.max(0, cols - visibleWidth(line) - 2)));
204
- return `\n${line}\n${fill}\n`;
142
+ const cols = process.stdout.columns ?? 80;
143
+ const left = ACCENT.bold(" snsagent");
144
+ const ver = chalk.dim(` v${version}`);
145
+ const modelStr = chalk.dim(" · ") + chalk.cyan(model);
146
+ const sep = chalk.dim("─".repeat(Math.max(0, cols - visibleWidth(left + ver + modelStr) - 2)));
147
+ return `\n${left}${ver}${modelStr}\n${sep}\n`;
205
148
  }
@@ -3,7 +3,6 @@
3
3
  */
4
4
  import { Markdown, visibleWidth } from "@oh-my-pi/pi-tui";
5
5
  import chalk from "chalk";
6
- import gradient from "gradient-string";
7
6
  import { getMarkdownTheme, highlightCode, type Theme } from "../modes/theme/theme";
8
7
  import {
9
8
  formatDuration,
@@ -241,7 +240,6 @@ export function renderCollapsibleOutput(
241
240
  theme: Theme,
242
241
  maxLines: number = 8,
243
242
  ): string[] {
244
- const grad = theme.fg;
245
243
  const lines: string[] = [];
246
244
  const inner = Math.max(20, width - 4);
247
245
 
@@ -249,20 +247,22 @@ export function renderCollapsibleOutput(
249
247
  // Collapsed: just header + hint
250
248
  const lineCount = content.split("\n").length;
251
249
  const hint = chalk.dim(` (${lineCount} lines, click to expand)`);
252
- lines.push(` ${grad("accent", "▸")} ${grad("toolTitle", label)}${hint}`);
250
+ lines.push(` ${chalk.cyan("▸")} ${chalk.bold(label)}${hint}`);
253
251
  return lines;
254
252
  }
255
253
 
256
254
  // Expanded: bordered block
257
- const gradColors = ["#00d2ff", "#7b2ff7"] as [string, string];
258
- const g = gradient(gradColors);
255
+ const accent = chalk.cyan;
256
+
257
+ // Expanded: bordered block
258
+ const accentFn = (s: string) => chalk.cyan(s);
259
259
 
260
260
  // Header
261
261
  const headerText = ` ${label} `;
262
262
  const headerFill = COLLAPSE_BORDER.horizontal.repeat(
263
263
  Math.max(0, inner - headerText.length - 2),
264
264
  );
265
- lines.push(g(COLLAPSE_BORDER.topLeft + COLLAPSE_BORDER.horizontal) + grad("toolTitle", headerText) + g(headerFill + COLLAPSE_BORDER.topRight));
265
+ lines.push(accent(COLLAPSE_BORDER.topLeft + COLLAPSE_BORDER.horizontal) + chalk.bold(headerText) + accent(headerFill + COLLAPSE_BORDER.topRight));
266
266
 
267
267
  // Content
268
268
  const rawLines = content.split("\n");
@@ -270,7 +270,7 @@ export function renderCollapsibleOutput(
270
270
  for (const line of visibleLines) {
271
271
  const visLen = visibleWidth(line);
272
272
  const fill = " ".repeat(Math.max(0, inner - visLen));
273
- lines.push(g(COLLAPSE_BORDER.vertical) + " " + line + fill + " " + g(COLLAPSE_BORDER.vertical));
273
+ lines.push(accent(COLLAPSE_BORDER.vertical) + " " + line + fill + " " + accent(COLLAPSE_BORDER.vertical));
274
274
  }
275
275
 
276
276
  // Hidden lines hint
@@ -278,11 +278,11 @@ export function renderCollapsibleOutput(
278
278
  if (remaining > 0) {
279
279
  const moreText = ` ... +${remaining} more lines`;
280
280
  const moreFill = " ".repeat(Math.max(0, inner - moreText.length));
281
- lines.push(g(COLLAPSE_BORDER.vertical) + chalk.dim(moreText) + moreFill + g(COLLAPSE_BORDER.vertical));
281
+ lines.push(accent(COLLAPSE_BORDER.vertical) + chalk.dim(moreText) + moreFill + accent(COLLAPSE_BORDER.vertical));
282
282
  }
283
283
 
284
284
  // Footer
285
- lines.push(g(COLLAPSE_BORDER.bottomLeft + COLLAPSE_BORDER.horizontal.repeat(inner) + COLLAPSE_BORDER.bottomRight));
285
+ lines.push(accent(COLLAPSE_BORDER.bottomLeft + COLLAPSE_BORDER.horizontal.repeat(inner) + COLLAPSE_BORDER.bottomRight));
286
286
 
287
287
  return lines;
288
288
  }
@@ -1,189 +1,76 @@
1
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.
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
- /** 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;
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
- /** 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;
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
- { 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" },
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
- 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
- }
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
+ }
@@ -149,7 +149,7 @@ export function fileHyperlink(filePath: string, displayText: string, opts?: { li
149
149
  * during the call/streaming phase before a result lands).
150
150
  *
151
151
  * Async-resolved schemes (`artifact://`, `agent://`, `skill://`, `rule://`,
152
- * `omp://`) are not handled here — those rely on `details.resolvedPath` set
152
+ * `snsagent://`) are not handled here — those rely on `details.resolvedPath` set
153
153
  * by the read tool's router resolution.
154
154
  */
155
155
  export function tryResolveInternalUrlSync(input: string): string | undefined {