thincoder 0.12.59 → 0.12.60
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 +38 -3
- package/README.md +2 -2
- package/bin/thincoder.mjs +80 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +7 -4
- package/src/advisor/messages.mjs +24 -4
- package/src/advisor/run.mjs +35 -33
- package/src/advisor.mjs +25 -6
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +102 -19
- package/src/agent/helpers.mjs +36 -0
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +18 -2
- package/src/agent/spawn-child.mjs +29 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +110 -108
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +121 -102
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/read-history.mjs +155 -31
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +7 -17
- package/src/agent-tools/subagent-actions.mjs +168 -130
- package/src/agent-tools/subagent-async.mjs +129 -174
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +45 -21
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +107 -555
- package/src/agent-tools/verify.mjs +118 -270
- package/src/agent.mjs +57 -190
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +3 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +56 -8
- package/src/context.mjs +5 -147
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +11 -11
- package/src/explore-distill.mjs +155 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +58 -48
- package/src/memory.mjs +3 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +9 -76
- package/src/prompts/advisor-round1.md +9 -68
- package/src/prompts/advisor-round2.md +7 -54
- package/src/prompts/advisor-round3.md +7 -54
- package/src/prompts/coder.md +7 -50
- package/src/prompts/consult-base.md +4 -24
- package/src/prompts/discipline.md +26 -44
- package/src/prompts/eng-coder.md +7 -32
- package/src/prompts/engineering-sub.md +3 -23
- package/src/prompts/engineering.md +53 -306
- package/src/prompts/explore.md +3 -12
- package/src/prompts/main.md +10 -32
- package/src/prompts/methodology-template.md +28 -48
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +16 -35
- package/src/provider/core.mjs +6 -67
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +11 -6
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/index.mjs +5 -6
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +3 -0
- package/src/tools/read.md +0 -1
- package/src/tools/shared.mjs +14 -13
- package/src/tools/system.mjs +44 -9
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +17 -228
- package/src/tui/cmd-config.mjs +48 -7
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +3 -2
- package/src/tui/interaction.mjs +3 -3
- package/src/tui/mouse.mjs +7 -1
- package/src/tui/pickers.mjs +40 -22
- package/src/tui/render-segments.mjs +27 -10
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +95 -263
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +61 -23
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +3 -3
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +37 -173
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +12 -6
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
package/src/tui/cmd-new.mjs
CHANGED
|
@@ -9,8 +9,9 @@ export async function handleNewCommand(ctx) {
|
|
|
9
9
|
const doNewSession = () => {
|
|
10
10
|
const slot = newSession(agent.cwd)
|
|
11
11
|
// 2026-08-31 会诊 F3:resetSessionState 清全量会话态(_fullHistory/title/_sessionStart/
|
|
12
|
-
// _engDesignToken
|
|
13
|
-
//
|
|
12
|
+
// 多槽设计凭证(_engDesignTokens——单值镜像 _engDesignToken 已退役 D3)/压缩与验证计数
|
|
13
|
+
// 等)——原实现只清 agent.history,新会话首次落盘把旧会话完整人类线 + 旧标题写进新
|
|
14
|
+
// slot(实锤 .19/.3 双副本)。_slot 更新为粘性新槽位。
|
|
14
15
|
resetSessionState(agent)
|
|
15
16
|
agent._slot = slot
|
|
16
17
|
state.tasks = []
|
package/src/tui/cmd-session.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy } from "../session.mjs"
|
|
1
|
+
import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy, readEndMarker } from "../session.mjs"
|
|
2
2
|
import { ansi, C } from "./ansi.mjs"
|
|
3
3
|
import { restoreLines } from "./startup.mjs"
|
|
4
4
|
import { stringWidth, sliceByWidth } from "./render.mjs"
|
|
@@ -19,8 +19,16 @@ export async function handleRenameCommand(ctx, args) {
|
|
|
19
19
|
pushLine(`Title too long (max 80 chars)`, C.error)
|
|
20
20
|
return
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// §12.2.5:renameSlot 契约 { ok, reason }——失败原因对用户可见(F3)
|
|
23
|
+
const r = renameSlot(agent.cwd, slot, title)
|
|
24
|
+
if (!r.ok) {
|
|
25
|
+
const why = {
|
|
26
|
+
"file-missing": `active session (slot ${slot}) not found`,
|
|
27
|
+
"parse-failure": `session file (slot ${slot}) is corrupted`,
|
|
28
|
+
"mtime-conflict": "session changed on disk concurrently — retry",
|
|
29
|
+
"invalid-slot": `invalid slot ${slot}`,
|
|
30
|
+
}[r.reason] ?? r.reason
|
|
31
|
+
pushLine(`Rename failed — ${why}`, C.error)
|
|
24
32
|
return
|
|
25
33
|
}
|
|
26
34
|
agent.title = title
|
|
@@ -37,6 +45,13 @@ export async function handleSessionCommand(ctx) {
|
|
|
37
45
|
pushLine("No sessions (use /new to start a new session)", C.dim)
|
|
38
46
|
return
|
|
39
47
|
}
|
|
48
|
+
// 2026-09-05 §10 D-5:本端高亮按端记录(● = 记录槽 ∈ 列表 ? 记录槽 : manifest active
|
|
49
|
+
// 回退——含"记录槽已被对端删除"的守卫);listSlots 的 manifest active 语义不变
|
|
50
|
+
// (ACP session/list 零变化——manifest active 保留为列表回退高亮)
|
|
51
|
+
const rec = readEndMarker(agent.cwd)
|
|
52
|
+
const highlightSlot = (rec?.slot != null && slots.some((s) => s.slot === rec.slot))
|
|
53
|
+
? rec.slot
|
|
54
|
+
: (slots.find((s) => s.isActive)?.slot ?? null)
|
|
40
55
|
const shortDate = (d) => {
|
|
41
56
|
const dt = new Date(d)
|
|
42
57
|
return `${dt.getMonth() + 1}/${dt.getDate()} ${String(dt.getHours()).padStart(2, "0")}:${String(dt.getMinutes()).padStart(2, "0")}`
|
|
@@ -53,7 +68,7 @@ export async function handleSessionCommand(ctx) {
|
|
|
53
68
|
const turns = s.turnCount > 0 ? `${s.turnCount} turns` : "0 turns"
|
|
54
69
|
const when = shortDate(s.updatedAt)
|
|
55
70
|
const model = s.activeProvider ? ` — ${s.activeProvider}` : ""
|
|
56
|
-
const marker = s.
|
|
71
|
+
const marker = s.slot === highlightSlot ? " ●" : ""
|
|
57
72
|
return {
|
|
58
73
|
type: "item",
|
|
59
74
|
text: `Slot ${s.slot} │ ${turns} │ ${when} │ ${label}${model}${marker}`,
|
package/src/tui/cmd-think.mjs
CHANGED
|
@@ -97,16 +97,16 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
|
|
|
97
97
|
// 开 auto = 要思考:清显式 off 标记(thinking:null,NF1 约定;交付评审 #1)——残留 null
|
|
98
98
|
// 会让 auto 每轮写入的 reasoning_effort 与 enable_thinking:false 矛盾同发(F2 违约)。
|
|
99
99
|
// 仅清 null:thinking-type 模型的 {type:"disabled"} 不在评审 #1 范围,保持既有语义。
|
|
100
|
-
if (cur.thinking === null) { delete cur.thinking; await syncProviderField("thinking", undefined) }
|
|
100
|
+
if (cur.thinking === null) { delete cur.thinking; await syncProviderField(agent.activeProvider, "thinking", undefined) }
|
|
101
101
|
delete cur.reasoningEffort
|
|
102
|
-
await syncProviderField("reasoningEffort", undefined)
|
|
102
|
+
await syncProviderField(agent.activeProvider, "reasoningEffort", undefined)
|
|
103
103
|
}
|
|
104
104
|
} else if (e.action === "effort") {
|
|
105
105
|
// 选档位 = 要思考:清显式 off 标记(thinking:null,NF1 约定;交付评审 #1)——残留 null 会让
|
|
106
106
|
// enable_thinking:false 与 reasoning_effort 矛盾同发(F2 违约)。仅清 null(同上注释)。
|
|
107
|
-
if (cur.thinking === null) { delete cur.thinking; await syncProviderField("thinking", undefined) }
|
|
107
|
+
if (cur.thinking === null) { delete cur.thinking; await syncProviderField(agent.activeProvider, "thinking", undefined) }
|
|
108
108
|
cur.reasoningEffort = e.level
|
|
109
|
-
await syncProviderField("reasoningEffort", e.level)
|
|
109
|
+
await syncProviderField(agent.activeProvider, "reasoningEffort", e.level)
|
|
110
110
|
} else {
|
|
111
111
|
const enable = e.action === "on"
|
|
112
112
|
if (isEffortOnly) {
|
|
@@ -116,9 +116,9 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
|
|
|
116
116
|
// "on" 默认 effort 取 spec 枚举首值(交付评审 #2):硬编码 "high" 对 qwen3.8-max
|
|
117
117
|
// (enum xhigh/medium/low)无效,会被 core.mjs 枚举校验 throw(400 前置)
|
|
118
118
|
else { delete cur.thinking; if (!cur.reasoningEffort) cur.reasoningEffort = spec.reasoningEffortEnum?.[0] ?? "high" }
|
|
119
|
-
await syncProviderField("thinking", cur.thinking)
|
|
120
|
-
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
121
|
-
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
119
|
+
await syncProviderField(agent.activeProvider, "thinking", cur.thinking)
|
|
120
|
+
if (!enable) await syncProviderField(agent.activeProvider, "reasoningEffort", undefined)
|
|
121
|
+
else await syncProviderField(agent.activeProvider, "reasoningEffort", cur.reasoningEffort)
|
|
122
122
|
} else {
|
|
123
123
|
if (enable) {
|
|
124
124
|
cur.thinking = { type: thinkOnValue }
|
|
@@ -127,11 +127,11 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
|
|
|
127
127
|
cur.thinking = isCustomThink ? undefined : { type: "disabled" }
|
|
128
128
|
delete cur.reasoningEffort
|
|
129
129
|
}
|
|
130
|
-
await syncProviderField("thinking", cur.thinking)
|
|
130
|
+
await syncProviderField(agent.activeProvider, "thinking", cur.thinking)
|
|
131
131
|
if (enable) {
|
|
132
|
-
await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
132
|
+
await syncProviderField(agent.activeProvider, "reasoningEffort", cur.reasoningEffort)
|
|
133
133
|
} else {
|
|
134
|
-
await syncProviderField("reasoningEffort", undefined)
|
|
134
|
+
await syncProviderField(agent.activeProvider, "reasoningEffort", undefined)
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
}
|
package/src/tui/cmd-upgrade.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ansi, C } from "./ansi.mjs"
|
|
2
|
+
import { upgradeFailureText } from "./update-notice.mjs"
|
|
2
3
|
|
|
3
4
|
/** /upgrade command: check for updates and optionally upgrade.
|
|
4
5
|
* ctx: { agent, pushLine, pushLabel, showPicker } */
|
|
5
6
|
export async function handleUpgradeCommand(ctx) {
|
|
6
|
-
const { pushLine, pushLabel, showPicker } = ctx
|
|
7
|
+
const { pushLine, pushLabel, showPicker, render } = ctx
|
|
7
8
|
const { checkForUpdate } = await import("../upgrade.mjs")
|
|
8
9
|
const { readFileSync } = await import("node:fs")
|
|
9
10
|
|
|
@@ -31,13 +32,27 @@ export async function handleUpgradeCommand(ctx) {
|
|
|
31
32
|
pushLine(`Upgrading to ${result.latest}...`, C.tool)
|
|
32
33
|
const { exec } = await import("node:child_process")
|
|
33
34
|
const cp = exec("npm install -g thincoder@latest", { windowsHide: true })
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
let stdout = ""
|
|
36
|
+
let done = false
|
|
37
|
+
cp.stdout?.on("data", (d) => { stdout += d })
|
|
38
|
+
cp.stderr?.on("data", (d) => { stdout += d })
|
|
39
|
+
// 2026-09-05(advisor 🟡#5/#7):异步回调必须显式 render(pushLine 只写 lines 不绘制——
|
|
40
|
+
// handleSlash 返回后的统一 render 先于 exec 完成,结论行要等下次输入才可见);失败文案
|
|
41
|
+
// 复用 upgradeFailureText(附 npm 输出尾 ≤3 行——与 update-notice 后台路径同诊断能力)
|
|
42
|
+
cp.on("error", (e) => {
|
|
43
|
+
if (done) return
|
|
44
|
+
done = true
|
|
45
|
+
pushLine(`✗ Upgrade failed (${e.message}). Run \`thincoder upgrade\` manually.`, C.error)
|
|
46
|
+
render?.()
|
|
47
|
+
})
|
|
36
48
|
cp.on("close", (code) => {
|
|
49
|
+
if (done) return
|
|
50
|
+
done = true
|
|
37
51
|
if (code === 0) {
|
|
38
52
|
pushLine(`✓ Upgraded to ${result.latest}. Restart to apply.`, C.tool)
|
|
39
53
|
} else {
|
|
40
|
-
pushLine(
|
|
54
|
+
pushLine(upgradeFailureText(code, stdout), C.error)
|
|
41
55
|
}
|
|
56
|
+
render?.()
|
|
42
57
|
})
|
|
43
58
|
}
|
|
@@ -1,26 +1,38 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
-
|
|
3
1
|
/** persistRaw / syncProviderField / maskKey: config read/write helpers.
|
|
4
2
|
* Extracted from index.mjs for shared use by slash-commands, wizard, and pickers.
|
|
5
|
-
* createConfigHelpers(agent) returns { persistRaw, syncProviderField, maskKey }
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
* createConfigHelpers(agent) returns { persistRaw, syncProviderField, maskKey }
|
|
4
|
+
* R10 F5(D-F5b):persistRaw 全链(磁盘新鲜读→mutate→写)收口进 config.mjs
|
|
5
|
+
* writeConfigAtomic——mtime 门控 + .bak + 冲突放弃(见 MULTI-INSTANCE-COLLAB.md §2a.2)。
|
|
6
|
+
* createConfigHelpers(agent, opts):opts.configPath 为测试注入缝(默认 configPath——
|
|
7
|
+
* 生产 index.mjs 单参调用零变化)。 */
|
|
8
|
+
export function createConfigHelpers(agent, opts = {}) {
|
|
9
|
+
const cfgPath = opts.configPath ?? null // null → 默认 configPath(动态 import 解)
|
|
10
|
+
|
|
11
|
+
/** 磁盘新鲜读 → mutate(单操作)→ mtime 门控写;冲突时 throw(提示语见下——TUI
|
|
12
|
+
* slash 分发层统一 [error] 行展示;调用方若自带 try/catch 展示 Save failed 同文案)。 */
|
|
8
13
|
async function persistRaw(mutate) {
|
|
9
|
-
const {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
saveConfig(raw)
|
|
14
|
+
const { writeConfigAtomic, configPath } = await import("../config.mjs")
|
|
15
|
+
const r = writeConfigAtomic(cfgPath ?? configPath, mutate)
|
|
16
|
+
if (!r.ok) throw new Error("config changed on disk concurrently — retry")
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
/** Sync a field of
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/** Sync a field of a provider (by name) to the providers array and persist to disk.
|
|
20
|
+
* D-F5a(#1):磁盘 fresh raw.providers 上单字段补丁——不取 agent.providers 整数组
|
|
21
|
+
* (长跑进程的内存 providers 快照不得整体写回——会抹掉对端对磁盘其他改动的保留)。
|
|
22
|
+
* 内存 agent.providers 目标项同步镜像(旧行为:改内存 + 整节写盘——镜像保留内存
|
|
23
|
+
* 一致性,磁盘写只剩单字段)。value === undefined → 删除该字段。 */
|
|
24
|
+
async function syncProviderField(name, field, value) {
|
|
25
|
+
const mem = agent.providers?.find((p) => p?.name === name)
|
|
26
|
+
if (!mem) return // 内存无此 provider(旧语义 early return——不产生空写)
|
|
21
27
|
await persistRaw((raw) => {
|
|
22
|
-
raw.providers
|
|
28
|
+
raw.providers ??= []
|
|
29
|
+
const target = raw.providers.find((p) => p?.name === name)
|
|
30
|
+
if (!target) return // 磁盘目标已被对端删除 → 只做内存镜像,不落盘
|
|
31
|
+
if (value === undefined) delete target[field]
|
|
32
|
+
else target[field] = value
|
|
23
33
|
})
|
|
34
|
+
if (value === undefined) delete mem[field]
|
|
35
|
+
else mem[field] = value
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
/** Mask API key for display */
|
package/src/tui/distill-cmd.mjs
CHANGED
|
@@ -22,7 +22,7 @@ export async function runDistill(ctx) {
|
|
|
22
22
|
}
|
|
23
23
|
let saved = 0
|
|
24
24
|
for (const c of candidates) {
|
|
25
|
-
pushLine(`── Candidate [${c.type}] ${c.title} (
|
|
25
|
+
pushLine(`── Candidate [${c.type}] ${c.title} (layer: ${c.layer ?? "personal"})`, C.warn)
|
|
26
26
|
for (const line of c.content.split("\n").slice(0, 6)) pushLine(` ${line}`, C.dim)
|
|
27
27
|
if (c.type === "rule") pushLine(" (rule type — consider writing manually; press y to extract)", C.warn)
|
|
28
28
|
const accept = await askPermission("distill-save", { title: c.title })
|
package/src/tui/index.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import { createRenderLoop } from "./render-loop.mjs"
|
|
|
27
27
|
import { makeDimsState } from "./dims.mjs"
|
|
28
28
|
import { SLASH_COMMANDS, SLASH_ALIASES, createSlashCommands } from "./slash-commands.mjs"
|
|
29
29
|
import { createWizard } from "./wizard.mjs"
|
|
30
|
-
import { writeStartupSequence, createExitCleanup } from "./tui-lifecycle.mjs"
|
|
30
|
+
import { writeStartupSequence, setTuiActive, createExitCleanup } from "./tui-lifecycle.mjs"
|
|
31
31
|
import { createPickers } from "./pickers.mjs"
|
|
32
32
|
import { runDistill as runDistillImpl } from "./distill-cmd.mjs"
|
|
33
33
|
import { createInteraction } from "./interaction.mjs"
|
|
@@ -106,7 +106,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
106
106
|
reasoning: "", // thinking stream buffer (dimmed display)
|
|
107
107
|
completion: null, // Tab completion state { candidates, index }
|
|
108
108
|
|
|
109
|
-
subTasks: {}, // sub-agent activity blocks (§7.2 D4): { "coder#1": { key, role, model, started, done, doneAt, blocks: [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError, dropped, blockEpoch, awaitingDigest(§17 挂起中间态), _freezeAt
|
|
109
|
+
subTasks: {}, // sub-agent activity blocks (§7.2 D4): { "coder#1": { key, role, model, started, done, doneAt, blocks: [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError, dropped, blockEpoch, awaitingDigest(§17 挂起中间态), _freezeAt(冻结锚点), stopped, children: [](§27 R23 嵌套子代理子块载体——subagent-children.mjs) } } — rendered as collapsible in-conversation blocks; persists across turns (blocks are the child activity's ONLY carrier — child tool calls never enter the parent history); bounded by the N2 500-line per-child ring buffer(R23:子块计入外层同配额——树级 trim)
|
|
110
110
|
currentTool: null, // currently executing tool name (shown in status bar)
|
|
111
111
|
processingStarted: 0, // current turn start time (status bar timer)
|
|
112
112
|
status: "Ready",
|
|
@@ -147,6 +147,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
147
147
|
// modifyOtherKeys lvl 2 (\x1b[>4;2m): Shift+Enter → \x1b[27;2;13~ (mintty / Git Bash)
|
|
148
148
|
// translateShiftEnter (stdin layer) maps both to \x1b\r → meta+return → multiline branch.
|
|
149
149
|
writeStartupSequence()
|
|
150
|
+
setTuiActive(true) // R25(F-R25a):终端接管完成——置 TUI 活动态(崩溃钩子恢复判定源)
|
|
150
151
|
|
|
151
152
|
const utf8Decoder = new TextDecoder("utf-8", { fatal: false })
|
|
152
153
|
|
package/src/tui/interaction.mjs
CHANGED
|
@@ -47,9 +47,9 @@ export function createInteraction(ctx) {
|
|
|
47
47
|
const action = String(args.action ?? "")
|
|
48
48
|
if (action === "put") return [`[${args.type ?? ""}] ${args.title ?? ""}`, ...cap(args.content ?? "", 500).split("\n")]
|
|
49
49
|
if (action === "delete") return args.id
|
|
50
|
-
? [`delete single: ${args.id} (
|
|
51
|
-
: [`batch delete
|
|
52
|
-
if (action === "clear") return [`clear personal memory`, `
|
|
50
|
+
? [`delete single: ${args.id}${args.layer ? ` (layer ${args.layer})` : ""}`]
|
|
51
|
+
: [`batch delete layer=${args.layer ?? ""} type=${args.type ?? ""} keyword=${args.keyword ?? ""}`, `confirm=${args.confirm}`]
|
|
52
|
+
if (action === "clear") return [`clear personal memory`, `layer=${args.layer ?? ""} confirm=${args.confirm}`]
|
|
53
53
|
return [cap(summarize(args), 300)]
|
|
54
54
|
}
|
|
55
55
|
return [cap(summarize(args), 300)]
|
package/src/tui/mouse.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { computeLayout, subagentLineIndex } from "./layout.mjs"
|
|
|
18
18
|
import { buildConvLines, convViewport } from "./render-conversation.mjs"
|
|
19
19
|
import { toggleFoldBlock, scrollFoldBlock, foldScrollOffset } from "./fold-block.mjs"
|
|
20
20
|
import { cancelAsyncSubagent } from "../agent-tools/subagent-async.mjs"
|
|
21
|
+
import { cancelAsyncAdvisor } from "../agent-tools/advisor-async.mjs"
|
|
21
22
|
import { C } from "./ansi.mjs"
|
|
22
23
|
|
|
23
24
|
/** 2026-08-31 滚轮事件分派(用户需求"展开块能滚动阅读全文"):坐标命中展开块内容行 →
|
|
@@ -184,7 +185,12 @@ export function createMouseDispatch({ agent, state, pushLine, render, popPicker
|
|
|
184
185
|
const cancelSubagent = (key) => {
|
|
185
186
|
try {
|
|
186
187
|
const id = key.slice(key.lastIndexOf("#") + 1)
|
|
187
|
-
const
|
|
188
|
+
const isAdvisorBlock = key.startsWith("advisor#")
|
|
189
|
+
// §24 D-24b (②-6b): ⏹ on an advisor block cancels the background review
|
|
190
|
+
// (directed abort → cancelled settle: no pending entry / no token).
|
|
191
|
+
const r = isAdvisorBlock
|
|
192
|
+
? cancelAsyncAdvisor(agent, id)
|
|
193
|
+
: cancelAsyncSubagent(agent, id)
|
|
188
194
|
if (r?.status === "error") {
|
|
189
195
|
// 池内无此条目但区块仍 live(!done)→ 阻塞型(sync)spawn 中——无池条目可定向
|
|
190
196
|
// 中止(§19.5 cancel 只针对 async 后台子代理);给出可操作指引而非神秘 unknown id
|
package/src/tui/pickers.mjs
CHANGED
|
@@ -300,22 +300,24 @@ export function createPickers(ctx) {
|
|
|
300
300
|
const target = agent.providers.find((pp) => pp.name === item.provider)
|
|
301
301
|
if (!target) return
|
|
302
302
|
const providerDefault = target.model
|
|
303
|
+
const nextActiveModel = item.model !== providerDefault ? item.model : null // default model → clear activeModel
|
|
304
|
+
// D-F5a(selectModel——清单外同型写回补正)先盘后存:磁盘 fresh raw 单操作(model + active 指针)——冲突放弃不留 ghost
|
|
305
|
+
await persistRaw((raw) => {
|
|
306
|
+
raw.providers ??= []
|
|
307
|
+
const t = raw.providers.find((p) => p?.name === item.provider)
|
|
308
|
+
if (t) t.model = item.model
|
|
309
|
+
raw.activeProvider = item.provider
|
|
310
|
+
raw.activeModel = nextActiveModel || undefined // null → omit from config
|
|
311
|
+
})
|
|
303
312
|
target.model = item.model
|
|
304
313
|
agent.activeProvider = item.provider
|
|
305
|
-
|
|
306
|
-
agent.activeModel = item.model !== providerDefault ? item.model : null
|
|
314
|
+
agent.activeModel = nextActiveModel
|
|
307
315
|
agent.provider = { ...target }
|
|
308
316
|
if (agent.config?.agent?.compactThresholdAuto) {
|
|
309
317
|
const { resolveCompactThreshold } = await import("../config.mjs")
|
|
310
318
|
// provider 对象(非 model 字符串)——阈值跟随 providers[].context 覆盖(PROVIDER.md §15 T-C2)
|
|
311
319
|
agent.config.agent.compactThreshold = resolveCompactThreshold(null, target).value
|
|
312
320
|
}
|
|
313
|
-
await persistRaw((raw) => {
|
|
314
|
-
// 落盘前剥离运行时注入的 proxyUri(由 loadConfig + injectProxy 在加载时重建)
|
|
315
|
-
raw.providers = agent.providers.map(({ proxyUri: _, ...p }) => p)
|
|
316
|
-
raw.activeProvider = item.provider
|
|
317
|
-
raw.activeModel = agent.activeModel || undefined // null → omit from config
|
|
318
|
-
})
|
|
319
321
|
agent.config.activeProvider = item.provider
|
|
320
322
|
agent.config.activeModel = agent.activeModel
|
|
321
323
|
if (!agent.provider.apiKey) {
|
|
@@ -354,8 +356,8 @@ export function createPickers(ctx) {
|
|
|
354
356
|
const cfg = { name, baseURL, model }
|
|
355
357
|
if (format.name === "anthropic" || format.name === "google") cfg.format = format.name
|
|
356
358
|
else if (format.name !== "openai") return // 防御:未知格式(理论不可达——picker 枚举)
|
|
359
|
+
await persistRaw((raw) => { raw.providers ??= []; raw.providers.push(cfg) }) // D-F5a(#3)先盘后存
|
|
357
360
|
agent.providers.push(cfg)
|
|
358
|
-
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
359
361
|
const key = await askQuestion(`Enter API key for ${name} (skip if none):`)
|
|
360
362
|
if (key) await setProviderKey(name, key)
|
|
361
363
|
return
|
|
@@ -368,8 +370,8 @@ export function createPickers(ctx) {
|
|
|
368
370
|
if (preset.maxTokens) cfg.maxTokens = preset.maxTokens
|
|
369
371
|
if (preset.chatPath) cfg.chatPath = preset.chatPath
|
|
370
372
|
if (preset.format) cfg.format = preset.format
|
|
373
|
+
await persistRaw((raw) => { raw.providers ??= []; raw.providers.push(cfg) }) // D-F5a(#2)先盘后存
|
|
371
374
|
agent.providers.push(cfg)
|
|
372
|
-
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
373
375
|
const key = await askQuestion(`Enter API key for ${se.name} (skip if none):`)
|
|
374
376
|
if (key) await setProviderKey(se.name, key)
|
|
375
377
|
}
|
|
@@ -382,8 +384,13 @@ export function createPickers(ctx) {
|
|
|
382
384
|
...candidates.map((p) => ({ type: "item", text: `${p.name} (${p.model})`, name: p.name })),
|
|
383
385
|
])
|
|
384
386
|
if (!se) return
|
|
387
|
+
// D-F5a(#4)先盘后存:磁盘 fresh raw 上 splice;冲突放弃不留下内存 ghost
|
|
388
|
+
await persistRaw((raw) => {
|
|
389
|
+
raw.providers ??= []
|
|
390
|
+
const idx = raw.providers.findIndex((p) => p?.name === se.name)
|
|
391
|
+
if (idx !== -1) raw.providers.splice(idx, 1)
|
|
392
|
+
})
|
|
385
393
|
agent.providers.splice(agent.providers.findIndex((p) => p.name === se.name), 1)
|
|
386
|
-
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
387
394
|
}
|
|
388
395
|
|
|
389
396
|
async function setKeyFlow() {
|
|
@@ -399,14 +406,18 @@ export function createPickers(ctx) {
|
|
|
399
406
|
async function setProviderKey(name, key) {
|
|
400
407
|
const target = agent.providers.find((p) => p.name === name)
|
|
401
408
|
if (!target) return
|
|
409
|
+
// D-F5a(#5)先盘后存:磁盘 fresh raw 上改目标项 apiKey;冲突放弃不留下内存 ghost
|
|
410
|
+
await persistRaw((raw) => {
|
|
411
|
+
raw.providers ??= []
|
|
412
|
+
const t = raw.providers.find((p) => p?.name === name)
|
|
413
|
+
if (t) t.apiKey = key
|
|
414
|
+
})
|
|
402
415
|
target.apiKey = key
|
|
403
416
|
if (name === agent.activeProvider) agent.provider.apiKey = key
|
|
404
|
-
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
405
417
|
}
|
|
406
418
|
|
|
407
|
-
/** /model provider 管理:context 窗口字段(K 单位,PROVIDER.md §15 D-C4/D-C5)——
|
|
408
|
-
*
|
|
409
|
-
* → persistRaw 全量写盘);空输入清空(回 spec 值);非法输入报错不落盘(D-C1 语义同 loadConfig)。 */
|
|
419
|
+
/** /model provider 管理:context 窗口字段(K 单位,PROVIDER.md §15 D-C4/D-C5)——picker 选
|
|
420
|
+
* provider + 表单输入;空清空/非法不落盘(D-C1);落盘 = D-F5a(#6)fresh 单字段补丁——先盘后存。 */
|
|
410
421
|
async function setContextFlow() {
|
|
411
422
|
const se = await showPicker("Set Context Window", [
|
|
412
423
|
{ type: "header", text: "Select provider" },
|
|
@@ -423,26 +434,33 @@ export function createPickers(ctx) {
|
|
|
423
434
|
const val = (await askQuestion(
|
|
424
435
|
`Context window for ${se.name} in K units (current: ${current ? `${current}K` : "spec default"} — e.g. 128 = 128K; empty to clear):`
|
|
425
436
|
))?.trim() ?? ""
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
} else {
|
|
437
|
+
let newCtx // undefined = 清空(回 spec 值)
|
|
438
|
+
if (val !== "") {
|
|
429
439
|
const n = Number(val)
|
|
430
440
|
if (!Number.isInteger(n) || n <= 0) {
|
|
431
441
|
pushLine(`Invalid context: "${val}" — must be a positive integer in K units (e.g. 128 = 128K)`, C.error)
|
|
432
442
|
return
|
|
433
443
|
}
|
|
434
|
-
|
|
444
|
+
newCtx = n
|
|
435
445
|
}
|
|
446
|
+
await persistRaw((raw) => {
|
|
447
|
+
raw.providers ??= []
|
|
448
|
+
const t = raw.providers.find((p) => p?.name === se.name)
|
|
449
|
+
if (!t) return
|
|
450
|
+
if (newCtx === undefined) delete t.context
|
|
451
|
+
else t.context = newCtx
|
|
452
|
+
})
|
|
453
|
+
if (newCtx === undefined) delete target.context
|
|
454
|
+
else target.context = newCtx
|
|
436
455
|
if (se.name === agent.activeProvider) {
|
|
437
456
|
// 运行时 provider 同步(同 setProviderKey 先例:只补 context,不重建对象以免丢 activeModel 覆盖)
|
|
438
|
-
if (
|
|
439
|
-
else agent.provider.context =
|
|
457
|
+
if (newCtx === undefined) delete agent.provider.context
|
|
458
|
+
else agent.provider.context = newCtx
|
|
440
459
|
if (agent.config?.agent?.compactThresholdAuto) {
|
|
441
460
|
const { resolveCompactThreshold } = await import("../config.mjs")
|
|
442
461
|
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
443
462
|
}
|
|
444
463
|
}
|
|
445
|
-
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
446
464
|
pushLine(target.context !== undefined
|
|
447
465
|
? `ctx = ${target.context}K (${target.context * 1024} tokens)`
|
|
448
466
|
: `context cleared — using model spec (${fmtContextK(providerSpec(target).context)})`, C.tool)
|
|
@@ -10,7 +10,7 @@ import { formatTables, sanitizeDisplay, sliceByWidth, wrapText } from "./render.
|
|
|
10
10
|
import {
|
|
11
11
|
foldTailLines, isExpanded, renderBlockTimeline, renderExpandedBlock, renderFoldedHead, renderMathAndMarkdown,
|
|
12
12
|
} from "./fold-block.mjs"
|
|
13
|
-
import {
|
|
13
|
+
import { renderSubChildSections, subChildFoldKey } from "./subagent-panel.mjs"
|
|
14
14
|
import { SUBAGENT_ROLES } from "./subagent-blocks.mjs"
|
|
15
15
|
|
|
16
16
|
const _toolSegCache = new WeakMap()
|
|
@@ -70,7 +70,8 @@ export function toolSeg(state, l, i, cols, maxRows) {
|
|
|
70
70
|
|
|
71
71
|
/** 冻结子agent 活动块渲染(§7.2 D4):state.lines 载体 {_frozenSubTask: sub}(subagent-blocks.mjs
|
|
72
72
|
* freezeSubTaskLines 推入)。折叠 = 身份头 + tail 3;展开 = 共享组件(60% 屏幕封顶 + 底部可达
|
|
73
|
-
* 折叠控制)。折叠键 `sub-${key}`——与运行中面板区块同键,折叠态跨冻结边界无缝延续。
|
|
73
|
+
* 折叠控制)。折叠键 `sub-${key}`——与运行中面板区块同键,折叠态跨冻结边界无缝延续。
|
|
74
|
+
* §27 R23:子块段(renderSubChildSections)随冻结载体渲染(冻结含子块——T-R23d.1)。 */
|
|
74
75
|
function frozenSubTaskLines(state, sub, cols, maxRows) {
|
|
75
76
|
const foldKey = `sub-${sub.key}`
|
|
76
77
|
const elapsed = Math.floor(((sub.doneAt ?? Date.now()) - sub.started) / 1000)
|
|
@@ -92,7 +93,7 @@ function frozenSubTaskLines(state, sub, cols, maxRows) {
|
|
|
92
93
|
if (isExpanded(state, foldKey)) {
|
|
93
94
|
// Expanded: shared component renders blank + ▼ control + full timeline,
|
|
94
95
|
// capped at 60% of the screen with a bottom collapse control.
|
|
95
|
-
const body = renderBlockTimeline(sub.blocks, cols)
|
|
96
|
+
const body = renderBlockTimeline(sub.blocks, cols)
|
|
96
97
|
out.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: "subagent activity" }))
|
|
97
98
|
} else {
|
|
98
99
|
// Folded: the header line itself is the control (▶ affordance), then tail 3.
|
|
@@ -105,22 +106,38 @@ function frozenSubTaskLines(state, sub, cols, maxRows) {
|
|
|
105
106
|
out.push({ text: `│ ${sliceByWidth(line, cols - 4)}`, color: C.dim, _skipDimFold: true })
|
|
106
107
|
}
|
|
107
108
|
}
|
|
109
|
+
// §27 R23:子块段随冻结载体(折叠/展开两态都渲染——子块自身折叠键独立控制)。
|
|
110
|
+
out.push(...renderSubChildSections(state, sub, cols, maxRows))
|
|
108
111
|
return out
|
|
109
112
|
}
|
|
110
113
|
|
|
111
|
-
/** 冻结子agent 段入口(2026-08-31 段缓存:冻结后内容不变——签名含 sub.key + blocks
|
|
112
|
-
*
|
|
114
|
+
/** 冻结子agent 段入口(2026-08-31 段缓存:冻结后内容不变——签名含 sub.key + blocks 计数 +
|
|
115
|
+
* 子块树签名(R23——children 折叠态/滚动/行数参与——toggle 子块/翻窗必须失效);注:
|
|
116
|
+
* foldKey 由位置键升级为 sub.key(loadOlder unshift 后不重绑——sub 自身携带 key)。 */
|
|
113
117
|
export function frozenSubSeg(state, l, i, cols, maxRows) {
|
|
114
118
|
const fKey = `sub-${l._frozenSubTask.key}`
|
|
119
|
+
// §27 R23:子块树渲染输入签名(key/done/stopped/blocks 数/折叠态/窗口偏移——递归)
|
|
120
|
+
const sub = l._frozenSubTask
|
|
121
|
+
let childSig = ""
|
|
122
|
+
const walk = (carrier, path) => {
|
|
123
|
+
for (const c of carrier.children ?? []) {
|
|
124
|
+
const innerPath = path ? `${path}/${c.key}` : c.key
|
|
125
|
+
const fk = subChildFoldKey(sub.key, innerPath)
|
|
126
|
+
childSig += `${innerPath}:${c.done ? 1 : 0}:${c.stopped ? 1 : 0}:${c.blocks?.length ?? 0}:${state.expandedBlocks?.has(fk) ? 1 : 0}:${state._foldScroll?.get(fk) ?? 0};`
|
|
127
|
+
walk(c, innerPath)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
walk(sub, "")
|
|
115
131
|
const fSig = [
|
|
116
|
-
cols, maxRows ?? 0,
|
|
117
|
-
|
|
132
|
+
cols, maxRows ?? 0, sub.key, sub.blocks?.length ?? 0,
|
|
133
|
+
sub.done ? 1 : 0, state.foldEnabled === false ? 0 : 1,
|
|
118
134
|
state.expandedBlocks?.has(fKey) ? 1 : 0, state._foldScroll?.get(fKey) ?? 0,
|
|
135
|
+
childSig,
|
|
119
136
|
].join("|")
|
|
120
137
|
const hit = _frozenSubCache.get(l)
|
|
121
|
-
if (hit && hit.textRef ===
|
|
122
|
-
const rows = frozenSubTaskLines(state,
|
|
123
|
-
_frozenSubCache.set(l, { textRef:
|
|
138
|
+
if (hit && hit.textRef === sub && hit.sig === fSig) return hit.rows
|
|
139
|
+
const rows = frozenSubTaskLines(state, sub, cols, maxRows)
|
|
140
|
+
_frozenSubCache.set(l, { textRef: sub, sig: fSig, rows })
|
|
124
141
|
return rows
|
|
125
142
|
}
|
|
126
143
|
|
package/src/tui/startup.mjs
CHANGED
|
@@ -187,6 +187,10 @@ export function showStartup(ctx) {
|
|
|
187
187
|
}
|
|
188
188
|
pushLine(`Tools: ${agent.tools.map((t) => t.name).join(", ")}`, C.dim)
|
|
189
189
|
|
|
190
|
+
// R25(F-R25c):上次运行异常终止提示——bin 入口扫描 crash-reports(24h 窗)经
|
|
191
|
+
// startTUI opts.crashNotice 传入——无匹配为 undefined → 不提示(负例)
|
|
192
|
+
if (opts.crashNotice) pushLine(opts.crashNotice, C.warn)
|
|
193
|
+
|
|
190
194
|
// Recover previous session: rebuild from history (lazy — display snapshot is
|
|
191
195
|
// deprecated; it drifted out of sync with history on VS Code writes).
|
|
192
196
|
if (opts.restored?.history?.length) {
|