min-agent 0.2.1 → 0.3.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 +146 -18
- package/dist/agent.js +293 -408
- package/dist/assistant-stream.js +11 -7
- package/dist/cli.js +397 -139
- package/dist/clipboard.js +59 -23
- package/dist/code-mode.js +3 -3
- package/dist/compaction.js +182 -81
- package/dist/config.js +186 -35
- package/dist/confirm.js +55 -6
- package/dist/context-window.js +67 -54
- package/dist/doom-loop.js +19 -12
- package/dist/http.js +119 -0
- package/dist/instructions.js +51 -33
- package/dist/logger.js +66 -0
- package/dist/markdown.js +3 -44
- package/dist/mcp.js +547 -100
- package/dist/memory.js +48 -6
- package/dist/output.js +36 -27
- package/dist/paste-handler.js +3 -3
- package/dist/plugins.js +33 -6
- package/dist/pricing.js +119 -0
- package/dist/provider.js +17 -15
- package/dist/serve.js +658 -369
- package/dist/sessions.js +151 -13
- package/dist/skills.js +466 -76
- package/dist/synthetic.js +7 -0
- package/dist/title-gen.js +2 -1
- package/dist/tool-display.js +173 -0
- package/dist/tool-output.js +54 -45
- package/dist/tools/apply_patch.js +191 -0
- package/dist/tools/backend.js +61 -0
- package/dist/tools/bash.js +147 -70
- package/dist/tools/code_search.js +6 -5
- package/dist/tools/edit.js +23 -7
- package/dist/tools/explore.js +80 -12
- package/dist/tools/glob.js +3 -3
- package/dist/tools/grep.js +146 -14
- package/dist/tools/index.js +7 -7
- package/dist/tools/question.js +4 -22
- package/dist/tools/read.js +71 -11
- package/dist/tools/task.js +33 -20
- package/dist/tools/todo.js +83 -73
- package/dist/tools/web_fetch.js +150 -46
- package/dist/tools/web_search.js +706 -28
- package/dist/tools/write.js +13 -7
- package/dist/tui/App.js +40 -6
- package/dist/tui/ConfirmBar.js +24 -3
- package/dist/tui/InputBar.js +390 -45
- package/dist/tui/MessageList.js +533 -20
- package/dist/tui/ModelPicker.js +108 -0
- package/dist/tui/QuestionBar.js +104 -0
- package/dist/tui/StatusBar.js +19 -11
- package/dist/tui/agent-runner.js +103 -0
- package/dist/tui/caret-pos.js +134 -0
- package/dist/tui/caret.js +69 -0
- package/dist/tui/diff-view.js +61 -0
- package/dist/tui/drag-state.js +44 -0
- package/dist/tui/index.js +153 -24
- package/dist/tui/input-history.js +44 -0
- package/dist/tui/layout.js +17 -0
- package/dist/tui/mouse.js +46 -0
- package/dist/tui/selection.js +134 -0
- package/dist/tui/slash-commands.js +90 -0
- package/dist/tui/slash-handler.js +370 -0
- package/dist/tui/text-width.js +91 -0
- package/dist/tui/theme.js +12 -0
- package/dist/tui/undo-stack.js +14 -0
- package/dist/tui/use-sgr-mouse.js +27 -0
- package/dist/tui-chat.js +111 -331
- package/dist/updater.js +57 -0
- package/docs/API.md +160 -14
- 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/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/package.json +7 -8
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
export function sysMsg(tui, content) {
|
|
2
|
+
tui.addMessage({
|
|
3
|
+
id: `sys-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
4
|
+
role: "system",
|
|
5
|
+
content,
|
|
6
|
+
timestamp: Date.now(),
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export async function handleSlashCommand(input, tui, messages, currentModel, tracker, setModel, requestExit, state) {
|
|
10
|
+
const { parseSlashArgs } = await import("./slash-commands.js");
|
|
11
|
+
const parsed = parseSlashArgs(input);
|
|
12
|
+
const rawParts = input.slice(1).trim().split(/\s+/);
|
|
13
|
+
const cmd = (parsed?.command || rawParts[0] || "").toLowerCase();
|
|
14
|
+
const rest = parsed ? [...parsed.tokens, ...(parsed.partial ? [parsed.partial] : [])] : rawParts.slice(1);
|
|
15
|
+
const arg = rest.join(" ");
|
|
16
|
+
const { truncateDisplay } = await import("../tool-display.js");
|
|
17
|
+
switch (cmd) {
|
|
18
|
+
case "exit":
|
|
19
|
+
case "quit":
|
|
20
|
+
case "q":
|
|
21
|
+
requestExit();
|
|
22
|
+
break;
|
|
23
|
+
case "clear": {
|
|
24
|
+
const { clearStack } = await import("./undo-stack.js");
|
|
25
|
+
messages.length = 0;
|
|
26
|
+
tui.update({ messages: [] });
|
|
27
|
+
state.setUndoStack(clearStack(state.undoStack));
|
|
28
|
+
sysMsg(tui, "✓ 对话已清除");
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
case "undo": {
|
|
32
|
+
const { popTurn } = await import("./undo-stack.js");
|
|
33
|
+
const popped = popTurn(state.undoStack);
|
|
34
|
+
if (!popped) {
|
|
35
|
+
sysMsg(tui, "没有可撤销的内容");
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
state.setUndoStack(popped.stack);
|
|
39
|
+
messages.splice(popped.turn.start, popped.turn.end - popped.turn.start);
|
|
40
|
+
tui.truncateMessages(popped.turn.uiEnd);
|
|
41
|
+
sysMsg(tui, "✓ 已撤销 1 轮");
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case "redo": {
|
|
45
|
+
if (!state.lastUserMessage) {
|
|
46
|
+
sysMsg(tui, "没有可重发的消息");
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
const text = state.lastUserMessage;
|
|
50
|
+
tui.addMessage({ id: `user-${Date.now()}`, role: "user", content: text, timestamp: Date.now() });
|
|
51
|
+
messages.push({ role: "user", content: text });
|
|
52
|
+
return "run";
|
|
53
|
+
}
|
|
54
|
+
case "diff": {
|
|
55
|
+
const { getWorkingTreeDiff } = await import("./diff-view.js");
|
|
56
|
+
const result = await getWorkingTreeDiff();
|
|
57
|
+
if (!result.ok) {
|
|
58
|
+
const msg = result.reason === "not_git" ? "非 git 仓库,无法显示 diff" : result.reason === "no_changes" ? "工作区无未提交变更" : `diff 失败: ${result.message}`;
|
|
59
|
+
sysMsg(tui, msg);
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
sysMsg(tui, result.diff);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case "plan": {
|
|
66
|
+
state.togglePlanMode();
|
|
67
|
+
sysMsg(tui, state.planMode ? "✓ 计划模式已开启:仅只读工具可用,输出计划后再次输入 /plan 确认并执行" : "✓ 已退出计划模式");
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "sessions": {
|
|
71
|
+
const { listSessions } = await import("../sessions.js");
|
|
72
|
+
const sessions = listSessions();
|
|
73
|
+
if (sessions.length === 0)
|
|
74
|
+
sysMsg(tui, "无已保存的会话");
|
|
75
|
+
else {
|
|
76
|
+
const header = state.sessionId ? "" : " (当前会话未保存)\n";
|
|
77
|
+
const lines = sessions.map((s) => {
|
|
78
|
+
const current = state.sessionId === s.id ? " ← 当前" : "";
|
|
79
|
+
const date = s.updated.split("T")[0];
|
|
80
|
+
return ` ${s.id} ${date} ${s.title} (${s.messageCount} msgs)${current}`;
|
|
81
|
+
});
|
|
82
|
+
sysMsg(tui, `会话 (${sessions.length}):\n${header}${lines.join("\n")}\n\n恢复: min-agent chat --resume <id>`);
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case "rename": {
|
|
87
|
+
const title = arg.trim();
|
|
88
|
+
if (!title) {
|
|
89
|
+
sysMsg(tui, "用法: /rename <标题>");
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
const { saveSession, renameSession } = await import("../sessions.js");
|
|
93
|
+
let id = state.sessionId;
|
|
94
|
+
if (!id) {
|
|
95
|
+
id = saveSession(messages);
|
|
96
|
+
state.setSessionId(id);
|
|
97
|
+
}
|
|
98
|
+
if (renameSession(id, title))
|
|
99
|
+
sysMsg(tui, `✓ 会话已重命名为: ${title}`);
|
|
100
|
+
else
|
|
101
|
+
sysMsg(tui, "重命名失败: 会话不存在");
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case "attach":
|
|
105
|
+
case "image": {
|
|
106
|
+
const filePath = arg;
|
|
107
|
+
if (!filePath) {
|
|
108
|
+
sysMsg(tui, "用法: /attach <图片路径>");
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
const { loadImageParts } = await import("../agent.js");
|
|
112
|
+
const notify = (kind, imgPath) => {
|
|
113
|
+
if (kind === "not_found")
|
|
114
|
+
sysMsg(tui, `⚠ 图片不存在: ${imgPath}`);
|
|
115
|
+
else if (kind === "too_large")
|
|
116
|
+
sysMsg(tui, `⚠ 图片超过 5MB 限制: ${imgPath}`);
|
|
117
|
+
};
|
|
118
|
+
const parts = [{ type: "text", text: "" }, ...loadImageParts([filePath], notify)];
|
|
119
|
+
if (parts.length <= 1)
|
|
120
|
+
break;
|
|
121
|
+
messages.push({ role: "user", content: parts });
|
|
122
|
+
tui.addMessage({ id: `user-${Date.now()}`, role: "user", content: `[📎 图片] ${filePath}`, timestamp: Date.now() });
|
|
123
|
+
return "run";
|
|
124
|
+
}
|
|
125
|
+
case "compact": {
|
|
126
|
+
const { compactMessages } = await import("../compaction.js");
|
|
127
|
+
const { estimateTokens } = await import("../compaction.js");
|
|
128
|
+
const { resolveModel } = await import("../provider.js");
|
|
129
|
+
const { clearStack } = await import("./undo-stack.js");
|
|
130
|
+
if (messages.length < 4)
|
|
131
|
+
sysMsg(tui, "消息太少,无需压缩");
|
|
132
|
+
else {
|
|
133
|
+
sysMsg(tui, "⟳ 正在压缩上下文...");
|
|
134
|
+
const model = resolveModel(currentModel);
|
|
135
|
+
state.setUndoStack(clearStack(state.undoStack));
|
|
136
|
+
const result = await compactMessages(messages, model, { keepRecentTurns: 2, autoContinue: false });
|
|
137
|
+
messages.length = 0;
|
|
138
|
+
messages.push(...result.messages);
|
|
139
|
+
if (tracker)
|
|
140
|
+
tracker.resetContext();
|
|
141
|
+
sysMsg(tui, `✓ 已压缩 (约 ${estimateTokens(messages)} tokens)`);
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case "provider": {
|
|
146
|
+
const { loadConfig, saveConfig } = await import("../config.js");
|
|
147
|
+
const cfg = loadConfig();
|
|
148
|
+
const providers = cfg.providers ?? [];
|
|
149
|
+
if (arg) {
|
|
150
|
+
const target = providers.find((p) => p.name === arg);
|
|
151
|
+
if (!target) {
|
|
152
|
+
sysMsg(tui, `Provider "${arg}" 不存在`);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
cfg.activeProvider = arg;
|
|
156
|
+
saveConfig(cfg);
|
|
157
|
+
setModel(target.defaultModel ?? "");
|
|
158
|
+
sysMsg(tui, `✓ 当前 Provider 已切换为: ${arg}`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
if (providers.length === 0) {
|
|
162
|
+
sysMsg(tui, "无已配置 Provider,运行: min-agent setup");
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
const lines = providers.map((p) => {
|
|
166
|
+
const cur = p.name === cfg.activeProvider ? " ← 当前" : "";
|
|
167
|
+
return ` ${p.name} ${p.baseURL}${cur}`;
|
|
168
|
+
});
|
|
169
|
+
sysMsg(tui, `Providers:\n${lines.join("\n")}\n\n切换: /provider <name>`);
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case "model": {
|
|
174
|
+
if (arg) {
|
|
175
|
+
const config = await import("../config.js");
|
|
176
|
+
const cfg = config.loadConfig();
|
|
177
|
+
const provider = config.getActiveProvider(cfg);
|
|
178
|
+
if (provider) {
|
|
179
|
+
provider.defaultModel = arg;
|
|
180
|
+
config.saveConfig(cfg);
|
|
181
|
+
}
|
|
182
|
+
setModel(arg);
|
|
183
|
+
sysMsg(tui, `✓ 默认模型已设为: ${arg}`);
|
|
184
|
+
}
|
|
185
|
+
else
|
|
186
|
+
tui.showModelPicker();
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case "models": {
|
|
190
|
+
const { fetchModels } = await import("../config.js");
|
|
191
|
+
const { loadConfig, getActiveProvider } = await import("../config.js");
|
|
192
|
+
const cfg = loadConfig();
|
|
193
|
+
const provider = getActiveProvider(cfg);
|
|
194
|
+
if (!provider?.baseURL || !provider?.apiKey) {
|
|
195
|
+
sysMsg(tui, "未配置 Provider,运行: min-agent setup");
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
sysMsg(tui, "正在获取模型列表...");
|
|
199
|
+
const models = await fetchModels(provider.baseURL, provider.apiKey);
|
|
200
|
+
if (models.length === 0)
|
|
201
|
+
sysMsg(tui, "无法获取模型列表");
|
|
202
|
+
else {
|
|
203
|
+
const lines = models.map((m) => {
|
|
204
|
+
const marker = m === provider.defaultModel ? " ← 默认" : "";
|
|
205
|
+
return ` ${m}${marker}`;
|
|
206
|
+
});
|
|
207
|
+
sysMsg(tui, `可用模型 (${models.length}):\n${lines.join("\n")}`);
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
case "memory": {
|
|
212
|
+
if (arg) {
|
|
213
|
+
const { addMemory } = await import("../memory.js");
|
|
214
|
+
addMemory(arg);
|
|
215
|
+
sysMsg(tui, `✓ 已保存记忆: "${arg}"`);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
const { loadMemories } = await import("../memory.js");
|
|
219
|
+
const memories = loadMemories();
|
|
220
|
+
if (memories.length === 0)
|
|
221
|
+
sysMsg(tui, "无已保存的记忆");
|
|
222
|
+
else {
|
|
223
|
+
const lines = memories.map((m, i) => {
|
|
224
|
+
const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
|
|
225
|
+
return ` #${i + 1}: ${m.content}${tags}`;
|
|
226
|
+
});
|
|
227
|
+
sysMsg(tui, `记忆 (${memories.length}):\n${lines.join("\n")}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
case "path":
|
|
233
|
+
case "pwd":
|
|
234
|
+
sysMsg(tui, process.cwd());
|
|
235
|
+
break;
|
|
236
|
+
case "paste": {
|
|
237
|
+
const { getClipboardImage } = await import("../clipboard.js");
|
|
238
|
+
const img = getClipboardImage();
|
|
239
|
+
if (!img) {
|
|
240
|
+
sysMsg(tui, "剪贴板中未找到图片");
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
sysMsg(tui, `📎 剪贴板图片 (${(img.data.length / 1024).toFixed(1)} KB)`);
|
|
244
|
+
const text = arg || "What's in this image?";
|
|
245
|
+
const content = [{ type: "text", text }, { type: "image", image: img.data, mimeType: img.mimeType }];
|
|
246
|
+
messages.push({ role: "user", content });
|
|
247
|
+
tui.addMessage({ id: `user-${Date.now()}`, role: "user", content: `${text} [📎 图片]`, timestamp: Date.now() });
|
|
248
|
+
return "run";
|
|
249
|
+
}
|
|
250
|
+
case "tokens": {
|
|
251
|
+
const { getContextWindow } = await import("../context-window.js");
|
|
252
|
+
const { getModelPrice, estimateCost, formatCost } = await import("../pricing.js");
|
|
253
|
+
const { estimateTokens } = await import("../compaction.js");
|
|
254
|
+
if (tracker && tracker.lastInputTokens > 0) {
|
|
255
|
+
const ctxWindow = await getContextWindow(currentModel);
|
|
256
|
+
const pct = Math.round((tracker.lastInputTokens / ctxWindow) * 100);
|
|
257
|
+
const price = await getModelPrice(currentModel ?? "");
|
|
258
|
+
const cost = estimateCost({ inputTokens: tracker.totalInputTokens, outputTokens: tracker.totalOutputTokens }, price);
|
|
259
|
+
sysMsg(tui, [`Context: ${tracker.lastInputTokens} / ${ctxWindow} tokens (${pct}%)`, `Total: ${tracker.totalInputTokens} in / ${tracker.totalOutputTokens} out`, ...(cost != null ? [`Cost: 约 ${formatCost(cost)}`] : []), `Messages: ${messages.length}`].join("\n"));
|
|
260
|
+
}
|
|
261
|
+
else
|
|
262
|
+
sysMsg(tui, `估算 tokens: ${estimateTokens(messages)} | Messages: ${messages.length}`);
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
case "budget": {
|
|
266
|
+
const { loadConfig, saveConfig } = await import("../config.js");
|
|
267
|
+
const { getModelPrice, estimateCost, formatCost } = await import("../pricing.js");
|
|
268
|
+
const cfg = loadConfig();
|
|
269
|
+
const maxCost = cfg.budget?.maxCostUSD;
|
|
270
|
+
if (arg) {
|
|
271
|
+
const n = parseFloat(arg);
|
|
272
|
+
if (isNaN(n) || n <= 0) {
|
|
273
|
+
sysMsg(tui, "用法: /budget <USD 金额>");
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
cfg.budget = { maxCostUSD: n };
|
|
277
|
+
saveConfig(cfg);
|
|
278
|
+
sysMsg(tui, `✓ 预算上限已设为: $${n}`);
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
const price = await getModelPrice(currentModel ?? "");
|
|
282
|
+
const cost = estimateCost({ inputTokens: tracker.totalInputTokens, outputTokens: tracker.totalOutputTokens }, price);
|
|
283
|
+
const costStr = cost != null ? `约 ${formatCost(cost)}` : "--";
|
|
284
|
+
sysMsg(tui, maxCost != null ? `预算: 已用${costStr} / 上限 $${maxCost}\n设置: /budget <金额>` : `预算: 已用${costStr} / 未设置上限\n设置: /budget <金额>`);
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case "mcp": {
|
|
288
|
+
const { loadMcpConfig } = await import("../mcp.js");
|
|
289
|
+
const { getMcpStatus } = await import("../mcp.js");
|
|
290
|
+
const config = loadMcpConfig();
|
|
291
|
+
const servers = Object.entries(config.mcpServers);
|
|
292
|
+
if (servers.length === 0)
|
|
293
|
+
sysMsg(tui, "无 MCP 服务器配置");
|
|
294
|
+
else {
|
|
295
|
+
const status = getMcpStatus();
|
|
296
|
+
const lines = servers.map(([name, cfg]) => {
|
|
297
|
+
const disabled = cfg.enabled === false;
|
|
298
|
+
const st = status[name];
|
|
299
|
+
const connected = st?.connected ?? false;
|
|
300
|
+
const toolCount = st?.tools.length ?? 0;
|
|
301
|
+
const state = disabled ? "disabled" : connected ? "connected" : "disconnected";
|
|
302
|
+
const err = !disabled && !connected && st?.error ? ` — ${st.error}` : "";
|
|
303
|
+
return ` ${name}: ${state}${toolCount > 0 ? ` (${toolCount} tools)` : ""}${err}`;
|
|
304
|
+
});
|
|
305
|
+
sysMsg(tui, `MCP 服务器 (${servers.length}):\n${lines.join("\n")}`);
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
case "skills": {
|
|
310
|
+
const { discoverSkills, getAllSkills, getSkill, getStaleDisabledSkills, setSkillEnabled } = await import("../skills.js");
|
|
311
|
+
discoverSkills({ silent: true });
|
|
312
|
+
const tokens = rest.filter(Boolean);
|
|
313
|
+
const action = tokens[0];
|
|
314
|
+
const all = getAllSkills().sort((a, b) => a.name.localeCompare(b.name));
|
|
315
|
+
if (action === "enable" || action === "disable") {
|
|
316
|
+
const scope = tokens.includes("--project") ? "project" : "global";
|
|
317
|
+
const names = tokens.slice(1).filter((t) => t !== "--project" && t !== "--global");
|
|
318
|
+
if (names.length === 0) {
|
|
319
|
+
sysMsg(tui, `用法: /skills ${action} <name...> [--project]`);
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
const known = new Set(all.map((s) => s.name));
|
|
323
|
+
const unknown = names.filter((n) => !known.has(n));
|
|
324
|
+
if (unknown.length > 0) {
|
|
325
|
+
sysMsg(tui, `未找到 skill: ${unknown.join(", ")}\n可用: ${known.size ? [...known].join(", ") : "无"}`);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
const results = setSkillEnabled(names, action === "enable", scope);
|
|
329
|
+
const notes = results.filter((r) => r.blockedBy).map((r) => ` ⚠ ${r.name} 仍为${r.enabled ? "启用" : "禁用"}(被 ${r.blockedBy} 范围覆盖)`);
|
|
330
|
+
sysMsg(tui, [`✓ 已${action === "enable" ? "启用" : "禁用"}(${scope}): ${names.join(", ")}`, ...notes].join("\n"));
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
if (action === "info") {
|
|
334
|
+
const name = tokens[1];
|
|
335
|
+
const skill = name ? getSkill(name) : undefined;
|
|
336
|
+
if (!skill) {
|
|
337
|
+
sysMsg(tui, `未找到 skill: ${name ?? "(未指定)"}\n可用: ${all.length ? all.map((s) => s.name).join(", ") : "无"}`);
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
const status = skill.enabled ? "enabled" : `disabled(${skill.disabledScope === "project" ? "proj" : "glob"})`;
|
|
341
|
+
const meta = [`Name: ${skill.name}`, `Status: ${status}`, ...(skill.version ? [`Version: ${skill.version}`] : []), `Description: ${skill.description.replace(/\s+/g, " ")}`, `Location: ${skill.location}`, ...(skill.allowedTools.length ? [`Tools: ${skill.allowedTools.join(", ")}`] : []), ...(skill.requiredBins.length ? [`Requires: ${skill.requiredBins.join(", ")}`] : [])];
|
|
342
|
+
const content = skill.content.trim();
|
|
343
|
+
sysMsg(tui, [...meta, ...(content ? ["", "--- Content ---", "", content.slice(0, 4000)] : [])].join("\n"));
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
if (action && action !== "list") {
|
|
347
|
+
sysMsg(tui, "用法: /skills [list|info <name>|enable <name...> [--project]|disable <name...> [--project]]");
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
const stale = getStaleDisabledSkills();
|
|
351
|
+
if (all.length === 0 && stale.length === 0) {
|
|
352
|
+
sysMsg(tui, "无可用 skills。添加 SKILL.md 到 .min-agent/skills/<name>/");
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
const lines = all.map((s) => {
|
|
356
|
+
const status = s.enabled ? "enabled" : `disabled(${s.disabledScope === "project" ? "proj" : "glob"})`;
|
|
357
|
+
return ` ${s.name} ${status} ${truncateDisplay(s.description.replace(/\s+/g, " "), 56)}`;
|
|
358
|
+
});
|
|
359
|
+
if (stale.length > 0)
|
|
360
|
+
lines.push(` ⚠ disabledSkills/enabledSkills 中无对应 skill: ${stale.join(", ")}`);
|
|
361
|
+
sysMsg(tui, `Skills (${all.filter((s) => s.enabled).length} enabled / ${all.length}):\n${lines.join("\n")}\n切换: /skills disable <name> [--project]`);
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
case "help":
|
|
365
|
+
sysMsg(tui, ["命令:", " 输入 / 弹出命令菜单 (↑↓ 选择, Enter 执行, Esc 关闭)", " /clear 清除对话历史", " /compact 压缩上下文", " /model [n] 交互式选择/切换模型", " /models 列出可用模型", " /provider [n] 查看/切换 Provider", " /memory [t] 查看/保存记忆", " /tokens 显示 token 用量与成本", " /budget [n] 查看/设置预算上限 (USD)", " /undo 撤销上一轮", " /redo 重发最后一条消息", " /diff 显示工作区未提交变更", " /plan 切换只读计划模式", " /sessions 列出已保存会话", " /rename <t> 重命名当前会话", " /attach <p> 附加本地图片", " /mcp 显示 MCP 状态", " /skills 显示/启用/禁用 skills(/skills info <name> 查看详情)", " /path 显示工作目录", " /paste [t] 粘贴剪贴板图片 + 可选提示", " /help 显示帮助", " /exit 退出", "", "快捷键:", " Esc 取消当前任务", " Ctrl+J 输入框换行", " Ctrl+C 退出程序", " PageUp/Down 滚动消息", " 鼠标滚轮 滚动消息", " 鼠标拖拽 选择文本并自动复制", " 鼠标点击 工具调用行展开/收起", " ← → 移动输入框光标", " 鼠标点击 输入框内定位光标"].join("\n"));
|
|
366
|
+
break;
|
|
367
|
+
default:
|
|
368
|
+
sysMsg(tui, `未知命令: /${cmd},输入 /help 查看可用命令`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal display-width helpers.
|
|
3
|
+
*
|
|
4
|
+
* CJK / fullwidth characters occupy two terminal cells, combining marks and
|
|
5
|
+
* variation selectors occupy none. The input bar needs the same notion of width
|
|
6
|
+
* that the terminal uses, otherwise the caret (and therefore the IME
|
|
7
|
+
* composition window) drifts as soon as non-ASCII text is typed.
|
|
8
|
+
*/
|
|
9
|
+
/** Zero-width: combining marks, variation selectors, zero-width spaces. */
|
|
10
|
+
function isZeroWidth(cp) {
|
|
11
|
+
return ((cp >= 0x0300 && cp <= 0x036f) ||
|
|
12
|
+
(cp >= 0x0483 && cp <= 0x0489) ||
|
|
13
|
+
(cp >= 0x0591 && cp <= 0x05bd) ||
|
|
14
|
+
(cp >= 0x0610 && cp <= 0x061a) ||
|
|
15
|
+
(cp >= 0x064b && cp <= 0x065f) ||
|
|
16
|
+
(cp >= 0x0e31 && cp <= 0x0e3a) ||
|
|
17
|
+
(cp >= 0x0e47 && cp <= 0x0e4e) ||
|
|
18
|
+
(cp >= 0x1ab0 && cp <= 0x1aff) ||
|
|
19
|
+
(cp >= 0x1dc0 && cp <= 0x1dff) ||
|
|
20
|
+
(cp >= 0x200b && cp <= 0x200f) ||
|
|
21
|
+
(cp >= 0x2060 && cp <= 0x2064) ||
|
|
22
|
+
(cp >= 0x20d0 && cp <= 0x20ff) ||
|
|
23
|
+
(cp >= 0xfe00 && cp <= 0xfe0f) ||
|
|
24
|
+
(cp >= 0xfe20 && cp <= 0xfe2f) ||
|
|
25
|
+
cp === 0xfeff ||
|
|
26
|
+
(cp >= 0xe0100 && cp <= 0xe01ef));
|
|
27
|
+
}
|
|
28
|
+
/** East Asian Wide / Fullwidth, plus the emoji blocks terminals render double. */
|
|
29
|
+
function isFullWidth(cp) {
|
|
30
|
+
return ((cp >= 0x1100 && cp <= 0x115f) ||
|
|
31
|
+
(cp >= 0x2e80 && cp <= 0x303e) ||
|
|
32
|
+
(cp >= 0x3041 && cp <= 0x33ff) ||
|
|
33
|
+
(cp >= 0x3400 && cp <= 0x4dbf) ||
|
|
34
|
+
(cp >= 0x4e00 && cp <= 0x9fff) ||
|
|
35
|
+
(cp >= 0xa000 && cp <= 0xa4cf) ||
|
|
36
|
+
(cp >= 0xa960 && cp <= 0xa97f) ||
|
|
37
|
+
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
38
|
+
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
39
|
+
(cp >= 0xfe10 && cp <= 0xfe19) ||
|
|
40
|
+
(cp >= 0xfe30 && cp <= 0xfe6f) ||
|
|
41
|
+
(cp >= 0xff00 && cp <= 0xff60) ||
|
|
42
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
43
|
+
(cp >= 0x1f300 && cp <= 0x1f64f) ||
|
|
44
|
+
(cp >= 0x1f680 && cp <= 0x1f6ff) ||
|
|
45
|
+
(cp >= 0x1f900 && cp <= 0x1f9ff) ||
|
|
46
|
+
(cp >= 0x20000 && cp <= 0x2fffd) ||
|
|
47
|
+
(cp >= 0x30000 && cp <= 0x3fffd));
|
|
48
|
+
}
|
|
49
|
+
/** Display width of a single code point, in terminal cells. */
|
|
50
|
+
export function charWidth(char) {
|
|
51
|
+
const cp = char.codePointAt(0);
|
|
52
|
+
if (cp === undefined)
|
|
53
|
+
return 0;
|
|
54
|
+
// C0 / C1 control characters render nothing useful.
|
|
55
|
+
if (cp < 0x20 || (cp >= 0x7f && cp < 0xa0))
|
|
56
|
+
return 0;
|
|
57
|
+
if (isZeroWidth(cp))
|
|
58
|
+
return 0;
|
|
59
|
+
return isFullWidth(cp) ? 2 : 1;
|
|
60
|
+
}
|
|
61
|
+
/** Display width of a string (single logical line, no ANSI escapes). */
|
|
62
|
+
export function displayWidth(text) {
|
|
63
|
+
let width = 0;
|
|
64
|
+
for (const char of text)
|
|
65
|
+
width += charWidth(char);
|
|
66
|
+
return width;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Break a single logical line into visual rows of at most `maxWidth` cells.
|
|
70
|
+
* Breaks on cell boundaries (not word boundaries) so the caret column can be
|
|
71
|
+
* derived exactly — a fullwidth character is never split across rows.
|
|
72
|
+
*/
|
|
73
|
+
export function wrapByWidth(line, maxWidth) {
|
|
74
|
+
if (maxWidth < 1)
|
|
75
|
+
return [line];
|
|
76
|
+
const rows = [];
|
|
77
|
+
let current = "";
|
|
78
|
+
let width = 0;
|
|
79
|
+
for (const char of line) {
|
|
80
|
+
const w = charWidth(char);
|
|
81
|
+
if (width + w > maxWidth) {
|
|
82
|
+
rows.push(current);
|
|
83
|
+
current = "";
|
|
84
|
+
width = 0;
|
|
85
|
+
}
|
|
86
|
+
current += char;
|
|
87
|
+
width += w;
|
|
88
|
+
}
|
|
89
|
+
rows.push(current);
|
|
90
|
+
return rows;
|
|
91
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function createUndoStack() {
|
|
2
|
+
return [];
|
|
3
|
+
}
|
|
4
|
+
export function pushTurn(stack, start, end, uiEnd) {
|
|
5
|
+
return [...stack, { start, end, uiEnd }];
|
|
6
|
+
}
|
|
7
|
+
export function popTurn(stack) {
|
|
8
|
+
if (stack.length === 0)
|
|
9
|
+
return null;
|
|
10
|
+
return { turn: stack[stack.length - 1], stack: stack.slice(0, -1) };
|
|
11
|
+
}
|
|
12
|
+
export function clearStack(stack) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useStdin } from "ink";
|
|
3
|
+
import { scanSgrMouse } from "./mouse.js";
|
|
4
|
+
export function useSgrMouse(enabled, onEvent) {
|
|
5
|
+
const { stdin } = useStdin();
|
|
6
|
+
const onEventRef = useRef(onEvent);
|
|
7
|
+
onEventRef.current = onEvent;
|
|
8
|
+
const bufRef = useRef("");
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!enabled || !stdin)
|
|
11
|
+
return;
|
|
12
|
+
const handle = (data) => {
|
|
13
|
+
bufRef.current += data.toString("utf-8");
|
|
14
|
+
let scanned;
|
|
15
|
+
while ((scanned = scanSgrMouse(bufRef.current))) {
|
|
16
|
+
bufRef.current = scanned.rest;
|
|
17
|
+
onEventRef.current(scanned.event);
|
|
18
|
+
}
|
|
19
|
+
if (bufRef.current.length > 64) {
|
|
20
|
+
const esc = bufRef.current.lastIndexOf("\x1b");
|
|
21
|
+
bufRef.current = esc === -1 ? "" : bufRef.current.slice(esc);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
stdin.on("data", handle);
|
|
25
|
+
return () => { stdin.off("data", handle); };
|
|
26
|
+
}, [enabled, stdin]);
|
|
27
|
+
}
|