thincoder 0.12.53 → 0.12.58
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 +74 -0
- package/bin/thincoder.mjs +17 -3
- package/package.json +3 -7
- package/src/acp/bridge.mjs +1 -1
- package/src/acp.mjs +60 -18
- package/src/advisor/messages.mjs +4 -2
- package/src/advisor/run.mjs +2 -2
- package/src/agent/dispatch.mjs +66 -26
- package/src/agent/helpers.mjs +13 -2
- package/src/agent/setup.mjs +16 -3
- package/src/agent/spawn-child.mjs +3 -1
- package/src/agent-tools/advisor.mjs +19 -9
- package/src/agent-tools/eng.mjs +2 -0
- package/src/agent-tools/subagent-check.mjs +107 -0
- package/src/agent-tools/subagent.mjs +205 -42
- package/src/agent.mjs +68 -3
- package/src/cli/make-agent.mjs +25 -0
- package/src/cli/memory-command.mjs +28 -7
- package/src/config.mjs +120 -8
- package/src/context.mjs +28 -7
- package/src/escape.mjs +110 -22
- package/src/git/checkpoint.mjs +32 -6
- package/src/mcp/transport-http.mjs +13 -1
- package/src/mcp.mjs +52 -7
- package/src/memory/core.mjs +78 -10
- package/src/memory/docs.mjs +33 -7
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +23 -0
- package/src/prompts/discipline.md +17 -3
- package/src/prompts/engineering.md +62 -5
- package/src/prompts/main.md +1 -0
- package/src/prompts/system.md +2 -1
- package/src/provider/anthropic.mjs +7 -5
- package/src/provider/core.mjs +90 -26
- 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-migrate.mjs +6 -0
- package/src/session-slots.mjs +361 -0
- package/src/session.mjs +282 -306
- package/src/tools/apply_patch.md +2 -0
- package/src/tools/bash.md +2 -2
- package/src/tools/edit-batch.mjs +104 -0
- package/src/tools/edit.md +3 -0
- package/src/tools/execute.md +4 -4
- package/src/tools/execute.mjs +14 -22
- package/src/tools/file.mjs +17 -55
- package/src/tools/file_ops.md +1 -1
- package/src/tools/git-checkpoint.mjs +143 -0
- package/src/tools/git-ext.mjs +173 -0
- package/src/tools/git.md +21 -8
- package/src/tools/git.mjs +55 -177
- package/src/tools/lint.md +1 -1
- package/src/tools/linter.mjs +9 -37
- package/src/tools/patch.mjs +1 -1
- package/src/tools/shared.mjs +7 -20
- package/src/tui/agent-turn.mjs +3 -3
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +2 -2
- package/src/tui/cmd-eng.mjs +1 -0
- package/src/tui/cmd-mcp-form.mjs +197 -0
- package/src/tui/cmd-mcp.mjs +255 -114
- 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/index.mjs +28 -27
- package/src/tui/interaction.mjs +28 -1
- package/src/tui/key-handler.mjs +14 -2
- package/src/tui/layout.mjs +81 -25
- package/src/tui/mouse.mjs +41 -2
- package/src/tui/pickers.mjs +62 -4
- package/src/tui/render-conversation.mjs +36 -93
- package/src/tui/render-frame.mjs +40 -16
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render.mjs +4 -4
- package/src/tui/startup.mjs +4 -2
- package/src/tui/subagent-blocks.mjs +119 -4
- package/src/tui/subagent-panel.mjs +81 -0
- package/src/tui/tool-events.mjs +61 -16
- package/src/tui/tui-lifecycle.mjs +45 -0
|
@@ -181,30 +181,21 @@ export function convCacheKey(state, maxRows) {
|
|
|
181
181
|
// Content prefix in the signature: same kind+length with different content
|
|
182
182
|
// would otherwise collide (stale render); 8 chars disambiguate in practice.
|
|
183
183
|
const blocksSig = (state._advisorBlocks ?? []).map((b) => `${b.kind}:${b.text?.length ?? 0}:${String(b.text ?? "").slice(0, 8)}`).join(",")
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
// Frozen blocks ride state.lines ({_frozenSubTask}) — the lines.length part of
|
|
201
|
-
// this key covers their existence; expanding/collapsing one flips expandedBlocks
|
|
202
|
-
// (covered by `exp`). One extra: the last frozen payload's header depends on
|
|
203
|
-
// blocks content which never changes post-freeze — nothing more needed.
|
|
204
|
-
// Same single pass also builds the per-line COLOR-CLASS signature: foldability
|
|
205
|
-
// is decided by color class since main output (C.text) never folds while
|
|
206
|
-
// thinking/dim do (2026-08-30) — two states differing only in line color used
|
|
207
|
-
// 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.
|
|
208
199
|
// Tool-block carriers ({_toolBlock}) contribute their BUFFER SIZE signature:
|
|
209
200
|
// output/result arrays mutate in place (streaming appends, result landing),
|
|
210
201
|
// and carrier text is always "" — without this the cache serves a stale block
|
|
@@ -225,7 +216,7 @@ export function convCacheKey(state, maxRows) {
|
|
|
225
216
|
// this the cache would serve the pre-search rows and highlight would never
|
|
226
217
|
// appear (P0-1, 2026-08-30 consult). query+index covers match navigation.
|
|
227
218
|
const searchPart = state.search?.query ? `${state.search.query}:${state.search.index ?? 0}` : ""
|
|
228
|
-
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}`
|
|
229
220
|
}
|
|
230
221
|
|
|
231
222
|
/** Fold marker line: bold-cyan icon + "click to …" phrase underlined (clickable affordance).
|
|
@@ -257,14 +248,16 @@ function highlightSearchMatches(text, query, matchesInLine, globalCurrentIndex,
|
|
|
257
248
|
}
|
|
258
249
|
|
|
259
250
|
/** Render a FROZEN child activity block carried on a state.lines entry
|
|
260
|
-
* (subagent-blocks.mjs freezeSubTaskLines pushes {_frozenSubTask: sub}).
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
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). */
|
|
268
261
|
function frozenSubTaskLines(state, sub, cols, maxRows) {
|
|
269
262
|
const foldKey = `sub-${sub.key}`
|
|
270
263
|
const elapsed = Math.floor(((sub.doneAt ?? Date.now()) - sub.started) / 1000)
|
|
@@ -337,7 +330,8 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
337
330
|
blankAfter = !nextMain
|
|
338
331
|
}
|
|
339
332
|
// Frozen subagent activity block (§7.2 D4, 2026-08-30): rendered as its own
|
|
340
|
-
// 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).
|
|
341
335
|
if (l._frozenSubTask) {
|
|
342
336
|
// 2026-08-31 段缓存:frozenSubTask 冻结后内容不变——签名含 sub.key + blocks 计数
|
|
343
337
|
const fKey = `sub-${l._frozenSubTask.key}`
|
|
@@ -425,65 +419,14 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
425
419
|
blankAfter = false
|
|
426
420
|
}
|
|
427
421
|
}
|
|
428
|
-
// ── Subagent activity blocks (§7.2
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
const runningSubs = Object.values(state.subTasks ?? {}).filter((s) => !s.done)
|
|
437
|
-
if (runningSubs.length > 0) {
|
|
438
|
-
// Divider between the conversation body and the running-subagent band
|
|
439
|
-
// (user request 2026-08-30, mirroring the task-panel divider in
|
|
440
|
-
// render-frame renderTodo). Only when at least one block actually renders —
|
|
441
|
-
// done children are frozen into state.lines above, so a divider for an
|
|
442
|
-
// empty band would hang over the section boundary.
|
|
443
|
-
// Unconditional divider (task-panel style): the preceding main-output
|
|
444
|
-
// trailing blank is breathing room, the divider is the section boundary —
|
|
445
|
-
// both belong. Only dedupe against another divider (idempotent re-render).
|
|
446
|
-
if (convLines.at(-1)?.color !== C.dim || !convLines.at(-1)?.text?.startsWith("─")) {
|
|
447
|
-
convLines.push({ text: "─".repeat(Math.max(1, cols - 1)), color: C.dim, _skipDimFold: true })
|
|
448
|
-
}
|
|
449
|
-
for (const sub of runningSubs) {
|
|
450
|
-
// Done children never reach this loop (filtered above): they are frozen
|
|
451
|
-
// into state.lines by subagent-blocks.mjs freezeSubTaskLines and removed
|
|
452
|
-
// from subTasks — a restored leftover would otherwise pin at the tail.
|
|
453
|
-
const foldKey = `sub-${sub.key}`
|
|
454
|
-
// Header summary: `[▶ coder#1 · glm-5.3 · 45s · turn 12/100] bash — npm test`
|
|
455
|
-
const icon = sub.approval ? "⏸" : "▶"
|
|
456
|
-
const elapsed = Math.floor(((sub.done ? sub.doneAt : Date.now()) - sub.started) / 1000)
|
|
457
|
-
const modelPart = sub.model ? ` · ${sub.model}` : ""
|
|
458
|
-
const turnPart = sub.maxTurns > 0 ? ` · turn ${sub.turn}/${sub.maxTurns}` : ""
|
|
459
|
-
let statePart
|
|
460
|
-
if (sub.approval) statePart = `等待审批: ${sub.approval}`
|
|
461
|
-
else if (sub.done) {
|
|
462
|
-
statePart = `done ${elapsed}s${sub.lastError ? ` — ${sub.lastError}` : ""}`
|
|
463
|
-
} else if (sub.currentTool) statePart = sub.currentTool
|
|
464
|
-
else statePart = "thinking..."
|
|
465
|
-
const argSummary = sub.currentTool && sub.toolArgs?.command
|
|
466
|
-
? ` — ${String(sub.toolArgs.command).replace(/\s+/g, " ").trim().slice(0, 60)}`
|
|
467
|
-
: ""
|
|
468
|
-
convLines.push({
|
|
469
|
-
text: `[${icon} ${sub.key}${modelPart} · ${elapsed}s${turnPart}] ${sliceByWidth(statePart + argSummary, Math.max(20, cols - 30))}`,
|
|
470
|
-
color: sub.done ? C.dim : C.tool,
|
|
471
|
-
_foldToggle: foldKey,
|
|
472
|
-
})
|
|
473
|
-
if (isExpanded(state, foldKey)) {
|
|
474
|
-
// Full activity timeline via the shared component (per-kind colors,
|
|
475
|
-
// 60% screen cap — the header control may sit above the viewport once
|
|
476
|
-
// expanded, the capped bottom control stays reachable).
|
|
477
|
-
const body = renderBlockTimeline(sub.blocks, cols)
|
|
478
|
-
convLines.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: "subagent activity" }))
|
|
479
|
-
} else {
|
|
480
|
-
// Folded: tail 3 non-empty block lines (most recent activity), dim.
|
|
481
|
-
for (const line of foldTailLines(sub.blocks)) {
|
|
482
|
-
convLines.push({ text: `│ ${sliceByWidth(line, cols - 4)}`, color: C.dim })
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
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
|
+
|
|
487
430
|
if (state.reasoning) {
|
|
488
431
|
// Live thinking streams INSIDE the unified box (user ruling 2026-08-30:
|
|
489
432
|
// "思考过程中为什么不是直接进这个框" — the flat tail render was a
|
package/src/tui/render-frame.mjs
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
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 {
|
|
13
|
-
import { computeLayout } from "./layout.mjs"
|
|
12
|
+
import { providerSpec } from "../config.mjs"
|
|
13
|
+
import { computeLayout, subagentVisibleLines } from "./layout.mjs"
|
|
14
14
|
import { basename } from "node:path"
|
|
15
15
|
import { readFileSync } from "node:fs"
|
|
16
16
|
|
|
@@ -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" : ""
|
|
@@ -89,8 +93,10 @@ export function renderPicker(state, cols, panel, overlay) {
|
|
|
89
93
|
// 过滤提示:无filter时显示 "type to filter",有filter时显示输入内容
|
|
90
94
|
const filterHint = p ? (p.filter ? `│ ${p.filter}` : "│ type to filter") : ""
|
|
91
95
|
const rawLeft = p ? ` ❯ ${p.title} ${filterHint} ` : " ❯ Setup "
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
// 2026-08-31 会诊(advisor round1 🔵):标题行与条目行一致留 8 格余量——Ambiguous 字符
|
|
97
|
+
// (❯/│ 等)在 CJK 终端渲染 2 格,余量不足时右侧 n/m 位置指示会被截。
|
|
98
|
+
const left = sliceByWidth(rawLeft, Math.max(1, cols - 8 - stringWidth(right)))
|
|
99
|
+
const titlePad = " ".repeat(Math.max(1, cols - 8 - stringWidth(left) - stringWidth(right)))
|
|
94
100
|
out.push(`${ansi.bold}${C.tool}${left}${ansi.reset}${ansi.dim}${titlePad}${right}${ansi.reset}`)
|
|
95
101
|
const hasMoreAbove = start > 0
|
|
96
102
|
const hasMoreBelow = start + winH < total
|
|
@@ -100,10 +106,14 @@ export function renderPicker(state, cols, panel, overlay) {
|
|
|
100
106
|
const moreAbove = i === 0 && hasMoreAbove
|
|
101
107
|
const moreBelow = i === shown.length - 1 && hasMoreBelow
|
|
102
108
|
const ind = moreAbove && moreBelow ? "↑↓ more" : moreAbove ? "↑ more" : moreBelow ? "↓ more" : ""
|
|
103
|
-
|
|
109
|
+
// 2026-08-31 会诊:右边距留 8 格余量——Ambiguous 宽度字符(│/—/●/▸/…/↑↓ 等)在中文
|
|
110
|
+
// locale 终端渲染 2 格而 stringWidth 按 1 格算,/session 行最坏带 ~7 个(▸ 前缀 +
|
|
111
|
+
// │×3 + … + — + ●)→ 行宽低估 7+ 格 → 实际超宽 → 物理 wrap → 残影;8 格余量覆盖
|
|
112
|
+
// 常见低估,最坏残余由 DECAWM 关闭硬截断兜底(见 render-loop)。
|
|
113
|
+
const maxW = cols - 8 - (ind ? stringWidth(ind) + 1 : 0)
|
|
104
114
|
// 超宽行截断并加省略号
|
|
105
115
|
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 -
|
|
116
|
+
const pad = ind ? " ".repeat(Math.max(1, cols - 8 - stringWidth(text) - stringWidth(ind))) : ""
|
|
107
117
|
out.push(`${l.color}${text}${ansi.reset}${ind ? `${ansi.dim}${pad}${ind}${ansi.reset}` : ""}`)
|
|
108
118
|
}
|
|
109
119
|
for (let i = shown.length; i < winH; i++) out.push("")
|
|
@@ -221,7 +231,7 @@ export function renderRows(state, agent, opts) {
|
|
|
221
231
|
const slashCommands = opts.slashCommands ?? []
|
|
222
232
|
|
|
223
233
|
const layout = computeLayout(state, { cols, rows })
|
|
224
|
-
const { W, panels, inputLayout, inputOffset, boxLines, visibleTasks, permPreviewLines, overlay } = layout
|
|
234
|
+
const { W, panels, inputLayout, inputOffset, boxLines, visibleTasks, permPreviewLines, overlay, subagentLines } = layout
|
|
225
235
|
|
|
226
236
|
const screen = new Array(rows).fill("")
|
|
227
237
|
const put = (y, lines) => {
|
|
@@ -232,6 +242,16 @@ export function renderRows(state, agent, opts) {
|
|
|
232
242
|
|
|
233
243
|
put(panels.header.y, [renderHeader(agent, cols)])
|
|
234
244
|
put(panels.conversation.y, renderConversation(state, cols, panels.conversation.h, state.scroll, rows))
|
|
245
|
+
// §7.2.1 D2: running-subagent fixed panel (between conversation and todo) —
|
|
246
|
+
// lines precomputed by layout (subagent-panel.mjs, neutral module), put
|
|
247
|
+
// directly (no double render); absent when no block is running (F6). Lines
|
|
248
|
+
// are objects ({text, color, _foldToggle/_foldBlock…} — mouse hit-testing
|
|
249
|
+
// reads the same array), converted to ANSI strings here like renderTodo.
|
|
250
|
+
if (panels.subagent) {
|
|
251
|
+
// 评审 #4:部分压缩(h < 全长)时保底截断——分隔线 + 末尾区块行(最新活动
|
|
252
|
+
// 优先),与 mouse 命中映射同一几何契约(subagentVisibleLines / subagentLineIndex)。
|
|
253
|
+
put(panels.subagent.y, subagentVisibleLines(subagentLines, panels.subagent.h).map((l) => `${l.color ?? ""}${l.text}${ansi.reset}`))
|
|
254
|
+
}
|
|
235
255
|
if (panels.todo) put(panels.todo.y, renderTodo(visibleTasks, cols))
|
|
236
256
|
if (panels.picker) put(panels.picker.y, renderPicker(state, cols, panels.picker, overlay))
|
|
237
257
|
if (panels.permission) put(panels.permission.y, renderPermission(permPreviewLines))
|
|
@@ -272,7 +292,9 @@ function inputBoxStyle(state) {
|
|
|
272
292
|
borderColor = C.tool; title = " Question "
|
|
273
293
|
} else if (state.permission) {
|
|
274
294
|
borderColor = C.warn
|
|
275
|
-
title = state.permission.name === "continue" ? " Continue? (y/n) "
|
|
295
|
+
title = state.permission.name === "continue" ? " Continue? (y/n) "
|
|
296
|
+
: state.permission.batch ? ` Allow ${state.permission.name}? (a/o/n) `
|
|
297
|
+
: ` Allow ${state.permission.name}? (y/n/a) `
|
|
276
298
|
} else if (state.picker) {
|
|
277
299
|
title = " Select "
|
|
278
300
|
} else if (state.wizard) {
|
|
@@ -296,9 +318,9 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
296
318
|
: " Type answer then Enter │ Esc: cancel"
|
|
297
319
|
}
|
|
298
320
|
if (state.permission) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
321
|
+
if (state.permission.name === "continue") return " y: continue │ n: stop"
|
|
322
|
+
if (state.permission.batch) return " a: approve all │ o: one by one │ n: deny"
|
|
323
|
+
return " y: approve │ n: deny │ a: approve all (AUTO)"
|
|
302
324
|
}
|
|
303
325
|
if (state.picker) return " type: filter │ ↑↓/PgUp/PgDn: select │ Enter: confirm │ Esc: cancel"
|
|
304
326
|
if (state.wizard) {
|
|
@@ -330,7 +352,9 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
330
352
|
const elapsed = state.processing ? ` ${Math.floor((Date.now() - state.processingStarted) / 1000)}s` : ""
|
|
331
353
|
const toolHint = state.currentTool ? ` ${state.currentTool}…` : ""
|
|
332
354
|
const statusText = state.processing ? `${state.status}${toolHint}${elapsed}` : state.status
|
|
333
|
-
|
|
355
|
+
// 2026-09-02 Q1(SESSION.md §8):provider 可为 null —— providerSpec(null) 保守 128K 不抛错
|
|
356
|
+
// 2026-09-02 §15:context 窗口跟随 providers[].context 覆盖(T-C6——百分比基于覆盖后的窗口)
|
|
357
|
+
const modelContext = providerSpec(agent.provider).context
|
|
334
358
|
const ctxPct = Math.round((state.ctxCache.tokens / modelContext) * 100)
|
|
335
359
|
const ctxTokensHint = state.ctxCache.tokens > 0 ? ` ${fmtK(state.ctxCache.tokens)}` : ""
|
|
336
360
|
const ctxHint = ctxPct > 0
|
package/src/tui/render-loop.mjs
CHANGED
|
@@ -114,7 +114,7 @@ export function createRenderLoop(state, agent, ctx, pushLine, write = (s) => pro
|
|
|
114
114
|
const hasOverlay = state.permission || state.question || state.picker || state.wizard?.step === "provider"
|
|
115
115
|
const cursorSuffix = hasOverlay ? "" : `\x1b[${cursorRow};${cursorCol}H${ansi.hideCursor}`
|
|
116
116
|
|
|
117
|
-
if (out.length || cursorSuffix) write(ansi.syncUpdateStart + out.join("") + ansi.syncUpdateEnd + cursorSuffix)
|
|
117
|
+
if (out.length || cursorSuffix) write(ansi.wrapOff + ansi.syncUpdateStart + out.join("") + ansi.syncUpdateEnd + cursorSuffix + ansi.wrapOn)
|
|
118
118
|
} catch (e) {
|
|
119
119
|
// Don't let a render error crash the TUI
|
|
120
120
|
if (process.env.THINCODER_DEBUG_RENDER) process.stderr.write(`[render-error] ${e?.stack ?? e}\n`)
|
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
|
@@ -140,12 +140,14 @@ export function showStartup(ctx) {
|
|
|
140
140
|
const { agent, state, opts, pushLine, pushLabel, render, startWizard } = ctx
|
|
141
141
|
|
|
142
142
|
// Startup screen
|
|
143
|
-
|
|
143
|
+
// 2026-09-02 Q1(SESSION.md §8 D-S2):provider 可为 null(无效 provider 被清空后用户 Esc 取消重选)
|
|
144
|
+
// —— 可选链守卫;`!apiKey` 触发既有 wizard(其 provider 菜单列出已存在 providers,可选中恢复,F3)
|
|
145
|
+
if (!agent.provider?.apiKey) {
|
|
144
146
|
pushLabel(`Welcome to ThinCoder!`, ansi.bold + C.tool)
|
|
145
147
|
pushLine("No API key configured yet — entering initial setup (Esc to skip anytime)", C.text)
|
|
146
148
|
startWizard()
|
|
147
149
|
} else {
|
|
148
|
-
pushLine(`Welcome to ThinCoder. Provider: ${agent.activeProvider} / ${agent.provider
|
|
150
|
+
pushLine(`Welcome to ThinCoder. Provider: ${agent.activeProvider} / ${agent.provider?.model ?? "(none)"}`, C.dim)
|
|
149
151
|
}
|
|
150
152
|
pushLine(`Tools: ${agent.tools.map((t) => t.name).join(", ")}`, C.dim)
|
|
151
153
|
|
|
@@ -16,9 +16,12 @@ import { describeToolArgs } from "./tool-args.mjs"
|
|
|
16
16
|
|
|
17
17
|
/** `role#id/` prefix router — hyphen included since the eng-coder fix (2026-08-21). */
|
|
18
18
|
export const SUB_PREFIX_RE = /^([\w-]+)#(\d+)\//
|
|
19
|
-
/** ⟦ev⟧ event token parser (D1/D2): `⟦ev⟧<name>\x1e<n>\x1e<max>\x1e<phase>\x1e<detail>`.
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
/** ⟦ev⟧ event token parser (D1/D2): `⟦ev⟧<name>\x1e<n>\x1e<max>\x1e<phase>\x1e<detail>`.
|
|
20
|
+
* phase "done" (§15 D-A3): async child finished — settle-time emits (each entry,
|
|
21
|
+
* it so the child's block freezes (the spawn tool result is a status JSON and
|
|
22
|
+
* must not freeze a still-running block). */
|
|
23
|
+
// 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
|
|
24
|
+
export const SUB_EVENT_RE = /^⟦ev⟧(turn|approval|done)\x1e([^\x1e]*)\x1e([^\x1e]*)\x1e([^\x1e]*)\x1e?([\s\S]*)$/
|
|
22
25
|
/** N2: per-child display-line ring buffer cap — oldest lines drop with a marker. */
|
|
23
26
|
export const SUB_BLOCK_LINE_LIMIT = 500
|
|
24
27
|
/** N1: render-layer throttle for child tool-output appends (generation relays verbatim). */
|
|
@@ -267,8 +270,21 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
267
270
|
if (!sub) return true // frozen tombstone — late token from an aborted child: drop
|
|
268
271
|
const payload = t.slice(subMatch[0].length)
|
|
269
272
|
// ⟦ev⟧ event token: turn/approval progress → header ONLY (never blocks,
|
|
270
|
-
// never the main stream — D1).
|
|
273
|
+
// never the main stream — D1). phase "done" (§15 D-A3): the async child
|
|
274
|
+
// finished — freeze its block (it stayed live through the background run).
|
|
271
275
|
if (payload.startsWith("⟦ev⟧")) {
|
|
276
|
+
const ev = payload.match(SUB_EVENT_RE)
|
|
277
|
+
if (ev?.[1] === "done") {
|
|
278
|
+
sub.done = true
|
|
279
|
+
sub.doneAt = Date.now()
|
|
280
|
+
sub.currentTool = null
|
|
281
|
+
sub.approval = null
|
|
282
|
+
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
283
|
+
freezeSubTaskLines(state, sub)
|
|
284
|
+
delete state.subTasks[sub.key]
|
|
285
|
+
scheduleRender()
|
|
286
|
+
return true
|
|
287
|
+
}
|
|
272
288
|
applySubEvent(sub, payload)
|
|
273
289
|
scheduleRender()
|
|
274
290
|
return true
|
|
@@ -329,3 +345,102 @@ export function routeSubToolOutput(state, name, part, scheduleRender) {
|
|
|
329
345
|
throttleSubRender(scheduleRender)
|
|
330
346
|
return true
|
|
331
347
|
}
|
|
348
|
+
|
|
349
|
+
// ─── Compression panel (CONTEXT-COMPACTION.md §7 D-C2) ─────────────────────
|
|
350
|
+
// The compression session reuses the subagent block machinery — user ruling
|
|
351
|
+
// "压缩会话像子agent 面板那样显示". While the summary call is in flight the panel
|
|
352
|
+
// lives in state.subTasks (role "compress") and renders in the running subagent
|
|
353
|
+
// panel (subagent-panel.mjs) — the existing 1s turn ticker (agent-turn.mjs
|
|
354
|
+
// subRunning) makes the elapsed header tick; on completion/fallback it freezes
|
|
355
|
+
// into the stream as a collapsible block via freezeSubTaskLines, exactly like a
|
|
356
|
+
// finished child. The panel carries STATUS ONLY (D-C2): the summary body is a
|
|
357
|
+
// machine artifact and never enters the blocks.
|
|
358
|
+
|
|
359
|
+
let _compressSeq = 0
|
|
360
|
+
|
|
361
|
+
/** Live compression panel (role "compress", not done) or null. */
|
|
362
|
+
function liveCompressPanel(state) {
|
|
363
|
+
return Object.values(state.subTasks ?? {}).find((s) => s.role === "compress" && !s.done) ?? null
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Open (or reset — retry) the compression panel. Fired by onCompressStart BEFORE
|
|
368
|
+
* the summary LLM call. info.messages = number of history messages being
|
|
369
|
+
* summarized ("summarizing N messages" stage label). Each attempt restarts the
|
|
370
|
+
* elapsed ticker (panel.started); the previous attempt's failure line stays in
|
|
371
|
+
* the block timeline so the retry history is visible.
|
|
372
|
+
*/
|
|
373
|
+
export function ensureCompressPanel(state, info = {}) {
|
|
374
|
+
state.subTasks ??= {}
|
|
375
|
+
let panel = liveCompressPanel(state)
|
|
376
|
+
if (!panel) {
|
|
377
|
+
panel = {
|
|
378
|
+
key: `compress#${++_compressSeq}`,
|
|
379
|
+
role: "compress",
|
|
380
|
+
model: undefined,
|
|
381
|
+
started: Date.now(), done: false, doneAt: null,
|
|
382
|
+
blocks: [], currentTool: null, toolArgs: null,
|
|
383
|
+
turn: 0, maxTurns: 0, approval: null,
|
|
384
|
+
lastError: null, dropped: 0,
|
|
385
|
+
}
|
|
386
|
+
state.subTasks[panel.key] = panel
|
|
387
|
+
}
|
|
388
|
+
// Per-attempt reset (D-C2 state machine): retries return to "Compressing…",
|
|
389
|
+
// the elapsed ticker restarts from this attempt's start.
|
|
390
|
+
panel.done = false
|
|
391
|
+
panel.doneAt = null
|
|
392
|
+
panel.lastError = null
|
|
393
|
+
panel.started = Date.now()
|
|
394
|
+
panel.currentTool = "compressing context…"
|
|
395
|
+
const messages = Number.isInteger(info.messages) && info.messages >= 0 ? info.messages : "?"
|
|
396
|
+
appendSubBlock(panel, "status", "Compressing context…\n", { fresh: true })
|
|
397
|
+
appendSubBlock(panel, "meta", `summarizing ${messages} messages\n`, { fresh: true })
|
|
398
|
+
return panel
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Failure state (onCompressFail): error text ONLY — no degradation note. The
|
|
402
|
+
* fallback note is bound to 3 CONSECUTIVE failures (markCompressFallback) and
|
|
403
|
+
* must not appear on a single failure (D-C2 state machine). */
|
|
404
|
+
export function markCompressFailed(state, error) {
|
|
405
|
+
const panel = liveCompressPanel(state)
|
|
406
|
+
if (!panel) return
|
|
407
|
+
const text = error?.message ? String(error.message) : String(error ?? "unknown error")
|
|
408
|
+
panel.lastError = text
|
|
409
|
+
appendSubBlock(panel, "err", `Compression failed: ${text}\n`, { fresh: true })
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Freeze the compression panel into the stream (collapsible, subagent-style —
|
|
413
|
+
* same carrier/fold-key as a finished child) and release the live entry. */
|
|
414
|
+
function freezeCompressPanel(state, panel) {
|
|
415
|
+
freezeSubTaskLines(state, panel)
|
|
416
|
+
delete state.subTasks[panel.key]
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Completion state (onCompress, LLM summary): "Compressed: N tokens freed →
|
|
420
|
+
* summary (Xs)" — N = pre-compression estimate − post-compression estimate,
|
|
421
|
+
* Xs = elapsed seconds. Block frozen + collapsible (T2). */
|
|
422
|
+
export function markCompressDone(state, info = {}) {
|
|
423
|
+
const panel = liveCompressPanel(state)
|
|
424
|
+
if (!panel) return
|
|
425
|
+
const tokensFreed = Number.isFinite(info.tokensFreed) ? Math.max(0, Math.round(info.tokensFreed)) : 0
|
|
426
|
+
const seconds = Number.isFinite(info.elapsedMs) ? Math.max(0, Math.round(info.elapsedMs / 1000)) : 0
|
|
427
|
+
panel.done = true
|
|
428
|
+
panel.doneAt = Date.now()
|
|
429
|
+
panel.currentTool = null
|
|
430
|
+
appendSubBlock(panel, "status", `Compressed: ${tokensFreed} tokens freed → summary (${seconds}s)\n`, { fresh: true })
|
|
431
|
+
freezeCompressPanel(state, panel)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Fallback state (onCompress with mode:"fallback"): the 3-consecutive-failures
|
|
435
|
+
* degradation note "Compression failed — fallback: truncated to N messages"
|
|
436
|
+
* (N = tail messages retained). Frozen + collapsible (T3b). */
|
|
437
|
+
export function markCompressFallback(state, info = {}) {
|
|
438
|
+
const panel = liveCompressPanel(state)
|
|
439
|
+
if (!panel) return
|
|
440
|
+
const tailMessages = Number.isFinite(info.tailMessages) ? Math.round(info.tailMessages) : "?"
|
|
441
|
+
panel.done = true
|
|
442
|
+
panel.doneAt = Date.now()
|
|
443
|
+
panel.currentTool = null
|
|
444
|
+
appendSubBlock(panel, "err", `Compression failed — fallback: truncated to ${tailMessages} messages\n`, { fresh: true })
|
|
445
|
+
freezeCompressPanel(state, panel)
|
|
446
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-panel.mjs — 运行中子 agent 固定底部面板渲染(AGENT-LOOP.md §7.2.1 D1/D2)。
|
|
3
|
+
*
|
|
4
|
+
* 面板位于 conversation 与 todo 之间(布局顺序 header → conversation → 面板 →
|
|
5
|
+
* todo → picker → permission → queue → input → status),高度完全自适应 = 全部
|
|
6
|
+
* 运行中区块的渲染行数(F2,会话区被挤小);无运行中区块 → 返回 [](F6 空态,
|
|
7
|
+
* 无悬空分隔线)。子 agent 完成后立即冻结进会话流(subagent-blocks.mjs
|
|
8
|
+
* freezeSubTaskLines,✓ 头 + 可展开,§7.2 D4 现状不变),面板下一帧自然移除
|
|
9
|
+
* 该区块(F5)——本模块只渲染 `!done` 条目。
|
|
10
|
+
*
|
|
11
|
+
* 中立模块(D1 评审 #6):layout.mjs 调 renderSubagentPanel 预计算面板高度
|
|
12
|
+
* (subagentLines → subagentH),render-frame.mjs 直接 put 预计算行(不重复
|
|
13
|
+
* 渲染)——若本函数放 render-frame 会引入 layout↔render-frame 循环依赖。
|
|
14
|
+
*
|
|
15
|
+
* 区块渲染逻辑自 render-conversation.mjs buildConvLines runningSubs 段迁移
|
|
16
|
+
* (§7.2.1 D2):折叠头 `[▶/⏸ key · model · elapsed · turn] state`(⏸ = 等待
|
|
17
|
+
* 审批态图标,sub.approval 非空时显示)+ tail 3;展开态经 fold-block.mjs 公共
|
|
18
|
+
* 组件(renderBlockTimeline + renderExpandedBlock,60% 封顶 + 块内滚动)。
|
|
19
|
+
* 折叠状态 key = `sub-${key}` 跨 turn 保持(D5,与冻结区块同一 key——冻结边界
|
|
20
|
+
* 无缝衔接)。
|
|
21
|
+
*/
|
|
22
|
+
import { C } from "./ansi.mjs"
|
|
23
|
+
import { sliceByWidth, stringWidth } from "./render.mjs"
|
|
24
|
+
import { isExpanded, renderBlockTimeline, renderExpandedBlock, foldTailLines } from "./fold-block.mjs"
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 面板行构建(纯函数):顶部分隔线 `─` + 各运行中区块(折叠头 + tail 3 /
|
|
28
|
+
* 展开全量)。maxRows = 终端行数(展开态 60% 封顶窗口化);省略 = 不封顶
|
|
29
|
+
* (单测/无终端环境)。
|
|
30
|
+
* @returns {Array<{text: string, color: string, ...}>}
|
|
31
|
+
*/
|
|
32
|
+
export function renderSubagentPanel(state, cols, maxRows) {
|
|
33
|
+
const runningSubs = Object.values(state.subTasks ?? {}).filter((s) => !s.done)
|
|
34
|
+
if (runningSubs.length === 0) return []
|
|
35
|
+
const out = []
|
|
36
|
+
// 面板顶部边界线(现状分隔线语义迁移,§7.2.1 D2/NF2)——面板存在即画线,
|
|
37
|
+
// 无运行区块时面板整体不渲染(F6:无悬空线)。
|
|
38
|
+
out.push({ text: "─".repeat(Math.max(1, cols - 1)), color: C.dim, _skipDimFold: true })
|
|
39
|
+
for (const sub of runningSubs) {
|
|
40
|
+
const foldKey = `sub-${sub.key}`
|
|
41
|
+
// 头部摘要:`[▶ coder#1 · glm-5.3 · 45s · turn 12/100] bash — npm test`
|
|
42
|
+
// ⏸ = 等待审批态(sub.approval 非空,评审 #5 定义);图标在括号内,
|
|
43
|
+
// 与冻结头 `[✓ …]` 格式统一(任务简报 UI 决策)。
|
|
44
|
+
const icon = sub.approval ? "⏸" : "▶"
|
|
45
|
+
const elapsed = Math.floor((Date.now() - sub.started) / 1000)
|
|
46
|
+
// 评审 #1 宽度预算:模型名先单独按显示宽度截断([model] token 原样记录可长
|
|
47
|
+
// 20-30+ 字符,不截断则括号前缀宽度不可预算、状态区被挤出终端右边距);
|
|
48
|
+
// 再量括号前缀实际显示宽度,状态区按 cols - bracketWidth - 2 截断——整行
|
|
49
|
+
// ≤ cols 铁律(TUI 布局纪律:任何写入帧的行 ≤ cols)。极端窄终端下括号
|
|
50
|
+
// 前缀自身也按 cols-2 截断兜底(状态区宁可让位也不撑破帧)。
|
|
51
|
+
const modelPart = sub.model
|
|
52
|
+
? ` · ${sliceByWidth(sub.model, Math.max(8, Math.floor(cols / 3)))}`
|
|
53
|
+
: ""
|
|
54
|
+
const turnPart = sub.maxTurns > 0 ? ` · turn ${sub.turn}/${sub.maxTurns}` : ""
|
|
55
|
+
const bracket = sliceByWidth(`[${icon} ${sub.key}${modelPart} · ${elapsed}s${turnPart}]`, Math.max(1, cols - 2))
|
|
56
|
+
const bracketWidth = stringWidth(bracket)
|
|
57
|
+
let statePart
|
|
58
|
+
if (sub.approval) statePart = `等待审批: ${sub.approval}`
|
|
59
|
+
else if (sub.currentTool) statePart = sub.currentTool
|
|
60
|
+
else statePart = "thinking..."
|
|
61
|
+
const argSummary = sub.currentTool && sub.toolArgs?.command
|
|
62
|
+
? ` — ${String(sub.toolArgs.command).replace(/\s+/g, " ").trim().slice(0, 60)}`
|
|
63
|
+
: ""
|
|
64
|
+
out.push({
|
|
65
|
+
text: `${bracket} ${sliceByWidth(statePart + argSummary, Math.max(0, cols - 2 - bracketWidth))}`,
|
|
66
|
+
color: C.tool,
|
|
67
|
+
_foldToggle: foldKey,
|
|
68
|
+
})
|
|
69
|
+
if (isExpanded(state, foldKey)) {
|
|
70
|
+
// 展开态:全量活动时间线(per-kind 着色,60% 屏封顶 + 块内滚动——公共组件)。
|
|
71
|
+
const body = renderBlockTimeline(sub.blocks, cols)
|
|
72
|
+
out.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: "subagent activity" }))
|
|
73
|
+
} else {
|
|
74
|
+
// 折叠态:tail 3 非空 block 行(最近活动),dim。
|
|
75
|
+
for (const line of foldTailLines(sub.blocks)) {
|
|
76
|
+
out.push({ text: `│ ${sliceByWidth(line, cols - 4)}`, color: C.dim })
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out
|
|
81
|
+
}
|