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.
Files changed (137) hide show
  1. package/README.md +242 -31
  2. package/dist/agent.js +1233 -485
  3. package/dist/assistant-stream.js +11 -7
  4. package/dist/cli/commands/chat.js +10 -0
  5. package/dist/cli/commands/exec.js +32 -0
  6. package/dist/cli/commands/history.js +58 -0
  7. package/dist/cli/commands/index.js +224 -0
  8. package/dist/cli/commands/init.js +18 -0
  9. package/dist/cli/commands/mcp.js +173 -0
  10. package/dist/cli/commands/memory.js +69 -0
  11. package/dist/cli/commands/models.js +21 -0
  12. package/dist/cli/commands/permission.js +12 -0
  13. package/dist/cli/commands/rules.js +33 -0
  14. package/dist/cli/commands/sandbox.js +13 -0
  15. package/dist/cli/commands/serve.js +9 -0
  16. package/dist/cli/commands/setup.js +4 -0
  17. package/dist/cli/commands/shared.js +16 -0
  18. package/dist/cli/commands/skills.js +119 -0
  19. package/dist/cli/commands/update.js +7 -0
  20. package/dist/cli/commands/write-config.js +30 -0
  21. package/dist/cli/errors.js +36 -0
  22. package/dist/cli/exec-prompt.js +26 -0
  23. package/dist/cli/option-helpers.js +53 -0
  24. package/dist/cli/program.js +180 -0
  25. package/dist/cli.js +7 -632
  26. package/dist/clipboard.js +59 -23
  27. package/dist/code-mode.js +35 -17
  28. package/dist/compaction.js +457 -169
  29. package/dist/config.js +298 -38
  30. package/dist/confirm.js +105 -9
  31. package/dist/context-window.js +156 -75
  32. package/dist/doom-loop.js +268 -26
  33. package/dist/fetch-timeout.js +152 -0
  34. package/dist/http-approvals.js +60 -0
  35. package/dist/http.js +119 -0
  36. package/dist/instructions.js +72 -33
  37. package/dist/logger.js +95 -0
  38. package/dist/markdown.js +35 -50
  39. package/dist/mcp.js +847 -102
  40. package/dist/memory.js +128 -45
  41. package/dist/output.js +42 -31
  42. package/dist/paste-handler.js +3 -3
  43. package/dist/permission-cli.js +43 -0
  44. package/dist/plugins.js +76 -11
  45. package/dist/pricing.js +119 -0
  46. package/dist/provider.js +34 -15
  47. package/dist/question-format.js +60 -0
  48. package/dist/sandbox-cli.js +82 -0
  49. package/dist/sandbox.js +403 -0
  50. package/dist/save-throttle.js +45 -0
  51. package/dist/serve/common.js +404 -0
  52. package/dist/serve/routes-chat.js +347 -0
  53. package/dist/serve/routes-mcp.js +212 -0
  54. package/dist/serve/routes-memory.js +66 -0
  55. package/dist/serve/routes-meta.js +205 -0
  56. package/dist/serve/routes-sessions.js +61 -0
  57. package/dist/serve/routes-skills.js +70 -0
  58. package/dist/serve.js +74 -635
  59. package/dist/sessions.js +197 -15
  60. package/dist/skills.js +531 -77
  61. package/dist/synthetic.js +7 -0
  62. package/dist/title-gen.js +9 -2
  63. package/dist/token-display.js +36 -0
  64. package/dist/tool-display.js +178 -0
  65. package/dist/tool-output.js +53 -46
  66. package/dist/tools/apply_patch.js +265 -0
  67. package/dist/tools/atomic-file.js +35 -0
  68. package/dist/tools/backend.js +61 -0
  69. package/dist/tools/bash.js +186 -71
  70. package/dist/tools/code_search.js +13 -6
  71. package/dist/tools/edit.js +26 -9
  72. package/dist/tools/explore.js +144 -16
  73. package/dist/tools/glob.js +7 -3
  74. package/dist/tools/grep.js +153 -14
  75. package/dist/tools/index.js +9 -24
  76. package/dist/tools/question.js +31 -30
  77. package/dist/tools/read.js +77 -15
  78. package/dist/tools/search-searxng.js +223 -0
  79. package/dist/tools/search-serper.js +189 -0
  80. package/dist/tools/task.js +100 -33
  81. package/dist/tools/todo.js +178 -67
  82. package/dist/tools/web_fetch.js +158 -46
  83. package/dist/tools/web_search.js +217 -29
  84. package/dist/tools/write.js +34 -11
  85. package/dist/tui/App.js +89 -6
  86. package/dist/tui/ConfirmBar.js +57 -4
  87. package/dist/tui/InputBar.js +504 -44
  88. package/dist/tui/MessageList.js +674 -20
  89. package/dist/tui/ModelPicker.js +113 -0
  90. package/dist/tui/QuestionBar.js +136 -0
  91. package/dist/tui/SessionPicker.js +79 -0
  92. package/dist/tui/StatusBar.js +14 -12
  93. package/dist/tui/agent-runner.js +223 -0
  94. package/dist/tui/caret-pos.js +177 -0
  95. package/dist/tui/caret.js +69 -0
  96. package/dist/tui/click-count.js +13 -0
  97. package/dist/tui/diff-view.js +61 -0
  98. package/dist/tui/drag-state.js +49 -0
  99. package/dist/tui/hydrate.js +129 -0
  100. package/dist/tui/index.js +189 -31
  101. package/dist/tui/input-history.js +125 -0
  102. package/dist/tui/layout.js +88 -0
  103. package/dist/tui/mouse.js +46 -0
  104. package/dist/tui/prompt-queue.js +24 -0
  105. package/dist/tui/selection.js +226 -0
  106. package/dist/tui/session-switch.js +28 -0
  107. package/dist/tui/slash-commands.js +106 -0
  108. package/dist/tui/slash-handler.js +545 -0
  109. package/dist/tui/text-width.js +113 -0
  110. package/dist/tui/theme.js +12 -0
  111. package/dist/tui/token-info.js +7 -0
  112. package/dist/tui/tool-children.js +19 -0
  113. package/dist/tui/undo-stack.js +14 -0
  114. package/dist/tui/use-sgr-mouse.js +29 -0
  115. package/dist/tui-chat.js +346 -330
  116. package/dist/updater.js +116 -0
  117. package/dist/xml-search.js +194 -0
  118. package/docs/API.md +410 -32
  119. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
  120. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
  121. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
  122. package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
  123. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
  124. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
  125. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
  126. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
  127. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
  128. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
  129. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
  130. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
  131. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
  132. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
  133. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
  134. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
  135. package/package.json +12 -8
  136. package/skills/self-config/SKILL.md +90 -0
  137. package/skills/self-config/reference.md +149 -0
