min-agent 0.5.0 → 0.6.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/README.md +32 -18
- package/dist/cli.js +538 -12
- package/docs/API.md +25 -8
- package/package.json +6 -6
- package/skills/self-config/SKILL.md +7 -3
- package/skills/self-config/reference.md +8 -4
- package/dist/agent.js +0 -1342
- package/dist/assistant-stream.js +0 -131
- package/dist/cli/commands/chat.js +0 -13
- package/dist/cli/commands/exec.js +0 -35
- package/dist/cli/commands/history.js +0 -58
- package/dist/cli/commands/index.js +0 -241
- package/dist/cli/commands/init.js +0 -18
- package/dist/cli/commands/mcp.js +0 -173
- package/dist/cli/commands/memory.js +0 -87
- package/dist/cli/commands/models.js +0 -21
- package/dist/cli/commands/permission.js +0 -12
- package/dist/cli/commands/rules.js +0 -33
- package/dist/cli/commands/sandbox.js +0 -13
- package/dist/cli/commands/serve.js +0 -9
- package/dist/cli/commands/setup.js +0 -4
- package/dist/cli/commands/shared.js +0 -16
- package/dist/cli/commands/skills.js +0 -119
- package/dist/cli/commands/think.js +0 -12
- package/dist/cli/commands/update.js +0 -7
- package/dist/cli/commands/write-config.js +0 -52
- package/dist/cli/errors.js +0 -36
- package/dist/cli/exec-prompt.js +0 -26
- package/dist/cli/option-helpers.js +0 -65
- package/dist/cli/program.js +0 -217
- package/dist/clipboard.js +0 -142
- package/dist/code-mode.js +0 -184
- package/dist/compaction.js +0 -644
- package/dist/config.js +0 -470
- package/dist/confirm.js +0 -172
- package/dist/context-window.js +0 -246
- package/dist/doom-loop.js +0 -278
- package/dist/fetch-timeout.js +0 -152
- package/dist/http-approvals.js +0 -60
- package/dist/http.js +0 -119
- package/dist/instructions.js +0 -196
- package/dist/logger.js +0 -95
- package/dist/markdown.js +0 -259
- package/dist/mcp.js +0 -1036
- package/dist/memory-cli.js +0 -33
- package/dist/memory.js +0 -295
- package/dist/model-catalog.js +0 -285
- package/dist/output.js +0 -76
- package/dist/paste-handler.js +0 -41
- package/dist/permission-cli.js +0 -40
- package/dist/plugins.js +0 -131
- package/dist/pricing.js +0 -119
- package/dist/provider.js +0 -63
- package/dist/question-format.js +0 -60
- package/dist/reasoning-stream.js +0 -158
- package/dist/sandbox-cli.js +0 -79
- package/dist/sandbox.js +0 -403
- package/dist/save-throttle.js +0 -45
- package/dist/scope.js +0 -23
- package/dist/serve/common.js +0 -425
- package/dist/serve/routes-chat.js +0 -367
- package/dist/serve/routes-mcp.js +0 -212
- package/dist/serve/routes-memory.js +0 -95
- package/dist/serve/routes-meta.js +0 -233
- package/dist/serve/routes-sessions.js +0 -61
- package/dist/serve/routes-skills.js +0 -70
- package/dist/serve.js +0 -138
- package/dist/sessions.js +0 -265
- package/dist/skills.js +0 -599
- package/dist/structured-output.js +0 -29
- package/dist/synthetic.js +0 -7
- package/dist/think-cli.js +0 -36
- package/dist/thinking-wire.js +0 -228
- package/dist/thinking.js +0 -142
- package/dist/title-gen.js +0 -55
- package/dist/token-display.js +0 -39
- package/dist/tool-display.js +0 -178
- package/dist/tool-output.js +0 -126
- package/dist/tools/apply_patch.js +0 -265
- package/dist/tools/atomic-file.js +0 -35
- package/dist/tools/backend.js +0 -61
- package/dist/tools/bash.js +0 -231
- package/dist/tools/code_search.js +0 -98
- package/dist/tools/edit.js +0 -68
- package/dist/tools/explore.js +0 -232
- package/dist/tools/glob.js +0 -40
- package/dist/tools/grep.js +0 -174
- package/dist/tools/index.js +0 -34
- package/dist/tools/question.js +0 -54
- package/dist/tools/read.js +0 -109
- package/dist/tools/search-searxng.js +0 -223
- package/dist/tools/search-serper.js +0 -189
- package/dist/tools/task.js +0 -165
- package/dist/tools/todo.js +0 -213
- package/dist/tools/web_fetch.js +0 -195
- package/dist/tools/web_search.js +0 -228
- package/dist/tools/write.js +0 -55
- package/dist/tui/App.js +0 -126
- package/dist/tui/ConfirmBar.js +0 -66
- package/dist/tui/InputBar.js +0 -616
- package/dist/tui/MessageList.js +0 -718
- package/dist/tui/ModelPicker.js +0 -113
- package/dist/tui/QuestionBar.js +0 -136
- package/dist/tui/SessionPicker.js +0 -79
- package/dist/tui/Spinner.js +0 -6
- package/dist/tui/StatusBar.js +0 -22
- package/dist/tui/ThinkPicker.js +0 -77
- package/dist/tui/agent-runner.js +0 -223
- package/dist/tui/bracketed-paste.js +0 -37
- package/dist/tui/caret-pos.js +0 -179
- package/dist/tui/caret.js +0 -69
- package/dist/tui/click-count.js +0 -13
- package/dist/tui/diff-view.js +0 -61
- package/dist/tui/drag-state.js +0 -49
- package/dist/tui/hydrate.js +0 -129
- package/dist/tui/index.js +0 -258
- package/dist/tui/input-history.js +0 -125
- package/dist/tui/layout.js +0 -105
- package/dist/tui/mouse.js +0 -46
- package/dist/tui/overlay-input.js +0 -12
- package/dist/tui/paste-draft.js +0 -173
- package/dist/tui/prompt-queue.js +0 -24
- package/dist/tui/selection.js +0 -232
- package/dist/tui/session-switch.js +0 -28
- package/dist/tui/slash-commands.js +0 -123
- package/dist/tui/slash-handler.js +0 -589
- package/dist/tui/text-width.js +0 -113
- package/dist/tui/theme.js +0 -12
- package/dist/tui/token-info.js +0 -7
- package/dist/tui/tool-children.js +0 -19
- package/dist/tui/types.js +0 -1
- package/dist/tui/undo-stack.js +0 -14
- package/dist/tui/use-sgr-mouse.js +0 -29
- package/dist/tui-chat.js +0 -456
- package/dist/updater.js +0 -116
- package/dist/xml-search.js +0 -194
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
- package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
- package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
- package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
- package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
- package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
- package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +0 -140
package/dist/tui/paste-draft.js
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { displayWidth, clustersOf } from "./text-width.js";
|
|
2
|
-
export const PASTE_CHIP_BASE = 0xe000;
|
|
3
|
-
export const SNIPPET_MAX_CHARS = 1_000_000;
|
|
4
|
-
export const IMAGE_MAX_BYTES = 5 * 1024 * 1024;
|
|
5
|
-
export const NOTICE_IMAGE_TOO_LARGE = "图片超过 5MB,未附加";
|
|
6
|
-
export const NOTICE_SNIPPET_TOO_LARGE = "粘贴内容过大,未附加";
|
|
7
|
-
const PUA_START = 0xe000;
|
|
8
|
-
const PUA_END = 0xf8ff;
|
|
9
|
-
const UNMAPPED = "�";
|
|
10
|
-
export function createPasteDraft() {
|
|
11
|
-
return {
|
|
12
|
-
nextCodePoint: PASTE_CHIP_BASE,
|
|
13
|
-
nextImageId: 1,
|
|
14
|
-
nextSnippetId: 1,
|
|
15
|
-
attachments: new Map(),
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
export function resetPasteDraft() {
|
|
19
|
-
return createPasteDraft();
|
|
20
|
-
}
|
|
21
|
-
export function imageLabel(id) {
|
|
22
|
-
return `[图片 #${id}]`;
|
|
23
|
-
}
|
|
24
|
-
export function snippetLabel(id, lineCount) {
|
|
25
|
-
return `[粘贴 #${id} · ${lineCount} 行]`;
|
|
26
|
-
}
|
|
27
|
-
export function normalizePastedText(raw) {
|
|
28
|
-
const normalized = raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
29
|
-
return normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
|
|
30
|
-
}
|
|
31
|
-
export function applyPastedText(draft, raw) {
|
|
32
|
-
const text = normalizePastedText(raw);
|
|
33
|
-
if (text.length > SNIPPET_MAX_CHARS) {
|
|
34
|
-
return { kind: "reject", notice: NOTICE_SNIPPET_TOO_LARGE, draft };
|
|
35
|
-
}
|
|
36
|
-
if (!text.includes("\n")) {
|
|
37
|
-
return { kind: "text", text, draft };
|
|
38
|
-
}
|
|
39
|
-
const { char, draft: next } = allocChip(draft, {
|
|
40
|
-
kind: "snippet",
|
|
41
|
-
id: draft.nextSnippetId,
|
|
42
|
-
text,
|
|
43
|
-
lineCount: text.split("\n").length,
|
|
44
|
-
});
|
|
45
|
-
return { kind: "snippet", char, draft: next };
|
|
46
|
-
}
|
|
47
|
-
export function applyClipboardImage(draft, image) {
|
|
48
|
-
if (image == null)
|
|
49
|
-
return { kind: "none", draft };
|
|
50
|
-
if (image.data.length > IMAGE_MAX_BYTES) {
|
|
51
|
-
return { kind: "reject", notice: NOTICE_IMAGE_TOO_LARGE, draft };
|
|
52
|
-
}
|
|
53
|
-
const { char, draft: next } = allocChip(draft, {
|
|
54
|
-
kind: "image",
|
|
55
|
-
id: draft.nextImageId,
|
|
56
|
-
data: image.data,
|
|
57
|
-
mimeType: image.mimeType,
|
|
58
|
-
});
|
|
59
|
-
return { kind: "image", char, draft: next };
|
|
60
|
-
}
|
|
61
|
-
export function pruneAttachments(draft, value) {
|
|
62
|
-
const present = new Set(Array.from(value));
|
|
63
|
-
const attachments = new Map([...draft.attachments].filter(([char]) => present.has(char)));
|
|
64
|
-
if (attachments.size === draft.attachments.size)
|
|
65
|
-
return draft;
|
|
66
|
-
return { ...draft, attachments };
|
|
67
|
-
}
|
|
68
|
-
export function expandDisplay(value, draft) {
|
|
69
|
-
return Array.from(value)
|
|
70
|
-
.map((char) => {
|
|
71
|
-
const att = draft.attachments.get(char);
|
|
72
|
-
if (att)
|
|
73
|
-
return attachmentLabel(att);
|
|
74
|
-
return isPrivateUse(char) ? UNMAPPED : char;
|
|
75
|
-
})
|
|
76
|
-
.join("");
|
|
77
|
-
}
|
|
78
|
-
export function expandContent(value, draft) {
|
|
79
|
-
const parts = [];
|
|
80
|
-
let textBuf = "";
|
|
81
|
-
const flushText = () => {
|
|
82
|
-
if (!textBuf)
|
|
83
|
-
return;
|
|
84
|
-
parts.push({ type: "text", text: textBuf });
|
|
85
|
-
textBuf = "";
|
|
86
|
-
};
|
|
87
|
-
for (const char of Array.from(value)) {
|
|
88
|
-
const att = draft.attachments.get(char);
|
|
89
|
-
if (att?.kind === "image") {
|
|
90
|
-
flushText();
|
|
91
|
-
parts.push({ type: "image", image: att.data, mimeType: att.mimeType });
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
if (att?.kind === "snippet") {
|
|
95
|
-
flushText();
|
|
96
|
-
parts.push({ type: "text", text: snippetBlock(att.id, att.text, parts) });
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
textBuf += isPrivateUse(char) ? UNMAPPED : char;
|
|
100
|
-
}
|
|
101
|
-
flushText();
|
|
102
|
-
if (!parts.some((part) => part.type === "image")) {
|
|
103
|
-
return parts.map((part) => (part.type === "text" ? part.text : "")).join("");
|
|
104
|
-
}
|
|
105
|
-
return parts;
|
|
106
|
-
}
|
|
107
|
-
export function buildSubmittedPrompt(value, draft) {
|
|
108
|
-
return { display: expandDisplay(value, draft), content: expandContent(value, draft) };
|
|
109
|
-
}
|
|
110
|
-
export function chipClusterWidth(cluster, draft, maxWidth) {
|
|
111
|
-
const att = draft.attachments.get(cluster);
|
|
112
|
-
if (!att)
|
|
113
|
-
return undefined;
|
|
114
|
-
return Math.min(displayWidth(attachmentLabel(att)), maxWidth);
|
|
115
|
-
}
|
|
116
|
-
export function paintedChip(cluster, draft, maxWidth) {
|
|
117
|
-
const att = draft.attachments.get(cluster);
|
|
118
|
-
if (!att)
|
|
119
|
-
return undefined;
|
|
120
|
-
return truncateToWidth(attachmentLabel(att), maxWidth);
|
|
121
|
-
}
|
|
122
|
-
export function paintChar(char, draft, maxWidth) {
|
|
123
|
-
return paintedChip(char, draft, maxWidth) ?? (isPrivateUse(char) ? UNMAPPED : char);
|
|
124
|
-
}
|
|
125
|
-
function attachmentLabel(att) {
|
|
126
|
-
return att.kind === "image" ? imageLabel(att.id) : snippetLabel(att.id, att.lineCount);
|
|
127
|
-
}
|
|
128
|
-
function snippetBlock(id, text, previous) {
|
|
129
|
-
const body = `--- 粘贴 #${id} ---\n${text}\n--- 粘贴 #${id} ---`;
|
|
130
|
-
if (previous.length === 0)
|
|
131
|
-
return body;
|
|
132
|
-
const last = previous[previous.length - 1];
|
|
133
|
-
if (last?.type === "text" && last.text.endsWith("\n"))
|
|
134
|
-
return body;
|
|
135
|
-
return `\n${body}`;
|
|
136
|
-
}
|
|
137
|
-
function allocChip(draft, attachment) {
|
|
138
|
-
const char = String.fromCodePoint(draft.nextCodePoint);
|
|
139
|
-
const attachments = new Map(draft.attachments);
|
|
140
|
-
attachments.set(char, attachment);
|
|
141
|
-
return {
|
|
142
|
-
char,
|
|
143
|
-
draft: {
|
|
144
|
-
nextCodePoint: draft.nextCodePoint + 1,
|
|
145
|
-
nextImageId: attachment.kind === "image" ? draft.nextImageId + 1 : draft.nextImageId,
|
|
146
|
-
nextSnippetId: attachment.kind === "snippet" ? draft.nextSnippetId + 1 : draft.nextSnippetId,
|
|
147
|
-
attachments,
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
function isPrivateUse(char) {
|
|
152
|
-
const cp = char.codePointAt(0);
|
|
153
|
-
return cp !== undefined && cp >= PUA_START && cp <= PUA_END;
|
|
154
|
-
}
|
|
155
|
-
function truncateToWidth(text, maxWidth) {
|
|
156
|
-
if (maxWidth < 1)
|
|
157
|
-
return "";
|
|
158
|
-
if (displayWidth(text) <= maxWidth)
|
|
159
|
-
return text;
|
|
160
|
-
const ellipsis = "…";
|
|
161
|
-
const ellipsisW = displayWidth(ellipsis);
|
|
162
|
-
let out = "";
|
|
163
|
-
let width = 0;
|
|
164
|
-
for (const { text: g, width: w } of clustersOf(text)) {
|
|
165
|
-
if (width + w + ellipsisW > maxWidth)
|
|
166
|
-
break;
|
|
167
|
-
out += g;
|
|
168
|
-
width += w;
|
|
169
|
-
}
|
|
170
|
-
out += ellipsis;
|
|
171
|
-
width += ellipsisW;
|
|
172
|
-
return width < maxWidth ? `${out}${" ".repeat(maxWidth - width)}` : out;
|
|
173
|
-
}
|
package/dist/tui/prompt-queue.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export function createPromptQueue() {
|
|
2
|
-
let tail = Promise.resolve();
|
|
3
|
-
let generation = 0;
|
|
4
|
-
let size = 0;
|
|
5
|
-
const settle = () => undefined;
|
|
6
|
-
return {
|
|
7
|
-
run(job, onCancelled) {
|
|
8
|
-
const gen = generation;
|
|
9
|
-
size++;
|
|
10
|
-
const start = () => (gen !== generation ? Promise.resolve(onCancelled()) : job());
|
|
11
|
-
const result = tail.then(start, start);
|
|
12
|
-
tail = result.then(settle, settle);
|
|
13
|
-
return result.finally(() => {
|
|
14
|
-
size--;
|
|
15
|
-
});
|
|
16
|
-
},
|
|
17
|
-
cancelAll() {
|
|
18
|
-
generation++;
|
|
19
|
-
},
|
|
20
|
-
get size() {
|
|
21
|
-
return size;
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
}
|
package/dist/tui/selection.js
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { clustersOf, displayWidth } from "./text-width.js";
|
|
2
|
-
const RESET = "\x1b[0m";
|
|
3
|
-
const INVERSE_ON = "\x1b[7m";
|
|
4
|
-
const INVERSE_OFF = "\x1b[27m";
|
|
5
|
-
/** Remove SGR color/style codes from a rendered line. */
|
|
6
|
-
export function stripAnsi(line) {
|
|
7
|
-
return line.replace(/\x1b\[[0-9;]*m/g, "");
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Wrap the cells in [start, end) with inverse video, preserving any styles
|
|
11
|
-
* already active. A style reset inside the selection would cancel the
|
|
12
|
-
* highlight, so a fresh INVERSE_ON is re-emitted after it; the closing
|
|
13
|
-
* INVERSE_OFF restores the caller's original attribute state.
|
|
14
|
-
*/
|
|
15
|
-
export function applySelection(line, start, end) {
|
|
16
|
-
if (end <= start)
|
|
17
|
-
return line;
|
|
18
|
-
let out = "";
|
|
19
|
-
let col = 0;
|
|
20
|
-
let inSel = false;
|
|
21
|
-
for (const token of line.split(/(\x1b\[[0-9;]*m)/g)) {
|
|
22
|
-
if (token.startsWith("\x1b[")) {
|
|
23
|
-
if (inSel && token === RESET) {
|
|
24
|
-
if (col < end)
|
|
25
|
-
out += token + INVERSE_ON;
|
|
26
|
-
else {
|
|
27
|
-
out += INVERSE_OFF + token;
|
|
28
|
-
inSel = false;
|
|
29
|
-
}
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
out += token;
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
for (const { text: g, width: w } of clustersOf(token)) {
|
|
36
|
-
const cellStart = col;
|
|
37
|
-
const cellEnd = col + w;
|
|
38
|
-
const selected = cellEnd > start && cellStart < end;
|
|
39
|
-
if (selected && !inSel) {
|
|
40
|
-
out += INVERSE_ON;
|
|
41
|
-
inSel = true;
|
|
42
|
-
}
|
|
43
|
-
else if (!selected && inSel) {
|
|
44
|
-
out += INVERSE_OFF;
|
|
45
|
-
inSel = false;
|
|
46
|
-
}
|
|
47
|
-
out += g;
|
|
48
|
-
col = cellEnd;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
if (inSel)
|
|
52
|
-
out += INVERSE_OFF;
|
|
53
|
-
return out;
|
|
54
|
-
}
|
|
55
|
-
/** The plain text of the cells in [start, end) of an ANSI line. */
|
|
56
|
-
export function extractText(line, start, end) {
|
|
57
|
-
if (end <= start)
|
|
58
|
-
return "";
|
|
59
|
-
let out = "";
|
|
60
|
-
let col = 0;
|
|
61
|
-
for (const { text: g, width: w } of clustersOf(cachedStrip(line))) {
|
|
62
|
-
const cellEnd = col + w;
|
|
63
|
-
if (cellEnd > start && col < end)
|
|
64
|
-
out += g;
|
|
65
|
-
col = cellEnd;
|
|
66
|
-
if (col >= end)
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
return out;
|
|
70
|
-
}
|
|
71
|
-
const strippedCache = new Map();
|
|
72
|
-
function cachedStrip(row) {
|
|
73
|
-
const hit = strippedCache.get(row);
|
|
74
|
-
if (hit !== undefined)
|
|
75
|
-
return hit;
|
|
76
|
-
const s = stripAnsi(row);
|
|
77
|
-
if (strippedCache.size > 512)
|
|
78
|
-
strippedCache.clear();
|
|
79
|
-
strippedCache.set(row, s);
|
|
80
|
-
return s;
|
|
81
|
-
}
|
|
82
|
-
export function textFromSelection(rows, kinds, ranges) {
|
|
83
|
-
const parts = [];
|
|
84
|
-
for (let i = 0; i < ranges.length; i++) {
|
|
85
|
-
const range = ranges[i];
|
|
86
|
-
const row = rows[i];
|
|
87
|
-
if (!range || !row)
|
|
88
|
-
continue;
|
|
89
|
-
parts.push(stripCopyDecorations(extractText(row, range.start, range.end), kinds[i] ?? "plain"));
|
|
90
|
-
}
|
|
91
|
-
while (parts.length > 0 && parts[parts.length - 1] === "")
|
|
92
|
-
parts.pop();
|
|
93
|
-
return parts.join("\n");
|
|
94
|
-
}
|
|
95
|
-
export function stripCopyDecorations(line, kind) {
|
|
96
|
-
if (kind === "user-head" && line.startsWith("> "))
|
|
97
|
-
return line.slice(2);
|
|
98
|
-
if (kind === "tool-head")
|
|
99
|
-
return line.replace(/^⚡ .*?[▸▾] /, "");
|
|
100
|
-
if (kind === "thinking-head")
|
|
101
|
-
return line.replace(/^💭 .*?[▸▾] /, "");
|
|
102
|
-
if ((kind === "tool-body" || kind === "thinking-body") && line.startsWith(" "))
|
|
103
|
-
return line.slice(2);
|
|
104
|
-
return line;
|
|
105
|
-
}
|
|
106
|
-
/** Visible prefix rendered in its own Ink Text (user `> `, tool `⚡ name ▸ `, thinking `💭 思考 ▸ `). */
|
|
107
|
-
export function visualPrefix(line, role) {
|
|
108
|
-
if (role === "user" && line.startsWith("> "))
|
|
109
|
-
return "> ";
|
|
110
|
-
if (role === "tool") {
|
|
111
|
-
const m = line.match(/^⚡ .*?[▸▾] /);
|
|
112
|
-
return m ? m[0] : "";
|
|
113
|
-
}
|
|
114
|
-
if (role === "thinking") {
|
|
115
|
-
const m = line.match(/^💭 .*?[▸▾] /);
|
|
116
|
-
return m ? m[0] : "";
|
|
117
|
-
}
|
|
118
|
-
return "";
|
|
119
|
-
}
|
|
120
|
-
/** Split a display-column range across a prefix of `at` cells. */
|
|
121
|
-
export function splitRangeAt(range, at) {
|
|
122
|
-
const head = range.start < at && range.end > 0 ? { start: range.start, end: Math.min(at, range.end) } : null;
|
|
123
|
-
const tail = range.end > at ? { start: Math.max(0, range.start - at), end: range.end - at } : null;
|
|
124
|
-
return {
|
|
125
|
-
head: head && head.end > head.start ? head : null,
|
|
126
|
-
tail: tail && tail.end > tail.start ? tail : null,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
export function wordRangeAt(line, col) {
|
|
130
|
-
const cells = [];
|
|
131
|
-
let x = 0;
|
|
132
|
-
for (const { text: g, width: w } of clustersOf(line)) {
|
|
133
|
-
cells.push({ ch: g, start: x, end: x + w });
|
|
134
|
-
x += w;
|
|
135
|
-
}
|
|
136
|
-
const hit = cells.find((c) => col >= c.start && col < c.end);
|
|
137
|
-
if (!hit || /\s/.test(hit.ch))
|
|
138
|
-
return { start: col, end: col };
|
|
139
|
-
let left = cells.indexOf(hit);
|
|
140
|
-
let right = left;
|
|
141
|
-
while (left > 0 && !/\s/.test(cells[left - 1].ch))
|
|
142
|
-
left--;
|
|
143
|
-
while (right < cells.length - 1 && !/\s/.test(cells[right + 1].ch))
|
|
144
|
-
right++;
|
|
145
|
-
return { start: cells[left].start, end: cells[right].end };
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Map a 1-based SGR mouse position to a cell in `rows`.
|
|
149
|
-
* `padLeft` is the Ink paddingLeft of every message row (first text cell is
|
|
150
|
-
* terminal column padLeft+1).
|
|
151
|
-
*/
|
|
152
|
-
export function mouseToCell(rows, termRow, termCol, padLeft = 1) {
|
|
153
|
-
const maxRow = Math.max(0, rows.length - 1);
|
|
154
|
-
return {
|
|
155
|
-
row: Math.min(maxRow, Math.max(0, termRow - 1)),
|
|
156
|
-
col: Math.max(0, termCol - 1 - padLeft),
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Distribute viewport ranges onto each message part's shown-line index.
|
|
161
|
-
* The resulting arrays are sparse: missing indices mean that shown line is
|
|
162
|
-
* not highlighted. Callers must not require `arr.length === shown.length`.
|
|
163
|
-
*/
|
|
164
|
-
export function alignPartSelections(ranges, partMeta) {
|
|
165
|
-
const map = new Map();
|
|
166
|
-
for (let i = 0; i < ranges.length; i++) {
|
|
167
|
-
const meta = partMeta[i];
|
|
168
|
-
const range = ranges[i];
|
|
169
|
-
if (!meta || !range || range.end <= range.start)
|
|
170
|
-
continue;
|
|
171
|
-
let arr = map.get(meta.part);
|
|
172
|
-
if (!arr) {
|
|
173
|
-
arr = [];
|
|
174
|
-
map.set(meta.part, arr);
|
|
175
|
-
}
|
|
176
|
-
arr[meta.shown] = range;
|
|
177
|
-
}
|
|
178
|
-
return map;
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Flow-style selection over the message area. `rows` mirrors the rendered
|
|
182
|
-
* rows (null = blank/chrome row, no text); anchor/current are 0-based
|
|
183
|
-
* (row = index into `rows`, col = display column of the cell under the
|
|
184
|
-
* pointer). Both endpoints are inclusive: dragging onto a character selects
|
|
185
|
-
* it. Dragging down selects the anchor through end-of-line, full middle rows,
|
|
186
|
-
* then start through current; dragging up is symmetric.
|
|
187
|
-
*/
|
|
188
|
-
export function selectionRanges(rows, anchor, current) {
|
|
189
|
-
const result = rows.map(() => null);
|
|
190
|
-
const top = Math.min(anchor.row, current.row);
|
|
191
|
-
const bottom = Math.max(anchor.row, current.row);
|
|
192
|
-
for (let r = top; r <= bottom; r++) {
|
|
193
|
-
const line = rows[r];
|
|
194
|
-
if (r < 0 || r >= rows.length || line === null)
|
|
195
|
-
continue;
|
|
196
|
-
const width = displayWidth(cachedStrip(line));
|
|
197
|
-
let start;
|
|
198
|
-
let end;
|
|
199
|
-
if (anchor.row === current.row) {
|
|
200
|
-
start = Math.min(anchor.col, current.col);
|
|
201
|
-
end = Math.max(anchor.col, current.col) + 1;
|
|
202
|
-
}
|
|
203
|
-
else if (r === anchor.row) {
|
|
204
|
-
if (anchor.row < current.row) {
|
|
205
|
-
start = anchor.col;
|
|
206
|
-
end = width;
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
start = 0;
|
|
210
|
-
end = anchor.col + 1;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
else if (r === current.row) {
|
|
214
|
-
if (anchor.row < current.row) {
|
|
215
|
-
start = 0;
|
|
216
|
-
end = current.col + 1;
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
start = current.col;
|
|
220
|
-
end = width;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
start = 0;
|
|
225
|
-
end = width;
|
|
226
|
-
}
|
|
227
|
-
start = Math.max(0, start);
|
|
228
|
-
if (end > start)
|
|
229
|
-
result[r] = { start, end };
|
|
230
|
-
}
|
|
231
|
-
return result;
|
|
232
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export function prepareSwitch(opts) {
|
|
2
|
-
if (opts.running)
|
|
3
|
-
return { ok: false, reason: "running" };
|
|
4
|
-
if (opts.currentId === opts.targetId)
|
|
5
|
-
return { ok: false, reason: "current" };
|
|
6
|
-
if (!opts.exists)
|
|
7
|
-
return { ok: false, reason: "missing" };
|
|
8
|
-
return { ok: true };
|
|
9
|
-
}
|
|
10
|
-
export function switchFailMessage(reason) {
|
|
11
|
-
if (reason === "running")
|
|
12
|
-
return "请先等待当前回合结束或按 Esc 取消";
|
|
13
|
-
if (reason === "current")
|
|
14
|
-
return "已是当前会话";
|
|
15
|
-
return "会话不存在";
|
|
16
|
-
}
|
|
17
|
-
export function prepareNew(opts) {
|
|
18
|
-
if (opts.running)
|
|
19
|
-
return { ok: false, reason: "running" };
|
|
20
|
-
if (!opts.sessionId && opts.messageCount === 0)
|
|
21
|
-
return { ok: false, reason: "already_new" };
|
|
22
|
-
return { ok: true };
|
|
23
|
-
}
|
|
24
|
-
export function newFailMessage(reason) {
|
|
25
|
-
if (reason === "running")
|
|
26
|
-
return "请先等待当前回合结束或按 Esc 取消";
|
|
27
|
-
return "已是新会话";
|
|
28
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
export const SLASH_COMMANDS = [
|
|
2
|
-
{ name: "new", description: "新建会话(保留当前)" },
|
|
3
|
-
{ name: "compact", description: "压缩上下文" },
|
|
4
|
-
{ name: "model", description: "选择/切换模型" },
|
|
5
|
-
{ name: "models", description: "列出可用模型" },
|
|
6
|
-
{ name: "provider", description: "查看/切换 Provider", argHint: "[name]" },
|
|
7
|
-
{ name: "memory", description: "查看/开关记忆,或保存一条", argHint: "[on|off|t] [--project]" },
|
|
8
|
-
{ name: "tokens", description: "显示上下文占用、累计用量与成本" },
|
|
9
|
-
{ name: "budget", description: "查看/设置预算上限", argHint: "[n] [--project]" },
|
|
10
|
-
{ name: "permission", description: "查看/设置确认模式", argHint: "[ask|accept-edits|allow-all] [--project]" },
|
|
11
|
-
{ name: "think", description: "选择思考强度", aliases: ["thinking"] },
|
|
12
|
-
{ name: "sandbox", description: "查看/设置隔离", argHint: "[off|workspace|strict] [--project]" },
|
|
13
|
-
{ name: "undo", description: "撤销上一轮" },
|
|
14
|
-
{ name: "redo", description: "重发最后一条消息" },
|
|
15
|
-
{ name: "diff", description: "显示工作区未提交变更" },
|
|
16
|
-
{ name: "plan", description: "切换只读计划模式" },
|
|
17
|
-
{ name: "sessions", description: "列出/切换已保存会话", argHint: "[id]", aliases: ["resume"] },
|
|
18
|
-
{ name: "reload", description: "重新加载规则" },
|
|
19
|
-
{ name: "rename", description: "重命名当前会话", argHint: "<title>" },
|
|
20
|
-
{ name: "attach", description: "附加本地图片", argHint: "<path>", aliases: ["image"] },
|
|
21
|
-
{ name: "mcp", description: "显示 MCP 状态" },
|
|
22
|
-
{
|
|
23
|
-
name: "skills",
|
|
24
|
-
description: "显示/启用/禁用 skills",
|
|
25
|
-
argHint: "[list|info <name>|enable|disable <name> [--project]]",
|
|
26
|
-
},
|
|
27
|
-
{ name: "path", description: "显示工作目录", aliases: ["pwd"] },
|
|
28
|
-
{ name: "paste", description: "粘贴剪贴板图片", argHint: "[t]" },
|
|
29
|
-
{ name: "help", description: "显示帮助" },
|
|
30
|
-
{ name: "exit", description: "退出", aliases: ["quit", "q"] },
|
|
31
|
-
];
|
|
32
|
-
export function filterSlashCommands(input, commands = SLASH_COMMANDS) {
|
|
33
|
-
const q = input.slice(1).trim().toLowerCase();
|
|
34
|
-
if (q === "")
|
|
35
|
-
return commands;
|
|
36
|
-
return commands.filter((c) => c.name.startsWith(q) || (c.aliases ?? []).some((a) => a.startsWith(q)));
|
|
37
|
-
}
|
|
38
|
-
/** Keep `selectedIndex` on screen; the highlight stays on the last row once the list scrolls. */
|
|
39
|
-
export function slashMenuWindow(items, selectedIndex, maxVisible) {
|
|
40
|
-
const max = Number.isFinite(maxVisible) ? Math.max(1, Math.floor(maxVisible)) : Math.max(1, items.length);
|
|
41
|
-
const index = items.length === 0 ? 0 : Math.min(Math.max(0, selectedIndex), items.length - 1);
|
|
42
|
-
if (items.length <= max) {
|
|
43
|
-
return { start: 0, shown: [...items], extraAbove: 0, extraBelow: 0 };
|
|
44
|
-
}
|
|
45
|
-
const start = Math.max(0, Math.min(index - max + 1, items.length - max));
|
|
46
|
-
const shown = items.slice(start, start + max);
|
|
47
|
-
return {
|
|
48
|
-
start,
|
|
49
|
-
shown,
|
|
50
|
-
extraAbove: start,
|
|
51
|
-
extraBelow: items.length - (start + shown.length),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
/** True for submitted input that is a known slash command (including aliases). */
|
|
55
|
-
export function isKnownSlashCommandInput(text, commands = SLASH_COMMANDS) {
|
|
56
|
-
if (!text.startsWith("/"))
|
|
57
|
-
return false;
|
|
58
|
-
const name = text.slice(1).trim().split(/\s/, 1)[0]?.toLowerCase() ?? "";
|
|
59
|
-
if (!name)
|
|
60
|
-
return true;
|
|
61
|
-
return commands.some((c) => c.name === name || (c.aliases ?? []).some((a) => a === name));
|
|
62
|
-
}
|
|
63
|
-
const argProviders = new Map();
|
|
64
|
-
/** Registered by the app at startup so this module stays free of app imports. */
|
|
65
|
-
export function setSlashArgProvider(command, provider) {
|
|
66
|
-
argProviders.set(command, provider);
|
|
67
|
-
}
|
|
68
|
-
export function clearSlashArgProviders() {
|
|
69
|
-
argProviders.clear();
|
|
70
|
-
}
|
|
71
|
-
function splitArgs(input) {
|
|
72
|
-
const out = [];
|
|
73
|
-
let cur = "";
|
|
74
|
-
let q = null;
|
|
75
|
-
for (const ch of input) {
|
|
76
|
-
if (q) {
|
|
77
|
-
if (ch === q)
|
|
78
|
-
q = null;
|
|
79
|
-
else
|
|
80
|
-
cur += ch;
|
|
81
|
-
}
|
|
82
|
-
else if (ch === '"' || ch === "'")
|
|
83
|
-
q = ch;
|
|
84
|
-
else if (/\s/.test(ch)) {
|
|
85
|
-
if (cur) {
|
|
86
|
-
out.push(cur);
|
|
87
|
-
cur = "";
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
else
|
|
91
|
-
cur += ch;
|
|
92
|
-
}
|
|
93
|
-
if (cur)
|
|
94
|
-
out.push(cur);
|
|
95
|
-
return out;
|
|
96
|
-
}
|
|
97
|
-
/** Null while the command name itself is still being typed (no space yet). */
|
|
98
|
-
export function parseSlashArgs(value) {
|
|
99
|
-
if (!value.startsWith("/"))
|
|
100
|
-
return null;
|
|
101
|
-
const raw = value.slice(1);
|
|
102
|
-
if (!/\s/.test(raw))
|
|
103
|
-
return null;
|
|
104
|
-
const parts = splitArgs(raw.trim());
|
|
105
|
-
const command = parts[0] ?? "";
|
|
106
|
-
const rest = parts.slice(1);
|
|
107
|
-
const partial = /\s$/.test(raw) ? "" : (rest.pop() ?? "");
|
|
108
|
-
return { command, tokens: rest, partial };
|
|
109
|
-
}
|
|
110
|
-
export function getSlashArgOptions(value) {
|
|
111
|
-
const ctx = parseSlashArgs(value);
|
|
112
|
-
if (!ctx)
|
|
113
|
-
return [];
|
|
114
|
-
const provider = argProviders.get(ctx.command);
|
|
115
|
-
if (!provider)
|
|
116
|
-
return [];
|
|
117
|
-
const prefix = ctx.partial.toLowerCase();
|
|
118
|
-
return provider(ctx.tokens, ctx.partial).filter((o) => o.value.toLowerCase().startsWith(prefix));
|
|
119
|
-
}
|
|
120
|
-
/** Replace the token being typed with `option`, leaving a trailing space. */
|
|
121
|
-
export function applySlashArgCompletion(value, option) {
|
|
122
|
-
return /\s$/.test(value) ? `${value}${option} ` : `${value.replace(/\S+$/, option)} `;
|
|
123
|
-
}
|