min-agent 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.md +32 -18
  2. package/dist/cli.js +538 -12
  3. package/docs/API.md +25 -8
  4. package/package.json +6 -6
  5. package/skills/self-config/SKILL.md +7 -3
  6. package/skills/self-config/reference.md +8 -4
  7. package/dist/agent.js +0 -1342
  8. package/dist/assistant-stream.js +0 -131
  9. package/dist/cli/commands/chat.js +0 -13
  10. package/dist/cli/commands/exec.js +0 -35
  11. package/dist/cli/commands/history.js +0 -58
  12. package/dist/cli/commands/index.js +0 -241
  13. package/dist/cli/commands/init.js +0 -18
  14. package/dist/cli/commands/mcp.js +0 -173
  15. package/dist/cli/commands/memory.js +0 -87
  16. package/dist/cli/commands/models.js +0 -21
  17. package/dist/cli/commands/permission.js +0 -12
  18. package/dist/cli/commands/rules.js +0 -33
  19. package/dist/cli/commands/sandbox.js +0 -13
  20. package/dist/cli/commands/serve.js +0 -9
  21. package/dist/cli/commands/setup.js +0 -4
  22. package/dist/cli/commands/shared.js +0 -16
  23. package/dist/cli/commands/skills.js +0 -119
  24. package/dist/cli/commands/think.js +0 -12
  25. package/dist/cli/commands/update.js +0 -7
  26. package/dist/cli/commands/write-config.js +0 -52
  27. package/dist/cli/errors.js +0 -36
  28. package/dist/cli/exec-prompt.js +0 -26
  29. package/dist/cli/option-helpers.js +0 -65
  30. package/dist/cli/program.js +0 -217
  31. package/dist/clipboard.js +0 -142
  32. package/dist/code-mode.js +0 -184
  33. package/dist/compaction.js +0 -644
  34. package/dist/config.js +0 -470
  35. package/dist/confirm.js +0 -172
  36. package/dist/context-window.js +0 -246
  37. package/dist/doom-loop.js +0 -278
  38. package/dist/fetch-timeout.js +0 -152
  39. package/dist/http-approvals.js +0 -60
  40. package/dist/http.js +0 -119
  41. package/dist/instructions.js +0 -196
  42. package/dist/logger.js +0 -95
  43. package/dist/markdown.js +0 -259
  44. package/dist/mcp.js +0 -1036
  45. package/dist/memory-cli.js +0 -33
  46. package/dist/memory.js +0 -295
  47. package/dist/model-catalog.js +0 -285
  48. package/dist/output.js +0 -76
  49. package/dist/paste-handler.js +0 -41
  50. package/dist/permission-cli.js +0 -40
  51. package/dist/plugins.js +0 -131
  52. package/dist/pricing.js +0 -119
  53. package/dist/provider.js +0 -63
  54. package/dist/question-format.js +0 -60
  55. package/dist/reasoning-stream.js +0 -158
  56. package/dist/sandbox-cli.js +0 -79
  57. package/dist/sandbox.js +0 -403
  58. package/dist/save-throttle.js +0 -45
  59. package/dist/scope.js +0 -23
  60. package/dist/serve/common.js +0 -425
  61. package/dist/serve/routes-chat.js +0 -367
  62. package/dist/serve/routes-mcp.js +0 -212
  63. package/dist/serve/routes-memory.js +0 -95
  64. package/dist/serve/routes-meta.js +0 -233
  65. package/dist/serve/routes-sessions.js +0 -61
  66. package/dist/serve/routes-skills.js +0 -70
  67. package/dist/serve.js +0 -138
  68. package/dist/sessions.js +0 -265
  69. package/dist/skills.js +0 -599
  70. package/dist/structured-output.js +0 -29
  71. package/dist/synthetic.js +0 -7
  72. package/dist/think-cli.js +0 -36
  73. package/dist/thinking-wire.js +0 -228
  74. package/dist/thinking.js +0 -142
  75. package/dist/title-gen.js +0 -55
  76. package/dist/token-display.js +0 -39
  77. package/dist/tool-display.js +0 -178
  78. package/dist/tool-output.js +0 -126
  79. package/dist/tools/apply_patch.js +0 -265
  80. package/dist/tools/atomic-file.js +0 -35
  81. package/dist/tools/backend.js +0 -61
  82. package/dist/tools/bash.js +0 -231
  83. package/dist/tools/code_search.js +0 -98
  84. package/dist/tools/edit.js +0 -68
  85. package/dist/tools/explore.js +0 -232
  86. package/dist/tools/glob.js +0 -40
  87. package/dist/tools/grep.js +0 -174
  88. package/dist/tools/index.js +0 -34
  89. package/dist/tools/question.js +0 -54
  90. package/dist/tools/read.js +0 -109
  91. package/dist/tools/search-searxng.js +0 -223
  92. package/dist/tools/search-serper.js +0 -189
  93. package/dist/tools/task.js +0 -165
  94. package/dist/tools/todo.js +0 -213
  95. package/dist/tools/web_fetch.js +0 -195
  96. package/dist/tools/web_search.js +0 -228
  97. package/dist/tools/write.js +0 -55
  98. package/dist/tui/App.js +0 -126
  99. package/dist/tui/ConfirmBar.js +0 -66
  100. package/dist/tui/InputBar.js +0 -616
  101. package/dist/tui/MessageList.js +0 -718
  102. package/dist/tui/ModelPicker.js +0 -113
  103. package/dist/tui/QuestionBar.js +0 -136
  104. package/dist/tui/SessionPicker.js +0 -79
  105. package/dist/tui/Spinner.js +0 -6
  106. package/dist/tui/StatusBar.js +0 -22
  107. package/dist/tui/ThinkPicker.js +0 -77
  108. package/dist/tui/agent-runner.js +0 -223
  109. package/dist/tui/bracketed-paste.js +0 -37
  110. package/dist/tui/caret-pos.js +0 -179
  111. package/dist/tui/caret.js +0 -69
  112. package/dist/tui/click-count.js +0 -13
  113. package/dist/tui/diff-view.js +0 -61
  114. package/dist/tui/drag-state.js +0 -49
  115. package/dist/tui/hydrate.js +0 -129
  116. package/dist/tui/index.js +0 -258
  117. package/dist/tui/input-history.js +0 -125
  118. package/dist/tui/layout.js +0 -105
  119. package/dist/tui/mouse.js +0 -46
  120. package/dist/tui/overlay-input.js +0 -12
  121. package/dist/tui/paste-draft.js +0 -173
  122. package/dist/tui/prompt-queue.js +0 -24
  123. package/dist/tui/selection.js +0 -232
  124. package/dist/tui/session-switch.js +0 -28
  125. package/dist/tui/slash-commands.js +0 -123
  126. package/dist/tui/slash-handler.js +0 -589
  127. package/dist/tui/text-width.js +0 -113
  128. package/dist/tui/theme.js +0 -12
  129. package/dist/tui/token-info.js +0 -7
  130. package/dist/tui/tool-children.js +0 -19
  131. package/dist/tui/types.js +0 -1
  132. package/dist/tui/undo-stack.js +0 -14
  133. package/dist/tui/use-sgr-mouse.js +0 -29
  134. package/dist/tui-chat.js +0 -456
  135. package/dist/updater.js +0 -116
  136. package/dist/xml-search.js +0 -194
  137. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
  138. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
  139. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
  140. package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
  141. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
  142. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
  143. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
  144. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
  145. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
  146. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
  147. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
  148. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
  149. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
  150. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
  151. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
  152. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
  153. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
  154. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
  155. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
  156. package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +0 -140