package/dist/tui-chat.js CHANGED
@@ -1,61 +1,181 @@
1
1
  import { TuiRenderer } from "./tui/index.js";
2
- import { initMcp, shutdownMcp, getMcpStatus, loadMcpConfig } from "./mcp.js";
3
- import { discoverSkills, getSkills } from "./skills.js";
2
+ import { initMcp, shutdownMcp } from "./mcp.js";
3
+ import { discoverSkills, getAllSkills, getStaleDisabledSkills } from "./skills.js";
4
+ import { setSlashArgProvider } from "./tui/slash-commands.js";
4
5
  import { loadInstructions } from "./instructions.js";
5
- import { TokenTracker, estimateTokens, compactMessages } from "./compaction.js";
6
- import { runOnce, runOnceWithSystem } from "./agent.js";
6
+ import { TokenTracker } from "./compaction.js";
7
+ import { emptyTaskState, copyTaskState, resetTaskState } from "./tools/todo.js";
8
+ import { loadImageParts } from "./agent.js";
9
+ import { createUndoStack, clearStack } from "./tui/undo-stack.js";
7
10
  import { killActiveProcesses } from "./tools/bash.js";
8
- import { setTuiConfirm } from "./confirm.js";
9
- import { getContextWindow } from "./context-window.js";
10
- import { loadConfig } from "./config.js";
11
- import { resolveModel } from "./provider.js";
11
+ import { setTuiConfirm, setTuiQuestion } from "./confirm.js";
12
+ import { loadConfig, getActiveProvider } from "./config.js";
12
13
  import { scanProject, buildCodeSystemPrompt } from "./code-mode.js";
14
+ import { handleSlashCommand, sysMsg } from "./tui/slash-handler.js";
15
+ import { createAgentRunner } from "./tui/agent-runner.js";
16
+ import { listSessions, loadSession, saveSession } from "./sessions.js";
17
+ import { hydrateMessages, lastUserText } from "./tui/hydrate.js";
18
+ import { createPromptQueue } from "./tui/prompt-queue.js";
19
+ import { prepareSwitch, switchFailMessage, prepareNew, newFailMessage } from "./tui/session-switch.js";
13
20
  let confirmResolver = null;
