thincoder 0.12.52 → 0.12.53
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 +19 -0
- package/package.json +1 -1
- package/src/advisor/run.mjs +9 -11
- package/src/agent/dispatch.mjs +38 -13
- package/src/agent.mjs +34 -0
- package/src/cli/make-agent.mjs +11 -5
- 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 +43 -0
- package/src/provider/anthropic.mjs +51 -18
- package/src/provider/core.mjs +121 -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/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.md +4 -2
- package/src/tools/git.mjs +35 -8
- 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/fold-block.mjs +59 -11
- package/src/tui/index.mjs +56 -45
- package/src/tui/key-handler.mjs +3 -1
- package/src/tui/mouse.mjs +46 -7
- package/src/tui/render-conversation.mjs +225 -122
- package/src/tui/render-loop.mjs +10 -0
- package/src/tui/startup.mjs +1 -1
- package/src/tui/subagent-blocks.mjs +5 -1
- package/src/tui/tool-args.mjs +4 -0
|
@@ -24,10 +24,160 @@ 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(",")
|
|
@@ -75,7 +225,7 @@ export function convCacheKey(state, maxRows) {
|
|
|
75
225
|
// this the cache would serve the pre-search rows and highlight would never
|
|
76
226
|
// appear (P0-1, 2026-08-30 consult). query+index covers match navigation.
|
|
77
227
|
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}|${subSig}|${frozenSig}|${toolSig}|${colorSig}|${state.foldEnabled !== false ? "f" : "u"}|${exp}|${capPart}|${searchPart}`
|
|
228
|
+
return `${state.lines.length}|${lastLine?.text.length ?? 0}|${state.streaming.length}|${state.reasoning.length}|${blocksSig}|${subSig}|${frozenSig}|${toolSig}|${colorSig}|${state.foldEnabled !== false ? "f" : "u"}|${exp}|${capPart}|${searchPart}|${foldScrollSig}`
|
|
79
229
|
}
|
|
80
230
|
|
|
81
231
|
/** Fold marker line: bold-cyan icon + "click to …" phrase underlined (clickable affordance).
|
|
@@ -189,47 +339,45 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
189
339
|
// Frozen subagent activity block (§7.2 D4, 2026-08-30): rendered as its own
|
|
190
340
|
// collapsible section — clickable expand/collapse like the running block.
|
|
191
341
|
if (l._frozenSubTask) {
|
|
192
|
-
|
|
342
|
+
// 2026-08-31 段缓存:frozenSubTask 冻结后内容不变——签名含 sub.key + blocks 计数
|
|
343
|
+
const fKey = `sub-${l._frozenSubTask.key}`
|
|
344
|
+
const fSig = [
|
|
345
|
+
cols, maxRows ?? 0, l._frozenSubTask.key, l._frozenSubTask.blocks?.length ?? 0,
|
|
346
|
+
l._frozenSubTask.done ? 1 : 0, state.foldEnabled === false ? 0 : 1,
|
|
347
|
+
state.expandedBlocks?.has(fKey) ? 1 : 0, state._foldScroll?.get(fKey) ?? 0,
|
|
348
|
+
].join("|")
|
|
349
|
+
const fHit = _lineSegCache.get(l)
|
|
350
|
+
if (fHit && fHit.textRef === l._frozenSubTask && fHit.sig === fSig) {
|
|
351
|
+
convLines.push(...fHit.rows)
|
|
352
|
+
} else {
|
|
353
|
+
const rows = frozenSubTaskLines(state, l._frozenSubTask, cols, maxRows)
|
|
354
|
+
_lineSegCache.set(l, { textRef: l._frozenSubTask, sig: fSig, rows })
|
|
355
|
+
convLines.push(...rows)
|
|
356
|
+
}
|
|
193
357
|
continue
|
|
194
358
|
}
|
|
195
359
|
// ONE BLOCK PER TOOL CALL (2026-08-30 user ruling): header = name+args+
|
|
196
360
|
// live status, body = args JSON + streaming output + result. Folded =
|
|
197
361
|
// ▶ name args · status/summary; expanded = 60%-capped body (shared component).
|
|
198
362
|
if (l._toolBlock) {
|
|
363
|
+
// 2026-08-31 段缓存:工具块签名含三缓冲长度+done/elapsed/summary+该块展开态——
|
|
364
|
+
// 流式 append 使 output.length 变 → 失效;real 会话 106 个工具块每帧全量 wrap 实测
|
|
365
|
+
// 32ms(rebuild 40ms 的大头),入缓存后命中只算签名拼接。
|
|
199
366
|
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() }))
|
|
367
|
+
const toolFoldKey = `tool-${l._lineId ?? i}`
|
|
368
|
+
const tSig = [
|
|
369
|
+
cols, maxRows ?? 0, b.argsJson?.length ?? 0, b.output?.length ?? 0, b.result?.length ?? 0,
|
|
370
|
+
b.done ? 1 : 0, b.elapsed ?? "", b.summary ?? "", b.name ?? "", b.roundTag ?? "",
|
|
371
|
+
l._lineId ?? "", state.foldEnabled === false ? 0 : 1,
|
|
372
|
+
state.expandedBlocks?.has(toolFoldKey) ? 1 : 0, state._foldScroll?.get(toolFoldKey) ?? 0,
|
|
373
|
+
].join("|")
|
|
374
|
+
const tHit = _lineSegCache.get(l)
|
|
375
|
+
if (tHit && tHit.textRef === b && tHit.sig === tSig) {
|
|
376
|
+
convLines.push(...tHit.rows)
|
|
216
377
|
} 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 }))
|
|
378
|
+
const rows = buildToolBlockSeg(state, l, i, cols, maxRows)
|
|
379
|
+
_lineSegCache.set(l, { textRef: b, sig: tSig, rows })
|
|
380
|
+
convLines.push(...rows)
|
|
233
381
|
}
|
|
234
382
|
continue
|
|
235
383
|
}
|
|
@@ -238,93 +386,37 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
238
386
|
// rendered, no gutter — review history convention kept from the flat era),
|
|
239
387
|
// 60% cap via the shared component.
|
|
240
388
|
if (l._frozenAdvisor) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
389
|
+
// 2026-08-31 段缓存:frozenAdvisor 文本冻结不变——签名含文本长度 + 展开态
|
|
390
|
+
// 注:foldKey 由 advisor-done-${i}(位置键)升级为 advisor-done-${_lineId ?? i}
|
|
391
|
+
// (同 long-N 判例——loadOlder unshift 后不重绑)
|
|
392
|
+
const frozenAdvKey = `advisor-done-${l._lineId ?? i}`
|
|
393
|
+
const aSig = [
|
|
394
|
+
cols, maxRows ?? 0, (l._frozenAdvisor ?? "").length,
|
|
395
|
+
state.foldEnabled === false ? 0 : 1,
|
|
396
|
+
state.expandedBlocks?.has(frozenAdvKey) ? 1 : 0, state._foldScroll?.get(frozenAdvKey) ?? 0,
|
|
397
|
+
].join("|")
|
|
398
|
+
const aHit = _lineSegCache.get(l)
|
|
399
|
+
if (aHit && aHit.textRef === l._frozenAdvisor && aHit.sig === aSig) {
|
|
400
|
+
convLines.push(...aHit.rows)
|
|
251
401
|
} else {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
_foldToggle: frozenAdvKey,
|
|
256
|
-
})
|
|
402
|
+
const rows = buildFrozenAdvSeg(state, l, i, cols, maxRows)
|
|
403
|
+
_lineSegCache.set(l, { textRef: l._frozenAdvisor, sig: aSig, rows })
|
|
404
|
+
convLines.push(...rows)
|
|
257
405
|
}
|
|
258
406
|
continue
|
|
259
407
|
}
|
|
260
|
-
let text = l.text
|
|
261
|
-
|
|
262
|
-
// Apply search highlighting
|
|
263
|
-
if (state.search && state.search.query && l._searchMatches) {
|
|
264
|
-
text = highlightSearchMatches(text, state.search.query, l._searchMatches, state.search.index, state.search.matches, i)
|
|
265
|
-
}
|
|
266
408
|
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
}
|
|
409
|
+
// ── 普通源行段(2026-08-31 懒加载卡顿优化②:段级缓存——行体 WeakMap 按行对象
|
|
410
|
+
// 缓存,签名含该段所有决定因素;unshift/loadOlder 后尾部段全命中,只算新增行。
|
|
411
|
+
// toggle/翻窗/收起只失效该块段,其它段照样命中。行为不变性是硬约束。
|
|
412
|
+
const seg = lineSegSig(state, l, i, cols, maxRows)
|
|
413
|
+
const hit = _lineSegCache.get(l)
|
|
414
|
+
if (hit && hit.textRef === seg.textRef && hit.sig === seg.sig) {
|
|
415
|
+
convLines.push(...hit.rows)
|
|
326
416
|
} else {
|
|
327
|
-
|
|
417
|
+
const rows = buildLineSeg(state, l, i, cols, maxRows)
|
|
418
|
+
_lineSegCache.set(l, { textRef: seg.textRef, sig: seg.sig, rows })
|
|
419
|
+
convLines.push(...rows)
|
|
328
420
|
}
|
|
329
421
|
// Trailing blank after a main-output segment (user request 2026-08-30) —
|
|
330
422
|
// landed after the segment's rendered content.
|
|
@@ -463,7 +555,6 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
463
555
|
}
|
|
464
556
|
// Fold long blocks (> 8 consecutive dim lines)
|
|
465
557
|
const FOLD_LINES = 8
|
|
466
|
-
let foldCounter = 0
|
|
467
558
|
const folded = []
|
|
468
559
|
let i = 0
|
|
469
560
|
while (i < convLines.length) {
|
|
@@ -475,7 +566,10 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
475
566
|
// Expanded long-fold blocks are exempt — otherwise folding stacks on folding
|
|
476
567
|
const hasExpandedLong = convLines.slice(i, j).some((l) => l._skipDimFold)
|
|
477
568
|
if (blockLen > FOLD_LINES && !hasExpandedLong) {
|
|
478
|
-
|
|
569
|
+
// 2026-08-31 会诊三家共识:fold-N 计数器键在 loadOlder/上游 dim 块增减时重绑——
|
|
570
|
+
// 用首行 _lineId 身份化(连续 dim 块首行即稳定锚);无 _lineId 时退 fold-i(防御)
|
|
571
|
+
const keySource = convLines[i]._lineId ?? i
|
|
572
|
+
const foldKey = `fold-${keySource}`
|
|
479
573
|
if (state.foldEnabled !== false && !state.expandedBlocks?.has(foldKey)) {
|
|
480
574
|
// FOLDED — unified named-header + last-3 form (same ruling as the
|
|
481
575
|
// long-message fold above).
|
|
@@ -514,13 +608,22 @@ export { buildConvLines }
|
|
|
514
608
|
|
|
515
609
|
export function renderConversation(state, cols, visibleH, scroll, maxRows) {
|
|
516
610
|
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
|
|
611
|
+
const { start, end, pad } = convViewport(convLines.length, visibleH, scroll)
|
|
612
|
+
const visible = convLines.slice(start, end)
|
|
522
613
|
const out = []
|
|
523
614
|
for (let p = 0; p < pad; p++) out.push("")
|
|
524
615
|
for (const l of visible) out.push(`${l.color ?? ""}${l.text}${ansi.reset}`)
|
|
525
616
|
return out
|
|
526
617
|
}
|
|
618
|
+
|
|
619
|
+
/** 视口数学单源(2026-08-31 会诊 kimi 缺陷 1——convGlobalIndex 未减 pad:
|
|
620
|
+
* 短会话顶部补 pad 空行后命中整体偏移,点击/滚轮落空或错行)。
|
|
621
|
+
* 返回 { start, end, pad }——renderConversation 与鼠标命中测试共用。 */
|
|
622
|
+
export function convViewport(convLen, convH, scroll) {
|
|
623
|
+
const maxScroll = Math.max(0, convLen - convH)
|
|
624
|
+
const clamped = Math.min(scroll, maxScroll)
|
|
625
|
+
const end = Math.max(0, convLen - clamped)
|
|
626
|
+
const start = Math.max(0, end - convH)
|
|
627
|
+
const pad = Math.max(0, convH - (end - start))
|
|
628
|
+
return { start, end, pad }
|
|
629
|
+
}
|
package/src/tui/render-loop.mjs
CHANGED
|
@@ -63,6 +63,16 @@ export function createRenderLoop(state, agent, ctx, pushLine, write = (s) => pro
|
|
|
63
63
|
// restored to full width only after flush stopped the output). dims are
|
|
64
64
|
// updated by: startup seed + a delayed re-sample + resize events.
|
|
65
65
|
const dims = state.dims ? state.dims.get() : { cols: process.stdout.columns || startupDims.cols, rows: process.stdout.rows || startupDims.rows }
|
|
66
|
+
// 2026-08-31 流式跟随(会诊 kimi 简化 + deepseek/glm 锚定):_followTail 即完整语义——
|
|
67
|
+
// 开 → 渲染前钉底(tool/pushLine 行同样生效,无跟随空洞);关(用户上滚)→ 锚定补偿:
|
|
68
|
+
// scroll 是距底偏移,暂停期间 convLen 增长会把用户读的行顶走——按增长量补偿。
|
|
69
|
+
const convLen = countConvLines(state, dims.cols, dims.rows)
|
|
70
|
+
if (state._followTail) {
|
|
71
|
+
state.scroll = 0
|
|
72
|
+
} else if (state._pauseAnchorLen != null && convLen > state._pauseAnchorLen) {
|
|
73
|
+
state.scroll += convLen - state._pauseAnchorLen
|
|
74
|
+
}
|
|
75
|
+
state._pauseAnchorLen = convLen
|
|
66
76
|
|
|
67
77
|
// NOTE (§7.2 D6): the old state.outputPanels prune is gone — output panels
|
|
68
78
|
// are abolished; subagent blocks live in the conversation and are never
|
package/src/tui/startup.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { slimToolResultForDisplay } from "./tool-events.mjs"
|
|
|
7
7
|
* the latest INITIAL_HISTORY_MESSAGES, then PgUp-at-top loads HISTORY_PAGE_MESSAGES
|
|
8
8
|
* more. Rebuilding an 8000-message session eagerly froze startup + first render. */
|
|
9
9
|
export const INITIAL_HISTORY_MESSAGES = 200
|
|
10
|
-
export const HISTORY_PAGE_MESSAGES =
|
|
10
|
+
export const HISTORY_PAGE_MESSAGES = 20
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Convert history[startIdx, endIdx) into conversation source lines (label lines
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { C } from "./ansi.mjs"
|
|
15
|
+
import { describeToolArgs } from "./tool-args.mjs"
|
|
15
16
|
|
|
16
17
|
/** `role#id/` prefix router — hyphen included since the eng-coder fix (2026-08-21). */
|
|
17
18
|
export const SUB_PREFIX_RE = /^([\w-]+)#(\d+)\//
|
|
@@ -307,7 +308,10 @@ export function routeSubToolCall(state, name, args, scheduleRender) {
|
|
|
307
308
|
sub.currentTool = name.slice(subMatch[0].length)
|
|
308
309
|
sub.toolArgs = args
|
|
309
310
|
sub.approval = null
|
|
310
|
-
|
|
311
|
+
// 2026-08-31: 工具行带参数摘要(与主 agent 工具块同款 describeToolArgs 单源)——
|
|
312
|
+
// 此前只显示工具名(bash 独享 "— 命令"),read/grep/glob 等全裸名。
|
|
313
|
+
const argsDesc = describeToolArgs(sub.currentTool, args)
|
|
314
|
+
appendSubBlock(sub, "tool", `❯ ${sub.currentTool}${argsDesc ? " " + argsDesc : ""}\n`, { fresh: true })
|
|
311
315
|
scheduleRender()
|
|
312
316
|
return true
|
|
313
317
|
}
|
package/src/tui/tool-args.mjs
CHANGED
|
@@ -36,6 +36,10 @@ export function describeToolArgs(name, args) {
|
|
|
36
36
|
const p = a.path ? ` in "${a.path}"` : ""
|
|
37
37
|
return `/${String(pat)}/${p}`
|
|
38
38
|
}
|
|
39
|
+
case "ls": {
|
|
40
|
+
const p = a.path ? String(a.path) : "."
|
|
41
|
+
return a.filter ? `${p} (filter: ${String(a.filter)})` : p
|
|
42
|
+
}
|
|
39
43
|
case "websearch": return String(a.query ?? "")
|
|
40
44
|
case "subagent": case "coder": case "explore": case "plan": case "eng-coder": {
|
|
41
45
|
const task = String(a.task ?? "").replace(/\s+/g, " ").trim()
|