min-agent 0.2.0 → 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.
Files changed (81) hide show
  1. package/README.md +146 -18
  2. package/dist/agent.js +293 -408
  3. package/dist/assistant-stream.js +11 -7
  4. package/dist/cli.js +403 -140
  5. package/dist/clipboard.js +59 -23
  6. package/dist/code-mode.js +3 -3
  7. package/dist/compaction.js +182 -81
  8. package/dist/config.js +186 -35
  9. package/dist/confirm.js +55 -6
  10. package/dist/context-window.js +67 -54
  11. package/dist/doom-loop.js +19 -12
  12. package/dist/http.js +119 -0
  13. package/dist/instructions.js +51 -33
  14. package/dist/logger.js +66 -0
  15. package/dist/markdown.js +3 -44
  16. package/dist/mcp.js +547 -100
  17. package/dist/memory.js +48 -6
  18. package/dist/output.js +36 -27
  19. package/dist/paste-handler.js +3 -3
  20. package/dist/plugins.js +33 -6
  21. package/dist/pricing.js +119 -0
  22. package/dist/provider.js +17 -15
  23. package/dist/serve.js +658 -369
  24. package/dist/sessions.js +151 -13
  25. package/dist/skills.js +466 -76
  26. package/dist/synthetic.js +7 -0
  27. package/dist/title-gen.js +2 -1
  28. package/dist/tool-display.js +173 -0
  29. package/dist/tool-output.js +54 -45
  30. package/dist/tools/apply_patch.js +191 -0
  31. package/dist/tools/backend.js +61 -0
  32. package/dist/tools/bash.js +147 -70
  33. package/dist/tools/code_search.js +6 -5
  34. package/dist/tools/edit.js +23 -7
  35. package/dist/tools/explore.js +80 -12
  36. package/dist/tools/glob.js +3 -3
  37. package/dist/tools/grep.js +146 -14
  38. package/dist/tools/index.js +7 -7
  39. package/dist/tools/question.js +4 -22
  40. package/dist/tools/read.js +71 -11
  41. package/dist/tools/task.js +33 -20
  42. package/dist/tools/todo.js +83 -73
  43. package/dist/tools/web_fetch.js +150 -46
  44. package/dist/tools/web_search.js +706 -28
  45. package/dist/tools/write.js +13 -7
  46. package/dist/tui/App.js +40 -6
  47. package/dist/tui/ConfirmBar.js +24 -3
  48. package/dist/tui/InputBar.js +390 -45
  49. package/dist/tui/MessageList.js +533 -20
  50. package/dist/tui/ModelPicker.js +108 -0
  51. package/dist/tui/QuestionBar.js +104 -0
  52. package/dist/tui/StatusBar.js +19 -11
  53. package/dist/tui/agent-runner.js +103 -0
  54. package/dist/tui/caret-pos.js +134 -0
  55. package/dist/tui/caret.js +69 -0
  56. package/dist/tui/diff-view.js +61 -0
  57. package/dist/tui/drag-state.js +44 -0
  58. package/dist/tui/index.js +153 -24
  59. package/dist/tui/input-history.js +44 -0
  60. package/dist/tui/layout.js +17 -0
  61. package/dist/tui/mouse.js +46 -0
  62. package/dist/tui/selection.js +134 -0
  63. package/dist/tui/slash-commands.js +90 -0
  64. package/dist/tui/slash-handler.js +370 -0
  65. package/dist/tui/text-width.js +91 -0
  66. package/dist/tui/theme.js +12 -0
  67. package/dist/tui/undo-stack.js +14 -0
  68. package/dist/tui/use-sgr-mouse.js +27 -0
  69. package/dist/tui-chat.js +111 -331
  70. package/dist/updater.js +57 -0
  71. package/docs/API.md +160 -14
  72. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
  73. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
  74. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
  75. package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
  76. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
  77. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
  78. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
  79. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
  80. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
  81. package/package.json +7 -8
