thincoder 0.12.67 → 0.12.69
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 +32 -0
- package/README.md +21 -5
- package/bin/thincoder.mjs +41 -344
- package/package.json +2 -2
- package/src/acp/bridge.mjs +5 -2
- package/src/acp/handlers-session.mjs +12 -1
- package/src/cli/make-agent.mjs +44 -119
- package/src/cli/memory-command.mjs +45 -3
- package/src/command-interactive.mjs +196 -0
- package/src/command-table.mjs +185 -0
- package/src/completions.mjs +36 -12
- package/src/crash-reports.mjs +42 -20
- package/src/distill.mjs +3 -3
- package/src/heap-watch.mjs +5 -14
- package/src/tui/agent-turn.mjs +88 -50
- package/src/tui/clipboard.mjs +74 -30
- package/src/tui/cmd-advisor.mjs +39 -13
- package/src/tui/cmd-clear.mjs +2 -2
- package/src/tui/cmd-config.mjs +17 -1
- package/src/tui/cmd-eng.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +1 -1
- package/src/tui/cmd-new.mjs +2 -2
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-session.mjs +13 -5
- package/src/tui/cmd-think.mjs +35 -17
- package/src/tui/cmd-timers.mjs +33 -0
- package/src/tui/conversation-writer.mjs +2 -2
- package/src/tui/display-budget.mjs +9 -9
- package/src/tui/fold-block.mjs +1 -1
- package/src/tui/index.mjs +54 -18
- package/src/tui/interaction.mjs +4 -2
- package/src/tui/key-handler-ctrlc.mjs +1 -1
- package/src/tui/key-handler-search.mjs +1 -1
- package/src/tui/key-handler.mjs +1 -1
- package/src/tui/key-modes.mjs +1 -1
- package/src/tui/layout.mjs +5 -5
- package/src/tui/lifecycle-records.mjs +199 -0
- package/src/tui/model-picker.mjs +16 -199
- package/src/tui/mouse.mjs +3 -3
- package/src/tui/pickers.mjs +6 -2
- package/src/tui/provider-admin.mjs +213 -0
- package/src/tui/queued-merge.mjs +4 -60
- package/src/tui/queued-pickup.mjs +3 -3
- package/src/tui/render-conversation.mjs +13 -4
- package/src/tui/render-frame.mjs +43 -6
- package/src/tui/render-loop.mjs +2 -3
- package/src/tui/render-segments.mjs +2 -2
- package/src/tui/render.mjs +1 -1
- package/src/tui/slash-commands.mjs +3 -0
- package/src/tui/startup.mjs +28 -3
- package/src/tui/subagent-blocks.mjs +27 -19
- package/src/tui/subagent-children.mjs +5 -5
- package/src/tui/subagent-freeze.mjs +39 -37
- package/src/tui/subagent-panel.mjs +7 -7
- package/src/tui/suspension-drive.mjs +172 -259
- package/src/tui/timer-watch.mjs +64 -0
- package/src/tui/tool-args.mjs +10 -71
- package/src/tui/tool-display.mjs +6 -6
- package/src/tui/tool-events.mjs +33 -28
- package/src/tui/tui-lifecycle.mjs +13 -4
- package/src/tui/tui-state.mjs +3 -2
- package/src/tui/turn-face.mjs +4 -1
- package/src/tui/wizard.mjs +7 -2
- package/src/tui/wrapped-spawn.mjs +3 -3
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* maxRows flows in from every caller; omitted/0 → uncapped (tests, odd envs).
|
|
11
11
|
*/
|
|
12
12
|
import { ansi, C } from "./ansi.mjs"
|
|
13
|
-
import { formatTables, sanitizeDisplay, sliceByWidth, wrapText } from "./render.mjs"
|
|
13
|
+
import { formatTables, sanitizeDisplay, sliceByWidth, stringWidth, wrapText } from "./render.mjs"
|
|
14
14
|
import {
|
|
15
15
|
isExpanded, foldHintLine, renderExpandedBlock, renderBlockTimeline,
|
|
16
16
|
renderMathAndMarkdown, foldCapRows, renderFoldedHead, foldTailLines,
|
|
@@ -133,7 +133,7 @@ export function convCacheKey(state, maxRows) {
|
|
|
133
133
|
// Content prefix in the signature: same kind+length with different content
|
|
134
134
|
// would otherwise collide (stale render); 8 chars disambiguate in practice.
|
|
135
135
|
const blocksSig = (state._advisorBlocks ?? []).map((b) => `${b.kind}:${b.text?.length ?? 0}:${String(b.text ?? "").slice(0, 8)}`).join(",")
|
|
136
|
-
// NOTE (§
|
|
136
|
+
// NOTE (docs/cli/design/TUI.md §6.8): running subagent blocks are NOT part of the conversation
|
|
137
137
|
// anymore — they render in the fixed bottom panel (subagent-panel.mjs,
|
|
138
138
|
// uncached per frame: the 1s ticker refreshes the panel's elapsed display).
|
|
139
139
|
// The old subSig (blockEpoch/turn/elapsed invalidation) is removed: the panel
|
|
@@ -200,6 +200,15 @@ function highlightSearchMatches(text, query, matchesInLine, globalCurrentIndex,
|
|
|
200
200
|
return result
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/** 编号态首行预算收窄(#677 · TUI.md §7.5 宽度口径):首行折宽 = `width − firstDeduct`(`i. ` 编号占位),余行逐字照 `wrapText`;`0` ⇒ 逐行同形(单条态负向锁)。 */
|
|
204
|
+
function wrapTextHead(text, width, firstDeduct) {
|
|
205
|
+
const nl = text.indexOf("\n"), head = nl < 0 ? text : text.slice(0, nl), rest = nl < 0 ? null : text.slice(nl + 1)
|
|
206
|
+
const budget = Math.max(1, width - firstDeduct)
|
|
207
|
+
if (stringWidth(head) <= budget) return rest === null ? [head] : [head, ...wrapText(rest, width)]
|
|
208
|
+
const cut = sliceByWidth(head, budget), over = head.slice([...cut].length)
|
|
209
|
+
return [cut, ...wrapText(rest === null ? over : `${over}\n${rest}`, width)]
|
|
210
|
+
}
|
|
211
|
+
|
|
203
212
|
/**
|
|
204
213
|
* Build the conversation lines for the given state.
|
|
205
214
|
* maxRows: terminal rows for the 60% expansion cap (undefined → uncapped —
|
|
@@ -279,7 +288,7 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
279
288
|
blankAfter = false
|
|
280
289
|
}
|
|
281
290
|
}
|
|
282
|
-
// ── Subagent activity blocks (§
|
|
291
|
+
// ── Subagent activity blocks (docs/cli/design/TUI.md §6.8 D2) — RUNNING blocks MOVED to the fixed
|
|
283
292
|
// bottom panel (subagent-panel.mjs renderSubagentPanel, layout.mjs precomputes
|
|
284
293
|
// the panel height; render-frame puts it between conversation and todo).
|
|
285
294
|
// buildConvLines no longer renders running children: on completion
|
|
@@ -374,8 +383,8 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
374
383
|
const indent = multi ? " " : ""
|
|
375
384
|
const wrapCols = cols - 1 // 与正文同口径(`sanitizeDisplay` + `wrapText`——TUI.md §7.5 逐字)
|
|
376
385
|
for (let qi = 0; qi < queuedItems.length; qi++) {
|
|
377
|
-
const rows = wrapText(sanitizeDisplay(String(queuedItems[qi])), wrapCols)
|
|
378
386
|
const head = multi ? `${qi + 1}. ` : ""
|
|
387
|
+
const rows = wrapTextHead(sanitizeDisplay(String(queuedItems[qi])), wrapCols, stringWidth(head))
|
|
379
388
|
for (let ri = 0; ri < Math.min(rows.length, QUEUED_ITEM_MAX_LINES); ri++) {
|
|
380
389
|
convLines.push({ text: `${ri === 0 ? head : indent}${rows[ri]}`, color: C.dim, _skipDimFold: true })
|
|
381
390
|
}
|
package/src/tui/render-frame.mjs
CHANGED
|
@@ -10,6 +10,10 @@ import { ansi, C, ESC } from "./ansi.mjs"
|
|
|
10
10
|
import { convCacheKey, renderConversation, countConvLines } from "./render-conversation.mjs"
|
|
11
11
|
import { sliceByWidth, stringWidth } from "./render.mjs"
|
|
12
12
|
import { providerSpec } from "@thincoder/core/config.mjs"
|
|
13
|
+
import { t } from "@thincoder/core/i18n.mjs" // 停滞轻显形(TUI.md §7.7):句式键 status.quiet——词面单源(CLI 直取)
|
|
14
|
+
// #363②(off 形族单源——MODEL-SPECS.md §16.2 / §16.4):顶栏 think 徽标的 off 判据引核(本档零副本)。
|
|
15
|
+
import { thinkOffShape, thinkOffPath } from "@thincoder/core/think-off.mjs"
|
|
16
|
+
import { isRealUserMsg } from "@thincoder/core/history-window.mjs" // B6 回退链:首条真实 user 消息谓词(纯件单源)
|
|
13
17
|
import { computeLayout, subagentVisibleLines } from "./layout.mjs"
|
|
14
18
|
import { basename } from "node:path"
|
|
15
19
|
import { readFileSync } from "node:fs"
|
|
@@ -32,6 +36,11 @@ const SLASH_HINTS = {
|
|
|
32
36
|
"/restore": "select checkpoint to restore",
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
// ---------- 停滞轻显形阈值(docs/cli/design/TUI.md §7.7 · stall-indicator 批 2026-09-29)----------
|
|
40
|
+
/** 静默显示阈值(毫秒)——静默 ≥ 本值才注入读数(首显值 = 10s);跳秒 = 既有 1s 拍
|
|
41
|
+
* (agent-turn ticker——零新定时器);三端同值(VSC ∕ 桌面各自持有常量面)。 */
|
|
42
|
+
export const QUIET_MS = 10_000
|
|
43
|
+
|
|
35
44
|
// ====================================================================
|
|
36
45
|
// Panel render functions (exported for incremental rendering)
|
|
37
46
|
// Each returns string[] — one element per screen row, ANSI-colored,
|
|
@@ -49,7 +58,11 @@ export function renderHeader(agent, cols) {
|
|
|
49
58
|
const thinkOnValue = spec.thinkEnabledValue ?? "enabled"
|
|
50
59
|
const t = agent.provider?.thinking
|
|
51
60
|
const effort = agent.provider?.reasoningEffort
|
|
52
|
-
|
|
61
|
+
// off 徽标(#363②·单源引核):off 可宣称(`thinkOffPath`)∧ 现形 = 该族 off 取形
|
|
62
|
+
// (`thinkOffShape`)——thinkAlwaysOn 族 / effort 族携残留标记不再误报 `think: off`
|
|
63
|
+
// (旧形裸判 `type === "disabled"`,与「有效 off 路径」无判据关系——MODEL-SPECS.md §16.4)。
|
|
64
|
+
const offShape = thinkOffPath(spec) ? thinkOffShape(spec) : null
|
|
65
|
+
const thinkBadge = offShape !== null && t?.type === offShape.type ? "│ think: off"
|
|
53
66
|
: effort ? `│ think: ${effort}`
|
|
54
67
|
: t?.type === thinkOnValue ? "│ think: on" : ""
|
|
55
68
|
return `${ansi.bold}${C.tool} ThinCoder ${ansi.reset}${ansi.dim}${THINCODER_VERSION} │ ${sliceByWidth(model, 30)}${thinkBadge ? " " + thinkBadge : ""} │ ${sliceByWidth(basename(agent.cwd), Math.max(10, cols - 60))}${ansi.reset}`
|
|
@@ -268,7 +281,7 @@ export function renderRows(state, agent, opts) {
|
|
|
268
281
|
|
|
269
282
|
put(panels.header.y, [renderHeader(agent, cols)])
|
|
270
283
|
put(panels.conversation.y, renderConversation(state, cols, panels.conversation.h, state.scroll, rows))
|
|
271
|
-
// §
|
|
284
|
+
// docs/cli/design/TUI.md §6.8 D2: running-subagent fixed panel (between conversation and todo) —
|
|
272
285
|
// lines precomputed by layout (subagent-panel.mjs, neutral module), put
|
|
273
286
|
// directly (no double render); absent when no block is running (F6). Lines
|
|
274
287
|
// are objects ({text, color, _foldToggle/_foldBlock…} — mouse hit-testing
|
|
@@ -341,6 +354,16 @@ function inputBoxStyle(state) {
|
|
|
341
354
|
return { borderColor, title }
|
|
342
355
|
}
|
|
343
356
|
|
|
357
|
+
/** 标题回退链(B6 消 · #677——对位 VSC `panel-session.mjs:243-247` 链值 `title → "firstMessage" → "(empty)"`;
|
|
358
|
+
* 截断逐字同该链 `truncate(s, 40)`——生成前 ∕ 失败期两端同值)。首条真实 user 消息 = 活读单源:
|
|
359
|
+
* 绑定态取槽摘要字段(`counters().firstMessage`——与 VSC `listSlots` 同字段);未绑定回退内存人读线。 */
|
|
360
|
+
function sessionTitleFallback(agent) {
|
|
361
|
+
const fm = (agent?._recordStore?.counters?.().firstMessage ?? "")
|
|
362
|
+
|| (agent?._fullHistory ?? agent?.history ?? []).find(isRealUserMsg)?.content
|
|
363
|
+
if (typeof fm !== "string" || fm === "") return "(empty)"
|
|
364
|
+
return `"${fm.length <= 40 ? fm : fm.slice(0, 39) + "…"}"`
|
|
365
|
+
}
|
|
366
|
+
|
|
344
367
|
function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
345
368
|
const scrollHint = state.scroll > 0 ? ` │ scrolled ${state.scroll}` : ""
|
|
346
369
|
const rawInput = state.input.join("")
|
|
@@ -387,7 +410,14 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
387
410
|
? ` │ ↑${fmtK(tk.prompt)} ↓${fmtK(tk.completion)}${tk.reasoningTokens > 0 ? ` ✦${fmtK(tk.reasoningTokens)}` : ""}${cacheTotal > 0 ? ` hit${Math.round((tk.cacheHit / cacheTotal) * 100)}%` : ""}` : ""
|
|
388
411
|
const elapsed = state.processing ? ` ${Math.floor((Date.now() - state.processingStarted) / 1000)}s` : ""
|
|
389
412
|
const toolHint = state.currentTool ? ` ${state.currentTool}…` : ""
|
|
390
|
-
|
|
413
|
+
// 停滞轻显形(docs/cli/design/TUI.md §7.7 · stall-indicator 批 2026-09-29):显示位 = elapsed 之后;
|
|
414
|
+
// 静默 = 距最近一次可见输出(`state.lastOutputAt`——回合起刻为初始锚,重置点三类见 tool-events ∕
|
|
415
|
+
// subagent-blocks);非显示条件(未至阈值 ∕ 非在飞 ∕ 未起算)零注入(负向锁——逐字节等价);纯读数零警示色。
|
|
416
|
+
const lastOut = state.lastOutputAt ?? 0
|
|
417
|
+
const quietHint = state.processing && lastOut > 0 && Date.now() - lastOut >= QUIET_MS
|
|
418
|
+
? ` ${t("status.quiet", { s: Math.floor((Date.now() - lastOut) / 1000) })}`
|
|
419
|
+
: ""
|
|
420
|
+
const statusText = state.processing ? `${state.status}${toolHint}${elapsed}${quietHint}` : state.status
|
|
391
421
|
// 2026-09-02 Q1(SESSION.md §6.8):provider 可为 null —— providerSpec(null) 保守 128K 不抛错
|
|
392
422
|
// 2026-09-02 §15:context 窗口跟随 providers[].context 覆盖(T-C6——百分比基于覆盖后的窗口)
|
|
393
423
|
const modelContext = providerSpec(agent.provider).context
|
|
@@ -415,9 +445,16 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
415
445
|
const ledgerHint = lg?.marker
|
|
416
446
|
? ` │ ${lg.warn ? `${ansi.reset}${C.warn}${lg.marker}${ansi.reset}${ansi.dim}` : lg.marker}`
|
|
417
447
|
: ""
|
|
448
|
+
// timer 可见面(`docs/cli/design/TUI.md` §7.6 · timer-wake 批 2026-09-27):状态段簇尾(ledgerHint 后、
|
|
449
|
+
// titleHint 前)——取值 = `agent._pendingTimers` 活读(每帧 recompute / 零缓存副本 / 不显倒计时);
|
|
450
|
+
// 零在途 ⇒ 零注入(负向锁——半态逐字节等价);到期态(任一在途项已过期未送达)⇒ 同段警示色。
|
|
451
|
+
const timers = agent._pendingTimers ?? []
|
|
452
|
+
const timerHint = timers.length === 0 ? "" : ` │ ${timers.some((t) => t.expiresAt <= Date.now()) ? `${ansi.reset}${C.warn}⏰${timers.length}${ansi.reset}${ansi.dim}` : `⏰${timers.length}`}`
|
|
418
453
|
// D8(2026-09-21 块标题行对齐批——会话标题常显 · `docs/cli/design/TUI.md` §7.4):状态段簇尾
|
|
419
|
-
// (ledgerHint 后、键位组前);取值 = `agent.title` 活读(每帧 recompute)·
|
|
454
|
+
// (ledgerHint 后、键位组前);取值 = `agent.title` 活读(每帧 recompute)· 40 列截断。
|
|
455
|
+
// B6 消(#677):空白 ⇒ 回退链值(对位 VSC 顶栏——生成前 ∕ 失败期两端同值;值逐字,行级超宽走 statusMax)。
|
|
420
456
|
const titleRaw = typeof agent.title === "string" ? agent.title.trim() : ""
|
|
421
|
-
const
|
|
422
|
-
|
|
457
|
+
const titleShown = titleRaw ? (stringWidth(titleRaw) > 40 ? sliceByWidth(titleRaw, 39) + "…" : titleRaw) : sessionTitleFallback(agent)
|
|
458
|
+
const titleHint = ` │ ${titleShown}`
|
|
459
|
+
return ` ${statusText}${taskHint}${turnHint}${tokenHint}${ctxHint}${scrollHint}${ledgerHint}${timerHint}${titleHint} │ ${enterHint} │ /: commands │ wheel/PgUp/PgDn: scroll │ Ctrl+I: inject │ Ctrl+C: exit (×2)`
|
|
423
460
|
}
|
package/src/tui/render-loop.mjs
CHANGED
|
@@ -76,7 +76,7 @@ export function createRenderLoop(state, agent, ctx, pushLine, write = (s) => pro
|
|
|
76
76
|
}
|
|
77
77
|
state._pauseAnchorLen = convLen
|
|
78
78
|
|
|
79
|
-
// NOTE (§
|
|
79
|
+
// NOTE (docs/cli/design/TUI.md §6.8 D6): the old state.outputPanels prune is gone — output panels
|
|
80
80
|
// are abolished; subagent blocks live in the conversation and are never
|
|
81
81
|
// auto-pruned (bounded by the N2 per-child line cap instead).
|
|
82
82
|
|
|
@@ -120,9 +120,8 @@ export function createRenderLoop(state, agent, ctx, pushLine, write = (s) => pro
|
|
|
120
120
|
const cursorSuffix = hasOverlay ? "" : `\x1b[${cursorRow};${cursorCol}H${ansi.hideCursor}`
|
|
121
121
|
|
|
122
122
|
if (out.length || cursorSuffix) write(ansi.wrapOff + ansi.syncUpdateStart + out.join("") + ansi.syncUpdateEnd + cursorSuffix + ansi.wrapOn)
|
|
123
|
-
} catch
|
|
123
|
+
} catch {
|
|
124
124
|
// Don't let a render error crash the TUI
|
|
125
|
-
if (process.env.THINCODER_DEBUG_RENDER) process.stderr.write(`[render-error] ${e?.stack ?? e}\n`)
|
|
126
125
|
}
|
|
127
126
|
}
|
|
128
127
|
|
|
@@ -67,7 +67,7 @@ export function toolSeg(state, l, i, cols, maxRows) {
|
|
|
67
67
|
return rows
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/** 冻结子agent
|
|
70
|
+
/** 冻结子agent 活动块渲染(docs/cli/design/TUI.md §6.8 D4):state.lines 载体 {_frozenSubTask: sub}(subagent-blocks.mjs
|
|
71
71
|
* freezeSubTaskLines 推入)。折叠 = 身份头 + tail 3;展开 = 共享组件(60% 屏幕封顶 + 底部可达
|
|
72
72
|
* 折叠控制)。折叠键 `sub-${key}`——与运行中面板区块同键,折叠态跨冻结边界无缝延续。
|
|
73
73
|
* SUBAGENT-TAIL:内层内容已并入 sub.blocks(数据层——取代小节;docs/design/TUI.md §6)
|
|
@@ -79,7 +79,7 @@ function frozenSubTaskLines(state, sub, cols, maxRows) {
|
|
|
79
79
|
// 同生命周期);仅真实 subagent 角色(compress 冻结等无语义)。整行 dim——
|
|
80
80
|
// 无需 ANSI 注入(running 面板头则套 dim + 恢复行色——subagent-panel.mjs)。
|
|
81
81
|
const isSubRole = SUBAGENT_ROLES.includes(sub.role)
|
|
82
|
-
// §
|
|
82
|
+
// AGENT-LOOP-SUBAGENT.md §6.9:冻结的 queued 块(中止清场兜底——interrupted)标 waiting——不误标 sync
|
|
83
83
|
const modePart = isSubRole
|
|
84
84
|
? ` · ${sub.queued ? "waiting" : (sub.async === true ? "async" : "sync")}`
|
|
85
85
|
: ""
|
package/src/tui/render.mjs
CHANGED
|
@@ -242,7 +242,7 @@ const ANSI_SEQUENCE_RE = new RegExp(ANSI_SEQUENCE.source, "g")
|
|
|
242
242
|
export function sanitizeDisplay(s) {
|
|
243
243
|
return s
|
|
244
244
|
.replace(ANSI_SEQUENCE_RE, "")
|
|
245
|
-
// §
|
|
245
|
+
// docs/cli/design/TUI.md §6.8 D5 fallback: an unparsed ⟦ev⟧ event token must never reach the grid —
|
|
246
246
|
// strip the sentinel + its RS-wrapped payload (⟦ev⟧turn\x1e…\x1e / bare RS/GS chars).
|
|
247
247
|
// 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
|
|
248
248
|
.replace(/⟦ev⟧[^\x1e\x1d]*\x1e[^\x1e\x1d]*\x1e[^\x1e\x1d]*\x1e[^\x1e\x1d]*\x1e?/g, "")
|
|
@@ -19,6 +19,7 @@ import { handleInitCommand } from "./cmd-init.mjs"
|
|
|
19
19
|
import { handleRestoreCommand } from "./cmd-restore.mjs"
|
|
20
20
|
import { handlePlanCommand } from "./cmd-plan.mjs"
|
|
21
21
|
import { handleGoalCommand } from "./cmd-goal.mjs"
|
|
22
|
+
import { handleTimersCommand } from "./cmd-timers.mjs" // timer 可见面(TUI.md §7.6):`/timers` 只读列表
|
|
22
23
|
import { handleSkillsCommand } from "./cmd-skills.mjs"
|
|
23
24
|
import { handleMcpCommand } from "./cmd-mcp.mjs"
|
|
24
25
|
import { handleAutoCommand } from "./cmd-auto.mjs"
|
|
@@ -45,6 +46,7 @@ export const SLASH_COMMANDS = [
|
|
|
45
46
|
{ name: "/submodel", group: "Agent", desc: "subagent model per type (explore/plan/coder/eng-coder/eng-designer)" },
|
|
46
47
|
{ name: "/shell", group: "System", desc: "bash tool shell (git-bash/pwsh path; win11 cmd encoding fix)" },
|
|
47
48
|
{ name: "/goal", group: "Agent", desc: "set/view/cancel long-term goal" },
|
|
49
|
+
{ name: "/timers", group: "Agent", desc: "list pending timers (read-only)" },
|
|
48
50
|
{ name: "/think", group: "Agent", desc: "thinking mode & reasoning effort" },
|
|
49
51
|
{ name: "/upgrade", group: "System", desc: "check for updates & upgrade" },
|
|
50
52
|
{ name: "/config", group: "System", desc: "agent config (embedding, proxy, turns, threshold, consult pool)" },
|
|
@@ -80,6 +82,7 @@ export const HANDLERS = {
|
|
|
80
82
|
"/restore": handleRestoreCommand,
|
|
81
83
|
"/plan": handlePlanCommand,
|
|
82
84
|
"/goal": handleGoalCommand,
|
|
85
|
+
"/timers": handleTimersCommand,
|
|
83
86
|
"/skills": handleSkillsCommand,
|
|
84
87
|
"/mcp": handleMcpCommand,
|
|
85
88
|
"/auto": handleAutoCommand,
|
package/src/tui/startup.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { listSlots } from "@thincoder/core/session.mjs"
|
|
1
|
+
import { listSlots, ledgerHealth } from "@thincoder/core/session.mjs"
|
|
2
|
+
import { stripAtRefs } from "@thincoder/core/file-refs.mjs"
|
|
2
3
|
import { ansi, C } from "./ansi.mjs"
|
|
3
4
|
import { describeToolArgs, toolArgsLines } from "./tool-args.mjs"
|
|
4
5
|
import { slimToolResultForDisplay } from "./tool-events.mjs"
|
|
@@ -6,6 +7,8 @@ import { countConvLines } from "./render-conversation.mjs"
|
|
|
6
7
|
// TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.3/§5.4):恢复/翻页行过额度 + state.lines 总量对账。
|
|
7
8
|
import { capLine, accountLine, accountAll, releaseLine, syncLineBudget } from "./display-budget.mjs"
|
|
8
9
|
import { shiftFreezeAnchors } from "./subagent-blocks.mjs"
|
|
10
|
+
// #726 记录分支承载(痕行文案 live∥重建同调 ∥ 跨页回扫 ∥ subagent 合成件)
|
|
11
|
+
import { digestTraceLines, resolveSplitTerminalNs, resolvedStartN, synthSubTask } from "./lifecycle-records.mjs"
|
|
9
12
|
|
|
10
13
|
/** Lazy history window (parity with VS Code HISTORY_PAGE_SIZE): first paint loads
|
|
11
14
|
* the latest INITIAL_HISTORY_MESSAGES, then PgUp-at-top loads HISTORY_PAGE_MESSAGES
|
|
@@ -20,6 +23,8 @@ export const HISTORY_PAGE_MESSAGES = 20
|
|
|
20
23
|
*/
|
|
21
24
|
export function historyToLines(history, startIdx, endIdx) {
|
|
22
25
|
const lines = []
|
|
26
|
+
// 痕行复列 = 全量(本批 2026-10-01 · 台账 #768 自然形跟正):逐记录出(记录序 ≡ 恢复序——零截点;
|
|
27
|
+
// 未结末轮照现——沿现行口径)。
|
|
23
28
|
// Cross-page turn state: if the message BEFORE this page is a tool/assistant,
|
|
24
29
|
// the page starts mid-turn and must NOT emit a fresh "❯ ThinCoder:" label
|
|
25
30
|
// (a turn gets ONE label in the live run; history stores one assistant
|
|
@@ -39,9 +44,11 @@ export function historyToLines(history, startIdx, endIdx) {
|
|
|
39
44
|
// text on restore — the image is invisible to the terminal and the user
|
|
40
45
|
// just saw it live. A label with no content under it is noise (user
|
|
41
46
|
// report 2026-08-30: stray "❯ You:" after the read_image block).
|
|
47
|
+
// B5 消(#677 · 跨线清零批):恢复面剥离——注入展开文 → `@原文`(核单源 `stripAtRefs`,零第二实现;
|
|
48
|
+
// 手打 `[File: x]` 形近文本零误伤 = 核件 fail-closed 判据——与 VSC `panel-session.mjs` 同款逆变换)。
|
|
42
49
|
const userText = typeof m.content === "string"
|
|
43
|
-
? m.content
|
|
44
|
-
: (Array.isArray(m.content) ? m.content.find((p) => p?.type === "text")?.text ?? "" : "")
|
|
50
|
+
? stripAtRefs(m.content)
|
|
51
|
+
: (Array.isArray(m.content) ? stripAtRefs(m.content.find((p) => p?.type === "text")?.text ?? "") : "")
|
|
45
52
|
const displayable = typeof m.content === "string" ? !!m.content : !!(userText && userText.trim())
|
|
46
53
|
if (!displayable) continue
|
|
47
54
|
if (lines.length > 0) lines.push({ text: "", color: C.dim })
|
|
@@ -107,6 +114,17 @@ export function historyToLines(history, startIdx, endIdx) {
|
|
|
107
114
|
},
|
|
108
115
|
})
|
|
109
116
|
}
|
|
117
|
+
} else if (m.kind === "digest") {
|
|
118
|
+
// #726 记录分支:痕行逐条复列(文案 = lifecycle-records 单一实现——与活流同算式);
|
|
119
|
+
// **本批 2026-10-01(台账 #768 自然形跟正)**:复列 = **全量**(记录序 ≡ 恢复序——零截点;
|
|
120
|
+
// 未结末轮照现——沿现行口径);
|
|
121
|
+
// end 的 `n` 解析序 = 预解析注记(跨页回扫产物)→ 页内(含 ±1 页沿)回扫。
|
|
122
|
+
const startN = m.status === "end" ? resolvedStartN(history, i) : null
|
|
123
|
+
lines.push(...digestTraceLines(m, startN))
|
|
124
|
+
} else if (m.kind === "subagent") {
|
|
125
|
+
// #726 记录分支:归档快照 ⇒ 冻结载体行 + 合成件(渲染端零改消费);#790:行文取值同源自合成件 `key`
|
|
126
|
+
const frozen = synthSubTask(m) // 读面已剥 `sub:` 前缀——显示 = 本地无前缀形(与活流冻结载体同形)
|
|
127
|
+
lines.push({ text: `subagent activity: ${frozen.key}`, color: C.dim, _frozenSubTask: frozen })
|
|
110
128
|
}
|
|
111
129
|
}
|
|
112
130
|
return lines
|
|
@@ -131,6 +149,8 @@ export function restoreLines(state, desc) {
|
|
|
131
149
|
const loaded = Math.min(INITIAL_HISTORY_MESSAGES, total)
|
|
132
150
|
const start = total - loaded
|
|
133
151
|
state._lineIdCounter = state._lineIdCounter ?? 0
|
|
152
|
+
// #726 终态行 `n` 预解析(跨页分裂 ⇒ 存储回扫——调用面持有存储读口;页内直用者零回扫)
|
|
153
|
+
resolveSplitTerminalNs(window, base, state._agent?._recordStore)
|
|
134
154
|
const fresh = historyToLines(window, Math.max(0, start - base), window.length)
|
|
135
155
|
for (const l of fresh) l.text = capLine(l.text) // 行额度(§5.3 恢复行过 capText)
|
|
136
156
|
// Stable per-line ids (P1, 2026-08-30): fold keys for tool blocks derive from
|
|
@@ -175,6 +195,8 @@ export function createLoadOlder({ agent, state, render }) {
|
|
|
175
195
|
let older
|
|
176
196
|
if (store?.page) {
|
|
177
197
|
const { messages, base } = store.page(start, end, { margin: 1 })
|
|
198
|
+
// #726 终态行 `n` 预解析(页内(含 ±1 页沿)缺席本轮 start ⇒ 存储回扫)
|
|
199
|
+
resolveSplitTerminalNs(messages, base, store)
|
|
178
200
|
older = historyToLines(messages, Math.max(0, start - base), Math.min(messages.length, end - base))
|
|
179
201
|
} else {
|
|
180
202
|
older = historyToLines(agent._fullHistory ?? [], start, end)
|
|
@@ -235,6 +257,9 @@ export function showStartup(ctx) {
|
|
|
235
257
|
if (allSlots.length > 1) {
|
|
236
258
|
pushLine(`Tip: ${allSlots.length} sessions — /session to view/switch`, C.dim)
|
|
237
259
|
}
|
|
260
|
+
// F-L4(账本可靠批 · SESSION.md §6.25 判据句 4):账本异常警示行——**不继承** `allSlots.length > 1`(单 / 零会话项目同样在场);正常 ⇒ 零行。
|
|
261
|
+
const lh = ledgerHealth(agent.cwd)
|
|
262
|
+
if (lh.refused > 0 || lh.scene) pushLine(`会话账本异常(${lh.refused > 0 ? lh.lastReason : "scene"})——打开会话即自动补回${lh.scene ? ";损坏现场档保留 30 天" : ""}`, C.warn)
|
|
238
263
|
render()
|
|
239
264
|
}
|
|
240
265
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* subagent-blocks.mjs — 子agent 活动区块缓冲(
|
|
2
|
+
* subagent-blocks.mjs — 子agent 活动区块缓冲(docs/cli/design/TUI.md §6.8,消费端)。
|
|
3
3
|
* state.subTasks[key] = { key, role, model, async(AGENT-LOOP-SUBAGENT.md §6.7.2 D-M7b——⟦ev⟧async 标记——
|
|
4
4
|
* undefined = sync), started, done, doneAt, blocks:
|
|
5
5
|
* [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError,
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { C } from "./ansi.mjs"
|
|
18
|
-
// 第 27
|
|
18
|
+
// 第 27 批:relay 前缀文法单一权威 = src/agent/relay-prefix.mjs——本文件
|
|
19
19
|
// 不再自持前缀正则/解析副本(防第二套平行正则再漂移)。
|
|
20
20
|
import { parseRelayPath } from "@thincoder/core/agent/relay-prefix.mjs"
|
|
21
21
|
// zero-block 批(TUI.md §6.8.3.2 P1):墓碑存活闸复活分支留痕——日志面(零新增 UI 形态)。
|
|
@@ -30,30 +30,29 @@ export { SUB_BLOCK_LINE_LIMIT, appendSubBlock } from "./subagent-children.mjs"
|
|
|
30
30
|
// routeSub*/compression panel 内部用本地 import;re-export 保外部 import 面(index.mjs 等)
|
|
31
31
|
// CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):面板手工镜像退役——re-export 面换现算导出
|
|
32
32
|
// computePanelBlocks(读时现算),本文件全部刷镜调用点删除(单账本——变更点不再手动同步)。
|
|
33
|
-
import { freezeSubTaskLines, finishSubTaskKey, finishSubTask, freezeDoneSubTasks,
|
|
34
|
-
export { computePanelBlocks, finishSubTask, finishSubTaskKey, freezeSubTaskLines, shiftFreezeAnchors, freezeDoneSubTasks,
|
|
33
|
+
import { freezeSubTaskLines, finishSubTaskKey, finishSubTask, freezeDoneSubTasks, freezeAllSubTasks, freezeReclaimDigestedBlocks, shiftFreezeAnchors, livePoolHas, removeFrozenSubTaskLine, tombstoneSubKey } from "./subagent-freeze.mjs"
|
|
34
|
+
export { computePanelBlocks, finishSubTask, finishSubTaskKey, freezeSubTaskLines, shiftFreezeAnchors, freezeDoneSubTasks, freezeAllSubTasks, freezeReclaimDigestedBlocks } from "./subagent-freeze.mjs"
|
|
35
35
|
|
|
36
|
-
// 前缀文法/解析已迁 src/agent/relay-prefix.mjs(第 27
|
|
36
|
+
// 前缀文法/解析已迁 src/agent/relay-prefix.mjs(第 27 批)——本文件不自持副本。
|
|
37
37
|
/** ⟦ev⟧ token parser:`⟦ev⟧<name>\x1e<n>\x1e<max>\x1e<phase>\x1e<detail>`。phase done
|
|
38
|
-
* =
|
|
39
|
-
*
|
|
38
|
+
* = 消费面补发(#746:settle 一律发 settled)——收即冻结;settled = 完成(挂起 ∥ 非挂起两态)——
|
|
39
|
+
* 冻结延迟至消费窗(AGENT-LOOP-ASYNC-POOL.md §6.8 freezeReclaimDigestedBlocks 逐条回收——不等池空 ∥ 退出兜底补发);
|
|
40
40
|
* stopped(AGENT-LOOP-SUBAGENT.md §6.7.2 D-M6)= cancel 中止——interrupted 语义立即冻结(标题 "stopped");
|
|
41
|
-
* queued
|
|
41
|
+
* queued(AGENT-LOOP-SUBAGENT.md §6.9 D-SD3b)= 排队 spawn 返回即建 waiting 块(round2 #2 事件通道——
|
|
42
42
|
* `⟦ev⟧queued\x1e<kind>\x1e<position>\x1equeued\x1e<detail>`——kind slot/wait/depc)——
|
|
43
|
-
* 启动后 ⟦ev⟧async 转 running(同 key 不重建);cancelled(§
|
|
43
|
+
* 启动后 ⟦ev⟧async 转 running(同 key 不重建);cancelled(§6.9)= 出队/取消——**零字段**
|
|
44
44
|
* 移除块(不冻结——无活动可冻结)——与 async 同型单独解析(不入本正则);
|
|
45
45
|
* async(AGENT-LOOP-SUBAGENT.md §6.7.2 D-M7b + 处置 #4)= **零字段**标记(`⟦ev⟧async\x1e`——无 n/max/phase/detail 段)——
|
|
46
46
|
* routeSubToken 单独解析设 sub.async = true(不入本正则——本正则要求 4 字段);**缺失 key
|
|
47
47
|
* (块尚未创建)缓冲 `state._pendingAsyncKeys`——ensureSubTaskKey 块创建时应用(兜底)。 */
|
|
48
48
|
export const SUB_EVENT_RE = /^⟦ev⟧(turn|approval|done|settled|stopped|queued)\x1e([^\x1e]*)\x1e([^\x1e]*)\x1e([^\x1e]*)\x1e?([\s\S]*)$/
|
|
49
49
|
|
|
50
|
-
// parseRelayPath 自 src/agent/relay-prefix.mjs import(第 27
|
|
50
|
+
// parseRelayPath 自 src/agent/relay-prefix.mjs import(第 27 批——文法单一权威,
|
|
51
51
|
// 语义与迁移前逐字一致;“嵌套解析任意深度/无前缀 → null”见该模块)。
|
|
52
52
|
|
|
53
53
|
/** N1: render-layer throttle for child tool-output appends (generation relays verbatim). */
|
|
54
54
|
export const SUB_RELAY_THROTTLE_MS = 250
|
|
55
|
-
/** Roles a subagent tool child can take(⏹
|
|
56
|
-
* finishSubTasksByRole——finishSubTask 已收窄 no-op)。 */
|
|
55
|
+
/** Roles a subagent tool child can take(⏹ 门控/渲染角色判据). */
|
|
57
56
|
export const SUBAGENT_ROLES = ["sub", "explore", "plan", "coder", "eng-coder", "eng-designer"]
|
|
58
57
|
|
|
59
58
|
let _subRenderLast = 0
|
|
@@ -100,7 +99,7 @@ export function ensureSubTaskKey(state, key, role) {
|
|
|
100
99
|
key, role, model: undefined, started: Date.now(), done: false, doneAt: null,
|
|
101
100
|
blocks: [], currentTool: null, toolArgs: null, turn: 0, maxTurns: 0, approval: null,
|
|
102
101
|
lastError: null, dropped: 0,
|
|
103
|
-
_lineCount: 0, _charCount: 0, // 双维记账(TUI.md §
|
|
102
|
+
_lineCount: 0, _charCount: 0, // 双维记账(TUI-SESSION-VIEW.md §5.4——行数维 + 字符维,subagent-children.mjs)
|
|
104
103
|
stopped: false, // AGENT-LOOP-SUBAGENT.md §6.7.2: ⟦ev⟧stopped 冻结标记(标题 "stopped")
|
|
105
104
|
children: [], // SUBAGENT-TAIL: 嵌套子代理守护载体(内容并入本块——subagent-children.mjs)
|
|
106
105
|
}
|
|
@@ -109,6 +108,9 @@ export function ensureSubTaskKey(state, key, role) {
|
|
|
109
108
|
// ——块创建(此处)即应用——sub.async 区块创建即知(⏹ 门控/头标判定源保真)。
|
|
110
109
|
if (state._pendingAsyncKeys?.delete(key)) state.subTasks[key].async = true
|
|
111
110
|
}
|
|
111
|
+
// 停滞轻显形(TUI.md §7.7 · stall-indicator 批 2026-09-29):③子代理面 = 可见输出事件——
|
|
112
|
+
// 内容回显四路由(text ∕ think ∕ 工具 ∕ 输出)与 ⟦ev⟧ 块状态事件均先经本函数 ⇒ 单点重置静默起算。
|
|
113
|
+
state.lastOutputAt = Date.now()
|
|
112
114
|
return state.subTasks[key]
|
|
113
115
|
}
|
|
114
116
|
|
|
@@ -158,7 +160,7 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
158
160
|
// pending 标志**(state._pendingAsyncKeys——ensureSubTaskKey 块创建时应用——async
|
|
159
161
|
// 事件先于块存在到达不丢——queued→running ⏹ 可见性保真——⏹ 门控与头标 async 判定源)。
|
|
160
162
|
// 内层(嵌套 explore#M/)async 剥除不路由(防污染外层块头)。
|
|
161
|
-
// §
|
|
163
|
+
// §6.9 D-SD3b:queued 等待块实际启动(补位/释放)→ **转 running——同 key 不重建**
|
|
162
164
|
// ——清 waiting 标注(sub.queued)+ started 归零(elapsed 从实际启动计时——与池
|
|
163
165
|
// startedAt 语义一致——排队等待不计 elapsed)。
|
|
164
166
|
if (!nested && /^⟦ev⟧async(\x1e|$)/.test(payload)) {
|
|
@@ -169,6 +171,7 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
169
171
|
delete live.queued
|
|
170
172
|
live.started = Date.now()
|
|
171
173
|
}
|
|
174
|
+
state.lastOutputAt = Date.now() // 停滞轻显形(§7.7):③块状态——queued→running 转正(头标 ∕ 走时可感)
|
|
172
175
|
}
|
|
173
176
|
else {
|
|
174
177
|
state._pendingAsyncKeys ??= new Set()
|
|
@@ -177,7 +180,7 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
177
180
|
scheduleRender()
|
|
178
181
|
return true
|
|
179
182
|
}
|
|
180
|
-
// §
|
|
183
|
+
// §6.9 D-SD3b(round2 #2——cancelled 事件):取消/出队 → 移除等待块(**不冻结**——
|
|
181
184
|
// 从未启动无活动可冻结;running 取消走 ⟦ev⟧stopped 冻结——两者通道分明)。守卫:
|
|
182
185
|
// 仅 !async 块(waiting 块未启动——async 置位 = running——running 的取消以 stopped
|
|
183
186
|
// 通道表达,cancelled 永不合法指向 running 块——迟到/失序事件不误删)。缺失块
|
|
@@ -190,16 +193,17 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
190
193
|
// ⇒ 后续 ⟦ev⟧stopped 经 ensureSubTaskKey 直接丢弃(零幻影冻结块——探针实证的
|
|
191
194
|
// state.lines 0 → 1 路径封死);no-block 面不写(无可防之幻影——语义零扩)。
|
|
192
195
|
tombstoneSubKey(state, path.head)
|
|
196
|
+
state.lastOutputAt = Date.now() // 停滞轻显形(§7.7):③块状态——出队 ∕ 取消块移除(可见)
|
|
193
197
|
}
|
|
194
198
|
scheduleRender()
|
|
195
199
|
return true
|
|
196
200
|
}
|
|
197
201
|
const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
|
|
198
202
|
if (!sub) return true // frozen tombstone — late token from an aborted child: drop
|
|
199
|
-
// ⟦ev⟧:turn/approval 进度 → 仅头部(D1——不进 blocks/主流);done
|
|
200
|
-
//
|
|
201
|
-
// "done · awaiting digestion"
|
|
202
|
-
// queued(§
|
|
203
|
+
// ⟦ev⟧:turn/approval 进度 → 仅头部(D1——不进 blocks/主流);done(AGENT-LOOP-SUBAGENT.md §6.7.3 D-A3)= 消费面补发(#746:
|
|
204
|
+
// settle 一律发 ⟦ev⟧settled)——收即冻结;settled(AGENT-LOOP-ASYNC-POOL.md §6.8 D-S8)= 完成(挂起 ∥ 非挂起两态)——
|
|
205
|
+
// 驻留面板中间态 "done · awaiting digestion",冻结延迟至消费窗(回收 ∥ 退出兜底);stopped(AGENT-LOOP-SUBAGENT.md §6.7.2)= cancel——立即冻结;
|
|
206
|
+
// queued(§6.9 D-SD3b)= 排队 spawn 等待块(spawn 返回即建/状态变迁刷新——覆盖式
|
|
203
207
|
// 更新 sub.queued——块不可展开(无活动流);启动后 async 事件清标转 running)。
|
|
204
208
|
if (payload.startsWith("⟦ev⟧")) {
|
|
205
209
|
// 内层完成信号(沿革:D-M8 子标 → R23 子块段 → SUBAGENT-TAIL 并入——显示契约
|
|
@@ -409,6 +413,7 @@ export function ensureCompressPanel(state, info = {}) {
|
|
|
409
413
|
const messages = Number.isInteger(info.messages) && info.messages >= 0 ? info.messages : "?"
|
|
410
414
|
appendSubBlock(panel, "status", "Compressing context…\n", { fresh: true })
|
|
411
415
|
appendSubBlock(panel, "meta", `summarizing ${messages} messages\n`, { fresh: true })
|
|
416
|
+
state.lastOutputAt = Date.now() // 停滞轻显形(§7.7 语义填充——压缩面板起始 ∕ 刷新 = 可见输出)
|
|
412
417
|
return panel
|
|
413
418
|
}
|
|
414
419
|
|
|
@@ -419,6 +424,7 @@ export function markCompressFailed(state, error) {
|
|
|
419
424
|
const text = error?.message ? String(error.message) : String(error ?? "unknown error")
|
|
420
425
|
panel.lastError = text
|
|
421
426
|
appendSubBlock(panel, "err", `Compression failed: ${text}\n`, { fresh: true })
|
|
427
|
+
state.lastOutputAt = Date.now() // 停滞轻显形(§7.7 语义填充——压缩失败行 = 可见输出)
|
|
422
428
|
}
|
|
423
429
|
|
|
424
430
|
/** 冻结压缩面板进流(同完成子代理的载体/折叠 key)+ 释放 live 条目。 */
|
|
@@ -438,6 +444,7 @@ export function markCompressDone(state, info = {}) {
|
|
|
438
444
|
panel.currentTool = null
|
|
439
445
|
appendSubBlock(panel, "status", `Compressed: ${tokensFreed} tokens freed → summary (${seconds}s)\n`, { fresh: true })
|
|
440
446
|
freezeCompressPanel(state, panel)
|
|
447
|
+
state.lastOutputAt = Date.now() // 停滞轻显形(§7.7 语义填充——压缩完成冻结块入流 = 可见输出)
|
|
441
448
|
}
|
|
442
449
|
|
|
443
450
|
/** 降级态(onCompress mode:"fallback")——3 连败后 "truncated to N messages"。 */
|
|
@@ -450,4 +457,5 @@ export function markCompressFallback(state, info = {}) {
|
|
|
450
457
|
panel.currentTool = null
|
|
451
458
|
appendSubBlock(panel, "err", `Compression failed — fallback: truncated to ${tailMessages} messages\n`, { fresh: true })
|
|
452
459
|
freezeCompressPanel(state, panel)
|
|
460
|
+
state.lastOutputAt = Date.now() // 停滞轻显形(§7.7 语义填充——降级冻结块入流 = 可见输出)
|
|
453
461
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 省略计数真值(N6——D-ST5):`…(已省略 N 行)` 的 N 只随内容移除增长;标记自身
|
|
17
17
|
* 不占额度、不被丢弃、不计入 N。
|
|
18
18
|
*
|
|
19
|
-
* TUI-OOM-ROOTCAUSE(TUI.md §
|
|
19
|
+
* TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5 动机注 / §5.3):原环按 `\n` 计数——无换行巨 chunk
|
|
20
20
|
* 净增 0 行 → 环永不触发、文本无界。本模块加**字符维**(第二维——行数维逐字不动,D-TB5):
|
|
21
21
|
* `carrier._charCount` 与 `SUB_BLOCK_CHAR_LIMIT` 双维同时满足(先到先裁);字符维裁剪
|
|
22
22
|
* 按被裁文本行数折算 N(无幽灵计数——N6)。
|
|
@@ -25,7 +25,7 @@ import { SUB_BLOCK_CHAR_LIMIT, MIDDLE_TRUNC_MARKER, capText } from "./display-bu
|
|
|
25
25
|
|
|
26
26
|
export const SUB_BLOCK_LINE_LIMIT = 500
|
|
27
27
|
|
|
28
|
-
/** 单块自身额度(TUI-OOM-ROOTCAUSE §
|
|
28
|
+
/** 单块自身额度(TUI-OOM-ROOTCAUSE·TUI-SESSION-VIEW.md §5 动机注 / §5.1:无换行巨 chunk —— 行数维净增 0、
|
|
29
29
|
* 环永不触发;入块文本先过本条(头 3/4 + 尾 1/4 + 中段标记)——单行内容不因字符维
|
|
30
30
|
* 裁剪被整行丢光(环总量裁剪仍按最旧先行)。 */
|
|
31
31
|
const SUB_BLOCK_CAP_OPTS = {
|
|
@@ -105,7 +105,7 @@ function dropCarrierLines(carrier, want) {
|
|
|
105
105
|
return dropped
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
/** 字符维裁剪(§
|
|
108
|
+
/** 字符维裁剪(§5.3——无换行巨 chunk 的堵口):逐行丢最旧直至释放 ≥ want 字符;
|
|
109
109
|
* 行数按实际被丢行折算 N(无幽灵计数——N6);标记不占额度。返回释放字符数。 */
|
|
110
110
|
function dropCarrierChars(carrier, want) {
|
|
111
111
|
if (!(want > 0)) return 0
|
|
@@ -123,7 +123,7 @@ function dropCarrierChars(carrier, want) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/** 单载体 trim(原树级 trimSubTree 收窄——D-ST4):超 500 显示行 → 最旧先行丢行;
|
|
126
|
-
* 超 SUB_BLOCK_CHAR_LIMIT 字符 → 同款最旧先行(双维先到先裁——TUI.md §
|
|
126
|
+
* 超 SUB_BLOCK_CHAR_LIMIT 字符 → 同款最旧先行(双维先到先裁——TUI-SESSION-VIEW.md §5.4)。 */
|
|
127
127
|
function trimSubCarrier(carrier) {
|
|
128
128
|
const over = (carrier._lineCount ?? 0) - SUB_BLOCK_LINE_LIMIT
|
|
129
129
|
if (over > 0) dropCarrierLines(carrier, over)
|
|
@@ -132,7 +132,7 @@ function trimSubCarrier(carrier) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/** kind 合并追加(载体级)——净增行记账(显示行口径;省略标记不参与合并——N6);
|
|
135
|
-
* 字符账同步累加(第二维——§
|
|
135
|
+
* 字符账同步累加(第二维——§5.4)。 */
|
|
136
136
|
function pushBlock(carrier, kind, text, fresh) {
|
|
137
137
|
if (!text) return
|
|
138
138
|
const capped = capText(text, SUB_BLOCK_CAP_OPTS) // 单块自身额度(无换行巨 chunk 堵口)
|