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.
Files changed (156) hide show
  1. package/README.md +32 -18
  2. package/dist/cli.js +538 -12
  3. package/docs/API.md +25 -8
  4. package/package.json +6 -6
  5. package/skills/self-config/SKILL.md +7 -3
  6. package/skills/self-config/reference.md +8 -4
  7. package/dist/agent.js +0 -1342
  8. package/dist/assistant-stream.js +0 -131
  9. package/dist/cli/commands/chat.js +0 -13
  10. package/dist/cli/commands/exec.js +0 -35
  11. package/dist/cli/commands/history.js +0 -58
  12. package/dist/cli/commands/index.js +0 -241
  13. package/dist/cli/commands/init.js +0 -18
  14. package/dist/cli/commands/mcp.js +0 -173
  15. package/dist/cli/commands/memory.js +0 -87
  16. package/dist/cli/commands/models.js +0 -21
  17. package/dist/cli/commands/permission.js +0 -12
  18. package/dist/cli/commands/rules.js +0 -33
  19. package/dist/cli/commands/sandbox.js +0 -13
  20. package/dist/cli/commands/serve.js +0 -9
  21. package/dist/cli/commands/setup.js +0 -4
  22. package/dist/cli/commands/shared.js +0 -16
  23. package/dist/cli/commands/skills.js +0 -119
  24. package/dist/cli/commands/think.js +0 -12
  25. package/dist/cli/commands/update.js +0 -7
  26. package/dist/cli/commands/write-config.js +0 -52
  27. package/dist/cli/errors.js +0 -36
  28. package/dist/cli/exec-prompt.js +0 -26
  29. package/dist/cli/option-helpers.js +0 -65
  30. package/dist/cli/program.js +0 -217
  31. package/dist/clipboard.js +0 -142
  32. package/dist/code-mode.js +0 -184
  33. package/dist/compaction.js +0 -644
  34. package/dist/config.js +0 -470
  35. package/dist/confirm.js +0 -172
  36. package/dist/context-window.js +0 -246
  37. package/dist/doom-loop.js +0 -278
  38. package/dist/fetch-timeout.js +0 -152
  39. package/dist/http-approvals.js +0 -60
  40. package/dist/http.js +0 -119
  41. package/dist/instructions.js +0 -196
  42. package/dist/logger.js +0 -95
  43. package/dist/markdown.js +0 -259
  44. package/dist/mcp.js +0 -1036
  45. package/dist/memory-cli.js +0 -33
  46. package/dist/memory.js +0 -295
  47. package/dist/model-catalog.js +0 -285
  48. package/dist/output.js +0 -76
  49. package/dist/paste-handler.js +0 -41
  50. package/dist/permission-cli.js +0 -40
  51. package/dist/plugins.js +0 -131
  52. package/dist/pricing.js +0 -119
  53. package/dist/provider.js +0 -63
  54. package/dist/question-format.js +0 -60
  55. package/dist/reasoning-stream.js +0 -158
  56. package/dist/sandbox-cli.js +0 -79
  57. package/dist/sandbox.js +0 -403
  58. package/dist/save-throttle.js +0 -45
  59. package/dist/scope.js +0 -23
  60. package/dist/serve/common.js +0 -425
  61. package/dist/serve/routes-chat.js +0 -367
  62. package/dist/serve/routes-mcp.js +0 -212
  63. package/dist/serve/routes-memory.js +0 -95
  64. package/dist/serve/routes-meta.js +0 -233
  65. package/dist/serve/routes-sessions.js +0 -61
  66. package/dist/serve/routes-skills.js +0 -70
  67. package/dist/serve.js +0 -138
  68. package/dist/sessions.js +0 -265
  69. package/dist/skills.js +0 -599
  70. package/dist/structured-output.js +0 -29
  71. package/dist/synthetic.js +0 -7
  72. package/dist/think-cli.js +0 -36
  73. package/dist/thinking-wire.js +0 -228
  74. package/dist/thinking.js +0 -142
  75. package/dist/title-gen.js +0 -55
  76. package/dist/token-display.js +0 -39
  77. package/dist/tool-display.js +0 -178
  78. package/dist/tool-output.js +0 -126
  79. package/dist/tools/apply_patch.js +0 -265
  80. package/dist/tools/atomic-file.js +0 -35
  81. package/dist/tools/backend.js +0 -61
  82. package/dist/tools/bash.js +0 -231
  83. package/dist/tools/code_search.js +0 -98
  84. package/dist/tools/edit.js +0 -68
  85. package/dist/tools/explore.js +0 -232
  86. package/dist/tools/glob.js +0 -40
  87. package/dist/tools/grep.js +0 -174
  88. package/dist/tools/index.js +0 -34
  89. package/dist/tools/question.js +0 -54
  90. package/dist/tools/read.js +0 -109
  91. package/dist/tools/search-searxng.js +0 -223
  92. package/dist/tools/search-serper.js +0 -189
  93. package/dist/tools/task.js +0 -165
  94. package/dist/tools/todo.js +0 -213
  95. package/dist/tools/web_fetch.js +0 -195
  96. package/dist/tools/web_search.js +0 -228
  97. package/dist/tools/write.js +0 -55
  98. package/dist/tui/App.js +0 -126
  99. package/dist/tui/ConfirmBar.js +0 -66
  100. package/dist/tui/InputBar.js +0 -616
  101. package/dist/tui/MessageList.js +0 -718
  102. package/dist/tui/ModelPicker.js +0 -113
  103. package/dist/tui/QuestionBar.js +0 -136
  104. package/dist/tui/SessionPicker.js +0 -79
  105. package/dist/tui/Spinner.js +0 -6
  106. package/dist/tui/StatusBar.js +0 -22
  107. package/dist/tui/ThinkPicker.js +0 -77
  108. package/dist/tui/agent-runner.js +0 -223
  109. package/dist/tui/bracketed-paste.js +0 -37
  110. package/dist/tui/caret-pos.js +0 -179
  111. package/dist/tui/caret.js +0 -69
  112. package/dist/tui/click-count.js +0 -13
  113. package/dist/tui/diff-view.js +0 -61
  114. package/dist/tui/drag-state.js +0 -49
  115. package/dist/tui/hydrate.js +0 -129
  116. package/dist/tui/index.js +0 -258
  117. package/dist/tui/input-history.js +0 -125
  118. package/dist/tui/layout.js +0 -105
  119. package/dist/tui/mouse.js +0 -46
  120. package/dist/tui/overlay-input.js +0 -12
  121. package/dist/tui/paste-draft.js +0 -173
  122. package/dist/tui/prompt-queue.js +0 -24
  123. package/dist/tui/selection.js +0 -232
  124. package/dist/tui/session-switch.js +0 -28
  125. package/dist/tui/slash-commands.js +0 -123
  126. package/dist/tui/slash-handler.js +0 -589
  127. package/dist/tui/text-width.js +0 -113
  128. package/dist/tui/theme.js +0 -12
  129. package/dist/tui/token-info.js +0 -7
  130. package/dist/tui/tool-children.js +0 -19
  131. package/dist/tui/types.js +0 -1
  132. package/dist/tui/undo-stack.js +0 -14
  133. package/dist/tui/use-sgr-mouse.js +0 -29
  134. package/dist/tui-chat.js +0 -456
  135. package/dist/updater.js +0 -116
  136. package/dist/xml-search.js +0 -194
  137. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
  138. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
  139. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
  140. package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
  141. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
  142. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
  143. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
  144. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
  145. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
  146. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
  147. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
  148. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
  149. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
  150. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
  151. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
  152. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
  153. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
  154. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
  155. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
  156. package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +0 -140
@@ -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
- }
@@ -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
- }
@@ -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
- }