21
+ let questionResolver = null;
22
+ /** Only one confirm/question overlay can be shown at a time; parallel tool calls queue up. */
23
+ const promptQueue = createPromptQueue();
24
+ function registerSessionCompletion() {
25
+ const provider = () => listSessions().map((s) => ({ value: s.id, description: s.title }));
26
+ setSlashArgProvider("sessions", provider);
27
+ setSlashArgProvider("resume", provider);
28
+ }
29
+ function registerPermissionCompletion() {
30
+ setSlashArgProvider("permission", (tokens) => {
31
+ const flags = [
32
+ { value: "--project", description: "写入当前项目" },
33
+ { value: "--global", description: "写入全局配置" },
34
+ ];
35
+ const used = new Set(tokens);
36
+ const remainingFlags = flags.filter((f) => !used.has(f.value));
37
+ if (tokens.length === 0) {
38
+ return [
39
+ { value: "ask", description: "危险操作前询问(默认)" },
40
+ { value: "accept-edits", description: "自动允许改文件,危险命令仍询问" },
41
+ { value: "allow-all", description: "全部自动允许" },
42
+ ...remainingFlags,
43
+ ];
44
+ }
45
+ return remainingFlags;
46
+ });
47
+ }
48
+ function registerSandboxCompletion() {
49
+ setSlashArgProvider("sandbox", (tokens) => {
50
+ const flags = [
51
+ { value: "--project", description: "写入当前项目" },
52
+ { value: "--global", description: "写入全局配置" },
53
+ ];
54
+ const used = new Set(tokens);
55
+ const remainingFlags = flags.filter((f) => !used.has(f.value));
56
+ if (tokens.length === 0) {
57
+ return [
58
+ { value: "off", description: "不隔离(默认)" },
59
+ { value: "workspace", description: "只允许写入工作目录" },
60
+ { value: "strict", description: "读写都限制在工作目录" },
61
+ { value: "network", description: "设置是否允许联网" },
62
+ ...remainingFlags,
63
+ ];
64
+ }
65
+ if (tokens[0] === "network") {
66
+ const nets = [
67
+ { value: "allow", description: "允许联网" },
68
+ { value: "deny", description: "禁止联网" },
69
+ ];
70
+ return [...nets.filter((n) => !used.has(n.value)), ...remainingFlags];
71
+ }
72
+ return remainingFlags;
73
+ });
74
+ }
75
+ function registerSkillCompletion() {
76
+ setSlashArgProvider("skills", (tokens) => {
77
+ const action = tokens[0];
78
+ if (action === undefined) {
79
+ return [
80
+ { value: "list", description: "列出全部 skills" },
81
+ { value: "info", description: "查看 skill 详情" },
82
+ { value: "enable", description: "启用 skill" },
83
+ { value: "disable", description: "禁用 skill" },
84
+ ];
85
+ }
86
+ if (action === "info")
87
+ return getAllSkills().map((s) => ({ value: s.name, description: s.description }));
88
+ if (action !== "enable" && action !== "disable")
89
+ return [];
90
+ const all = getAllSkills();
91
+ const pool = action === "enable"
92
+ ? [
93
+ ...all.filter((s) => !s.enabled).map((s) => ({ value: s.name, description: s.description })),
94
+ ...getStaleDisabledSkills().map((name) => ({ value: name, description: "(配置中的孤儿条目)" })),
95
+ ]
96
+ : all.filter((s) => s.enabled).map((s) => ({ value: s.name, description: s.description }));
97
+ return [
98
+ ...pool.filter((o) => !tokens.includes(o.value)),
99
+ ...(tokens.includes("--project") ? [] : [{ value: "--project", description: "仅对当前项目生效" }]),
100
+ ];
101
+ });
102
+ }
14
103
  export async function runTui(opts) {
15
- const { modelId, resumeSessionId, mode } = opts;
104
+ const { modelId, resumeSessionId, images, providerName, initialPrompt } = opts;
16
105
  await initMcp();
17
106
  discoverSkills();
107
+ registerSkillCompletion();
108
+ registerPermissionCompletion();
109
+ registerSandboxCompletion();
110
+ registerSessionCompletion();
18
111
  const instructions = await loadInstructions();
19
112
  const config = loadConfig();
20
- // Code mode: scan project and build code-specific system prompt
21
- let codeSystemPrompt;
22
- if (mode === "code") {
23
- const project = scanProject();
24
- codeSystemPrompt = buildCodeSystemPrompt(project, instructions);
25
- }
113
+ const project = scanProject();
114
+ let systemPrompt = buildCodeSystemPrompt(project, instructions);
26
115
  let messages = [];
27
116
  let sessionId = resumeSessionId;
117
+ let currentModel = modelId ?? getActiveProvider(config)?.defaultModel;
118
+ const currentProvider = providerName;
28
119
  const tracker = new TokenTracker();
29
- let abortController = null;
30
- // Resume session if requested
120
+ const taskState = emptyTaskState();
121
+ let undoStack = createUndoStack();
122
+ let lastUserMessage = null;
123
+ let planMode = false;
31
124
  if (resumeSessionId) {
32
- const { loadSession } = await import("./sessions.js");
33
125
  const session = loadSession(resumeSessionId);
34
126
  if (session) {
35
127
  messages = session.messages;
36
128
  sessionId = resumeSessionId;
129
+ copyTaskState(session.taskState, taskState);
37
130
  }
38
131
  }
39
- // Forward reference — assigned after tui is created
40
- let executeAgent;
132
+ let runner = null;
41
133
  const tui = new TuiRenderer({
42
134
  onSubmit: async (text) => {
43
135
  if (text.startsWith("/")) {
44
- const result = await handleSlashCommand(text, tui, messages, modelId, tracker);
45
- if (result === "run") {
46
- await executeAgent();
136
+ const result = await handleSlashCommand(text, tui, messages, currentModel, tracker, (model) => {
137
+ currentModel = model;
138
+ tui.setModel(model);
139
+ }, requestExit, {
140
+ undoStack,
141
+ setUndoStack: (s) => {
142
+ undoStack = s;
143
+ },
144
+ lastUserMessage,
145
+ setLastUserMessage: (t) => {
146
+ lastUserMessage = t;
147
+ },
148
+ sessionId,
149
+ setSessionId: (id) => {
150
+ sessionId = id;
151
+ },
152
+ planMode,
153
+ togglePlanMode: () => {
154
+ planMode = !planMode;
155
+ },
156
+ reloadSystemPrompt: async () => {
157
+ const next = await loadInstructions();
158
+ systemPrompt = buildCodeSystemPrompt(scanProject(), next);
159
+ sysMsg(tui, `✓ 已重载规则(${next.join("").length} 字符)`);
160
+ },
161
+ applySession,
162
+ startNewSession,
163
+ openSessionPicker,
164
+ taskState,
165
+ });
166
+ if (result === "run" && runner) {
167
+ await runner.run();
168
+ persistSession();
47
169
  }
48
170
  return;
49
171
  }
50
- // Add user message
51
- tui.addMessage({
52
- id: `user-${Date.now()}`,
53
- role: "user",
54
- content: text,
55
- timestamp: Date.now(),
56
- });
172
+ lastUserMessage = text;
173
+ tui.addMessage({ id: `user-${Date.now()}`, role: "user", content: text, timestamp: Date.now() });
57
174
  messages.push({ role: "user", content: text });
58
- await executeAgent();
175
+ if (runner) {
176
+ await runner.run();
177
+ persistSession();
178
+ }
59
179
  },
60
180
  onConfirm: (accepted) => {
61
181
  tui.hideConfirm();
@@ -64,321 +184,217 @@ export async function runTui(opts) {
64
184
  confirmResolver = null;
65
185
  }
66
186
  },
67
- onExit: () => {
68
- if (abortController) {
69
- killActiveProcesses();
70
- abortController.abort();
71
- abortController = null;
72
- tui.setRunning(false);
73
- tui.addMessage({
74
- id: `sys-${Date.now()}`,
75
- role: "system",
76
- content: "(已取消)",
77
- timestamp: Date.now(),
78
- });
187
+ onQuestionAnswer: (answer) => {
188
+ tui.hideQuestion();
189
+ if (questionResolver) {
190
+ questionResolver(answer);
191
+ questionResolver = null;
79
192
  }
80
193
  },
81
- }, modelId ?? config.provider?.defaultModel);
82
- // Define the agent execution function (uses tui, messages, etc. from closure)
83
- executeAgent = async () => {
84
- abortController = new AbortController();
85
- tui.setRunning(true, "思考中...");
86
- const callbacks = {
87
- onAssistantDisplayDelta: (delta) => {
88
- // Pass raw delta — markdown formatting is applied at display time
89
- tui.appendToLast(delta);
90
- },
91
- onThinkingDelta: (delta) => {
92
- tui.setRunning(true, "推理中...");
93
- tui.appendToThinking(delta);
94
- },
95
- onToolCall: (toolName, input) => {
96
- tui.setRunning(true, `执行 ${toolName}...`);
97
- const args = typeof input === "object" && input
98
- ? Object.entries(input)
99
- .map(([k, v]) => `${k}=${typeof v === "string" ? (v.length > 40 ? v.slice(0, 40) + "..." : v) : JSON.stringify(v)}`)
100
- .join(" ")
101
- : "";
102
- tui.addMessage({
103
- id: `tool-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
104
- role: "tool",
105
- content: args,
106
- toolName,
107
- timestamp: Date.now(),
108
- });
109
- },
110
- onToolResult: (_toolName, _output) => {
111
- tui.setRunning(true, "思考中...");
112
- },
113
- onStreamError: (msg) => {
114
- tui.addMessage({
115
- id: `err-${Date.now()}`,
116
- role: "system",
117
- content: `错误: ${msg}`,
118
- timestamp: Date.now(),
119
- });
120
- },
121
- onRunFinish: async (info) => {
122
- abortController = null;
123
- tui.setRunning(false);
124
- if (info.usage) {
125
- const input = info.usage.inputTokens ?? 0;
126
- const output = info.usage.outputTokens ?? 0;
127
- const total = input + output;
128
- const ctxWindow = await getContextWindow(modelId);
129
- const pct = ctxWindow > 0 ? Math.round((input / ctxWindow) * 100) : 0;
130
- tui.setTokenInfo(input, output, ctxWindow);
131
- tui.addMessage({
132
- id: `done-${Date.now()}`,
133
- role: "system",
134
- content: `Done in ${info.stepCount} step(s) | Tokens: ${input} in / ${output} out / ${total} total | Context: ${pct}%`,
135
- timestamp: Date.now(),
136
- });
137
- }
138
- },
139
- };
140
- if (mode === "code" && codeSystemPrompt) {
141
- await runOnceWithSystem(messages, codeSystemPrompt, modelId, abortController?.signal, callbacks, tracker);
142
- }
143
- else {
144
- await runOnce(messages, instructions, modelId, abortController?.signal, callbacks, tracker);
145
- }
146
- abortController = null;
147
- tui.setRunning(false);
148
- };
149
- // Override the confirm module to use TUI overlay
150
- setTuiConfirm((message) => {
151
- return new Promise((resolve) => {
152
- confirmResolver = resolve;
153
- tui.showConfirm(message);
154
- });
155
- });
156
- tui.start();
157
- // Keep process alive until exit
158
- await new Promise((resolve) => {
159
- process.on("SIGINT", () => {
160
- if (abortController) {
161
- killActiveProcesses();
162
- abortController.abort();
163
- abortController = null;
164
- tui.setRunning(false);
165
- }
166
- else {
167
- resolve();
194
+ onModelPick: (model) => {
195
+ tui.hideModelPicker();
196
+ const cfg = loadConfig();
197
+ const provider = getActiveProvider(cfg);
198
+ if (provider) {
199
+ provider.defaultModel = model;
200
+ import("./config.js").then((m) => m.saveConfig(cfg));
168
201
  }
169
- });
202
+ currentModel = model;
203
+ tui.setModel(model);
204
+ sysMsg(tui, `✓ 默认模型已设为: ${model}`);
205
+ },
206
+ onModelCancel: () => {
207
+ tui.hideModelPicker();
208
+ },
209
+ onSessionPick: (id) => {
210
+ tui.hideSessionPicker();
211
+ applySession(id);
212
+ },
213
+ onSessionCancel: () => {
214
+ tui.hideSessionPicker();
215
+ },
216
+ onToggleSelectionMode: () => {
217
+ tui.toggleSelectionMode();
218
+ },
219
+ onExitSelectionMode: () => {
220
+ tui.setSelectionMode(false);
221
+ },
222
+ onExit: () => {
223
+ requestExit();
224
+ },
225
+ }, modelId ?? getActiveProvider(config)?.defaultModel, sessionId);
226
+ runner = createAgentRunner({
227
+ tui,
228
+ messages,
229
+ tracker,
230
+ getModel: () => currentModel,
231
+ getProvider: () => currentProvider,
232
+ getPlanMode: () => planMode,
233
+ getUndoStack: () => undoStack,
234
+ setUndoStack: (s) => {
235
+ undoStack = s;
236
+ },
237
+ getSystemPrompt: () => systemPrompt,
238
+ getTaskState: () => taskState,
239
+ persist: () => persistSession(),
170
240
  });
171
- // Save session
172
- if (messages.length > 0) {
173
- const { saveSession } = await import("./sessions.js");
174
- sessionId = saveSession(messages, sessionId);
241
+ function persistSession() {
242
+ if (messages.length === 0)
243
+ return;
244
+ try {
245
+ sessionId = saveSession(messages, sessionId, undefined, undefined, taskState);
246
+ }
247
+ catch { }
175
248
  }
176
- setTuiConfirm(null);
177
- tui.destroy();
178
- await shutdownMcp();
179
- }
180
- async function handleSlashCommand(input, tui, messages, modelId, tracker) {
181
- const [cmd, ...rest] = input.slice(1).split(/\s+/);
182
- const arg = rest.join(" ");
183
- switch (cmd) {
184
- case "exit":
185
- case "quit":
186
- case "q":
187
- process.emit("SIGINT", "SIGINT");
188
- break;
189
- case "clear":
190
- messages.length = 0;
191
- tui.update({ messages: [] });
192
- sysMsg(tui, "✓ 对话已清除");
193
- break;
194
- case "compact": {
195
- if (messages.length < 4) {
196
- sysMsg(tui, "消息太少,无需压缩");
197
- }
198
- else {
199
- sysMsg(tui, "⟳ 正在压缩上下文...");
200
- const model = resolveModel(modelId);
201
- const result = await compactMessages(messages, model, { keepRecentTurns: 2, autoContinue: false });
202
- messages.length = 0;
203
- messages.push(...result.messages);
204
- if (tracker)
205
- tracker.resetContext();
206
- sysMsg(tui, `✓ 已压缩 (约 ${estimateTokens(messages)} tokens)`);
207
- }
208
- break;
249
+ async function refreshTokenBar() {
250
+ const { snapshotTokenInfo } = await import("./tui/token-info.js");
251
+ tui.setTokenInfo(await snapshotTokenInfo(tracker, currentModel, messages));
252
+ }
253
+ function applySession(id) {
254
+ const ready = prepareSwitch({
255
+ running: Boolean(runner?.getController()),
256
+ currentId: sessionId,
257
+ targetId: id,
258
+ exists: Boolean(loadSession(id)),
259
+ });
260
+ if (!ready.ok) {
261
+ sysMsg(tui, switchFailMessage(ready.reason));
262
+ return;
209
263
  }
210
- case "model": {
211
- if (arg) {
212
- const config = await import("./config.js");
213
- const cfg = config.loadConfig();
214
- if (cfg.provider) {
215
- cfg.provider.defaultModel = arg;
216
- config.saveConfig(cfg);
217
- sysMsg(tui, `✓ 默认模型已设为: ${arg}`);
218
- }
219
- }
220
- else {
221
- const config = loadConfig();
222
- sysMsg(tui, `当前模型: ${config.provider?.defaultModel ?? "未设置"}`);
223
- }
224
- break;
264
+ if (messages.length > 0)
265
+ sessionId = saveSession(messages, sessionId, undefined, undefined, taskState);
266
+ const session = loadSession(id);
267
+ if (!session) {
268
+ sysMsg(tui, switchFailMessage("missing"));
269
+ return;
225
270
  }
226
- case "models": {
227
- const { fetchModels } = await import("./config.js");
228
- const cfg = loadConfig();
229
- if (!cfg.provider?.baseURL || !cfg.provider?.apiKey) {
230
- sysMsg(tui, "未配置 Provider,运行: min-agent setup");
231
- break;
232
- }
233
- sysMsg(tui, "正在获取模型列表...");
234
- const models = await fetchModels(cfg.provider.baseURL, cfg.provider.apiKey);
235
- if (models.length === 0) {
236
- sysMsg(tui, "无法获取模型列表");
237
- }
238
- else {
239
- const lines = models.map((m) => {
240
- const marker = m === cfg.provider?.defaultModel ? " ← 默认" : "";
241
- return ` ${m}${marker}`;
242
- });
243
- sysMsg(tui, `可用模型 (${models.length}):\n${lines.join("\n")}`);
244
- }
245
- break;
271
+ messages.length = 0;
272
+ messages.push(...session.messages);
273
+ copyTaskState(session.taskState, taskState);
274
+ sessionId = id;
275
+ undoStack = clearStack(undoStack);
276
+ tracker.reset();
277
+ lastUserMessage = lastUserText(session.messages);
278
+ tui.setSessionId(id);
279
+ tui.update({ messages: hydrateMessages(session.messages) });
280
+ void refreshTokenBar();
281
+ sysMsg(tui, `✓ 已切换到: ${session.meta.title}`);
282
+ }
283
+ function startNewSession() {
284
+ const ready = prepareNew({
285
+ running: Boolean(runner?.getController()),
286
+ sessionId,
287
+ messageCount: messages.length,
288
+ });
289
+ if (!ready.ok) {
290
+ sysMsg(tui, newFailMessage(ready.reason));
291
+ return;
246
292
  }
247
- case "memory": {
248
- if (arg) {
249
- const { addMemory } = await import("./memory.js");
250
- addMemory(arg);
251
- sysMsg(tui, `✓ 已保存记忆: "${arg}"`);
293
+ if (messages.length > 0)
294
+ sessionId = saveSession(messages, sessionId, undefined, undefined, taskState);
295
+ messages.length = 0;
296
+ resetTaskState(taskState);
297
+ sessionId = undefined;
298
+ undoStack = clearStack(undoStack);
299
+ tracker.reset();
300
+ lastUserMessage = null;
301
+ tui.setSessionId(undefined);
302
+ tui.update({ messages: [], tokenInfo: undefined });
303
+ sysMsg(tui, "✓ 已新建会话");
304
+ }
305
+ function openSessionPicker() {
306
+ if (listSessions().length === 0) {
307
+ sysMsg(tui, "无已保存的会话");
308
+ return;
309
+ }
310
+ tui.showSessionPicker();
311
+ }
312
+ setTuiConfirm((message) => promptQueue.run(() => new Promise((resolve) => {
313
+ confirmResolver = resolve;
314
+ tui.showConfirm(message);
315
+ }), () => false));
316
+ setTuiQuestion((prompt, options) => promptQueue.run(() => new Promise((resolve) => {
317
+ questionResolver = resolve;
318
+ tui.showQuestion(prompt, options);
319
+ }), () => null));
320
+ let exitResolve;
321
+ const exitPromise = new Promise((resolve) => {
322
+ exitResolve = resolve;
323
+ });
324
+ const requestExit = () => {
325
+ tui.setSelectionMode(false);
326
+ persistSession();
327
+ if (runner?.getController()) {
328
+ killActiveProcesses();
329
+ promptQueue.cancelAll();
330
+ if (confirmResolver) {
331
+ confirmResolver(false);
332
+ confirmResolver = null;
333
+ tui.hideConfirm();
252
334
  }
253
- else {
254
- const { loadMemories } = await import("./memory.js");
255
- const memories = loadMemories();
256
- if (memories.length === 0) {
257
- sysMsg(tui, "无已保存的记忆");
258
- }
259
- else {
260
- const lines = memories.map((m, i) => {
261
- const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
262
- return ` #${i + 1}: ${m.content}${tags}`;
263
- });
264
- sysMsg(tui, `记忆 (${memories.length}):\n${lines.join("\n")}`);
265
- }
335
+ if (questionResolver) {
336
+ questionResolver(null);
337
+ questionResolver = null;
338
+ tui.hideQuestion();
266
339
  }
267
- break;
340
+ runner.getController().abort();
341
+ tui.setRunning(false);
342
+ tui.addMessage({ id: `sys-${Date.now()}`, role: "system", content: "(已取消)", timestamp: Date.now() });
268
343
  }
269
- case "path":
270
- case "pwd":
271
- sysMsg(tui, process.cwd());
272
- break;
273
- case "paste": {
274
- const { getClipboardImage } = await import("./clipboard.js");
275
- const img = getClipboardImage();
276
- if (!img) {
277
- sysMsg(tui, "剪贴板中未找到图片");
278
- break;
279
- }
280
- sysMsg(tui, `📎 剪贴板图片 (${(img.data.length / 1024).toFixed(1)} KB)`);
281
- const text = arg || "What's in this image?";
282
- const content = [
283
- { type: "text", text },
284
- { type: "image", image: img.data, mimeType: img.mimeType },
285
- ];
286
- messages.push({ role: "user", content });
344
+ else
345
+ exitResolve?.();
346
+ };
347
+ tui.start();
348
+ if (messages.length > 0)
349
+ tui.update({ messages: hydrateMessages(messages) });
350
+ if (messages.length > 0)
351
+ await refreshTokenBar();
352
+ const prompt = initialPrompt?.trim() ?? "";
353
+ const notify = (kind, imgPath) => {
354
+ if (kind === "not_found")
355
+ sysMsg(tui, `⚠ 图片不存在: ${imgPath}`);
356
+ else if (kind === "too_large")
357
+ sysMsg(tui, `⚠ 图片超过 5 MB 限制: ${imgPath}`);
358
+ else
359
+ sysMsg(tui, `📎 ${imgPath}`);
360
+ };
361
+ if (images && images.length > 0) {
362
+ const parts = [{ type: "text", text: prompt }, ...loadImageParts(images, notify)];
363
+ if (parts.length > 1 || prompt) {
364
+ messages.push({ role: "user", content: parts });
287
365
  tui.addMessage({
288
366
  id: `user-${Date.now()}`,
289
367
  role: "user",
290
- content: `${text} [📎 图片]`,
368
+ content: prompt ? prompt : `[📎 图片] ${images.join(", ")}`,
291
369
  timestamp: Date.now(),
292
370
  });
293
- // Signal that we need to run the agent
294
- return "run";
295
- }
296
- case "tokens": {
297
- if (tracker && tracker.lastInputTokens > 0) {
298
- const ctxWindow = await getContextWindow(modelId);
299
- const pct = Math.round((tracker.lastInputTokens / ctxWindow) * 100);
300
- sysMsg(tui, [
301
- `Context: ${tracker.lastInputTokens} / ${ctxWindow} tokens (${pct}%)`,
302
- `Total: ${tracker.totalInputTokens} in / ${tracker.totalOutputTokens} out`,
303
- `Messages: ${messages.length}`,
304
- ].join("\n"));
305
- }
306
- else {
307
- sysMsg(tui, `估算 tokens: ${estimateTokens(messages)} | Messages: ${messages.length}`);
308
- }
309
- break;
310
- }
311
- case "mcp": {
312
- const config = loadMcpConfig();
313
- const servers = Object.entries(config.mcpServers);
314
- if (servers.length === 0) {
315
- sysMsg(tui, "无 MCP 服务器配置");
316
- }
317
- else {
318
- const status = getMcpStatus();
319
- const lines = servers.map(([name, cfg]) => {
320
- const disabled = cfg.enabled === false;
321
- const connected = status[name]?.connected ?? false;
322
- const toolCount = status[name]?.tools.length ?? 0;
323
- const state = disabled ? "disabled" : connected ? "connected" : "disconnected";
324
- return ` ${name}: ${state}${toolCount > 0 ? ` (${toolCount} tools)` : ""}`;
325
- });
326
- sysMsg(tui, `MCP 服务器 (${servers.length}):\n${lines.join("\n")}`);
327
- }
328
- break;
371
+ if (prompt)
372
+ lastUserMessage = prompt;
373
+ await runner.run();
374
+ persistSession();
329
375
  }
330
- case "skills": {
331
- discoverSkills({ silent: true });
332
- const skills = getSkills();
333
- const cfg = loadConfig();
334
- const disabledNames = cfg.disabledSkills ?? [];
335
- if (skills.length === 0 && disabledNames.length === 0) {
336
- sysMsg(tui, "无可用 skills。添加 SKILL.md 到 .min-agent/skills/<name>/");
337
- }
338
- else {
339
- const lines = [];
340
- for (const s of skills) {
341
- lines.push(` ${s.name} enabled`);
342
- }
343
- for (const n of disabledNames) {
344
- lines.push(` ${n} disabled`);
345
- }
346
- sysMsg(tui, `Skills:\n${lines.join("\n")}`);
347
- }
348
- break;
349
- }
350
- case "help":
351
- sysMsg(tui, [
352
- "命令:",
353
- " /clear 清除对话历史",
354
- " /compact 压缩上下文",
355
- " /model [n] 显示/切换模型",
356
- " /models 列出可用模型",
357
- " /memory [t] 查看/保存记忆",
358
- " /tokens 显示 token 用量",
359
- " /mcp 显示 MCP 状态",
360
- " /skills 显示可用 skills",
361
- " /path 显示工作目录",
362
- " /paste [t] 粘贴剪贴板图片 + 可选提示",
363
- " /help 显示帮助",
364
- " /exit 退出",
365
- "",
366
- "快捷键:",
367
- " Esc 取消当前任务",
368
- " Ctrl+J 输入框换行",
369
- " Ctrl+C 退出程序",
370
- " PageUp/Down 滚动消息",
371
- ].join("\n"));
372
- break;
373
- default:
374
- sysMsg(tui, `未知命令: /${cmd},输入 /help 查看可用命令`);
375
376
  }
376
- }
377
- function sysMsg(tui, content) {
378
- tui.addMessage({
379
- id: `sys-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
380
- role: "system",
381
- content,
382
- timestamp: Date.now(),
383
- });
377
+ else if (prompt) {
378
+ lastUserMessage = prompt;
379
+ tui.addMessage({ id: `user-${Date.now()}`, role: "user", content: prompt, timestamp: Date.now() });
380
+ messages.push({ role: "user", content: prompt });
381
+ await runner.run();
382
+ persistSession();
383
+ }
384
+ const onSigint = () => {
385
+ if (runner?.getController())
386
+ requestExit();
387
+ };
388
+ process.on("SIGINT", onSigint);
389
+ await exitPromise;
390
+ process.off("SIGINT", onSigint);
391
+ try {
392
+ persistSession();
393
+ }
394
+ finally {
395
+ setTuiConfirm(null);
396
+ setTuiQuestion(null);
397
+ tui.destroy();
398
+ await shutdownMcp();
399
+ }
384
400
  }