thincoder 0.12.67 → 0.12.68
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 +23 -0
- package/README.md +17 -5
- package/bin/thincoder.mjs +41 -344
- package/package.json +2 -2
- package/src/acp/bridge.mjs +5 -2
- package/src/acp/handlers-session.mjs +12 -1
- package/src/cli/make-agent.mjs +44 -119
- package/src/cli/memory-command.mjs +45 -3
- package/src/command-interactive.mjs +186 -0
- package/src/command-table.mjs +185 -0
- package/src/completions.mjs +36 -12
- package/src/crash-reports.mjs +42 -20
- package/src/distill.mjs +3 -3
- package/src/heap-watch.mjs +5 -14
- package/src/tui/agent-turn.mjs +88 -50
- package/src/tui/clipboard.mjs +74 -30
- package/src/tui/cmd-advisor.mjs +39 -13
- package/src/tui/cmd-clear.mjs +2 -2
- package/src/tui/cmd-config.mjs +17 -1
- package/src/tui/cmd-eng.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +1 -1
- package/src/tui/cmd-new.mjs +2 -2
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-session.mjs +13 -5
- package/src/tui/cmd-think.mjs +35 -17
- package/src/tui/cmd-timers.mjs +33 -0
- package/src/tui/conversation-writer.mjs +2 -2
- package/src/tui/display-budget.mjs +9 -9
- package/src/tui/fold-block.mjs +1 -1
- package/src/tui/index.mjs +20 -1
- package/src/tui/interaction.mjs +4 -2
- package/src/tui/key-handler-ctrlc.mjs +1 -1
- package/src/tui/key-handler-search.mjs +1 -1
- package/src/tui/key-handler.mjs +1 -1
- package/src/tui/key-modes.mjs +1 -1
- package/src/tui/layout.mjs +5 -5
- package/src/tui/lifecycle-records.mjs +199 -0
- package/src/tui/model-picker.mjs +16 -199
- package/src/tui/mouse.mjs +3 -3
- package/src/tui/pickers.mjs +6 -2
- package/src/tui/provider-admin.mjs +213 -0
- package/src/tui/queued-merge.mjs +4 -60
- package/src/tui/queued-pickup.mjs +3 -3
- package/src/tui/render-conversation.mjs +13 -4
- package/src/tui/render-frame.mjs +43 -6
- package/src/tui/render-loop.mjs +2 -3
- package/src/tui/render-segments.mjs +2 -2
- package/src/tui/render.mjs +1 -1
- package/src/tui/slash-commands.mjs +3 -0
- package/src/tui/startup.mjs +28 -3
- package/src/tui/subagent-blocks.mjs +27 -19
- package/src/tui/subagent-children.mjs +5 -5
- package/src/tui/subagent-freeze.mjs +39 -37
- package/src/tui/subagent-panel.mjs +7 -7
- package/src/tui/suspension-drive.mjs +172 -259
- package/src/tui/timer-watch.mjs +64 -0
- package/src/tui/tool-args.mjs +10 -71
- package/src/tui/tool-display.mjs +6 -6
- package/src/tui/tool-events.mjs +33 -28
- package/src/tui/tui-lifecycle.mjs +13 -4
- package/src/tui/tui-state.mjs +3 -2
- package/src/tui/turn-face.mjs +4 -1
- package/src/tui/wizard.mjs +7 -2
- package/src/tui/wrapped-spawn.mjs +3 -3
package/src/tui/clipboard.mjs
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { readFile, stat, unlink } from "node:fs/promises"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { IMAGE_MAX_BYTES, savePastedImages } from "@thincoder/core/attachments.mjs"
|
|
6
|
+
import { cleanupOldToolResults } from "@thincoder/core/agent/helpers.mjs"
|
|
1
7
|
import { C } from "./ansi.mjs"
|
|
2
8
|
|
|
3
9
|
/** Windows clipboard-read command: force UTF-8 console output so Get-Clipboard's bytes
|
|
@@ -132,49 +138,87 @@ export function stripKeyboardProtocol(text) {
|
|
|
132
138
|
return text.replace(/\x1b\[\d+;\d+u/g, "").replace(/\x1b\[27;\d+;\d+~/g, "")
|
|
133
139
|
}
|
|
134
140
|
|
|
135
|
-
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
|
|
139
|
-
const { agent, state, pushLine, render } = ctx
|
|
141
|
+
/** 平台剪贴板图像捕获(缺省实现——写 staging ∥ 无图抛):Windows = PowerShell
|
|
142
|
+
* System.Windows.Forms(无图 ⇒ exit 1 ⇒ 抛);macOS = osascript PNGf;其余 = xclip → wl-paste 回退。
|
|
143
|
+
* 截图直接写 `dest`(系统临时区 staging——不入项目树)。 */
|
|
144
|
+
async function captureClipboardImage(dest) {
|
|
140
145
|
const { execFile } = await import("node:child_process")
|
|
141
|
-
const { stat, unlink } = await import("node:fs/promises")
|
|
142
|
-
const { join } = await import("node:path")
|
|
143
|
-
|
|
144
146
|
const run = (cmd, args) => new Promise((resolve, reject) => {
|
|
145
147
|
execFile(cmd, args, { timeout: 10000 }, (err, stdout) => { if (err) reject(err); else resolve(stdout) })
|
|
146
148
|
})
|
|
147
|
-
|
|
148
|
-
const dest = join(agent.cwd, `.thincoder-paste-${Date.now()}.png`)
|
|
149
149
|
const isWin = process.platform === "win32"
|
|
150
150
|
const isMac = process.platform === "darwin"
|
|
151
|
+
if (isWin) {
|
|
152
|
+
const psScript = `Add-Type -AssemblyName System.Windows.Forms; if ([System.Windows.Forms.Clipboard]::ContainsImage()) { [System.Windows.Forms.Clipboard]::GetImage().Save('${dest.replace(/\\/g, "\\\\")}', [System.Drawing.Imaging.ImageFormat]::Png); exit 0 } else { exit 1 }`
|
|
153
|
+
await run("powershell", ["-NoProfile", "-Command", psScript])
|
|
154
|
+
} else if (isMac) {
|
|
155
|
+
const script = `try; set f to (POSIX file "${dest}"); set img to the clipboard as «class PNGf»; set fd to open for access f with write permission; write img to fd; close access fd; end try`
|
|
156
|
+
await run("osascript", ["-e", script])
|
|
157
|
+
} else {
|
|
158
|
+
await run("bash", ["-c", `xclip -selection clipboard -t image/png -o > "${dest}" 2>/dev/null || { which wl-paste >/dev/null 2>&1 && wl-paste -t image/png > "${dest}" 2>/dev/null; } || exit 1`])
|
|
159
|
+
}
|
|
160
|
+
}
|
|
151
161
|
|
|
162
|
+
/** 贴图 buffer ⇒ 核正路落盘(**唯一写盘者 = 核 `savePastedImages`**):写时扫除
|
|
163
|
+
* (`cleanupOldToolResults(<cwd>/.thincoder/tmp)`——3 天窗 mtime 回收;扫除失败不影响落盘)⇒
|
|
164
|
+
* 构 dataURL ⇒ 调核 `savePastedImages(dataUrls, cwd, { fs })` ⇒ 路径 ∥ null(核弃项 ∥ 写败)。
|
|
165
|
+
* @returns {Promise<string|null>} `<cwd>/.thincoder/tmp/paste-<id>-0.png` 绝对路径 / null */
|
|
166
|
+
export async function savePastedImageBuffer(buffer, cwd) {
|
|
167
|
+
const tmpDir = join(cwd, ".thincoder", "tmp")
|
|
168
|
+
await cleanupOldToolResults(tmpDir)
|
|
169
|
+
const dataUrl = `data:image/png;base64,${buffer.toString("base64")}`
|
|
170
|
+
// 契约「路径 ∥ null」——核内异常(未接线等)归「写败」径,不越出本函数
|
|
152
171
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
await run("powershell", ["-NoProfile", "-Command", psScript])
|
|
156
|
-
} else if (isMac) {
|
|
157
|
-
const script = `try; set f to (POSIX file "${dest}"); set img to the clipboard as «class PNGf»; set fd to open for access f with write permission; write img to fd; close access fd; end try`
|
|
158
|
-
await run("osascript", ["-e", script])
|
|
159
|
-
} else {
|
|
160
|
-
await run("bash", ["-c", `xclip -selection clipboard -t image/png -o > "${dest}" 2>/dev/null || { which wl-paste >/dev/null 2>&1 && wl-paste -t image/png > "${dest}" 2>/dev/null; } || exit 1`])
|
|
161
|
-
}
|
|
172
|
+
const { paths } = savePastedImages([dataUrl], cwd, { fs: { mkdirSync, writeFileSync } })
|
|
173
|
+
return paths[0] ?? null
|
|
162
174
|
} catch {
|
|
163
|
-
|
|
164
|
-
try { await unlink(dest) } catch {}
|
|
165
|
-
return
|
|
175
|
+
return null
|
|
166
176
|
}
|
|
177
|
+
}
|
|
167
178
|
|
|
168
|
-
|
|
179
|
+
/** Ctrl+V / Alt+V:读剪贴板图像 → 系统临时区 staging(不入项目树)→ 读 buffer ⇒ staging 用毕即清
|
|
180
|
+
* (读取后——与失败径同口径)⇒ `savePastedImageBuffer` 落 `<cwd>/.thincoder/tmp/paste-<id>-0.png`
|
|
181
|
+
* → 插入 `read_image <最终绝对路径>`。
|
|
182
|
+
* 失败径(dim 提示行 ×3):无图(捕获抛 ∥ 零字节)· 超阈(`IMAGE_MAX_BYTES` 单源早筛——stat 后
|
|
183
|
+
* 读前,不读巨件)· 写败(核落盘 ≤0 项)——均零插入、返回 null。
|
|
184
|
+
* @param {{agent:{cwd:string}, state, pushLine:Function, render:Function}} ctx
|
|
185
|
+
* @param {((dest:string)=>Promise<void>)|null} [captureImpl] — 捕获面测试缝(写 staging ∥ 无图抛);
|
|
186
|
+
* 缺省 null ⇒ 平台捕获实现(生产调用点零改;参数作用域——零跨调用残留)
|
|
187
|
+
* @returns {Promise<{insert:string, path:string}|null>} 成功 = 插入文本 + 最终绝对路径;失败 = null */
|
|
188
|
+
export async function pasteClipboardImage(ctx, captureImpl = null) {
|
|
189
|
+
const { agent, state, pushLine, render } = ctx
|
|
190
|
+
const staging = join(tmpdir(), `thincoder-paste-${Date.now()}.png`)
|
|
191
|
+
const capture = captureImpl ?? captureClipboardImage
|
|
192
|
+
try {
|
|
193
|
+
await capture(staging)
|
|
194
|
+
} catch {
|
|
195
|
+
pushLine("Clipboard does not contain an image, or clipboard access failed", C.dim)
|
|
196
|
+
try { await unlink(staging) } catch {}
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
const st = await stat(staging).catch(() => null)
|
|
169
200
|
if (!st || st.size === 0) {
|
|
170
201
|
pushLine("Clipboard does not contain an image, or clipboard access failed", C.dim)
|
|
171
|
-
try { await unlink(
|
|
172
|
-
return
|
|
202
|
+
try { await unlink(staging) } catch {}
|
|
203
|
+
return null
|
|
173
204
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
205
|
+
if (st.size > IMAGE_MAX_BYTES) {
|
|
206
|
+
pushLine("Clipboard image too large (max 15MB)", C.dim)
|
|
207
|
+
try { await unlink(staging) } catch {}
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
const buffer = await readFile(staging)
|
|
211
|
+
try { await unlink(staging) } catch {}
|
|
212
|
+
const path = await savePastedImageBuffer(buffer, agent.cwd)
|
|
213
|
+
if (!path) {
|
|
214
|
+
pushLine("Clipboard image could not be saved (write failed)", C.dim)
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
const insert = `read_image ${path}`
|
|
218
|
+
const chars = [...insert] // codepoint 数组(同 insertPastedText 口径——cwd 含星平面字符时游标不漂)
|
|
219
|
+
state.input.splice(state.cursor, 0, ...chars)
|
|
220
|
+
state.cursor += chars.length
|
|
221
|
+
pushLine(`[image pasted → ${path}]`, C.tool)
|
|
179
222
|
render()
|
|
223
|
+
return { insert, path }
|
|
180
224
|
}
|
package/src/tui/cmd-advisor.mjs
CHANGED
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
import { readFileSync } from "node:fs"
|
|
5
5
|
import { ansi, C } from "./ansi.mjs"
|
|
6
6
|
import { activeSlot, slotPath } from "@thincoder/core/session.mjs"
|
|
7
|
+
// off 形族单源(`docs/core/design/MODEL-SPECS.md` §16.2 / §16.3 / §16.4)——取形与「有效 off 路径」
|
|
8
|
+
// 判据一律自本档取,本档零副本(叶档:零 import 纯函数)。
|
|
9
|
+
import { thinkOffShape, thinkOffPath } from "@thincoder/core/think-off.mjs"
|
|
7
10
|
import { writeSessionFile } from "./cmd-eng.mjs"
|
|
8
11
|
|
|
9
12
|
export async function handleAdvisorCommand(ctx) {
|
|
10
13
|
const { agent, showPicker, pushLine, pushLabel } = ctx
|
|
11
14
|
const cfg = agent.config.advisor ??= {}
|
|
15
|
+
// 入口预载规格面(懒加载面零改:不做顶层静态 import)——同步的状态行(`advisorStatus`)亦须取 spec。
|
|
16
|
+
const { specForModel } = await import("@thincoder/core/config.mjs")
|
|
12
17
|
// Deprecated field cleanup (2026-08-21): advisor.enabled is no longer read
|
|
13
18
|
// anywhere; drop it here so persist() never re-writes the stale flag.
|
|
14
19
|
delete cfg.enabled
|
|
@@ -17,13 +22,20 @@ export async function handleAdvisorCommand(ctx) {
|
|
|
17
22
|
if (ctx.persistRaw) {
|
|
18
23
|
await ctx.persistRaw((raw) => {
|
|
19
24
|
raw.agent ??= {}
|
|
20
|
-
|
|
25
|
+
// advisor.guard is session-level (#382 同口径收 · slot authority — VSC parity): the guard
|
|
26
|
+
// never rides the config write — only the config-scoped keys (model/thinking/effort/timeout)
|
|
27
|
+
// persist;盘上既有 guard 值原样保留(回退面不动 —— 读链 = 槽 ?? config)。
|
|
28
|
+
const scoped = { ...cfg }
|
|
29
|
+
delete scoped.guard
|
|
30
|
+
const diskAdvisor = typeof raw.agent.advisor === "object" && raw.agent.advisor !== null ? raw.agent.advisor : {}
|
|
31
|
+
raw.agent.advisor = { ...diskAdvisor, ...scoped }
|
|
21
32
|
})
|
|
22
33
|
}
|
|
23
34
|
}
|
|
24
35
|
|
|
25
|
-
// Guard-only
|
|
26
|
-
// the CURRENT session slot
|
|
36
|
+
// Guard-only slot write (2026-08-29 — advisor.guard is session-level; #382 同口径收): the guard
|
|
37
|
+
// goes into the CURRENT session slot (shared with VS Code) — the slot is the single authority;
|
|
38
|
+
// no config.json mirror write (与 VSC 面板写面同判——guard 不入 config 写面).
|
|
27
39
|
// Other advisor keys (model/thinking/effort/timeout) stay config-scoped — persist() only.
|
|
28
40
|
const persistGuard = async () => {
|
|
29
41
|
try {
|
|
@@ -33,19 +45,23 @@ export async function handleAdvisorCommand(ctx) {
|
|
|
33
45
|
data.advisor = { ...(typeof data.advisor === "object" && data.advisor !== null ? data.advisor : {}), guard: cfg.guard === true }
|
|
34
46
|
writeSessionFile(p, data)
|
|
35
47
|
}
|
|
36
|
-
} catch { /* slot missing/unreadable —
|
|
37
|
-
await persist()
|
|
48
|
+
} catch { /* slot missing/unreadable — guard stays in-memory for this session (zero config write) */ }
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
// Lazy model cache — fetched once per /advisor session
|
|
41
52
|
let modelCache = null
|
|
42
53
|
|
|
43
54
|
// ── State helpers ──
|
|
55
|
+
/** 状态行(§16.4):无有效 off 路径(`thinkOffPath === false`)⇒ **不报 off**——残留 off 标记
|
|
56
|
+
* (`null` / `{type:"disabled"}`)按「未设档」渲染:有档报 `on (<档>)`,无档报 `on`
|
|
57
|
+
* (服务端恒思考 / 形不发字段 ⇒ 「Think: off」会是假断言)。 */
|
|
44
58
|
function advisorStatus() {
|
|
45
59
|
const curModel = cfg.model || agent.provider.model
|
|
46
|
-
const
|
|
47
|
-
|
|
60
|
+
const offPath = thinkOffPath(specForModel(getEffectiveModel(agent, cfg)))
|
|
61
|
+
const offMarked = cfg.thinking === null || cfg.thinking?.type === "disabled"
|
|
62
|
+
const thinkInfo = offPath && offMarked ? "off"
|
|
48
63
|
: cfg.reasoningEffort ? `on (${cfg.reasoningEffort})`
|
|
64
|
+
: offMarked ? "on"
|
|
49
65
|
: cfg.thinking ? `on (${cfg.thinking.type})` : "(main)"
|
|
50
66
|
return `Advisor | Model: ${curModel} | Think: ${thinkInfo}`
|
|
51
67
|
}
|
|
@@ -55,12 +71,14 @@ export async function handleAdvisorCommand(ctx) {
|
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
/** off 形落盘(`think_off` 与归一后的 `effort_none` 共用——单一口径,防同状态两公式):
|
|
58
|
-
*
|
|
74
|
+
* 取形 = 单源 `thinkOffShape`(`@thincoder/core/think-off.mjs`;§15.4-2 族别判据 / §16.2 生产者表)——
|
|
75
|
+
* effort 族(`thinkApi === "effort"`)⇒ `thinking: null`(载荷层 off 门首款要求;`{type:"disabled"}`
|
|
76
|
+
* 不开门 ⇒ 关思考静默失效);其余(type 族默认 / 自定义开值族)⇒ `{type:"disabled"}`(达载荷层)。
|
|
77
|
+
* 三支同清 `reasoningEffort`(§16.6-⑵:off 标记与档位不得同盘——F2 违约族)。 */
|
|
59
78
|
async function applyThinkOff() {
|
|
60
|
-
const { specForModel } = await import("@thincoder/core/config.mjs")
|
|
61
79
|
const spec = specForModel(getEffectiveModel(agent, cfg))
|
|
62
|
-
|
|
63
|
-
cfg.
|
|
80
|
+
cfg.thinking = thinkOffShape(spec)
|
|
81
|
+
delete cfg.reasoningEffort
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
// ── Model picker sub-loop ──
|
|
@@ -107,7 +125,6 @@ export async function handleAdvisorCommand(ctx) {
|
|
|
107
125
|
pushLabel("❯ Advisor", ansi.bold + C.tool)
|
|
108
126
|
pushLine("Thinking: using main model settings", C.tool)
|
|
109
127
|
} else if (c.action === "think_on") {
|
|
110
|
-
const { specForModel } = await import("@thincoder/core/config.mjs")
|
|
111
128
|
const spec = specForModel(getEffectiveModel(agent, cfg))
|
|
112
129
|
cfg.thinking = { type: spec.thinkEnabledValue ?? "enabled" }
|
|
113
130
|
if (spec.thinkApi === "effort") delete cfg.thinking
|
|
@@ -129,6 +146,10 @@ export async function handleAdvisorCommand(ctx) {
|
|
|
129
146
|
pushLabel("❯ Advisor", ansi.bold + C.tool)
|
|
130
147
|
pushLine("Thinking: OFF", C.tool)
|
|
131
148
|
} else if (c.action.startsWith("effort_")) {
|
|
149
|
+
// #346-①(§16.4 表末行):选档 = 要思考 ⇒ 先清 `thinking === null` 标记(对齐
|
|
150
|
+
// `cmd-think.mjs:119-120` 先例 / §15.4-2 末句);`{type:"disabled"}` 不动(同先例;
|
|
151
|
+
// 残余登记 = §16.6-⑸——type 族 off ⇒ 选档 后该标记保持)。
|
|
152
|
+
if (cfg.thinking === null) delete cfg.thinking
|
|
132
153
|
cfg.reasoningEffort = c.action.slice(7)
|
|
133
154
|
await persist()
|
|
134
155
|
pushLabel("❯ Advisor", ansi.bold + C.tool)
|
|
@@ -247,6 +268,7 @@ async function buildThinkingEntries(agent, cfg) {
|
|
|
247
268
|
const thinkOnValue = spec.thinkEnabledValue ?? "enabled"
|
|
248
269
|
const isCustomThink = thinkOnValue !== "enabled"
|
|
249
270
|
const isEffortOnly = spec.thinkApi === "effort"
|
|
271
|
+
const offPath = thinkOffPath(spec)
|
|
250
272
|
const effortLevels = spec.reasoningEffortEnum ?? ["high", "max"]
|
|
251
273
|
|
|
252
274
|
const curEffort = cfg.reasoningEffort ?? providerForDefaults.reasoningEffort
|
|
@@ -260,7 +282,11 @@ async function buildThinkingEntries(agent, cfg) {
|
|
|
260
282
|
if (!isEffortOnly) {
|
|
261
283
|
entries.push({ type: "header", text: "Thinking mode" })
|
|
262
284
|
entries.push({ type: "item", text: `Enabled ${thinkingEnabled ? "← current" : ""}`, action: "think_on" })
|
|
263
|
-
|
|
285
|
+
// §16.4:无有效 off 路径(服务端强制族 / 形不发字段)⇒ Disabled 项**不渲染**;
|
|
286
|
+
// Enabled 项保留 = 残留 `{type:"disabled"}` 标记的恢复径(`/think on` 同旨)。
|
|
287
|
+
if (offPath) {
|
|
288
|
+
entries.push({ type: "item", text: `Disabled ${(curThinking?.type === "disabled" || curThinking === null) ? "← current" : ""}`, action: "think_off" })
|
|
289
|
+
}
|
|
264
290
|
}
|
|
265
291
|
entries.push({ type: "header", text: "Reasoning effort" })
|
|
266
292
|
for (const level of effortLevels) {
|
package/src/tui/cmd-clear.mjs
CHANGED
|
@@ -9,14 +9,14 @@ export async function handleClearCommand(ctx) {
|
|
|
9
9
|
], { defaultIndex: 1 })
|
|
10
10
|
if (e?.action === "yes") {
|
|
11
11
|
state.lines = []
|
|
12
|
-
state._linesChars = 0 // TUI-OOM-ROOTCAUSE
|
|
12
|
+
state._linesChars = 0 // TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.3):行集清空 → 字符账同步归零
|
|
13
13
|
state.streaming = ""
|
|
14
14
|
render()
|
|
15
15
|
}
|
|
16
16
|
return
|
|
17
17
|
}
|
|
18
18
|
state.lines = []
|
|
19
|
-
state._linesChars = 0 // TUI-OOM-ROOTCAUSE
|
|
19
|
+
state._linesChars = 0 // TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.3):行集清空 → 字符账同步归零
|
|
20
20
|
state.streaming = ""
|
|
21
21
|
render()
|
|
22
22
|
}
|
package/src/tui/cmd-config.mjs
CHANGED
|
@@ -35,6 +35,7 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
35
35
|
let ac = agent.config?.agent ?? {}
|
|
36
36
|
let ec = agent.config?.embedding ?? {}
|
|
37
37
|
let tc = agent.config?.traces ?? {}
|
|
38
|
+
let dc = agent.config?.diagnostics ?? {}
|
|
38
39
|
|
|
39
40
|
// agent.config.proxy 已被 loadConfig 归一化为 { uri, web, model } | undefined
|
|
40
41
|
function proxySummary() {
|
|
@@ -250,7 +251,7 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
250
251
|
}
|
|
251
252
|
}
|
|
252
253
|
|
|
253
|
-
// ──
|
|
254
|
+
// ── 并发池子菜单(agent.poolLimits——读/改三域;保存经 saveProxy
|
|
254
255
|
// 落盘 + reloadConfig 热应用——下个 spawn 生效)──
|
|
255
256
|
async function poolMenu() {
|
|
256
257
|
let poolIdx = 0
|
|
@@ -341,6 +342,7 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
341
342
|
ac = agent.config?.agent ?? {}
|
|
342
343
|
ec = agent.config?.embedding ?? {}
|
|
343
344
|
tc = agent.config?.traces ?? {}
|
|
345
|
+
dc = agent.config?.diagnostics ?? {}
|
|
344
346
|
const consultCount = (ac.consultModels ?? []).length
|
|
345
347
|
const pl = ac.poolLimits ?? {}
|
|
346
348
|
const cur = poolCur(pl, DEFAULTS)
|
|
@@ -354,6 +356,7 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
354
356
|
{ type: "item", text: `agent.verifyGuard = ${ac.verifyGuard === true ? "on" : "off"}`, action: "agent.verifyGuard" },
|
|
355
357
|
{ type: "item", text: `traces.enabled = ${tc.enabled === false ? "off" : "on"}(轨迹存档——发布默认关——隐私)`, action: "traces.enabled" },
|
|
356
358
|
{ type: "item", text: `traces.retentionHours = ${tc.retentionHours ?? 24} h(超期文件启动时清理)`, action: "traces.retentionHours" },
|
|
359
|
+
{ type: "item", text: `diagnostics.heapSnapshot = ${dc.heapSnapshot === true ? "on" : "off"}(堆快照采集——默认关——桌面会话即时生效)`, action: "diagnostics.heapSnapshot" },
|
|
357
360
|
{ type: "item", text: `agent.consultModels = ${consultCount} model(s)${consultCount ? ` (${(ac.consultModels ?? []).map((m) => m.provider + ":" + m.model).join(", ")})` : ""}`, action: "consult" },
|
|
358
361
|
{ type: "item", text: `agent.consultTurns = ${ac.consultTurns ?? 40}`, action: "agent.consultTurns" },
|
|
359
362
|
{ type: "item", text: `agent.consultTimeoutMs = ${Math.round((ac.consultTimeoutMs ?? 600000) / 60000)} min`, action: "agent.consultTimeoutMs" },
|
|
@@ -377,6 +380,7 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
377
380
|
pushLine(`agent.verifyGuard: ${ac.verifyGuard === true ? "on" : "off"}`, C.dim)
|
|
378
381
|
pushLine(`traces.enabled: ${tc.enabled === false ? "off" : "on"}(默认 off——发布隐私——本地分析可开)`, C.dim)
|
|
379
382
|
pushLine(`traces.retentionHours: ${tc.retentionHours ?? 24}(超期文件启动清理——D-TR10)`, C.dim)
|
|
383
|
+
pushLine(`diagnostics.heapSnapshot: ${dc.heapSnapshot === true ? "on" : "off"}(堆快照采集——默认关——桌面会话即时生效)`, C.dim)
|
|
380
384
|
pushLine(`agent.consultModels: ${(ac.consultModels ?? []).map((m) => `${m.provider}:${m.model}${m.effort ? ` (${m.effort})` : ""}`).join(", ") || "(none)"}`, C.dim)
|
|
381
385
|
pushLine(`agent.poolLimits: { engCoder: ${cur("engCoder")}, other: ${cur("other")}, advisor: ${cur("advisor")} }(async 分域上限——默认 4/4/4——agent.poolLimits 可配)`, C.dim)
|
|
382
386
|
pushLine(`agent.consultTurns: ${ac.consultTurns ?? 40}`, C.dim)
|
|
@@ -437,6 +441,18 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
437
441
|
continue // 回主菜单(Esc 退出)
|
|
438
442
|
}
|
|
439
443
|
|
|
444
|
+
if (choice.action === "diagnostics.heapSnapshot") {
|
|
445
|
+
const newVal = dc.heapSnapshot !== true // 显式写布尔(默认关——fail-closed 显示判定同向)
|
|
446
|
+
try {
|
|
447
|
+
await saveProxy((raw) => {
|
|
448
|
+
raw.diagnostics = { ...(raw.diagnostics ?? {}), heapSnapshot: newVal }
|
|
449
|
+
})
|
|
450
|
+
pushLabel("❯ Config", ansi.bold + C.tool)
|
|
451
|
+
pushLine(`diagnostics.heapSnapshot = ${newVal ? "on" : "off"}(桌面会话即时生效 ∕ 本进程下次启动)`, C.tool)
|
|
452
|
+
} catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
|
|
453
|
+
continue // 回主菜单(Esc 退出)
|
|
454
|
+
}
|
|
455
|
+
|
|
440
456
|
// Embedding model is fixed (BAAI/bge-m3, SiliconFlow) — no picker; it's over-engineering
|
|
441
457
|
// to expose model choice when the vector index format assumes one embedding space.
|
|
442
458
|
|
package/src/tui/cmd-eng.mjs
CHANGED
|
@@ -52,7 +52,7 @@ export async function handleEngCommand(ctx) {
|
|
|
52
52
|
// FR31 ③ / KD10(翻转点②·CLI `/eng`):ON ⇒ 清 plan 残留(内存位 + 未注入的 plan 提示语);
|
|
53
53
|
// OFF 方向不动(普通模式零改——FR31 边界)。提示行仅在真清到东西时出(不喧宾)。
|
|
54
54
|
const planReset = agent.config.agent.engineering ? clearPlanMode(agent) : false
|
|
55
|
-
// §
|
|
55
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.10 D-24b: per-review instances die with the mode (fresh convergence cycles
|
|
56
56
|
// on the next toggle).
|
|
57
57
|
agent._advisorRuns = new Map()
|
|
58
58
|
if (!agent.config.agent.engineering) {
|
package/src/tui/cmd-mcp.mjs
CHANGED
|
@@ -181,7 +181,7 @@ Return ONLY the JSON object:`,
|
|
|
181
181
|
}],
|
|
182
182
|
tools: [],
|
|
183
183
|
signal: AbortSignal.timeout(15_000),
|
|
184
|
-
// §
|
|
184
|
+
// TRACES.md §6.1 D-TR4/D-TR6(2026-09-04 fix round1):/mcp ai 生成调用经 chat() 唯一
|
|
185
185
|
// 采集点——补轨迹元数据 + traces 开关透传(agent.config.traces.enabled——
|
|
186
186
|
// 关=不落盘必须全覆盖——与 agent.mjs/context.mjs 同模式)
|
|
187
187
|
logCtx: {
|
package/src/tui/cmd-new.mjs
CHANGED
|
@@ -15,12 +15,12 @@ export async function handleNewCommand(ctx) {
|
|
|
15
15
|
// slot(实锤 .19/.3 双副本)。_slot 更新为粘性新槽位。
|
|
16
16
|
resetSessionState(agent)
|
|
17
17
|
agent._slot = slot
|
|
18
|
-
// §14
|
|
18
|
+
// SESSION.md §6.14:/new 绑定新槽记录存储(baseHistory 空——identity 此时为 null = 待固化;
|
|
19
19
|
// 首次保存/首条追加时补写现场身份)
|
|
20
20
|
bindRecordStore(agent, { slotFile: slotPath(agent.cwd, slot), identity: agent._sessionStart ?? null, baseHistory: [] })
|
|
21
21
|
state.tasks = []
|
|
22
22
|
state.lines = []
|
|
23
|
-
state._linesChars = 0 // TUI-OOM-ROOTCAUSE
|
|
23
|
+
state._linesChars = 0 // TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.3):行集清空 → 字符账同步归零
|
|
24
24
|
state.streaming = ""
|
|
25
25
|
render()
|
|
26
26
|
pushLine(`New session started (slot ${slot}; /session to switch back)`, C.dim)
|
package/src/tui/cmd-reindex.mjs
CHANGED
|
@@ -45,7 +45,7 @@ export async function handleReindexCommand(ctx) {
|
|
|
45
45
|
const unlisted = cr?.unlistedExts?.count ? cr.unlistedExts : dr?.unlistedExts
|
|
46
46
|
if (unlisted?.count > 0) {
|
|
47
47
|
const sample = unlisted.exts.map((e) => e.ext).join(" ")
|
|
48
|
-
pushLine(` ${unlisted.count} file(s) with unlisted extension(s) skipped (${sample}) — declare them in .
|
|
48
|
+
pushLine(` ${unlisted.count} file(s) with unlisted extension(s) skipped (${sample}) — declare them in PROJECT-MANIFEST.json (index.codeExtensions / index.docExtensions) to index them`, C.warn)
|
|
49
49
|
}
|
|
50
50
|
pushLine(`[reindex] Done, ${total} entries total. Vectors will be lazily generated on next search.`, C.tool)
|
|
51
51
|
}
|
package/src/tui/cmd-session.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy, readEndMarker, sessionDescriptor, loadSlotFile } from "@thincoder/core/session.mjs"
|
|
1
|
+
import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy, readEndMarker, sessionDescriptor, loadSlotFile, ledgerHealth } from "@thincoder/core/session.mjs"
|
|
2
2
|
import { resolveEngineeringManifest } from "@thincoder/core/manifest.mjs"
|
|
3
3
|
import { ansi, C } from "./ansi.mjs"
|
|
4
4
|
import { restoreLines } from "./startup.mjs"
|
|
@@ -20,7 +20,7 @@ export async function handleRenameCommand(ctx, args) {
|
|
|
20
20
|
pushLine(`Title too long (max 80 chars)`, C.error)
|
|
21
21
|
return
|
|
22
22
|
}
|
|
23
|
-
//
|
|
23
|
+
// renameSlot 契约 { ok, reason }——失败原因对用户可见(F3)
|
|
24
24
|
const r = renameSlot(agent.cwd, slot, title)
|
|
25
25
|
if (!r.ok) {
|
|
26
26
|
const why = {
|
|
@@ -58,7 +58,14 @@ export async function handleSessionCommand(ctx) {
|
|
|
58
58
|
return `${dt.getMonth() + 1}/${dt.getDate()} ${String(dt.getHours()).padStart(2, "0")}:${String(dt.getMinutes()).padStart(2, "0")}`
|
|
59
59
|
}
|
|
60
60
|
const truncate = (s, n) => (stringWidth(s) <= n ? s : sliceByWidth(s, Math.max(1, n - 1)) + "…")
|
|
61
|
+
// F-L4(账本可靠批 · SESSION.md §6.25 判据句 4):账本异常 ⇒ 列表头部警示行(正常 ⇒ 一行不加
|
|
62
|
+
// ——负断言);在场条件 = `refused > 0 ∨ scene`(不继承会话计数条件)。文案 = 三端同句(zh 在册字面)。
|
|
63
|
+
const lh = ledgerHealth(agent.cwd)
|
|
64
|
+
const ledgerWarn = (lh.refused > 0 || lh.scene)
|
|
65
|
+
? `会话账本异常(${lh.refused > 0 ? lh.lastReason : "scene"})——打开会话即自动补回${lh.scene ? ";损坏现场档保留 30 天" : ""}`
|
|
66
|
+
: null
|
|
61
67
|
const entries = [
|
|
68
|
+
...(ledgerWarn ? [{ type: "header", text: ledgerWarn }] : []),
|
|
62
69
|
{ type: "header", text: `Sessions (● = active, ↑↓ select, Enter switch, Esc cancel; /rename <title> renames the active one)` },
|
|
63
70
|
...slots.map((s) => {
|
|
64
71
|
// 2026-08-31 会诊:行宽必须按显示宽度截断(原按 UTF-16 length 截 40 = 中文 80 格,
|
|
@@ -66,7 +73,8 @@ export async function handleSessionCommand(ctx) {
|
|
|
66
73
|
// wrap → 残影);title 此前完全不截断(可任意长)也是超宽源,一并宽度截断。
|
|
67
74
|
const rawLabel = s.title || (s.firstMessage ? `"${s.firstMessage}"` : "(empty)")
|
|
68
75
|
const label = truncate(rawLabel, 36)
|
|
69
|
-
|
|
76
|
+
// 计数不可得(`null`)⇒ 「— turns」占位(禁显示 0——真 0 与未知可分;§6.25 判据句 4)
|
|
77
|
+
const turns = s.turnCount == null ? "— turns" : `${s.turnCount} turns`
|
|
70
78
|
const when = shortDate(s.updatedAt)
|
|
71
79
|
const model = s.activeProvider ? ` — ${s.activeProvider}` : ""
|
|
72
80
|
const marker = s.slot === highlightSlot ? " ●" : ""
|
|
@@ -114,11 +122,11 @@ export async function handleSessionCommand(ctx) {
|
|
|
114
122
|
// #41 准翻列:附着动作 = 翻转面自己赋值(`agent.manifest` ← 判据结果——相位行当回合起活)。
|
|
115
123
|
if (manifestAfterSwitch) agent.manifest = manifestAfterSwitch
|
|
116
124
|
// Rebuild from history (lazy) — the display snapshot is deprecated.
|
|
117
|
-
// §14
|
|
125
|
+
// SESSION.md §6.14:描述符 { history(尾窗+±1), total, base }——标签口径 = total(非窗口长度);
|
|
118
126
|
// 未绑定(占用分支——模式 F)回退全量数组。
|
|
119
127
|
const desc = sessionDescriptor(agent, data)
|
|
120
128
|
state.lines = []
|
|
121
|
-
state._linesChars = 0 // TUI-OOM-ROOTCAUSE
|
|
129
|
+
state._linesChars = 0 // TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.3):切槽重建行集 → 字符账归零,restoreLines 重新入账
|
|
122
130
|
restoreLines(state, desc)
|
|
123
131
|
state.tasks = agent.tasks ?? []
|
|
124
132
|
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
package/src/tui/cmd-think.mjs
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Interactive loop UX — stays in menu after each action, Esc to exit.
|
|
3
3
|
* ctx: { agent, showPicker, syncProviderField, pushLine, pushLabel } */
|
|
4
4
|
import { ansi, C } from "./ansi.mjs"
|
|
5
|
+
// off 形族单源(`docs/core/design/MODEL-SPECS.md` §16.2 / §16.4)——取形与「有效 off 路径」判据
|
|
6
|
+
// 一律自本档取,本档零副本(叶档:零 import 纯函数)。
|
|
7
|
+
import { thinkOffShape, thinkOffPath } from "@thincoder/core/think-off.mjs"
|
|
5
8
|
|
|
6
9
|
export async function handleThinkCommand(ctx, args = []) {
|
|
7
10
|
const { agent, showPicker, syncProviderField, pushLine, pushLabel } = ctx
|
|
@@ -14,11 +17,26 @@ export async function handleThinkCommand(ctx, args = []) {
|
|
|
14
17
|
const thinkOnValue = spec.thinkEnabledValue ?? "enabled"
|
|
15
18
|
const isCustomThink = thinkOnValue !== "enabled"
|
|
16
19
|
const effortLevels = spec.reasoningEffortEnum ?? ["high", "max"]
|
|
20
|
+
// 有效 off 路径(判据单源 = `@thincoder/core/think-off.mjs`;§16.4)——无 ⇒ 不提供 / 不宣称 off。
|
|
21
|
+
const offPath = thinkOffPath(spec)
|
|
22
|
+
|
|
23
|
+
/** 环内头与回执共用的状态式(§16.4):无有效 off 路径 ⇒ 恒报 ON——残留 off 标记(`null` /
|
|
24
|
+
* `{type:"disabled"}`)不生效(服务端恒思考 / 形不发字段),「Thinking: OFF」会是假断言。
|
|
25
|
+
* `thinking:null` 是显式 off 标记(NF1 约定)——不得落入"未设置即 ON"的默认显示(评审 #3);
|
|
26
|
+
* 用 `!== null` 而非 `!= null`:undefined(从未设置)须保持既有 ON 显示(qwen3.x 服务端默认开思考)。 */
|
|
27
|
+
const thinkingOn = () => !offPath || cur.thinking?.type === thinkOnValue
|
|
28
|
+
|| (cur.thinking !== null && cur.thinking?.type === undefined && !isCustomThink)
|
|
17
29
|
|
|
18
30
|
const autoThinkEnabled = agent.config?.agent?.autoThink === true
|
|
19
31
|
const sub = args[0]?.toLowerCase()
|
|
20
32
|
if (sub === "on" || sub === "off") {
|
|
21
33
|
if (autoThinkEnabled) { pushLine("Auto-think is ON — manual settings are overridden each turn; turn Auto off first via /think", C.error); return }
|
|
34
|
+
// §16.4:无有效 off 路径 ⇒ 拒绝该动作(不写盘)——服务端恒思考 / 形不发字段,供一个不生效
|
|
35
|
+
// 的入口 + 措辞免责 = 半实现(§16.12 项 3)。`/think on` 与档位面零改。
|
|
36
|
+
if (sub === "off" && !offPath) {
|
|
37
|
+
pushLine("No off path for this model — thinking cannot be turned off here (use /think effort <level>)", C.error)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
22
40
|
await applyThink({ action: sub }, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue)
|
|
23
41
|
pushLabel("❯ Think", ansi.bold + C.tool)
|
|
24
42
|
pushLine(`Thinking: ${sub}`, C.tool)
|
|
@@ -43,16 +61,16 @@ export async function handleThinkCommand(ctx, args = []) {
|
|
|
43
61
|
let mainIdx = 0
|
|
44
62
|
for (;;) {
|
|
45
63
|
const autoOn = agent.config?.agent?.autoThink === true
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
const thinkingEnabled = cur.thinking?.type === thinkOnValue
|
|
49
|
-
|| (cur.thinking !== null && cur.thinking?.type === undefined && !isCustomThink)
|
|
64
|
+
// 头 / 回执同式(`thinkingOn` 单口径)——状态与回执不得各自成式。
|
|
65
|
+
const thinkingEnabled = thinkingOn()
|
|
50
66
|
|
|
51
67
|
const entries = [
|
|
52
68
|
{ type: "header", text: `Auto: ${autoOn ? "ON" : "OFF"} | Thinking: ${thinkingEnabled ? "ON" : "OFF"} | Effort: ${cur.reasoningEffort || "—"}` },
|
|
53
69
|
{ type: "item", text: `Auto: ${autoOn ? "ON" : "OFF"}`, action: "auto" },
|
|
54
70
|
]
|
|
55
|
-
|
|
71
|
+
// Thinking 开关项:非 effort 族 ∧ 有有效 off 路径才渲染(§16.4——无有效 off 路径族不提供该动作,
|
|
72
|
+
// 入口在位 = 对无效操作的假告知)。
|
|
73
|
+
if (!isEffortOnly && offPath && !autoOn) {
|
|
56
74
|
entries.push({ type: "item", text: `Thinking: ${thinkingEnabled ? "ON" : "OFF"}`, action: thinkingEnabled ? "off" : "on" })
|
|
57
75
|
}
|
|
58
76
|
if (!autoOn) {
|
|
@@ -66,10 +84,6 @@ export async function handleThinkCommand(ctx, args = []) {
|
|
|
66
84
|
if (!e) return // Esc
|
|
67
85
|
mainIdx = Math.max(0, entries.filter((en) => en.type === "item").indexOf(e))
|
|
68
86
|
|
|
69
|
-
const prevAuto = autoOn
|
|
70
|
-
const prevThinking = thinkingEnabled
|
|
71
|
-
const prevEffort = cur.reasoningEffort
|
|
72
|
-
|
|
73
87
|
await applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue)
|
|
74
88
|
|
|
75
89
|
// Feedback
|
|
@@ -81,11 +95,9 @@ export async function handleThinkCommand(ctx, args = []) {
|
|
|
81
95
|
} else if (e.action === "effort") {
|
|
82
96
|
pushLine(e.level === "none" ? "Thinking: OFF" : `Reasoning effort: ${e.level}`, C.tool)
|
|
83
97
|
} else {
|
|
84
|
-
//
|
|
85
|
-
// effort 型 off 后回执误报 ON
|
|
86
|
-
|
|
87
|
-
|| (cur.thinking !== null && cur.thinking?.type === undefined && !isCustomThink)
|
|
88
|
-
pushLine(`Thinking: ${nowEnabled ? "ON" : "OFF"}`, C.tool)
|
|
98
|
+
// 与菜单头(`thinkingOn`)同式:`thinking:null`(NF1 显式 off)落 OFF;无有效 off 路径族
|
|
99
|
+
// 恒报 ON(§16.4——残留 off 标记不生效;本式缺守卫时 effort 型 off 后回执误报 ON = 红)。
|
|
100
|
+
pushLine(`Thinking: ${thinkingOn() ? "ON" : "OFF"}`, C.tool)
|
|
89
101
|
}
|
|
90
102
|
}
|
|
91
103
|
}
|
|
@@ -93,7 +105,9 @@ export async function handleThinkCommand(ctx, args = []) {
|
|
|
93
105
|
/** Shared apply logic — extracted from handleThinkCommand for reuse in both fast path and loop.
|
|
94
106
|
* Named export (2026-09-20): the on-default-effort rule (§2.8) is unit-tested by driving this
|
|
95
107
|
* directly with a synthetic spec — behavior otherwise unchanged.
|
|
96
|
-
* Effort 档位 `"none"` = 关思考 ⇒ 入口归一到 off 路径(与菜单 off 项同形——见内注释)。
|
|
108
|
+
* Effort 档位 `"none"` = 关思考 ⇒ 入口归一到 off 路径(与菜单 off 项同形——见内注释)。
|
|
109
|
+
* `spec` 同时供 off 取形(单源 = `@thincoder/core/think-off.mjs`);`isCustomThink` 参数为调用面
|
|
110
|
+
* 既有形状保留(off 支取形已归单源,本档不再自行分族)。 */
|
|
97
111
|
export async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue) {
|
|
98
112
|
const cur = agent.provider
|
|
99
113
|
// `effort` 档位取 `"none"` = **关思考**(非“强度零”):与本面 off 动作同语义 ⇒ 归一到 off 路径
|
|
@@ -124,7 +138,8 @@ export async function applyThink(e, agent, syncProviderField, spec, isEffortOnly
|
|
|
124
138
|
if (isEffortOnly) {
|
|
125
139
|
// Explicit off persists thinking:null (NF1 convention — distinguishable from autoThink's
|
|
126
140
|
// delete/undefined); "on" deletes the marker so enable_thinking maps from effort again.
|
|
127
|
-
|
|
141
|
+
// off 取形同走单源(`thinkOffShape`——effort 族 ⇒ `null`;本支与下方非 effort 支同源零副本)。
|
|
142
|
+
if (!enable) { cur.thinking = thinkOffShape(spec); delete cur.reasoningEffort }
|
|
128
143
|
// "on" 默认 effort 取 spec 枚举的**首个非 "none"** 档(批次档 §1.8-② / 设计 §2.8 D-9):硬编码 "high"
|
|
129
144
|
// 对 qwen3.8-max(enum xhigh/medium/low)无效,会被 core.mjs 枚举校验 throw(400 前置);而直接取枚举
|
|
130
145
|
// 首项在新档(首项 "none")等于把思考关掉 ⇒ 跳过 "none";枚举全 "none" 时与无枚举同型 ⇒ 回退 "high"
|
|
@@ -137,7 +152,10 @@ export async function applyThink(e, agent, syncProviderField, spec, isEffortOnly
|
|
|
137
152
|
cur.thinking = { type: thinkOnValue }
|
|
138
153
|
if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
139
154
|
} else {
|
|
140
|
-
|
|
155
|
+
// off 取形 = 单源 `thinkOffShape`(§15.4-2 / §16.2 生产者表第 2 行):非 effort 族 ⇒
|
|
156
|
+
// `{type:"disabled"}`——自定义开值族随 §15.4-2 表改判由 `undefined`(不发字段 = 服务端
|
|
157
|
+
// 默认 = 想)改本形(达载荷层 = 真 off)。
|
|
158
|
+
cur.thinking = thinkOffShape(spec)
|
|
141
159
|
delete cur.reasoningEffort
|
|
142
160
|
}
|
|
143
161
|
await syncProviderField(agent.activeProvider, "thinking", cur.thinking)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cmd-timers.mjs — `/timers`:在途 timer 只读列表(最薄)。
|
|
3
|
+
*
|
|
4
|
+
* 机制单源 = `docs/core/design/AGENT-LOOP-ASYNC-POOL.md` §6.30;显示形态单源 = `docs/cli/design/TUI.md`
|
|
5
|
+
* §7.6(逐条 dim 行 `⏰<i> · 剩余 <mm:ss> · <message 首行(截断)>`;零在途 ⇒ 一行 `无在途 timer`)。
|
|
6
|
+
* **取消面不做**(取消 = 控制面——门 / 回执 / 持久化语义;停轮 = Ctrl+C,停机制 = `agent.timerWake` 关)。
|
|
7
|
+
* 取值 = `agent._pendingTimers` 活读(每次调用现算——零缓存);剩余按 `expiresAt - now` 现算。
|
|
8
|
+
*/
|
|
9
|
+
import { C } from "./ansi.mjs"
|
|
10
|
+
import { sanitizeDisplay, sliceByWidth, stringWidth } from "./render.mjs"
|
|
11
|
+
|
|
12
|
+
/** 剩余 mm:ss(到期未送达 ⇒ 夹到 `00:00`——到期态另由状态行警示色承载,见 TUI.md §7.6)。 */
|
|
13
|
+
function remainText(expiresAt, now) {
|
|
14
|
+
const sec = Math.max(0, Math.ceil((expiresAt - now) / 1000))
|
|
15
|
+
return `${String(Math.floor(sec / 60)).padStart(2, "0")}:${String(sec % 60).padStart(2, "0")}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** message 首行(截断——60 显示列 + `…`,同会话标题段口径)。 */
|
|
19
|
+
function headText(message) {
|
|
20
|
+
const head = sanitizeDisplay(String(message ?? "").split("\n")[0])
|
|
21
|
+
return stringWidth(head) > 60 ? sliceByWidth(head, 59) + "…" : head
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function handleTimersCommand(ctx) {
|
|
25
|
+
const { agent, pushLine } = ctx
|
|
26
|
+
const timers = agent?._pendingTimers ?? []
|
|
27
|
+
if (timers.length === 0) {
|
|
28
|
+
pushLine("无在途 timer", C.dim)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
const now = Date.now()
|
|
32
|
+
timers.forEach((t, i) => pushLine(`⏰${i + 1} · 剩余 ${remainText(t.expiresAt, now)} · ${headText(t.message)}`, C.dim))
|
|
33
|
+
}
|
|
@@ -11,7 +11,7 @@ import { shiftFreezeAnchors } from "./subagent-blocks.mjs"
|
|
|
11
11
|
* assistantLabeled: boolean }} */
|
|
12
12
|
export function createConversationWriter({ state, render }) {
|
|
13
13
|
const pushLine = (text, color, kind) => {
|
|
14
|
-
// TUI-OOM-ROOTCAUSE(TUI.md §
|
|
14
|
+
// TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.1/§5.3):行额度单点——行文本过 capLine
|
|
15
15
|
// (LINE_MAX_CHARS——单行巨内容/无换行巨 chunk 的堵口)+ 总量账 + 预算对账。
|
|
16
16
|
const line = { text: capLine(text), color, _kind: kind }
|
|
17
17
|
state.lines.push(line)
|
|
@@ -31,7 +31,7 @@ export function createConversationWriter({ state, render }) {
|
|
|
31
31
|
|
|
32
32
|
/** Message block label: blank line + label line. Breathing space between user/assistant messages */
|
|
33
33
|
const pushLabel = (text, color) => {
|
|
34
|
-
// 行额度同 pushLine(§
|
|
34
|
+
// 行额度同 pushLine(§5.1——恢复行/标签行同口径)
|
|
35
35
|
if (state.lines.length > 0) {
|
|
36
36
|
const blank = { text: "", color: C.dim }
|
|
37
37
|
state.lines.push(blank)
|