thincoder 0.12.48 → 0.12.50
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/CHANGELOG.md +32 -0
- package/package.json +1 -1
- package/src/agent/setup.mjs +4 -4
- package/src/agent-tools/subagent.mjs +8 -2
- package/src/config.mjs +30 -0
- package/src/escape.mjs +15 -2
- package/src/prompts/eng-coder.md +1 -0
- package/src/prompts/engineering.md +20 -5
- package/src/prompts/methodology-template.md +8 -3
- package/src/provider/core.mjs +6 -1
- package/src/session.mjs +8 -0
- package/src/tui/clipboard.mjs +6 -0
- package/src/tui/cmd-advisor.mjs +20 -2
- package/src/tui/cmd-eng.mjs +44 -7
- package/src/tui/cmd-think.mjs +17 -3
- package/src/tui/slash-commands.mjs +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录 ThinCoder CLI 的发布历史。格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),版本遵循[语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [0.12.50] — 2026-08-29
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **engineering 与 advisor.guard 改为会话级(跨端污染修复)**:旧设计里 engineering 只存 config.json 全局(`agent.engineering`),CLI `/eng` 与 VS Code 面板都写它 → 两端互相翻转对方的工程模式("VS Code 工程模式下模型仍委托 role='coder'")。现事实源是当前会话槽位文件(`engineering` 字段 + `advisor.guard`),config.json 降为 CLI 兼容/可见性镜像(双写保留:slot 先、config 后,slot 失败不阻断)。改动:`/eng` toggle 双写 slot(`cmd-eng.mjs` persistEngineering);`/advisor` guard 切换双写 slot(`cmd-advisor.mjs` persistGuard,model/thinking 仍 config-scoped);`saveSession`/`applySession` 往返 slot 值(无字段旧槽位回退 config,兼容锁定);启动恢复链 `bin/thincoder.mjs` applySession 时 slot 覆盖 config 播种值。权威文档:`docs/design/ENGINEERING-MODE.md` §5 重写
|
|
10
|
+
- **METHODOLOGY 三缺口修复**(核对工程模式提示词时发现):① 需求文档三层结构(总目标/功能用户故事/非功能标准)落地进 METHODOLOGY.md 与 methodology-template.md——engineering.md 的 "three layers per METHODOLOGY" 引用此前悬空;② engineering.md 交付评审补测试文档口径(METHODOLOGY 存在时每条用户故事须有测试用例覆盖正常/边界/异常,无覆盖=评审不通过)——与三文档硬流程对齐;③ METHODOLOGY.md 缺失警告点名后果(引用悬空+硬流程失效+恢复路径),不再静默降级
|
|
11
|
+
- **工程模式 UI/交互决策全链路落档**(用户报告"agent 无视讨论过的 UI 设计"):设计文档要素扩项——涉及界面时必须收录与用户达成的每条 UI/交互决策(布局/流程/控件行为/状态反馈),未定标 open 不静默发明;eng-coder 任务书必须复述这些决策(或指向设计文档具体章节)——子代理零上下文,留在聊天里的决策永远到不了它;`eng-coder.md` 执行侧闭合——缺失的界面决策停下报告,不自行发明。两端 prompts byte-identical;`ENGINEERING-MODE.md` §7 变更记录
|
|
12
|
+
|
|
13
|
+
### Tests
|
|
14
|
+
|
|
15
|
+
- 新增 `test/session-eng-advisor.test.mjs`:/eng 双写断言(slot+config)、applySession slot 恢复(true/false/显式 false 压过 config true)、旧槽位无字段回退 config 锁定、saveSession 每 turn 往返、/advisor guard 双写(model 等仍 config-scoped)
|
|
16
|
+
|
|
17
|
+
## [0.12.49] — 2026-08-29
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Qwen enable_thinking 全链路**(CLI + VS Code 0.8.4 双端 parity):`resolveEnableThinking` 按"模型前缀 qwen*(排除 qwen3-coder*)+ 百炼域名"白名单注入 `enable_thinking`——`thinking === null` 即显式 off(区别于未设置=ON);`/think off` 真正关闭百炼强制思考(真实端点冒烟验证:off 1.0s 无 reasoning / 档位有思考);PROVIDER.md §12 权威文档
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **opencode 400 残余排查**:`stripLocalMessageFields` 补齐 provider 载荷净化(Gitee IKBGX4 follow-up,与 VS Code 端 parity 测试互锁)
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **subagent 工具描述升级**:角色能力矩阵 + Mode filtering 段(修复 role 参数 description 被运行期覆盖点的死文本);read_image 多模态模型清单修正(补 GLM-5.3-Flash、移除纯文本 Qwen3.7)
|
|
30
|
+
|
|
5
31
|
## [0.12.48] — 2026-08-28
|
|
6
32
|
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **opencode/LiteLLM 严校验端点 400 "Extra inputs are not permitted"(Gitee IKBGX4)**:本地标记 `transient` 泄漏进发送载荷——`escapeMessages` 发送前剥离整消息本地字段(新增 `stripLocalMessageFields`),云端不再拒绝 `messages[i].transient`;VS Code 端口同修(provider.mjs 净化链,函数体 parity)
|
|
36
|
+
- **Ctrl+I 注入框粘贴落主输入框(Gitee IKBU3J)**:`insertPastedText` 目标选择缺 `interruptPrompt` 分支——粘贴直接进 `state.input`、Esc 后残留主输入框;补分支(去换行,与按键路径同语义)
|
|
37
|
+
- **/think 交互异常卡死 TUI(Gitee IKBNUI)**:命令 handler 异常(如 config 写盘失败)一路冒泡击穿 submit 主循环→面板卡死回不了输入框;`handleSlash` 包 try/catch,异常转错误行、UI 保持存活
|
|
38
|
+
|
|
7
39
|
### Changed
|
|
8
40
|
|
|
9
41
|
- **系统提示词加两条约束**:①「先定正确再谈实现」——动手前先回答"正确应该是什么"(每个入口/视图/边界),再谈怎么实现,实现规模是"正确"的结果、不是决策标准;②「确认时暴露方案权衡」——确认理解时列出选的做法、为什么它对(绝不是"改动最小")、考虑过但没选的替代方案。治"最小改动"偷懒病根
|
package/package.json
CHANGED
package/src/agent/setup.mjs
CHANGED
|
@@ -183,12 +183,12 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
183
183
|
const subagentRoles = (depth === 0 && agent.config?.agent?.engineering)
|
|
184
184
|
? {
|
|
185
185
|
enum: ["explore", "plan", "eng-coder"],
|
|
186
|
-
description: "
|
|
186
|
+
description: "The sub-agent role — see the tool description for the role capability matrix. Exact spelling required.",
|
|
187
187
|
suffix: " In engineering mode, use role='eng-coder' for implementation (coder is disabled).",
|
|
188
188
|
}
|
|
189
189
|
: {
|
|
190
190
|
enum: ["explore", "plan", "coder"],
|
|
191
|
-
description: "
|
|
191
|
+
description: "The sub-agent role — see the tool description for the role capability matrix. Exact spelling required.",
|
|
192
192
|
suffix: "",
|
|
193
193
|
}
|
|
194
194
|
const filteredSubagent = depth === 0 ? {
|
|
@@ -252,12 +252,12 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
252
252
|
warnings.push(`Engineering template (${agent._role === "eng-coder" ? "engineering-sub.md" : "engineering.md"}) not found — using degraded constraints.`)
|
|
253
253
|
}
|
|
254
254
|
if (engResult.methodologyMissing) {
|
|
255
|
-
warnings.push("METHODOLOGY.md not found — project-
|
|
255
|
+
warnings.push("METHODOLOGY.md not found in the project root — no project methodology is loaded, so every 'per METHODOLOGY' reference in the engineering prompt is dangling and the three-document hard flow (requirements / design / test doc) is NOT enforced. Ask the user whether to create METHODOLOGY.md (scaffold available as src/prompts/methodology-template.md) before designing.")
|
|
256
256
|
}
|
|
257
257
|
if (warnings.length > 0) {
|
|
258
258
|
agent.history.push({
|
|
259
259
|
role: "user",
|
|
260
|
-
content: `[System reminder: ENGINEERING MODE is active but ${warnings.join(" ")}
|
|
260
|
+
content: `[System reminder: ENGINEERING MODE is active but ${warnings.join(" ")}]`,
|
|
261
261
|
})
|
|
262
262
|
}
|
|
263
263
|
}
|
|
@@ -53,7 +53,13 @@ export const subagentTool = {
|
|
|
53
53
|
name: "subagent",
|
|
54
54
|
description:
|
|
55
55
|
"Spawn a sub-agent to handle an independent subtask in an isolated context. The sub-agent returns only its final report. Spawn MULTIPLE subagents in the SAME response for parallel work—they run concurrently.\n" +
|
|
56
|
-
"
|
|
56
|
+
"Why delegate? A sub-agent runs in its own isolated context — its reads, searches, tool calls and edits never enter your history or pollute your window; only its final report comes back. Delegation keeps your working context lean (you see the whole session, not the child's noise) and the child single-mindedly focused on one task. Parallel children run concurrently, saving wall-clock time. Every coder/eng-coder child carries its own verify + advisor self-review discipline — handed-off work is already verified before you read a word of it.\n\n" +
|
|
57
|
+
"Available roles (which roles are exposed depends on the active mode — see Mode filtering below):\n" +
|
|
58
|
+
"- explore — read-only search & analysis. Toolset: the read/search family (grep, read, glob, code_search, doc_search, repo_outline, lsp, tree...). Receives git context auto-injected (branch, recent commits, working-tree state) when the project is a git repo. Its report must list what it searched and what it did NOT find. Fast — specify thoroughness in the task: quick / medium / thorough (default medium).\n" +
|
|
59
|
+
"- plan — read-only implementation planning. Same read/search toolset; NEVER edits files. Returns a step-by-step plan for the parent to execute.\n" +
|
|
60
|
+
"- coder — full implementation. The parent's complete read/write/execute toolset plus verify and advisor for self-review. Its final report must include a delivery transparency table with one row per task requirement (Done / Simplified / Not done — no deferred column).\n" +
|
|
61
|
+
"- eng-coder — engineering-mode coder (available only in engineering mode, replacing coder). Same full toolset as coder plus the design-driven methodology overlay; REQUIRES a valid designToken arg obtained from a passed advisor(type='design') review.\n" +
|
|
62
|
+
"Mode filtering: normal mode exposes explore/plan/coder; engineering mode exposes explore/plan/eng-coder. The schema enum reflects the active mode.\n\n" +
|
|
57
63
|
"Writing the prompt:\n" +
|
|
58
64
|
"- The sub-agent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.\n" +
|
|
59
65
|
"- Put exact paths and commands in the prompt when you know them. The sub-agent should not search for things you already know.\n" +
|
|
@@ -64,7 +70,7 @@ export const subagentTool = {
|
|
|
64
70
|
properties: {
|
|
65
71
|
task: { type: "string", description: "Self-contained task description for the sub-agent" },
|
|
66
72
|
context: { type: "string", description: "Optional background the sub-agent needs (it cannot see this conversation)" },
|
|
67
|
-
role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder"], description: "
|
|
73
|
+
role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder"], description: "The sub-agent role — see the tool description for the role capability matrix. Exact spelling required." },
|
|
68
74
|
model: { type: "string", description: "Provider/model override for this sub-agent: 'provider:model', a provider name from config, or a model name on the parent's provider. Defaults to the agent.subagentModel config, then the parent's provider. Useful for offloading heavy work to a cheaper model." },
|
|
69
75
|
designToken: { type: "string", description: "Required when role='eng-coder': the token returned by advisor(type='design') after the design review passed. Without a valid token, eng-coder cannot modify files." },
|
|
70
76
|
},
|
package/src/config.mjs
CHANGED
|
@@ -187,6 +187,36 @@ export function resolveCompactThreshold(explicit, model) {
|
|
|
187
187
|
return { value, auto: true }
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Bailian (阿里云百炼) host check — enable_thinking is a Bailian-only extension parameter;
|
|
192
|
+
* sending it to other endpoints (kimi/glm/custom proxies) would pollute the request.
|
|
193
|
+
*/
|
|
194
|
+
export function isBailianHost(baseURL) {
|
|
195
|
+
return typeof baseURL === "string"
|
|
196
|
+
&& (baseURL.includes("dashscope.aliyuncs.com") || baseURL.includes(".maas.aliyuncs.com"))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Resolve the Bailian `enable_thinking` switch for qwen hybrid-thinking models (PROVIDER.md §12).
|
|
201
|
+
* qwen3.x on Bailian defaults to thinking ON, so an explicit off must send enable_thinking:false
|
|
202
|
+
* or the server silently keeps thinking. Whitelist: model name starts with "qwen" (excluding the
|
|
203
|
+
* non-thinking qwen3-coder line) AND the provider points at a Bailian host.
|
|
204
|
+
* provider.thinking === null → false (explicit off: /think off, panel off — NF1 convention)
|
|
205
|
+
* provider.reasoningEffort → true (effort tier implies thinking on; rides with reasoning_effort)
|
|
206
|
+
* otherwise → undefined (field omitted — server default stays, no behavior change)
|
|
207
|
+
* NOTE: spec carries no model field today — the name comes from provider.model (spec?.model is
|
|
208
|
+
* a forward-compatible fallback). Keep the body byte-aligned with thincoder-vscode config.mjs
|
|
209
|
+
* (cross-repo parity test compares them).
|
|
210
|
+
*/
|
|
211
|
+
export function resolveEnableThinking(provider, spec) {
|
|
212
|
+
const model = (provider?.model ?? spec?.model ?? "").toLowerCase()
|
|
213
|
+
if (!model.startsWith("qwen") || model.startsWith("qwen3-coder")) return undefined
|
|
214
|
+
if (!isBailianHost(provider?.baseURL)) return undefined
|
|
215
|
+
if (provider.thinking === null) return false
|
|
216
|
+
if (provider.reasoningEffort) return true
|
|
217
|
+
return undefined
|
|
218
|
+
}
|
|
219
|
+
|
|
190
220
|
/**
|
|
191
221
|
* Find provider by name in providers[].
|
|
192
222
|
* Throws if name is non-empty but not found — a typo in activeProvider silently falling to the first provider would use the wrong key on the wrong endpoint.
|
package/src/escape.mjs
CHANGED
|
@@ -43,7 +43,20 @@ export function escapeMessageContent(message) {
|
|
|
43
43
|
return message
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
46
|
+
/** IKBGX4 (2026-08-28):剥离仅本地使用的整消息标记字段(transient 等)——发送给 provider 前移除。
|
|
47
|
+
* 严格 OpenAI 兼容服务端(opencode/LiteLLM 等)会拒绝消息级未知 key
|
|
48
|
+
* ("Extra inputs are not permitted, field: 'messages[i].transient'")。 */
|
|
49
|
+
export function stripLocalMessageFields(messages) {
|
|
50
|
+
return messages.map((m) => {
|
|
51
|
+
if (m && typeof m === "object" && "transient" in m) {
|
|
52
|
+
const { transient, ...rest } = m
|
|
53
|
+
return rest
|
|
54
|
+
}
|
|
55
|
+
return m
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 对整个 messages 数组逐条应用 escapeMessageContent(先剥离本地字段,再转义)。 */
|
|
47
60
|
export function escapeMessages(messages) {
|
|
48
|
-
return messages.map(escapeMessageContent)
|
|
61
|
+
return stripLocalMessageFields(messages).map(escapeMessageContent)
|
|
49
62
|
}
|
package/src/prompts/eng-coder.md
CHANGED
|
@@ -14,6 +14,7 @@ The parent agent ran an independent design review (`advisor` with `type="design"
|
|
|
14
14
|
|
|
15
15
|
- Work independently. The parent only sees your final report.
|
|
16
16
|
- Follow the design document. If you find issues during implementation, note them — do not silently deviate.
|
|
17
|
+
- UI/interaction: implement exactly what the task brief and design doc state (layout, flows, control behavior, states, feedback). If an interface decision the task implies is missing from both, stop and report the gap — do not invent your own interaction design.
|
|
17
18
|
- Write code one file at a time, verify each before moving on: call `verify` after each logical group (it runs syntax checks + related tests), syntax check after each edit.
|
|
18
19
|
- Do not modify any file not listed in the design.
|
|
19
20
|
- If the task is ambiguous, note the ambiguity in your report; do not ask the user.
|
|
@@ -35,7 +35,11 @@ subagents only.
|
|
|
35
35
|
discipline.
|
|
36
36
|
2. **Design.** Write the design document in `docs/` (problem statement,
|
|
37
37
|
solution approach, full affected-file list, verifiable acceptance criteria).
|
|
38
|
-
|
|
38
|
+
When the task involves a user interface, the design document MUST also
|
|
39
|
+
capture every UI/interaction decision agreed with the user — layout, flows,
|
|
40
|
+
control behavior, states and feedback — exactly as discussed; parts not yet
|
|
41
|
+
decided are marked open, never silently invented. Do NOT open any code file
|
|
42
|
+
for editing before this document exists.
|
|
39
43
|
3. **Remind readiness — never self-initiate review.** Present the design
|
|
40
44
|
summary and say it is ready for review, then WAIT. You do NOT call the
|
|
41
45
|
advisor yourself — the initiation right belongs to the user: you prepare
|
|
@@ -59,15 +63,21 @@ subagents only.
|
|
|
59
63
|
6. **Implement via eng-coder.** Spawn a subagent with `role="eng-coder"`,
|
|
60
64
|
providing the METHODOLOGY task structure: the **Docs involved** list (design
|
|
61
65
|
doc + requirements + referenced docs), the file list, the acceptance
|
|
62
|
-
criteria.
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
criteria. When the task has UI, the task text MUST restate the agreed
|
|
67
|
+
UI/interaction decisions (or point to the exact design-doc sections that
|
|
68
|
+
hold them) — an eng-coder has NO conversation context, so a decision that
|
|
69
|
+
lives only in the chat never reaches it. Pass the designToken via the
|
|
70
|
+
`designToken` PARAMETER — never in the task text. The token is required —
|
|
71
|
+
eng-coder cannot modify files without it.
|
|
65
72
|
7. **Delivery review — automatic flow node.** After eng-coder returns, verify
|
|
66
73
|
the delivery against the acceptance criteria from the design (run the
|
|
67
74
|
tests it claims pass, read the changed files) AND run the code review with
|
|
68
75
|
the `advisor` tool (`type="code"`, `documents=[...]` = the task's Docs
|
|
69
76
|
involved list). This review happens automatically — no user initiation
|
|
70
|
-
needed (2026-08-24 decision).
|
|
77
|
+
needed (2026-08-24 decision). When METHODOLOGY.md is present, the
|
|
78
|
+
METHODOLOGY test document is part of the delivery too: each user story must
|
|
79
|
+
map to at least one test case (normal / edge / error) — a delivery without
|
|
80
|
+
its test coverage fails the review.
|
|
71
81
|
8. **Verify.** Run `verify` — it must pass before you claim the task complete.
|
|
72
82
|
|
|
73
83
|
## Work Loop (every user message)
|
|
@@ -136,6 +146,11 @@ cannot enumerate. When using the `question` tool:
|
|
|
136
146
|
constraint, or preference during design discussion or review, update the
|
|
137
147
|
relevant docs (design doc, METHODOLOGY.md, ENGINEERING-MODE.md) right away —
|
|
138
148
|
do not wait to be asked. A decision that isn't in a doc didn't land.
|
|
149
|
+
- **UI/interaction decisions ride the full chain**: every UI/interaction
|
|
150
|
+
decision agreed with the user MUST land in the design document AND be
|
|
151
|
+
restated in the eng-coder task (or pointer to its exact design-doc section).
|
|
152
|
+
"Discussed but not written down" is the most common reason an implementation
|
|
153
|
+
ignores what the user asked for — the subagent never saw the discussion.
|
|
139
154
|
- Review initiation split: the DESIGN review is called ONLY when the user
|
|
140
155
|
explicitly asks (e.g. "评审吧") — remind them when the design is ready,
|
|
141
156
|
never fire it yourself; each round of findings goes back to the user for
|
|
@@ -8,10 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
Every task follows four steps, no skipping:
|
|
10
10
|
|
|
11
|
-
1. **Requirements** — Discuss and document what's needed
|
|
12
|
-
|
|
11
|
+
1. **Requirements** — Discuss and document what's needed, then write the requirements doc organized in **three layers**:
|
|
12
|
+
- **Overall goal** — one sentence: what problem does this task solve, for whom;
|
|
13
|
+
- **Functional user stories** — individually acceptable, format: **As a [role], I want [feature], so that [goal]**. Describe who / what / why — never how;
|
|
14
|
+
- **Non-functional standards** — performance, security, compatibility, usability constraints, each with how it will be measured.
|
|
15
|
+
|
|
16
|
+
Requirements are DONE when all three layers are concrete enough to design against (the user confirms, or the answers stop changing the requirement). After confirming, create a checklist entry for each story. No checklist entry means the requirement hasn't landed yet.
|
|
17
|
+
2. **Design** — Write a design document: problem statement, approach and rationale, full affected-file list, and verifiable acceptance criteria (each criterion traces back to a user story). Design is approved before coding starts.
|
|
13
18
|
3. **Implementation** — Write the code.
|
|
14
|
-
4. **Testing** — Verify
|
|
19
|
+
4. **Testing** — Verify with a test document: each user story maps to at least one test case covering normal path, edge cases, and error conditions. Describe what to test, what input to give, and what output to expect.
|
|
15
20
|
|
|
16
21
|
These four steps are not "best practice" — they are hard process. Three documents required: **requirements doc**, **design doc**, **test doc**. Skipping to step 3 and writing code first is wrong nine times out of ten.
|
|
17
22
|
|
package/src/provider/core.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* SSE parsing → provider/sse.mjs
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { specForModel } from "../config.mjs"
|
|
7
|
+
import { specForModel, resolveEnableThinking } from "../config.mjs"
|
|
8
8
|
import { proxyFetch } from "../proxy.mjs"
|
|
9
9
|
import { escapeMessages } from "../escape.mjs"
|
|
10
10
|
import { readSSE } from "./sse.mjs"
|
|
@@ -104,6 +104,11 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
|
|
|
104
104
|
}
|
|
105
105
|
body.reasoning_effort = provider.reasoningEffort
|
|
106
106
|
}
|
|
107
|
+
// enable_thinking — Bailian hybrid-thinking switch (PROVIDER.md §12): qwen3.x defaults to
|
|
108
|
+
// thinking ON, so an explicit off must send enable_thinking:false or the server keeps thinking.
|
|
109
|
+
// NOT gated by isRouter: the whitelist keys on model prefix + Bailian host, not the model-ID slash.
|
|
110
|
+
const enableThinking = resolveEnableThinking(provider, spec)
|
|
111
|
+
if (enableThinking !== undefined) body.enable_thinking = enableThinking
|
|
107
112
|
if (tools?.length) body.tools = tools
|
|
108
113
|
|
|
109
114
|
const estimated = estimateRequestTokens(body)
|
package/src/session.mjs
CHANGED
|
@@ -44,6 +44,7 @@ export function sessionPath(cwd) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function slotPath(cwd, n) { return sessionPath(cwd) + "." + n }
|
|
47
|
+
export { slotPath }
|
|
47
48
|
function manifestPath(cwd) { return sessionPath(cwd) + ".manifest" }
|
|
48
49
|
|
|
49
50
|
/** Path to the active slot's file */
|
|
@@ -417,6 +418,13 @@ export function applySession(agent, data) {
|
|
|
417
418
|
agent._pendingReminders = data.pendingReminders ?? []
|
|
418
419
|
agent._sessionStart = data.sessionStart ?? null
|
|
419
420
|
agent._engDesignToken = data.engDesignToken ?? null
|
|
421
|
+
// engineering is session-level (2026-08-29): the slot value is the CLI session's authority
|
|
422
|
+
// — config.json is only the initial default / cross-end mirror. A legacy slot without the
|
|
423
|
+
// field keeps whatever config.json seeded (unchanged behavior).
|
|
424
|
+
if (data.engineering !== undefined) {
|
|
425
|
+
agent.config.agent ??= {}
|
|
426
|
+
agent.config.agent.engineering = data.engineering === true
|
|
427
|
+
}
|
|
420
428
|
if (data.advisor) {
|
|
421
429
|
agent.config.advisor = { ...data.advisor }
|
|
422
430
|
}
|
package/src/tui/clipboard.mjs
CHANGED
|
@@ -84,6 +84,12 @@ export function insertPastedText(state, rawText) {
|
|
|
84
84
|
q.answer = (q.answer ?? "") + rawText.replace(/[\r\n]+/g, "")
|
|
85
85
|
return
|
|
86
86
|
}
|
|
87
|
+
// IKBU3J (2026-08-28):Ctrl+I 注入框激活时,粘贴进注入文本(与按键路径同语义:去换行保单行),
|
|
88
|
+
// 不得落入主输入框——此前缺失该分支导致粘贴进 state.input、Esc 后残留主输入框。
|
|
89
|
+
if (state.interruptPrompt) {
|
|
90
|
+
state.interruptPrompt.text += rawText.replace(/[\r\n]+/g, "")
|
|
91
|
+
return
|
|
92
|
+
}
|
|
87
93
|
const text = rawText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ")
|
|
88
94
|
const chars = [...text]
|
|
89
95
|
state.input.splice(state.cursor, 0, ...chars)
|
package/src/tui/cmd-advisor.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/** /advisor command: configure review model/thinking and toggle the review guard.
|
|
2
2
|
* Interactive loop UX — stays in menu after each action, Esc to exit.
|
|
3
3
|
* ctx: { agent, showPicker, pushLine, pushLabel, persistRaw } */
|
|
4
|
+
import { readFileSync } from "node:fs"
|
|
4
5
|
import { ansi, C } from "./ansi.mjs"
|
|
6
|
+
import { activeSlot, slotPath } from "../session.mjs"
|
|
7
|
+
import { writeSessionFile } from "./cmd-eng.mjs"
|
|
5
8
|
|
|
6
9
|
export async function handleAdvisorCommand(ctx) {
|
|
7
10
|
const { agent, showPicker, pushLine, pushLabel } = ctx
|
|
@@ -19,6 +22,21 @@ export async function handleAdvisorCommand(ctx) {
|
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
// Guard-only dual write (2026-08-29 — advisor.guard is session-level): the guard goes into
|
|
26
|
+
// the CURRENT session slot first (shared with VS Code), the config.json mirror follows.
|
|
27
|
+
// Other advisor keys (model/thinking/effort/timeout) stay config-scoped — persist() only.
|
|
28
|
+
const persistGuard = async () => {
|
|
29
|
+
try {
|
|
30
|
+
const p = slotPath(agent.cwd, activeSlot(agent.cwd))
|
|
31
|
+
const data = JSON.parse(readFileSync(p, "utf8"))
|
|
32
|
+
if (data && typeof data === "object" && Array.isArray(data.history)) {
|
|
33
|
+
data.advisor = { ...(typeof data.advisor === "object" && data.advisor !== null ? data.advisor : {}), guard: cfg.guard === true }
|
|
34
|
+
writeSessionFile(p, data)
|
|
35
|
+
}
|
|
36
|
+
} catch { /* slot missing/unreadable — config mirror still written */ }
|
|
37
|
+
await persist()
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
// Lazy model cache — fetched once per /advisor session
|
|
23
41
|
let modelCache = null
|
|
24
42
|
|
|
@@ -133,9 +151,9 @@ export async function handleAdvisorCommand(ctx) {
|
|
|
133
151
|
|
|
134
152
|
if (choice.action === "guard") {
|
|
135
153
|
cfg.guard = !(cfg.guard === true)
|
|
136
|
-
await
|
|
154
|
+
await persistGuard().catch(err => pushLine(`[error] ${err.message}`, C.error))
|
|
137
155
|
pushLabel("❯ Advisor", ansi.bold + C.tool)
|
|
138
|
-
pushLine(`Advisor: ${cfg.guard === true ? "on" : "off"}`, C.tool)
|
|
156
|
+
pushLine(`Advisor: ${cfg.guard === true ? "on" : "off"} (session)`, C.tool)
|
|
139
157
|
continue
|
|
140
158
|
}
|
|
141
159
|
|
package/src/tui/cmd-eng.mjs
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
/** /eng command: toggle engineering mode.
|
|
2
2
|
* Requires METHODOLOGY.md in project root. Offers to create one if missing.
|
|
3
3
|
* ctx: { agent, pushLine, pushLabel, persistRaw, showPicker } */
|
|
4
|
-
import { existsSync, copyFileSync } from "node:fs"
|
|
5
|
-
import { join } from "node:path"
|
|
4
|
+
import { existsSync, copyFileSync, readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from "node:fs"
|
|
5
|
+
import { join, dirname } from "node:path"
|
|
6
6
|
import { fileURLToPath } from "node:url"
|
|
7
7
|
import { ansi, C } from "./ansi.mjs"
|
|
8
|
+
import { activeSlot, slotPath } from "../session.mjs"
|
|
8
9
|
|
|
9
10
|
const templateDir = join(fileURLToPath(import.meta.url), "..", "..", "prompts")
|
|
10
11
|
import { ENG_OFF_REMINDER } from "../agent.mjs"
|
|
11
12
|
|
|
13
|
+
/** Atomic slot write (same shape as session.mjs writeSessionFile — kept local to avoid a
|
|
14
|
+
* private-import; cmd-advisor's guard toggle shares this helper). */
|
|
15
|
+
export function writeSessionFile(p, data) {
|
|
16
|
+
mkdirSync(dirname(p), { recursive: true })
|
|
17
|
+
const tmp = `${p}.tmp`
|
|
18
|
+
writeFileSync(tmp, JSON.stringify(data), "utf8")
|
|
19
|
+
try {
|
|
20
|
+
renameSync(tmp, p)
|
|
21
|
+
} catch {
|
|
22
|
+
try { unlinkSync(p) } catch {}
|
|
23
|
+
try { renameSync(tmp, p) } catch { writeFileSync(p, readFileSync(tmp, "utf8"), "utf8") }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
export async function handleEngCommand(ctx) {
|
|
13
28
|
const { agent, pushLine, pushLabel, persistRaw, showPicker } = ctx
|
|
14
29
|
agent.config.agent ??= {}
|
|
@@ -40,13 +55,35 @@ export async function handleEngCommand(ctx) {
|
|
|
40
55
|
agent._pendingReminders = agent._pendingReminders ?? []
|
|
41
56
|
agent._pendingReminders.push(ENG_OFF_REMINDER)
|
|
42
57
|
}
|
|
43
|
-
await
|
|
44
|
-
raw.agent ??= {}
|
|
45
|
-
raw.agent.engineering = agent.config.agent.engineering
|
|
46
|
-
})
|
|
58
|
+
await persistEngineering(ctx, agent)
|
|
47
59
|
pushLabel("❯ Eng", ansi.bold + C.tool)
|
|
48
|
-
pushLine(`Engineering mode: ${agent.config.agent.engineering ? "ON" : "OFF"}`, C.tool)
|
|
60
|
+
pushLine(`Engineering mode: ${agent.config.agent.engineering ? "ON" : "OFF"} (session)`, C.tool)
|
|
49
61
|
if (agent.config.agent.engineering) {
|
|
50
62
|
pushLine(` → strictly following ${methodologyPath}`, C.dim)
|
|
51
63
|
}
|
|
52
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Dual persistence (2026-08-29 — engineering is session-level): write the flipped flag into
|
|
68
|
+
* the CURRENT session slot first (slot authority — shared with VS Code, per-session), then
|
|
69
|
+
* the config.json mirror (CLI visibility/compat; no longer the cross-session source of truth).
|
|
70
|
+
* The in-memory agent.config.agent.engineering (already flipped) stays the live authority for
|
|
71
|
+
* this process; saveSession also round-trips it on every turn-end write.
|
|
72
|
+
*/
|
|
73
|
+
async function persistEngineering(ctx, agent) {
|
|
74
|
+
const slot = activeSlot(agent.cwd)
|
|
75
|
+
try {
|
|
76
|
+
const p = slotPath(agent.cwd, slot)
|
|
77
|
+
const data = JSON.parse(readFileSync(p, "utf8"))
|
|
78
|
+
if (data && typeof data === "object" && Array.isArray(data.history)) {
|
|
79
|
+
data.engineering = agent.config.agent.engineering
|
|
80
|
+
writeSessionFile(p, data)
|
|
81
|
+
}
|
|
82
|
+
} catch { /* slot missing/unreadable — config mirror still written */ }
|
|
83
|
+
if (ctx.persistRaw) {
|
|
84
|
+
await ctx.persistRaw((raw) => {
|
|
85
|
+
raw.agent ??= {}
|
|
86
|
+
raw.agent.engineering = agent.config.agent.engineering
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/tui/cmd-think.mjs
CHANGED
|
@@ -42,8 +42,10 @@ export async function handleThinkCommand(ctx, args = []) {
|
|
|
42
42
|
let mainIdx = 0
|
|
43
43
|
for (;;) {
|
|
44
44
|
const autoOn = agent.config?.agent?.autoThink === true
|
|
45
|
+
// thinking:null 是显式 off 标记(NF1 约定)——不得落入"未设置即 ON"的默认显示(评审 #3)。
|
|
46
|
+
// 用 !== null 而非 != null:undefined(从未设置)须保持既有 ON 显示(qwen3.x 服务端默认开思考)。
|
|
45
47
|
const thinkingEnabled = cur.thinking?.type === thinkOnValue
|
|
46
|
-
|| (cur.thinking?.type === undefined && !isCustomThink)
|
|
48
|
+
|| (cur.thinking !== null && cur.thinking?.type === undefined && !isCustomThink)
|
|
47
49
|
|
|
48
50
|
const entries = [
|
|
49
51
|
{ type: "header", text: `Auto: ${autoOn ? "ON" : "OFF"} | Thinking: ${thinkingEnabled ? "ON" : "OFF"} | Effort: ${cur.reasoningEffort || "—"}` },
|
|
@@ -92,17 +94,29 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
|
|
|
92
94
|
const cfg = agent.config.agent ??= {}
|
|
93
95
|
cfg.autoThink = !cfg.autoThink
|
|
94
96
|
if (cfg.autoThink) {
|
|
97
|
+
// 开 auto = 要思考:清显式 off 标记(thinking:null,NF1 约定;交付评审 #1)——残留 null
|
|
98
|
+
// 会让 auto 每轮写入的 reasoning_effort 与 enable_thinking:false 矛盾同发(F2 违约)。
|
|
99
|
+
// 仅清 null:thinking-type 模型的 {type:"disabled"} 不在评审 #1 范围,保持既有语义。
|
|
100
|
+
if (cur.thinking === null) { delete cur.thinking; await syncProviderField("thinking", undefined) }
|
|
95
101
|
delete cur.reasoningEffort
|
|
96
102
|
await syncProviderField("reasoningEffort", undefined)
|
|
97
103
|
}
|
|
98
104
|
} else if (e.action === "effort") {
|
|
105
|
+
// 选档位 = 要思考:清显式 off 标记(thinking:null,NF1 约定;交付评审 #1)——残留 null 会让
|
|
106
|
+
// enable_thinking:false 与 reasoning_effort 矛盾同发(F2 违约)。仅清 null(同上注释)。
|
|
107
|
+
if (cur.thinking === null) { delete cur.thinking; await syncProviderField("thinking", undefined) }
|
|
99
108
|
cur.reasoningEffort = e.level
|
|
100
109
|
await syncProviderField("reasoningEffort", e.level)
|
|
101
110
|
} else {
|
|
102
111
|
const enable = e.action === "on"
|
|
103
112
|
if (isEffortOnly) {
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
// Explicit off persists thinking:null (NF1 convention — distinguishable from autoThink's
|
|
114
|
+
// delete/undefined); "on" deletes the marker so enable_thinking maps from effort again.
|
|
115
|
+
if (!enable) { cur.thinking = null; delete cur.reasoningEffort }
|
|
116
|
+
// "on" 默认 effort 取 spec 枚举首值(交付评审 #2):硬编码 "high" 对 qwen3.8-max
|
|
117
|
+
// (enum xhigh/medium/low)无效,会被 core.mjs 枚举校验 throw(400 前置)
|
|
118
|
+
else { delete cur.thinking; if (!cur.reasoningEffort) cur.reasoningEffort = spec.reasoningEffortEnum?.[0] ?? "high" }
|
|
119
|
+
await syncProviderField("thinking", cur.thinking)
|
|
106
120
|
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
107
121
|
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
108
122
|
} else {
|
|
@@ -114,7 +114,14 @@ export function createSlashCommands(ctx) {
|
|
|
114
114
|
const resolved = SLASH_ALIASES[cmd] ?? cmd
|
|
115
115
|
const handler = HANDLERS[resolved]
|
|
116
116
|
if (handler) {
|
|
117
|
-
|
|
117
|
+
// IKBNUI (2026-08-28):命令 handler 抛出的异常必须被拦截成错误行,不得击穿 TUI 主循环——
|
|
118
|
+
// 此前 /think 交互循环里 config 写盘失败会一路冒泡(submit 无 catch)导致面板卡死、回不了输入框。
|
|
119
|
+
// 调用点(index.mjs submit 路径)统一在 handleSlash 返回后 render,UI 保持存活。
|
|
120
|
+
try {
|
|
121
|
+
await handler(handlerCtx, args)
|
|
122
|
+
} catch (e) {
|
|
123
|
+
ctx.pushLine(`[error] ${e?.message ?? String(e)}`, C.error)
|
|
124
|
+
}
|
|
118
125
|
return
|
|
119
126
|
}
|
|
120
127
|
ctx.pushLine(`Unknown command: ${rawCmd} (/help for available commands)`, C.error)
|