@@ -1,450 +0,0 @@
1
- # 思考线协议适配 — 实现计划
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** 用户仍只配置 `off|low|medium|high|max`;发出 chat 请求时按 `(host, model)` 选择 `thinking.type` / `reasoning_effort` 的线协议。MiniMax 启发式先发 `adaptive`;其余若 400 且错误可解析,静默改 profile、同一请求只重试一次,并缓存 7 天。
6
-
7
- **Architecture:** 用户意图留在 `ThinkingRequestBody`(ALS)。新建 `thinking-wire.ts`:启发式、学习缓存、`parseThinkingWireHint`、`fetchWithThinkingWire`。`applyThinkingToChatBody` 增加 profile 参数。`modelFetch` 改为 `fetchWithThinkingWire(inner, …).then(foldReasoningInChatResponse)`。Lonae 目录不改。
8
-
9
- **Tech Stack:** TypeScript, bun:test
10
-
11
- **Spec:** `docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md`
12
-
13
- 测试命令(`packages/min-agent` 下):`bun test tests/<file>`;类型:`npx tsc --noEmit`;仓库根目录:`bun run typecheck`、`bun run check`。
14
-
15
- ---
16
-
17
- ## 文件结构
18
-
19
- | 文件 | 职责 |
20
- |------|------|
21
- | `src/thinking-wire.ts`(新) | profile 类型、默认值、缓存键、启发式、缓存、hint 解析、`fetchWithThinkingWire` |
22
- | `src/thinking.ts`(改) | `applyThinkingToChatBody(body, spec, profile?)` |
23
- | `src/provider.ts`(改) | `modelFetch` 走 `fetchWithThinkingWire` |
24
- | `tests/thinking-wire.test.ts`(新) | 启发式、hint、缓存、fetch 重试 |
25
- | `tests/thinking.test.ts`(改) | 三种 thinkingType × 开/关、effort omit |
26
- | `skills/self-config/SKILL.md`(改) | 映射说明 + 缓存文件名 |
27
- | `skills/self-config/reference.md`(改) | `MIN_AGENT_CONFIG_DIR` 旁注明缓存文件 |
28
-
29
- `docs/API.md`、`README.md`、`/help`、Lonae catalog、`/think` UI **不改**。
30
-
31
- ---
32
-
33
- ## Task 1: thinking-wire 纯函数与缓存
34
-
35
- **Files:**
36
- - Create: `src/thinking-wire.ts`
37
- - Test: `tests/thinking-wire.test.ts`
38
-
39
- - [ ] **Step 1: 写失败测试**
40
-
41
- ```ts
42
- import { expect, test, afterEach } from "bun:test"
43
- import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "fs"
44
- import os from "os"
45
- import path from "path"
46
- import {
47
- DEFAULT_THINKING_WIRE,
48
- THINKING_WIRE_TTL,
49
- clearThinkingWireCache,
50
- inferThinkingWire,
51
- parseThinkingWireHint,
52
- resolveThinkingWireProfile,
53
- saveLearnedThinkingWire,
54
- thinkingWireCacheKey,
55
- } from "../src/thinking-wire.js"
56
-
57
- function withIsolatedConfig(fn: () => void): void {
58
- const home = realpathSync(mkdtempSync(path.join(os.tmpdir(), "min-agent-wire-")))
59
- const old = process.env.MIN_AGENT_CONFIG_DIR
60
- process.env.MIN_AGENT_CONFIG_DIR = home
61
- clearThinkingWireCache()
62
- try {
63
- fn()
64
- } finally {
65
- clearThinkingWireCache()
66
- if (old) process.env.MIN_AGENT_CONFIG_DIR = old
67
- else delete process.env.MIN_AGENT_CONFIG_DIR
68
- rmSync(home, { recursive: true, force: true })
69
- }
70
- }
71
-
72
- afterEach(() => clearThinkingWireCache())
73
-
74
- test("cache key lowercases host and model", () => {
75
- expect(thinkingWireCacheKey("Api.Example.COM", "MiniMax-M3")).toBe("api.example.com::minimax-m3")
76
- expect(thinkingWireCacheKey("", "m")).toBe("m")
77
- })
78
-
79
- test("inferThinkingWire prefers minimax then openai then default", () => {
80
- expect(inferThinkingWire("minimax-m3", "api.console.go", "openai-compatible")).toEqual({
81
- thinkingType: "adaptive-disabled",
82
- effort: "reasoning_effort",
83
- })
84
- expect(inferThinkingWire("gpt-4", "api.minimax.io", undefined)).toEqual({
85
- thinkingType: "adaptive-disabled",
86
- effort: "reasoning_effort",
87
- })
88
- expect(inferThinkingWire("gpt-5", "api.openai.com", "openai")).toEqual({
89
- thinkingType: "omit",
90
- effort: "reasoning_effort",
91
- })
92
- expect(inferThinkingWire("deepseek-v3", "api.deepseek.com", "openai-compatible")).toEqual({
93
- thinkingType: "enabled-disabled",
94
- effort: "reasoning_effort",
95
- })
96
- })
97
-
98
- test("parseThinkingWireHint reads MiniMax allowed list", () => {
99
- const raw =
100
- 'AI_APICallError: Error from provider (Console Go): Upstream request failed: [bad_request_error] invalid params, invalid thinking.type: "enabled" (allowed: adaptive, disabled) (2013)'
101
- expect(parseThinkingWireHint(raw)).toEqual({ thinkingType: "adaptive-disabled" })
102
- expect(parseThinkingWireHint('invalid thinking.type: "adaptive" (allowed: enabled, disabled)')).toEqual({
103
- thinkingType: "enabled-disabled",
104
- })
105
- })
106
-
107
- test("parseThinkingWireHint reads unknown parameter", () => {
108
- expect(parseThinkingWireHint("Unknown parameter: 'thinking'")).toEqual({ thinkingType: "omit" })
109
- expect(parseThinkingWireHint("unknown parameter: reasoning_effort")).toEqual({ effort: "omit" })
110
- expect(parseThinkingWireHint("rate limit exceeded")).toBeNull()
111
- })
112
-
113
- test("resolveThinkingWireProfile prefers learned cache over heuristic", () => {
114
- withIsolatedConfig(() => {
115
- expect(resolveThinkingWireProfile("h.example", "minimax-m3").thinkingType).toBe("adaptive-disabled")
116
- saveLearnedThinkingWire("h.example", "minimax-m3", {
117
- ...DEFAULT_THINKING_WIRE,
118
- thinkingType: "omit",
119
- effort: "omit",
120
- })
121
- const learned = resolveThinkingWireProfile("h.example", "minimax-m3")
122
- expect(learned.thinkingType).toBe("omit")
123
- expect(learned.effort).toBe("omit")
124
- const disk = JSON.parse(readFileSync(path.join(process.env.MIN_AGENT_CONFIG_DIR!, "thinking-wire-cache.json"), "utf-8"))
125
- expect(disk["h.example::minimax-m3"].thinkingType).toBe("omit")
126
- })
127
- })
128
-
129
- test("expired or wrong version cache is ignored", () => {
130
- withIsolatedConfig(() => {
131
- const file = path.join(process.env.MIN_AGENT_CONFIG_DIR!, "thinking-wire-cache.json")
132
- writeFileSync(
133
- file,
134
- JSON.stringify({
135
- "h::m": { version: 1, thinkingType: "omit", effort: "omit", timestamp: Date.now() - THINKING_WIRE_TTL - 1 },
136
- "h::old": { version: 99, thinkingType: "omit", effort: "omit", timestamp: Date.now() },
137
- }),
138
- )
139
- expect(resolveThinkingWireProfile("h", "m", "openai-compatible").thinkingType).toBe("enabled-disabled")
140
- expect(resolveThinkingWireProfile("h", "old", "openai-compatible").thinkingType).toBe("enabled-disabled")
141
- })
142
- })
143
- ```
144
-
145
- 先跑确认失败:
146
-
147
- ```bash
148
- cd packages/min-agent && bun test tests/thinking-wire.test.ts
149
- ```
150
-
151
- - [ ] **Step 2: 实现**
152
-
153
- `src/thinking-wire.ts`(与 `model-catalog.ts` 同样用 `getConfigDir()` + `atomicWriteFileSync`):
154
-
155
- ```ts
156
- export const THINKING_WIRE_VERSION = 1
157
- export const THINKING_WIRE_TTL = 7 * 24 * 60 * 60 * 1000
158
- export const DEFAULT_THINKING_WIRE = {
159
- version: THINKING_WIRE_VERSION,
160
- thinkingType: "enabled-disabled" as const,
161
- effort: "reasoning_effort" as const,
162
- }
163
-
164
- export type ThinkingTypeMode = "enabled-disabled" | "adaptive-disabled" | "omit"
165
- export type EffortMode = "reasoning_effort" | "omit"
166
- export interface ThinkingWireProfile {
167
- version: number
168
- thinkingType: ThinkingTypeMode
169
- effort: EffortMode
170
- timestamp: number
171
- }
172
- export type ThinkingWireHint = Partial<Pick<ThinkingWireProfile, "thinkingType" | "effort">>
173
- ```
174
-
175
- - `thinkingWireCacheKey(host, modelId)`:都 `toLowerCase()`;host 空则只要 modelId
176
- - `inferThinkingWire(modelId, host, providerType?)`:`minimax` 出现在 model 或 host(大小写不敏感)→ adaptive-disabled;`providerType === "openai"` → omit thinking;否则默认
177
- - `resolveThinkingWireProfile(host, modelId, providerType?)`:有效学习缓存 → 否则启发式填 `timestamp: Date.now()`(启发式**不写盘**)
178
- - `saveLearnedThinkingWire` 写内存 + `thinking-wire-cache.json`
179
- - `clearThinkingWireCache()` 清内存(测试用);磁盘随 `MIN_AGENT_CONFIG_DIR` 隔离
180
- - `parseThinkingWireHint(text)`:
181
- - `/thinking\.type[\s\S]*?allowed:\s*([^)]+)/i`,列表按逗号拆、trim、去引号、小写;含 `adaptive` 且含 `disabled` → `adaptive-disabled`;含 `enabled` 且含 `disabled` → `enabled-disabled`
182
- - `/unknown parameter[:\s]+['"`]?thinking['"`]?/i` → omit thinking(不要误匹配 `thinking.type` 那段里的 thinking)
183
- - `/unknown parameter[:\s]+['"`]?reasoning_effort['"`]?/i` → omit effort
184
- - 都不中 → `null`
185
- - 未知 `version` 或过期 → 当未命中。损坏 JSON → `{}`
186
-
187
- 本任务**不要**实现 fetch 包装。
188
-
189
- - [ ] **Step 3: 测试通过并提交**
190
-
191
- ```bash
192
- cd packages/min-agent && bun test tests/thinking-wire.test.ts
193
- git add src/thinking-wire.ts tests/thinking-wire.test.ts
194
- git commit -m "feat: 增加思考线协议 profile 与错误 hint 解析"
195
- ```
196
-
197
- ---
198
-
199
- ## Task 2: applyThinkingToChatBody 按 profile 拼 body
200
-
201
- **Files:**
202
- - Modify: `src/thinking.ts`
203
- - Modify: `tests/thinking.test.ts`
204
-
205
- - [ ] **Step 1: 改测试**
206
-
207
- 把现有「injects reasoning_effort and thinking.type」扩成显式 profile(缺省第三参 = `DEFAULT_THINKING_WIRE` 的 type/effort):
208
-
209
- ```ts
210
- import { DEFAULT_THINKING_WIRE } from "../src/thinking-wire.js"
211
-
212
- test("applyThinkingToChatBody follows thinkingType and effort modes", () => {
213
- const specOn = { reasoningEffort: "high", thinkingEnabled: true }
214
- const specOff = { reasoningEffort: "none", thinkingEnabled: false }
215
- expect(applyThinkingToChatBody({ model: "m" }, specOn)).toEqual({
216
- model: "m",
217
- reasoning_effort: "high",
218
- thinking: { type: "enabled" },
219
- })
220
- expect(
221
- applyThinkingToChatBody({ model: "m" }, specOn, { ...DEFAULT_THINKING_WIRE, thinkingType: "adaptive-disabled" }),
222
- ).toEqual({
223
- model: "m",
224
- reasoning_effort: "high",
225
- thinking: { type: "adaptive" },
226
- })
227
- expect(applyThinkingToChatBody({ model: "m" }, specOff, { ...DEFAULT_THINKING_WIRE, thinkingType: "adaptive-disabled" })).toEqual({
228
- model: "m",
229
- reasoning_effort: "none",
230
- thinking: { type: "disabled" },
231
- })
232
- expect(applyThinkingToChatBody({ thinking: { type: "enabled" }, reasoning_effort: "x" }, specOn, {
233
- ...DEFAULT_THINKING_WIRE,
234
- thinkingType: "omit",
235
- effort: "omit",
236
- })).toEqual({})
237
- })
238
- ```
239
-
240
- - [ ] **Step 2: 实现**
241
-
242
- ```ts
243
- export function applyThinkingToChatBody(
244
- body: Record<string, unknown>,
245
- spec: ThinkingRequestBody,
246
- profile: Pick<ThinkingWireProfile, "thinkingType" | "effort"> = DEFAULT_THINKING_WIRE,
247
- ): Record<string, unknown> {
248
- const next: Record<string, unknown> = { ...body }
249
- if (profile.effort === "omit") delete next.reasoning_effort
250
- else next.reasoning_effort = spec.reasoningEffort
251
- if (profile.thinkingType === "omit") delete next.thinking
252
- else {
253
- const on = profile.thinkingType === "adaptive-disabled" ? "adaptive" : "enabled"
254
- next.thinking = { type: spec.thinkingEnabled ? on : "disabled" }
255
- }
256
- return next
257
- }
258
- ```
259
-
260
- `ThinkingRequestBody` 不变。`resolveThinkingRequest` / `toWireEffort` 不变。
261
-
262
- - [ ] **Step 3: 测试通过并提交**
263
-
264
- ```bash
265
- cd packages/min-agent && bun test tests/thinking.test.ts
266
- git add src/thinking.ts tests/thinking.test.ts
267
- git commit -m "feat: 按 wire profile 组装思考请求体"
268
- ```
269
-
270
- ---
271
-
272
- ## Task 3: fetch 400 改参并只重试一次
273
-
274
- **Files:**
275
- - Modify: `src/thinking-wire.ts`
276
- - Modify: `tests/thinking-wire.test.ts`
277
-
278
- - [ ] **Step 1: 写失败测试**
279
-
280
- 在 `thinking-wire.test.ts` 增加(仍用隔离 `MIN_AGENT_CONFIG_DIR` + `thinkingRequestStore.run`):
281
-
282
- ```ts
283
- import { thinkingRequestStore } from "../src/thinking.js"
284
- import { fetchWithThinkingWire } from "../src/thinking-wire.js"
285
-
286
- test("fetchWithThinkingWire retries once when 400 names adaptive", async () => {
287
- withIsolatedConfig(() => {
288
- const bodies: string[] = []
289
- const inner: FetchLike = async (_input, init) => {
290
- bodies.push(String(init?.body ?? ""))
291
- if (bodies.length === 1) {
292
- return new Response(
293
- 'invalid thinking.type: "enabled" (allowed: adaptive, disabled)',
294
- { status: 400 },
295
- )
296
- }
297
- return new Response("{}", { status: 200 })
298
- }
299
- return thinkingRequestStore.run({ reasoningEffort: "high", thinkingEnabled: true }, async () => {
300
- const res = await fetchWithThinkingWire(inner, "https://gw.example/v1/chat/completions", {
301
- method: "POST",
302
- body: JSON.stringify({ model: "other-model", messages: [] }),
303
- })
304
- expect(res.status).toBe(200)
305
- expect(bodies).toHaveLength(2)
306
- expect(JSON.parse(bodies[0]!).thinking).toEqual({ type: "enabled" })
307
- expect(JSON.parse(bodies[1]!).thinking).toEqual({ type: "adaptive" })
308
- })
309
- })
310
- })
311
-
312
- test("fetchWithThinkingWire does not retry unrelated 400", async () => {
313
- withIsolatedConfig(() => {
314
- let n = 0
315
- const inner: FetchLike = async () => {
316
- n++
317
- return new Response("insufficient quota", { status: 400 })
318
- }
319
- return thinkingRequestStore.run({ reasoningEffort: "high", thinkingEnabled: true }, async () => {
320
- const res = await fetchWithThinkingWire(inner, "https://gw.example/v1/chat/completions", {
321
- method: "POST",
322
- body: JSON.stringify({ model: "m" }),
323
- })
324
- expect(res.status).toBe(400)
325
- expect(await res.text()).toBe("insufficient quota")
326
- expect(n).toBe(1)
327
- })
328
- })
329
- })
330
- ```
331
-
332
- `withIsolatedConfig` 若目前是同步的,改成支持 `fn: () => void | Promise<void>` 并 `await`。`FetchLike` 从 `fetch-timeout.js` 导入。
333
-
334
- 再加一条:hint 相对当前 profile 无变化(已经是 adaptive,400 仍说 allowed adaptive)→ 不重试。可用 minimax 模型名让第一次就发 adaptive,inner 只 400 一次。
335
-
336
- - [ ] **Step 2: 实现 `fetchWithThinkingWire`**
337
-
338
- 签名:
339
-
340
- ```ts
341
- export async function fetchWithThinkingWire(
342
- inner: FetchLike,
343
- input: string | URL | Request,
344
- init?: RequestInit,
345
- opts?: { providerType?: string },
346
- ): Promise<Response>
347
- ```
348
-
349
- 逻辑:
350
-
351
- 1. `spec = thinkingRequestStore.getStore()`;无 spec 或 `init.body` 非字符串 → `return inner(input, init)`
352
- 2. JSON.parse body;非对象 → `return inner(input, init)`
353
- 3. `host` 从 `input` 解析 `URL.hostname`(`Request` 用 `.url`);失败则 `""`
354
- 4. `modelId` = body.model 字符串,否则 `""`
355
- 5. `providerType` = `opts?.providerType ?? getActiveProvider(getEffectiveConfig())?.type`
356
- 6. `profile = resolveThinkingWireProfile(host, modelId, providerType)`
357
- 7. `firstInit = { ...init, body: JSON.stringify(applyThinkingToChatBody(parsed, spec, profile)) }`
358
- 8. `first = await inner(input, firstInit)`;`first.status !== 400` → `return first`
359
- 9. `text = await first.text()`;`hint = parseThinkingWireHint(text)`
360
- 10. 合并 hint:`next = { ...profile, ...hint, version: 1, timestamp: Date.now() }`;`thinkingType` 与 `effort` 都与当前相同 → `return rebuild400(first, text)`
361
- 11. `saveLearnedThinkingWire(host, modelId, next)`;第二次 `inner(input, { ...init, body: JSON.stringify(applyThinkingToChatBody(parsed, spec, next)) })`,**不再**根据第二次状态重试
362
- 12. `rebuild400`:`new Response(text, { status: 400, statusText: first.statusText, headers: first.headers })`
363
-
364
- `getActiveProvider` 在 `thinking-wire.ts` 引用 `config.js` 可以;测试不依赖真实 config(默认走 enabled-disabled)。
365
-
366
- - [ ] **Step 3: 测试通过并提交**
367
-
368
- ```bash
369
- cd packages/min-agent && bun test tests/thinking-wire.test.ts
370
- git add src/thinking-wire.ts tests/thinking-wire.test.ts
371
- git commit -m "feat: 思考字段 400 时改 profile 并只重试一次"
372
- ```
373
-
374
- ---
375
-
376
- ## Task 4: 接入 modelFetch
377
-
378
- **Files:**
379
- - Modify: `src/provider.ts`
380
-
381
- - [ ] **Step 1: 接线**
382
-
383
- 删除 `withThinkingBody`。`modelFetch`:
384
-
385
- ```ts
386
- return (input, init) => fetchWithThinkingWire(inner, input, init).then(foldReasoningInChatResponse)
387
- ```
388
-
389
- 从 `thinking-wire.js` 导入 `fetchWithThinkingWire`;`thinking.ts` 不再被 `provider.ts` 直接用来拼 body(ALS 仍由 agent 设置,fetch 内部读)。
390
-
391
- 现有 `tests/provider.test.ts` 应仍通过(不发真实请求)。
392
-
393
- - [ ] **Step 2: 测试并提交**
394
-
395
- ```bash
396
- cd packages/min-agent && bun test tests/provider.test.ts tests/thinking-wire.test.ts tests/thinking.test.ts
397
- git add src/provider.ts
398
- git commit -m "feat: 模型请求走思考线协议适配"
399
- ```
400
-
401
- ---
402
-
403
- ## Task 5: self-config 文档
404
-
405
- **Files:**
406
- - Modify: `skills/self-config/SKILL.md`
407
- - Modify: `skills/self-config/reference.md`
408
-
409
- - [ ] **Step 1: 文案**
410
-
411
- `SKILL.md` 思考段落里「The request sends `reasoning_effort` plus `thinking.type`」改为:请求会按当前模型和网关映射思考字段(例如 MiniMax 用 `adaptive`/`disabled`,官方 OpenAI 不发 `thinking` 对象);学到的映射缓存在 `~/.min-agent/thinking-wire-cache.json`(7 天)。官方 OpenAI 仍把 `max` 映射为 `xhigh`。不要写 400 重试、fetch 包装、profile 类型名。
412
-
413
- `reference.md` 环境变量表 `MIN_AGENT_CONFIG_DIR` 一行改为同时提到目录缓存与思考映射缓存,例如:`Replaces ~/.min-agent`(含 `model-catalog-cache.json`、`thinking-wire-cache.json`)。
414
-
415
- README / API.md / `/help` 不改。
416
-
417
- - [ ] **Step 2: 提交**
418
-
419
- ```bash
420
- git add packages/min-agent/skills/self-config/SKILL.md packages/min-agent/skills/self-config/reference.md
421
- git commit -m "docs: 说明思考请求按模型映射线协议"
422
- ```
423
-
424
- (在仓库根目录 add 时用上表路径;在 `packages/min-agent` 内则用 `skills/self-config/…`。)
425
-
426
- ---
427
-
428
- ## Task 6: 回归
429
-
430
- - [ ] **Step 1: 全量相关测试与检查**
431
-
432
- ```bash
433
- cd packages/min-agent && bun test tests/thinking.test.ts tests/thinking-wire.test.ts tests/provider.test.ts
434
- # 仓库根目录
435
- bun run typecheck
436
- bun run check
437
- ```
438
-
439
- - [ ] **Step 2: 若有格式问题则 `bun run format` 后再 check,不要 `--no-verify`**
440
-
441
- ---
442
-
443
- ## 手工验收
444
-
445
- 在 `packages/min-agent` 下用 MiniMax-M3(思考非 off)发一句 `hello`:
446
-
447
- 1. 不应再出现 `invalid thinking.type: "enabled"`
448
- 2. 状态栏仍是「思考 high」这类用户档位,不出现 `adaptive`
449
- 3. 换一个会 400 且错误带 `allowed: adaptive, disabled` 的网关:第一次失败后同一句应自动成功;`~/.min-agent/thinking-wire-cache.json` 出现对应 host+model 条目
450
- 4. `/think off` 发送 `disabled`,对话仍可用
@@ -1,183 +0,0 @@
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 属第二批,届时同步)