min-agent 0.2.1 → 0.4.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 +242 -31
- package/dist/agent.js +1233 -485
- package/dist/assistant-stream.js +11 -7
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/exec.js +32 -0
- package/dist/cli/commands/history.js +58 -0
- package/dist/cli/commands/index.js +224 -0
- package/dist/cli/commands/init.js +18 -0
- package/dist/cli/commands/mcp.js +173 -0
- package/dist/cli/commands/memory.js +69 -0
- package/dist/cli/commands/models.js +21 -0
- package/dist/cli/commands/permission.js +12 -0
- package/dist/cli/commands/rules.js +33 -0
- package/dist/cli/commands/sandbox.js +13 -0
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/setup.js +4 -0
- package/dist/cli/commands/shared.js +16 -0
- package/dist/cli/commands/skills.js +119 -0
- package/dist/cli/commands/update.js +7 -0
- package/dist/cli/commands/write-config.js +30 -0
- package/dist/cli/errors.js +36 -0
- package/dist/cli/exec-prompt.js +26 -0
- package/dist/cli/option-helpers.js +53 -0
- package/dist/cli/program.js +180 -0
- package/dist/cli.js +7 -632
- package/dist/clipboard.js +59 -23
- package/dist/code-mode.js +35 -17
- package/dist/compaction.js +457 -169
- package/dist/config.js +298 -38
- package/dist/confirm.js +105 -9
- package/dist/context-window.js +156 -75
- package/dist/doom-loop.js +268 -26
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/http.js +119 -0
- package/dist/instructions.js +72 -33
- package/dist/logger.js +95 -0
- package/dist/markdown.js +35 -50
- package/dist/mcp.js +847 -102
- package/dist/memory.js +128 -45
- package/dist/output.js +42 -31
- package/dist/paste-handler.js +3 -3
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +76 -11
- package/dist/pricing.js +119 -0
- package/dist/provider.js +34 -15
- package/dist/question-format.js +60 -0
- package/dist/sandbox-cli.js +82 -0
- package/dist/sandbox.js +403 -0
- package/dist/save-throttle.js +45 -0
- package/dist/serve/common.js +404 -0
- package/dist/serve/routes-chat.js +347 -0
- package/dist/serve/routes-mcp.js +212 -0
- package/dist/serve/routes-memory.js +66 -0
- package/dist/serve/routes-meta.js +205 -0
- package/dist/serve/routes-sessions.js +61 -0
- package/dist/serve/routes-skills.js +70 -0
- package/dist/serve.js +74 -635
- package/dist/sessions.js +197 -15
- package/dist/skills.js +531 -77
- package/dist/synthetic.js +7 -0
- package/dist/title-gen.js +9 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +178 -0
- package/dist/tool-output.js +53 -46
- package/dist/tools/apply_patch.js +265 -0
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +61 -0
- package/dist/tools/bash.js +186 -71
- package/dist/tools/code_search.js +13 -6
- package/dist/tools/edit.js +26 -9
- package/dist/tools/explore.js +144 -16
- package/dist/tools/glob.js +7 -3
- package/dist/tools/grep.js +153 -14
- package/dist/tools/index.js +9 -24
- package/dist/tools/question.js +31 -30
- package/dist/tools/read.js +77 -15
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +100 -33
- package/dist/tools/todo.js +178 -67
- package/dist/tools/web_fetch.js +158 -46
- package/dist/tools/web_search.js +217 -29
- package/dist/tools/write.js +34 -11
- package/dist/tui/App.js +89 -6
- package/dist/tui/ConfirmBar.js +57 -4
- package/dist/tui/InputBar.js +504 -44
- package/dist/tui/MessageList.js +674 -20
- package/dist/tui/ModelPicker.js +113 -0
- package/dist/tui/QuestionBar.js +136 -0
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +14 -12
- package/dist/tui/agent-runner.js +223 -0
- package/dist/tui/caret-pos.js +177 -0
- package/dist/tui/caret.js +69 -0
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/diff-view.js +61 -0
- package/dist/tui/drag-state.js +49 -0
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +189 -31
- package/dist/tui/input-history.js +125 -0
- package/dist/tui/layout.js +88 -0
- package/dist/tui/mouse.js +46 -0
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +226 -0
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +106 -0
- package/dist/tui/slash-handler.js +545 -0
- package/dist/tui/text-width.js +113 -0
- package/dist/tui/theme.js +12 -0
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +14 -0
- package/dist/tui/use-sgr-mouse.js +29 -0
- package/dist/tui-chat.js +346 -330
- package/dist/updater.js +116 -0
- package/dist/xml-search.js +194 -0
- package/docs/API.md +410 -32
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
- package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
- package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
- package/package.json +12 -8
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
|
@@ -0,0 +1,226 @@
|
|
|
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 === "tool-body" && line.startsWith(" "))
|
|
101
|
+
return line.slice(2);
|
|
102
|
+
return line;
|
|
103
|
+
}
|
|
104
|
+
/** Visible prefix rendered in its own Ink Text (user `> `, tool `⚡ name ▸ `). */
|
|
105
|
+
export function visualPrefix(line, role) {
|
|
106
|
+
if (role === "user" && line.startsWith("> "))
|
|
107
|
+
return "> ";
|
|
108
|
+
if (role === "tool") {
|
|
109
|
+
const m = line.match(/^⚡ .*?[▸▾] /);
|
|
110
|
+
return m ? m[0] : "";
|
|
111
|
+
}
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
/** Split a display-column range across a prefix of `at` cells. */
|
|
115
|
+
export function splitRangeAt(range, at) {
|
|
116
|
+
const head = range.start < at && range.end > 0 ? { start: range.start, end: Math.min(at, range.end) } : null;
|
|
117
|
+
const tail = range.end > at ? { start: Math.max(0, range.start - at), end: range.end - at } : null;
|
|
118
|
+
return {
|
|
119
|
+
head: head && head.end > head.start ? head : null,
|
|
120
|
+
tail: tail && tail.end > tail.start ? tail : null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function wordRangeAt(line, col) {
|
|
124
|
+
const cells = [];
|
|
125
|
+
let x = 0;
|
|
126
|
+
for (const { text: g, width: w } of clustersOf(line)) {
|
|
127
|
+
cells.push({ ch: g, start: x, end: x + w });
|
|
128
|
+
x += w;
|
|
129
|
+
}
|
|
130
|
+
const hit = cells.find((c) => col >= c.start && col < c.end);
|
|
131
|
+
if (!hit || /\s/.test(hit.ch))
|
|
132
|
+
return { start: col, end: col };
|
|
133
|
+
let left = cells.indexOf(hit);
|
|
134
|
+
let right = left;
|
|
135
|
+
while (left > 0 && !/\s/.test(cells[left - 1].ch))
|
|
136
|
+
left--;
|
|
137
|
+
while (right < cells.length - 1 && !/\s/.test(cells[right + 1].ch))
|
|
138
|
+
right++;
|
|
139
|
+
return { start: cells[left].start, end: cells[right].end };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Map a 1-based SGR mouse position to a cell in `rows`.
|
|
143
|
+
* `padLeft` is the Ink paddingLeft of every message row (first text cell is
|
|
144
|
+
* terminal column padLeft+1).
|
|
145
|
+
*/
|
|
146
|
+
export function mouseToCell(rows, termRow, termCol, padLeft = 1) {
|
|
147
|
+
const maxRow = Math.max(0, rows.length - 1);
|
|
148
|
+
return {
|
|
149
|
+
row: Math.min(maxRow, Math.max(0, termRow - 1)),
|
|
150
|
+
col: Math.max(0, termCol - 1 - padLeft),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Distribute viewport ranges onto each message part's shown-line index.
|
|
155
|
+
* The resulting arrays are sparse: missing indices mean that shown line is
|
|
156
|
+
* not highlighted. Callers must not require `arr.length === shown.length`.
|
|
157
|
+
*/
|
|
158
|
+
export function alignPartSelections(ranges, partMeta) {
|
|
159
|
+
const map = new Map();
|
|
160
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
161
|
+
const meta = partMeta[i];
|
|
162
|
+
const range = ranges[i];
|
|
163
|
+
if (!meta || !range || range.end <= range.start)
|
|
164
|
+
continue;
|
|
165
|
+
let arr = map.get(meta.part);
|
|
166
|
+
if (!arr) {
|
|
167
|
+
arr = [];
|
|
168
|
+
map.set(meta.part, arr);
|
|
169
|
+
}
|
|
170
|
+
arr[meta.shown] = range;
|
|
171
|
+
}
|
|
172
|
+
return map;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Flow-style selection over the message area. `rows` mirrors the rendered
|
|
176
|
+
* rows (null = blank/chrome row, no text); anchor/current are 0-based
|
|
177
|
+
* (row = index into `rows`, col = display column of the cell under the
|
|
178
|
+
* pointer). Both endpoints are inclusive: dragging onto a character selects
|
|
179
|
+
* it. Dragging down selects the anchor through end-of-line, full middle rows,
|
|
180
|
+
* then start through current; dragging up is symmetric.
|
|
181
|
+
*/
|
|
182
|
+
export function selectionRanges(rows, anchor, current) {
|
|
183
|
+
const result = rows.map(() => null);
|
|
184
|
+
const top = Math.min(anchor.row, current.row);
|
|
185
|
+
const bottom = Math.max(anchor.row, current.row);
|
|
186
|
+
for (let r = top; r <= bottom; r++) {
|
|
187
|
+
const line = rows[r];
|
|
188
|
+
if (r < 0 || r >= rows.length || line === null)
|
|
189
|
+
continue;
|
|
190
|
+
const width = displayWidth(cachedStrip(line));
|
|
191
|
+
let start;
|
|
192
|
+
let end;
|
|
193
|
+
if (anchor.row === current.row) {
|
|
194
|
+
start = Math.min(anchor.col, current.col);
|
|
195
|
+
end = Math.max(anchor.col, current.col) + 1;
|
|
196
|
+
}
|
|
197
|
+
else if (r === anchor.row) {
|
|
198
|
+
if (anchor.row < current.row) {
|
|
199
|
+
start = anchor.col;
|
|
200
|
+
end = width;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
start = 0;
|
|
204
|
+
end = anchor.col + 1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else if (r === current.row) {
|
|
208
|
+
if (anchor.row < current.row) {
|
|
209
|
+
start = 0;
|
|
210
|
+
end = current.col + 1;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
start = current.col;
|
|
214
|
+
end = width;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
start = 0;
|
|
219
|
+
end = width;
|
|
220
|
+
}
|
|
221
|
+
start = Math.max(0, start);
|
|
222
|
+
if (end > start)
|
|
223
|
+
result[r] = { start, end };
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
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: "[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: "sandbox", description: "查看/设置隔离", argHint: "[off|workspace|strict] [--project]" },
|
|
12
|
+
{ name: "undo", description: "撤销上一轮" },
|
|
13
|
+
{ name: "redo", description: "重发最后一条消息" },
|
|
14
|
+
{ name: "diff", description: "显示工作区未提交变更" },
|
|
15
|
+
{ name: "plan", description: "切换只读计划模式" },
|
|
16
|
+
{ name: "sessions", description: "列出/切换已保存会话", argHint: "[id]", aliases: ["resume"] },
|
|
17
|
+
{ name: "reload", description: "重新加载规则" },
|
|
18
|
+
{ name: "rename", description: "重命名当前会话", argHint: "<title>" },
|
|
19
|
+
{ name: "attach", description: "附加本地图片", argHint: "<path>", aliases: ["image"] },
|
|
20
|
+
{ name: "mcp", description: "显示 MCP 状态" },
|
|
21
|
+
{
|
|
22
|
+
name: "skills",
|
|
23
|
+
description: "显示/启用/禁用 skills",
|
|
24
|
+
argHint: "[list|info <name>|enable|disable <name> [--project]]",
|
|
25
|
+
},
|
|
26
|
+
{ name: "path", description: "显示工作目录", aliases: ["pwd"] },
|
|
27
|
+
{ name: "paste", description: "粘贴剪贴板图片", argHint: "[t]" },
|
|
28
|
+
{ name: "help", description: "显示帮助" },
|
|
29
|
+
{ name: "exit", description: "退出", aliases: ["quit", "q"] },
|
|
30
|
+
];
|
|
31
|
+
export function filterSlashCommands(input, commands = SLASH_COMMANDS) {
|
|
32
|
+
const q = input.slice(1).trim().toLowerCase();
|
|
33
|
+
if (q === "")
|
|
34
|
+
return commands;
|
|
35
|
+
return commands.filter((c) => c.name.startsWith(q) || (c.aliases ?? []).some((a) => a.startsWith(q)));
|
|
36
|
+
}
|
|
37
|
+
/** True for submitted input that is a known slash command (including aliases). */
|
|
38
|
+
export function isKnownSlashCommandInput(text, commands = SLASH_COMMANDS) {
|
|
39
|
+
if (!text.startsWith("/"))
|
|
40
|
+
return false;
|
|
41
|
+
const name = text.slice(1).trim().split(/\s/, 1)[0]?.toLowerCase() ?? "";
|
|
42
|
+
if (!name)
|
|
43
|
+
return true;
|
|
44
|
+
return commands.some((c) => c.name === name || (c.aliases ?? []).some((a) => a === name));
|
|
45
|
+
}
|
|
46
|
+
const argProviders = new Map();
|
|
47
|
+
/** Registered by the app at startup so this module stays free of app imports. */
|
|
48
|
+
export function setSlashArgProvider(command, provider) {
|
|
49
|
+
argProviders.set(command, provider);
|
|
50
|
+
}
|
|
51
|
+
export function clearSlashArgProviders() {
|
|
52
|
+
argProviders.clear();
|
|
53
|
+
}
|
|
54
|
+
function splitArgs(input) {
|
|
55
|
+
const out = [];
|
|
56
|
+
let cur = "";
|
|
57
|
+
let q = null;
|
|
58
|
+
for (const ch of input) {
|
|
59
|
+
if (q) {
|
|
60
|
+
if (ch === q)
|
|
61
|
+
q = null;
|
|
62
|
+
else
|
|
63
|
+
cur += ch;
|
|
64
|
+
}
|
|
65
|
+
else if (ch === '"' || ch === "'")
|
|
66
|
+
q = ch;
|
|
67
|
+
else if (/\s/.test(ch)) {
|
|
68
|
+
if (cur) {
|
|
69
|
+
out.push(cur);
|
|
70
|
+
cur = "";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else
|
|
74
|
+
cur += ch;
|
|
75
|
+
}
|
|
76
|
+
if (cur)
|
|
77
|
+
out.push(cur);
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/** Null while the command name itself is still being typed (no space yet). */
|
|
81
|
+
export function parseSlashArgs(value) {
|
|
82
|
+
if (!value.startsWith("/"))
|
|
83
|
+
return null;
|
|
84
|
+
const raw = value.slice(1);
|
|
85
|
+
if (!/\s/.test(raw))
|
|
86
|
+
return null;
|
|
87
|
+
const parts = splitArgs(raw.trim());
|
|
88
|
+
const command = parts[0] ?? "";
|
|
89
|
+
const rest = parts.slice(1);
|
|
90
|
+
const partial = /\s$/.test(raw) ? "" : (rest.pop() ?? "");
|
|
91
|
+
return { command, tokens: rest, partial };
|
|
92
|
+
}
|
|
93
|
+
export function getSlashArgOptions(value) {
|
|
94
|
+
const ctx = parseSlashArgs(value);
|
|
95
|
+
if (!ctx)
|
|
96
|
+
return [];
|
|
97
|
+
const provider = argProviders.get(ctx.command);
|
|
98
|
+
if (!provider)
|
|
99
|
+
return [];
|
|
100
|
+
const prefix = ctx.partial.toLowerCase();
|
|
101
|
+
return provider(ctx.tokens, ctx.partial).filter((o) => o.value.toLowerCase().startsWith(prefix));
|
|
102
|
+
}
|
|
103
|
+
/** Replace the token being typed with `option`, leaving a trailing space. */
|
|
104
|
+
export function applySlashArgCompletion(value, option) {
|
|
105
|
+
return /\s$/.test(value) ? `${value}${option} ` : `${value.replace(/\S+$/, option)} `;
|
|
106
|
+
}
|