thincoder 0.12.59 → 0.12.61
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 +62 -4
- package/README.md +10 -8
- package/bin/thincoder.mjs +99 -133
- package/package.json +6 -4
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +45 -21
- package/src/acp.mjs +6 -1
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +59 -137
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +156 -359
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +27 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +145 -27
- package/src/agent/helpers.mjs +107 -13
- package/src/agent/record-results.mjs +55 -11
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +242 -0
- package/src/agent/setup-reminders.mjs +69 -0
- package/src/agent/setup.mjs +107 -127
- package/src/agent/spawn-child.mjs +55 -13
- package/src/agent-tools/advisor-async.mjs +346 -0
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +167 -116
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +139 -107
- package/src/agent-tools/design-token.mjs +117 -0
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +289 -0
- package/src/agent-tools/read-history.mjs +166 -32
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +137 -34
- package/src/agent-tools/subagent-actions.mjs +180 -133
- package/src/agent-tools/subagent-async.mjs +184 -177
- package/src/agent-tools/subagent-panel.mjs +160 -0
- package/src/agent-tools/subagent-run.mjs +205 -0
- package/src/agent-tools/subagent-scheduler.mjs +100 -27
- package/src/agent-tools/subagent-spawn.mjs +453 -0
- package/src/agent-tools/subagent.mjs +256 -578
- package/src/agent-tools/verify.mjs +119 -292
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +89 -205
- package/src/cli/distill-command.mjs +12 -6
- package/src/cli/make-agent.mjs +26 -8
- package/src/cli/memory-command.mjs +4 -3
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +42 -17
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +180 -63
- package/src/context.mjs +5 -147
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +128 -0
- package/src/distill.mjs +11 -11
- package/src/expand-home.mjs +16 -0
- package/src/explore-distill.mjs +155 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +72 -32
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +236 -0
- package/src/memory/docs.mjs +68 -54
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/memory.mjs +3 -1
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +82 -0
- package/src/prompts/advisor-design.md +18 -75
- package/src/prompts/advisor-round1.md +14 -67
- package/src/prompts/advisor-round2.md +15 -51
- package/src/prompts/advisor-round3.md +15 -51
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +5 -23
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +55 -0
- package/src/prompts/persona-engineering.md +54 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +18 -98
- package/src/provider/errors.mjs +101 -0
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/retry.mjs +8 -45
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- 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 +188 -60
- package/src/session.mjs +104 -124
- package/src/token-ttl.mjs +274 -0
- package/src/tools/{system.mjs → bash.mjs} +19 -221
- 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 +41 -16
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +23 -34
- 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 +9 -8
- package/src/tools/ops.mjs +188 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +4 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -2
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tools/shared.mjs +14 -13
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +36 -228
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-config.mjs +142 -30
- package/src/tui/cmd-eng.mjs +28 -40
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-undo.mjs +4 -3
- 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 +40 -38
- package/src/tui/interaction.mjs +3 -3
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +52 -9
- package/src/tui/pickers.mjs +28 -392
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +12 -9
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +106 -295
- package/src/tui/subagent-children.mjs +162 -0
- package/src/tui/subagent-freeze.mjs +169 -0
- package/src/tui/subagent-panel.mjs +24 -31
- package/src/tui/suspension-drive.mjs +297 -0
- package/src/tui/tool-args.mjs +7 -5
- package/src/tui/tool-display.mjs +143 -0
- package/src/tui/tool-events.mjs +56 -185
- package/src/tui/tui-lifecycle.mjs +46 -4
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +61 -21
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -56
- package/src/prompts/discipline.md +0 -102
- package/src/prompts/eng-coder.md +0 -44
- package/src/prompts/engineering-sub.md +0 -34
- package/src/prompts/engineering.md +0 -340
- package/src/prompts/explore.md +0 -21
- package/src/prompts/main.md +0 -56
- package/src/prompts/methodology-template.md +0 -58
- package/src/prompts/plan.md +0 -16
- package/src/prompts/system.md +0 -63
- 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/render.mjs
CHANGED
|
@@ -161,14 +161,19 @@ export function layoutInput(chars, cursor, width) {
|
|
|
161
161
|
const PROMPT = "\u25b8 " // first-line prefix (display width 2)
|
|
162
162
|
const CONT = " " // continuation prefix (width 2) — keeps left edge aligned
|
|
163
163
|
const lines = []
|
|
164
|
+
const starts = [] // lineStarts so far(第 31 批 additive):每个可视行的行首下标
|
|
165
|
+
let lineStart = 0 // 当前行的行首下标(chars 索引空间)
|
|
164
166
|
let cursorLine = 0
|
|
165
167
|
let cursorCol = 0
|
|
166
168
|
let cur = ""
|
|
167
169
|
let col = 0
|
|
168
170
|
let firstLine = true
|
|
169
171
|
const avail = () => width - 2 // every line reserves 2 cols for its prefix
|
|
170
|
-
|
|
172
|
+
// next = 下一行的行首下标(折行 = 当前字符 i;行尾 \n 之后 = i + 1)
|
|
173
|
+
const flush = (next) => {
|
|
171
174
|
lines.push((firstLine ? PROMPT : CONT) + cur)
|
|
175
|
+
starts.push(lineStart)
|
|
176
|
+
lineStart = next
|
|
172
177
|
firstLine = false
|
|
173
178
|
cur = ""
|
|
174
179
|
col = 0
|
|
@@ -177,7 +182,7 @@ export function layoutInput(chars, cursor, width) {
|
|
|
177
182
|
const ch = chars[i]
|
|
178
183
|
if (ch !== undefined && ch !== "\n") {
|
|
179
184
|
const w = charWidth(ch.codePointAt(0))
|
|
180
|
-
if (col + w > avail()) flush()
|
|
185
|
+
if (col + w > avail()) flush(i)
|
|
181
186
|
if (i === cursor) {
|
|
182
187
|
cursorLine = lines.length
|
|
183
188
|
cursorCol = 2 + col
|
|
@@ -189,12 +194,39 @@ export function layoutInput(chars, cursor, width) {
|
|
|
189
194
|
cursorLine = lines.length
|
|
190
195
|
cursorCol = 2 + col
|
|
191
196
|
}
|
|
192
|
-
if (ch === "\n") flush()
|
|
197
|
+
if (ch === "\n") flush(i + 1)
|
|
193
198
|
}
|
|
194
199
|
}
|
|
195
200
|
const endsWithNewline = chars.length > 0 && chars[chars.length - 1] === "\n"
|
|
196
|
-
if (cur || lines.length === 0 || endsWithNewline) flush()
|
|
197
|
-
|
|
201
|
+
if (cur || lines.length === 0 || endsWithNewline) flush(chars.length)
|
|
202
|
+
// lineStarts[k] = 第 k 行行首;行 k 区间 = [lineStarts[k], lineStarts[k+1])(含行尾 \n);
|
|
203
|
+
// 末项哨兵 = chars.length(上界、不指向行)——竖移定位用(TUI-INPUT-BOX.md §1 不变量 5)。
|
|
204
|
+
return { lines, cursorLine, cursorCol, lineStarts: [...starts, chars.length] }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 竖直移动定位(第 31 批——TUI-INPUT-BOX.md §3 规则 2 / §9.3 #2):纯函数,
|
|
208
|
+
* `(chars, cursor, width, dir) → number | null`(null = 该方向无邻行)。
|
|
209
|
+
* 目标行显示列 = 当前光标显示列(列保持——`layoutInput` 同源行列;行前缀 2 列等宽故整体
|
|
210
|
+
* 相减);目标行更短则钳制到行尾(行尾含 \n → 落在 \n 前)。逐键现算——不记忆列(D-31.2)。
|
|
211
|
+
* 不读不写任何 state。 */
|
|
212
|
+
export function moveCursorVertical(chars, cursor, width, dir) {
|
|
213
|
+
const { cursorLine, cursorCol, lineStarts } = layoutInput(chars, cursor, width)
|
|
214
|
+
const lineCount = lineStarts.length - 1
|
|
215
|
+
const targetLine = dir === "up" ? cursorLine - 1 : cursorLine + 1
|
|
216
|
+
if (targetLine < 0 || targetLine >= lineCount) return null
|
|
217
|
+
const targetCol = Math.max(0, cursorCol - 2) // 2 列行前缀(`▸ ` / 续行空格)——各行等宽
|
|
218
|
+
const start = lineStarts[targetLine]
|
|
219
|
+
let end = lineStarts[targetLine + 1]
|
|
220
|
+
if (end > start && chars[end - 1] === "\n") end -= 1 // 行尾 \n 不占显示列
|
|
221
|
+
let col = 0
|
|
222
|
+
let idx = start
|
|
223
|
+
while (idx < end) {
|
|
224
|
+
const w = charWidth(chars[idx].codePointAt(0))
|
|
225
|
+
if (col + w > targetCol) break // 宽字符列中不落点——落在该字符起点
|
|
226
|
+
col += w
|
|
227
|
+
idx += 1
|
|
228
|
+
}
|
|
229
|
+
return idx
|
|
198
230
|
}
|
|
199
231
|
|
|
200
232
|
/**
|
|
@@ -42,7 +42,7 @@ export const SLASH_COMMANDS = [
|
|
|
42
42
|
{ name: "/eng", group: "Agent", desc: "toggle engineering mode — strict methodology enforcement" },
|
|
43
43
|
{ name: "/advisor", group: "Agent", desc: "advisor settings (model, thinking, review gate)" },
|
|
44
44
|
{ name: "/model", group: "Agent", desc: "select model & manage providers" },
|
|
45
|
-
{ name: "/submodel", group: "Agent", desc: "subagent model per type (explore/plan/coder/eng-coder)" },
|
|
45
|
+
{ name: "/submodel", group: "Agent", desc: "subagent model per type (explore/plan/coder/eng-coder/eng-designer)" },
|
|
46
46
|
{ name: "/shell", group: "System", desc: "bash tool shell (git-bash/pwsh path; win11 cmd encoding fix)" },
|
|
47
47
|
{ name: "/goal", group: "Agent", desc: "set/view/cancel long-term goal" },
|
|
48
48
|
{ name: "/think", group: "Agent", desc: "thinking mode & reasoning effort" },
|
|
@@ -144,7 +144,7 @@ export function createSlashCommands(ctx) {
|
|
|
144
144
|
const match = (cands) => cands.filter((c) => c.startsWith(last)).map((c) => `${head} ${c}`)
|
|
145
145
|
if (cmd === "/model" && argIndex === 0) return match(agent.providers.map((p) => p.name))
|
|
146
146
|
if (cmd === "/submodel") {
|
|
147
|
-
if (argIndex === 0) return match(["explore", "plan", "coder", "eng-coder", "reset"])
|
|
147
|
+
if (argIndex === 0) return match(["explore", "plan", "coder", "eng-coder", "eng-designer", "reset"])
|
|
148
148
|
if (parts[1] && !["reset"].includes(parts[1]) && argIndex === 1) return match(agent.providers.map((p) => p.name))
|
|
149
149
|
}
|
|
150
150
|
if (cmd === "/think") {
|
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) {
|
|
@@ -3,16 +3,35 @@
|
|
|
3
3
|
* state.subTasks[key] = { key, role, model, async(§19.5 D-M7b——⟦ev⟧async 标记——
|
|
4
4
|
* undefined = sync), started, done, doneAt, blocks:
|
|
5
5
|
* [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError,
|
|
6
|
-
* dropped, blockEpoch, awaitingDigest(§17), _freezeAt(冻结锚点), stopped(§19.5
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* dropped, blockEpoch, awaitingDigest(§17), _freezeAt(冻结锚点), stopped(§19.5),
|
|
7
|
+
* children: [](SUBAGENT-TAIL:嵌套子代理**守护载体**——内容并入本块 blocks——见
|
|
8
|
+
* subagent-children.mjs)}。
|
|
9
|
+
* 职责:前缀路由(parseRelayPath——**SUBAGENT-TAIL(D-ST1):嵌套段内容行并入外层
|
|
10
|
+
* blocks——append 目标上移,子块载体只余守护元数据**)、
|
|
11
|
+
* 事件 token 解析(**内层 ⟦ev⟧done/stopped 生成侧补发射(D-R23c1)路由到子块
|
|
12
|
+
* 定格;F2:done 后完全定格——迟到 chunk 丢弃**)、kind 合并、N2 单环 500 显示行上限
|
|
13
|
+
* (SUBAGENT-TAIL D-ST4——内层行同环计数、单载体最旧先行)、N1 渲染节流、
|
|
14
|
+
* 完成冻结(freezeSubTaskLines 家族——锚点 splice)。显示契约 = docs/design/TUI.md §6。
|
|
9
15
|
*/
|
|
10
16
|
|
|
11
17
|
import { C } from "./ansi.mjs"
|
|
18
|
+
// 第 27 批 §12.3①/②:relay 前缀文法单一权威 = src/agent/relay-prefix.mjs——本文件
|
|
19
|
+
// 不再自持前缀正则/解析副本(防第二套平行正则再漂移)。
|
|
20
|
+
import { parseRelayPath } from "../agent/relay-prefix.mjs"
|
|
12
21
|
import { describeToolArgs } from "./tool-args.mjs"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
22
|
+
import {
|
|
23
|
+
appendSubBlock, descendSubChild,
|
|
24
|
+
closeSubChild,
|
|
25
|
+
} from "./subagent-children.mjs"
|
|
26
|
+
export { SUB_BLOCK_LINE_LIMIT, appendSubBlock } from "./subagent-children.mjs"
|
|
27
|
+
// 2026-09-05 module-split:finish/freeze 族 + §19.6 面板现算迁 subagent-freeze.mjs——
|
|
28
|
+
// routeSub*/compression panel 内部用本地 import;re-export 保外部 import 面(index.mjs 等)
|
|
29
|
+
// CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):面板手工镜像退役——re-export 面换现算导出
|
|
30
|
+
// computePanelBlocks(读时现算),本文件全部刷镜调用点删除(单账本——变更点不再手动同步)。
|
|
31
|
+
import { freezeSubTaskLines, finishSubTaskKey, finishSubTask, freezeDoneSubTasks, finishSubTasksByRole, freezeAllSubTasks, freezeReclaimDigestedBlocks, shiftFreezeAnchors } from "./subagent-freeze.mjs"
|
|
32
|
+
export { computePanelBlocks, finishSubTask, finishSubTaskKey, freezeSubTaskLines, shiftFreezeAnchors, freezeDoneSubTasks, finishSubTasksByRole, freezeAllSubTasks, freezeReclaimDigestedBlocks } from "./subagent-freeze.mjs"
|
|
33
|
+
|
|
34
|
+
// 前缀文法/解析已迁 src/agent/relay-prefix.mjs(第 27 批 §12.3②)——本文件不自持副本。
|
|
16
35
|
/** ⟦ev⟧ token parser:`⟦ev⟧<name>\x1e<n>\x1e<max>\x1e<phase>\x1e<detail>`。phase done
|
|
17
36
|
* = async 完成即冻结(settle 时发);settled = 挂起期完成——冻结延迟至 digest 消化
|
|
18
37
|
* 完成(§17.5.5 freezeReclaimDigestedBlocks 逐条回收——不等池空)或池空退出兜底补发;
|
|
@@ -26,64 +45,14 @@ export const SUB_PREFIX_RE = /^([\w-]+)#(\d+)\//
|
|
|
26
45
|
* (块尚未创建)缓冲 `state._pendingAsyncKeys`——ensureSubTaskKey 块创建时应用(兜底)。 */
|
|
27
46
|
export const SUB_EVENT_RE = /^⟦ev⟧(turn|approval|done|settled|stopped|queued)\x1e([^\x1e]*)\x1e([^\x1e]*)\x1e([^\x1e]*)\x1e?([\s\S]*)$/
|
|
28
47
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* 单层 = inner[]/label ""——与既有单段匹配语义零改;无前缀 → null。 */
|
|
32
|
-
export function parseRelayPath(text) {
|
|
33
|
-
const segments = []
|
|
34
|
-
let rest = String(text)
|
|
35
|
-
for (;;) {
|
|
36
|
-
const m = rest.match(SUB_PREFIX_RE)
|
|
37
|
-
if (!m) break
|
|
38
|
-
segments.push(`${m[1]}#${m[2]}`)
|
|
39
|
-
rest = rest.slice(m[0].length)
|
|
40
|
-
}
|
|
41
|
-
if (segments.length === 0) return null
|
|
42
|
-
return {
|
|
43
|
-
head: segments[0],
|
|
44
|
-
inner: segments.slice(1),
|
|
45
|
-
label: segments.slice(1).join("/"),
|
|
46
|
-
rest,
|
|
47
|
-
}
|
|
48
|
-
}
|
|
48
|
+
// parseRelayPath 自 src/agent/relay-prefix.mjs import(第 27 批 §12.3①——文法单一权威,
|
|
49
|
+
// 语义与迁移前逐字一致;“嵌套解析任意深度/无前缀 → null”见该模块)。
|
|
49
50
|
|
|
50
|
-
/** §19.5 D-M8 子标行首变换(文本/think):parseRelayPath 已消费完整前缀段——此处只
|
|
51
|
-
* 判行首(块首或上一内容以 \n 收尾——chunk 边界碎片在前缀段消费后自然无声剥除,
|
|
52
|
-
* 防 explore#1/ 字样泄漏);行首 → 前置字面子标 `explore#1 · `(渲染端套 dim)。 */
|
|
53
|
-
export function sublabelLine(sub, text, label) {
|
|
54
|
-
if (!label) return String(text)
|
|
55
|
-
const last = sub.blocks.at(-1)
|
|
56
|
-
const atLineHead = !last || String(last.text).endsWith("\n")
|
|
57
|
-
return atLineHead ? `${label} · ${text}` : String(text)
|
|
58
|
-
}
|
|
59
|
-
/** N2: per-child display-line ring buffer cap — oldest lines drop with a marker. */
|
|
60
|
-
export const SUB_BLOCK_LINE_LIMIT = 500
|
|
61
51
|
/** N1: render-layer throttle for child tool-output appends (generation relays verbatim). */
|
|
62
52
|
export const SUB_RELAY_THROTTLE_MS = 250
|
|
63
|
-
/** Roles a subagent tool child can take
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// ─── §19.6 D-P1 面板镜像(subagent panel 检查工具)───
|
|
67
|
-
// TUI 装配处(index.mjs startTUI)state._agent = agent——块级状态变更点经
|
|
68
|
-
// syncPanelSnapshot 刷新 agent._panelSnapshot(区块数组快照:{key, role,
|
|
69
|
-
// status: running|done|awaitingDigest, startedAt}——状态变更即刷——O(n) 小——
|
|
70
|
-
// 不逐 token——NF-P)。agent 层 subagent action:"panel" 读此镜像(视图与用户
|
|
71
|
-
// 所见一致)+ 冻结门控(D-P3)。未挂载(headless/mock——无 state._agent)=
|
|
72
|
-
// 无镜像——no-op(降级路径由 agent 侧判定)。
|
|
73
|
-
|
|
74
|
-
/** 刷新面板镜像(调用点 = 本模块全部 state.subTasks 块级变更点末尾——map 已稳定)。 */
|
|
75
|
-
function syncPanelSnapshot(state) {
|
|
76
|
-
const agent = state._agent
|
|
77
|
-
if (!agent) return // 未挂载(headless/mock 无 TUI 装配)——无镜像
|
|
78
|
-
const snap = []
|
|
79
|
-
for (const sub of Object.values(state.subTasks ?? {})) {
|
|
80
|
-
// §20 D-SD3b:waiting/等位块(sub.queued——未启动)以 queued 态入镜(镜像与面板
|
|
81
|
-
// 所见一致——action:'panel' 视图 + freeze 门控读此态)。
|
|
82
|
-
const status = sub.done ? (sub.awaitingDigest ? "awaitingDigest" : "done") : (sub.queued ? "queued" : "running")
|
|
83
|
-
snap.push({ key: sub.key, role: sub.role ?? null, status, startedAt: sub.started ?? Date.now() })
|
|
84
|
-
}
|
|
85
|
-
agent._panelSnapshot = snap
|
|
86
|
-
}
|
|
53
|
+
/** Roles a subagent tool child can take(⏹ 门控/渲染角色判据;F-2 后角色匹配完成面 =
|
|
54
|
+
* finishSubTasksByRole——finishSubTask 已收窄 no-op)。 */
|
|
55
|
+
export const SUBAGENT_ROLES = ["sub", "explore", "plan", "coder", "eng-coder", "eng-designer"]
|
|
87
56
|
|
|
88
57
|
let _subRenderLast = 0
|
|
89
58
|
let _subRenderTimer = null
|
|
@@ -119,12 +88,12 @@ export function ensureSubTaskKey(state, key, role) {
|
|
|
119
88
|
blocks: [], currentTool: null, toolArgs: null, turn: 0, maxTurns: 0, approval: null,
|
|
120
89
|
lastError: null, dropped: 0,
|
|
121
90
|
stopped: false, // §19.5: ⟦ev⟧stopped 冻结标记(标题 "stopped")
|
|
91
|
+
children: [], // SUBAGENT-TAIL: 嵌套子代理守护载体(内容并入本块——subagent-children.mjs)
|
|
122
92
|
}
|
|
123
93
|
// §19.5 D-M7b ①(处置 #4 兜底——2026-09-03):缺失 key 的 ⟦ev⟧async 事件已缓冲
|
|
124
94
|
// pending 标志(routeSubToken——async 先于块创建到达:排队条目补位启动的时序窗口)
|
|
125
95
|
// ——块创建(此处)即应用——sub.async 区块创建即知(⏹ 门控/头标判定源保真)。
|
|
126
96
|
if (state._pendingAsyncKeys?.delete(key)) state.subTasks[key].async = true
|
|
127
|
-
syncPanelSnapshot(state) // §19.6 D-P1: 块创建 → running 入镜
|
|
128
97
|
}
|
|
129
98
|
return state.subTasks[key]
|
|
130
99
|
}
|
|
@@ -133,213 +102,10 @@ export function ensureSubTask(state, subMatch) {
|
|
|
133
102
|
return ensureSubTaskKey(state, `${subMatch[1]}#${subMatch[2]}`, subMatch[1])
|
|
134
103
|
}
|
|
135
104
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
export
|
|
140
|
-
if (!text) return
|
|
141
|
-
const last = sub.blocks.at(-1)
|
|
142
|
-
if (!fresh && last && last.kind === kind) {
|
|
143
|
-
// 合并只算净增行数(逐 chunk 全量 split 会把单行碎片虚计成 2 段——P1, 2026-08-30)
|
|
144
|
-
const before = countBlockLines(last.text)
|
|
145
|
-
last.text += text
|
|
146
|
-
sub._lineCount = (sub._lineCount ?? 0) + countBlockLines(last.text) - before
|
|
147
|
-
} else {
|
|
148
|
-
sub.blocks.push({ kind, text })
|
|
149
|
-
sub._lineCount = (sub._lineCount ?? 0) + countBlockLines(text)
|
|
150
|
-
}
|
|
151
|
-
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
152
|
-
trimSubBlocks(sub)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** N2:超限丢最旧行 + 留一条累计省略标记行(done 块同受约束)。 */
|
|
156
|
-
export function trimSubBlocks(sub) {
|
|
157
|
-
// 增量行记账(P1):appendSubBlock 维护 _lineCount——旧实现每 chunk 全量 reduce
|
|
158
|
-
sub._lineCount = (sub._lineCount ?? 0)
|
|
159
|
-
let over = sub._lineCount - SUB_BLOCK_LINE_LIMIT
|
|
160
|
-
if (over <= 0) return
|
|
161
|
-
let droppedNow = 0
|
|
162
|
-
while (over > 0 && sub.blocks.length > 0) {
|
|
163
|
-
const first = sub.blocks[0]
|
|
164
|
-
const lines = countBlockLines(first.text)
|
|
165
|
-
const take = Math.min(lines, over)
|
|
166
|
-
if (take >= lines) {
|
|
167
|
-
sub.blocks.shift()
|
|
168
|
-
droppedNow += lines
|
|
169
|
-
} else {
|
|
170
|
-
first.text = first.text.split("\n").slice(take).join("\n")
|
|
171
|
-
droppedNow += take
|
|
172
|
-
}
|
|
173
|
-
over -= take
|
|
174
|
-
}
|
|
175
|
-
sub._lineCount -= droppedNow
|
|
176
|
-
sub.dropped += droppedNow
|
|
177
|
-
const marker = `…(已省略 ${sub.dropped} 行)`
|
|
178
|
-
const first = sub.blocks[0]
|
|
179
|
-
if (first && first.kind === "meta") first.text = marker
|
|
180
|
-
else { sub.blocks.unshift({ kind: "meta", text: marker }); sub._lineCount += 1 }
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Mark the earliest running child of the given role(s) as done (✓ header, frozen elapsed). */
|
|
184
|
-
export function finishSubTask(state, roles, lastError = null) {
|
|
185
|
-
state.subTasks ??= {}
|
|
186
|
-
const roleSet = new Set(Array.isArray(roles) ? roles : [roles])
|
|
187
|
-
const running = Object.values(state.subTasks)
|
|
188
|
-
.filter((s) => !s.done && roleSet.has(s.role))
|
|
189
|
-
.sort((a, b) => a.started - b.started)
|
|
190
|
-
if (running.length === 0) return
|
|
191
|
-
const sub = running[0]
|
|
192
|
-
sub.done = true
|
|
193
|
-
sub.doneAt = Date.now()
|
|
194
|
-
sub.currentTool = null
|
|
195
|
-
sub.approval = null
|
|
196
|
-
if (lastError) sub.lastError = lastError
|
|
197
|
-
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
198
|
-
syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** §7.2.3 sync spawn 完成精确冻结(2026-09-03):按 relay key 精确 settle。finishSubTask
|
|
202
|
-
* 的"最早 started"启发式只在面板单 running 块时成立——§15 async 化后 async eng-coder
|
|
203
|
-
* (先启动)与 sync explore 并存时 explore 完成会误冻先启动的 eng-coder 块(7.2.3.1)。
|
|
204
|
-
* dispatch 沿 ctx._subagentKey(relayPrefix 去尾 = `role#N`)把 key 传进 onToolResult——
|
|
205
|
-
* 有 key 即精确标 done(冻结载体进流 + 删条目由调用方 freezeDoneSubTasks 承接——与
|
|
206
|
-
* finishSubTask 同一契约);无匹配块(已冻结 tombstone/不存在)返回 null——调用方不得
|
|
207
|
-
* 落启发式兜底(精确 key 无匹配 = 无从归属——不误冻他块)。 */
|
|
208
|
-
export function finishSubTaskKey(state, key, lastError = null) {
|
|
209
|
-
const sub = state.subTasks?.[key]
|
|
210
|
-
if (!sub) return null
|
|
211
|
-
sub.done = true
|
|
212
|
-
sub.doneAt = Date.now()
|
|
213
|
-
sub.currentTool = null
|
|
214
|
-
sub.approval = null
|
|
215
|
-
if (lastError) sub.lastError = lastError
|
|
216
|
-
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
217
|
-
syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
|
|
218
|
-
return sub
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** 冻结完成/中断区块进 state.lines(§7.2 D4——_frozenSubTask 载体行,渲染端
|
|
222
|
-
* render-conversation 识别;折叠交互 key = sub-${key} 与运行面板同源跨冻结延续)。
|
|
223
|
-
* 锚点插入(2026-09-03 修复轮):settled 块带 _freezeAt(settle 时刻流位置)——
|
|
224
|
-
* splice 落位使挂起期补发冻结块位于其 digest 总览文本之前;无锚点尾推不变;
|
|
225
|
-
* 多锚点批量冻结按降序(绝对位置 splice——先插小锚点会移走大锚点目标)。 */
|
|
226
|
-
export function freezeSubTaskLines(state, sub) {
|
|
227
|
-
if (!sub) return
|
|
228
|
-
state._frozenSubKeys ??= new Set()
|
|
229
|
-
state._frozenSubKeys.add(sub.key)
|
|
230
|
-
sub.done = true
|
|
231
|
-
sub.doneAt = sub.doneAt ?? Date.now()
|
|
232
|
-
const anchor = sub._freezeAt ?? state.lines.length
|
|
233
|
-
state.lines.splice(Math.min(anchor, state.lines.length), 0, {
|
|
234
|
-
text: `subagent activity: ${sub.key}`, color: C.dim, _frozenSubTask: sub,
|
|
235
|
-
})
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/** 头裁锚点校正(index.mjs pushLine 调用):裁 removedCount 补 1 标记行 = 净位移
|
|
239
|
-
* removedCount−1(code review round1 #3);在途锚点前移,min 0 兜底。 */
|
|
240
|
-
export function shiftFreezeAnchors(state, removedCount) {
|
|
241
|
-
const shift = removedCount - 1
|
|
242
|
-
for (const sub of Object.values(state.subTasks ?? {})) {
|
|
243
|
-
if (sub._freezeAt !== undefined) sub._freezeAt = Math.max(0, sub._freezeAt - shift)
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** 冻结 + 释放全部已 done 块(工具结果清扫路径)。锚点降序(后 settle 先插——
|
|
248
|
-
* 绝对位置 splice 语义);无锚点(-1)排末尾推;sort 稳定。 */
|
|
249
|
-
export function freezeDoneSubTasks(state) {
|
|
250
|
-
const subs = Object.values(state.subTasks ?? {})
|
|
251
|
-
.filter((s) => s.done)
|
|
252
|
-
.sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1))
|
|
253
|
-
for (const sub of subs) {
|
|
254
|
-
freezeSubTaskLines(state, sub)
|
|
255
|
-
delete state.subTasks[sub.key]
|
|
256
|
-
}
|
|
257
|
-
syncPanelSnapshot(state) // §19.6 D-P1: 删除后刷镜(块移出面板)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** 按角色整组标记 done——consult N 并行 children 的会话级 settle(单发
|
|
261
|
-
* finishSubTask 只结最早一个——consult 残项 2026-08-30)。 */
|
|
262
|
-
export function finishSubTasksByRole(state, roles, lastError = null) {
|
|
263
|
-
state.subTasks ??= {}
|
|
264
|
-
const roleSet = new Set(Array.isArray(roles) ? roles : [roles])
|
|
265
|
-
for (const sub of Object.values(state.subTasks)) {
|
|
266
|
-
if (!sub.done && roleSet.has(sub.role)) {
|
|
267
|
-
sub.done = true
|
|
268
|
-
sub.doneAt = Date.now()
|
|
269
|
-
sub.currentTool = null
|
|
270
|
-
sub.approval = null
|
|
271
|
-
if (lastError) sub.lastError = lastError
|
|
272
|
-
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/** 按 [model] 记录的 model 精确 settle(consult_check 返回 provider:model——
|
|
279
|
-
* 乱序 settle 时最早启发式冻错块)。比对尾段(bare ↔ provider:model)。 */
|
|
280
|
-
export function finishSubTaskByModel(state, role, model, lastError = null) {
|
|
281
|
-
state.subTasks ??= {}
|
|
282
|
-
const want = String(model ?? "").includes(":") ? String(model).split(":").pop() : String(model ?? "")
|
|
283
|
-
for (const sub of Object.values(state.subTasks)) {
|
|
284
|
-
const have = String(sub.model ?? "")
|
|
285
|
-
const haveTail = have.includes(":") ? have.split(":").pop() : have
|
|
286
|
-
if (!sub.done && sub.role === role && haveTail === want) {
|
|
287
|
-
sub.done = true
|
|
288
|
-
sub.doneAt = Date.now()
|
|
289
|
-
sub.currentTool = null
|
|
290
|
-
sub.approval = null
|
|
291
|
-
if (lastError) sub.lastError = lastError
|
|
292
|
-
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
293
|
-
syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
|
|
294
|
-
return sub
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
return null
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/** 回合尾/挂起退出清扫(runAgentTurn finally / suspensionSession finally):冻结全部
|
|
301
|
-
* 剩余块——中断(Ctrl+C/错误)不留下钉住输入框的 ghost;未 done 者
|
|
302
|
-
* lastError="interrupted"(Ready 态跳过)。§17.5.5:挂起自然退出时本函数只兜底
|
|
303
|
-
* **未消化残项**(已消化块由 freezeReclaimDigestedBlocks 逐条先行回收——块回收与
|
|
304
|
-
* 池空解耦)。锚点降序同 freezeDoneSubTasks(挂起期 settle 锚点交错批次各按其
|
|
305
|
-
* settle 位置落位)。 */
|
|
306
|
-
export function freezeAllSubTasks(state) {
|
|
307
|
-
const subs = Object.values(state.subTasks ?? {})
|
|
308
|
-
.sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1))
|
|
309
|
-
for (const sub of subs) {
|
|
310
|
-
if (!sub.done) {
|
|
311
|
-
sub.done = true
|
|
312
|
-
sub.doneAt = Date.now()
|
|
313
|
-
if (!sub.lastError && state.status !== "Ready") sub.lastError = "interrupted"
|
|
314
|
-
}
|
|
315
|
-
freezeSubTaskLines(state, sub)
|
|
316
|
-
delete state.subTasks[sub.key]
|
|
317
|
-
}
|
|
318
|
-
syncPanelSnapshot(state) // §19.6 D-P1: 冻结回收后刷镜(块移出面板)
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/** §17.5.5 消化完成逐条冻结回收(2026-09-03 实测修订 + round1 #1 位置裁定):digest/会话内
|
|
322
|
-
* 用户回合消化完 pending 条目(run 首行已注入)后调用——把"已消化但仍驻留面板"的
|
|
323
|
-
* awaitingDigest 块立即冻结进流(不等池空——块回收与池空解耦;池空 freezeAllSubTasks
|
|
324
|
-
* 仅兜底未消化残项)。归属不变式:会话内任何 run 开始前 pinned 块的条目必在 pending
|
|
325
|
-
* (settle 即移交);run 消费后条目不在 pending 的 pinned 块 = 本 run 消化者——无需
|
|
326
|
-
* 快照即精确归属。位置(round1 #1 裁定——与 17.5.5 早版文本的矛盾已消解,见
|
|
327
|
-
* AGENT-LOOP.md §17.5.5):**settle 锚点 splice 落位——digest 总览文本之前**——同
|
|
328
|
-
* §7.2 D4 修复轮/D-S8 锚点语义(T-S6/T-S14 位置断言同口径)——锚点降序逐块冻结
|
|
329
|
-
* (splice 绝对位互不位移)。@returns {number} 回收块数 */
|
|
330
|
-
export function freezeReclaimDigestedBlocks(state, pendingList) {
|
|
331
|
-
const pend = pendingList ?? []
|
|
332
|
-
const targets = Object.values(state.subTasks ?? {})
|
|
333
|
-
.filter((s) => s.awaitingDigest && !pend.some((e) => `${e.role}#${e.id}` === s.key))
|
|
334
|
-
.sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1)) // 锚点降序(同 freezeAllSubTasks)
|
|
335
|
-
for (const sub of targets) {
|
|
336
|
-
freezeSubTaskLines(state, sub) // splice 落 settle 锚点(digest 总览文本之前)
|
|
337
|
-
delete state.subTasks[sub.key]
|
|
338
|
-
}
|
|
339
|
-
syncPanelSnapshot(state) // §19.6 D-P1: 逐条回收后刷镜(块移出面板)
|
|
340
|
-
return targets.length
|
|
341
|
-
}
|
|
342
|
-
|
|
105
|
+
/** Append(kind 合并追加)+ N2 单环 500 显示行上限;fresh=true 强制新块(每工具调用)。
|
|
106
|
+
* R23(2026-09-07):appendSubBlock/trimSubBlocks 实现迁 subagent-children.mjs——
|
|
107
|
+
* SUBAGENT-TAIL(D-ST4)后为单载体最旧先行(内层并入行同环计数);本文件 import +
|
|
108
|
+
* re-export 保 import 面(压缩面板等)。 */
|
|
343
109
|
/** 事件 token → 区块头部(turn/approval 更新 turn n/max + 等待态)。事件永不进
|
|
344
110
|
* blocks/主流——仅头部。@returns {boolean} 良构事件(已消费) */
|
|
345
111
|
export function applySubEvent(sub, payload) {
|
|
@@ -359,8 +125,12 @@ export function applySubEvent(sub, payload) {
|
|
|
359
125
|
|
|
360
126
|
// ─── Prefix routing(agent-turn callbacks 委派):true = 带前缀已消费;false = 主路径
|
|
361
127
|
|
|
362
|
-
/** onToken 分支:子文本 / [model] 元数据 / ⟦ev⟧
|
|
363
|
-
*
|
|
128
|
+
/** onToken 分支:子文本 / [model] 元数据 / ⟦ev⟧ 事件。SUBAGENT-TAIL(D-ST1——supersede
|
|
129
|
+
* R23 子块段方案,显示契约 docs/design/TUI.md §6):head 段路由块——inner 段**内容行
|
|
130
|
+
* 并入外层块 blocks**(append 目标上移),子块载体只余守护元数据(done 守卫——F2
|
|
131
|
+
* 迟到丢弃;currentTool——fresh 判别);内层 [model] 记子块模型;内层 ⟦ev⟧ 除
|
|
132
|
+
* done/stopped(生成侧补发射——D-R23c1——路由子块定格)外剥除不路由(防 explore
|
|
133
|
+
* 进度污染外层块头——round1 #4)。 */
|
|
364
134
|
export function routeSubToken(state, t, scheduleRender) {
|
|
365
135
|
const path = parseRelayPath(t)
|
|
366
136
|
if (!path) return false
|
|
@@ -384,7 +154,6 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
384
154
|
if (live.queued) {
|
|
385
155
|
delete live.queued
|
|
386
156
|
live.started = Date.now()
|
|
387
|
-
syncPanelSnapshot(state) // §19.6 D-P1: queued → running 状态变更刷镜
|
|
388
157
|
}
|
|
389
158
|
}
|
|
390
159
|
else {
|
|
@@ -403,7 +172,6 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
403
172
|
const live = state.subTasks?.[path.head]
|
|
404
173
|
if (live && !live.done && live.async !== true) {
|
|
405
174
|
delete state.subTasks[path.head]
|
|
406
|
-
syncPanelSnapshot(state) // §19.6 D-P1: 移除后刷镜(块出面板)
|
|
407
175
|
}
|
|
408
176
|
scheduleRender()
|
|
409
177
|
return true
|
|
@@ -416,7 +184,23 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
416
184
|
// queued(§20 D-SD3b)= 排队 spawn 等待块(spawn 返回即建/状态变迁刷新——覆盖式
|
|
417
185
|
// 更新 sub.queued——块不可展开(无活动流);启动后 async 事件清标转 running)。
|
|
418
186
|
if (payload.startsWith("⟦ev⟧")) {
|
|
419
|
-
|
|
187
|
+
// 内层完成信号(沿革:D-M8 子标 → R23 子块段 → SUBAGENT-TAIL 并入——显示契约
|
|
188
|
+
// docs/design/TUI.md §6):done/stopped = 生成侧补发射(D-R23c1——sync 同步收尾段带
|
|
189
|
+
// 完整嵌套前缀)→ 路由到子块定格(closeSubChild——不冻结外层、不落 preview);
|
|
190
|
+
// 缺失子块(迟到事件/
|
|
191
|
+
// 从未开块)→ 不建幻影(no-op)。其余内层事件(turn/approval/queued/settled/
|
|
192
|
+
// async/cancelled)剥除不路由(防 explore 进度污染外层块头——round1 #4)。
|
|
193
|
+
if (nested) {
|
|
194
|
+
const innerEv = payload.match(SUB_EVENT_RE)
|
|
195
|
+
if (innerEv && (innerEv[1] === "done" || innerEv[1] === "stopped")) {
|
|
196
|
+
const leaf = descendSubChild(sub, path.inner, { create: false })
|
|
197
|
+
// F2 保全:done 子块完全定格——迟到 done/stopped 丢弃(重复 done 不重放
|
|
198
|
+
// 定格;stopped 不打回 done 定格块)
|
|
199
|
+
if (leaf && !leaf.done) closeSubChild(sub, leaf, innerEv[1] === "stopped", path.label)
|
|
200
|
+
scheduleRender()
|
|
201
|
+
}
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
420
204
|
const ev = payload.match(SUB_EVENT_RE)
|
|
421
205
|
if (ev?.[1] === "queued") {
|
|
422
206
|
// 防御:已启动块(async 标记已置)收到迟到的 queued 刷新 → 丢弃(事件序保证
|
|
@@ -430,7 +214,6 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
430
214
|
sub.currentTool = null
|
|
431
215
|
sub.approval = null
|
|
432
216
|
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
433
|
-
syncPanelSnapshot(state) // §19.6 D-P1: waiting 块建/刷新刷镜
|
|
434
217
|
scheduleRender()
|
|
435
218
|
return true
|
|
436
219
|
}
|
|
@@ -445,7 +228,6 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
445
228
|
sub.currentTool = null
|
|
446
229
|
sub.approval = null
|
|
447
230
|
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
448
|
-
syncPanelSnapshot(state) // §19.6 D-P1: settled → awaitingDigest 状态变更刷镜
|
|
449
231
|
scheduleRender()
|
|
450
232
|
return true
|
|
451
233
|
}
|
|
@@ -457,7 +239,6 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
457
239
|
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
458
240
|
freezeSubTaskLines(state, sub)
|
|
459
241
|
delete state.subTasks[sub.key]
|
|
460
|
-
syncPanelSnapshot(state) // §19.6 D-P1: 冻结删除后刷镜
|
|
461
242
|
scheduleRender()
|
|
462
243
|
return true
|
|
463
244
|
}
|
|
@@ -470,7 +251,6 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
470
251
|
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
471
252
|
freezeSubTaskLines(state, sub)
|
|
472
253
|
delete state.subTasks[sub.key]
|
|
473
|
-
syncPanelSnapshot(state) // §19.6 D-P1: 冻结删除后刷镜
|
|
474
254
|
scheduleRender()
|
|
475
255
|
return true
|
|
476
256
|
}
|
|
@@ -482,62 +262,95 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
482
262
|
// parent's) — shown in the block header, NOT appended to its content stream.
|
|
483
263
|
// Only treat as metadata when the model isn't set yet (it's always the FIRST token);
|
|
484
264
|
// a child content token that happens to start with "[model]" must not be swallowed.
|
|
265
|
+
// SUBAGENT-TAIL:内层 [model] 记到守护载体(类目 B 字面量——不渲染、无正读者;
|
|
266
|
+
// docs/design/TUI.md §6);单层规则不变。
|
|
485
267
|
if (payload.startsWith("[model]") && (nested || sub.model === undefined)) {
|
|
486
|
-
if (
|
|
268
|
+
if (nested) {
|
|
269
|
+
const leaf = descendSubChild(sub, path.inner)
|
|
270
|
+
// F2: done 子块完全定格——迟到 [model] 不写定格块头
|
|
271
|
+
if (!leaf.done && leaf.model === undefined) {
|
|
272
|
+
leaf.model = payload.slice(7)
|
|
273
|
+
scheduleRender()
|
|
274
|
+
}
|
|
275
|
+
} else if (sub.model === undefined) {
|
|
487
276
|
sub.model = payload.slice(7)
|
|
488
277
|
scheduleRender()
|
|
489
|
-
}
|
|
278
|
+
}
|
|
490
279
|
return true
|
|
491
280
|
}
|
|
492
|
-
// Child LLM text → text block (N2 cap inside appendSubBlock).
|
|
493
|
-
|
|
281
|
+
// Child LLM text → text block (N2 cap inside appendSubBlock). SUBAGENT-TAIL(D-ST1):
|
|
282
|
+
// 内层文本**并入外层 blocks**(append 目标上移——子块不再承载内容行,取代小节)。
|
|
283
|
+
// F2:done 子块完全定格——迟到 chunk 丢弃(leaf.done 守卫——免无谓 render)。
|
|
284
|
+
const leaf = nested ? descendSubChild(sub, path.inner) : null
|
|
285
|
+
if (leaf?.done) return true
|
|
286
|
+
appendSubBlock(sub, "text", payload)
|
|
494
287
|
scheduleRender()
|
|
495
288
|
return true
|
|
496
289
|
}
|
|
497
290
|
|
|
498
291
|
/** Child reasoning token → think block (F2: same treatment as main reasoning).
|
|
499
|
-
*
|
|
292
|
+
* SUBAGENT-TAIL:内层 think 并入外层 blocks(append 目标上移——显示契约 TUI.md §6)。 */
|
|
500
293
|
export function routeSubReasoning(state, t, scheduleRender) {
|
|
501
294
|
const path = parseRelayPath(t)
|
|
502
295
|
if (!path) return false
|
|
503
296
|
const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
|
|
504
297
|
if (!sub) return true // frozen tombstone — drop late token
|
|
505
298
|
const nested = path.inner.length > 0
|
|
506
|
-
|
|
299
|
+
const leaf = nested ? descendSubChild(sub, path.inner) : null
|
|
300
|
+
if (leaf?.done) return true // F2: done 子块完全定格——迟到 think 丢弃
|
|
301
|
+
appendSubBlock(sub, "think", path.rest)
|
|
507
302
|
scheduleRender()
|
|
508
303
|
return true
|
|
509
304
|
}
|
|
510
305
|
|
|
511
|
-
/** Child tool call → fresh tool block + header currentTool
|
|
512
|
-
*
|
|
513
|
-
* (`explore#1/read
|
|
306
|
+
/** Child tool call → fresh tool block + header currentTool。SUBAGENT-TAIL(D-ST1):内层
|
|
307
|
+
* 工具行并入外层 blocks(同款 `❯ tool` 行——取代小节);currentTool 外层存全路径
|
|
308
|
+
* (`explore#1/read`——评审 #8:外层块头状态区现状保持不缩改)+ 子块存工具名(守护
|
|
309
|
+
* 元数据——fresh 判别源,routeSubToolOutput 读)。 */
|
|
514
310
|
export function routeSubToolCall(state, name, args, scheduleRender) {
|
|
515
311
|
const path = parseRelayPath(name)
|
|
516
312
|
if (!path) return false
|
|
517
313
|
const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
|
|
518
314
|
if (!sub) return true // frozen tombstone — drop late token
|
|
519
315
|
const nested = path.inner.length > 0
|
|
316
|
+
// F2: done 子块完全定格——迟到 tool call 丢弃(不复活外层 currentTool/子块状态)
|
|
317
|
+
const leaf = nested ? descendSubChild(sub, path.inner) : null
|
|
318
|
+
if (leaf?.done) return true
|
|
520
319
|
sub.currentTool = nested ? `${path.label}/${path.rest}` : path.rest
|
|
521
320
|
sub.toolArgs = args
|
|
522
321
|
sub.approval = null
|
|
523
322
|
const argsDesc = describeToolArgs(path.rest, args)
|
|
524
|
-
|
|
323
|
+
if (leaf) {
|
|
324
|
+
leaf.currentTool = path.rest
|
|
325
|
+
leaf.toolArgs = args
|
|
326
|
+
leaf.approval = null
|
|
327
|
+
}
|
|
328
|
+
appendSubBlock(sub, "tool", `❯ ${path.rest}${argsDesc ? " " + argsDesc : ""}\n`, { fresh: true })
|
|
525
329
|
scheduleRender()
|
|
526
330
|
return true
|
|
527
331
|
}
|
|
528
332
|
|
|
529
333
|
/** Child tool output(D1 前缀 name relay)→ 追加当前 tool block。RAW 拼接(2026-09-03
|
|
530
334
|
* 修复轮——relay chunk 是任意字节边界碎片,逐 chunk 补 \n 会把词拦腰断行 + 烧 N2
|
|
531
|
-
* 配额;emit
|
|
532
|
-
*
|
|
335
|
+
* 配额;emit 端自带换行结构无损还原)。SUBAGENT-TAIL(D-ST1):内层输出并入外层
|
|
336
|
+
* blocks(append 目标上移);fresh 判别照旧在子块层(守护元数据 leaf.currentTool=
|
|
337
|
+
* 工具名)——内外/同层同名工具互不串块。 */
|
|
533
338
|
export function routeSubToolOutput(state, name, part, scheduleRender) {
|
|
534
339
|
const path = parseRelayPath(name)
|
|
535
340
|
if (!path) return false
|
|
536
341
|
const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
|
|
537
342
|
if (!sub) return true // frozen tombstone — drop late token
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
if (
|
|
343
|
+
const nested = path.inner.length > 0
|
|
344
|
+
const leaf = nested ? descendSubChild(sub, path.inner) : null
|
|
345
|
+
if (leaf?.done) return true // F2: done 子块完全定格——迟到输出丢弃(不复活 currentTool)
|
|
346
|
+
const toolName = path.rest
|
|
347
|
+
if (leaf) {
|
|
348
|
+
appendSubBlock(sub, "tool", part.text, { fresh: leaf.currentTool !== toolName })
|
|
349
|
+
if (leaf.currentTool !== toolName) leaf.currentTool = toolName
|
|
350
|
+
} else {
|
|
351
|
+
appendSubBlock(sub, "tool", part.text, { fresh: sub.currentTool !== toolName })
|
|
352
|
+
if (sub.currentTool !== toolName) sub.currentTool = toolName
|
|
353
|
+
}
|
|
541
354
|
throttleSubRender(scheduleRender)
|
|
542
355
|
return true
|
|
543
356
|
}
|
|
@@ -578,7 +391,6 @@ export function ensureCompressPanel(state, info = {}) {
|
|
|
578
391
|
const messages = Number.isInteger(info.messages) && info.messages >= 0 ? info.messages : "?"
|
|
579
392
|
appendSubBlock(panel, "status", "Compressing context…\n", { fresh: true })
|
|
580
393
|
appendSubBlock(panel, "meta", `summarizing ${messages} messages\n`, { fresh: true })
|
|
581
|
-
syncPanelSnapshot(state) // §19.6 D-P1: 压缩面板建/重置刷镜
|
|
582
394
|
return panel
|
|
583
395
|
}
|
|
584
396
|
|
|
@@ -595,7 +407,6 @@ export function markCompressFailed(state, error) {
|
|
|
595
407
|
function freezeCompressPanel(state, panel) {
|
|
596
408
|
freezeSubTaskLines(state, panel)
|
|
597
409
|
delete state.subTasks[panel.key]
|
|
598
|
-
syncPanelSnapshot(state) // §19.6 D-P1: 压缩面板冻结删除后刷镜
|
|
599
410
|
}
|
|
600
411
|
|
|
601
412
|
/** 完成态(onCompress):"Compressed: N tokens freed → summary (Xs)"——冻结可折叠。 */
|