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.
- package/README.md +32 -18
- package/dist/cli.js +538 -12
- package/docs/API.md +25 -8
- package/package.json +6 -6
- package/skills/self-config/SKILL.md +7 -3
- package/skills/self-config/reference.md +8 -4
- package/dist/agent.js +0 -1342
- package/dist/assistant-stream.js +0 -131
- package/dist/cli/commands/chat.js +0 -13
- package/dist/cli/commands/exec.js +0 -35
- package/dist/cli/commands/history.js +0 -58
- package/dist/cli/commands/index.js +0 -241
- package/dist/cli/commands/init.js +0 -18
- package/dist/cli/commands/mcp.js +0 -173
- package/dist/cli/commands/memory.js +0 -87
- package/dist/cli/commands/models.js +0 -21
- package/dist/cli/commands/permission.js +0 -12
- package/dist/cli/commands/rules.js +0 -33
- package/dist/cli/commands/sandbox.js +0 -13
- package/dist/cli/commands/serve.js +0 -9
- package/dist/cli/commands/setup.js +0 -4
- package/dist/cli/commands/shared.js +0 -16
- package/dist/cli/commands/skills.js +0 -119
- package/dist/cli/commands/think.js +0 -12
- package/dist/cli/commands/update.js +0 -7
- package/dist/cli/commands/write-config.js +0 -52
- package/dist/cli/errors.js +0 -36
- package/dist/cli/exec-prompt.js +0 -26
- package/dist/cli/option-helpers.js +0 -65
- package/dist/cli/program.js +0 -217
- package/dist/clipboard.js +0 -142
- package/dist/code-mode.js +0 -184
- package/dist/compaction.js +0 -644
- package/dist/config.js +0 -470
- package/dist/confirm.js +0 -172
- package/dist/context-window.js +0 -246
- package/dist/doom-loop.js +0 -278
- package/dist/fetch-timeout.js +0 -152
- package/dist/http-approvals.js +0 -60
- package/dist/http.js +0 -119
- package/dist/instructions.js +0 -196
- package/dist/logger.js +0 -95
- package/dist/markdown.js +0 -259
- package/dist/mcp.js +0 -1036
- package/dist/memory-cli.js +0 -33
- package/dist/memory.js +0 -295
- package/dist/model-catalog.js +0 -285
- package/dist/output.js +0 -76
- package/dist/paste-handler.js +0 -41
- package/dist/permission-cli.js +0 -40
- package/dist/plugins.js +0 -131
- package/dist/pricing.js +0 -119
- package/dist/provider.js +0 -63
- package/dist/question-format.js +0 -60
- package/dist/reasoning-stream.js +0 -158
- package/dist/sandbox-cli.js +0 -79
- package/dist/sandbox.js +0 -403
- package/dist/save-throttle.js +0 -45
- package/dist/scope.js +0 -23
- package/dist/serve/common.js +0 -425
- package/dist/serve/routes-chat.js +0 -367
- package/dist/serve/routes-mcp.js +0 -212
- package/dist/serve/routes-memory.js +0 -95
- package/dist/serve/routes-meta.js +0 -233
- package/dist/serve/routes-sessions.js +0 -61
- package/dist/serve/routes-skills.js +0 -70
- package/dist/serve.js +0 -138
- package/dist/sessions.js +0 -265
- package/dist/skills.js +0 -599
- package/dist/structured-output.js +0 -29
- package/dist/synthetic.js +0 -7
- package/dist/think-cli.js +0 -36
- package/dist/thinking-wire.js +0 -228
- package/dist/thinking.js +0 -142
- package/dist/title-gen.js +0 -55
- package/dist/token-display.js +0 -39
- package/dist/tool-display.js +0 -178
- package/dist/tool-output.js +0 -126
- package/dist/tools/apply_patch.js +0 -265
- package/dist/tools/atomic-file.js +0 -35
- package/dist/tools/backend.js +0 -61
- package/dist/tools/bash.js +0 -231
- package/dist/tools/code_search.js +0 -98
- package/dist/tools/edit.js +0 -68
- package/dist/tools/explore.js +0 -232
- package/dist/tools/glob.js +0 -40
- package/dist/tools/grep.js +0 -174
- package/dist/tools/index.js +0 -34
- package/dist/tools/question.js +0 -54
- package/dist/tools/read.js +0 -109
- package/dist/tools/search-searxng.js +0 -223
- package/dist/tools/search-serper.js +0 -189
- package/dist/tools/task.js +0 -165
- package/dist/tools/todo.js +0 -213
- package/dist/tools/web_fetch.js +0 -195
- package/dist/tools/web_search.js +0 -228
- package/dist/tools/write.js +0 -55
- package/dist/tui/App.js +0 -126
- package/dist/tui/ConfirmBar.js +0 -66
- package/dist/tui/InputBar.js +0 -616
- package/dist/tui/MessageList.js +0 -718
- package/dist/tui/ModelPicker.js +0 -113
- package/dist/tui/QuestionBar.js +0 -136
- package/dist/tui/SessionPicker.js +0 -79
- package/dist/tui/Spinner.js +0 -6
- package/dist/tui/StatusBar.js +0 -22
- package/dist/tui/ThinkPicker.js +0 -77
- package/dist/tui/agent-runner.js +0 -223
- package/dist/tui/bracketed-paste.js +0 -37
- package/dist/tui/caret-pos.js +0 -179
- package/dist/tui/caret.js +0 -69
- package/dist/tui/click-count.js +0 -13
- package/dist/tui/diff-view.js +0 -61
- package/dist/tui/drag-state.js +0 -49
- package/dist/tui/hydrate.js +0 -129
- package/dist/tui/index.js +0 -258
- package/dist/tui/input-history.js +0 -125
- package/dist/tui/layout.js +0 -105
- package/dist/tui/mouse.js +0 -46
- package/dist/tui/overlay-input.js +0 -12
- package/dist/tui/paste-draft.js +0 -173
- package/dist/tui/prompt-queue.js +0 -24
- package/dist/tui/selection.js +0 -232
- package/dist/tui/session-switch.js +0 -28
- package/dist/tui/slash-commands.js +0 -123
- package/dist/tui/slash-handler.js +0 -589
- package/dist/tui/text-width.js +0 -113
- package/dist/tui/theme.js +0 -12
- package/dist/tui/token-info.js +0 -7
- package/dist/tui/tool-children.js +0 -19
- package/dist/tui/types.js +0 -1
- package/dist/tui/undo-stack.js +0 -14
- package/dist/tui/use-sgr-mouse.js +0 -29
- package/dist/tui-chat.js +0 -456
- package/dist/updater.js +0 -116
- package/dist/xml-search.js +0 -194
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
- package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
- package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
- package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
- package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
- package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
- 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 属第二批,届时同步)
|