min-agent 0.3.0 → 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 +111 -28
- package/dist/agent.js +1119 -256
- 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 +5 -888
- package/dist/code-mode.js +32 -14
- package/dist/compaction.js +347 -160
- package/dist/config.js +119 -10
- package/dist/confirm.js +56 -9
- package/dist/context-window.js +107 -39
- package/dist/doom-loop.js +264 -29
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/instructions.js +21 -0
- package/dist/logger.js +33 -4
- package/dist/markdown.js +37 -11
- package/dist/mcp.js +328 -30
- package/dist/memory.js +97 -56
- package/dist/output.js +7 -5
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +46 -8
- package/dist/pricing.js +4 -4
- package/dist/provider.js +23 -6
- 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 +33 -883
- package/dist/sessions.js +53 -9
- package/dist/skills.js +82 -18
- package/dist/title-gen.js +8 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +5 -0
- package/dist/tool-output.js +1 -3
- package/dist/tools/apply_patch.js +85 -11
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +2 -2
- package/dist/tools/bash.js +57 -19
- package/dist/tools/code_search.js +7 -1
- package/dist/tools/edit.js +11 -10
- package/dist/tools/explore.js +74 -14
- package/dist/tools/glob.js +4 -0
- package/dist/tools/grep.js +17 -10
- package/dist/tools/index.js +6 -21
- package/dist/tools/question.js +28 -9
- package/dist/tools/read.js +6 -4
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +84 -30
- package/dist/tools/todo.js +120 -19
- package/dist/tools/web_fetch.js +11 -3
- package/dist/tools/web_search.js +66 -556
- package/dist/tools/write.js +23 -6
- package/dist/tui/App.js +63 -14
- package/dist/tui/ConfirmBar.js +45 -13
- package/dist/tui/InputBar.js +150 -35
- package/dist/tui/MessageList.js +266 -125
- package/dist/tui/ModelPicker.js +8 -3
- package/dist/tui/QuestionBar.js +51 -19
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +8 -14
- package/dist/tui/agent-runner.js +142 -22
- package/dist/tui/caret-pos.js +48 -5
- package/dist/tui/caret.js +1 -1
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/drag-state.js +8 -3
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +42 -13
- package/dist/tui/input-history.js +92 -11
- package/dist/tui/layout.js +75 -4
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +113 -21
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +22 -6
- package/dist/tui/slash-handler.js +233 -58
- package/dist/tui/text-width.js +38 -16
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +1 -1
- package/dist/tui/use-sgr-mouse.js +3 -1
- package/dist/tui-chat.js +276 -40
- package/dist/updater.js +88 -29
- package/dist/xml-search.js +194 -0
- package/docs/API.md +257 -25
- 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-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 +6 -1
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
|
@@ -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
|
+
}
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
export const SLASH_COMMANDS = [
|
|
2
|
-
{ name: "
|
|
2
|
+
{ name: "new", description: "新建会话(保留当前)" },
|
|
3
3
|
{ name: "compact", description: "压缩上下文" },
|
|
4
4
|
{ name: "model", description: "选择/切换模型" },
|
|
5
5
|
{ name: "models", description: "列出可用模型" },
|
|
6
6
|
{ name: "provider", description: "查看/切换 Provider", argHint: "[name]" },
|
|
7
|
-
{ name: "memory", description: "查看/保存记忆", argHint: "[t]" },
|
|
8
|
-
{ name: "tokens", description: "
|
|
9
|
-
{ name: "budget", description: "查看/设置预算上限", argHint: "[n]" },
|
|
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]" },
|
|
10
12
|
{ name: "undo", description: "撤销上一轮" },
|
|
11
13
|
{ name: "redo", description: "重发最后一条消息" },
|
|
12
14
|
{ name: "diff", description: "显示工作区未提交变更" },
|
|
13
15
|
{ name: "plan", description: "切换只读计划模式" },
|
|
14
|
-
{ name: "sessions", description: "
|
|
16
|
+
{ name: "sessions", description: "列出/切换已保存会话", argHint: "[id]", aliases: ["resume"] },
|
|
17
|
+
{ name: "reload", description: "重新加载规则" },
|
|
15
18
|
{ name: "rename", description: "重命名当前会话", argHint: "<title>" },
|
|
16
19
|
{ name: "attach", description: "附加本地图片", argHint: "<path>", aliases: ["image"] },
|
|
17
20
|
{ name: "mcp", description: "显示 MCP 状态" },
|
|
18
|
-
{
|
|
21
|
+
{
|
|
22
|
+
name: "skills",
|
|
23
|
+
description: "显示/启用/禁用 skills",
|
|
24
|
+
argHint: "[list|info <name>|enable|disable <name> [--project]]",
|
|
25
|
+
},
|
|
19
26
|
{ name: "path", description: "显示工作目录", aliases: ["pwd"] },
|
|
20
27
|
{ name: "paste", description: "粘贴剪贴板图片", argHint: "[t]" },
|
|
21
28
|
{ name: "help", description: "显示帮助" },
|
|
@@ -27,6 +34,15 @@ export function filterSlashCommands(input, commands = SLASH_COMMANDS) {
|
|
|
27
34
|
return commands;
|
|
28
35
|
return commands.filter((c) => c.name.startsWith(q) || (c.aliases ?? []).some((a) => a.startsWith(q)));
|
|
29
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
|
+
}
|
|
30
46
|
const argProviders = new Map();
|
|
31
47
|
/** Registered by the app at startup so this module stays free of app imports. */
|
|
32
48
|
export function setSlashArgProvider(command, provider) {
|
|
@@ -20,14 +20,9 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
|
|
|
20
20
|
case "q":
|
|
21
21
|
requestExit();
|
|
22
22
|
break;
|
|
23
|
-
case "
|
|
24
|
-
|
|
25
|
-
messages.length = 0;
|
|
26
|
-
tui.update({ messages: [] });
|
|
27
|
-
state.setUndoStack(clearStack(state.undoStack));
|
|
28
|
-
sysMsg(tui, "✓ 对话已清除");
|
|
23
|
+
case "new":
|
|
24
|
+
state.startNewSession();
|
|
29
25
|
break;
|
|
30
|
-
}
|
|
31
26
|
case "undo": {
|
|
32
27
|
const { popTurn } = await import("./undo-stack.js");
|
|
33
28
|
const popped = popTurn(state.undoStack);
|
|
@@ -55,7 +50,11 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
|
|
|
55
50
|
const { getWorkingTreeDiff } = await import("./diff-view.js");
|
|
56
51
|
const result = await getWorkingTreeDiff();
|
|
57
52
|
if (!result.ok) {
|
|
58
|
-
const msg = result.reason === "not_git"
|
|
53
|
+
const msg = result.reason === "not_git"
|
|
54
|
+
? "非 git 仓库,无法显示 diff"
|
|
55
|
+
: result.reason === "no_changes"
|
|
56
|
+
? "工作区无未提交变更"
|
|
57
|
+
: `diff 失败: ${result.message}`;
|
|
59
58
|
sysMsg(tui, msg);
|
|
60
59
|
}
|
|
61
60
|
else
|
|
@@ -67,20 +66,17 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
|
|
|
67
66
|
sysMsg(tui, state.planMode ? "✓ 计划模式已开启:仅只读工具可用,输出计划后再次输入 /plan 确认并执行" : "✓ 已退出计划模式");
|
|
68
67
|
break;
|
|
69
68
|
}
|
|
69
|
+
case "reload":
|
|
70
|
+
await state.reloadSystemPrompt();
|
|
71
|
+
break;
|
|
72
|
+
case "resume":
|
|
70
73
|
case "sessions": {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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>`);
|
|
74
|
+
const id = arg.trim();
|
|
75
|
+
if (!id) {
|
|
76
|
+
state.openSessionPicker();
|
|
77
|
+
break;
|
|
83
78
|
}
|
|
79
|
+
state.applySession(id);
|
|
84
80
|
break;
|
|
85
81
|
}
|
|
86
82
|
case "rename": {
|
|
@@ -92,7 +88,7 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
|
|
|
92
88
|
const { saveSession, renameSession } = await import("../sessions.js");
|
|
93
89
|
let id = state.sessionId;
|
|
94
90
|
if (!id) {
|
|
95
|
-
id = saveSession(messages);
|
|
91
|
+
id = saveSession(messages, undefined, undefined, undefined, state.taskState);
|
|
96
92
|
state.setSessionId(id);
|
|
97
93
|
}
|
|
98
94
|
if (renameSession(id, title))
|
|
@@ -119,7 +115,12 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
|
|
|
119
115
|
if (parts.length <= 1)
|
|
120
116
|
break;
|
|
121
117
|
messages.push({ role: "user", content: parts });
|
|
122
|
-
tui.addMessage({
|
|
118
|
+
tui.addMessage({
|
|
119
|
+
id: `user-${Date.now()}`,
|
|
120
|
+
role: "user",
|
|
121
|
+
content: `[📎 图片] ${filePath}`,
|
|
122
|
+
timestamp: Date.now(),
|
|
123
|
+
});
|
|
123
124
|
return "run";
|
|
124
125
|
}
|
|
125
126
|
case "compact": {
|
|
@@ -133,12 +134,17 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
|
|
|
133
134
|
sysMsg(tui, "⟳ 正在压缩上下文...");
|
|
134
135
|
const model = resolveModel(currentModel);
|
|
135
136
|
state.setUndoStack(clearStack(state.undoStack));
|
|
136
|
-
const result = await compactMessages(messages, model, {
|
|
137
|
+
const result = await compactMessages(messages, model, {
|
|
138
|
+
keepRecentTurns: 2,
|
|
139
|
+
taskGoal: state.taskState.goal || undefined,
|
|
140
|
+
});
|
|
137
141
|
messages.length = 0;
|
|
138
142
|
messages.push(...result.messages);
|
|
139
143
|
if (tracker)
|
|
140
144
|
tracker.resetContext();
|
|
141
145
|
sysMsg(tui, `✓ 已压缩 (约 ${estimateTokens(messages)} tokens)`);
|
|
146
|
+
const { snapshotTokenInfo } = await import("./token-info.js");
|
|
147
|
+
tui.setTokenInfo(await snapshotTokenInfo(tracker, currentModel, messages));
|
|
142
148
|
}
|
|
143
149
|
break;
|
|
144
150
|
}
|
|
@@ -209,23 +215,34 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
|
|
|
209
215
|
break;
|
|
210
216
|
}
|
|
211
217
|
case "memory": {
|
|
212
|
-
|
|
218
|
+
const { takeScopeFlags, defaultMemoryScope } = await import("../memory.js");
|
|
219
|
+
const { scope, rest: memRest } = takeScopeFlags(rest);
|
|
220
|
+
const text = memRest.join(" ").trim();
|
|
221
|
+
if (text) {
|
|
213
222
|
const { addMemory } = await import("../memory.js");
|
|
214
|
-
|
|
215
|
-
|
|
223
|
+
const resolved = scope ?? defaultMemoryScope();
|
|
224
|
+
addMemory(text, [], resolved);
|
|
225
|
+
sysMsg(tui, `✓ 已保存${resolved === "project" ? "项目" : "全局"}记忆: "${text}"`);
|
|
216
226
|
}
|
|
217
227
|
else {
|
|
218
228
|
const { loadMemories } = await import("../memory.js");
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
229
|
+
const scopes = scope ? [scope] : ["project", "global"];
|
|
230
|
+
const sections = scopes.map((s) => {
|
|
231
|
+
const memories = loadMemories(s);
|
|
232
|
+
const label = s === "project" ? "项目记忆" : "全局记忆";
|
|
233
|
+
if (memories.length === 0)
|
|
234
|
+
return `${label}: 无`;
|
|
223
235
|
const lines = memories.map((m, i) => {
|
|
224
236
|
const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
|
|
225
237
|
return ` #${i + 1}: ${m.content}${tags}`;
|
|
226
238
|
});
|
|
227
|
-
|
|
239
|
+
return `${label} (${memories.length}):\n${lines.join("\n")}`;
|
|
240
|
+
});
|
|
241
|
+
if (!scope && loadMemories("project").length === 0 && loadMemories("global").length === 0) {
|
|
242
|
+
sysMsg(tui, "无已保存的记忆");
|
|
228
243
|
}
|
|
244
|
+
else
|
|
245
|
+
sysMsg(tui, sections.join("\n"));
|
|
229
246
|
}
|
|
230
247
|
break;
|
|
231
248
|
}
|
|
@@ -242,46 +259,87 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
|
|
|
242
259
|
}
|
|
243
260
|
sysMsg(tui, `📎 剪贴板图片 (${(img.data.length / 1024).toFixed(1)} KB)`);
|
|
244
261
|
const text = arg || "What's in this image?";
|
|
245
|
-
const content = [
|
|
262
|
+
const content = [
|
|
263
|
+
{ type: "text", text },
|
|
264
|
+
{ type: "image", image: img.data, mimeType: img.mimeType },
|
|
265
|
+
];
|
|
246
266
|
messages.push({ role: "user", content });
|
|
247
267
|
tui.addMessage({ id: `user-${Date.now()}`, role: "user", content: `${text} [📎 图片]`, timestamp: Date.now() });
|
|
248
268
|
return "run";
|
|
249
269
|
}
|
|
250
270
|
case "tokens": {
|
|
251
|
-
const {
|
|
271
|
+
const { getContextWindowInfo } = await import("../context-window.js");
|
|
252
272
|
const { getModelPrice, estimateCost, formatCost } = await import("../pricing.js");
|
|
253
273
|
const { estimateTokens } = await import("../compaction.js");
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
274
|
+
const ctx = await getContextWindowInfo(currentModel);
|
|
275
|
+
const estimated = estimateTokens(messages);
|
|
276
|
+
const occupied = tracker.lastInputTokens > 0 ? tracker.lastInputTokens : estimated;
|
|
277
|
+
const pct = ctx.tokens > 0 && occupied > 0 ? Math.round((occupied / ctx.tokens) * 100) : 0;
|
|
278
|
+
const price = await getModelPrice(currentModel ?? "");
|
|
279
|
+
const cost = estimateCost({ inputTokens: tracker.totalInputTokens, outputTokens: tracker.totalOutputTokens }, price);
|
|
280
|
+
const occupancyNote = tracker.lastInputTokens > 0 ? "" : ",按对话内容估算";
|
|
281
|
+
const windowNote = ctx.source === "fallback" ? ",未读到接口窗口,按 512k 默认" : "";
|
|
282
|
+
sysMsg(tui, [
|
|
283
|
+
`上下文: ${occupied} / ${ctx.tokens} tokens (${pct}%${occupancyNote}${windowNote})`,
|
|
284
|
+
`累计: ${tracker.totalInputTokens} in / ${tracker.totalOutputTokens} out`,
|
|
285
|
+
...(cost != null ? [`成本: 约 ${formatCost(cost)}`] : []),
|
|
286
|
+
`消息: ${messages.length}`,
|
|
287
|
+
].join("\n"));
|
|
263
288
|
break;
|
|
264
289
|
}
|
|
265
290
|
case "budget": {
|
|
266
|
-
const {
|
|
291
|
+
const { takeScopeFlags } = await import("../memory.js");
|
|
292
|
+
const { getBudgetSnapshot, setBudgetMaxCost } = await import("../config.js");
|
|
267
293
|
const { getModelPrice, estimateCost, formatCost } = await import("../pricing.js");
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
if (
|
|
271
|
-
const n = parseFloat(
|
|
272
|
-
if (isNaN(n) || n <= 0) {
|
|
273
|
-
sysMsg(tui, "用法: /budget <USD 金额>");
|
|
294
|
+
const { scope, rest: budgetRest } = takeScopeFlags(rest);
|
|
295
|
+
const amount = budgetRest.join(" ").trim();
|
|
296
|
+
if (amount) {
|
|
297
|
+
const n = parseFloat(amount);
|
|
298
|
+
if (Number.isNaN(n) || n <= 0) {
|
|
299
|
+
sysMsg(tui, "用法: /budget <USD 金额> [--project]");
|
|
274
300
|
break;
|
|
275
301
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
sysMsg(tui, `✓ 预算上限已设为: $${n}
|
|
302
|
+
const resolved = scope === "project" ? "project" : "global";
|
|
303
|
+
setBudgetMaxCost(n, resolved);
|
|
304
|
+
sysMsg(tui, `✓ 预算上限已设为: $${n}(${resolved === "project" ? "项目" : "全局"})`);
|
|
279
305
|
break;
|
|
280
306
|
}
|
|
307
|
+
const snap = getBudgetSnapshot();
|
|
308
|
+
const maxCost = snap.maxCostUSD;
|
|
309
|
+
const sourceLabel = snap.source === "project" ? "项目" : snap.source === "global" ? "全局" : null;
|
|
281
310
|
const price = await getModelPrice(currentModel ?? "");
|
|
282
311
|
const cost = estimateCost({ inputTokens: tracker.totalInputTokens, outputTokens: tracker.totalOutputTokens }, price);
|
|
283
312
|
const costStr = cost != null ? `约 ${formatCost(cost)}` : "--";
|
|
284
|
-
|
|
313
|
+
const cap = maxCost != null ? `上限 $${maxCost}${sourceLabel ? `(${sourceLabel})` : ""}` : "未设置上限";
|
|
314
|
+
sysMsg(tui, `预算: 已用${costStr} / ${cap}\n设置: /budget <金额> 或 /budget <金额> --project`);
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
case "permission": {
|
|
318
|
+
const { takeScopeFlags } = await import("../memory.js");
|
|
319
|
+
const { getPermissionSnapshot, parsePermissionMode, permissionModeLabel, setPermissionMode } = await import("../config.js");
|
|
320
|
+
const { getPermissionOverride, setPermissionOverride } = await import("../confirm.js");
|
|
321
|
+
const { scope, rest: permRest } = takeScopeFlags(rest);
|
|
322
|
+
const sourceLabel = (source) => source === "cli" ? "本次启动参数" : source === "project" ? "项目" : source === "global" ? "全局" : "默认";
|
|
323
|
+
if (permRest.length === 0) {
|
|
324
|
+
const snap = getPermissionSnapshot();
|
|
325
|
+
const override = getPermissionOverride();
|
|
326
|
+
const mode = override ?? snap.permission;
|
|
327
|
+
const source = override ? "cli" : snap.source;
|
|
328
|
+
sysMsg(tui, [
|
|
329
|
+
`当前确认: ${permissionModeLabel(mode)}(${sourceLabel(source)})`,
|
|
330
|
+
"设置: /permission ask|accept-edits|allow-all [--project]",
|
|
331
|
+
].join("\n"));
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
const parsed = parsePermissionMode(permRest[0]);
|
|
335
|
+
if (!parsed || permRest.length > 1) {
|
|
336
|
+
sysMsg(tui, "用法: /permission ask|accept-edits|allow-all [--project]");
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
const resolved = scope === "project" ? "project" : "global";
|
|
340
|
+
setPermissionMode(parsed, resolved);
|
|
341
|
+
setPermissionOverride(parsed);
|
|
342
|
+
sysMsg(tui, `✓ 确认已设为${permissionModeLabel(parsed)}(${resolved === "project" ? "项目" : "全局"})`);
|
|
285
343
|
break;
|
|
286
344
|
}
|
|
287
345
|
case "mcp": {
|
|
@@ -298,9 +356,16 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
|
|
|
298
356
|
const st = status[name];
|
|
299
357
|
const connected = st?.connected ?? false;
|
|
300
358
|
const toolCount = st?.tools.length ?? 0;
|
|
359
|
+
const extra = [
|
|
360
|
+
toolCount > 0 ? `${toolCount} tools` : "",
|
|
361
|
+
(st?.resource_count ?? 0) > 0 ? `${st.resource_count} resources` : "",
|
|
362
|
+
(st?.prompt_count ?? 0) > 0 ? `${st.prompt_count} prompts` : "",
|
|
363
|
+
]
|
|
364
|
+
.filter(Boolean)
|
|
365
|
+
.join(", ");
|
|
301
366
|
const state = disabled ? "disabled" : connected ? "connected" : "disconnected";
|
|
302
367
|
const err = !disabled && !connected && st?.error ? ` — ${st.error}` : "";
|
|
303
|
-
return ` ${name}: ${state}${
|
|
368
|
+
return ` ${name}: ${state}${extra ? ` (${extra})` : ""}${err}`;
|
|
304
369
|
});
|
|
305
370
|
sysMsg(tui, `MCP 服务器 (${servers.length}):\n${lines.join("\n")}`);
|
|
306
371
|
}
|
|
@@ -326,7 +391,9 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
|
|
|
326
391
|
break;
|
|
327
392
|
}
|
|
328
393
|
const results = setSkillEnabled(names, action === "enable", scope);
|
|
329
|
-
const notes = results
|
|
394
|
+
const notes = results
|
|
395
|
+
.filter((r) => r.blockedBy)
|
|
396
|
+
.map((r) => ` ⚠ ${r.name} 仍为${r.enabled ? "启用" : "禁用"}(被 ${r.blockedBy} 范围覆盖)`);
|
|
330
397
|
sysMsg(tui, [`✓ 已${action === "enable" ? "启用" : "禁用"}(${scope}): ${names.join(", ")}`, ...notes].join("\n"));
|
|
331
398
|
break;
|
|
332
399
|
}
|
|
@@ -338,7 +405,16 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
|
|
|
338
405
|
break;
|
|
339
406
|
}
|
|
340
407
|
const status = skill.enabled ? "enabled" : `disabled(${skill.disabledScope === "project" ? "proj" : "glob"})`;
|
|
341
|
-
const meta = [
|
|
408
|
+
const meta = [
|
|
409
|
+
`Name: ${skill.name}`,
|
|
410
|
+
`Status: ${status}`,
|
|
411
|
+
...(skill.builtin ? ["Source: 内置"] : []),
|
|
412
|
+
...(skill.version ? [`Version: ${skill.version}`] : []),
|
|
413
|
+
`Description: ${skill.description.replace(/\s+/g, " ")}`,
|
|
414
|
+
`Location: ${skill.location}`,
|
|
415
|
+
...(skill.allowedTools.length ? [`Tools: ${skill.allowedTools.join(", ")}`] : []),
|
|
416
|
+
...(skill.requiredBins.length ? [`Requires: ${skill.requiredBins.join(", ")}`] : []),
|
|
417
|
+
];
|
|
342
418
|
const content = skill.content.trim();
|
|
343
419
|
sysMsg(tui, [...meta, ...(content ? ["", "--- Content ---", "", content.slice(0, 4000)] : [])].join("\n"));
|
|
344
420
|
break;
|
|
@@ -354,15 +430,114 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
|
|
|
354
430
|
}
|
|
355
431
|
const lines = all.map((s) => {
|
|
356
432
|
const status = s.enabled ? "enabled" : `disabled(${s.disabledScope === "project" ? "proj" : "glob"})`;
|
|
357
|
-
|
|
433
|
+
const origin = s.builtin ? " 内置" : "";
|
|
434
|
+
return ` ${s.name} ${status}${origin} ${truncateDisplay(s.description.replace(/\s+/g, " "), 56)}`;
|
|
358
435
|
});
|
|
359
436
|
if (stale.length > 0)
|
|
360
437
|
lines.push(` ⚠ disabledSkills/enabledSkills 中无对应 skill: ${stale.join(", ")}`);
|
|
361
438
|
sysMsg(tui, `Skills (${all.filter((s) => s.enabled).length} enabled / ${all.length}):\n${lines.join("\n")}\n切换: /skills disable <name> [--project]`);
|
|
362
439
|
break;
|
|
363
440
|
}
|
|
441
|
+
case "sandbox": {
|
|
442
|
+
const { takeScopeFlags } = await import("../memory.js");
|
|
443
|
+
const { getSandboxSnapshot, setSandboxConfig } = await import("../config.js");
|
|
444
|
+
const { parseSandboxMode, parseNetworkPolicy, getEffectiveSandboxPolicy, sandboxEnforcementCaveat, sandboxModeLabel, setSandboxOverride, } = await import("../sandbox.js");
|
|
445
|
+
const { scope, rest: sbRest } = takeScopeFlags(rest);
|
|
446
|
+
const sourceLabel = (source) => source === "cli"
|
|
447
|
+
? "本次启动参数"
|
|
448
|
+
: source === "env"
|
|
449
|
+
? "环境变量"
|
|
450
|
+
: source === "project"
|
|
451
|
+
? "项目"
|
|
452
|
+
: source === "global"
|
|
453
|
+
? "全局"
|
|
454
|
+
: "默认";
|
|
455
|
+
if (sbRest.length === 0) {
|
|
456
|
+
const snap = getSandboxSnapshot();
|
|
457
|
+
const extras = [
|
|
458
|
+
...(snap.extraWriteRoots.length > 0 ? [`额外可写目录: ${snap.extraWriteRoots.join(", ")}`] : []),
|
|
459
|
+
...(snap.extraReadRoots.length > 0 ? [`额外可读目录: ${snap.extraReadRoots.join(", ")}`] : []),
|
|
460
|
+
];
|
|
461
|
+
const caveat = sandboxEnforcementCaveat(getEffectiveSandboxPolicy().mode);
|
|
462
|
+
sysMsg(tui, [
|
|
463
|
+
`当前隔离: ${snap.label}(${sourceLabel(snap.source)})`,
|
|
464
|
+
...extras,
|
|
465
|
+
...(caveat ? [caveat] : []),
|
|
466
|
+
"设置: /sandbox off|workspace|strict [--project]",
|
|
467
|
+
" /sandbox network allow|deny [--project]",
|
|
468
|
+
].join("\n"));
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
const resolved = scope === "project" ? "project" : "global";
|
|
472
|
+
const scopeLabel = resolved === "project" ? "项目" : "全局";
|
|
473
|
+
if (sbRest[0] === "network") {
|
|
474
|
+
const parsed = parseNetworkPolicy(sbRest[1]);
|
|
475
|
+
if (!parsed || sbRest.length > 2) {
|
|
476
|
+
sysMsg(tui, "用法: /sandbox network allow|deny [--project]");
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
setSandboxConfig({ network: parsed }, resolved);
|
|
480
|
+
setSandboxOverride({ network: parsed });
|
|
481
|
+
sysMsg(tui, `✓ 联网已设为${parsed === "deny" ? "禁止" : "允许"}(${scopeLabel})`);
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
const parsed = parseSandboxMode(sbRest[0]);
|
|
485
|
+
if (!parsed || sbRest.length > 1) {
|
|
486
|
+
sysMsg(tui, "用法: /sandbox off|workspace|strict [--project]\n /sandbox network allow|deny [--project]");
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
setSandboxConfig({ mode: parsed }, resolved);
|
|
490
|
+
setSandboxOverride({ mode: parsed });
|
|
491
|
+
const caveat = sandboxEnforcementCaveat(parsed);
|
|
492
|
+
sysMsg(tui, [`✓ 隔离已设为${sandboxModeLabel(parsed)}(${scopeLabel})`, ...(caveat ? [caveat] : [])].join("\n"));
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
364
495
|
case "help":
|
|
365
|
-
sysMsg(tui, [
|
|
496
|
+
sysMsg(tui, [
|
|
497
|
+
"命令:",
|
|
498
|
+
" 输入 / 弹出命令菜单 (输入过滤, Tab/Enter 执行, Esc 关闭)",
|
|
499
|
+
" /new 新建会话(保留当前)",
|
|
500
|
+
" /compact 压缩上下文",
|
|
501
|
+
" /model [n] 交互式选择/切换模型",
|
|
502
|
+
" /models 列出可用模型",
|
|
503
|
+
" /provider [n] 查看/切换 Provider",
|
|
504
|
+
" /memory [t] 查看/保存记忆(--project 写入当前项目)",
|
|
505
|
+
" /tokens 显示上下文占用、累计用量与成本",
|
|
506
|
+
" /budget [n] 查看/设置预算上限(--project 写入当前项目)",
|
|
507
|
+
" /permission 查看/设置确认模式(ask|accept-edits|allow-all,--project 写入当前项目)",
|
|
508
|
+
" /sandbox 查看/设置隔离(off|workspace|strict,--project 写入当前项目)",
|
|
509
|
+
" /undo 撤销上一轮",
|
|
510
|
+
" /redo 重发最后一条消息",
|
|
511
|
+
" /diff 显示工作区未提交变更",
|
|
512
|
+
" /plan 切换只读计划模式",
|
|
513
|
+
" /sessions 列出/切换已保存会话",
|
|
514
|
+
" /resume [id] 同 /sessions",
|
|
515
|
+
" /reload 重新加载规则",
|
|
516
|
+
" /rename <t> 重命名当前会话",
|
|
517
|
+
" /attach <p> 附加本地图片",
|
|
518
|
+
" /mcp 显示 MCP 状态",
|
|
519
|
+
" /skills 显示/启用/禁用 skills(/skills info <name> 查看详情)",
|
|
520
|
+
" /path 显示工作目录",
|
|
521
|
+
" /paste [t] 粘贴剪贴板图片 + 可选提示",
|
|
522
|
+
" /help 显示帮助",
|
|
523
|
+
" /exit 退出",
|
|
524
|
+
"",
|
|
525
|
+
"快捷键:",
|
|
526
|
+
" Esc 取消当前任务",
|
|
527
|
+
" Ctrl+J 输入框换行",
|
|
528
|
+
" Ctrl+C 清空输入(↑↓ 可找回);生成中则中断,空输入连按两次退出",
|
|
529
|
+
" Ctrl+B 消息区选择模式",
|
|
530
|
+
" Home/End 行首/行尾",
|
|
531
|
+
" PageUp/Down 滚动消息",
|
|
532
|
+
" 鼠标滚轮 滚动消息",
|
|
533
|
+
" 鼠标拖拽 选择文本并自动复制",
|
|
534
|
+
" 鼠标双击 选词并复制",
|
|
535
|
+
" 鼠标三击 选中该条消息并复制",
|
|
536
|
+
" 鼠标点击 工具调用行展开/收起",
|
|
537
|
+
" ↑↓ 翻阅输入历史",
|
|
538
|
+
" ← → 移动输入框光标",
|
|
539
|
+
" 鼠标点击 输入框内定位光标",
|
|
540
|
+
].join("\n"));
|
|
366
541
|
break;
|
|
367
542
|
default:
|
|
368
543
|
sysMsg(tui, `未知命令: /${cmd},输入 /help 查看可用命令`);
|
package/dist/tui/text-width.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Terminal display-width helpers.
|
|
3
3
|
*
|
|
4
4
|
* CJK / fullwidth characters occupy two terminal cells, combining marks and
|
|
5
|
-
* variation selectors occupy none.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* variation selectors occupy none. Width is measured per grapheme cluster so
|
|
6
|
+
* ZWJ emoji (and Ink's own layout, which uses the same cluster rules) stay
|
|
7
|
+
* aligned with the mouse column.
|
|
8
8
|
*/
|
|
9
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
10
|
+
const EXT_PICT = /\p{Extended_Pictographic}/u;
|
|
9
11
|
/** Zero-width: combining marks, variation selectors, zero-width spaces. */
|
|
10
12
|
function isZeroWidth(cp) {
|
|
11
13
|
return ((cp >= 0x0300 && cp <= 0x036f) ||
|
|
@@ -46,44 +48,64 @@ function isFullWidth(cp) {
|
|
|
46
48
|
(cp >= 0x20000 && cp <= 0x2fffd) ||
|
|
47
49
|
(cp >= 0x30000 && cp <= 0x3fffd));
|
|
48
50
|
}
|
|
49
|
-
|
|
50
|
-
export function charWidth(char) {
|
|
51
|
+
function codePointWidth(char) {
|
|
51
52
|
const cp = char.codePointAt(0);
|
|
52
53
|
if (cp === undefined)
|
|
53
54
|
return 0;
|
|
54
|
-
// C0 / C1 control characters render nothing useful.
|
|
55
55
|
if (cp < 0x20 || (cp >= 0x7f && cp < 0xa0))
|
|
56
56
|
return 0;
|
|
57
57
|
if (isZeroWidth(cp))
|
|
58
58
|
return 0;
|
|
59
|
-
|
|
59
|
+
if (isFullWidth(cp) || EXT_PICT.test(char))
|
|
60
|
+
return 2;
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
/** Display width of a grapheme cluster (or a single code point), in terminal cells. */
|
|
64
|
+
export function clusterWidth(cluster) {
|
|
65
|
+
if (cluster.length === 0)
|
|
66
|
+
return 0;
|
|
67
|
+
if (EXT_PICT.test(cluster) || cluster.includes("\u200D"))
|
|
68
|
+
return 2;
|
|
69
|
+
let width = 0;
|
|
70
|
+
for (const ch of cluster)
|
|
71
|
+
width += codePointWidth(ch);
|
|
72
|
+
return cluster.includes("\uFE0F") ? Math.max(width, 2) : width;
|
|
73
|
+
}
|
|
74
|
+
/** Display width of a single code point, in terminal cells. */
|
|
75
|
+
export function charWidth(char) {
|
|
76
|
+
return clusterWidth(char);
|
|
77
|
+
}
|
|
78
|
+
export function* clustersOf(text) {
|
|
79
|
+
for (const { segment } of segmenter.segment(text)) {
|
|
80
|
+
yield { text: segment, width: clusterWidth(segment) };
|
|
81
|
+
}
|
|
60
82
|
}
|
|
61
83
|
/** Display width of a string (single logical line, no ANSI escapes). */
|
|
62
84
|
export function displayWidth(text) {
|
|
63
85
|
let width = 0;
|
|
64
|
-
for (const
|
|
65
|
-
width +=
|
|
86
|
+
for (const { width: w } of clustersOf(text))
|
|
87
|
+
width += w;
|
|
66
88
|
return width;
|
|
67
89
|
}
|
|
68
90
|
/**
|
|
69
91
|
* Break a single logical line into visual rows of at most `maxWidth` cells.
|
|
70
|
-
* Breaks on
|
|
71
|
-
* derived exactly — a fullwidth character is never split across rows.
|
|
92
|
+
* Breaks on cluster boundaries (not word boundaries) so the caret column can
|
|
93
|
+
* be derived exactly — a fullwidth character or emoji is never split across rows.
|
|
72
94
|
*/
|
|
73
95
|
export function wrapByWidth(line, maxWidth) {
|
|
96
|
+
const text = typeof line === "string" ? line : line == null ? "" : String(line);
|
|
74
97
|
if (maxWidth < 1)
|
|
75
|
-
return [
|
|
98
|
+
return [text];
|
|
76
99
|
const rows = [];
|
|
77
100
|
let current = "";
|
|
78
101
|
let width = 0;
|
|
79
|
-
for (const
|
|
80
|
-
|
|
81
|
-
if (width + w > maxWidth) {
|
|
102
|
+
for (const { text: g, width: w } of clustersOf(text)) {
|
|
103
|
+
if (width + w > maxWidth && current.length > 0) {
|
|
82
104
|
rows.push(current);
|
|
83
105
|
current = "";
|
|
84
106
|
width = 0;
|
|
85
107
|
}
|
|
86
|
-
current +=
|
|
108
|
+
current += g;
|
|
87
109
|
width += w;
|
|
88
110
|
}
|
|
89
111
|
rows.push(current);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { estimateTokens } from "../compaction.js";
|
|
2
|
+
import { getContextWindowInfo } from "../context-window.js";
|
|
3
|
+
import { tokenInfoFromTracker } from "../token-display.js";
|
|
4
|
+
export async function snapshotTokenInfo(tracker, modelId, messages) {
|
|
5
|
+
const ctx = await getContextWindowInfo(modelId);
|
|
6
|
+
return tokenInfoFromTracker(tracker, ctx.tokens, estimateTokens(messages));
|
|
7
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function mergeToolChild(messages, parentToolCallId, child) {
|
|
2
|
+
let idx = -1;
|
|
3
|
+
if (parentToolCallId)
|
|
4
|
+
idx = messages.findIndex((m) => m.role === "tool" && m.toolCallId === parentToolCallId);
|
|
5
|
+
if (idx === -1) {
|
|
6
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
7
|
+
if (messages[i].role === "tool" && messages[i].toolName === "task") {
|
|
8
|
+
idx = i;
|
|
9
|
+
break;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (idx === -1)
|
|
14
|
+
return messages;
|
|
15
|
+
const target = messages[idx];
|
|
16
|
+
const next = messages.slice();
|
|
17
|
+
next[idx] = { ...target, toolChildren: [...(target.toolChildren ?? []), child] };
|
|
18
|
+
return next;
|
|
19
|
+
}
|
package/dist/tui/undo-stack.js
CHANGED