thincoder 0.12.58 → 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 +78 -2
- package/README.md +3 -3
- package/bin/thincoder.mjs +88 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +135 -26
- package/src/advisor/messages.mjs +57 -4
- package/src/advisor/run.mjs +119 -79
- package/src/advisor.mjs +34 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +182 -22
- package/src/agent/helpers.mjs +71 -4
- 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 +107 -20
- package/src/agent/spawn-child.mjs +54 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +133 -109
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +154 -104
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +26 -30
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +284 -0
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +152 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +470 -0
- package/src/agent-tools/subagent-async.mjs +382 -0
- 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 +343 -0
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +203 -377
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +198 -238
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +145 -242
- package/src/auto-think.mjs +14 -0
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +4 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +61 -8
- package/src/context.mjs +81 -163
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +30 -12
- package/src/escape.mjs +6 -4
- package/src/explore-distill.mjs +155 -0
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +11 -72
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +206 -87
- package/src/memory.mjs +3 -1
- package/src/model-specs.mjs +15 -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 +18 -39
- package/src/prompts/advisor-round1.md +20 -32
- package/src/prompts/advisor-round2.md +16 -16
- package/src/prompts/advisor-round3.md +16 -16
- package/src/prompts/coder.md +7 -28
- package/src/prompts/consult-base.md +4 -11
- package/src/prompts/discipline.md +31 -44
- package/src/prompts/eng-coder.md +9 -34
- package/src/prompts/engineering-sub.md +10 -8
- package/src/prompts/engineering.md +61 -264
- package/src/prompts/explore.md +4 -14
- package/src/prompts/main.md +18 -35
- package/src/prompts/methodology-template.md +32 -38
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +18 -35
- package/src/provider/core.mjs +62 -69
- 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/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +131 -44
- package/src/tools/edit-diff.mjs +348 -0
- package/src/tools/edit.md +20 -13
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +55 -24
- package/src/tools/file.mjs +25 -70
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- 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/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -3
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +84 -13
- package/src/tools/question.md +5 -1
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +18 -25
- package/src/tools/system.mjs +50 -30
- package/src/tools/tree.md +2 -1
- package/src/tools/wait_for.md +22 -0
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +179 -27
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +77 -16
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +17 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +11 -11
- 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 +31 -96
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +46 -0
- package/src/tui/pickers.mjs +51 -25
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +182 -0
- package/src/tui/startup.mjs +40 -0
- package/src/tui/subagent-blocks.mjs +272 -262
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +125 -12
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +127 -231
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +76 -0
- package/src/tui/wizard.mjs +48 -12
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-children.mjs — 嵌套子代理子块载体数据层(AGENT-LOOP.md §27 R23 D-R23a/c2/NFR)。
|
|
3
|
+
*
|
|
4
|
+
* 子块 = 外层 state.subTasks 块内的嵌套载体(外层 .children[]——每层一子块——D-R23d):
|
|
5
|
+
* carrier = { key(父内段标 role#N), role, model, started, done, doneAt, stopped,
|
|
6
|
+
* blocks, currentTool, toolArgs, approval, lastError, dropped,
|
|
7
|
+
* blockEpoch, children: [], _lineCount }——与外层块同形(渲染复用
|
|
8
|
+
* renderBlockTimeline/renderExpandedBlock 公共组件)。
|
|
9
|
+
* 内容/工具/模型全部归属子块(D-R23b——不再混外层 blocks——T-R23a.2 输出全在子块)。
|
|
10
|
+
*
|
|
11
|
+
* N2 共享配额(R23 NFR——评审 #6):子块行数计入**外层** 500 行环形上限——树级 trim
|
|
12
|
+
* (子块批量输出先丢、外层叙述保留优先——见 trimSubTree)——子块不独立扩容——面板
|
|
13
|
+
* 不无限增长。
|
|
14
|
+
*/
|
|
15
|
+
export const SUB_BLOCK_LINE_LIMIT = 500
|
|
16
|
+
|
|
17
|
+
const countBlockLines = (text) => text.split("\n").length
|
|
18
|
+
|
|
19
|
+
/** 载体树总行数(根自身 + 全部子孙块)——共享配额记账。 */
|
|
20
|
+
export function carrierTreeLines(carrier) {
|
|
21
|
+
let n = carrier._lineCount ?? 0
|
|
22
|
+
for (const c of carrier.children ?? []) n += carrierTreeLines(c)
|
|
23
|
+
return n
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 载体级丢行(原 trimSubBlocks 逐块丢弃数学——整块 shift、末块按行裁切);返回实际
|
|
27
|
+
* 丢弃行数。每载体独立 dropped 计数 + meta 省略标记(同既有 trimSubBlocks 语义)。 */
|
|
28
|
+
function dropCarrierLines(carrier, want) {
|
|
29
|
+
if (want <= 0 || carrier.blocks.length === 0) return 0
|
|
30
|
+
let dropped = 0
|
|
31
|
+
while (want > dropped && carrier.blocks.length > 0) {
|
|
32
|
+
const first = carrier.blocks[0]
|
|
33
|
+
const lines = countBlockLines(first.text)
|
|
34
|
+
const take = Math.min(lines, want - dropped)
|
|
35
|
+
if (take >= lines) {
|
|
36
|
+
carrier.blocks.shift()
|
|
37
|
+
dropped += lines
|
|
38
|
+
} else {
|
|
39
|
+
first.text = first.text.split("\n").slice(take).join("\n")
|
|
40
|
+
dropped += take
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
carrier._lineCount = (carrier._lineCount ?? 0) - dropped
|
|
44
|
+
carrier.dropped = (carrier.dropped ?? 0) + dropped
|
|
45
|
+
const marker = `…(已省略 ${carrier.dropped} 行)`
|
|
46
|
+
const first = carrier.blocks[0]
|
|
47
|
+
if (first && first.kind === "meta") first.text = marker
|
|
48
|
+
else { carrier.blocks.unshift({ kind: "meta", text: marker }); carrier._lineCount = (carrier._lineCount ?? 0) + 1 }
|
|
49
|
+
return dropped
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** R23 NFR:树级配额 trim——总行(根 + 子块树)超限 → 丢行。丢序 = **子树递归先于
|
|
53
|
+
* 根载体**(后序:叶子/子块先、根最后)——子块承载批量输出(explore 工具流),外层
|
|
54
|
+
* 叙述行(折叠头 tail/状态区的展示源)保留优先;每载体内部仍最旧行先行(dropCarrierLines
|
|
55
|
+
* 逐块 FIFO)。单载体(无子块)情形与既有 trimSubBlocks 行为逐字同效。
|
|
56
|
+
* §27.1 F1(2026-09-07 三缺陷修复批——缺陷③主修):丢行遍历**跳过 done 子块**——
|
|
57
|
+
* done = 定格快照——配额压力不再蚕食已定格子块行,丢行落点移到外层自身行(live
|
|
58
|
+
* 块「已省略 N 行」增长语义正确)。边界(评审 #6):live 行耗尽(全树皆 done)→
|
|
59
|
+
* 允许超限(定格快照优先于配额——N2 上限对 done 存量 best-effort)。 */
|
|
60
|
+
export function trimSubTree(root) {
|
|
61
|
+
let over = carrierTreeLines(root) - SUB_BLOCK_LINE_LIMIT
|
|
62
|
+
if (over <= 0) return
|
|
63
|
+
const walk = (carrier) => {
|
|
64
|
+
for (const c of carrier.children ?? []) {
|
|
65
|
+
if (over <= 0) return
|
|
66
|
+
if (c.done) continue // §27.1 F1: done 子块 = 定格快照——丢行遍历跳过(豁免)
|
|
67
|
+
walk(c)
|
|
68
|
+
}
|
|
69
|
+
if (over <= 0) return
|
|
70
|
+
over -= dropCarrierLines(carrier, over)
|
|
71
|
+
}
|
|
72
|
+
walk(root)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** kind 合并追加(载体级)——既有 appendSubBlock 数学(净增行记账——P1)。 */
|
|
76
|
+
function pushBlock(carrier, kind, text, fresh) {
|
|
77
|
+
if (!text) return
|
|
78
|
+
const last = carrier.blocks.at(-1)
|
|
79
|
+
if (!fresh && last && last.kind === kind) {
|
|
80
|
+
const before = countBlockLines(last.text)
|
|
81
|
+
last.text += text
|
|
82
|
+
carrier._lineCount = (carrier._lineCount ?? 0) + countBlockLines(last.text) - before
|
|
83
|
+
} else {
|
|
84
|
+
carrier.blocks.push({ kind, text })
|
|
85
|
+
carrier._lineCount = (carrier._lineCount ?? 0) + countBlockLines(text)
|
|
86
|
+
}
|
|
87
|
+
carrier.blockEpoch = (carrier.blockEpoch ?? 0) + 1
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 追加到根载体(外层块/压缩面板——无父载体者自身即配额树根)。树级 trim——
|
|
91
|
+
* 无子块时与既有 trimSubBlocks 同效(N2 既有测试口径不变)。 */
|
|
92
|
+
export function appendSubBlock(sub, kind, text, { fresh = false } = {}) {
|
|
93
|
+
if (!text) return
|
|
94
|
+
pushBlock(sub, kind, text, fresh)
|
|
95
|
+
trimSubTree(sub)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** R23:追加到子块载体——行数计入根(外层)树配额(NFR——同配额不独立扩容)。
|
|
99
|
+
* §27.1 F2(2026-09-07):child.done 守卫——done 后完全定格,迟到 chunk 丢弃
|
|
100
|
+
* (与 §7.2 D4 完成态冻结语义统一——数据层 tombstone 拒绝)。 */
|
|
101
|
+
export function appendSubChild(root, child, kind, text, { fresh = false } = {}) {
|
|
102
|
+
if (!text) return
|
|
103
|
+
if (child.done) return
|
|
104
|
+
pushBlock(child, kind, text, fresh)
|
|
105
|
+
root.blockEpoch = (root.blockEpoch ?? 0) + 1
|
|
106
|
+
trimSubTree(root)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** R23 D-R23a:子块载体创建/定位——按段(role#N)在父载体 children 查找,缺失即建
|
|
110
|
+
* (每层一子块——任意 inner 深度同路径——D-R23d)。@returns 子块载体 */
|
|
111
|
+
export function ensureSubChild(carrier, seg) {
|
|
112
|
+
carrier.children ??= []
|
|
113
|
+
let c = carrier.children.find((x) => x.key === seg)
|
|
114
|
+
if (!c) {
|
|
115
|
+
c = {
|
|
116
|
+
key: seg,
|
|
117
|
+
role: seg.slice(0, seg.lastIndexOf("#")),
|
|
118
|
+
model: undefined,
|
|
119
|
+
started: Date.now(), done: false, doneAt: null, stopped: false,
|
|
120
|
+
blocks: [], currentTool: null, toolArgs: null,
|
|
121
|
+
approval: null, lastError: null, dropped: 0,
|
|
122
|
+
blockEpoch: 0, children: [], _lineCount: 0,
|
|
123
|
+
}
|
|
124
|
+
carrier.children.push(c)
|
|
125
|
+
}
|
|
126
|
+
return c
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 沿 inner 段逐层下降。create=false 时缺层返回 null(事件关闭路径——不建幻影空块)。 */
|
|
130
|
+
export function descendSubChild(root, inner, { create = true } = {}) {
|
|
131
|
+
let carrier = root
|
|
132
|
+
for (const seg of inner) {
|
|
133
|
+
if (create) carrier = ensureSubChild(carrier, seg)
|
|
134
|
+
else {
|
|
135
|
+
const next = (carrier.children ?? []).find((x) => x.key === seg)
|
|
136
|
+
if (!next) return null
|
|
137
|
+
carrier = next
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return carrier === root ? null : carrier
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** R23 D-R23c2 子块完成定格(内层 ⟦ev⟧done/stopped → 子块尾定格——不落 preview、不冻结
|
|
144
|
+
* 外层);stopped = interrupted(外层 abort 传播/内层错误收尾——T-R23a.3 不悬空)。
|
|
145
|
+
* 外层块头 currentTool 若指向该子块(innerPath 前缀——评审 #8 全路径现状保持)→ 清空
|
|
146
|
+
* (子块已定格——工具不再是 current)。 */
|
|
147
|
+
export function closeSubChild(root, child, stopped, innerPath) {
|
|
148
|
+
child.done = true
|
|
149
|
+
child.doneAt = Date.now()
|
|
150
|
+
if (stopped) child.stopped = true
|
|
151
|
+
child.currentTool = null
|
|
152
|
+
child.approval = null
|
|
153
|
+
child.blockEpoch = (child.blockEpoch ?? 0) + 1
|
|
154
|
+
if (root.currentTool && innerPath && String(root.currentTool).startsWith(`${innerPath}/`)) {
|
|
155
|
+
root.currentTool = null
|
|
156
|
+
root.toolArgs = null
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** R23 D-R23c2 收尾语义(T-R23c.2a):外层先冻结而内层未收尾(外层 abort/中断)→
|
|
161
|
+
* 内层子块随外层冻结时定格 stopped——不悬空。freezeSubTaskLines 冻结前调用。 */
|
|
162
|
+
export function closeOpenSubChildren(sub) {
|
|
163
|
+
const walk = (carrier) => {
|
|
164
|
+
for (const c of carrier.children ?? []) {
|
|
165
|
+
if (!c.done) {
|
|
166
|
+
c.done = true
|
|
167
|
+
c.doneAt = Date.now()
|
|
168
|
+
c.stopped = true
|
|
169
|
+
c.currentTool = null
|
|
170
|
+
c.approval = null
|
|
171
|
+
}
|
|
172
|
+
walk(c)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
walk(sub)
|
|
176
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-freeze.mjs — 子agent 区块完成/冻结族(2026-09-05 module-split:
|
|
3
|
+
* subagent-blocks.mjs 625 > 500 硬限——§19.6 面板镜像 syncPanelSnapshot + finish/
|
|
4
|
+
* freeze 家族 verbatim 迁入,语义零变;subagent-blocks.mjs import 回(routeSub* 与
|
|
5
|
+
* compression panel 调用点)+ re-export 保外部 import 面(index.mjs 等)。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { C } from "./ansi.mjs"
|
|
9
|
+
import { closeOpenSubChildren } from "./subagent-children.mjs"
|
|
10
|
+
|
|
11
|
+
// ─── §19.6 D-P1 面板镜像(subagent panel 检查工具)───
|
|
12
|
+
// TUI 装配处(index.mjs startTUI)state._agent = agent——块级状态变更点经
|
|
13
|
+
// syncPanelSnapshot 刷新 agent._panelSnapshot(区块数组快照:{key, role,
|
|
14
|
+
// status: running|done|awaitingDigest, startedAt}——状态变更即刷——O(n) 小——
|
|
15
|
+
// 不逐 token——NF-P)。agent 层 subagent action:"panel" 读此镜像(视图与用户
|
|
16
|
+
// 所见一致)+ 冻结门控(D-P3)。未挂载(headless/mock——无 state._agent)=
|
|
17
|
+
// 无镜像——no-op(降级路径由 agent 侧判定)。
|
|
18
|
+
|
|
19
|
+
/** 刷新面板镜像(调用点 = 本模块全部 state.subTasks 块级变更点末尾——map 已稳定)。 */
|
|
20
|
+
export function syncPanelSnapshot(state) {
|
|
21
|
+
const agent = state._agent
|
|
22
|
+
if (!agent) return // 未挂载(headless/mock 无 TUI 装配)——无镜像
|
|
23
|
+
const snap = []
|
|
24
|
+
for (const sub of Object.values(state.subTasks ?? {})) {
|
|
25
|
+
// §20 D-SD3b:waiting/等位块(sub.queued——未启动)以 queued 态入镜(镜像与面板
|
|
26
|
+
// 所见一致——action:'panel' 视图 + freeze 门控读此态)。
|
|
27
|
+
const status = sub.done ? (sub.awaitingDigest ? "awaitingDigest" : "done") : (sub.queued ? "queued" : "running")
|
|
28
|
+
snap.push({ key: sub.key, role: sub.role ?? null, status, startedAt: sub.started ?? Date.now() })
|
|
29
|
+
}
|
|
30
|
+
agent._panelSnapshot = snap
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Mark the earliest running child of the given role(s) as done (✓ header, frozen elapsed). */
|
|
34
|
+
export function finishSubTask(state, roles, lastError = null) {
|
|
35
|
+
state.subTasks ??= {}
|
|
36
|
+
const roleSet = new Set(Array.isArray(roles) ? roles : [roles])
|
|
37
|
+
const running = Object.values(state.subTasks)
|
|
38
|
+
.filter((s) => !s.done && roleSet.has(s.role))
|
|
39
|
+
.sort((a, b) => a.started - b.started)
|
|
40
|
+
if (running.length === 0) return
|
|
41
|
+
const sub = running[0]
|
|
42
|
+
sub.done = true
|
|
43
|
+
sub.doneAt = Date.now()
|
|
44
|
+
sub.currentTool = null
|
|
45
|
+
sub.approval = null
|
|
46
|
+
if (lastError) sub.lastError = lastError
|
|
47
|
+
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
48
|
+
syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** §7.2.3 sync spawn 完成精确冻结(2026-09-03):按 relay key 精确 settle。finishSubTask
|
|
52
|
+
* 的"最早 started"启发式只在面板单 running 块时成立——§15 async 化后 async eng-coder
|
|
53
|
+
* (先启动)与 sync explore 并存时 explore 完成会误冻先启动的 eng-coder 块(7.2.3.1)。
|
|
54
|
+
* dispatch 沿 ctx._subagentKey(relayPrefix 去尾 = `role#N`)把 key 传进 onToolResult——
|
|
55
|
+
* 有 key 即精确标 done(冻结载体进流 + 删条目由调用方 freezeDoneSubTasks 承接——与
|
|
56
|
+
* finishSubTask 同一契约);无匹配块(已冻结 tombstone/不存在)返回 null——调用方不得
|
|
57
|
+
* 落启发式兜底(精确 key 无匹配 = 无从归属——不误冻他块)。 */
|
|
58
|
+
export function finishSubTaskKey(state, key, lastError = null) {
|
|
59
|
+
const sub = state.subTasks?.[key]
|
|
60
|
+
if (!sub) return null
|
|
61
|
+
sub.done = true
|
|
62
|
+
sub.doneAt = Date.now()
|
|
63
|
+
sub.currentTool = null
|
|
64
|
+
sub.approval = null
|
|
65
|
+
if (lastError) sub.lastError = lastError
|
|
66
|
+
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
67
|
+
syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
|
|
68
|
+
return sub
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 冻结完成/中断区块进 state.lines(§7.2 D4——_frozenSubTask 载体行,渲染端
|
|
72
|
+
* render-conversation 识别;折叠交互 key = sub-${key} 与运行面板同源跨冻结延续)。
|
|
73
|
+
* §27 R23 D-R23c2 收尾语义(T-R23c.2a):外层先冻结而内层子块未收尾(外层 abort/
|
|
74
|
+
* 中断)→ 子块随外层冻结定格 stopped(closeOpenSubChildren——不悬空)。
|
|
75
|
+
* 锚点插入(2026-09-03 修复轮):settled 块带 _freezeAt(settle 时刻流位置)——
|
|
76
|
+
* splice 落位使挂起期补发冻结块位于其 digest 总览文本之前;无锚点尾推不变;
|
|
77
|
+
* 多锚点批量冻结按降序(绝对位置 splice——先插小锚点会移走大锚点目标)。 */
|
|
78
|
+
export function freezeSubTaskLines(state, sub) {
|
|
79
|
+
if (!sub) return
|
|
80
|
+
closeOpenSubChildren(sub) // R23 D-R23c2——开子块随外层冻结定格 stopped
|
|
81
|
+
state._frozenSubKeys ??= new Set()
|
|
82
|
+
state._frozenSubKeys.add(sub.key)
|
|
83
|
+
sub.done = true
|
|
84
|
+
sub.doneAt = sub.doneAt ?? Date.now()
|
|
85
|
+
const anchor = sub._freezeAt ?? state.lines.length
|
|
86
|
+
state.lines.splice(Math.min(anchor, state.lines.length), 0, {
|
|
87
|
+
text: `subagent activity: ${sub.key}`, color: C.dim, _frozenSubTask: sub,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 头裁锚点校正(index.mjs pushLine 调用):裁 removedCount 补 1 标记行 = 净位移
|
|
92
|
+
* removedCount−1(code review round1 #3);在途锚点前移,min 0 兜底。 */
|
|
93
|
+
export function shiftFreezeAnchors(state, removedCount) {
|
|
94
|
+
const shift = removedCount - 1
|
|
95
|
+
for (const sub of Object.values(state.subTasks ?? {})) {
|
|
96
|
+
if (sub._freezeAt !== undefined) sub._freezeAt = Math.max(0, sub._freezeAt - shift)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 冻结 + 释放全部已 done 块(工具结果清扫路径)。锚点降序(后 settle 先插——
|
|
101
|
+
* 绝对位置 splice 语义);无锚点(-1)排末尾推;sort 稳定。 */
|
|
102
|
+
export function freezeDoneSubTasks(state) {
|
|
103
|
+
const subs = Object.values(state.subTasks ?? {})
|
|
104
|
+
.filter((s) => s.done)
|
|
105
|
+
.sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1))
|
|
106
|
+
for (const sub of subs) {
|
|
107
|
+
freezeSubTaskLines(state, sub)
|
|
108
|
+
delete state.subTasks[sub.key]
|
|
109
|
+
}
|
|
110
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 删除后刷镜(块移出面板)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 按角色整组标记 done——consult N 并行 children 的会话级 settle(单发
|
|
114
|
+
* finishSubTask 只结最早一个——consult 残项 2026-08-30)。 */
|
|
115
|
+
export function finishSubTasksByRole(state, roles, lastError = null) {
|
|
116
|
+
state.subTasks ??= {}
|
|
117
|
+
const roleSet = new Set(Array.isArray(roles) ? roles : [roles])
|
|
118
|
+
for (const sub of Object.values(state.subTasks)) {
|
|
119
|
+
if (!sub.done && roleSet.has(sub.role)) {
|
|
120
|
+
sub.done = true
|
|
121
|
+
sub.doneAt = Date.now()
|
|
122
|
+
sub.currentTool = null
|
|
123
|
+
sub.approval = null
|
|
124
|
+
if (lastError) sub.lastError = lastError
|
|
125
|
+
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 回合尾/挂起退出清扫(runAgentTurn finally / suspensionSession finally):冻结全部
|
|
132
|
+
* 剩余块——中断(Ctrl+C/错误)不留下钉住输入框的 ghost;未 done 者
|
|
133
|
+
* lastError="interrupted"(Ready 态跳过)。§17.5.5:挂起自然退出时本函数只兜底
|
|
134
|
+
* **未消化残项**(已消化块由 freezeReclaimDigestedBlocks 逐条先行回收——块回收与
|
|
135
|
+
* 池空解耦)。锚点降序同 freezeDoneSubTasks(挂起期 settle 锚点交错批次各按其
|
|
136
|
+
* settle 位置落位)。 */
|
|
137
|
+
export function freezeAllSubTasks(state) {
|
|
138
|
+
const subs = Object.values(state.subTasks ?? {})
|
|
139
|
+
.sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1))
|
|
140
|
+
for (const sub of subs) {
|
|
141
|
+
if (!sub.done) {
|
|
142
|
+
sub.done = true
|
|
143
|
+
sub.doneAt = Date.now()
|
|
144
|
+
if (!sub.lastError && state.status !== "Ready") sub.lastError = "interrupted"
|
|
145
|
+
}
|
|
146
|
+
freezeSubTaskLines(state, sub)
|
|
147
|
+
delete state.subTasks[sub.key]
|
|
148
|
+
}
|
|
149
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 冻结回收后刷镜(块移出面板)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** §17.5.5 消化完成逐条冻结回收(2026-09-03 实测修订 + round1 #1 位置裁定):digest/会话内
|
|
153
|
+
* 用户回合消化完 pending 条目(run 首行已注入)后调用——把"已消化但仍驻留面板"的
|
|
154
|
+
* awaitingDigest 块立即冻结进流(不等池空——块回收与池空解耦;池空 freezeAllSubTasks
|
|
155
|
+
* 仅兜底未消化残项)。归属不变式:会话内任何 run 开始前 pinned 块的条目必在 pending
|
|
156
|
+
* (settle 即移交);run 消费后条目不在 pending 的 pinned 块 = 本 run 消化者——无需
|
|
157
|
+
* 快照即精确归属。位置(round1 #1 裁定——与 17.5.5 早版文本的矛盾已消解,见
|
|
158
|
+
* AGENT-LOOP.md §17.5.5):**settle 锚点 splice 落位——digest 总览文本之前**——同
|
|
159
|
+
* §7.2 D4 修复轮/D-S8 锚点语义(T-S6/T-S14 位置断言同口径)——锚点降序逐块冻结
|
|
160
|
+
* (splice 绝对位互不位移)。@returns {number} 回收块数 */
|
|
161
|
+
export function freezeReclaimDigestedBlocks(state, pendingList) {
|
|
162
|
+
const pend = pendingList ?? []
|
|
163
|
+
const targets = Object.values(state.subTasks ?? {})
|
|
164
|
+
.filter((s) => s.awaitingDigest && !pend.some((e) => `${e.role}#${e.id}` === s.key))
|
|
165
|
+
.sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1)) // 锚点降序(同 freezeAllSubTasks)
|
|
166
|
+
for (const sub of targets) {
|
|
167
|
+
freezeSubTaskLines(state, sub) // splice 落 settle 锚点(digest 总览文本之前)
|
|
168
|
+
delete state.subTasks[sub.key]
|
|
169
|
+
}
|
|
170
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 逐条回收后刷镜(块移出面板)
|
|
171
|
+
return targets.length
|
|
172
|
+
}
|
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
* 运行中区块的渲染行数(F2,会话区被挤小);无运行中区块 → 返回 [](F6 空态,
|
|
7
7
|
* 无悬空分隔线)。子 agent 完成后立即冻结进会话流(subagent-blocks.mjs
|
|
8
8
|
* freezeSubTaskLines,✓ 头 + 可展开,§7.2 D4 现状不变),面板下一帧自然移除
|
|
9
|
-
* 该区块(F5)——本模块只渲染 `!done`
|
|
9
|
+
* 该区块(F5)——本模块只渲染 `!done` 条目。§17 T-S14 中间态例外:挂起期已结算
|
|
10
|
+
* 区块(sub.done && sub.awaitingDigest)冻结被延迟,驻留面板显示
|
|
11
|
+
* "done · awaiting digestion",池空补发冻结后才移除。
|
|
12
|
+
*
|
|
13
|
+
* §19.5 D-M7 ⏹:运行中(非 done)折叠头右缘停止标记(dim,仅折叠头)——点击 =
|
|
14
|
+
* cancel(mouse.mjs 列级命中 _stopCol——不触发折叠翻转)。
|
|
15
|
+
* §27 R23(supersede §19.5 D-M8 子标方案):嵌套子代理渲染为子块段(renderSubChildSections
|
|
16
|
+
* ——子块头 + 独立折叠键——面板与冻结渲染共用——styleSubLabelRow 已随子标机制退役)。
|
|
10
17
|
*
|
|
11
18
|
* 中立模块(D1 评审 #6):layout.mjs 调 renderSubagentPanel 预计算面板高度
|
|
12
19
|
* (subagentLines → subagentH),render-frame.mjs 直接 put 预计算行(不重复
|
|
@@ -19,10 +26,66 @@
|
|
|
19
26
|
* 折叠状态 key = `sub-${key}` 跨 turn 保持(D5,与冻结区块同一 key——冻结边界
|
|
20
27
|
* 无缝衔接)。
|
|
21
28
|
*/
|
|
22
|
-
import { C } from "./ansi.mjs"
|
|
29
|
+
import { ansi, C } from "./ansi.mjs"
|
|
23
30
|
import { sliceByWidth, stringWidth } from "./render.mjs"
|
|
24
31
|
import { isExpanded, renderBlockTimeline, renderExpandedBlock, foldTailLines } from "./fold-block.mjs"
|
|
32
|
+
import { SUBAGENT_ROLES } from "./subagent-blocks.mjs"
|
|
25
33
|
|
|
34
|
+
/**
|
|
35
|
+
* §27 R23 子块(嵌套子代理)段渲染——面板与冻结渲染共用(中立纯函数模块):
|
|
36
|
+
* 每子块 = 工具式头行(`❯ explore#N · model · elapsed`——D-R23b,dim ❯ + role#N 亮 +
|
|
37
|
+
* model/elapsed dim——与 C 工具头同排式;done/stopped 定格动词——D-R23c2)+ 折叠态
|
|
38
|
+
* tail 2 / 展开态全量窗口(60% 封顶 + 块内滚动——fold-block 公共组件);孙块递归
|
|
39
|
+
* (任意 inner 深度——D-R23d——每层独立折叠键)。折叠键 = `sub-{outerKey}/{innerPath}`
|
|
40
|
+
* (D-R23e——与既有 sub-{key} 同族——_foldToggle 命中映射复用 fold-block 通用通道——
|
|
41
|
+
* mouse.mjs 零改动)。行数配额在数据层(子块计入外层 500 行环——NFR)。
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** 子块折叠键(D-R23e):sub-{outerKey}/{innerPath}。 */
|
|
45
|
+
export function subChildFoldKey(rootKey, innerPath) {
|
|
46
|
+
return `sub-${rootKey}/${innerPath}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 子块头行(D-R23b/c2)——返回带 _foldToggle 的行(可点击折叠/展开——fold-block 通道)。 */
|
|
50
|
+
function subChildHeadRow(rootKey, child, innerPath, cols) {
|
|
51
|
+
const foldKey = subChildFoldKey(rootKey, innerPath)
|
|
52
|
+
const end = child.done ? (child.doneAt ?? Date.now()) : Date.now()
|
|
53
|
+
const secs = Math.max(0, Math.floor((end - (child.started ?? end)) / 1000))
|
|
54
|
+
const verb = child.done ? (child.stopped ? `stopped ${secs}s` : `done ${secs}s`) : `${secs}s`
|
|
55
|
+
const raw = `❯ ${child.key}${child.model ? ` · ${child.model}` : ""} · ${verb}`
|
|
56
|
+
let text = sliceByWidth(raw, Math.max(1, cols - 2))
|
|
57
|
+
// dim 注入(截断后——自闭合:行末由打印端 reset 兜底;灰色段间 restore 恢复行基色)。
|
|
58
|
+
const restore = ansi.fg(6) // 行基色 C.tool(cyan)
|
|
59
|
+
if (text.startsWith("❯")) text = `${ansi.gray}❯${restore}${text.slice(1)}`
|
|
60
|
+
const sep = text.indexOf(" · ")
|
|
61
|
+
if (sep > 0) text = `${text.slice(0, sep)}${ansi.gray}${text.slice(sep)}`
|
|
62
|
+
return { text, color: C.tool, _foldToggle: foldKey }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 子块段行(head + 折叠 tail2 / 展开窗口)——递归孙块。root = 外层块(key 为折叠前缀源)。 */
|
|
66
|
+
export function renderSubChildSections(state, root, cols, maxRows) {
|
|
67
|
+
const out = []
|
|
68
|
+
const walk = (carrier, path) => {
|
|
69
|
+
for (const child of carrier.children ?? []) {
|
|
70
|
+
const innerPath = path ? `${path}/${child.key}` : child.key
|
|
71
|
+
out.push(subChildHeadRow(root.key, child, innerPath, cols))
|
|
72
|
+
const foldKey = subChildFoldKey(root.key, innerPath)
|
|
73
|
+
if (isExpanded(state, foldKey)) {
|
|
74
|
+
// 展开:全量时间线窗口化(per-kind 着色,60% 封顶 + 块内滚动——公共组件)
|
|
75
|
+
const body = renderBlockTimeline(child.blocks, cols)
|
|
76
|
+
out.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: `${child.key} activity` }))
|
|
77
|
+
} else {
|
|
78
|
+
// 折叠:tail 2 非空行(最近活动)——dim(与既有外层折叠 tail 同型)
|
|
79
|
+
for (const line of foldTailLines(child.blocks, 2)) {
|
|
80
|
+
out.push({ text: `│ ${sliceByWidth(line, cols - 4)}`, color: C.dim, _skipDimFold: true })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
walk(child, innerPath)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
walk(root, "")
|
|
87
|
+
return out
|
|
88
|
+
}
|
|
26
89
|
/**
|
|
27
90
|
* 面板行构建(纯函数):顶部分隔线 `─` + 各运行中区块(折叠头 + tail 3 /
|
|
28
91
|
* 展开全量)。maxRows = 终端行数(展开态 60% 封顶窗口化);省略 = 不封顶
|
|
@@ -30,7 +93,7 @@ import { isExpanded, renderBlockTimeline, renderExpandedBlock, foldTailLines } f
|
|
|
30
93
|
* @returns {Array<{text: string, color: string, ...}>}
|
|
31
94
|
*/
|
|
32
95
|
export function renderSubagentPanel(state, cols, maxRows) {
|
|
33
|
-
const runningSubs = Object.values(state.subTasks ?? {}).filter((s) => !s.done)
|
|
96
|
+
const runningSubs = Object.values(state.subTasks ?? {}).filter((s) => !s.done || s.awaitingDigest)
|
|
34
97
|
if (runningSubs.length === 0) return []
|
|
35
98
|
const out = []
|
|
36
99
|
// 面板顶部边界线(现状分隔线语义迁移,§7.2.1 D2/NF2)——面板存在即画线,
|
|
@@ -41,8 +104,26 @@ export function renderSubagentPanel(state, cols, maxRows) {
|
|
|
41
104
|
// 头部摘要:`[▶ coder#1 · glm-5.3 · 45s · turn 12/100] bash — npm test`
|
|
42
105
|
// ⏸ = 等待审批态(sub.approval 非空,评审 #5 定义);图标在括号内,
|
|
43
106
|
// 与冻结头 `[✓ …]` 格式统一(任务简报 UI 决策)。
|
|
44
|
-
const icon = sub.approval ? "⏸" : "▶"
|
|
45
|
-
const elapsed = Math.floor((Date.now() - sub.started) / 1000)
|
|
107
|
+
const icon = sub.approval ? "⏸" : sub.done ? "✓" : "▶"
|
|
108
|
+
const elapsed = Math.floor(((sub.done ? (sub.doneAt ?? Date.now()) : Date.now()) - sub.started) / 1000)
|
|
109
|
+
// §19.5 D-M7b ②: sync/async 显式头标(B 形态——不靠"没标推断")——async 由
|
|
110
|
+
// ⟦ev⟧async 标记置位;sync 区块(无标记)显式标 sync。真实 subagent 角色
|
|
111
|
+
// (escalate/consult/compress 等复用面板槽的条目无语义——非 spawn 角色豁免);
|
|
112
|
+
// §24 D-24b:role "advisor" 伪角色同面板(块/⏹/冻结复用——ruling ②-4 A)。
|
|
113
|
+
// 冻结后保留(与 model 标识同生命周期——render-conversation frozenSubTaskLines
|
|
114
|
+
// 同款 modePart)。**颜色后置注入**(code review 🔵#4):bracket 宽度预算用纯文
|
|
115
|
+
// 本(dim ANSI 内嵌会被 sliceByWidth 截断在 restore 之前 → 行尾残留 dim)——
|
|
116
|
+
// 截断后对完整存活的 mode word 单独套 dim + 恢复行色(自闭合——截断落在词内
|
|
117
|
+
// 则 replace 不命中 → 无 ANSI 泄漏,词以行色显示)。
|
|
118
|
+
const isSubRole = SUBAGENT_ROLES.includes(sub.role) || sub.role === "advisor"
|
|
119
|
+
// §20 D-SD3b waiting 块(sub.queued——排队 spawn 返回即建——未启动无 relay 流):
|
|
120
|
+
// 括号状态词 = waiting(依赖/域冲突等位——detail 即原因)或 queued(槽满等位——
|
|
121
|
+
// 状态区显示 position);不显示 sync/async 词(尚未启动——无 async 标记可言——
|
|
122
|
+
// sync 词会误导:async spawn 排队的块不是 sync);⏹ 门控不变(async 启动后才置)。
|
|
123
|
+
const queued = sub.queued ?? null
|
|
124
|
+
const statusWord = queued ? (queued.kind === "slot" ? "queued" : "waiting") : null
|
|
125
|
+
const modeWord = isSubRole && !queued ? (sub.async === true ? "async" : "sync") : null
|
|
126
|
+
const modePart = modeWord ? ` · ${modeWord}` : (statusWord ? ` · ${statusWord}` : "")
|
|
46
127
|
// 评审 #1 宽度预算:模型名先单独按显示宽度截断([model] token 原样记录可长
|
|
47
128
|
// 20-30+ 字符,不截断则括号前缀宽度不可预算、状态区被挤出终端右边距);
|
|
48
129
|
// 再量括号前缀实际显示宽度,状态区按 cols - bracketWidth - 2 截断——整行
|
|
@@ -52,20 +133,50 @@ export function renderSubagentPanel(state, cols, maxRows) {
|
|
|
52
133
|
? ` · ${sliceByWidth(sub.model, Math.max(8, Math.floor(cols / 3)))}`
|
|
53
134
|
: ""
|
|
54
135
|
const turnPart = sub.maxTurns > 0 ? ` · turn ${sub.turn}/${sub.maxTurns}` : ""
|
|
55
|
-
const
|
|
136
|
+
const bracketRaw = `[${icon} ${sub.key}${modePart}${modelPart} · ${elapsed}s${turnPart}]`
|
|
137
|
+
let bracket = sliceByWidth(bracketRaw, Math.max(1, cols - 2))
|
|
138
|
+
if (modeWord) {
|
|
139
|
+
const painted = bracket.replace(` · ${modeWord}`, ` · ${ansi.dim}${modeWord}${C.tool}`)
|
|
140
|
+
if (painted !== bracket) bracket = painted // 词被截断则保持纯文本(不泄漏 dim)
|
|
141
|
+
}
|
|
142
|
+
if (statusWord) {
|
|
143
|
+
const painted = bracket.replace(` · ${statusWord}`, ` · ${ansi.dim}${statusWord}${C.tool}`)
|
|
144
|
+
if (painted !== bracket) bracket = painted
|
|
145
|
+
}
|
|
56
146
|
const bracketWidth = stringWidth(bracket)
|
|
57
147
|
let statePart
|
|
58
|
-
if (
|
|
148
|
+
if (queued) {
|
|
149
|
+
// waiting 块状态区:detail 即原因文本(waiting for: …/dependency cancelled: …);
|
|
150
|
+
// slot 等位 → queued · position N(槽满)。
|
|
151
|
+
statePart = queued.kind === "slot"
|
|
152
|
+
? `queued · position ${queued.position ?? "?"}(槽满等位)`
|
|
153
|
+
: (queued.detail || "queued")
|
|
154
|
+
} else if (sub.approval) statePart = `等待审批: ${sub.approval}`
|
|
155
|
+
else if (sub.awaitingDigest) statePart = "done · awaiting digestion"
|
|
59
156
|
else if (sub.currentTool) statePart = sub.currentTool
|
|
60
157
|
else statePart = "thinking..."
|
|
61
158
|
const argSummary = sub.currentTool && sub.toolArgs?.command
|
|
62
159
|
? ` — ${String(sub.toolArgs.command).replace(/\s+/g, " ").trim().slice(0, 60)}`
|
|
63
160
|
: ""
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
161
|
+
let headText = `${bracket} ${sliceByWidth(statePart + argSummary, Math.max(0, cols - 2 - bracketWidth))}`
|
|
162
|
+
const line = { color: C.tool, _foldToggle: foldKey }
|
|
163
|
+
// §19.5 D-M7 ⏹ + D-M7b ③ 门控:⏹ 只对 async 区块(running && SUBAGENT_ROLES &&
|
|
164
|
+
// sub.async——sync 区块无 ⏹——杜绝"可见但不可中止"误导——用户裁定 B 形态)。
|
|
165
|
+
// done/awaitingDigest/压缩面板(role compress)/consult 无标记(点击只对池内
|
|
166
|
+
// async 子代理有意义):dim 停止标记钉在折叠头右缘**内收一列**(code review
|
|
167
|
+
// 🟡#1——glyph 在 cols−1、最右列留 margin——避免终端最末列点击不可靠/全角字形
|
|
168
|
+
// 顶格被裁;命中区 = col ≥ _stopCol = cols−1——含 glyph 与其右 margin,左邻
|
|
169
|
+
// padding 空格仍走折叠)。整行 ≤ cols:内容先按 cols−3 截断。
|
|
170
|
+
if (!sub.done && sub.async === true && (SUBAGENT_ROLES.includes(sub.role) || sub.role === "advisor")) {
|
|
171
|
+
const cut = sliceByWidth(headText, Math.max(0, cols - 3))
|
|
172
|
+
headText = cut + " ".repeat(Math.max(0, cols - 3 - stringWidth(cut)))
|
|
173
|
+
line.text = `${headText} ${ansi.dim}⏹${ansi.reset} `
|
|
174
|
+
line._stopSub = sub.key
|
|
175
|
+
line._stopCol = Math.max(1, cols - 1)
|
|
176
|
+
} else {
|
|
177
|
+
line.text = headText
|
|
178
|
+
}
|
|
179
|
+
out.push(line)
|
|
69
180
|
if (isExpanded(state, foldKey)) {
|
|
70
181
|
// 展开态:全量活动时间线(per-kind 着色,60% 屏封顶 + 块内滚动——公共组件)。
|
|
71
182
|
const body = renderBlockTimeline(sub.blocks, cols)
|
|
@@ -76,6 +187,8 @@ export function renderSubagentPanel(state, cols, maxRows) {
|
|
|
76
187
|
out.push({ text: `│ ${sliceByWidth(line, cols - 4)}`, color: C.dim })
|
|
77
188
|
}
|
|
78
189
|
}
|
|
190
|
+
// §27 R23:子块段(嵌套子代理——头 + tail2/展开窗口)随外层块渲染(面板与冻结共用)。
|
|
191
|
+
out.push(...renderSubChildSections(state, sub, cols, maxRows))
|
|
79
192
|
}
|
|
80
193
|
return out
|
|
81
194
|
}
|