thincoder 0.12.54 → 0.12.59
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 +98 -0
- package/README.md +1 -1
- package/bin/thincoder.mjs +25 -3
- package/package.json +3 -7
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +38 -3
- package/src/advisor/run.mjs +91 -53
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +156 -39
- package/src/agent/helpers.mjs +46 -4
- package/src/agent/setup.mjs +102 -19
- package/src/agent/spawn-child.mjs +28 -1
- package/src/agent-tools/advisor.mjs +43 -11
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +4 -1
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +160 -0
- package/src/agent-tools/settings.mjs +162 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +432 -0
- package/src/agent-tools/subagent-async.mjs +427 -0
- package/src/agent-tools/subagent-scheduler.mjs +319 -0
- package/src/agent-tools/subagent.mjs +565 -128
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +161 -49
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +182 -81
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +27 -1
- package/src/cli/memory-command.mjs +28 -7
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +125 -8
- package/src/context.mjs +115 -34
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +82 -27
- package/src/log.mjs +195 -0
- package/src/mcp/transport-http.mjs +13 -1
- package/src/mcp.mjs +52 -7
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +204 -10
- package/src/memory/docs.mjs +197 -62
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +38 -1
- package/src/prompts/advisor-design.md +46 -0
- package/src/prompts/advisor-round1.md +49 -2
- package/src/prompts/advisor-round2.md +47 -0
- package/src/prompts/advisor-round3.md +47 -0
- package/src/prompts/coder.md +22 -0
- package/src/prompts/consult-base.md +13 -0
- package/src/prompts/discipline.md +25 -6
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +157 -50
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -5
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +5 -2
- package/src/provider/anthropic.mjs +7 -5
- package/src/provider/core.mjs +104 -28
- package/src/provider/google.mjs +57 -24
- package/src/provider/normalize.mjs +1 -1
- package/src/provider/rate.mjs +0 -2
- package/src/provider/responses.mjs +8 -13
- package/src/provider/sse.mjs +20 -0
- package/src/session.mjs +15 -0
- package/src/tools/apply_patch.md +5 -1
- package/src/tools/bash.md +3 -3
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +92 -0
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +11 -6
- package/src/tools/execute.md +8 -8
- package/src/tools/execute.mjs +31 -35
- package/src/tools/file.mjs +26 -114
- package/src/tools/file_ops.md +3 -2
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +8 -16
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -2
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +3 -1
- package/src/tools/linter.mjs +9 -37
- package/src/tools/lsp.md +4 -1
- package/src/tools/patch.mjs +84 -13
- package/src/tools/pdf-parse-text.mjs +497 -0
- package/src/tools/pdf-parse-xref.mjs +499 -0
- package/src/tools/pdf.mjs +155 -0
- package/src/tools/question.md +2 -1
- package/src/tools/read.md +1 -0
- package/src/tools/read_pdf.md +21 -0
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +11 -32
- package/src/tools/system.mjs +6 -21
- package/src/tools/tree.md +2 -1
- 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 +387 -24
- package/src/tui/clipboard.mjs +17 -6
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-eng.mjs +1 -0
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp-form.mjs +197 -0
- package/src/tui/cmd-mcp.mjs +264 -114
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +49 -95
- package/src/tui/interaction.mjs +41 -3
- package/src/tui/key-handler.mjs +105 -143
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +41 -1
- package/src/tui/pickers.mjs +73 -7
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +45 -20
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +165 -0
- package/src/tui/render.mjs +4 -4
- package/src/tui/startup.mjs +40 -2
- package/src/tui/subagent-blocks.mjs +404 -111
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +172 -95
- package/src/tui/update-notice.mjs +72 -0
- package/src/tui/wizard.mjs +36 -6
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/tools/exec-prelude.mjs +0 -84
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
renderMathAndMarkdown, foldCapRows, renderFoldedHead, foldTailLines,
|
|
17
17
|
} from "./fold-block.mjs"
|
|
18
18
|
import { ADVISOR_THINKING_PLACEHOLDER } from "../advisor/run.mjs"
|
|
19
|
+
import { frozenSubSeg, toolSeg, frozenAdvSeg } from "./render-segments.mjs"
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
// Test seam (mirrors the _-prefixed seams in run.mjs) — moved to fold-block.mjs.
|
|
@@ -27,7 +28,9 @@ let _convCache = { key: "", cols: 0, lines: [] }
|
|
|
27
28
|
/** 2026-08-31 懒加载卡顿优化②:段级行体缓存(行对象→conv 行数组)。
|
|
28
29
|
* 与行级 wrapRowsCached 分层:wrap 缓存只省 markdown/换行重算(streaming 行 text 变失效),
|
|
29
30
|
* 段缓存省**整个行体的折叠/展开/窗口化组装**——loadOlder unshift 后尾部 987 行段全命中,
|
|
30
|
-
* rebuild 从 25.7ms → ~8ms 量级。签名由 lineSegSig 集中计算(该段输出的所有决定因素)。
|
|
31
|
+
* rebuild 从 25.7ms → ~8ms 量级。签名由 lineSegSig 集中计算(该段输出的所有决定因素)。
|
|
32
|
+
* 2026-09-03 D-S2:tool/frozenSub/frozenAdvisor 三段缓存随段实现迁至 render-segments.mjs
|
|
33
|
+
* (各段独立 WeakMap——行对象载体系互斥——此处只剩普通源行段缓存)。 */
|
|
31
34
|
const _lineSegCache = new WeakMap()
|
|
32
35
|
|
|
33
36
|
/** 段签名:段输出的所有决定因素(漏一项 → 缓存失效不全 → 显示 stale)。
|
|
@@ -94,61 +97,6 @@ function buildLineSeg(state, l, i, cols, maxRows) {
|
|
|
94
97
|
return out
|
|
95
98
|
}
|
|
96
99
|
|
|
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
100
|
|
|
153
101
|
/** 2026-08-31 懒加载卡顿根因修复:行级 wrap/markdown 渲染缓存。
|
|
154
102
|
* buildConvLines 全量重建 O(总行数)——真实 200 条历史 → 987 conv 行 94ms、loadOlder 后
|
|
@@ -219,9 +167,6 @@ export function convCacheKey(state, maxRows) {
|
|
|
219
167
|
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}`
|
|
220
168
|
}
|
|
221
169
|
|
|
222
|
-
/** Fold marker line: bold-cyan icon + "click to …" phrase underlined (clickable affordance).
|
|
223
|
-
* No indent — flush with the content below it; the caller adds a blank line BEFORE it
|
|
224
|
-
* so the control line stands apart from unrelated content (reported UX). */
|
|
225
170
|
|
|
226
171
|
function highlightSearchMatches(text, query, matchesInLine, globalCurrentIndex, allMatches, lineIndex) {
|
|
227
172
|
if (!matchesInLine || matchesInLine.length === 0 || !query) return text
|
|
@@ -247,45 +192,6 @@ function highlightSearchMatches(text, query, matchesInLine, globalCurrentIndex,
|
|
|
247
192
|
return result
|
|
248
193
|
}
|
|
249
194
|
|
|
250
|
-
/** Render a FROZEN child activity block carried on a state.lines entry
|
|
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). */
|
|
261
|
-
function frozenSubTaskLines(state, sub, cols, maxRows) {
|
|
262
|
-
const foldKey = `sub-${sub.key}`
|
|
263
|
-
const elapsed = Math.floor(((sub.doneAt ?? Date.now()) - sub.started) / 1000)
|
|
264
|
-
const modelPart = sub.model ? ` · ${sub.model}` : ""
|
|
265
|
-
const turnPart = sub.maxTurns > 0 ? ` · turn ${sub.turn}/${sub.maxTurns}` : ""
|
|
266
|
-
const errPart = sub.lastError ? ` — ${sub.lastError}` : ""
|
|
267
|
-
const icon = sub.approval ? "⏸" : "✓"
|
|
268
|
-
const header = `[${icon} ${sub.key}${modelPart} · done ${elapsed}s${turnPart}${errPart}]`
|
|
269
|
-
const out = []
|
|
270
|
-
if (isExpanded(state, foldKey)) {
|
|
271
|
-
// Expanded: shared component renders blank + ▼ control + full timeline,
|
|
272
|
-
// capped at 60% of the screen with a bottom collapse control.
|
|
273
|
-
const body = renderBlockTimeline(sub.blocks, cols)
|
|
274
|
-
out.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: "subagent activity" }))
|
|
275
|
-
} else {
|
|
276
|
-
// Folded: the header line itself is the control (▶ affordance), then tail 3.
|
|
277
|
-
out.push({
|
|
278
|
-
text: `▶ ${header} … subagent activity — click to expand`,
|
|
279
|
-
color: C.dim,
|
|
280
|
-
_foldToggle: foldKey,
|
|
281
|
-
})
|
|
282
|
-
for (const line of foldTailLines(sub.blocks)) {
|
|
283
|
-
out.push({ text: `│ ${sliceByWidth(line, cols - 4)}`, color: C.dim, _skipDimFold: true })
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return out
|
|
287
|
-
}
|
|
288
|
-
|
|
289
195
|
/**
|
|
290
196
|
* Build the conversation lines for the given state.
|
|
291
197
|
* maxRows: terminal rows for the 60% expansion cap (undefined → uncapped —
|
|
@@ -329,77 +235,23 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
329
235
|
if (!prevMain) pushBlank()
|
|
330
236
|
blankAfter = !nextMain
|
|
331
237
|
}
|
|
332
|
-
// Frozen subagent activity block (§7.2 D4, 2026-08-30):
|
|
333
|
-
//
|
|
334
|
-
// §7.2.1 — the frozen form keeps the same clickable interaction).
|
|
238
|
+
// Frozen subagent activity block (§7.2 D4, 2026-08-30): frozen form keeps the
|
|
239
|
+
// same clickable interaction as the running panel (impl in render-segments.mjs).
|
|
335
240
|
if (l._frozenSubTask) {
|
|
336
|
-
|
|
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
|
-
}
|
|
241
|
+
convLines.push(...frozenSubSeg(state, l, i, cols, maxRows))
|
|
351
242
|
continue
|
|
352
243
|
}
|
|
353
|
-
// ONE BLOCK PER TOOL CALL (2026-08-30 user ruling)
|
|
354
|
-
// live status, body = args JSON + streaming output + result. Folded =
|
|
355
|
-
// ▶ name args · status/summary; expanded = 60%-capped body (shared component).
|
|
244
|
+
// ONE BLOCK PER TOOL CALL (2026-08-30 user ruling) — impl in render-segments.mjs.
|
|
356
245
|
if (l._toolBlock) {
|
|
357
|
-
|
|
358
|
-
// 流式 append 使 output.length 变 → 失效;real 会话 106 个工具块每帧全量 wrap 实测
|
|
359
|
-
// 32ms(rebuild 40ms 的大头),入缓存后命中只算签名拼接。
|
|
360
|
-
const b = l._toolBlock
|
|
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)
|
|
371
|
-
} else {
|
|
372
|
-
const rows = buildToolBlockSeg(state, l, i, cols, maxRows)
|
|
373
|
-
_lineSegCache.set(l, { textRef: b, sig: tSig, rows })
|
|
374
|
-
convLines.push(...rows)
|
|
375
|
-
}
|
|
246
|
+
convLines.push(...toolSeg(state, l, i, cols, maxRows))
|
|
376
247
|
continue
|
|
377
248
|
}
|
|
378
|
-
// Frozen advisor review (2026-08-30): same collapsible-box treatment —
|
|
379
|
-
//
|
|
380
|
-
// rendered, no gutter — review history convention kept from the flat era),
|
|
381
|
-
// 60% cap via the shared component.
|
|
249
|
+
// Frozen advisor review (2026-08-30): same collapsible-box treatment — impl
|
|
250
|
+
// in render-segments.mjs.
|
|
382
251
|
if (l._frozenAdvisor) {
|
|
383
|
-
|
|
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)
|
|
395
|
-
} else {
|
|
396
|
-
const rows = buildFrozenAdvSeg(state, l, i, cols, maxRows)
|
|
397
|
-
_lineSegCache.set(l, { textRef: l._frozenAdvisor, sig: aSig, rows })
|
|
398
|
-
convLines.push(...rows)
|
|
399
|
-
}
|
|
252
|
+
convLines.push(...frozenAdvSeg(state, l, i, cols, maxRows))
|
|
400
253
|
continue
|
|
401
254
|
}
|
|
402
|
-
|
|
403
255
|
// ── 普通源行段(2026-08-31 懒加载卡顿优化②:段级缓存——行体 WeakMap 按行对象
|
|
404
256
|
// 缓存,签名含该段所有决定因素;unshift/loadOlder 后尾部段全命中,只算新增行。
|
|
405
257
|
// toggle/翻窗/收起只失效该块段,其它段照样命中。行为不变性是硬约束。
|
|
@@ -425,7 +277,7 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
425
277
|
// buildConvLines no longer renders running children: on completion
|
|
426
278
|
// onToolResult freezes the block into state.lines (subagent-blocks.mjs
|
|
427
279
|
// freezeSubTaskLines) and it scrolls away with the conversation (D4 现状).
|
|
428
|
-
// Frozen blocks render above via the _frozenSubTask branch (
|
|
280
|
+
// Frozen blocks render above via the _frozenSubTask branch (render-segments.mjs).
|
|
429
281
|
|
|
430
282
|
if (state.reasoning) {
|
|
431
283
|
// Live thinking streams INSIDE the unified box (user ruling 2026-08-30:
|
package/src/tui/render-frame.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
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
|
-
import {
|
|
12
|
+
import { providerSpec } from "../config.mjs"
|
|
13
13
|
import { computeLayout, subagentVisibleLines } from "./layout.mjs"
|
|
14
14
|
import { basename } from "node:path"
|
|
15
15
|
import { readFileSync } from "node:fs"
|
|
@@ -39,11 +39,15 @@ const SLASH_HINTS = {
|
|
|
39
39
|
|
|
40
40
|
/** Header panel (always 1 line). */
|
|
41
41
|
export function renderHeader(agent, cols) {
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// 2026-09-02 Q1(SESSION.md §8):provider 可为 null(无效 provider 被清空后用户 Esc 取消重选)
|
|
43
|
+
// —— 可选链守卫,头部显示 "no provider" 占位
|
|
44
|
+
const model = agent.provider?.model ?? "no provider"
|
|
45
|
+
// providerSpec(PROVIDER.md §15):模型信息显示跟随 provider 级 context 覆盖(D-C5);
|
|
46
|
+
// provider 为 null 时返回默认 spec 且不产生 "no provider" 查表警告(specForModel("") 空串不告警)
|
|
47
|
+
const spec = providerSpec(agent.provider)
|
|
44
48
|
const thinkOnValue = spec.thinkEnabledValue ?? "enabled"
|
|
45
|
-
const t = agent.provider
|
|
46
|
-
const effort = agent.provider
|
|
49
|
+
const t = agent.provider?.thinking
|
|
50
|
+
const effort = agent.provider?.reasoningEffort
|
|
47
51
|
const thinkBadge = t?.type === "disabled" ? "│ think: off"
|
|
48
52
|
: effort ? `│ think: ${effort}`
|
|
49
53
|
: t?.type === thinkOnValue ? "│ think: on" : ""
|
|
@@ -128,11 +132,19 @@ export function renderInputBox(state, W, boxLines, cols, inputLayout, inputOffse
|
|
|
128
132
|
if (title === " Input " || title === " Question " || title === " Inject Message " || title === " Processing... ") {
|
|
129
133
|
const parts = []
|
|
130
134
|
if (title === " Input " || title === " Processing... ") parts.push(" Ctrl+U clear ")
|
|
131
|
-
if (title === " Question ")
|
|
135
|
+
if (title === " Question ") {
|
|
136
|
+
// question 两形态各自真实按键提示(TUI-INPUT-BOX.md §7.2——自由文本态单行不变式:
|
|
137
|
+
// 无换行键——Shift+Enter/Ctrl+J 不插行,提示不沿用主输入框那组)。
|
|
138
|
+
parts.push((state.question?.options?.length ?? 0) > 0
|
|
139
|
+
? " ↑↓ select │ Enter confirm │ Esc: cancel "
|
|
140
|
+
: " Enter submit │ Esc: cancel │ Ctrl+U clear │ Ctrl+V paste ")
|
|
141
|
+
}
|
|
132
142
|
if (title === " Inject Message ") parts.push(" Enter send, Esc cancel ")
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
if (title !== " Question ") {
|
|
144
|
+
parts.push(" Shift+Enter / Ctrl+J newline ")
|
|
145
|
+
parts.push(" Ctrl+V paste ")
|
|
146
|
+
parts.push(" Ctrl+I inject ")
|
|
147
|
+
}
|
|
136
148
|
const hint = parts.join("")
|
|
137
149
|
topBorder = `╭─${title}${"─".repeat(Math.max(0, W - 4 - stringWidth(title) - stringWidth(hint)))}${hint}─╮`
|
|
138
150
|
} else {
|
|
@@ -140,8 +152,11 @@ export function renderInputBox(state, W, boxLines, cols, inputLayout, inputOffse
|
|
|
140
152
|
}
|
|
141
153
|
const out = [`${borderColor}${topBorder}${ansi.reset}`]
|
|
142
154
|
|
|
143
|
-
// Visual cursor position in the input box (hardware cursor stays hidden)
|
|
144
|
-
|
|
155
|
+
// Visual cursor position in the input box (hardware cursor stays hidden).
|
|
156
|
+
// hasOverlay 例外细化(TUI-INPUT-BOX.md §7.2):question 自由文本态(无 options)保留
|
|
157
|
+
// 反显光标(与主输入框同路径);options/permission/picker/wizard-provider 态无光标。
|
|
158
|
+
const qOptionsOnly = Boolean(state.question && state.question.options.length > 0)
|
|
159
|
+
const hasOverlay = state.permission || qOptionsOnly || state.picker || state.wizard?.step === "provider"
|
|
145
160
|
const curLine = (!hasOverlay && inputLayout) ? inputLayout.cursorLine - (inputOffset ?? 0) : -1
|
|
146
161
|
const curCol = (!hasOverlay && inputLayout) ? inputLayout.cursorCol : -1
|
|
147
162
|
|
|
@@ -252,13 +267,19 @@ export function renderRows(state, agent, opts) {
|
|
|
252
267
|
if (panels.picker) put(panels.picker.y, renderPicker(state, cols, panels.picker, overlay))
|
|
253
268
|
if (panels.permission) put(panels.permission.y, renderPermission(permPreviewLines))
|
|
254
269
|
if (panels.queue) put(panels.queue.y, [renderQueue(state, W)])
|
|
255
|
-
|
|
270
|
+
// question 自由文本态:box 内容是 layoutAnswer 行——光标布局/滚动随 box 内容走
|
|
271
|
+
// (questionLayout/questionOffset);主输入 inputLayout 此时无意义(被 question 行替换)。
|
|
272
|
+
const qFree = Boolean(state.question && state.question.options.length === 0)
|
|
273
|
+
const qOptionsOnly = Boolean(state.question && state.question.options.length > 0)
|
|
274
|
+
const boxLayout = qFree ? (layout.questionLayout ?? layout.inputLayout) : layout.inputLayout
|
|
275
|
+
const boxOffset = qFree ? (layout.questionOffset ?? 0) : layout.inputOffset
|
|
276
|
+
put(panels.inputBox.y, renderInputBox(state, W, boxLines, cols, boxLayout, boxOffset))
|
|
256
277
|
put(panels.status.y, [renderStatus(state, agent, cols, slashCommands)])
|
|
257
278
|
|
|
258
279
|
let cursorRow = 0, cursorCol = 0
|
|
259
|
-
if (!state.permission && !state.
|
|
260
|
-
cursorRow = panels.inputBox.y + 1 + (
|
|
261
|
-
cursorCol = 3 +
|
|
280
|
+
if (!state.permission && !state.picker && state.wizard?.step !== "provider" && !qOptionsOnly) {
|
|
281
|
+
cursorRow = panels.inputBox.y + 1 + (boxLayout.cursorLine - boxOffset) + 1
|
|
282
|
+
cursorCol = 3 + boxLayout.cursorCol
|
|
262
283
|
}
|
|
263
284
|
|
|
264
285
|
return { rows: screen, cursorRow, cursorCol, layout }
|
|
@@ -288,7 +309,9 @@ function inputBoxStyle(state) {
|
|
|
288
309
|
borderColor = C.tool; title = " Question "
|
|
289
310
|
} else if (state.permission) {
|
|
290
311
|
borderColor = C.warn
|
|
291
|
-
title = state.permission.name === "continue" ? " Continue? (y/n) "
|
|
312
|
+
title = state.permission.name === "continue" ? " Continue? (y/n) "
|
|
313
|
+
: state.permission.batch ? ` Allow ${state.permission.name}? (a/o/n) `
|
|
314
|
+
: ` Allow ${state.permission.name}? (y/n/a) `
|
|
292
315
|
} else if (state.picker) {
|
|
293
316
|
title = " Select "
|
|
294
317
|
} else if (state.wizard) {
|
|
@@ -312,9 +335,9 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
312
335
|
: " Type answer then Enter │ Esc: cancel"
|
|
313
336
|
}
|
|
314
337
|
if (state.permission) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
338
|
+
if (state.permission.name === "continue") return " y: continue │ n: stop"
|
|
339
|
+
if (state.permission.batch) return " a: approve all │ o: one by one │ n: deny"
|
|
340
|
+
return " y: approve │ n: deny │ a: approve all (AUTO)"
|
|
318
341
|
}
|
|
319
342
|
if (state.picker) return " type: filter │ ↑↓/PgUp/PgDn: select │ Enter: confirm │ Esc: cancel"
|
|
320
343
|
if (state.wizard) {
|
|
@@ -346,7 +369,9 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
346
369
|
const elapsed = state.processing ? ` ${Math.floor((Date.now() - state.processingStarted) / 1000)}s` : ""
|
|
347
370
|
const toolHint = state.currentTool ? ` ${state.currentTool}…` : ""
|
|
348
371
|
const statusText = state.processing ? `${state.status}${toolHint}${elapsed}` : state.status
|
|
349
|
-
|
|
372
|
+
// 2026-09-02 Q1(SESSION.md §8):provider 可为 null —— providerSpec(null) 保守 128K 不抛错
|
|
373
|
+
// 2026-09-02 §15:context 窗口跟随 providers[].context 覆盖(T-C6——百分比基于覆盖后的窗口)
|
|
374
|
+
const modelContext = providerSpec(agent.provider).context
|
|
350
375
|
const ctxPct = Math.round((state.ctxCache.tokens / modelContext) * 100)
|
|
351
376
|
const ctxTokensHint = state.ctxCache.tokens > 0 ? ` ${fmtK(state.ctxCache.tokens)}` : ""
|
|
352
377
|
const ctxHint = ctxPct > 0
|
package/src/tui/render-loop.mjs
CHANGED
|
@@ -111,7 +111,10 @@ export function createRenderLoop(state, agent, ctx, pushLine, write = (s) => pro
|
|
|
111
111
|
}
|
|
112
112
|
prevRows = rows
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
// hasOverlay 例外细化(TUI-INPUT-BOX.md §7.2):question 自由文本态(无 options)
|
|
115
|
+
// 保留硬件定位(cursorSuffix 正常发——与主输入框同路径);options 态无光标。
|
|
116
|
+
const qOptionsOnly = Boolean(state.question && state.question.options.length > 0)
|
|
117
|
+
const hasOverlay = state.permission || qOptionsOnly || state.picker || state.wizard?.step === "provider"
|
|
115
118
|
const cursorSuffix = hasOverlay ? "" : `\x1b[${cursorRow};${cursorCol}H${ansi.hideCursor}`
|
|
116
119
|
|
|
117
120
|
if (out.length || cursorSuffix) write(ansi.wrapOff + ansi.syncUpdateStart + out.join("") + ansi.syncUpdateEnd + cursorSuffix + ansi.wrapOn)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render-segments.mjs — 对话行三类特殊段渲染(2026-09-03 D-S2 自 render-conversation.mjs 拆出):
|
|
3
|
+
* tool 块 / frozenSubTask 冻结子agent 块 / frozenAdvisor 冻结评审块。各段自带独立 WeakMap
|
|
4
|
+
* 段级缓存(行对象载体系互斥——_toolBlock/_frozenSubTask/_frozenAdvisor 三型不共存于一行,
|
|
5
|
+
* 分支互斥 if——与共享缓存行为等价);签名由各段集中计算(该段输出的所有决定因素,漏一项
|
|
6
|
+
* → 缓存失效不全 → 显示 stale)。buildConvLines 主循环只留三分支 ~3 行调用。
|
|
7
|
+
*/
|
|
8
|
+
import { C } from "./ansi.mjs"
|
|
9
|
+
import { formatTables, sanitizeDisplay, sliceByWidth, wrapText } from "./render.mjs"
|
|
10
|
+
import {
|
|
11
|
+
foldTailLines, isExpanded, renderBlockTimeline, renderExpandedBlock, renderFoldedHead, renderMathAndMarkdown,
|
|
12
|
+
} from "./fold-block.mjs"
|
|
13
|
+
import { styleSubLabelRow } from "./subagent-panel.mjs"
|
|
14
|
+
import { SUBAGENT_ROLES } from "./subagent-blocks.mjs"
|
|
15
|
+
|
|
16
|
+
const _toolSegCache = new WeakMap()
|
|
17
|
+
const _frozenSubCache = new WeakMap()
|
|
18
|
+
const _frozenAdvCache = new WeakMap()
|
|
19
|
+
|
|
20
|
+
/** 工具块段(2026-08-31 段缓存抽出——真实会话 106 个工具块每帧全量 wrap 32ms 的根治)。
|
|
21
|
+
* 返回 conv 行数组(工具块的折叠头+尾/展开窗口+控制行),逻辑与原 L221-258 逐字同构。 */
|
|
22
|
+
function buildToolBlockSeg(state, l, i, cols, maxRows) {
|
|
23
|
+
const b = l._toolBlock
|
|
24
|
+
const foldKey = `tool-${l._lineId ?? i}`
|
|
25
|
+
const out = []
|
|
26
|
+
const status = !b.done
|
|
27
|
+
? "running"
|
|
28
|
+
: `${b.elapsed !== null ? b.elapsed + "ms" : ""}${b.summary ? (b.elapsed !== null ? " · " : "") + sliceByWidth(b.summary, 50) : ""}`.trim() || "done"
|
|
29
|
+
if (isExpanded(state, foldKey)) {
|
|
30
|
+
const body = []
|
|
31
|
+
const pushWrapped = (raw, color) => {
|
|
32
|
+
for (const w of wrapText(raw, cols - 4)) body.push({ text: " " + w, color, _skipDimFold: true })
|
|
33
|
+
}
|
|
34
|
+
for (const jl of b.argsJson) pushWrapped(jl, C.dim)
|
|
35
|
+
for (const ol of b.output) pushWrapped(ol, C.tool)
|
|
36
|
+
if (b.result) for (const rl of b.result) pushWrapped(rl, C.dim)
|
|
37
|
+
out.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: `${b.name}${b.roundTag || ""} ${b.argsSummary}`.trim() }))
|
|
38
|
+
} else {
|
|
39
|
+
const headText = sliceByWidth(
|
|
40
|
+
`❯ ${b.name}${b.roundTag || ""}${b.argsSummary ? " " + b.argsSummary : ""} · ${status}`,
|
|
41
|
+
Math.max(20, cols - 2),
|
|
42
|
+
)
|
|
43
|
+
const body = []
|
|
44
|
+
for (const jl of b.argsJson) for (const w of wrapText(jl, cols - 4)) body.push({ text: w, color: C.dim, _skipDimFold: true })
|
|
45
|
+
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 })
|
|
46
|
+
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 })
|
|
47
|
+
out.push(...renderFoldedHead({ header: { text: headText, color: C.tool, _foldToggle: foldKey }, body, cols }))
|
|
48
|
+
}
|
|
49
|
+
return out
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 工具块段入口(2026-08-31 段缓存:工具块签名含三缓冲长度+done/elapsed/summary+该块展开态——
|
|
53
|
+
* 流式 append 使 output.length 变 → 失效;real 会话 106 个工具块每帧全量 wrap 实测
|
|
54
|
+
* 32ms(rebuild 40ms 的大头),入缓存后命中只算签名拼接)。 */
|
|
55
|
+
export function toolSeg(state, l, i, cols, maxRows) {
|
|
56
|
+
const b = l._toolBlock
|
|
57
|
+
const toolFoldKey = `tool-${l._lineId ?? i}`
|
|
58
|
+
const tSig = [
|
|
59
|
+
cols, maxRows ?? 0, b.argsJson?.length ?? 0, b.output?.length ?? 0, b.result?.length ?? 0,
|
|
60
|
+
b.done ? 1 : 0, b.elapsed ?? "", b.summary ?? "", b.name ?? "", b.roundTag ?? "",
|
|
61
|
+
l._lineId ?? "", state.foldEnabled === false ? 0 : 1,
|
|
62
|
+
state.expandedBlocks?.has(toolFoldKey) ? 1 : 0, state._foldScroll?.get(toolFoldKey) ?? 0,
|
|
63
|
+
].join("|")
|
|
64
|
+
const hit = _toolSegCache.get(l)
|
|
65
|
+
if (hit && hit.textRef === b && hit.sig === tSig) return hit.rows
|
|
66
|
+
const rows = buildToolBlockSeg(state, l, i, cols, maxRows)
|
|
67
|
+
_toolSegCache.set(l, { textRef: b, sig: tSig, rows })
|
|
68
|
+
return rows
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 冻结子agent 活动块渲染(§7.2 D4):state.lines 载体 {_frozenSubTask: sub}(subagent-blocks.mjs
|
|
72
|
+
* freezeSubTaskLines 推入)。折叠 = 身份头 + tail 3;展开 = 共享组件(60% 屏幕封顶 + 底部可达
|
|
73
|
+
* 折叠控制)。折叠键 `sub-${key}`——与运行中面板区块同键,折叠态跨冻结边界无缝延续。 */
|
|
74
|
+
function frozenSubTaskLines(state, sub, cols, maxRows) {
|
|
75
|
+
const foldKey = `sub-${sub.key}`
|
|
76
|
+
const elapsed = Math.floor(((sub.doneAt ?? Date.now()) - sub.started) / 1000)
|
|
77
|
+
// §19.5 D-M7b ②: 冻结头保留 sync/async 标识(done 头含历史语义——与 model 标识
|
|
78
|
+
// 同生命周期);仅真实 subagent 角色(compress 冻结等无语义)。整行 dim——
|
|
79
|
+
// 无需 ANSI 注入(running 面板头则套 dim + 恢复行色——subagent-panel.mjs)。
|
|
80
|
+
const isSubRole = SUBAGENT_ROLES.includes(sub.role)
|
|
81
|
+
// §20:冻结的 queued 块(中止清场兜底——interrupted)标 waiting——不误标 sync
|
|
82
|
+
const modePart = isSubRole
|
|
83
|
+
? ` · ${sub.queued ? "waiting" : (sub.async === true ? "async" : "sync")}`
|
|
84
|
+
: ""
|
|
85
|
+
const modelPart = sub.model ? ` · ${sub.model}` : ""
|
|
86
|
+
const turnPart = sub.maxTurns > 0 ? ` · turn ${sub.turn}/${sub.maxTurns}` : ""
|
|
87
|
+
const errPart = sub.lastError ? ` — ${sub.lastError}` : ""
|
|
88
|
+
const icon = sub.approval ? "⏸" : "✓"
|
|
89
|
+
const verb = sub.stopped ? "stopped" : "done" // §19.5: cancel 冻结标题 "stopped"
|
|
90
|
+
const header = `[${icon} ${sub.key}${modePart}${modelPart} · ${verb} ${elapsed}s${turnPart}${errPart}]`
|
|
91
|
+
const out = []
|
|
92
|
+
if (isExpanded(state, foldKey)) {
|
|
93
|
+
// Expanded: shared component renders blank + ▼ control + full timeline,
|
|
94
|
+
// capped at 60% of the screen with a bottom collapse control.
|
|
95
|
+
const body = renderBlockTimeline(sub.blocks, cols).map(styleSubLabelRow)
|
|
96
|
+
out.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: "subagent activity" }))
|
|
97
|
+
} else {
|
|
98
|
+
// Folded: the header line itself is the control (▶ affordance), then tail 3.
|
|
99
|
+
out.push({
|
|
100
|
+
text: `▶ ${header} … subagent activity — click to expand`,
|
|
101
|
+
color: C.dim,
|
|
102
|
+
_foldToggle: foldKey,
|
|
103
|
+
})
|
|
104
|
+
for (const line of foldTailLines(sub.blocks)) {
|
|
105
|
+
out.push({ text: `│ ${sliceByWidth(line, cols - 4)}`, color: C.dim, _skipDimFold: true })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 冻结子agent 段入口(2026-08-31 段缓存:冻结后内容不变——签名含 sub.key + blocks 计数;
|
|
112
|
+
* 注:foldKey 由位置键升级为 sub.key(loadOlder unshift 后不重绑——sub 自身携带 key)。 */
|
|
113
|
+
export function frozenSubSeg(state, l, i, cols, maxRows) {
|
|
114
|
+
const fKey = `sub-${l._frozenSubTask.key}`
|
|
115
|
+
const fSig = [
|
|
116
|
+
cols, maxRows ?? 0, l._frozenSubTask.key, l._frozenSubTask.blocks?.length ?? 0,
|
|
117
|
+
l._frozenSubTask.done ? 1 : 0, state.foldEnabled === false ? 0 : 1,
|
|
118
|
+
state.expandedBlocks?.has(fKey) ? 1 : 0, state._foldScroll?.get(fKey) ?? 0,
|
|
119
|
+
].join("|")
|
|
120
|
+
const hit = _frozenSubCache.get(l)
|
|
121
|
+
if (hit && hit.textRef === l._frozenSubTask && hit.sig === fSig) return hit.rows
|
|
122
|
+
const rows = frozenSubTaskLines(state, l._frozenSubTask, cols, maxRows)
|
|
123
|
+
_frozenSubCache.set(l, { textRef: l._frozenSubTask, sig: fSig, rows })
|
|
124
|
+
return rows
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** frozenAdvisor 段渲染——冻结后文本不变(markdown 渲染、无 gutter——review 历史惯例);
|
|
128
|
+
* foldKey 由 advisor-done-${i}(位置键)升级为 advisor-done-${_lineId ?? i}(同 long-N
|
|
129
|
+
* 判例——loadOlder unshift 后不重绑)。 */
|
|
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
|
+
/** frozenAdvisor 段入口(2026-08-31 段缓存:frozenAdvisor 文本冻结不变——签名含文本长度 + 展开态)。 */
|
|
153
|
+
export function frozenAdvSeg(state, l, i, cols, maxRows) {
|
|
154
|
+
const frozenAdvKey = `advisor-done-${l._lineId ?? i}`
|
|
155
|
+
const aSig = [
|
|
156
|
+
cols, maxRows ?? 0, (l._frozenAdvisor ?? "").length,
|
|
157
|
+
state.foldEnabled === false ? 0 : 1,
|
|
158
|
+
state.expandedBlocks?.has(frozenAdvKey) ? 1 : 0, state._foldScroll?.get(frozenAdvKey) ?? 0,
|
|
159
|
+
].join("|")
|
|
160
|
+
const hit = _frozenAdvCache.get(l)
|
|
161
|
+
if (hit && hit.textRef === l._frozenAdvisor && hit.sig === aSig) return hit.rows
|
|
162
|
+
const rows = buildFrozenAdvSeg(state, l, i, cols, maxRows)
|
|
163
|
+
_frozenAdvCache.set(l, { textRef: l._frozenAdvisor, sig: aSig, rows })
|
|
164
|
+
return rows
|
|
165
|
+
}
|
package/src/tui/render.mjs
CHANGED
|
@@ -203,7 +203,7 @@ export function layoutInput(chars, cursor, width) {
|
|
|
203
203
|
* Display-layer only — raw tool results the model sees are unchanged; dirty displays already in session
|
|
204
204
|
* are also cleaned during replay.
|
|
205
205
|
*/
|
|
206
|
-
//
|
|
206
|
+
// 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
|
|
207
207
|
const ANSI_SEQUENCE = /\x1b\[[0-9;?]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][0-9A-B]|\x1b[=>#][0-9]?/
|
|
208
208
|
// Global variant for replace()/split(); the non-global one keeps match.index for slicing
|
|
209
209
|
const ANSI_SEQUENCE_RE = new RegExp(ANSI_SEQUENCE.source, "g")
|
|
@@ -212,7 +212,7 @@ export function sanitizeDisplay(s) {
|
|
|
212
212
|
.replace(ANSI_SEQUENCE_RE, "")
|
|
213
213
|
// §7.2 D5 fallback: an unparsed ⟦ev⟧ event token must never reach the grid —
|
|
214
214
|
// strip the sentinel + its RS-wrapped payload (⟦ev⟧turn\x1e…\x1e / bare RS/GS chars).
|
|
215
|
-
//
|
|
215
|
+
// 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
|
|
216
216
|
.replace(/⟦ev⟧[^\x1e\x1d]*\x1e[^\x1e\x1d]*\x1e[^\x1e\x1d]*\x1e[^\x1e\x1d]*\x1e?/g, "")
|
|
217
217
|
// GitHub-#4-class pitfall (2026-08-31): the residue strip used to be
|
|
218
218
|
// /⟦ev⟧[^\x1e\x1d]*/ — "swallow to end of line/string", which ATE REAL
|
|
@@ -222,12 +222,12 @@ export function sanitizeDisplay(s) {
|
|
|
222
222
|
// tokens start with a phase word (turn/approval); a bare sentinel with no
|
|
223
223
|
// letters attached is not a live token. Strip only sentinel+letters.
|
|
224
224
|
.replace(/⟦ev⟧[A-Za-z]*/g, "")
|
|
225
|
-
//
|
|
225
|
+
// 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
|
|
226
226
|
.replace(/[\x1d\x1e]/g, "")
|
|
227
227
|
.replace(/\r\n/g, "\n")
|
|
228
228
|
.replace(/\r/g, "\n")
|
|
229
229
|
.replace(/\t/g, " ")
|
|
230
|
-
//
|
|
230
|
+
// 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
|
|
231
231
|
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
|
232
232
|
.replace(/\n+$/, "")
|
|
233
233
|
}
|
package/src/tui/startup.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { listSlots } from "../session.mjs"
|
|
|
2
2
|
import { ansi, C } from "./ansi.mjs"
|
|
3
3
|
import { describeToolArgs, toolArgsLines } from "./tool-args.mjs"
|
|
4
4
|
import { slimToolResultForDisplay } from "./tool-events.mjs"
|
|
5
|
+
import { countConvLines } from "./render-conversation.mjs"
|
|
5
6
|
|
|
6
7
|
/** Lazy history window (parity with VS Code HISTORY_PAGE_SIZE): first paint loads
|
|
7
8
|
* the latest INITIAL_HISTORY_MESSAGES, then PgUp-at-top loads HISTORY_PAGE_MESSAGES
|
|
@@ -133,6 +134,41 @@ export function restoreLines(state, history) {
|
|
|
133
134
|
state.lines.unshift({ text: `… ${start} more earlier messages (PgUp at top to load)`, color: C.dim })
|
|
134
135
|
}
|
|
135
136
|
}
|
|
137
|
+
/** 懒加载更早历史(2026-08-31 用户约定:"滚动到头自动加载"——滚轮/PgUp 到顶皆触发;
|
|
138
|
+
* 2026-08-31 前只挂 PgUp 键 = 违约,滚轮到头无反应)。加载后滚动补偿保持锚定。
|
|
139
|
+
* 2026-09-03 D-S1b 自 index.mjs 迁入(startup 职责本含懒加载历史窗口——TUI.md §1 归属修复):
|
|
140
|
+
* 调用方(index.mjs)= stdin data 滚轮分支 + createKeyHandler ctx(PgUp 分支)。
|
|
141
|
+
* ctx: { agent, state, render } */
|
|
142
|
+
export function createLoadOlder({ agent, state, render }) {
|
|
143
|
+
return () => {
|
|
144
|
+
if (!state._hasOlder) return
|
|
145
|
+
const full = agent._fullHistory ?? []
|
|
146
|
+
const loaded = state._historyLoaded
|
|
147
|
+
const start = Math.max(0, full.length - loaded - HISTORY_PAGE_MESSAGES)
|
|
148
|
+
const end = full.length - loaded
|
|
149
|
+
if (start >= end) return
|
|
150
|
+
|
|
151
|
+
const d = state.dims ? state.dims.get() : {}
|
|
152
|
+
const cols = d.cols ?? ((state.dims?.get() ?? {}).cols ?? (process.stdout.columns || 80))
|
|
153
|
+
const before = countConvLines(state, cols, d.rows ?? (process.stdout.rows || 24))
|
|
154
|
+
|
|
155
|
+
if (state.lines[0]?.text?.startsWith("… ")) state.lines.shift()
|
|
156
|
+
state._lineIdCounter = state._lineIdCounter ?? 0
|
|
157
|
+
const older = historyToLines(full, start, end)
|
|
158
|
+
for (const l of older) l._lineId = ++state._lineIdCounter
|
|
159
|
+
state.lines.unshift(...older)
|
|
160
|
+
state._historyLoaded += end - start
|
|
161
|
+
state._hasOlder = start > 0
|
|
162
|
+
if (state._hasOlder) {
|
|
163
|
+
state.lines.unshift({ text: `… ${start} more earlier messages (scroll to top to load)`, color: C.dim })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const after = countConvLines(state, cols, (state.dims?.get() ?? {}).rows ?? (process.stdout.rows || 24))
|
|
167
|
+
state.scroll += Math.max(0, after - before)
|
|
168
|
+
render()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
136
172
|
/** Startup screen + session recovery + background indexing.
|
|
137
173
|
* Extracted from index.mjs.
|
|
138
174
|
* ctx: { agent, state, opts, pushLine, pushLabel, render, startWizard } */
|
|
@@ -140,12 +176,14 @@ export function showStartup(ctx) {
|
|
|
140
176
|
const { agent, state, opts, pushLine, pushLabel, render, startWizard } = ctx
|
|
141
177
|
|
|
142
178
|
// Startup screen
|
|
143
|
-
|
|
179
|
+
// 2026-09-02 Q1(SESSION.md §8 D-S2):provider 可为 null(无效 provider 被清空后用户 Esc 取消重选)
|
|
180
|
+
// —— 可选链守卫;`!apiKey` 触发既有 wizard(其 provider 菜单列出已存在 providers,可选中恢复,F3)
|
|
181
|
+
if (!agent.provider?.apiKey) {
|
|
144
182
|
pushLabel(`Welcome to ThinCoder!`, ansi.bold + C.tool)
|
|
145
183
|
pushLine("No API key configured yet — entering initial setup (Esc to skip anytime)", C.text)
|
|
146
184
|
startWizard()
|
|
147
185
|
} else {
|
|
148
|
-
pushLine(`Welcome to ThinCoder. Provider: ${agent.activeProvider} / ${agent.provider
|
|
186
|
+
pushLine(`Welcome to ThinCoder. Provider: ${agent.activeProvider} / ${agent.provider?.model ?? "(none)"}`, C.dim)
|
|
149
187
|
}
|
|
150
188
|
pushLine(`Tools: ${agent.tools.map((t) => t.name).join(", ")}`, C.dim)
|
|
151
189
|
|