thincoder 0.12.66 → 0.12.68
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 +52 -0
- package/README.md +23 -5
- package/bin/thincoder.mjs +43 -331
- package/package.json +2 -2
- package/src/acp/bridge.mjs +13 -12
- 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/cli/permission.mjs +1 -1
- package/src/command-interactive.mjs +186 -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/prompt-injections.mjs +1 -1
- package/src/tui/agent-turn.mjs +114 -69
- package/src/tui/clipboard.mjs +74 -30
- package/src/tui/cmd-advisor.mjs +54 -13
- package/src/tui/cmd-clear.mjs +2 -2
- package/src/tui/cmd-config.mjs +26 -2
- 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 +23 -5
- package/src/tui/interaction.mjs +5 -3
- package/src/tui/key-handler-busy.mjs +17 -12
- package/src/tui/key-handler-ctrlc.mjs +5 -5
- package/src/tui/key-handler-edit.mjs +12 -8
- package/src/tui/key-handler-search.mjs +1 -1
- package/src/tui/key-handler.mjs +2 -2
- package/src/tui/key-modes.mjs +1 -1
- package/src/tui/layout.mjs +5 -5
- package/src/tui/ledger-surface.mjs +26 -5
- package/src/tui/lifecycle-records.mjs +199 -0
- package/src/tui/model-picker.mjs +16 -199
- package/src/tui/mouse.mjs +5 -5
- package/src/tui/pickers.mjs +6 -2
- package/src/tui/provider-admin.mjs +213 -0
- package/src/tui/queued-merge.mjs +12 -0
- package/src/tui/queued-pickup.mjs +34 -0
- package/src/tui/render-conversation.mjs +49 -4
- package/src/tui/render-frame.mjs +54 -11
- package/src/tui/render-loop.mjs +2 -3
- package/src/tui/render-segments.mjs +4 -4
- 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 +37 -29
- package/src/tui/subagent-children.mjs +5 -5
- package/src/tui/subagent-freeze.mjs +43 -41
- package/src/tui/subagent-panel.mjs +11 -11
- package/src/tui/suspension-drive.mjs +173 -253
- package/src/tui/timer-watch.mjs +64 -0
- package/src/tui/tool-args.mjs +10 -71
- package/src/tui/tool-display.mjs +7 -7
- package/src/tui/tool-events.mjs +40 -35
- package/src/tui/tui-lifecycle.mjs +13 -4
- package/src/tui/tui-state.mjs +3 -2
- package/src/tui/turn-face.mjs +6 -3
- 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,
|
|
@@ -23,6 +23,10 @@ import { frozenSubSeg, toolSeg, frozenAdvSeg } from "./render-segments.mjs"
|
|
|
23
23
|
import { renderMarkdownPreservingWidth as _rmpw } from "./fold-block.mjs"
|
|
24
24
|
export { _rmpw as _renderMarkdownPreservingWidth }
|
|
25
25
|
|
|
26
|
+
/** 排队块形态上限(`docs/cli/design/TUI.md` §7.5——queue-visible 批 2026-09-24):每条原文行
|
|
27
|
+
* ≤ 该值,超限 ⇒ 该条尾标记行 `… [该条共 N 行——发送后完整显示]`(与 `MAX_INPUT_LINES` 同口径)。 */
|
|
28
|
+
const QUEUED_ITEM_MAX_LINES = 3
|
|
29
|
+
|
|
26
30
|
let _convCache = { key: "", cols: 0, lines: [] }
|
|
27
31
|
|
|
28
32
|
/** 2026-08-31 懒加载卡顿优化②:段级行体缓存(行对象→conv 行数组)。
|
|
@@ -129,7 +133,7 @@ export function convCacheKey(state, maxRows) {
|
|
|
129
133
|
// Content prefix in the signature: same kind+length with different content
|
|
130
134
|
// would otherwise collide (stale render); 8 chars disambiguate in practice.
|
|
131
135
|
const blocksSig = (state._advisorBlocks ?? []).map((b) => `${b.kind}:${b.text?.length ?? 0}:${String(b.text ?? "").slice(0, 8)}`).join(",")
|
|
132
|
-
// NOTE (§
|
|
136
|
+
// NOTE (docs/cli/design/TUI.md §6.8): running subagent blocks are NOT part of the conversation
|
|
133
137
|
// anymore — they render in the fixed bottom panel (subagent-panel.mjs,
|
|
134
138
|
// uncached per frame: the 1s ticker refreshes the panel's elapsed display).
|
|
135
139
|
// The old subSig (blockEpoch/turn/elapsed invalidation) is removed: the panel
|
|
@@ -164,7 +168,11 @@ export function convCacheKey(state, maxRows) {
|
|
|
164
168
|
// this the cache would serve the pre-search rows and highlight would never
|
|
165
169
|
// appear (P0-1, 2026-08-30 consult). query+index covers match navigation.
|
|
166
170
|
const searchPart = state.search?.query ? `${state.search.query}:${state.search.index ?? 0}` : ""
|
|
167
|
-
|
|
171
|
+
// queue-visible 批(TUI.md §7.5):排队签名(条数 + 各条长度 + 首 8 字——同上方 blocksSig 口径)
|
|
172
|
+
// ——待发送块是**派生插槽**(零 state.lines 写入)⇒ 不入签即缓存命中出陈旧帧(块不现 / 不消)。
|
|
173
|
+
const queuedItems = state.pendingInput ?? []
|
|
174
|
+
const queuedPart = `${queuedItems.length}|${queuedItems.map((m) => `${String(m).length}:${String(m).slice(0, 8)}`).join(",")}`
|
|
175
|
+
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}|${queuedPart}`
|
|
168
176
|
}
|
|
169
177
|
|
|
170
178
|
|
|
@@ -192,6 +200,15 @@ function highlightSearchMatches(text, query, matchesInLine, globalCurrentIndex,
|
|
|
192
200
|
return result
|
|
193
201
|
}
|
|
194
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
|
+
|
|
195
212
|
/**
|
|
196
213
|
* Build the conversation lines for the given state.
|
|
197
214
|
* maxRows: terminal rows for the 60% expansion cap (undefined → uncapped —
|
|
@@ -271,7 +288,7 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
271
288
|
blankAfter = false
|
|
272
289
|
}
|
|
273
290
|
}
|
|
274
|
-
// ── Subagent activity blocks (§
|
|
291
|
+
// ── Subagent activity blocks (docs/cli/design/TUI.md §6.8 D2) — RUNNING blocks MOVED to the fixed
|
|
275
292
|
// bottom panel (subagent-panel.mjs renderSubagentPanel, layout.mjs precomputes
|
|
276
293
|
// the panel height; render-frame puts it between conversation and todo).
|
|
277
294
|
// buildConvLines no longer renders running children: on completion
|
|
@@ -348,6 +365,34 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
348
365
|
}
|
|
349
366
|
}
|
|
350
367
|
}
|
|
368
|
+
// ── 排队期「待发送」块(queue-visible 批 2026-09-24 · TUI.md §7.5)——会话流尾派生插槽:
|
|
369
|
+
// 判据 = `state.pendingInput` 非空(渲染期现算——零 state.lines 写入 / 零生命周期簿记,
|
|
370
|
+
// 消费 / 中止随判据消失;不入搜索面、无 `_lineId` / `_foldToggle`)。渲染序在 state.streaming
|
|
371
|
+
// **之后** ⇒ 流在块上方增长、块恒居会话区底(F1 跟随语义)。体行 `_skipDimFold` = 连 dim
|
|
372
|
+
// 折叠豁免(§7.5 形态锚「永不被折走」:条数 ≥3 时 dim 连跑必过 FOLD_LINES ⇒ 显式豁免)。
|
|
373
|
+
const queuedItems = state.pendingInput ?? []
|
|
374
|
+
if (queuedItems.length > 0) {
|
|
375
|
+
if (convLines.at(-1)?.text !== "") convLines.push({ text: "", color: C.text }) // 块前空行(主输出呼吸行同约)
|
|
376
|
+
const multi = queuedItems.length >= 2
|
|
377
|
+
convLines.push({
|
|
378
|
+
text: multi
|
|
379
|
+
? `⏳ 待发送 · ${queuedItems.length} 条消息(不打断当前执行,合并发送)`
|
|
380
|
+
: `⏳ 待发送 · 不打断当前执行,自动发送`,
|
|
381
|
+
color: C.warn,
|
|
382
|
+
})
|
|
383
|
+
const indent = multi ? " " : ""
|
|
384
|
+
const wrapCols = cols - 1 // 与正文同口径(`sanitizeDisplay` + `wrapText`——TUI.md §7.5 逐字)
|
|
385
|
+
for (let qi = 0; qi < queuedItems.length; qi++) {
|
|
386
|
+
const head = multi ? `${qi + 1}. ` : ""
|
|
387
|
+
const rows = wrapTextHead(sanitizeDisplay(String(queuedItems[qi])), wrapCols, stringWidth(head))
|
|
388
|
+
for (let ri = 0; ri < Math.min(rows.length, QUEUED_ITEM_MAX_LINES); ri++) {
|
|
389
|
+
convLines.push({ text: `${ri === 0 ? head : indent}${rows[ri]}`, color: C.dim, _skipDimFold: true })
|
|
390
|
+
}
|
|
391
|
+
if (rows.length > QUEUED_ITEM_MAX_LINES) {
|
|
392
|
+
convLines.push({ text: `${indent}… [该条共 ${rows.length} 行——发送后完整显示]`, color: C.dim, _skipDimFold: true })
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
351
396
|
// Fold long blocks (> 8 consecutive dim lines)
|
|
352
397
|
const FOLD_LINES = 8
|
|
353
398
|
const folded = []
|
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
|
|
@@ -396,12 +426,18 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
396
426
|
const ctxHint = ctxPct > 0
|
|
397
427
|
? ctxPct >= 80 ? ` │ ${ansi.reset}${C.warn}context ${ctxPct}%${ctxTokensHint}${ansi.reset}${ansi.dim}` : ` │ context ${ctxPct}%${ctxTokensHint}` : ""
|
|
398
428
|
// INPUT-LOCK-ASYNC(C'——F-3):busy(processing 含 digest)状态栏提示——取代旧排队提示
|
|
399
|
-
// (F-7 已删);F16(TUI.md §7.5
|
|
400
|
-
//
|
|
429
|
+
// (F-7 已删);F16(TUI.md §7.5)**四态分流**(queue-visible 批 2026-09-24):队列非空 ⇒
|
|
430
|
+
// 条数段(dim 段样式 · 零注意力色对——F13 豁免 · 消费即消失);队空 ⇒ 按 busy 面分排队句
|
|
431
|
+
// (普通 / 挂起内用户回合 = 步边界「当前步骤结束后自动发送」· 挂起内系统轮 = 轮末「本轮结束后
|
|
432
|
+
// 优先发送」)(判据同 TUI-INPUT-BOX.md §4.1;面感知 = `agent._inAutoTurn` 活读)。
|
|
433
|
+
const queuedN = state.pendingInput?.length ?? 0
|
|
401
434
|
const enterHint = state.processing
|
|
402
|
-
? (
|
|
403
|
-
: state.suspended || state._suspPending
|
|
404
|
-
|
|
435
|
+
? (queuedN > 0 ? `已排队 ${queuedN} 条消息`
|
|
436
|
+
: state.suspended || state._suspPending
|
|
437
|
+
? (agent._inAutoTurn === true
|
|
438
|
+
? `会话内回合处理中 — Enter 排队(本轮结束后优先发送)`
|
|
439
|
+
: `会话内回合处理中 — Enter 排队(当前步骤结束后自动发送)`)
|
|
440
|
+
: `主会话处理中 — Enter 排队(当前步骤结束后自动发送)`)
|
|
405
441
|
: "Enter: send"
|
|
406
442
|
// LEDGER-SURFACE(§2.30.3.3/§2.30.3.4):L1 常驻标记——状态段簇尾(scrollHint 后、键位组前);
|
|
407
443
|
// 空标记零注入(半态逐字节等价——同 :233-238 纪律);warn = 当前项目老化 > 0(警示色段包裹)
|
|
@@ -409,9 +445,16 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
409
445
|
const ledgerHint = lg?.marker
|
|
410
446
|
? ` │ ${lg.warn ? `${ansi.reset}${C.warn}${lg.marker}${ansi.reset}${ansi.dim}` : lg.marker}`
|
|
411
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}`}`
|
|
412
453
|
// D8(2026-09-21 块标题行对齐批——会话标题常显 · `docs/cli/design/TUI.md` §7.4):状态段簇尾
|
|
413
|
-
// (ledgerHint 后、键位组前);取值 = `agent.title` 活读(每帧 recompute)·
|
|
454
|
+
// (ledgerHint 后、键位组前);取值 = `agent.title` 活读(每帧 recompute)· 40 列截断。
|
|
455
|
+
// B6 消(#677):空白 ⇒ 回退链值(对位 VSC 顶栏——生成前 ∕ 失败期两端同值;值逐字,行级超宽走 statusMax)。
|
|
414
456
|
const titleRaw = typeof agent.title === "string" ? agent.title.trim() : ""
|
|
415
|
-
const
|
|
416
|
-
|
|
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)`
|
|
417
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)
|
|
@@ -75,11 +75,11 @@ export function toolSeg(state, l, i, cols, maxRows) {
|
|
|
75
75
|
function frozenSubTaskLines(state, sub, cols, maxRows) {
|
|
76
76
|
const foldKey = `sub-${sub.key}`
|
|
77
77
|
const elapsed = Math.floor(((sub.doneAt ?? Date.now()) - sub.started) / 1000)
|
|
78
|
-
// §
|
|
78
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2 D-M7b ②: 冻结头保留 sync/async 标识(done 头含历史语义——与 model 标识
|
|
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
|
: ""
|
|
@@ -89,7 +89,7 @@ function frozenSubTaskLines(state, sub, cols, maxRows) {
|
|
|
89
89
|
// 图标三态互斥(2026-09-20 端差·显示面消差批 M5):修前 stopped 冻结头 `✓` 与 verb 相抵
|
|
90
90
|
// ⇒ 补齐 VSC `webview/activity-view.js:46-49` 已成的 cancelled → `⏹` 形态(VSC 标尺,CLI 缺面)。
|
|
91
91
|
const icon = sub.approval ? "⏸" : sub.stopped ? "⏹" : "✓"
|
|
92
|
-
const verb = sub.stopped ? "stopped" : "done" // §
|
|
92
|
+
const verb = sub.stopped ? "stopped" : "done" // AGENT-LOOP-SUBAGENT.md §6.7.2: cancel 冻结标题 "stopped"
|
|
93
93
|
const header = `[${icon} ${sub.key}${modePart}${modelPart} · ${verb} ${elapsed}s${turnPart}${errPart}]`
|
|
94
94
|
const out = []
|
|
95
95
|
if (isExpanded(state, foldKey)) {
|
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,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* subagent-blocks.mjs — 子agent 活动区块缓冲(
|
|
3
|
-
* state.subTasks[key] = { key, role, model, async
|
|
2
|
+
* subagent-blocks.mjs — 子agent 活动区块缓冲(docs/cli/design/TUI.md §6.8,消费端)。
|
|
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,
|
|
6
|
-
* dropped, blockEpoch, awaitingDigest
|
|
6
|
+
* dropped, blockEpoch, awaitingDigest(AGENT-LOOP-ASYNC-POOL.md §6.8), _freezeAt(冻结锚点), stopped(AGENT-LOOP-SUBAGENT.md §6.7.2),
|
|
7
7
|
* children: [](SUBAGENT-TAIL:嵌套子代理**守护载体**——内容并入本块 blocks——见
|
|
8
8
|
* subagent-children.mjs)}。
|
|
9
9
|
* 职责:前缀路由(parseRelayPath——**SUBAGENT-TAIL(D-ST1):嵌套段内容行并入外层
|
|
@@ -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 形态)。
|
|
@@ -26,34 +26,33 @@ import {
|
|
|
26
26
|
closeSubChild,
|
|
27
27
|
} from "./subagent-children.mjs"
|
|
28
28
|
export { SUB_BLOCK_LINE_LIMIT, appendSubBlock } from "./subagent-children.mjs"
|
|
29
|
-
// 2026-09-05 module-split:finish/freeze 族 + §
|
|
29
|
+
// 2026-09-05 module-split:finish/freeze 族 + AGENT-LOOP-SUBAGENT.md §6.7.2 面板现算迁 subagent-freeze.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
|
-
*
|
|
40
|
-
* stopped
|
|
41
|
-
* queued
|
|
38
|
+
* = 消费面补发(#746:settle 一律发 settled)——收即冻结;settled = 完成(挂起 ∥ 非挂起两态)——
|
|
39
|
+
* 冻结延迟至消费窗(AGENT-LOOP-ASYNC-POOL.md §6.8 freezeReclaimDigestedBlocks 逐条回收——不等池空 ∥ 退出兜底补发);
|
|
40
|
+
* stopped(AGENT-LOOP-SUBAGENT.md §6.7.2 D-M6)= cancel 中止——interrupted 语义立即冻结(标题 "stopped");
|
|
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
|
-
* async
|
|
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
|
|
@@ -78,7 +77,7 @@ export function throttleSubRender(scheduleRender) {
|
|
|
78
77
|
_subRenderTimer.unref?.()
|
|
79
78
|
}
|
|
80
79
|
|
|
81
|
-
/** §
|
|
80
|
+
/** AGENT-LOOP-SUBAGENT.md §6.7.2: key/role 分拆版核心(routeSub* 经 parseRelayPath;兼容 ensureSubTask(match))。 */
|
|
82
81
|
export function ensureSubTaskKey(state, key, role) {
|
|
83
82
|
// Tombstone guard:abort 子代理冻结后晚到 token 不得复活区块(2026-08-30 残项)
|
|
84
83
|
state._frozenSubKeys ??= new Set()
|
|
@@ -100,15 +99,18 @@ 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 §
|
|
104
|
-
stopped: false, // §
|
|
102
|
+
_lineCount: 0, _charCount: 0, // 双维记账(TUI-SESSION-VIEW.md §5.4——行数维 + 字符维,subagent-children.mjs)
|
|
103
|
+
stopped: false, // AGENT-LOOP-SUBAGENT.md §6.7.2: ⟦ev⟧stopped 冻结标记(标题 "stopped")
|
|
105
104
|
children: [], // SUBAGENT-TAIL: 嵌套子代理守护载体(内容并入本块——subagent-children.mjs)
|
|
106
105
|
}
|
|
107
|
-
// §
|
|
106
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2 D-M7b ①(处置 #4 兜底——2026-09-03):缺失 key 的 ⟦ev⟧async 事件已缓冲
|
|
108
107
|
// pending 标志(routeSubToken——async 先于块创建到达:排队条目补位启动的时序窗口)
|
|
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
|
|
|
@@ -150,7 +152,7 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
150
152
|
if (!path) return false
|
|
151
153
|
const payload = path.rest
|
|
152
154
|
const nested = path.inner.length > 0
|
|
153
|
-
// §
|
|
155
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2 D-M7b ① + 处置 #4(2026-09-03 评审 #4——兜底):零字段 async 标记在
|
|
154
156
|
// ensureSubTaskKey **之前**单独消费(async spawn 实际启动即发——sync 不发——精确
|
|
155
157
|
// 匹配:async 后须 RS 或串尾——正文伪前缀形态不误吞;内容侧伪哨兵已由生成侧
|
|
156
158
|
// stripEventToken 先行剥除——本分支只见真事件)。live 区块直接置位 sub.async;
|
|
@@ -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 并入——显示契约
|
|
@@ -239,7 +243,7 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
239
243
|
sub.done = true
|
|
240
244
|
sub.doneAt = Date.now()
|
|
241
245
|
sub.awaitingDigest = true
|
|
242
|
-
// 冻结锚点(2026-09-03 修复轮):settle
|
|
246
|
+
// 冻结锚点(2026-09-03 修复轮):settle 时刻流位置——AGENT-LOOP-ASYNC-POOL.md §6.8 digest 完成逐条回收
|
|
243
247
|
// (freezeReclaimDigestedBlocks)与未消化残项的池空退出兜底(freezeAllSubTasks)
|
|
244
248
|
// 都按它 splice 落位——digest 总览文本之前(round1 #1 裁定——T-S6/T-S14 同口径)。
|
|
245
249
|
sub._freezeAt = state.lines?.length ?? 0
|
|
@@ -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
|
}
|