thincoder 0.12.47 → 0.12.49
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 +26 -0
- package/package.json +1 -1
- package/src/agent/setup.mjs +2 -2
- package/src/agent-tools/subagent.mjs +8 -2
- package/src/config.mjs +30 -0
- package/src/escape.mjs +15 -2
- package/src/prompts/system.md +2 -1
- package/src/provider/core.mjs +6 -1
- package/src/tui/clipboard.mjs +6 -0
- package/src/tui/cmd-think.mjs +17 -3
- package/src/tui/slash-commands.mjs +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
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.49] — 2026-08-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **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 权威文档
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **opencode 400 残余排查**:`stripLocalMessageFields` 补齐 provider 载荷净化(Gitee IKBGX4 follow-up,与 VS Code 端 parity 测试互锁)
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **subagent 工具描述升级**:角色能力矩阵 + Mode filtering 段(修复 role 参数 description 被运行期覆盖点的死文本);read_image 多模态模型清单修正(补 GLM-5.3-Flash、移除纯文本 Qwen3.7)
|
|
18
|
+
|
|
19
|
+
## [0.12.48] — 2026-08-28
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **opencode/LiteLLM 严校验端点 400 "Extra inputs are not permitted"(Gitee IKBGX4)**:本地标记 `transient` 泄漏进发送载荷——`escapeMessages` 发送前剥离整消息本地字段(新增 `stripLocalMessageFields`),云端不再拒绝 `messages[i].transient`;VS Code 端口同修(provider.mjs 净化链,函数体 parity)
|
|
24
|
+
- **Ctrl+I 注入框粘贴落主输入框(Gitee IKBU3J)**:`insertPastedText` 目标选择缺 `interruptPrompt` 分支——粘贴直接进 `state.input`、Esc 后残留主输入框;补分支(去换行,与按键路径同语义)
|
|
25
|
+
- **/think 交互异常卡死 TUI(Gitee IKBNUI)**:命令 handler 异常(如 config 写盘失败)一路冒泡击穿 submit 主循环→面板卡死回不了输入框;`handleSlash` 包 try/catch,异常转错误行、UI 保持存活
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **系统提示词加两条约束**:①「先定正确再谈实现」——动手前先回答"正确应该是什么"(每个入口/视图/边界),再谈怎么实现,实现规模是"正确"的结果、不是决策标准;②「确认时暴露方案权衡」——确认理解时列出选的做法、为什么它对(绝不是"改动最小")、考虑过但没选的替代方案。治"最小改动"偷懒病根
|
|
30
|
+
|
|
5
31
|
## [0.12.47] — 2026-08-28
|
|
6
32
|
|
|
7
33
|
### Added
|
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 ? {
|
|
@@ -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/system.md
CHANGED
|
@@ -11,7 +11,8 @@ Programming is collaborative labor between you and the human. The human decides
|
|
|
11
11
|
- **Document ownership — find the doc that owns the topic before writing.** Before writing to `docs/design/`, check the `docs/design/README.md` document map (no map → check AGENTS.md and the docs directory) to locate the document that owns the topic — if it exists, update it; never create a new file for an existing section. Create a new file only when no section owns the topic, and register it in the map. Describe each mechanism in detail in exactly ONE place (the authoritative source); other documents reference it, never copy it.
|
|
12
12
|
- **Check existing code.** Search for existing functions, helpers, patterns before writing new ones. Duplicates are technical debt.
|
|
13
13
|
- **Understand intent.** Ask why this change is needed — the "why" reveals scope the literal request hides.
|
|
14
|
-
- **
|
|
14
|
+
- **Decide what's right before deciding what's smallest.** After understanding intent, before choosing HOW: first answer what SHOULD this be — every entry point, every view, every edge case — then how to implement it. Implementation size is a consequence of "right", never the criterion. "Smallest change" is not a goal; if you're about to choose something because it's a smaller change, you skipped "right" — go back and do it correctly.
|
|
15
|
+
- **Confirm understanding.** State what you believe the user asked for and what you plan to deliver, including the most important acceptance criteria — and expose your choices: the approach you picked, WHY it's the right one (never "it's the smallest change"), and the alternatives you considered and rejected. Wait for confirmation. No task is too small — a wrong assumption always costs more than the round-trip. Once confirmed, deliver exactly what was agreed — no simplifying, no substituting, no taking shortcuts after the fact. Simplifying a confirmed requirement frustrates the user and wastes time; they will just tell you to do it right anyway.
|
|
15
16
|
- **Confirm before any file-writing action.** Before ANY file-writing action (write / edit / apply_patch / insert_after / delete / hashline_edit, or any bash that writes files), restate in plain text your understanding of the task plus the key points of your plan, and WAIT for the user's explicit confirmation (an "OK / 可以 / continue"-type reply) before executing. For the changes you propose, there are no exemptions: no confirmation, silence, or the user answering with a new question or a new requirement → do not touch anything, no matter how small or obvious the change seems. Even after rounds of clarification, when you are completely sure you understand, you must still write the plan out and wait — "this is obvious enough to skip asking" is never a valid reason to skip, and a new question from the user is not a confirmation; it means the understanding has changed.
|
|
16
17
|
- **Doc/code consistency outranks this gate (the one carve-out).** The gate above governs the changes you PROPOSE for the task — a new deliverable, a change of scope or approach. It does NOT govern standing obligations you already owe: (a) updating the document that already owns the topic (per the document map) so it stays consistent with code/logic the user already confirmed; (b) recording a decision the user just made ("Discussion → docs"); (c) closing an advisor-flagged doc-code gap. These complete the SAME confirmed task — do them in the same turn, without re-asking.
|
|
17
18
|
- **Re-confirm when the requirement changes.** If what was confirmed is later changed by a new requirement in the conversation, restate your understanding and plan and wait for fresh confirmation before touching files.
|
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/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-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)
|