@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.
- package/package.json +6 -4
- package/scripts/smoke-tui.ts +110 -0
- package/src/cli/agents-cli.ts +1 -1
- package/src/cli/commands/init-xdg.ts +1 -1
- package/src/cli/profile-alias.ts +6 -6
- package/src/cli/ssh-cli.ts +7 -7
- package/src/commands/read.ts +1 -1
- package/src/config/settings-schema.ts +1 -1
- package/src/config/settings.ts +1 -1
- package/src/dap/session.ts +2 -2
- package/src/hindsight/bank.ts +1 -1
- package/src/hindsight/config.ts +1 -1
- package/src/internal-urls/docs-index.ts +1 -1
- package/src/internal-urls/index.ts +1 -1
- package/src/internal-urls/local-protocol.ts +2 -2
- package/src/internal-urls/router.ts +3 -3
- package/src/internal-urls/{omp-protocol.ts → snsagent-protocol.ts} +12 -12
- package/src/internal-urls/types.ts +1 -1
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/welcome.ts +9 -7
- package/src/modes/internal-url-autocomplete.ts +1 -1
- package/src/modes/setup-wizard/scenes/splash.ts +1 -1
- package/src/task/omp-command.ts +1 -1
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/read.ts +2 -2
- package/src/tools/report-tool-issue.ts +1 -1
- package/src/tools/search.ts +4 -4
- 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/hyperlink.ts +1 -1
- package/src/tui/index.ts +1 -1
- package/src/tui/splash.ts +67 -111
- package/src/ui/banner.ts +1 -1
- 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
- package/src/web/scrapers/crates-io.ts +1 -1
- package/src/web/scrapers/docs-rs.ts +1 -1
- package/src/web/scrapers/github.ts +1 -1
- package/src/web/search/providers/codex.ts +2 -2
package/src/tui/chat-blocks.ts
CHANGED
|
@@ -1,205 +1,148 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Chat message block renderers — bordered
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
teeRight: "├",
|
|
20
|
-
teeLeft: "┤",
|
|
10
|
+
topLeft: "╭",
|
|
11
|
+
topRight: "╮",
|
|
12
|
+
bottomLeft: "╰",
|
|
13
|
+
bottomRight: "╯",
|
|
14
|
+
horizontal: "─",
|
|
15
|
+
vertical: "│",
|
|
21
16
|
} as const;
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
// ── 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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
130
|
+
toolName: string,
|
|
131
|
+
status: "running" | "done" | "error",
|
|
132
|
+
detail?: string,
|
|
173
133
|
): string {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
}
|
package/src/tui/code-cell.ts
CHANGED
|
@@ -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(` ${
|
|
250
|
+
lines.push(` ${chalk.cyan("▸")} ${chalk.bold(label)}${hint}`);
|
|
253
251
|
return lines;
|
|
254
252
|
}
|
|
255
253
|
|
|
256
254
|
// Expanded: bordered block
|
|
257
|
-
const
|
|
258
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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/hyperlink.ts
CHANGED
|
@@ -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
|
-
* `
|
|
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 {
|