@@ -0,0 +1,183 @@
1
+ # 第一批:Bug 修复 + TUI 交互体验 — 设计文档
2
+
3
+ **日期:** 2026-08-16
4
+ **状态:** 已批准
5
+ **范围:** 30 项改进清单中的第一批(8 项):TUI 交互体验与缺陷修复
6
+
7
+ ---
8
+
9
+ ## 背景
10
+
11
+ min-agent 功能审查发现 30 项缺失/欠缺,按"价值/风险"划分为三批实施。本 spec 覆盖第一批:最高价值、低风险的 TUI 交互体验项目。
12
+
13
+ ## 目标与非目标
14
+
15
+ **目标:**
16
+ - 修复 `chat -i` 图片静默丢弃缺陷
17
+ - 补齐 TUI 缺失的核心交互:输入历史、undo、redo、diff、会话管理
18
+ - 增加成本估算显示
19
+ - README 与 `/help` 同步
20
+
21
+ **非目标:**
22
+ - 多 provider、采样参数、预算上限、日志、CI、/plan(第二、三批)
23
+ - 输入历史持久化(会话内内存即可)
24
+ - 成本预算执行(第三批)
25
+
26
+ ## 设计决策(已与用户确认)
27
+
28
+ | 决策 | 结论 |
29
+ |------|------|
30
+ | 范围 | 全部 30 项,分三批推进,本批为第一批 |
31
+ | 成本数据源 | models.dev 自动拉取 + config 手动覆盖 |
32
+ | 计划模式 | 内置 /plan(第三批) |
33
+
34
+ ## 模块设计
35
+
36
+ ### 1. 图片附加修复 + `/attach`
37
+
38
+ **现状缺陷:** `cli.ts:225-227` — `min-agent chat -i img.png`(无消息)时 images 被静默丢弃,`TuiOptions` 无 images 字段。
39
+
40
+ **修改:**
41
+
42
+ - `src/tui-chat.ts` `TuiOptions` 增加 `images?: string[]`
43
+ - `src/cli.ts` chat 分支:`runTui({ modelId, resumeSessionId, mode, images })`
44
+ - `runTui` 启动时:若 `images` 非空且 resume 会话无未处理图片,用 `buildUserContent()` 构建首条用户消息注入,TUI 显示 `[📎 图片]` 占位(对齐 `/paste` 交互)
45
+ - 新斜杠命令 `/attach <path>`(alias: `/image`):读取本地图片(复用 5MB 上限逻辑),push 到 messages 并触发 agent 运行;读取失败警告并跳过,不崩溃
46
+ - 图片注入逻辑提取为可复用函数(`src/tui-chat.ts` 内部),`/paste` 与 `/attach` 共用
47
+
48
+ ### 2. README 与 `/help` 同步
49
+
50
+ - `README.md` 交互命令表补充 `/models`、`/mcp`、`/skills`
51
+ - 本批新增命令(`/undo`、`/redo`、`/diff`、`/sessions`、`/rename`、`/attach`)同步进 README 表与 `/help` 输出及 `slash-commands.ts` 菜单
52
+
53
+ ### 3. 输入历史(新模块 `src/tui/input-history.ts`)
54
+
55
+ 纯函数,无 React/Ink 依赖:
56
+
57
+ ```ts
58
+ interface HistoryState {
59
+ entries: string[] // 已提交输入,最新在后
60
+ index: number // 当前浏览位置,-1 表示草稿态
61
+ draft: string // 进入历史模式前的草稿
62
+ }
63
+ createHistory(): HistoryState
64
+ pushInput(state, text): HistoryState // 提交输入
65
+ browseHistory(state, direction): { text, state } // direction: 1 更早 | -1 更新
66
+ exitHistory(state): { text, state } // 恢复草稿
67
+ hasHistory(state): boolean
68
+ ```
69
+
70
+ **行为:**
71
+ - ↑(upArrow)进入历史模式:草稿暂存,从最近输入开始;再次 ↑ 更早,↓ 回退
72
+ - 浏览历史期间用户编辑文本 → 退出历史模式(回到草稿)
73
+ - Esc → 退出历史模式恢复草稿
74
+ - 空历史时 ↑ 无效果(保持现有行为)
75
+ - 重复输入去重(连续相同输入只记录一次)
76
+
77
+ **InputBar.tsx 集成:** upArrow/downArrow 处理路径改为调用 input-history;当前编辑文本替换为历史条目
78
+
79
+ ### 4. `/undo`(新模块 `src/tui/undo-stack.ts`)
80
+
81
+ 纯函数:
82
+
83
+ ```ts
84
+ interface TurnRecord { start: number; end: number }
85
+ createUndoStack(): TurnRecord[]
86
+ pushTurn(stack, start, end): TurnRecord[]
87
+ popTurn(stack): TurnRecord | undefined
88
+ ```
89
+
90
+ **tui-chat.ts 集成:**
91
+ - `executeAgent` 前记录 `turnStart = messages.length`;结束后 `pushTurn(stack, turnStart, messages.length)`(try/finally 中完成,失败回合也入栈以便撤销)
92
+ - `/undo`:pop 栈顶,`messages.splice(start, end - start)`,TUI 消息列表同步截断(`tui.update` 重新渲染剩余消息),系统提示 "✓ 已撤销 1 轮"
93
+ - 栈空 → "没有可撤销的内容"
94
+ - `/clear` 清空 undo 栈;`/redo` 的回合正常入栈(可撤销 redo)
95
+
96
+ ### 5. 成本估算(新模块 `src/pricing.ts`)
97
+
98
+ 对齐 `context-window.ts` 模式(内存缓存 + 磁盘缓存 + in-flight 共享 + 7 天 TTL):
99
+
100
+ ```ts
101
+ estimateCost(usage: { inputTokens: number; outputTokens: number }, modelId: string): Promise<number | null>
102
+ getModelPrice(modelId: string): Promise<{ inputPerMillion: number; outputPerMillion: number } | null>
103
+ ```
104
+
105
+ **数据源:** models.dev `api.json`(`context-window.ts` 已在用),读取 `model.pricing.input` / `model.pricing.output`(USD / 1M tokens)
106
+
107
+ **覆盖配置:** `config.json` 新增:
108
+
109
+ ```json
110
+ "pricing": {
111
+ "gpt-4o": { "inputPerMillion": 2.5, "outputPerMillion": 10 }
112
+ }
113
+ ```
114
+
115
+ **集成点:**
116
+ - `src/tui-chat.ts` `/tokens` 输出追加 `| 约 $0.012`
117
+ - `onRunFinish` 完成消息追加成本(`Done in N step(s) | Tokens: ... | ~$0.012`)
118
+ - 无价格数据 → 显示 `--`,不阻塞
119
+ - 缓存文件 `~/.min-agent/pricing-cache.json`
120
+
121
+ ### 6. `/diff`(新模块 `src/tui/diff-view.ts`)
122
+
123
+ ```ts
124
+ getWorkingTreeDiff(): Promise<{ ok: true; diff: string } | { ok: false; reason: "not_git" | "no_changes" | "error"; message: string }>
125
+ ```
126
+
127
+ - 执行 `git diff --stat` + `git diff`(cwd 为 git 仓库时),输出合并展示
128
+ - 非 git 仓库 → `{ ok: false, reason: "not_git" }`,TUI 提示 "非 git 仓库,无法显示 diff"
129
+ - 无未提交变更 → `"no_changes"`,提示 "工作区无未提交变更"
130
+ - 输出超长截断(复用 `truncateToolOutput` 思路,限制 8000 字符)
131
+ - TUI `/diff` 命令将 diff 作为系统消息展示
132
+
133
+ ### 7. `/redo`(中断重试)
134
+
135
+ - `lastUserMessage: string | null` 变量,每次用户文本消息提交时更新(`/paste`、`/attach` 不更新)
136
+ - `/redo`:`lastUserMessage` 存在 → push 相同消息到 messages + TUI + `executeAgent()`;不存在 → "没有可重发的消息"
137
+ - Esc 取消后的重试流程:用户输入 `/redo`
138
+
139
+ ### 8. `/sessions` + `/rename <title>`
140
+
141
+ - `src/sessions.ts` 新增:
142
+
143
+ ```ts
144
+ renameSession(id: string, title: string): boolean
145
+ ```
146
+
147
+ - `/sessions`:`listSessions()` 输出(id、标题、消息数、日期),当前会话标 `←`;当前会话 id 未知(未保存过)时用 `(未保存)`
148
+ - `/rename <title>`:若 `sessionId` 为空(会话未保存过),先 `sessionId = saveSession(messages)` 落盘;再调用 `renameSession(sessionId, title)`,失败提示"会话不存在"
149
+ - 退出时 `saveSession(messages, sessionId)` 沿用已改标题(内部读取现有 meta.created)
150
+
151
+ ## 错误处理
152
+
153
+ | 场景 | 行为 |
154
+ |------|------|
155
+ | 图片文件不存在/超 5MB | 警告并跳过该图片,继续运行 |
156
+ | 价格拉取失败 | 缓存空结果,成本显示 `--` |
157
+ | undo 栈空 | "没有可撤销的内容" |
158
+ | redo 无消息 | "没有可重发的消息" |
159
+ | diff 非 git / 无变更 | 明确提示 |
160
+ | rename 不存在的会话 | 返回 false,TUI 提示 |
161
+
162
+ ## 测试
163
+
164
+ 新增测试文件:
165
+
166
+ | 文件 | 覆盖 |
167
+ |------|------|
168
+ | `tests/input-history.test.ts` | 入栈/导航/去重/草稿恢复/边界 |
169
+ | `tests/undo-stack.test.ts` | 入栈/出栈/空栈/顺序 |
170
+ | `tests/pricing.test.ts` | 价格解析、缓存命中、覆盖配置优先、失败回退(mock fetch) |
171
+ | `tests/diff-view.test.ts` | git diff 提取(mock execSync)、非 git、无变更 |
172
+
173
+ 验证命令(`packages/min-agent` 下):
174
+ - `npx tsc --noEmit`
175
+ - `bun test`
176
+ - `bun run src/cli.ts` 手动冒烟
177
+
178
+ ## 文档同步(AGENTS.md 强制)
179
+
180
+ - `README.md`:交互命令表补 `/models` `/mcp` `/skills` `/undo` `/redo` `/diff` `/sessions` `/rename` `/attach`
181
+ - TUI `/help` 输出同步
182
+ - `src/tui/slash-commands.ts` 菜单同步
183
+ - `docs/API.md`:本批无 API 变更(会话重命名 API 属第二批,届时同步)
@@ -0,0 +1,220 @@
1
+ # 第二批:CLI + 工具 + HTTP API — 设计文档
2
+
3
+ **日期:** 2026-08-16
4
+ **状态:** 已批准
5
+ **范围:** 30 项改进清单中的第二批(17 项):CLI 增强、工具增强、HTTP API 补齐
6
+
7
+ ---
8
+
9
+ ## 背景
10
+
11
+ min-agent 功能审查发现的 30 项缺失按"价值/风险"分三批实施。本 spec 覆盖第二批:CLI 命令补全、工具能力增强、HTTP API 完整性。第一批(TUI 交互)已完成并合并到 master。
12
+
13
+ ## 设计决策(已与用户确认)
14
+
15
+ | 决策 | 结论 |
16
+ |------|------|
17
+ | update 命令 | 检查 npm registry 版本后自动执行 `npm install -g min-agent@latest` |
18
+ | apply_patch | 完整 unified diff(解析 + 应用 + 冲突报告) |
19
+ | 会话导出格式 | 完整 session JSON(meta + messages,与 sessions/ 目录同格式) |
20
+
21
+ ## 模块设计
22
+
23
+ ### 1. CLI 增强
24
+
25
+ #### 1.1 `min-agent history delete <id>` / `rename <id> <title>`
26
+
27
+ - `src/cli.ts` history 分支扩展:
28
+ - `history delete <id>` → `deleteSession(id)`,成功提示 ✓,失败报"会话不存在"(exit 1)
29
+ - `history rename <id> <title>` → `renameSession(id, title)`,同样处理
30
+ - 复用第一批已实现的 `src/sessions.ts` 函数,无新模块
31
+
32
+ #### 1.2 `min-agent mcp add --env KEY=VALUE`
33
+
34
+ - `parseMcpAddArgs` 增加 `--env` 解析(可重复,`KEY=VALUE` 格式,非法格式报错)
35
+ - 构建 `entry.environment = { KEY: "VALUE", ... }`(仅当有 env 时设置)
36
+ - `mcp info` 已支持显示 environment 键列表,无需改
37
+
38
+ #### 1.3 `min-agent update` 自动升级
39
+
40
+ - 新逻辑(放 `src/cli.ts` 或独立 `src/updater.ts`;因含 fetch + execSync 逻辑,独立模块 `src/updater.ts` 更清晰):
41
+
42
+ ```ts
43
+ export async function runUpdate(): Promise<void>
44
+ ```
45
+
46
+ - 流程:
47
+ 1. 读取本地版本(现有 `--version` 的读 package.json 逻辑)
48
+ 2. fetch `https://registry.npmjs.org/min-agent/latest`(10s 超时),取 `version`
49
+ 3. 本地已最新 → 提示并返回
50
+ 4. 有新版 → 打印版本信息,执行 `npm install -g min-agent@latest`(`execSync(..., { stdio: "inherit" })`),成功提示重启
51
+ 5. 网络失败 → 提示无法检查
52
+ - 注意事项:npm 不可用时报错提示改用 `npm install -g min-agent@latest` 手动执行
53
+
54
+ #### 1.4 `min-agent history export <id> [-o file]`
55
+
56
+ - `history export <id>`:`loadSession(id)`,无会话报错 exit 1
57
+ - 输出完整 SessionData JSON(`JSON.stringify(data, null, 2)`);`-o <file>` 写文件并提示路径,否则 stdout
58
+ - 复用 `loadSession`,无新模块
59
+
60
+ ### 2. 工具增强
61
+
62
+ #### 2.1 bash 支持 `cwd` 参数
63
+
64
+ - `src/tools/bash.ts`:
65
+ - `BashInput` 加可选 `cwd?: string`
66
+ - `executeBash(command, timeout, cwd?)` 签名扩展,spawn 用 `cwd: cwd ? path.resolve(process.cwd(), cwd) : process.cwd()`
67
+ - 工具描述补充"cwd:在指定目录执行(相对当前目录或绝对路径)"
68
+ - `src/tools/explore.ts` 的 `readOnlyBashTool` 同步支持(executeBash 已扩展,只需 schema 加 cwd)
69
+ - 不引入确认流程变化(cwd 本身无危险)
70
+
71
+ #### 2.2 `apply_patch` 工具(新模块 `src/tools/apply_patch.ts`)
72
+
73
+ Unified diff 解析与应用:
74
+
75
+ ```ts
76
+ interface ApplyPatchInput {
77
+ diff: string
78
+ }
79
+ ```
80
+
81
+ **解析器**(纯函数,可测试):
82
+ - 扫描 `--- <path>` / `+++ <path>` 文件头(忽略 `a/`、`b/` 前缀;`/dev/null` 表示创建新文件)
83
+ - 扫描 `@@ -start,count +start,count @@` hunks(行号可省略 count)
84
+ - hunk 内行:`' '` 上下文、`-` 删除、`+` 新增、`\` 续行标记(No newline at end of file 忽略处理)
85
+ - 多文件、多 hunk 支持
86
+
87
+ **应用器**:
88
+ - 目标文件不存在且 diff 标记为新增(`/dev/null` 或 b/ 文件无 a/)→ 创建
89
+ - 目标文件不存在且无新增标记 → 报错"文件不存在"
90
+ - 逐 hunk 应用:按上下文匹配定位(允许行偏移——匹配时计算目标位置;简化策略:从 hunk 期望位置附近 ±3 行内查找匹配起点,找不到则冲突)
91
+ - 冲突 → 返回错误信息(哪个文件、哪个 hunk 行号范围),不部分写入
92
+ - 应用顺序:每个文件独立应用,全部成功才写(原子性:先全量解析验证,再写文件)
93
+ - 写文件前 confirm(危险操作,遵循项目规则)
94
+ - 输出:每个文件的变更统计(`-N/+M` 行数)
95
+
96
+ #### 2.3 write 支持 `append`
97
+
98
+ - `src/tools/write.ts`:`WriteInput` 加可选 `append?: boolean`
99
+ - `append: true` 时:`writeFile(resolved, content, { flag: "a" })`,跳过"覆盖现有文件"确认(追加不需要)
100
+ - 描述更新:追加模式说明
101
+
102
+ #### 2.4 web_search 参数扩展
103
+
104
+ - `src/tools/web_search.ts`:`WebSearchInput` 加 `language?: string`、`time_range?: string`
105
+ - 透传 SearXNG:`language`(ISO 639-1,如 `zh`、`en`)、`time_range`(`day`/`week`/`month`/`year`,校验白名单)
106
+ - language 白名单校验(简化:非空字符串直接透传,SearXNG 自己处理无效值);time_range 白名单校验
107
+
108
+ #### 2.5 chat 模式工具扩展
109
+
110
+ - `src/tools/index.ts` `createChatTools()`:加 `todo: todoTool`
111
+ - `src/agent.ts` `buildTools()`:explore 工厂改为两种模式都加:
112
+
113
+ ```ts
114
+ if (mode === "code") {
115
+ const { createTaskTool } = await import("./tools/task.js")
116
+ const { createExploreTool } = await import("./tools/explore.js")
117
+ allTools["task"] = createTaskTool(modelId, abortSignal)
118
+ allTools["explore"] = createExploreTool(modelId, abortSignal)
119
+ }
120
+ ```
121
+
122
+ 改为:
123
+
124
+ ```ts
125
+ const { createExploreTool } = await import("./tools/explore.js")
126
+ allTools["explore"] = createExploreTool(modelId, abortSignal)
127
+ if (mode === "code") {
128
+ const { createTaskTool } = await import("./tools/task.js")
129
+ allTools["task"] = createTaskTool(modelId, abortSignal)
130
+ }
131
+ ```
132
+
133
+ - task 保留 code 模式;explore 两种模式都有;todo 两种模式都有
134
+
135
+ ### 3. HTTP API 补齐
136
+
137
+ #### 3.1 `GET /v1/sessions/:id`
138
+
139
+ - `loadSession(id)` → 404(不存在)/ 200(`{ session: { meta, messages } }`)
140
+
141
+ #### 3.2 `POST /v1/sessions/:id/rename`
142
+
143
+ - body `{ title: string }`(必填非空,否则 400)
144
+ - `renameSession(id, title)` → false 时 404
145
+ - 成功 200 `{ ok: true, session_id, title }`
146
+
147
+ #### 3.3 `POST /v1/skills/:name/enable|disable`
148
+
149
+ - enable:从 `config.disabledSkills` 移除 name
150
+ - disable:加入(去重)
151
+ - 保存 config,200 `{ ok: true, name, enabled: bool }`
152
+
153
+ #### 3.4 `POST /v1/mcp/:name/enable|disable`
154
+
155
+ - 更新 `config.mcpServers[name].enabled`(不存在 404)
156
+ - 200 `{ ok: true, name, enabled: bool }`
157
+
158
+ #### 3.5 `/v1/paste` 支持 code 模式
159
+
160
+ - body 加 `code?: boolean`
161
+ - code 时:`scanProject()` + `buildCodeSystemPrompt()`,用 `runOnceWithSystem`,响应带 `mode: "code"` 与 `project` 字段(与 /v1/code 一致)
162
+ - stream 与 JSON 两种模式都支持
163
+
164
+ #### 3.6 `GET /v1/cost`
165
+
166
+ - 参数:`model`(可选,默认配置默认模型)、`input`、`output`(token 数,可选)
167
+ - 响应:`{ model, price: ModelPrice | null, cost_usd: number | null }`
168
+ - `getModelPrice(model)` + `estimateCost`
169
+ - 复用第一批的 pricing.ts
170
+
171
+ #### 3.7 `POST /v1/chat/undo`
172
+
173
+ - body `{ session_id }`(必填,404 检查)
174
+ - 语义:截断 messages 到最后一条 user 消息之后(保留该 user 消息,删除其后所有 assistant/tool 消息)——与 TUI /undo 的"保留 user 消息"语义一致
175
+ - 实现:从后往前找 `role === "user"` 的索引 `i`,`messages = messages.slice(0, i + 1)`;无 user 消息 → 400
176
+ - 保存并返回 `{ ok: true, message_count, messages }`
177
+
178
+ #### 3.8 采样参数透传
179
+
180
+ - `ChatBody` 加 `temperature?: number`、`maxTokens?: number`、`topP?: number`
181
+ - `src/agent.ts` `runOnce`/`runOnceWithSystem`/`runOnceCore` 加可选 `options?: { temperature?: number; maxTokens?: number; topP?: number }`
182
+ - `streamText({ ..., temperature, maxTokens, topP })` 条件展开(undefined 不传,保持默认)
183
+ - serve.ts 两个聊天 handler(/v1/chat、/v1/code、/v1/paste)透传
184
+ - 注意:AI SDK 的 streamText 参数是 `temperature?: number`、`maxTokens?: number`(Vercel AI SDK v6 是 `maxTokens`)、`topP?: number`
185
+
186
+ ### 4. 文档同步
187
+
188
+ - `docs/API.md`:新端点表格 + 各端点说明、采样参数字段、cost 端点
189
+ - `README.md`:Commands 区补 `history delete/rename/export`、`mcp add --env`、`update`
190
+ - `src/cli.ts` printUsage 同步
191
+
192
+ ## 错误处理
193
+
194
+ | 场景 | 行为 |
195
+ |------|------|
196
+ | update 网络失败 | 提示无法检查新版本,不崩溃 |
197
+ | update npm 失败 | 报错并提示手动 `npm install -g min-agent@latest` |
198
+ | apply_patch 冲突 | 明确报告文件与 hunk 位置,拒绝写入 |
199
+ | apply_patch 目标文件不存在 | 报错(除非 diff 标记新增) |
200
+ | history delete/rename/export 目标不存在 | 报错 exit 1 |
201
+ | mcp add --env 非法格式 | 报错并提示 `--env KEY=VALUE` |
202
+ | 采样参数非法值 | 透传给模型 API(不校验),由 provider 拒绝 |
203
+
204
+ ## 测试
205
+
206
+ | 文件 | 覆盖 |
207
+ |------|------|
208
+ | `tests/apply-patch.test.ts`(新) | 解析(文件头/hunk/行)、应用(修改/新增/删除)、新文件创建、上下文偏移匹配、冲突报告、原子性(冲突不部分写入) |
209
+ | `tests/web-search.test.ts`(改) | language/time_range 参数透传(mock fetch) |
210
+ | `tests/serve.test.ts`(改) | 新端点:sessions/:id、rename、skills enable/disable、mcp enable/disable、cost、chat/undo、paste code、采样参数透传 |
211
+ | bash cwd | 直接执行验证(`executeBash("pwd", undefined, tmpdir)` 输出为 tmpdir) |
212
+ | CLI 新命令 | 冒烟验证 |
213
+
214
+ 验证命令(`packages/min-agent` 下):`bun test`、`bun run typecheck`(仓库根)、手动冒烟。
215
+
216
+ ## 文档同步清单
217
+
218
+ - `docs/API.md`:新端点 + 采样参数 + cost
219
+ - `README.md`:history 子命令、mcp --env、update、export
220
+ - `src/cli.ts` printUsage
@@ -0,0 +1,196 @@
1
+ # 第三批:配置 + 工程 — 设计文档
2
+
3
+ **日期:** 2026-08-16
4
+ **状态:** 已批准
5
+ **范围:** 30 项改进清单中的第三批(7 项):多 provider、采样参数、预算上限、日志系统、CI、/plan 计划模式、文档完善
6
+
7
+ ---
8
+
9
+ ## 背景
10
+
11
+ min-agent 功能审查发现的 30 项缺失按三批实施。前两批(TUI 交互、CLI/工具/API)已完成并合并到 master。本批为配置体系与工程基础设施。
12
+
13
+ ## 设计决策(已与用户确认)
14
+
15
+ | 决策 | 结论 |
16
+ |------|------|
17
+ | 多 provider 切换 | 破坏式改造:`providers[]` + `activeProvider`,自动迁移旧配置;CLI `--provider <name>` + TUI `/provider <name>` |
18
+ | 预算超限行为 | 回合结束检查 + 提示(不硬中断流式输出) |
19
+ | 日志粒度 | 运行日志 + 日期滚动(保留 7 天) |
20
+
21
+ ## 模块设计
22
+
23
+ ### 1. 多 Provider(破坏式改造 + 自动迁移)
24
+
25
+ #### 1.1 配置结构(src/config.ts)
26
+
27
+ ```ts
28
+ export interface ProviderConfig {
29
+ name?: string
30
+ type?: "openai-compatible" | "openai" | "ollama"
31
+ baseURL: string
32
+ apiKey: string
33
+ defaultModel?: string
34
+ contextWindow?: number
35
+ }
36
+
37
+ export interface AppConfig {
38
+ providers?: ProviderConfig[]
39
+ activeProvider?: string
40
+ instructions?: string[]
41
+ disabledSkills?: string[]
42
+ permission?: "allow-all" | "ask"
43
+ compaction?: { model?: string; autoContinue?: boolean }
44
+ webSearchURL?: string
45
+ webFetchURL?: string
46
+ sampling?: { temperature?: number; maxTokens?: number; topP?: number }
47
+ budget?: { maxCostUSD?: number }
48
+ pricing?: Record<string, { inputPerMillion: number; outputPerMillion: number }>
49
+ }
50
+ ```
51
+
52
+ **移除** `provider?: ProviderConfig`(旧字段)。
53
+
54
+ #### 1.2 自动迁移
55
+
56
+ - `migrateLegacyConfig(config: AppConfig): { config: AppConfig; migrated: boolean }`
57
+ - 检测 `config.provider`(旧单对象)存在 → 转为 `providers: [{ name: "default", ...旧字段 }]` + `activeProvider: "default"`,删除 `provider` 字段,`migrated = true`
58
+ - `loadConfig()` 调用迁移并在 migrated 时 `saveConfig` 写回磁盘(一次性迁移;cachedConfig 缓存迁移后对象)
59
+ - 导出 `getActiveProvider(config?: AppConfig): ProviderConfig | undefined`(activeProvider 名称匹配;未设 activeProvider 且 providers 长度 1 → 返回该 provider;无匹配 → undefined)
60
+
61
+ #### 1.3 provider 解析(src/provider.ts)
62
+
63
+ - `resolveModelForProvider(provider: ProviderConfig, modelId?)` 签名不变(参数类型改为新接口)
64
+ - `resolveModel(modelId?, providerName?)`:取 `getActiveProvider`,providerName 覆盖时按名字查找(找不到报错 `Provider "x" not found`)
65
+ - `getProviderNames(config): string[]`(供 CLI/TUI 展示)
66
+
67
+ #### 1.4 CLI(src/cli.ts)
68
+
69
+ - `parseFlags` 加 `--provider <name>`(`-p`?不,`-p` 已是 port。用 `--provider` 长参数)
70
+ - `chat`/`code` 分支:`resolveModel(modelId, providerOverride)` 生效——通过 runTui/runAgent 传 providerName,最终进 resolveModel
71
+ - runTui TuiOptions 加 `providerName?: string`;runAgent 加参数
72
+ - `models` 命令:显示 activeProvider(或 --provider 指定)的模型列表
73
+ - `setup` 重构:列出现有 providers(名称 + URL + 默认模型 + 当前标记),选项:切换默认 / 添加新 provider / 修改当前;新 provider 信息收集复用现有流程(type/URL/key/model)
74
+ - `mcp`/`skills`/`history` 等不涉及 provider,不变
75
+
76
+ #### 1.5 TUI(src/tui-chat.ts)
77
+
78
+ - `/provider`:无参列出 providers(当前标记 ←),有参切换 activeProvider(saveConfig + sysMsg)
79
+ - `/model`:显示当前 provider 的模型列表(fetchModels 用 activeProvider 的 baseURL/apiKey)
80
+ - 状态栏/启动信息展示当前 provider 名(可选,保持精简)
81
+
82
+ #### 1.6 serve(src/serve.ts)
83
+
84
+ - 所有 `config.provider?.xxx` 引用改为 `getActiveProvider(config)?.xxx`
85
+ - `/v1/models`、`/v1/context` 等同理
86
+ - `body.provider` 可选:指定 provider 名(覆盖 activeProvider)
87
+
88
+ ### 2. 采样参数 config(src/config.ts + src/agent.ts)
89
+
90
+ - `AppConfig.sampling?: { temperature?: number; maxTokens?: number; topP?: number }`
91
+ - `runOnceCore`:options 为空(或字段 undefined)时回退 `loadConfig().sampling` 对应字段
92
+ - 优先级:API 请求显式 options > config.sampling > 模型默认
93
+ - 实现:在 runOnceCore 开头合并 `const effective = { temperature: options?.temperature ?? sampling?.temperature, ... }`(仅当字段非 undefined 时传 streamText)
94
+
95
+ ### 3. 预算上限(src/agent.ts + tui-chat.ts + serve.ts)
96
+
97
+ - `AppConfig.budget?: { maxCostUSD?: number }`
98
+ - 成本累计:tracker 已有 totalInputTokens/totalOutputTokens;新增 `tracker.usage` 或直接在 runOnce 结束后估算
99
+ - `runOnceCore` 结束(onRunFinish 前):`const maxCost = loadConfig().budget?.maxCostUSD`;若 `estimateCost({ totalInput, totalOutput }, price) > maxCost` → 置 `budgetExceeded = true`,onRunFinish 的 info 加 `budgetExceeded?: boolean` 字段
100
+ - TUI onRunFinish:budgetExceeded 时完成消息追加"⚠ 已达到预算上限(约 $X / $Y),/budget 查看或调高"
101
+ - `/budget` 命令:无参显示 `已用约 $X / 上限 $Y(未设置则提示)`;`/budget <n>` 设置上限
102
+ - serve:done 事件加 `budget_exceeded` 字段
103
+ - 注意:estimateCost 需要 getModelPrice(异步)——runOnceCore 结束时 await
104
+
105
+ ### 4. 日志系统(新模块 src/logger.ts)
106
+
107
+ ```ts
108
+ export function initLogger(): void // 创建 ~/.min-agent/logs/ 目录 + 清理 7 天前日志
109
+ export function log(level: "info" | "warn" | "error", msg: string): void // 追加到当天日志文件
110
+ export function logToolCall(name: string, input: unknown): void
111
+ export function logToolResult(name: string, output: unknown): void
112
+ ```
113
+
114
+ - 文件:`~/.min-agent/logs/min-agent.YYYY-MM-DD.log`(日期滚动,用 `getConfigDir()`)
115
+ - 追加写(`writeFileSync(..., { flag: "a" })`),日志行格式:`[HH:mm:ss] [level] msg`(JSON 参数摘要截断 200 字符)
116
+ - 清理:initLogger 时删除 7 天前的日志文件
117
+ - 记录点:
118
+ - agent.ts `runOnceCore` 开头:`log("info", `run start mode=${mode} model=${model.modelId} messages=${messages.length}`)`
119
+ - tool-call 事件:`logToolCall`
120
+ - tool-result 事件:`logToolResult`
121
+ - 错误:`log("error", ...)`(onStreamError / catch 路径)
122
+ - runOnceCore 结束:`log("info", `run end steps=${stepCount} tokens=...`)`
123
+ - cli.ts 启动:`log("info", "min-agent started")`
124
+ - 不记录消息内容(隐私),只记录数量与工具参数摘要
125
+
126
+ ### 5. CI(新文件 .github/workflows/ci.yml)
127
+
128
+ ```yaml
129
+ name: CI
130
+ on:
131
+ push:
132
+ branches: [master]
133
+ pull_request:
134
+
135
+ jobs:
136
+ test:
137
+ runs-on: ubuntu-latest
138
+ steps:
139
+ - uses: actions/checkout@v4
140
+ - uses: oven-sh/setup-bun@v2
141
+ with:
142
+ bun-version: latest
143
+ - run: bun install --frozen-lockfile
144
+ - run: bun run typecheck
145
+ - run: cd packages/min-agent && bun test
146
+ ```
147
+
148
+ (仓库根为 bun workspaces monorepo;typecheck 脚本在根 package.json)
149
+
150
+ ### 6. /plan 计划模式(src/agent.ts + tui-chat.ts)
151
+
152
+ - `RunOptions` 加 `planMode?: boolean`
153
+ - `runOnceCore` 的 `buildTools` 结果在 planMode 时过滤:
154
+ - 移除:`bash`、`write`、`edit`、`apply_patch`(写/执行类)
155
+ - 保留:`read`、`glob`、`grep`、`web_search`、`web_fetch`、`todo`、`question`、`explore`(内部只读 bash)、`skill`、mcp 工具、memory 工具、plugin 工具(保留——插件只读性未知,但删除会让插件用户困惑;保留并说明)
156
+ - TUI `/plan`:切换 planMode 状态(`let planMode = false`):
157
+ - 进入:sysMsg 提示"计划模式:仅只读工具可用,输出计划后再次输入 /plan 确认并执行"
158
+ - 退出:sysMsg 提示"已退出计划模式"
159
+ - executeAgent 调 runOnce 时传 `{ ...(planMode ? { planMode: true } : {}) }`(与 options 合并)
160
+ - `/redo` 等不受影响
161
+
162
+ ### 7. 文档完善
163
+
164
+ - `README.md`:Configuration 区更新(providers[] 结构 + 迁移说明 + sampling/budget 字段)、Commands 区加 `--provider`、交互命令加 `/provider` `/budget` `/plan`、日志位置说明
165
+ - `docs/API.md`:`/v1/chat` 请求加 `provider` 字段;done 事件加 `budget_exceeded`
166
+ - `src/cli.ts` printUsage:`--provider`、setup 新流程说明
167
+ - `/help` 与 `slash-commands.ts` 菜单同步(/provider、/budget、/plan)
168
+
169
+ ## 错误处理
170
+
171
+ | 场景 | 行为 |
172
+ |------|------|
173
+ | --provider 指定不存在 | 报错 `Provider "x" not found` exit 1 |
174
+ | 无 providers 且无旧配置 | isConfigured false,"Run: min-agent setup" |
175
+ | 预算超限 | 回合结束提示,不中断;/budget 调高后继续 |
176
+ | 日志写入失败 | 静默忽略(log 内 try/catch),不阻塞主流程 |
177
+ | 迁移写回失败 | 内存中继续使用迁移后配置,忽略写回错误 |
178
+
179
+ ## 测试
180
+
181
+ | 文件 | 覆盖 |
182
+ |------|------|
183
+ | `tests/config.test.ts`(改) | 旧格式自动迁移(provider → providers[])、activeProvider 解析、getActiveProvider |
184
+ | `tests/provider.test.ts`(改) | resolveModel 按 activeProvider / providerName 覆盖 / 不存在报错 |
185
+ | `tests/logger.test.ts`(新) | 日志写入格式、日期滚动文件名、7 天清理(mock 时间或直接创建旧文件) |
186
+ | `tests/serve.test.ts`(改) | body.provider 覆盖、done 事件 budget_exceeded |
187
+ | 预算逻辑 | runOnceCore 结束检查(纯函数提取 `shouldStopForBudget(totalTokens, price, maxCostUSD)` 可测) |
188
+ | 采样回退 | config.sampling 生效(无 API options 时)—— 函数级验证 |
189
+
190
+ 验证命令(`packages/min-agent` 下):`bun test`、`bun run typecheck`(仓库根)、手动冒烟。
191
+
192
+ ## 文档同步清单
193
+
194
+ - `README.md`:Configuration 区、Commands、交互命令、日志
195
+ - `docs/API.md`:provider 字段、budget_exceeded
196
+ - `src/cli.ts` printUsage、TUI `/help`、`src/tui/slash-commands.ts` 菜单
@@ -0,0 +1,63 @@
1
+ # 输入框光标编辑设计
2
+
3
+ 日期:2026-08-16
4
+ 状态:已批准(用户确认设计)
5
+
6
+ ## 问题
7
+
8
+ min-agent TUI 输入框的光标固定在文本末尾:
9
+ - 左右键被显式忽略(InputBar.tsx:172),无法移动光标
10
+ - 点击输入框无任何反应(未启用终端鼠标协议)
11
+ - 用户无法在文本中间插入/删除内容
12
+
13
+ ## 目标
14
+
15
+ 1. 左右键移动光标(跨折行边界可达全文)
16
+ 2. 点击输入框定位光标
17
+ 3. 所有编辑操作(输入/退格/删除)在光标处进行
18
+
19
+ ## 设计
20
+
21
+ ### 1. 光标状态
22
+
23
+ InputBar 新增 `caretIndex` state,基于 code point 数组索引(`Array.from(value)`),兼容 CJK/emoji。
24
+
25
+ - 左右键:`caretIndex ± 1`;行首左移 → 上一行尾,行尾右移 → 下一行头
26
+ - 输入字符:插入到 `caretIndex`
27
+ - 退格:删 `caretIndex - 1`;Delete:删 `caretIndex`
28
+ - Enter/提交、斜杠菜单逻辑不变
29
+ - menuOpen 时:上下键选菜单,左右键不移动光标(保持现状)
30
+
31
+ ### 2. 点击定位(SGR 鼠标协议)
32
+
33
+ - 启用:`\x1b[?1000h\x1b[?1006h`(按下事件 + SGR 格式);卸载/卸载组件时:`\x1b[?1000l\x1b[?1006l`
34
+ - 鼠标序列在 InputBar 已有的 raw stdin 监听中解析(与 Kitty Shift+Enter 共用通道);useInput 中过滤残留片段(类似 `isKittySequenceFragment`)
35
+ - 输入框绝对位置:输入框渲染在 App footer,其起始行 = `terminalRows - 输入框视觉行数 + 1`;点击行落在输入框内容区内才处理
36
+ - 坐标换算:点击列 → 行内字符索引,逐字符按 `displayWidth` 累加,找到最接近点击列的边界(全角字符取最近边界)
37
+
38
+ ### 3. 光标渲染与硬件光标
39
+
40
+ - `█` 块移动到 `caretIndex` 处(渲染该行时插入);空值时在 gutter 后显示 `█`
41
+ - caret.ts 的 rowsAbove 计算:光标不再恒在最后一行,`rowsAbove = 输入框总行数 - 光标行号 + border/hint/menu 行数`;caret.ts 本身无需修改(接受任意 rowsAbove/column)
42
+
43
+ ### 4. 文件改动
44
+
45
+ | 文件 | 改动 |
46
+ |---|---|
47
+ | `src/tui/InputBar.tsx` | caretIndex state、左右键/插入/退格逻辑、`█` 渲染位置、鼠标启用与解析 |
48
+ | `src/tui/mouse.ts`(新) | 鼠标协议启用/禁用序列、`parseSgrMouse()` 纯函数 |
49
+ | `src/tui/caret-pos.ts`(新) | 视觉行 → 字符索引换算、光标跨行移动等纯函数 |
50
+
51
+ ### 5. 测试
52
+
53
+ - `parseSgrMouse` 序列解析(按下事件、坐标提取、非鼠标序列返回 null)
54
+ - 点击坐标 → caretIndex 换算(含 CJK 宽度、折行、空行)
55
+ - 左右键跨行移动边界(行首/行尾)
56
+ - 插入/退格/删除在光标处
57
+
58
+ ## 不做(YAGNI)
59
+
60
+ - 鼠标选中文本、拖拽选择
61
+ - 点击斜杠菜单项选择
62
+ - 上下键行间移动(左右键跨行已覆盖)
63
+ - Home/End、Ctrl+A/E 等快捷键