thincoder 0.12.52 → 0.12.54
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 +36 -0
- package/package.json +1 -1
- package/src/acp.mjs +60 -18
- package/src/advisor/run.mjs +9 -11
- package/src/agent/dispatch.mjs +38 -13
- package/src/agent/setup.mjs +2 -1
- package/src/agent.mjs +34 -0
- package/src/cli/make-agent.mjs +11 -5
- package/src/escape.mjs +43 -8
- package/src/git/checkpoint.mjs +32 -6
- package/src/mcp/helpers.mjs +14 -5
- package/src/mcp/transport-http.mjs +79 -27
- package/src/mcp/transport-stdio.mjs +57 -3
- package/src/mcp/transport-ws.mjs +46 -12
- package/src/mcp.mjs +197 -58
- package/src/prompts/discipline.md +44 -1
- package/src/provider/anthropic.mjs +51 -18
- package/src/provider/core.mjs +163 -36
- package/src/provider/google.mjs +41 -15
- package/src/provider/rate.mjs +5 -0
- package/src/provider/responses.mjs +498 -0
- package/src/provider/retry.mjs +125 -0
- package/src/provider/sse.mjs +58 -24
- package/src/proxy.mjs +36 -6
- package/src/session-migrate.mjs +6 -0
- package/src/session-slots.mjs +361 -0
- package/src/session.mjs +267 -306
- package/src/tools/bash.md +2 -2
- package/src/tools/execute.md +1 -1
- package/src/tools/execute.mjs +3 -3
- package/src/tools/fetch.md +1 -0
- package/src/tools/file.mjs +136 -11
- package/src/tools/git-checkpoint.mjs +143 -0
- package/src/tools/git-ext.mjs +173 -0
- package/src/tools/git.md +21 -6
- package/src/tools/git.mjs +68 -155
- package/src/tools/shared.mjs +5 -3
- package/src/tools/system.mjs +19 -1
- package/src/tools/web.mjs +44 -14
- package/src/tools/websearch.md +3 -1
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/cmd-new.mjs +6 -6
- package/src/tui/cmd-restore.mjs +27 -6
- package/src/tui/cmd-session.mjs +17 -4
- package/src/tui/fold-block.mjs +59 -11
- package/src/tui/index.mjs +59 -67
- package/src/tui/key-handler.mjs +3 -1
- package/src/tui/layout.mjs +81 -25
- package/src/tui/mouse.mjs +86 -8
- package/src/tui/render-conversation.mjs +260 -214
- package/src/tui/render-frame.mjs +22 -6
- package/src/tui/render-loop.mjs +11 -1
- package/src/tui/startup.mjs +1 -1
- package/src/tui/subagent-blocks.mjs +5 -1
- package/src/tui/subagent-panel.mjs +81 -0
- package/src/tui/tool-args.mjs +4 -0
- package/src/tui/tool-events.mjs +1 -1
- package/src/tui/tui-lifecycle.mjs +45 -0
|
@@ -24,37 +24,178 @@ export { _rmpw as _renderMarkdownPreservingWidth }
|
|
|
24
24
|
|
|
25
25
|
let _convCache = { key: "", cols: 0, lines: [] }
|
|
26
26
|
|
|
27
|
+
/** 2026-08-31 懒加载卡顿优化②:段级行体缓存(行对象→conv 行数组)。
|
|
28
|
+
* 与行级 wrapRowsCached 分层:wrap 缓存只省 markdown/换行重算(streaming 行 text 变失效),
|
|
29
|
+
* 段缓存省**整个行体的折叠/展开/窗口化组装**——loadOlder unshift 后尾部 987 行段全命中,
|
|
30
|
+
* rebuild 从 25.7ms → ~8ms 量级。签名由 lineSegSig 集中计算(该段输出的所有决定因素)。 */
|
|
31
|
+
const _lineSegCache = new WeakMap()
|
|
32
|
+
|
|
33
|
+
/** 段签名:段输出的所有决定因素(漏一项 → 缓存失效不全 → 显示 stale)。
|
|
34
|
+
* 返回 { textRef, sig }——text 用**引用比较**(O(1);streaming 同对象 text 变 → 新引用
|
|
35
|
+
* ≠ 旧引用 → 失效),其余短字段(列宽/行数上限/颜色/种类/折行 id/lineId/foldEnabled/
|
|
36
|
+
* 该段折叠展开态+offset/search)拼接——**不**把 l.text 全量拼进 sig(987 行 × KB 级
|
|
37
|
+
* 字符串拼接实测 20ms,等于没优化)。 */
|
|
38
|
+
function lineSegSig(state, l, i, cols, maxRows) {
|
|
39
|
+
const longKey = `long-${l._lineId ?? i}`
|
|
40
|
+
const expanded = state.expandedBlocks?.has(longKey) ? 1 : 0
|
|
41
|
+
const offset = state._foldScroll?.get(longKey) ?? 0
|
|
42
|
+
const searchSig = state.search?.query
|
|
43
|
+
? `${state.search.query}:${state.search.index ?? 0}:${l._searchMatches?.length ?? 0}`
|
|
44
|
+
: ""
|
|
45
|
+
return {
|
|
46
|
+
textRef: l.text,
|
|
47
|
+
sig: [
|
|
48
|
+
cols, maxRows ?? 0, l.color ?? "", l._kind ?? "", l._foldId ?? "",
|
|
49
|
+
l._lineId ?? "", state.foldEnabled === false ? 0 : 1, expanded, offset, searchSig,
|
|
50
|
+
].join("|"),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 普通源行 → conv 行数组(行体;不含前后空行——空行由 buildConvLines 外层逻辑补)。
|
|
55
|
+
* 从 buildConvLines 循环抽出(2026-08-31 段缓存);逻辑与原位置逐字同构。 */
|
|
56
|
+
function buildLineSeg(state, l, i, cols, maxRows) {
|
|
57
|
+
const LONG_FOLD_LINES = 12
|
|
58
|
+
const out = []
|
|
59
|
+
let text = l.text
|
|
60
|
+
|
|
61
|
+
// Apply search highlighting
|
|
62
|
+
if (state.search && state.search.query && l._searchMatches) {
|
|
63
|
+
text = highlightSearchMatches(text, state.search.query, l._searchMatches, state.search.index, state.search.matches, i)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const longKey = `long-${l._lineId ?? i}`
|
|
67
|
+
const isReasoning = l._kind === "thinking" || (l._kind === undefined && l.color === C.reason)
|
|
68
|
+
const foldable = isReasoning || (l._kind === "tool" || (l._kind === undefined && l.color === C.dim) || (l._kind === undefined && l.color !== C.text && l.color !== C.reason))
|
|
69
|
+
const threshold = isReasoning ? 0 : LONG_FOLD_LINES
|
|
70
|
+
const folded = foldable && state.foldEnabled !== false && !state.expandedBlocks?.has(longKey)
|
|
71
|
+
const block = []
|
|
72
|
+
const renderedRows = wrapRowsCached(state, l, text, cols)
|
|
73
|
+
for (const wrapped of renderedRows) {
|
|
74
|
+
block.push({ text: wrapped, color: l.color, _foldId: l._foldId, _src: i })
|
|
75
|
+
}
|
|
76
|
+
if (folded && block.length > threshold) {
|
|
77
|
+
const kind = l.color === C.reason ? "thinking" : l.color === C.dim ? "tool output" : "message"
|
|
78
|
+
out.push(...renderFoldedHead({
|
|
79
|
+
header: foldHintLine(`▶ ${kind} · ${block.length} lines — click to expand`, longKey, i),
|
|
80
|
+
body: block, cols,
|
|
81
|
+
}))
|
|
82
|
+
} else if (foldable && block.length > threshold) {
|
|
83
|
+
if (state.foldEnabled === false) {
|
|
84
|
+
out.push(...block)
|
|
85
|
+
} else {
|
|
86
|
+
if (l.color === C.dim) {
|
|
87
|
+
for (const line of block) line._skipDimFold = true
|
|
88
|
+
}
|
|
89
|
+
out.push(...renderExpandedBlock({ body: block, foldKey: longKey, state, maxRows, cols, label: `${block.length} lines` }))
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
out.push(...block)
|
|
93
|
+
}
|
|
94
|
+
return out
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 工具块段(2026-08-31 段缓存抽出——真实会话 106 个工具块每帧全量 wrap 32ms 的根治)。
|
|
98
|
+
* 返回 conv 行数组(工具块的折叠头+尾/展开窗口+控制行),逻辑与原 L221-258 逐字同构。 */
|
|
99
|
+
function buildToolBlockSeg(state, l, i, cols, maxRows) {
|
|
100
|
+
const b = l._toolBlock
|
|
101
|
+
const foldKey = `tool-${l._lineId ?? i}`
|
|
102
|
+
const out = []
|
|
103
|
+
const status = !b.done
|
|
104
|
+
? "running"
|
|
105
|
+
: `${b.elapsed !== null ? b.elapsed + "ms" : ""}${b.summary ? (b.elapsed !== null ? " · " : "") + sliceByWidth(b.summary, 50) : ""}`.trim() || "done"
|
|
106
|
+
if (isExpanded(state, foldKey)) {
|
|
107
|
+
const body = []
|
|
108
|
+
const pushWrapped = (raw, color) => {
|
|
109
|
+
for (const w of wrapText(raw, cols - 4)) body.push({ text: " " + w, color, _skipDimFold: true })
|
|
110
|
+
}
|
|
111
|
+
for (const jl of b.argsJson) pushWrapped(jl, C.dim)
|
|
112
|
+
for (const ol of b.output) pushWrapped(ol, C.tool)
|
|
113
|
+
if (b.result) for (const rl of b.result) pushWrapped(rl, C.dim)
|
|
114
|
+
out.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: `${b.name}${b.roundTag || ""} ${b.argsSummary}`.trim() }))
|
|
115
|
+
} else {
|
|
116
|
+
const headText = sliceByWidth(
|
|
117
|
+
`❯ ${b.name}${b.roundTag || ""}${b.argsSummary ? " " + b.argsSummary : ""} · ${status}`,
|
|
118
|
+
Math.max(20, cols - 2),
|
|
119
|
+
)
|
|
120
|
+
const body = []
|
|
121
|
+
for (const jl of b.argsJson) for (const w of wrapText(jl, cols - 4)) body.push({ text: w, color: C.dim, _skipDimFold: true })
|
|
122
|
+
for (const ol of b.output.slice(-3)) for (const w of wrapText(ol, cols - 4)) body.push({ text: w, color: C.dim, _skipDimFold: true })
|
|
123
|
+
if (b.result) for (const rl of b.result) for (const w of wrapText(rl, cols - 4)) body.push({ text: w, color: C.dim, _skipDimFold: true })
|
|
124
|
+
out.push(...renderFoldedHead({ header: { text: headText, color: C.tool, _foldToggle: foldKey }, body, cols }))
|
|
125
|
+
}
|
|
126
|
+
return out
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** frozenAdvisor 段(2026-08-31 段缓存抽出)——foldKey 升级为 _lineId 派生(同 long-N 判例)。 */
|
|
130
|
+
function buildFrozenAdvSeg(state, l, i, cols, maxRows) {
|
|
131
|
+
const frozenAdvKey = `advisor-done-${l._lineId ?? i}`
|
|
132
|
+
const out = []
|
|
133
|
+
if (isExpanded(state, frozenAdvKey)) {
|
|
134
|
+
const body = []
|
|
135
|
+
const rendered = renderMathAndMarkdown(sanitizeDisplay(l._frozenAdvisor))
|
|
136
|
+
for (const line of formatTables(rendered, cols - 1)) {
|
|
137
|
+
for (const wrapped of wrapText(line, cols - 1)) {
|
|
138
|
+
body.push({ text: wrapped, color: C.reason, _skipDimFold: true })
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
out.push(...renderExpandedBlock({ body, foldKey: frozenAdvKey, state, maxRows, cols, label: "[advisor · review done]" }))
|
|
142
|
+
} else {
|
|
143
|
+
out.push({
|
|
144
|
+
text: `▶ [advisor · review done] … click to expand`,
|
|
145
|
+
color: C.fold,
|
|
146
|
+
_foldToggle: frozenAdvKey,
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
return out
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
/** 2026-08-31 懒加载卡顿根因修复:行级 wrap/markdown 渲染缓存。
|
|
154
|
+
* buildConvLines 全量重建 O(总行数)——真实 200 条历史 → 987 conv 行 94ms、loadOlder 后
|
|
155
|
+
* 111ms(主线程阻塞卡顿)。行对象 + cols + 加工后 text(含 search 高亮注入)为键,
|
|
156
|
+
* 已有行直接复用——loadOlder/prepend 只算新增行;streaming 行 text 变自动失效;
|
|
157
|
+
* 行对象在 unshift 间引用稳定(行级隔离,无跨 state 串扰)。 */
|
|
158
|
+
const _wrapCache = new WeakMap()
|
|
159
|
+
|
|
160
|
+
function wrapRowsCached(state, line, text, cols) {
|
|
161
|
+
const hit = _wrapCache.get(line)
|
|
162
|
+
if (hit && hit.cols === cols && hit.text === text && hit.color === line.color) return hit.rows
|
|
163
|
+
const renderedText = renderMathAndMarkdown(sanitizeDisplay(text))
|
|
164
|
+
const rows = []
|
|
165
|
+
for (const l of formatTables(renderedText, cols - 1)) {
|
|
166
|
+
for (const wrapped of wrapText(l, cols - 1)) rows.push(wrapped)
|
|
167
|
+
}
|
|
168
|
+
_wrapCache.set(line, { cols, text, color: line.color, rows })
|
|
169
|
+
return rows
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
27
173
|
export function convCacheKey(state, maxRows) {
|
|
28
174
|
const lastLine = state.lines.length > 0 ? state.lines[state.lines.length - 1] : null
|
|
29
175
|
// expandedBlocks participates: expanding/folding a block must invalidate the cache
|
|
30
176
|
const exp = state.expandedBlocks ? [...state.expandedBlocks].sort().join(",") : ""
|
|
177
|
+
// 2026-08-31 块内滚动:_foldScroll(foldKey→offset)参与签名——翻窗必须重新渲染
|
|
178
|
+
const foldScrollSig = state._foldScroll
|
|
179
|
+
? [...state._foldScroll.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1).map(([k, v]) => `${k}:${v}`).join(",")
|
|
180
|
+
: ""
|
|
31
181
|
// Content prefix in the signature: same kind+length with different content
|
|
32
182
|
// would otherwise collide (stale render); 8 chars disambiguate in practice.
|
|
33
183
|
const blocksSig = (state._advisorBlocks ?? []).map((b) => `${b.kind}:${b.text?.length ?? 0}:${String(b.text ?? "").slice(0, 8)}`).join(",")
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
// Frozen blocks ride state.lines ({_frozenSubTask}) — the lines.length part of
|
|
51
|
-
// this key covers their existence; expanding/collapsing one flips expandedBlocks
|
|
52
|
-
// (covered by `exp`). One extra: the last frozen payload's header depends on
|
|
53
|
-
// blocks content which never changes post-freeze — nothing more needed.
|
|
54
|
-
// Same single pass also builds the per-line COLOR-CLASS signature: foldability
|
|
55
|
-
// is decided by color class since main output (C.text) never folds while
|
|
56
|
-
// thinking/dim do (2026-08-30) — two states differing only in line color used
|
|
57
|
-
// to collide on this key and serve a stale cached render.
|
|
184
|
+
// NOTE (§7.2.1): running subagent blocks are NOT part of the conversation
|
|
185
|
+
// anymore — they render in the fixed bottom panel (subagent-panel.mjs,
|
|
186
|
+
// uncached per frame: the 1s ticker refreshes the panel's elapsed display).
|
|
187
|
+
// The old subSig (blockEpoch/turn/elapsed invalidation) is removed: the panel
|
|
188
|
+
// re-renders independently, so child activity must NOT invalidate the
|
|
189
|
+
// conversation cache (that would rebuild the whole conversation per child
|
|
190
|
+
// token — exactly what the 2026-08-31 lazy-load optimization eliminated).
|
|
191
|
+
// Frozen blocks ride state.lines ({_frozenSubTask}) — the lines.length part
|
|
192
|
+
// of this key covers their existence; expanding/collapsing one flips
|
|
193
|
+
// expandedBlocks (covered by `exp`). One extra: the last frozen payload's
|
|
194
|
+
// header depends on blocks content which never changes post-freeze — nothing
|
|
195
|
+
// more needed. Same single pass also builds the per-line COLOR-CLASS
|
|
196
|
+
// signature: foldability is decided by color class since main output (C.text)
|
|
197
|
+
// never folds while thinking/dim do (2026-08-30) — two states differing only
|
|
198
|
+
// in line color used to collide on this key and serve a stale cached render.
|
|
58
199
|
// Tool-block carriers ({_toolBlock}) contribute their BUFFER SIZE signature:
|
|
59
200
|
// output/result arrays mutate in place (streaming appends, result landing),
|
|
60
201
|
// and carrier text is always "" — without this the cache serves a stale block
|
|
@@ -75,7 +216,7 @@ export function convCacheKey(state, maxRows) {
|
|
|
75
216
|
// this the cache would serve the pre-search rows and highlight would never
|
|
76
217
|
// appear (P0-1, 2026-08-30 consult). query+index covers match navigation.
|
|
77
218
|
const searchPart = state.search?.query ? `${state.search.query}:${state.search.index ?? 0}` : ""
|
|
78
|
-
return `${state.lines.length}|${lastLine?.text.length ?? 0}|${state.streaming.length}|${state.reasoning.length}|${blocksSig}|${
|
|
219
|
+
return `${state.lines.length}|${lastLine?.text.length ?? 0}|${state.streaming.length}|${state.reasoning.length}|${blocksSig}|${frozenSig}|${toolSig}|${colorSig}|${state.foldEnabled !== false ? "f" : "u"}|${exp}|${capPart}|${searchPart}|${foldScrollSig}`
|
|
79
220
|
}
|
|
80
221
|
|
|
81
222
|
/** Fold marker line: bold-cyan icon + "click to …" phrase underlined (clickable affordance).
|
|
@@ -107,14 +248,16 @@ function highlightSearchMatches(text, query, matchesInLine, globalCurrentIndex,
|
|
|
107
248
|
}
|
|
108
249
|
|
|
109
250
|
/** Render a FROZEN child activity block carried on a state.lines entry
|
|
110
|
-
* (subagent-blocks.mjs freezeSubTaskLines pushes {_frozenSubTask: sub}).
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
251
|
+
* (subagent-blocks.mjs freezeSubTaskLines pushes {_frozenSubTask: sub}). The
|
|
252
|
+
* running form renders in the fixed bottom panel (§7.2.1 subagent-panel.mjs);
|
|
253
|
+
* the frozen form stays in the stream with the same interaction: folded =
|
|
254
|
+
* `[✓ coder#1 · glm-5.3 · done 45s · turn 12/100] … click to expand` header +
|
|
255
|
+
* tail 3 block lines; expanded = blank + ▼ control + full timeline (60% screen
|
|
256
|
+
* cap via the shared component — capped view ends in a reachable collapse
|
|
257
|
+
* control). Toggle key `sub-${key}` — the SAME key the panel section uses, so
|
|
258
|
+
* fold state carries across the freeze boundary seamlessly (user ruled
|
|
259
|
+
* 2026-08-30: frozen stays clickable — full design interaction, not a
|
|
260
|
+
* dim-lines fallback). */
|
|
118
261
|
function frozenSubTaskLines(state, sub, cols, maxRows) {
|
|
119
262
|
const foldKey = `sub-${sub.key}`
|
|
120
263
|
const elapsed = Math.floor(((sub.doneAt ?? Date.now()) - sub.started) / 1000)
|
|
@@ -187,49 +330,48 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
187
330
|
blankAfter = !nextMain
|
|
188
331
|
}
|
|
189
332
|
// Frozen subagent activity block (§7.2 D4, 2026-08-30): rendered as its own
|
|
190
|
-
// collapsible section
|
|
333
|
+
// collapsible section in the stream (running blocks live in the fixed panel,
|
|
334
|
+
// §7.2.1 — the frozen form keeps the same clickable interaction).
|
|
191
335
|
if (l._frozenSubTask) {
|
|
192
|
-
|
|
336
|
+
// 2026-08-31 段缓存:frozenSubTask 冻结后内容不变——签名含 sub.key + blocks 计数
|
|
337
|
+
const fKey = `sub-${l._frozenSubTask.key}`
|
|
338
|
+
const fSig = [
|
|
339
|
+
cols, maxRows ?? 0, l._frozenSubTask.key, l._frozenSubTask.blocks?.length ?? 0,
|
|
340
|
+
l._frozenSubTask.done ? 1 : 0, state.foldEnabled === false ? 0 : 1,
|
|
341
|
+
state.expandedBlocks?.has(fKey) ? 1 : 0, state._foldScroll?.get(fKey) ?? 0,
|
|
342
|
+
].join("|")
|
|
343
|
+
const fHit = _lineSegCache.get(l)
|
|
344
|
+
if (fHit && fHit.textRef === l._frozenSubTask && fHit.sig === fSig) {
|
|
345
|
+
convLines.push(...fHit.rows)
|
|
346
|
+
} else {
|
|
347
|
+
const rows = frozenSubTaskLines(state, l._frozenSubTask, cols, maxRows)
|
|
348
|
+
_lineSegCache.set(l, { textRef: l._frozenSubTask, sig: fSig, rows })
|
|
349
|
+
convLines.push(...rows)
|
|
350
|
+
}
|
|
193
351
|
continue
|
|
194
352
|
}
|
|
195
353
|
// ONE BLOCK PER TOOL CALL (2026-08-30 user ruling): header = name+args+
|
|
196
354
|
// live status, body = args JSON + streaming output + result. Folded =
|
|
197
355
|
// ▶ name args · status/summary; expanded = 60%-capped body (shared component).
|
|
198
356
|
if (l._toolBlock) {
|
|
357
|
+
// 2026-08-31 段缓存:工具块签名含三缓冲长度+done/elapsed/summary+该块展开态——
|
|
358
|
+
// 流式 append 使 output.length 变 → 失效;real 会话 106 个工具块每帧全量 wrap 实测
|
|
359
|
+
// 32ms(rebuild 40ms 的大头),入缓存后命中只算签名拼接。
|
|
199
360
|
const b = l._toolBlock
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
?
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
for (const w of wrapText(raw, cols - 4)) body.push({ text: " " + w, color, _skipDimFold: true })
|
|
211
|
-
}
|
|
212
|
-
for (const jl of b.argsJson) pushWrapped(jl, C.dim)
|
|
213
|
-
for (const ol of b.output) pushWrapped(ol, C.tool)
|
|
214
|
-
if (b.result) for (const rl of b.result) pushWrapped(rl, C.dim)
|
|
215
|
-
convLines.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: `${b.name}${b.roundTag || ""} ${b.argsSummary}`.trim() }))
|
|
361
|
+
const toolFoldKey = `tool-${l._lineId ?? i}`
|
|
362
|
+
const tSig = [
|
|
363
|
+
cols, maxRows ?? 0, b.argsJson?.length ?? 0, b.output?.length ?? 0, b.result?.length ?? 0,
|
|
364
|
+
b.done ? 1 : 0, b.elapsed ?? "", b.summary ?? "", b.name ?? "", b.roundTag ?? "",
|
|
365
|
+
l._lineId ?? "", state.foldEnabled === false ? 0 : 1,
|
|
366
|
+
state.expandedBlocks?.has(toolFoldKey) ? 1 : 0, state._foldScroll?.get(toolFoldKey) ?? 0,
|
|
367
|
+
].join("|")
|
|
368
|
+
const tHit = _lineSegCache.get(l)
|
|
369
|
+
if (tHit && tHit.textRef === b && tHit.sig === tSig) {
|
|
370
|
+
convLines.push(...tHit.rows)
|
|
216
371
|
} else {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
// (the "code breaks the input box border" report, 2026-08-30).
|
|
221
|
-
const headText = sliceByWidth(
|
|
222
|
-
`❯ ${b.name}${b.roundTag || ""}${b.argsSummary ? " " + b.argsSummary : ""} · ${status}`,
|
|
223
|
-
Math.max(20, cols - 2),
|
|
224
|
-
)
|
|
225
|
-
const body = []
|
|
226
|
-
for (const jl of b.argsJson) for (const w of wrapText(jl, cols - 4)) body.push({ text: w, color: C.dim, _skipDimFold: true })
|
|
227
|
-
for (const ol of b.output.slice(-3)) for (const w of wrapText(ol, cols - 4)) body.push({ text: w, color: C.dim, _skipDimFold: true })
|
|
228
|
-
// Result lines join the tail pool too — restore carrier has no output
|
|
229
|
-
// rows, so without this its folded tail showed only args JSON and the
|
|
230
|
-
// result vanished from the folded view (parity bug, 2026-08-30).
|
|
231
|
-
if (b.result) for (const rl of b.result) for (const w of wrapText(rl, cols - 4)) body.push({ text: w, color: C.dim, _skipDimFold: true })
|
|
232
|
-
convLines.push(...renderFoldedHead({ header: { text: headText, color: C.tool, _foldToggle: foldKey }, body, cols }))
|
|
372
|
+
const rows = buildToolBlockSeg(state, l, i, cols, maxRows)
|
|
373
|
+
_lineSegCache.set(l, { textRef: b, sig: tSig, rows })
|
|
374
|
+
convLines.push(...rows)
|
|
233
375
|
}
|
|
234
376
|
continue
|
|
235
377
|
}
|
|
@@ -238,93 +380,37 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
238
380
|
// rendered, no gutter — review history convention kept from the flat era),
|
|
239
381
|
// 60% cap via the shared component.
|
|
240
382
|
if (l._frozenAdvisor) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
383
|
+
// 2026-08-31 段缓存:frozenAdvisor 文本冻结不变——签名含文本长度 + 展开态
|
|
384
|
+
// 注:foldKey 由 advisor-done-${i}(位置键)升级为 advisor-done-${_lineId ?? i}
|
|
385
|
+
// (同 long-N 判例——loadOlder unshift 后不重绑)
|
|
386
|
+
const frozenAdvKey = `advisor-done-${l._lineId ?? i}`
|
|
387
|
+
const aSig = [
|
|
388
|
+
cols, maxRows ?? 0, (l._frozenAdvisor ?? "").length,
|
|
389
|
+
state.foldEnabled === false ? 0 : 1,
|
|
390
|
+
state.expandedBlocks?.has(frozenAdvKey) ? 1 : 0, state._foldScroll?.get(frozenAdvKey) ?? 0,
|
|
391
|
+
].join("|")
|
|
392
|
+
const aHit = _lineSegCache.get(l)
|
|
393
|
+
if (aHit && aHit.textRef === l._frozenAdvisor && aHit.sig === aSig) {
|
|
394
|
+
convLines.push(...aHit.rows)
|
|
251
395
|
} else {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
_foldToggle: frozenAdvKey,
|
|
256
|
-
})
|
|
396
|
+
const rows = buildFrozenAdvSeg(state, l, i, cols, maxRows)
|
|
397
|
+
_lineSegCache.set(l, { textRef: l._frozenAdvisor, sig: aSig, rows })
|
|
398
|
+
convLines.push(...rows)
|
|
257
399
|
}
|
|
258
400
|
continue
|
|
259
401
|
}
|
|
260
|
-
let text = l.text
|
|
261
402
|
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// not by expanding; a folded core answer hid the actual result behind a
|
|
270
|
-
// click. Foldable subjects narrow to THINKING (C.reason) and dim tool
|
|
271
|
-
// summaries — the auxiliary streams. (This re-enacts the pre-0.12.7 rule
|
|
272
|
-
// for main output only; the 0.12.7 "revert" had reopened folding for it.)
|
|
273
|
-
// Keyed by the source-line index (`long-${i}`) so the toggle survives
|
|
274
|
-
// re-renders.
|
|
275
|
-
const longKey = `long-${i}`
|
|
276
|
-
// Single source of truth: the producer stamps _kind ("thinking" / "text" /
|
|
277
|
-
// "tool") — buildConvLines READS the stamp instead of GUESSING from color.
|
|
278
|
-
// Three producers (live flushStream / restored historyToLines / injected
|
|
279
|
-
// lines) now emit the identical grammar; the renderer is one place.
|
|
280
|
-
// Fallback: unstamped lines keep the legacy color-based inference (defensive
|
|
281
|
-
// for any path this refactor missed — empty until proven otherwise).
|
|
282
|
-
const isReasoning = l._kind === "thinking" || (l._kind === undefined && l.color === C.reason)
|
|
283
|
-
// Foldable classes: thinking (ALWAYS — threshold 0) and dim auxiliaries.
|
|
284
|
-
// "text" (main output / user messages) NEVER folds.
|
|
285
|
-
const foldable = isReasoning || (l._kind === "tool" || (l._kind === undefined && l.color === C.dim) || (l._kind === undefined && l.color !== C.text && l.color !== C.reason))
|
|
286
|
-
const threshold = isReasoning ? 0 : LONG_FOLD_LINES
|
|
287
|
-
const folded = foldable && state.foldEnabled !== false && !state.expandedBlocks?.has(longKey)
|
|
288
|
-
const block = []
|
|
289
|
-
// Lightweight markdown display (IK5VW3): render BEFORE measuring — the
|
|
290
|
-
// table column math (formatTables) and wrapping must see the RENDERED
|
|
291
|
-
// text (ANSI consumes zero display width; the width functions are
|
|
292
|
-
// ANSI-aware). Rendering after wrapping measured raw markdown
|
|
293
|
-
// (`**bold**` = 8) against displayed text (4) and sliced markers
|
|
294
|
-
// mid-sequence — the table misalignment the user kept reporting.
|
|
295
|
-
const renderedText = renderMathAndMarkdown(sanitizeDisplay(text))
|
|
296
|
-
for (const line of formatTables(renderedText, cols - 1)) {
|
|
297
|
-
for (const wrapped of wrapText(line, cols - 1)) {
|
|
298
|
-
block.push({ text: wrapped, color: l.color, _foldId: l._foldId, _src: i })
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
if (folded && block.length > threshold) {
|
|
302
|
-
// FOLDED — unified form (fold-block.mjs renderFoldedHead, 2026-08-30 user
|
|
303
|
-
// ruling): named identity header + last 3 lines. Replaces the legacy
|
|
304
|
-
// [first 4, anonymous ▶ at the ellipsis, last] whose orphaned-looking
|
|
305
|
-
// "… N more lines" segment confused the scrollback.
|
|
306
|
-
const kind = l.color === C.reason ? "thinking" : l.color === C.dim ? "tool output" : "message"
|
|
307
|
-
convLines.push(...renderFoldedHead({
|
|
308
|
-
header: foldHintLine(`▶ ${kind} · ${block.length} lines — click to expand`, longKey, i),
|
|
309
|
-
body: block, cols,
|
|
310
|
-
}))
|
|
311
|
-
} else if (foldable && block.length > threshold) {
|
|
312
|
-
if (state.foldEnabled === false) {
|
|
313
|
-
// Folding fully off — content already fully visible; a "click to
|
|
314
|
-
// collapse" hint would be misleading (toggling has no effect).
|
|
315
|
-
convLines.push(...block)
|
|
316
|
-
} else {
|
|
317
|
-
// EXPANDED thinking/dim long block via the shared component: blank + ▼
|
|
318
|
-
// control at the HEAD, content, 60% cap with a bottom collapse control.
|
|
319
|
-
// DIM blocks must not re-trigger the consecutive-dim folding below
|
|
320
|
-
// (folding stacked on folding — reported regression).
|
|
321
|
-
if (l.color === C.dim) {
|
|
322
|
-
for (const line of block) line._skipDimFold = true
|
|
323
|
-
}
|
|
324
|
-
convLines.push(...renderExpandedBlock({ body: block, foldKey: longKey, state, maxRows, cols, label: `${block.length} lines` }))
|
|
325
|
-
}
|
|
403
|
+
// ── 普通源行段(2026-08-31 懒加载卡顿优化②:段级缓存——行体 WeakMap 按行对象
|
|
404
|
+
// 缓存,签名含该段所有决定因素;unshift/loadOlder 后尾部段全命中,只算新增行。
|
|
405
|
+
// toggle/翻窗/收起只失效该块段,其它段照样命中。行为不变性是硬约束。
|
|
406
|
+
const seg = lineSegSig(state, l, i, cols, maxRows)
|
|
407
|
+
const hit = _lineSegCache.get(l)
|
|
408
|
+
if (hit && hit.textRef === seg.textRef && hit.sig === seg.sig) {
|
|
409
|
+
convLines.push(...hit.rows)
|
|
326
410
|
} else {
|
|
327
|
-
|
|
411
|
+
const rows = buildLineSeg(state, l, i, cols, maxRows)
|
|
412
|
+
_lineSegCache.set(l, { textRef: seg.textRef, sig: seg.sig, rows })
|
|
413
|
+
convLines.push(...rows)
|
|
328
414
|
}
|
|
329
415
|
// Trailing blank after a main-output segment (user request 2026-08-30) —
|
|
330
416
|
// landed after the segment's rendered content.
|
|
@@ -333,65 +419,14 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
333
419
|
blankAfter = false
|
|
334
420
|
}
|
|
335
421
|
}
|
|
336
|
-
// ── Subagent activity blocks (§7.2
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
const runningSubs = Object.values(state.subTasks ?? {}).filter((s) => !s.done)
|
|
345
|
-
if (runningSubs.length > 0) {
|
|
346
|
-
// Divider between the conversation body and the running-subagent band
|
|
347
|
-
// (user request 2026-08-30, mirroring the task-panel divider in
|
|
348
|
-
// render-frame renderTodo). Only when at least one block actually renders —
|
|
349
|
-
// done children are frozen into state.lines above, so a divider for an
|
|
350
|
-
// empty band would hang over the section boundary.
|
|
351
|
-
// Unconditional divider (task-panel style): the preceding main-output
|
|
352
|
-
// trailing blank is breathing room, the divider is the section boundary —
|
|
353
|
-
// both belong. Only dedupe against another divider (idempotent re-render).
|
|
354
|
-
if (convLines.at(-1)?.color !== C.dim || !convLines.at(-1)?.text?.startsWith("─")) {
|
|
355
|
-
convLines.push({ text: "─".repeat(Math.max(1, cols - 1)), color: C.dim, _skipDimFold: true })
|
|
356
|
-
}
|
|
357
|
-
for (const sub of runningSubs) {
|
|
358
|
-
// Done children never reach this loop (filtered above): they are frozen
|
|
359
|
-
// into state.lines by subagent-blocks.mjs freezeSubTaskLines and removed
|
|
360
|
-
// from subTasks — a restored leftover would otherwise pin at the tail.
|
|
361
|
-
const foldKey = `sub-${sub.key}`
|
|
362
|
-
// Header summary: `[▶ coder#1 · glm-5.3 · 45s · turn 12/100] bash — npm test`
|
|
363
|
-
const icon = sub.approval ? "⏸" : "▶"
|
|
364
|
-
const elapsed = Math.floor(((sub.done ? sub.doneAt : Date.now()) - sub.started) / 1000)
|
|
365
|
-
const modelPart = sub.model ? ` · ${sub.model}` : ""
|
|
366
|
-
const turnPart = sub.maxTurns > 0 ? ` · turn ${sub.turn}/${sub.maxTurns}` : ""
|
|
367
|
-
let statePart
|
|
368
|
-
if (sub.approval) statePart = `等待审批: ${sub.approval}`
|
|
369
|
-
else if (sub.done) {
|
|
370
|
-
statePart = `done ${elapsed}s${sub.lastError ? ` — ${sub.lastError}` : ""}`
|
|
371
|
-
} else if (sub.currentTool) statePart = sub.currentTool
|
|
372
|
-
else statePart = "thinking..."
|
|
373
|
-
const argSummary = sub.currentTool && sub.toolArgs?.command
|
|
374
|
-
? ` — ${String(sub.toolArgs.command).replace(/\s+/g, " ").trim().slice(0, 60)}`
|
|
375
|
-
: ""
|
|
376
|
-
convLines.push({
|
|
377
|
-
text: `[${icon} ${sub.key}${modelPart} · ${elapsed}s${turnPart}] ${sliceByWidth(statePart + argSummary, Math.max(20, cols - 30))}`,
|
|
378
|
-
color: sub.done ? C.dim : C.tool,
|
|
379
|
-
_foldToggle: foldKey,
|
|
380
|
-
})
|
|
381
|
-
if (isExpanded(state, foldKey)) {
|
|
382
|
-
// Full activity timeline via the shared component (per-kind colors,
|
|
383
|
-
// 60% screen cap — the header control may sit above the viewport once
|
|
384
|
-
// expanded, the capped bottom control stays reachable).
|
|
385
|
-
const body = renderBlockTimeline(sub.blocks, cols)
|
|
386
|
-
convLines.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: "subagent activity" }))
|
|
387
|
-
} else {
|
|
388
|
-
// Folded: tail 3 non-empty block lines (most recent activity), dim.
|
|
389
|
-
for (const line of foldTailLines(sub.blocks)) {
|
|
390
|
-
convLines.push({ text: `│ ${sliceByWidth(line, cols - 4)}`, color: C.dim })
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
422
|
+
// ── Subagent activity blocks (§7.2.1 D2) — RUNNING blocks MOVED to the fixed
|
|
423
|
+
// bottom panel (subagent-panel.mjs renderSubagentPanel, layout.mjs precomputes
|
|
424
|
+
// the panel height; render-frame puts it between conversation and todo).
|
|
425
|
+
// buildConvLines no longer renders running children: on completion
|
|
426
|
+
// onToolResult freezes the block into state.lines (subagent-blocks.mjs
|
|
427
|
+
// freezeSubTaskLines) and it scrolls away with the conversation (D4 现状).
|
|
428
|
+
// Frozen blocks render above via the _frozenSubTask branch (frozenSubTaskLines).
|
|
429
|
+
|
|
395
430
|
if (state.reasoning) {
|
|
396
431
|
// Live thinking streams INSIDE the unified box (user ruling 2026-08-30:
|
|
397
432
|
// "思考过程中为什么不是直接进这个框" — the flat tail render was a
|
|
@@ -463,7 +498,6 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
463
498
|
}
|
|
464
499
|
// Fold long blocks (> 8 consecutive dim lines)
|
|
465
500
|
const FOLD_LINES = 8
|
|
466
|
-
let foldCounter = 0
|
|
467
501
|
const folded = []
|
|
468
502
|
let i = 0
|
|
469
503
|
while (i < convLines.length) {
|
|
@@ -475,7 +509,10 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
475
509
|
// Expanded long-fold blocks are exempt — otherwise folding stacks on folding
|
|
476
510
|
const hasExpandedLong = convLines.slice(i, j).some((l) => l._skipDimFold)
|
|
477
511
|
if (blockLen > FOLD_LINES && !hasExpandedLong) {
|
|
478
|
-
|
|
512
|
+
// 2026-08-31 会诊三家共识:fold-N 计数器键在 loadOlder/上游 dim 块增减时重绑——
|
|
513
|
+
// 用首行 _lineId 身份化(连续 dim 块首行即稳定锚);无 _lineId 时退 fold-i(防御)
|
|
514
|
+
const keySource = convLines[i]._lineId ?? i
|
|
515
|
+
const foldKey = `fold-${keySource}`
|
|
479
516
|
if (state.foldEnabled !== false && !state.expandedBlocks?.has(foldKey)) {
|
|
480
517
|
// FOLDED — unified named-header + last-3 form (same ruling as the
|
|
481
518
|
// long-message fold above).
|
|
@@ -514,13 +551,22 @@ export { buildConvLines }
|
|
|
514
551
|
|
|
515
552
|
export function renderConversation(state, cols, visibleH, scroll, maxRows) {
|
|
516
553
|
const convLines = buildConvLines(state, cols, maxRows)
|
|
517
|
-
const
|
|
518
|
-
const
|
|
519
|
-
const end = convLines.length - clamped
|
|
520
|
-
const visible = convLines.slice(Math.max(0, end - visibleH), end)
|
|
521
|
-
const pad = visibleH - visible.length
|
|
554
|
+
const { start, end, pad } = convViewport(convLines.length, visibleH, scroll)
|
|
555
|
+
const visible = convLines.slice(start, end)
|
|
522
556
|
const out = []
|
|
523
557
|
for (let p = 0; p < pad; p++) out.push("")
|
|
524
558
|
for (const l of visible) out.push(`${l.color ?? ""}${l.text}${ansi.reset}`)
|
|
525
559
|
return out
|
|
526
560
|
}
|
|
561
|
+
|
|
562
|
+
/** 视口数学单源(2026-08-31 会诊 kimi 缺陷 1——convGlobalIndex 未减 pad:
|
|
563
|
+
* 短会话顶部补 pad 空行后命中整体偏移,点击/滚轮落空或错行)。
|
|
564
|
+
* 返回 { start, end, pad }——renderConversation 与鼠标命中测试共用。 */
|
|
565
|
+
export function convViewport(convLen, convH, scroll) {
|
|
566
|
+
const maxScroll = Math.max(0, convLen - convH)
|
|
567
|
+
const clamped = Math.min(scroll, maxScroll)
|
|
568
|
+
const end = Math.max(0, convLen - clamped)
|
|
569
|
+
const start = Math.max(0, end - convH)
|
|
570
|
+
const pad = Math.max(0, convH - (end - start))
|
|
571
|
+
return { start, end, pad }
|
|
572
|
+
}
|
package/src/tui/render-frame.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { ansi, C, ESC } from "./ansi.mjs"
|
|
|
10
10
|
import { convCacheKey, renderConversation, countConvLines } from "./render-conversation.mjs"
|
|
11
11
|
import { sliceByWidth, stringWidth } from "./render.mjs"
|
|
12
12
|
import { specForModel } from "../config.mjs"
|
|
13
|
-
import { computeLayout } from "./layout.mjs"
|
|
13
|
+
import { computeLayout, subagentVisibleLines } from "./layout.mjs"
|
|
14
14
|
import { basename } from "node:path"
|
|
15
15
|
import { readFileSync } from "node:fs"
|
|
16
16
|
|
|
@@ -89,8 +89,10 @@ export function renderPicker(state, cols, panel, overlay) {
|
|
|
89
89
|
// 过滤提示:无filter时显示 "type to filter",有filter时显示输入内容
|
|
90
90
|
const filterHint = p ? (p.filter ? `│ ${p.filter}` : "│ type to filter") : ""
|
|
91
91
|
const rawLeft = p ? ` ❯ ${p.title} ${filterHint} ` : " ❯ Setup "
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
// 2026-08-31 会诊(advisor round1 🔵):标题行与条目行一致留 8 格余量——Ambiguous 字符
|
|
93
|
+
// (❯/│ 等)在 CJK 终端渲染 2 格,余量不足时右侧 n/m 位置指示会被截。
|
|
94
|
+
const left = sliceByWidth(rawLeft, Math.max(1, cols - 8 - stringWidth(right)))
|
|
95
|
+
const titlePad = " ".repeat(Math.max(1, cols - 8 - stringWidth(left) - stringWidth(right)))
|
|
94
96
|
out.push(`${ansi.bold}${C.tool}${left}${ansi.reset}${ansi.dim}${titlePad}${right}${ansi.reset}`)
|
|
95
97
|
const hasMoreAbove = start > 0
|
|
96
98
|
const hasMoreBelow = start + winH < total
|
|
@@ -100,10 +102,14 @@ export function renderPicker(state, cols, panel, overlay) {
|
|
|
100
102
|
const moreAbove = i === 0 && hasMoreAbove
|
|
101
103
|
const moreBelow = i === shown.length - 1 && hasMoreBelow
|
|
102
104
|
const ind = moreAbove && moreBelow ? "↑↓ more" : moreAbove ? "↑ more" : moreBelow ? "↓ more" : ""
|
|
103
|
-
|
|
105
|
+
// 2026-08-31 会诊:右边距留 8 格余量——Ambiguous 宽度字符(│/—/●/▸/…/↑↓ 等)在中文
|
|
106
|
+
// locale 终端渲染 2 格而 stringWidth 按 1 格算,/session 行最坏带 ~7 个(▸ 前缀 +
|
|
107
|
+
// │×3 + … + — + ●)→ 行宽低估 7+ 格 → 实际超宽 → 物理 wrap → 残影;8 格余量覆盖
|
|
108
|
+
// 常见低估,最坏残余由 DECAWM 关闭硬截断兜底(见 render-loop)。
|
|
109
|
+
const maxW = cols - 8 - (ind ? stringWidth(ind) + 1 : 0)
|
|
104
110
|
// 超宽行截断并加省略号
|
|
105
111
|
const text = stringWidth(l.text) > maxW ? sliceByWidth(l.text, Math.max(0, maxW - 1)) + "…" : l.text
|
|
106
|
-
const pad = ind ? " ".repeat(Math.max(1, cols -
|
|
112
|
+
const pad = ind ? " ".repeat(Math.max(1, cols - 8 - stringWidth(text) - stringWidth(ind))) : ""
|
|
107
113
|
out.push(`${l.color}${text}${ansi.reset}${ind ? `${ansi.dim}${pad}${ind}${ansi.reset}` : ""}`)
|
|
108
114
|
}
|
|
109
115
|
for (let i = shown.length; i < winH; i++) out.push("")
|
|
@@ -221,7 +227,7 @@ export function renderRows(state, agent, opts) {
|
|
|
221
227
|
const slashCommands = opts.slashCommands ?? []
|
|
222
228
|
|
|
223
229
|
const layout = computeLayout(state, { cols, rows })
|
|
224
|
-
const { W, panels, inputLayout, inputOffset, boxLines, visibleTasks, permPreviewLines, overlay } = layout
|
|
230
|
+
const { W, panels, inputLayout, inputOffset, boxLines, visibleTasks, permPreviewLines, overlay, subagentLines } = layout
|
|
225
231
|
|
|
226
232
|
const screen = new Array(rows).fill("")
|
|
227
233
|
const put = (y, lines) => {
|
|
@@ -232,6 +238,16 @@ export function renderRows(state, agent, opts) {
|
|
|
232
238
|
|
|
233
239
|
put(panels.header.y, [renderHeader(agent, cols)])
|
|
234
240
|
put(panels.conversation.y, renderConversation(state, cols, panels.conversation.h, state.scroll, rows))
|
|
241
|
+
// §7.2.1 D2: running-subagent fixed panel (between conversation and todo) —
|
|
242
|
+
// lines precomputed by layout (subagent-panel.mjs, neutral module), put
|
|
243
|
+
// directly (no double render); absent when no block is running (F6). Lines
|
|
244
|
+
// are objects ({text, color, _foldToggle/_foldBlock…} — mouse hit-testing
|
|
245
|
+
// reads the same array), converted to ANSI strings here like renderTodo.
|
|
246
|
+
if (panels.subagent) {
|
|
247
|
+
// 评审 #4:部分压缩(h < 全长)时保底截断——分隔线 + 末尾区块行(最新活动
|
|
248
|
+
// 优先),与 mouse 命中映射同一几何契约(subagentVisibleLines / subagentLineIndex)。
|
|
249
|
+
put(panels.subagent.y, subagentVisibleLines(subagentLines, panels.subagent.h).map((l) => `${l.color ?? ""}${l.text}${ansi.reset}`))
|
|
250
|
+
}
|
|
235
251
|
if (panels.todo) put(panels.todo.y, renderTodo(visibleTasks, cols))
|
|
236
252
|
if (panels.picker) put(panels.picker.y, renderPicker(state, cols, panels.picker, overlay))
|
|
237
253
|
if (panels.permission) put(panels.permission.y, renderPermission(permPreviewLines))
|