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
package/src/tui/tool-args.mjs
CHANGED
|
@@ -1,81 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* tool-args.mjs —
|
|
2
|
+
* tool-args.mjs — 工具调用参数的可读展示(**转口档** —— B7 1a 收编:`describeToolArgs` 体迁核件
|
|
3
|
+
* 单源 `@thincoder/core/tool-args.mjs`,本档同名转口、消费面零改;`toolArgsLines`(恢复路径全量
|
|
4
|
+
* pretty JSON dim 行)留端)。
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 恢复的历史会话 = ` [tool] name` 完全没有参数(display snapshot 废弃后
|
|
7
|
-
* historyToLines 是恢复的唯一路径,参数可见性随之丢失——用户报告)。
|
|
6
|
+
* 消费面(零改):`tool-events.mjs`(live 标题行)· `startup.mjs`(历史恢复)· `subagent-blocks.mjs`
|
|
7
|
+
* (子代理块头)。
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 全量必须落行)。
|
|
9
|
+
* 已登记差额(B7 §2 修正轮 1 ⑨):默认分支截断 = 核件简版切片(码元 `slice`)替代本端
|
|
10
|
+
* `render.mjs` `sliceByWidth`(宽度感知 ∕ 全角计宽)——全角字符续接点可异于旧端(160 ∕ 80
|
|
11
|
+
* 两档触发);码元切可断代理对(展示面);机制句单源 = 核件档头。
|
|
13
12
|
*/
|
|
14
|
-
import { sliceByWidth } from "./render.mjs"
|
|
15
13
|
import { capLines, ARGS_JSON_MAX_CHARS } from "./display-budget.mjs"
|
|
16
|
-
|
|
17
|
-
/** 单行参数摘要:按工具挑关键字段(vscode 卡片头对齐),未知工具回退 JSON。 */
|
|
18
|
-
export function describeToolArgs(name, args) {
|
|
19
|
-
if (!args || typeof args !== "object") return ""
|
|
20
|
-
const a = args
|
|
21
|
-
switch (name) {
|
|
22
|
-
case "bash": case "cmd-shell": {
|
|
23
|
-
const cmd = String(a.command ?? "").replace(/\s+/g, " ").trim()
|
|
24
|
-
return cmd ? cmd + (a.workdir ? ` (in ${a.workdir})` : "") : ""
|
|
25
|
-
}
|
|
26
|
-
case "read": case "write": case "edit": case "hashline_edit": {
|
|
27
|
-
const p = a.path ? String(a.path) : ""
|
|
28
|
-
const extras = [
|
|
29
|
-
a.offset ? `offset ${a.offset}` : "",
|
|
30
|
-
a.limit ? `limit ${a.limit}` : "",
|
|
31
|
-
name === "edit" && a.old_string ? `old: ${String(a.old_string).replace(/\s+/g, " ").trim().slice(0, 30)}` : "",
|
|
32
|
-
].filter(Boolean).join(", ")
|
|
33
|
-
return p ? `"${p}"${extras ? " · " + extras : ""}` : extras
|
|
34
|
-
}
|
|
35
|
-
case "grep": case "glob": case "code_search": case "doc_search": case "search": {
|
|
36
|
-
const pat = a.pattern ?? a.query ?? ""
|
|
37
|
-
const p = a.path ? ` in "${a.path}"` : ""
|
|
38
|
-
return `/${String(pat)}/${p}`
|
|
39
|
-
}
|
|
40
|
-
case "ls": {
|
|
41
|
-
const p = a.path ? String(a.path) : "."
|
|
42
|
-
return a.filter ? `${p} (filter: ${String(a.filter)})` : p
|
|
43
|
-
}
|
|
44
|
-
case "websearch": return String(a.query ?? "")
|
|
45
|
-
case "subagent": case "coder": case "explore": case "plan": case "eng-coder": case "eng-designer": {
|
|
46
|
-
const task = String(a.task ?? "").replace(/\s+/g, " ").trim()
|
|
47
|
-
if (task) return task.slice(0, 60) + (task.length > 60 ? "…" : "")
|
|
48
|
-
// action-only 调用(status/observe/send/cancel/escalate/consume-design…)无 task——action 兜底(2.4 批)
|
|
49
|
-
return a.action ? `(${String(a.action)})` : ""
|
|
50
|
-
}
|
|
51
|
-
case "advisor": return String(a.type ?? "review")
|
|
52
|
-
case "read_image": return String(a.path ?? "")
|
|
53
|
-
case "question": return String(a.question ?? "").replace(/\s+/g, " ").trim().slice(0, 60)
|
|
54
|
-
case "memory": {
|
|
55
|
-
// §6 action-routed summary (D-M5): one readable line per action
|
|
56
|
-
const action = String(a.action ?? "")
|
|
57
|
-
if (action === "put") return String(a.title ?? "")
|
|
58
|
-
if (action === "search") return String(a.query ?? "")
|
|
59
|
-
if (action === "list") return [a.layer && `layer ${a.layer}`, a.type && `type ${a.type}`, a.keyword && `kw ${a.keyword}`].filter(Boolean).join(" ")
|
|
60
|
-
if (action === "delete") return a.id ? `id ${a.id}${a.layer ? ` (layer ${a.layer})` : ""}` : `batch ${a.layer ?? ""} ${a.type ? `type ${a.type} ` : ""}${a.keyword ? `kw ${a.keyword}` : ""}`.trim()
|
|
61
|
-
if (action === "clear") return `clear ${a.layer ?? ""}`
|
|
62
|
-
return action || JSON.stringify(a)
|
|
63
|
-
}
|
|
64
|
-
case "lsp": return [a.subcommand, a.uri].filter(Boolean).map(String).join(" ")
|
|
65
|
-
case "repo_outline": return a.path ? String(a.path) : ""
|
|
66
|
-
case "checkpoint": return [a.checkpointAction ?? a.action, a.checkpointId, a.path].filter(Boolean).map(String).join(" ")
|
|
67
|
-
case "git": return [a.action, a.ref, a.name, a.message].filter(Boolean).map(String).join(" ").slice(0, 60)
|
|
68
|
-
default: {
|
|
69
|
-
// MCP 工具 / 未知工具:单行紧凑 JSON(vscode 卡片头同样给原始 JSON 截断)。
|
|
70
|
-
// 硬上限 160:超大 arguments(如 execute 的千字符 code)绝不进单行头。
|
|
71
|
-
const s = JSON.stringify(a)
|
|
72
|
-
return s.length > 160 ? sliceByWidth(s, 156) + "…" : s.length > 80 ? sliceByWidth(s, 78) + "…" : s
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
14
|
+
export { describeToolArgs } from "@thincoder/core/tool-args.mjs"
|
|
76
15
|
|
|
77
16
|
/** 恢复路径用:全量参数 pretty JSON 的 dim 行(非空才输出)。
|
|
78
|
-
* 与工具结果的恢复惯例一致——完整落行;**总量额度**(TUI-OOM-ROOTCAUSE §
|
|
17
|
+
* 与工具结果的恢复惯例一致——完整落行;**总量额度**(TUI-OOM-ROOTCAUSE·TUI-SESSION-VIEW.md §5.1
|
|
79
18
|
* ARGS_JSON_MAX_CHARS——`write`/`apply_patch` 整文件内容进显示层的堵口):超出尾截断
|
|
80
19
|
* + 标记,首行保真。 */
|
|
81
20
|
export function toolArgsLines(args) {
|
package/src/tui/tool-display.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 不禁内容变更(.set/.delete/.push),调用方行为与同模块时代一致。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
// TUI-OOM-ROOTCAUSE(TUI.md §
|
|
10
|
+
// TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.1):显示层额度常量/工具行定位(display-budget 叶子)。
|
|
11
11
|
import { capLines, TOOL_RESULT_MAX_CHARS } from "./display-budget.mjs"
|
|
12
12
|
|
|
13
13
|
// Tool execution start timestamps (performance.now ms). Keyed by tool_call id
|
|
@@ -26,7 +26,7 @@ export const REMINDER_CAP = 3 // max pending reminders shown on tu
|
|
|
26
26
|
export const REMINDER_PERSIST_TURNS = 5 // persist reminders every N turns
|
|
27
27
|
|
|
28
28
|
export const _toolTicks = new Map()
|
|
29
|
-
// §
|
|
29
|
+
// AGENT-LOOP-SUBAGENT.md §6.7 action registry: tool_call id → subagent action (non-spawn only — spawn is
|
|
30
30
|
// the default when no record exists). onToolCall sees the args, onToolResult only
|
|
31
31
|
// the name; without the record every subagent result would route as a spawn.
|
|
32
32
|
export const _subActions = new Map()
|
|
@@ -77,7 +77,7 @@ export function sweepToolBlocks(state) {
|
|
|
77
77
|
* the human needs only the text part (model gets images via the multimodal
|
|
78
78
|
* channel); 2) results beyond maxRows are truncated in the block (full
|
|
79
79
|
* text always lives in history for the model). Returns row array.
|
|
80
|
-
* TUI-OOM-ROOTCAUSE(§
|
|
80
|
+
* TUI-OOM-ROOTCAUSE(§5.1 TOOL_RESULT_MAX_CHARS):总量额度与 400 行双维
|
|
81
81
|
* (单行任意大——无 `\n` 巨 chunk/JSON 尾行),尾截断 + 标记。 */
|
|
82
82
|
export function slimToolResultForDisplay(result, maxRows = 400) {
|
|
83
83
|
let displayResult = result
|
|
@@ -112,10 +112,10 @@ export function settleToolBlock(state, name, toolId, summary) {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
/** Async spawn detection (§
|
|
115
|
+
/** Async spawn detection (AGENT-LOOP-SUBAGENT.md §6.7.3 D-A1): the subagent tool's async:true result is a
|
|
116
116
|
* status JSON ({id, role, status: running|queued}), NOT a report — the child
|
|
117
|
-
* keeps running, so its activity block must not be frozen at spawn time
|
|
118
|
-
*
|
|
117
|
+
* keeps running, so its activity block must not be frozen at spawn time(块保 live——settle 发
|
|
118
|
+
* ⟦ev⟧settled;冻结落消费窗——#746 · ASYNC-POOL §6.8)。 */
|
|
119
119
|
export function isAsyncSpawnResult(result) {
|
|
120
120
|
try {
|
|
121
121
|
const o = JSON.parse(result)
|
|
@@ -125,7 +125,7 @@ export function isAsyncSpawnResult(result) {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
/** §
|
|
128
|
+
/** docs/cli/design/TUI.md §6.8 spawn 门拒错误探测(T-F5):subagent spawn 的机械拒绝以 {status:"error"}
|
|
129
129
|
* JSON 返回(例:manual auto-turn digest spawn 门——digest 语义 organize-only)——
|
|
130
130
|
* 错误路径不得触发完成冻结(round1 #1——错误路径不冻结任何 running 块)。该形态只
|
|
131
131
|
* 出现在无 subKey 的拒绝路径(成功路径恒带 dispatch 传的 ctx._subagentKey)。 */
|
package/src/tui/tool-events.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 满足 500 行硬限)。只做「事件 → TUI 状态/对话行」的映射:
|
|
4
4
|
*
|
|
5
5
|
* - onToken/onReasoning : 子agent 前缀分流(routeSub*)→ 主流 streaming/reasoning
|
|
6
|
-
* - onToolCall : 状态栏 + `❯ name args` 标题行 + 计时 + §
|
|
6
|
+
* - onToolCall : 状态栏 + `❯ name args` 标题行 + 计时 + AGENT-LOOP-SUBAGENT.md §6.7 action 记录
|
|
7
7
|
* - onToolResult : 子agent 完成冻结(finishSubTaskKey 精确冻 + freezeDoneSubTasks——
|
|
8
8
|
* F-2 后 finishSubTask 为 no-op 兼容保留)、
|
|
9
9
|
* 工具块结果入块、advisor 评审冻结框
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* - 其余 : usage 累计、等待提示、task 面板、回合末增量落盘
|
|
12
12
|
*
|
|
13
13
|
* flushStream 同时返回给调用方(回合循环 / onTurnEnd 共用)。纯回调装配,无终端副作用
|
|
14
|
-
* (除经 deps 注入的 pushLine/render
|
|
15
|
-
*
|
|
14
|
+
* (除经 deps 注入的 pushLine/render)。AGENT-LOOP-SUBAGENT.md §6.7: subagent_check/escalate 工具退役后
|
|
15
|
+
* (AGENT-LOOP-SUBAGENT.md §6.7.5——check 动作已删),subagent 家族全部调用以工具名 "subagent" +
|
|
16
16
|
* action 到达——完成路由按 onToolCall 时记录的 action 分流(spawn 区块 / escalate
|
|
17
17
|
* 区块 / status/observe/send 普通工具块——observe/send 不建子代理区块)。
|
|
18
18
|
*/
|
|
@@ -22,11 +22,11 @@ import { describeToolArgs, toolArgsLines } from "./tool-args.mjs"
|
|
|
22
22
|
import { ADVISOR_THINKING_PLACEHOLDER, resolveAdvisorProvider } from "@thincoder/core/advisor/run.mjs"
|
|
23
23
|
import {
|
|
24
24
|
SUBAGENT_ROLES, routeSubToken, routeSubReasoning, routeSubToolCall,
|
|
25
|
-
routeSubToolOutput, finishSubTask, finishSubTaskKey,
|
|
25
|
+
routeSubToolOutput, finishSubTask, finishSubTaskKey, freezeDoneSubTasks,
|
|
26
26
|
ensureCompressPanel, markCompressFailed, markCompressDone, markCompressFallback,
|
|
27
27
|
shiftFreezeAnchors,
|
|
28
28
|
} from "./subagent-blocks.mjs"
|
|
29
|
-
// 第 27
|
|
29
|
+
// 第 27 批:前缀正则换名 + import 源改文法模块(纯换名——语义零改)。
|
|
30
30
|
import { RELAY_PREFIX_RE } from "@thincoder/core/agent/relay-prefix.mjs"
|
|
31
31
|
import { TURN_CAP_MARK, STOPPED_MARK } from "@thincoder/core/agent/spawn-child.mjs"
|
|
32
32
|
// 批 1 CORE-DEFECT-FIXES B3:onWait 相位值域 + 文案单源(消费面禁各自枚举——PROVIDER.md §6.20)
|
|
@@ -40,14 +40,14 @@ import {
|
|
|
40
40
|
findToolBlock, findToolLine, slimToolResultForDisplay, sweepToolBlocks,
|
|
41
41
|
} from "./tool-display.mjs"
|
|
42
42
|
export { sweepToolBlocks, slimToolResultForDisplay } from "./tool-display.mjs"
|
|
43
|
-
// TUI-OOM-ROOTCAUSE(TUI.md §
|
|
43
|
+
// TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.1/§5.3):显示层额度(行/载体/输出环/评审/流式)。
|
|
44
44
|
import {
|
|
45
45
|
capText, capLine, capLines, appendCapped, capAdvisorText, accountLine, syncLineBudget,
|
|
46
46
|
LINE_TRUNC_MARKER, MIDDLE_TRUNC_MARKER, ADVISOR_CAP_OPTS, STREAM_CAP_OPTS,
|
|
47
47
|
TOOL_OUTPUT_ENTRY_MAX_CHARS, TOOL_OUTPUT_TOTAL_MAX_CHARS, ADVISOR_TEXT_MAX_CHARS,
|
|
48
48
|
} from "./display-budget.mjs"
|
|
49
49
|
|
|
50
|
-
/** 输出环字符维总量(§
|
|
50
|
+
/** 输出环字符维总量(§5.1 TOOL_OUTPUT_TOTAL_MAX_CHARS——与 200 条目环同款丢最旧)。 */
|
|
51
51
|
function trimOutputChars(block) {
|
|
52
52
|
let total = 0
|
|
53
53
|
for (const s of block.output) total += s.length
|
|
@@ -60,9 +60,9 @@ function trimOutputChars(block) {
|
|
|
60
60
|
* askPermission, askQuestion, saveSessionImpl } */
|
|
61
61
|
export function buildToolCallbacks(deps) {
|
|
62
62
|
const { agent, state, pushLine, render, scheduleRender, ensureAssistantLabel, askPermission, askBatchPermission, askQuestion, saveSessionImpl } = deps
|
|
63
|
-
// 行集总量对账便捷式(§
|
|
63
|
+
// 行集总量对账便捷式(§5.3——冻结锚点平移交 subagent 族函数)
|
|
64
64
|
const budgetSync = () => syncLineBudget(state, { onTrim: (st, n) => shiftFreezeAnchors(st, n) })
|
|
65
|
-
/** Advisor 有序块累积 + 额度(§
|
|
65
|
+
/** Advisor 有序块累积 + 额度(§5.1 ADVISOR_TEXT_MAX_CHARS——头 32K/尾 96K/中段标记)。 */
|
|
66
66
|
const pushAdvisorChunk = (raw, kind) => {
|
|
67
67
|
const blocks = state._advisorBlocks ??= []
|
|
68
68
|
const last = blocks.at(-1)
|
|
@@ -100,7 +100,8 @@ export function buildToolCallbacks(deps) {
|
|
|
100
100
|
// block content, never the main stream (D1). Routing details in subagent-blocks.
|
|
101
101
|
if (routeSubToken(state, t, scheduleRender)) return
|
|
102
102
|
ensureAssistantLabel()
|
|
103
|
-
state.streaming = appendCapped(state.streaming, t, STREAM_CAP_OPTS) // §
|
|
103
|
+
state.streaming = appendCapped(state.streaming, t, STREAM_CAP_OPTS) // §5.1 STREAM_MAX_CHARS
|
|
104
|
+
state.lastOutputAt = Date.now() // 停滞轻显形(TUI.md §7.7):①流式——主文本增量
|
|
104
105
|
scheduleRender()
|
|
105
106
|
},
|
|
106
107
|
onReasoning: (t) => {
|
|
@@ -108,14 +109,15 @@ export function buildToolCallbacks(deps) {
|
|
|
108
109
|
// buffer as kind=think (F2: same treatment as the main reasoning stream).
|
|
109
110
|
if (routeSubReasoning(state, t, scheduleRender)) return
|
|
110
111
|
ensureAssistantLabel()
|
|
111
|
-
state.reasoning = appendCapped(state.reasoning, t, STREAM_CAP_OPTS) // §
|
|
112
|
+
state.reasoning = appendCapped(state.reasoning, t, STREAM_CAP_OPTS) // §5.1 STREAM_MAX_CHARS
|
|
113
|
+
state.lastOutputAt = Date.now() // 停滞轻显形(TUI.md §7.7):①流式——推理增量
|
|
112
114
|
scheduleRender()
|
|
113
115
|
},
|
|
114
116
|
onToolCall: (name, args, toolId) => {
|
|
115
117
|
// Subagent tool call: prefix role#id/toolName → open a fresh tool block and
|
|
116
118
|
// set currentTool for the header summary line.
|
|
117
119
|
if (routeSubToolCall(state, name, args, scheduleRender)) return
|
|
118
|
-
// §
|
|
120
|
+
// AGENT-LOOP-SUBAGENT.md §6.7: record the action of a merged subagent-family call (spawn is the
|
|
119
121
|
// default — only non-spawn actions need a record for result-time routing).
|
|
120
122
|
if (name === "subagent" && args?.action && args.action !== "spawn") {
|
|
121
123
|
if (toolId !== undefined && toolId !== null) _subActions.set(toolId, args.action)
|
|
@@ -127,6 +129,7 @@ export function buildToolCallbacks(deps) {
|
|
|
127
129
|
flushStream()
|
|
128
130
|
ensureAssistantLabel()
|
|
129
131
|
state.currentTool = name
|
|
132
|
+
state.lastOutputAt = Date.now() // 停滞轻显形(TUI.md §7.7):②工具面——调用(标题行 ∕ 状态栏)
|
|
130
133
|
// Advisor's effective model (resolved once for the status line + inline title below).
|
|
131
134
|
const advModel = name === "advisor" ? (() => { try { return resolveAdvisorProvider(agent).model } catch { return null } })() : null
|
|
132
135
|
// Update status bar with current tool and key arguments for user visibility
|
|
@@ -170,16 +173,17 @@ export function buildToolCallbacks(deps) {
|
|
|
170
173
|
done: false,
|
|
171
174
|
},
|
|
172
175
|
})
|
|
173
|
-
accountLine(state, state.lines[state.lines.length - 1]) // 载体入总量账(§
|
|
176
|
+
accountLine(state, state.lines[state.lines.length - 1]) // 载体入总量账(§5.4)
|
|
174
177
|
budgetSync()
|
|
175
178
|
tickStart(name, toolId)
|
|
176
179
|
},
|
|
177
|
-
// §
|
|
180
|
+
// docs/cli/design/TUI.md §6.8(方案 e):dispatch runOne 把工具 ctx 上的 _subagentKey(sync spawn/
|
|
178
181
|
// escalate 成功路径设置——relayPrefix 去尾)作为第 4 参传来——undefined 兼容既有
|
|
179
182
|
// 签名(普通工具/老回调/错误路径不带 key)。
|
|
180
183
|
onToolResult: (name, result, toolId, subKey) => {
|
|
181
184
|
state.currentTool = null
|
|
182
|
-
|
|
185
|
+
state.lastOutputAt = Date.now() // 停滞轻显形(TUI.md §7.7):②工具面——结果(含子代理 ∕ escalate ∕ advisor 面同刻)
|
|
186
|
+
// AGENT-LOOP-SUBAGENT.md §6.7 merged family: route per the action recorded at onToolCall (no record = default spawn).
|
|
183
187
|
let subAction = null
|
|
184
188
|
if (name === "subagent") {
|
|
185
189
|
subAction = (toolId !== undefined && toolId !== null)
|
|
@@ -192,18 +196,18 @@ export function buildToolCallbacks(deps) {
|
|
|
192
196
|
// Subagent complete: mark the earliest running child as done — the block
|
|
193
197
|
// persists (✓ frozen elapsed header, expandable) as the ONLY carrier of the
|
|
194
198
|
// child's activity; memory bounded by the N2 line cap.
|
|
195
|
-
// §
|
|
199
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2: cancel 动作排除在 isSubagent 外——ack/错误 JSON 走普通工具块;区块冻结由
|
|
196
200
|
// ⟦ev⟧stopped settle 事件承担(此处 finishSubTask 会误冻最早 running 区块)。
|
|
197
201
|
if (isSubagent) {
|
|
198
202
|
// The dispatch-level tool-block carrier for this call would otherwise
|
|
199
203
|
// never be marked done (its result lands in the subagent block, not the
|
|
200
204
|
// carrier) and the turn sweep would mislabel it "(interrupted)".
|
|
201
205
|
settleToolBlock(state, name, toolId, "completed")
|
|
202
|
-
// Async spawn (§
|
|
206
|
+
// Async spawn (AGENT-LOOP-SUBAGENT.md §6.7.3 D-A1): the result is a status JSON, not a report — the
|
|
203
207
|
// child KEEPS running; skip the freeze (it would tombstone a live block and
|
|
204
|
-
// drop its relay stream).
|
|
208
|
+
// drop its relay stream). 块保 live——settle 发 ⟦ev⟧settled;冻结落消费窗(#746 · ASYNC-POOL §6.8)。
|
|
205
209
|
if (!isAsyncSpawnResult(result)) {
|
|
206
|
-
// §
|
|
210
|
+
// §6.8 sync spawn 完成精确冻结:结果按 key 归属三支——
|
|
207
211
|
// ① dispatch 同步成功路径带 subKey(ctx._subagentKey = relayPrefix 去尾):
|
|
208
212
|
// finishSubTaskKey 按 key 精确冻——不再落 finishSubTask 的"最早 started"
|
|
209
213
|
// 启发式(async eng-coder 先启动时 explore 完成会误冻其块——7.2.3.1/T-F2);
|
|
@@ -232,12 +236,12 @@ export function buildToolCallbacks(deps) {
|
|
|
232
236
|
// 飞刀 post-op report landed under the subagent tool name — freeze the
|
|
233
237
|
// escalate#N activity block (no preview; legacy surface).
|
|
234
238
|
settleToolBlock(state, name, toolId, "completed")
|
|
235
|
-
// §
|
|
239
|
+
// ESCALATE.md §5 D-R17b (R17): async escalate ack ({id, role:"escalate", status:
|
|
236
240
|
// running|queued}) — the child KEEPS running — skip the freeze like the
|
|
237
|
-
// async spawn path (
|
|
238
|
-
//
|
|
241
|
+
// async spawn path (settle ⇒ ⟦ev⟧settled 驻留;冻结落消费窗——#746 · ASYNC-POOL §6.8;
|
|
242
|
+
// cancel 径 ⟦ev⟧stopped 即冻——照旧). Sync results (async:false) freeze below.
|
|
239
243
|
if (!isAsyncSpawnResult(result)) {
|
|
240
|
-
// §
|
|
244
|
+
// §6.8(round1 #2):escalate 成功返回带 subKey(escalate#N)→ 精确冻;
|
|
241
245
|
// 失败/老回调无 subKey → 不冻结任何块(F-2 收窄——finishSubTask 恒 no-op,
|
|
242
246
|
// 块由回合尾 freezeAllSubTasks 兜底清场)。
|
|
243
247
|
// SYNC-CANCEL(R6):同上——escalate 路径同款扩展(sync escalate 无 registry——
|
|
@@ -251,12 +255,12 @@ export function buildToolCallbacks(deps) {
|
|
|
251
255
|
freezeDoneSubTasks(state)
|
|
252
256
|
}
|
|
253
257
|
} else if (name === "consult_stop") {
|
|
254
|
-
// R17
|
|
255
|
-
// children 各自 settle(settleChild)时发 ⟦ev⟧
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
258
|
+
// R17(CONSULTATION.md §6.2 D-R17a——check 已删):consult_stop = 取消指定会诊会话。被取消会话的
|
|
259
|
+
// children 各自 settle(settleChild)时发 ⟦ev⟧stopped 即折自己的卡(cancel 径——消费永不来);
|
|
260
|
+
// 未取消边 settled 的子块驻留待消化(消费窗三端收口——#748)——此处**不做按角色整组
|
|
261
|
+
// 清扫**(会诊会话可并发——按角色会误冻其他仍在运行的会话的卡,冻结即截断其活动流——
|
|
262
|
+
// tool-events advisor 复评 🟡1 修正);取消的即时可见性由 abort settle 事件承担(abort
|
|
263
|
+
// 解绕通常在同回合内完成——卡片在其 child settle 即折)。仅结果本身落本工具调用自己的载体块(下方通用分支)。
|
|
260
264
|
}
|
|
261
265
|
if (!isSubagent && !isEscalate && name !== "advisor") {
|
|
262
266
|
// Result lands INSIDE the block (restore parity — the restored carrier
|
|
@@ -269,7 +273,7 @@ export function buildToolCallbacks(deps) {
|
|
|
269
273
|
// screen of garbage (user report 2026-08-30). The model gets the image
|
|
270
274
|
// via the multimodal channel (agent.mjs), the human needs only the
|
|
271
275
|
// text part: strip image parts from the displayed result.
|
|
272
|
-
block.result = slimToolResultForDisplay(result) // §
|
|
276
|
+
block.result = slimToolResultForDisplay(result) // §5.1 RESULT 额度(行 400 + 字符 64K 双维)
|
|
273
277
|
block.summary = formatToolSummary(name, result)
|
|
274
278
|
block.done = true
|
|
275
279
|
const started = tickTake(name, toolId)
|
|
@@ -290,7 +294,7 @@ export function buildToolCallbacks(deps) {
|
|
|
290
294
|
// _advisorBlocks keep rendering the running view until cleared at turn end.
|
|
291
295
|
const blocks = state._advisorBlocks ?? []
|
|
292
296
|
if (blocks.length > 0) {
|
|
293
|
-
// 冻结文本额度(§
|
|
297
|
+
// 冻结文本额度(§5.1:头 32K + 尾 96K 保裁决尾部 + 中段标记——非静默截断,
|
|
294
298
|
// 代码评审 #3:capLines(...)[0] 会丢标记且截尾)
|
|
295
299
|
const text = capAdvisorText(blocks
|
|
296
300
|
.map((b) => b.text.replaceAll(ADVISOR_THINKING_PLACEHOLDER, ""))
|
|
@@ -326,6 +330,7 @@ export function buildToolCallbacks(deps) {
|
|
|
326
330
|
}
|
|
327
331
|
if (!part.text) return
|
|
328
332
|
if (routeSubToolOutput(state, name, part, scheduleRender)) return
|
|
333
|
+
state.lastOutputAt = Date.now() // 停滞轻显形(TUI.md §7.7):②工具面——输出流(含 advisor 输出)
|
|
329
334
|
if (name === "advisor") {
|
|
330
335
|
// Accumulate to buffer — formatTables + wrapText in render-conversation
|
|
331
336
|
// handles markdown formatting, same as main agent response.
|
|
@@ -338,14 +343,14 @@ export function buildToolCallbacks(deps) {
|
|
|
338
343
|
const isString = typeof chunk === "string"
|
|
339
344
|
const raw = isString ? chunk : String(chunk?.text ?? "")
|
|
340
345
|
const kind = isString ? "text" : (chunk?.kind ?? "text")
|
|
341
|
-
pushAdvisorChunk(raw, kind) // §
|
|
346
|
+
pushAdvisorChunk(raw, kind) // §5.1 累积额度(多块丢最旧)
|
|
342
347
|
scheduleRender()
|
|
343
348
|
return
|
|
344
349
|
}
|
|
345
350
|
// Append into the CURRENT tool block's output buffer (the block is the
|
|
346
351
|
// display; no _live scroll lines anymore). N2-style cap keeps memory
|
|
347
352
|
// bounded: keep the LAST 200 output lines per call; 字符维双维
|
|
348
|
-
// (§
|
|
353
|
+
// (§5.1:单条目 ≤ TOOL_OUTPUT_ENTRY_MAX_CHARS、总量 ≤ TOOL_OUTPUT_TOTAL_MAX_CHARS 丢最旧)。
|
|
349
354
|
const block = findToolBlock(state, name, toolId)
|
|
350
355
|
if (block) {
|
|
351
356
|
for (const line of part.text.split("\n")) {
|
|
@@ -362,10 +367,10 @@ export function buildToolCallbacks(deps) {
|
|
|
362
367
|
scheduleRender()
|
|
363
368
|
},
|
|
364
369
|
// Manual-tier auto-turn digests (agent-turn.mjs suspension driver) pass null
|
|
365
|
-
// handlers — permission requests then deny WITHOUT a panel (§
|
|
370
|
+
// handlers — permission requests then deny WITHOUT a panel (AGENT-LOOP-ASYNC-POOL.md §6.8 D-S7: no modal
|
|
366
371
|
// during unattended digestion) and question errors out instead of hanging.
|
|
367
372
|
...(askPermission ? { onPermissionRequest: (name, args) => askPermission(name, args) } : {}),
|
|
368
|
-
// Merged batch ask (
|
|
373
|
+
// Merged batch ask (D-B1): one confirmation for N non-readonly tools in
|
|
369
374
|
// the same response — "approve all / one by one / deny" (key-handler resolves
|
|
370
375
|
// the verdict string; approveAll is batch-scope only, never the AUTO flag).
|
|
371
376
|
...(askBatchPermission ? { onBatchPermissionRequest: (req) => askBatchPermission(req) } : {}),
|
|
@@ -43,7 +43,7 @@ export function writeCleanupSequence(write = (s) => process.stdout.write(s)) {
|
|
|
43
43
|
// T-R25a.2 注入 mock 走同一 setter)。
|
|
44
44
|
let tuiActive = false
|
|
45
45
|
|
|
46
|
-
/** 置/清 TUI 活动态(TUI
|
|
46
|
+
/** 置/清 TUI 活动态(TUI 启动接管处与退出清理处调用;测试钩(argv 门)注入同走此 setter)。 */
|
|
47
47
|
export function setTuiActive(active) {
|
|
48
48
|
tuiActive = active === true
|
|
49
49
|
}
|
|
@@ -53,13 +53,22 @@ export function isTuiActive() {
|
|
|
53
53
|
return tuiActive
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// R25 崩溃恢复测试缝(T-R25a.2——argv 门注入):bin 入口解析 `--test-cleanup-out=<路径>` 后经
|
|
57
|
+
// setter 注入(启动时一次性读点——单半例外,复位面无消费者);生产零调用 → null(恒 stdout)。
|
|
58
|
+
let _cleanupOut = null
|
|
59
|
+
|
|
60
|
+
/** 测试缝 setter(仅测试注入——生产零调用)。 */
|
|
61
|
+
export function _setCleanupOutPathForTest(path) {
|
|
62
|
+
_cleanupOut = typeof path === "string" && path.length > 0 ? path : null
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
/** R25 崩溃恢复:仅 TUI 活动态执行 writeCleanupSequence(复用本模块清理序列——符号锚)。
|
|
57
|
-
* 测试缝(T-R25a.2——
|
|
58
|
-
* (观察恢复被调 + stdout 管道零 ANSI
|
|
66
|
+
* 测试缝(T-R25a.2——argv 门注入):`_setCleanupOutPathForTest` 指到文件时序列写入该文件
|
|
67
|
+
* (观察恢复被调 + stdout 管道零 ANSI)——生产不指 → 恒 stdout(零变化)。
|
|
59
68
|
* @returns {boolean} 是否执行了恢复(false = TUI 未启动——调用方无需处理) */
|
|
60
69
|
export function restoreTerminalAfterCrash() {
|
|
61
70
|
if (!tuiActive) return false
|
|
62
|
-
const seamOut =
|
|
71
|
+
const seamOut = _cleanupOut
|
|
63
72
|
if (seamOut) {
|
|
64
73
|
writeCleanupSequence((s) => appendFileSync(seamOut, s, "utf8"))
|
|
65
74
|
} else {
|
package/src/tui/tui-state.mjs
CHANGED
|
@@ -37,9 +37,10 @@ export function createTuiState({ cols, rows, agent }) {
|
|
|
37
37
|
reasoning: "", // thinking stream buffer (dimmed display)
|
|
38
38
|
completion: null, // Tab completion state { candidates, index }
|
|
39
39
|
|
|
40
|
-
subTasks: {}, // sub-agent activity blocks (§
|
|
40
|
+
subTasks: {}, // sub-agent activity blocks (docs/cli/design/TUI.md §6.8 D4): { "coder#1": { key, role, model, started, done, doneAt, blocks: [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError, dropped, blockEpoch, awaitingDigest(AGENT-LOOP-ASYNC-POOL.md §6.8 挂起中间态), _freezeAt(冻结锚点), stopped, children: [](SUBAGENT-TAIL:嵌套子代理**守护载体**——内容行并入本块 blocks——subagent-children.mjs) } } — rendered as collapsible in-conversation blocks; persists across turns (blocks are the child activity's ONLY carrier — child tool calls never enter the parent history); bounded by the N2 500-line per-child ring buffer(SUBAGENT-TAIL 单环:内层行同环计数、单载体最旧先行——TUI.md §6)
|
|
41
41
|
currentTool: null, // currently executing tool name (shown in status bar)
|
|
42
42
|
processingStarted: 0, // current turn start time (status bar timer)
|
|
43
|
+
lastOutputAt: 0, // 停滞轻显形(TUI.md §7.7 · stall-indicator 批):最近一次可见输出时刻(静默起算——回合起刻为初始锚;0 = 未起算)
|
|
43
44
|
status: "Ready",
|
|
44
45
|
ledger: { marker: null, warn: false, scannedAt: 0 }, // LEDGER-SURFACE(§2.30.3.4):L1 标记位——ledger-surface 写 / render-frame 读
|
|
45
46
|
queue: [], // 交接残项单容器 [{ text }](INPUT-LOCK:submit 不再排队——仅释放窗口兜底/挂起中止残余——回合尾队列循环续发,至多一条)
|
|
@@ -54,7 +55,7 @@ export function createTuiState({ cols, rows, agent }) {
|
|
|
54
55
|
_historyLoaded: 0, // messages loaded from the TAIL of _fullHistory
|
|
55
56
|
_historyTotal: 0, // total messages in the restored session
|
|
56
57
|
_hasOlder: false, // more earlier messages remain unloaded
|
|
57
|
-
_linesChars: 0, // TUI-OOM-ROOTCAUSE
|
|
58
|
+
_linesChars: 0, // TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.2):state.lines 全部行与载体文本总量(字符账——唯一新增状态位)
|
|
58
59
|
_agent: null, // TUI state → agent 回指挂载点:startTUI 装配时置 agent 引用——
|
|
59
60
|
// ① SYNC-CANCEL ⏹ 门控读 state._agent._syncChildAborts(subagent-panel.mjs,2026-09-09);
|
|
60
61
|
// ② CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):agent._tuiState = state 反向挂载——
|
package/src/tui/turn-face.mjs
CHANGED
|
@@ -15,8 +15,8 @@ export function createTurnFace(ctx) {
|
|
|
15
15
|
async function submit() {
|
|
16
16
|
const text = state.input.join("").trim()
|
|
17
17
|
if (!text) return
|
|
18
|
-
// INPUT-LOCK 防御(C'——2026-09-09 + F16 busy
|
|
19
|
-
// (busy Enter 非模态 =
|
|
18
|
+
// INPUT-LOCK 防御(C'——2026-09-09 + F16 busy 队列——2026-09-21 · queue-visible 2026-09-24):门禁在 key-handler
|
|
19
|
+
// (busy Enter 非模态 = 队列受理(容量 8)/ 模态·斜杠·空·满队 = 吞 + 提示,文本保留——TUI-INPUT-BOX.md
|
|
20
20
|
// §4.1);submit 只在非 busy 期达此;防御直呼/上游改动:busy 期拒绝(不清输入框不吞内容)。
|
|
21
21
|
if (state.processing) {
|
|
22
22
|
pushLine(`[主会话处理中 —— 消息未发送(回合结束后请重按 Enter)]`, C.warn)
|
|
@@ -55,10 +55,13 @@ export function createTurnFace(ctx) {
|
|
|
55
55
|
const turnCtx = {
|
|
56
56
|
agent, state, pushLine, pushLabel, render, scheduleRender: render, ensureAssistantLabel,
|
|
57
57
|
askPermission, askQuestion, askBatchPermission, handleSlash: null,
|
|
58
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.30.5 载体②:空闲 idle deadline 闩(index.mjs 装配后回填——同 handleSlash 惰性回填法)
|
|
59
|
+
timerWatch: null,
|
|
58
60
|
get assistantLabeled() { return conversation.assistantLabeled },
|
|
59
61
|
set assistantLabeled(v) { conversation.assistantLabeled = v },
|
|
60
62
|
}
|
|
61
|
-
|
|
63
|
+
// opts 透传(timer 空闲唤醒轮 = `{ autoTurn: true, timerTurn: true }`——§6.30.3 开轮两处①)
|
|
64
|
+
const turn = (text, opts) => runAgentTurn(turnCtx, text, opts)
|
|
62
65
|
|
|
63
66
|
return { submit, turn, turnCtx, askPermission, askQuestion, askBatchPermission, pasteClipboardImage }
|
|
64
67
|
}
|
package/src/tui/wizard.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { probeChannelModels } from "./model-catalog.mjs"
|
|
|
16
16
|
* Returns { startWizard, renderWizard, wizardChooseProvider, wizardSubmitText, cancelWizard, finishWizard }
|
|
17
17
|
*/
|
|
18
18
|
export function createWizard(ctx) {
|
|
19
|
-
const { agent, state, pushLine, pushLabel, render, persistRaw } = ctx
|
|
19
|
+
const { agent, state, pushLine, pushLabel, render, persistRaw, onModalClose = null } = ctx
|
|
20
20
|
|
|
21
21
|
/** Candidates for the menu step: existing providers (marked "no key" if missing), unadded presets, custom
|
|
22
22
|
* MODEL-SELECTION v2:渠道默认模型 = 单值 `model`(preset 自带;候选清单运行期拉取) */
|
|
@@ -166,6 +166,8 @@ export function createWizard(ctx) {
|
|
|
166
166
|
: "Skipped initial setup. Use /model to add providers and configure API keys anytime."
|
|
167
167
|
pushLine(hint, C.dim)
|
|
168
168
|
render()
|
|
169
|
+
// #448①「关闭后补评估」:wizard 面退场(无 picker 同在场)⇒ 通知链尾(闩重武装)
|
|
170
|
+
if (state.picker == null) ctx.onModalClose?.()
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
/** Wizard complete: write provider (update if exists) with its single default model (`model`),
|
|
@@ -234,7 +236,10 @@ export function createWizard(ctx) {
|
|
|
234
236
|
pushLine(`Vector search disabled (memory falls back to text-only search). Run /config embedkey <key> to enable.`, C.dim)
|
|
235
237
|
}
|
|
236
238
|
pushLine(`Select model (Esc to keep ${agent.activeModel})`, C.dim)
|
|
237
|
-
|
|
239
|
+
// #448①「关闭后补评估」:收尾链落定(模型 picker 关闭 ∕ 零弹面两态)⇒ 通知链尾(闩重武装——幂等)。
|
|
240
|
+
ctx.openModelPicker()
|
|
241
|
+
.catch((e) => pushLine(`[error] ${e.message}`, C.error))
|
|
242
|
+
.then(() => { if (state.picker == null && state.wizard == null) ctx.onModalClose?.() })
|
|
238
243
|
}
|
|
239
244
|
|
|
240
245
|
return { startWizard, renderWizard, wizardChooseProvider, wizardSubmitText, cancelWizard, finishWizard, wizardProviderItems }
|
|
@@ -44,10 +44,10 @@ export function spawnTuiWrapped({ dir = crashReportsDir(), script = fileURLToPat
|
|
|
44
44
|
try { writeImpl(RECOVERY_SEQUENCE) } catch { /* 尽力面:终端已关/管道已断——不阻断收尾 */ }
|
|
45
45
|
}
|
|
46
46
|
// F3③(CRASH-REPORTS):近堆上限快照落点定向——--diagnostic-dir 为 Node 选项,须在脚本路径前;
|
|
47
|
-
//
|
|
48
|
-
const child = spawnImpl(process.execPath, [`--diagnostic-dir=${dir}`, script, ...process.argv.slice(2)], {
|
|
47
|
+
// 无条件注入(判定单点在 bin 入口读配置键 `diagnostics.heapSnapshot` 后经参数传入——包装器不判);未触发快照时零副作用。
|
|
48
|
+
const child = spawnImpl(process.execPath, [`--diagnostic-dir=${dir}`, script, "--tui-wrapped", ...process.argv.slice(2)], {
|
|
49
49
|
stdio: ["inherit", "inherit", "pipe"], // 子 stderr pipe → tee;stdin/stdout 继承(TTY 原样)
|
|
50
|
-
|
|
50
|
+
// argv 门(子内 bin 顶部自剥离 `--tui-wrapped`、判定不包装——纯现逻辑(红线));env 直传
|
|
51
51
|
windowsHide: false,
|
|
52
52
|
})
|
|
53
53
|
child.stderr.on("data", (chunk) => { try { process.stderr.write(chunk) } catch { /* 终端已关——日志仍落 */ } note(chunk) }) // ② tee 双写:终端实时 + 日志
|