thincoder 0.12.58 → 0.12.60
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 +78 -2
- package/README.md +3 -3
- package/bin/thincoder.mjs +88 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +135 -26
- package/src/advisor/messages.mjs +57 -4
- package/src/advisor/run.mjs +119 -79
- package/src/advisor.mjs +34 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +182 -22
- package/src/agent/helpers.mjs +71 -4
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +107 -20
- package/src/agent/spawn-child.mjs +54 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +133 -109
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +154 -104
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +26 -30
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +284 -0
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +152 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +470 -0
- package/src/agent-tools/subagent-async.mjs +382 -0
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +343 -0
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +203 -377
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +198 -238
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +145 -242
- package/src/auto-think.mjs +14 -0
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +4 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +61 -8
- package/src/context.mjs +81 -163
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +30 -12
- package/src/escape.mjs +6 -4
- package/src/explore-distill.mjs +155 -0
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +11 -72
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +206 -87
- package/src/memory.mjs +3 -1
- package/src/model-specs.mjs +15 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +18 -39
- package/src/prompts/advisor-round1.md +20 -32
- package/src/prompts/advisor-round2.md +16 -16
- package/src/prompts/advisor-round3.md +16 -16
- package/src/prompts/coder.md +7 -28
- package/src/prompts/consult-base.md +4 -11
- package/src/prompts/discipline.md +31 -44
- package/src/prompts/eng-coder.md +9 -34
- package/src/prompts/engineering-sub.md +10 -8
- package/src/prompts/engineering.md +61 -264
- package/src/prompts/explore.md +4 -14
- package/src/prompts/main.md +18 -35
- package/src/prompts/methodology-template.md +32 -38
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +18 -35
- package/src/provider/core.mjs +62 -69
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +131 -44
- package/src/tools/edit-diff.mjs +348 -0
- package/src/tools/edit.md +20 -13
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +55 -24
- package/src/tools/file.mjs +25 -70
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -3
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +84 -13
- package/src/tools/question.md +5 -1
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +18 -25
- package/src/tools/system.mjs +50 -30
- package/src/tools/tree.md +2 -1
- package/src/tools/wait_for.md +22 -0
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +179 -27
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +77 -16
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +17 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +11 -11
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +31 -96
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +46 -0
- package/src/tui/pickers.mjs +51 -25
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +182 -0
- package/src/tui/startup.mjs +40 -0
- package/src/tui/subagent-blocks.mjs +272 -262
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +125 -12
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +127 -231
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +76 -0
- package/src/tui/wizard.mjs +48 -12
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
package/src/tui/tool-events.mjs
CHANGED
|
@@ -3,145 +3,37 @@
|
|
|
3
3
|
* 满足 500 行硬限)。只做「事件 → TUI 状态/对话行」的映射:
|
|
4
4
|
*
|
|
5
5
|
* - onToken/onReasoning : 子agent 前缀分流(routeSub*)→ 主流 streaming/reasoning
|
|
6
|
-
* - onToolCall : 状态栏 + `❯ name args` 标题行 + 计时
|
|
6
|
+
* - onToolCall : 状态栏 + `❯ name args` 标题行 + 计时 + §19 action 记录
|
|
7
7
|
* - onToolResult : 子agent 完成冻结(finishSubTask + freezeDoneSubTasks)、
|
|
8
|
-
*
|
|
9
|
-
* - onToolOutput : advisor 有序块缓冲 /
|
|
8
|
+
* 工具块结果入块、advisor 评审冻结框
|
|
9
|
+
* - onToolOutput : advisor 有序块缓冲 / 工具块输出流
|
|
10
10
|
* - 其余 : usage 累计、等待提示、task 面板、回合末增量落盘
|
|
11
11
|
*
|
|
12
12
|
* flushStream 同时返回给调用方(回合循环 / onTurnEnd 共用)。纯回调装配,无终端副作用
|
|
13
|
-
* (除经 deps 注入的 pushLine/render
|
|
13
|
+
* (除经 deps 注入的 pushLine/render)。§19: subagent_check/escalate 工具退役后
|
|
14
|
+
* (§19.8——check 动作已删),subagent 家族全部调用以工具名 "subagent" +
|
|
15
|
+
* action 到达——完成路由按 onToolCall 时记录的 action 分流(spawn 区块 / escalate
|
|
16
|
+
* 区块 / status/observe/send 普通工具块——observe/send 不建子代理区块)。
|
|
14
17
|
*/
|
|
15
18
|
import { C } from "./ansi.mjs"
|
|
16
19
|
import { formatToolSummary } from "./tool-summaries.mjs"
|
|
17
20
|
import { describeToolArgs, toolArgsLines } from "./tool-args.mjs"
|
|
18
21
|
import { ADVISOR_THINKING_PLACEHOLDER, resolveAdvisorProvider } from "../advisor/run.mjs"
|
|
19
22
|
import {
|
|
20
|
-
SUBAGENT_ROLES, routeSubToken, routeSubReasoning, routeSubToolCall,
|
|
21
|
-
routeSubToolOutput, finishSubTask,
|
|
23
|
+
SUB_PREFIX_RE, SUBAGENT_ROLES, routeSubToken, routeSubReasoning, routeSubToolCall,
|
|
24
|
+
routeSubToolOutput, finishSubTask, finishSubTaskKey, finishSubTasksByRole, freezeDoneSubTasks,
|
|
22
25
|
ensureCompressPanel, markCompressFailed, markCompressDone, markCompressFallback,
|
|
23
26
|
} from "./subagent-blocks.mjs"
|
|
24
27
|
import { TURN_CAP_MARK } from "../agent/spawn-child.mjs"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const TOOL_OUTPUT_LINE_CAP = 200 // per-call streaming output ring buffer
|
|
35
|
-
const SUBAGENT_PREVIEW_LINES = 8 // report preview rows in the conversation
|
|
36
|
-
const PREVIEW_LINE_CHARS = 120 // per-line preview slice
|
|
37
|
-
const REMINDER_CAP = 3 // max pending reminders shown on turn end
|
|
38
|
-
const REMINDER_PERSIST_TURNS = 5 // persist reminders every N turns
|
|
39
|
-
|
|
40
|
-
const _toolTicks = new Map()
|
|
41
|
-
|
|
42
|
-
function tickStart(name, toolId) {
|
|
43
|
-
const key = toolId ?? name
|
|
44
|
-
if (toolId) { _toolTicks.set(key, performance.now()); return }
|
|
45
|
-
const q = _toolTicks.get(key) ?? []
|
|
46
|
-
q.push(performance.now())
|
|
47
|
-
_toolTicks.set(key, q)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Settle one pending tick: by tool_call id, or per-name FIFO. Returns start or null. */
|
|
51
|
-
function tickTake(name, toolId) {
|
|
52
|
-
const key = toolId ?? name
|
|
53
|
-
const v = _toolTicks.get(key)
|
|
54
|
-
if (v === undefined) return null
|
|
55
|
-
if (toolId) { _toolTicks.delete(key); return v }
|
|
56
|
-
if (v.length === 0) { _toolTicks.delete(key); return null }
|
|
57
|
-
const started = v.shift()
|
|
58
|
-
if (v.length === 0) _toolTicks.delete(key)
|
|
59
|
-
return started
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** P0-2 sweep (2026-08-30 consult): an interrupted turn (Ctrl+C / error) leaves
|
|
63
|
-
* running tool-block carriers without an onToolResult — their header would say
|
|
64
|
-
* "running" forever. runAgentTurn's finally calls this: mark them done with an
|
|
65
|
-
* "(interrupted)" status and clear the tick table so no stale start time leaks
|
|
66
|
-
* into the next turn. Mirrors freezeAllSubTasks for the tool-block family. */
|
|
67
|
-
export function sweepToolBlocks(state) {
|
|
68
|
-
for (const l of state.lines ?? []) {
|
|
69
|
-
const b = l._toolBlock
|
|
70
|
-
if (b && !b.done) {
|
|
71
|
-
b.done = true
|
|
72
|
-
b.summary = "(interrupted)"
|
|
73
|
-
b.interrupted = true
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
_toolTicks.clear()
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Shared display guard for tool results — LIVE and RESTORE use the same
|
|
80
|
-
* function (P1, 2026-08-30 consult: restore lacked the live path's guards).
|
|
81
|
-
* 1) Multimodal results (read_image) embed FULL base64 images in the JSON —
|
|
82
|
-
* the human needs only the text part (model gets images via the multimodal
|
|
83
|
-
* channel); 2) results beyond maxRows are truncated in the block (full
|
|
84
|
-
* text always lives in history for the model). Returns row array. */
|
|
85
|
-
export function slimToolResultForDisplay(result, maxRows = 400) {
|
|
86
|
-
let displayResult = result
|
|
87
|
-
try {
|
|
88
|
-
const parsed = JSON.parse(result)
|
|
89
|
-
if (parsed?.images?.length) displayResult = parsed.text ?? result
|
|
90
|
-
} catch { /* not JSON — show as-is */ }
|
|
91
|
-
const rows = String(displayResult).split("\n").filter((l) => l.trim())
|
|
92
|
-
return rows.length > maxRows
|
|
93
|
-
? [...rows.slice(0, maxRows), `… (result truncated at ${maxRows} rows — full text in history)`]
|
|
94
|
-
: rows
|
|
95
|
-
}
|
|
96
|
-
/** Mark the dispatch-level tool carrier done when its result is consumed by a
|
|
97
|
-
* dedicated branch (subagent/escalate/advisor blocks) instead of the carrier
|
|
98
|
-
* body — without this the turn sweep mislabels successful calls as
|
|
99
|
-
* "(interrupted)" (consult P1, 2026-08-30). */
|
|
100
|
-
function settleToolBlock(state, name, toolId, summary) {
|
|
101
|
-
const block = findToolBlock(state, name, toolId)
|
|
102
|
-
if (block) {
|
|
103
|
-
block.done = true
|
|
104
|
-
block.summary = summary
|
|
105
|
-
const started = tickTake(name, toolId)
|
|
106
|
-
block.elapsed = started !== null ? Math.round(performance.now() - started) : null
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Async spawn detection (§15 D-A1): the subagent tool's async:true result is a
|
|
111
|
-
* status JSON ({id, role, status: running|queued}), NOT a report — the child
|
|
112
|
-
* keeps running, so its activity block must not be frozen at spawn time (it
|
|
113
|
-
* freezes via the ⟦ev⟧done event emitted at settle time — §15 D-A3). A real blocking
|
|
114
|
-
* report that happens to parse as this shape is a freak accident; the only
|
|
115
|
-
* consequence would be a late block freeze at turn end (cosmetic). */
|
|
116
|
-
function isAsyncSpawnResult(result) {
|
|
117
|
-
try {
|
|
118
|
-
const o = JSON.parse(result)
|
|
119
|
-
return Boolean(o && typeof o === "object" && typeof o.id !== "undefined" && (o.status === "running" || o.status === "queued"))
|
|
120
|
-
} catch {
|
|
121
|
-
return false
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
/** Find the live tool-block carrier for a tool event: exact id match when the
|
|
128
|
-
* callback carries one (P0-3 — parallel same-name tools route to their own
|
|
129
|
-
* block); falls back to the last unfinished block of that name. */
|
|
130
|
-
function findToolBlock(state, name, toolId) {
|
|
131
|
-
const lines = state.lines ?? []
|
|
132
|
-
if (toolId !== undefined && toolId !== null) {
|
|
133
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
134
|
-
if (lines[i]._toolBlock?.id === toolId) return lines[i]._toolBlock
|
|
135
|
-
}
|
|
136
|
-
return null
|
|
137
|
-
}
|
|
138
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
139
|
-
const b = lines[i]._toolBlock
|
|
140
|
-
if (b && b.name === name && !b.done) return b
|
|
141
|
-
}
|
|
142
|
-
return null
|
|
143
|
-
}
|
|
144
|
-
|
|
28
|
+
// 2026-09-05 module-split:ticks/maps/sweep/slim/settle/探测/find 族迁 tool-display.mjs——
|
|
29
|
+
// buildToolCallbacks 内部引用用本地 import;sweepToolBlocks re-export(agent-turn 消费面)
|
|
30
|
+
import {
|
|
31
|
+
TOOL_OUTPUT_LINE_CAP, SUBAGENT_PREVIEW_LINES, PREVIEW_LINE_CHARS, REMINDER_CAP, REMINDER_PERSIST_TURNS,
|
|
32
|
+
_toolTicks, _subActions, _subActionQ,
|
|
33
|
+
tickStart, tickTake, settleToolBlock, isAsyncSpawnResult, isSpawnErrorResult,
|
|
34
|
+
findToolBlock, slimToolResultForDisplay, sweepToolBlocks,
|
|
35
|
+
} from "./tool-display.mjs"
|
|
36
|
+
export { sweepToolBlocks, slimToolResultForDisplay } from "./tool-display.mjs"
|
|
145
37
|
|
|
146
38
|
/** Build the agent callbacks + the shared flushStream for one turn.
|
|
147
39
|
* askPermission, askQuestion, saveSessionImpl } */
|
|
@@ -156,11 +48,8 @@ export function buildToolCallbacks(deps) {
|
|
|
156
48
|
const flushStream = () => {
|
|
157
49
|
if (state.reasoning) {
|
|
158
50
|
pushLine(state.reasoning, C.reason, "thinking")
|
|
159
|
-
// Reasoning folds IMMEDIATELY on flush (user ruling 2026-08-30:
|
|
160
|
-
//
|
|
161
|
-
// rejected pre-fold plan — thinking must be DEFAULT FOLDED in the exact
|
|
162
|
-
// unified form: named header + tail 3, expand ≤60%, click back). Same
|
|
163
|
-
// shape as the restore path — zero exceptions on either path.
|
|
51
|
+
// Reasoning folds IMMEDIATELY on flush (user ruling 2026-08-30): default-folded,
|
|
52
|
+
// named header + tail 3 — same shape as the restore path, zero exceptions.
|
|
164
53
|
state.reasoning = ""
|
|
165
54
|
}
|
|
166
55
|
if (state.streaming) {
|
|
@@ -181,9 +70,8 @@ export function buildToolCallbacks(deps) {
|
|
|
181
70
|
scheduleRender()
|
|
182
71
|
},
|
|
183
72
|
onReasoning: (t) => {
|
|
184
|
-
// Subagent reasoning tokens also carry role#id/ prefix — appended into the
|
|
185
|
-
//
|
|
186
|
-
// previously the token only created the entry and the content was discarded).
|
|
73
|
+
// Subagent reasoning tokens also carry role#id/ prefix — appended into the block
|
|
74
|
+
// buffer as kind=think (F2: same treatment as the main reasoning stream).
|
|
187
75
|
if (routeSubReasoning(state, t, scheduleRender)) return
|
|
188
76
|
ensureAssistantLabel()
|
|
189
77
|
state.reasoning += t
|
|
@@ -193,9 +81,14 @@ export function buildToolCallbacks(deps) {
|
|
|
193
81
|
// Subagent tool call: prefix role#id/toolName → open a fresh tool block and
|
|
194
82
|
// set currentTool for the header summary line.
|
|
195
83
|
if (routeSubToolCall(state, name, args, scheduleRender)) return
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
|
|
84
|
+
// §19: record the action of a merged subagent-family call (spawn is the
|
|
85
|
+
// default — only non-spawn actions need a record for result-time routing).
|
|
86
|
+
if (name === "subagent" && args?.action && args.action !== "spawn") {
|
|
87
|
+
if (toolId !== undefined && toolId !== null) _subActions.set(toolId, args.action)
|
|
88
|
+
else _subActionQ.push(args.action)
|
|
89
|
+
}
|
|
90
|
+
// Redundant with flushStream() below (it clears both buffers) — defense-in-depth
|
|
91
|
+
// so a future flushStream change cannot leak advisor buffers into the next view.
|
|
199
92
|
if (name === "advisor") { state._advisorBlocks = [] }
|
|
200
93
|
flushStream()
|
|
201
94
|
ensureAssistantLabel()
|
|
@@ -223,17 +116,12 @@ export function buildToolCallbacks(deps) {
|
|
|
223
116
|
// is unreliable (it glues onto the previous line), so the round belongs here.
|
|
224
117
|
// Also show the advisor's effective model (it may differ from the main agent's).
|
|
225
118
|
const roundTag = name === "advisor" ? ` (round ${(agent._advisorRound || 0) + 1}${advModel ? " · " + advModel : ""})` : ""
|
|
226
|
-
// Readable key-args summary (vscode card-header parity, 2026-08-30) —
|
|
227
|
-
//
|
|
228
|
-
// and the crucial argument was often past the cut. Unknown/MCP tools
|
|
229
|
-
// fall back to compact JSON inside describeToolArgs.
|
|
119
|
+
// Readable key-args summary (vscode card-header parity, 2026-08-30) — replaces
|
|
120
|
+
// the raw JSON.stringify-80 slice. Unknown/MCP tools fall back to compact JSON.
|
|
230
121
|
const argSummary = describeToolArgs(name, args)
|
|
231
|
-
// ONE BLOCK PER TOOL CALL (user ruling 2026-08-30:
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
// the whole call: header = name+args+live status, body = args JSON +
|
|
235
|
-
// streaming output + result. buildConvLines renders it via the shared
|
|
236
|
-
// fold-block component; restore (historyToLines) emits the SAME carrier.
|
|
122
|
+
// ONE BLOCK PER TOOL CALL (user ruling 2026-08-30): header = name+args+live
|
|
123
|
+
// status, body = args JSON + streaming output + result. buildConvLines renders
|
|
124
|
+
// it via the shared fold-block component; restore emits the SAME carrier.
|
|
237
125
|
state.lines.push({
|
|
238
126
|
text: "", color: C.tool,
|
|
239
127
|
_lineId: (state._lineIdCounter = (state._lineIdCounter ?? 0) + 1),
|
|
@@ -250,71 +138,87 @@ export function buildToolCallbacks(deps) {
|
|
|
250
138
|
})
|
|
251
139
|
tickStart(name, toolId)
|
|
252
140
|
},
|
|
253
|
-
|
|
141
|
+
// §7.2.3(方案 e):dispatch runOne 把工具 ctx 上的 _subagentKey(sync spawn/
|
|
142
|
+
// escalate 成功路径设置——relayPrefix 去尾)作为第 4 参传来——undefined 兼容既有
|
|
143
|
+
// 签名(普通工具/老回调/错误路径不带 key)。
|
|
144
|
+
onToolResult: (name, result, toolId, subKey) => {
|
|
254
145
|
state.currentTool = null
|
|
146
|
+
// §19 merged family: route per the action recorded at onToolCall (no record = default spawn).
|
|
147
|
+
let subAction = null
|
|
148
|
+
if (name === "subagent") {
|
|
149
|
+
subAction = (toolId !== undefined && toolId !== null)
|
|
150
|
+
? _subActions.get(toolId) ?? null
|
|
151
|
+
: _subActionQ.shift() ?? null
|
|
152
|
+
if (toolId !== undefined && toolId !== null) _subActions.delete(toolId)
|
|
153
|
+
}
|
|
154
|
+
const isSubagent = name === "subagent" && subAction !== "status" && subAction !== "escalate" && subAction !== "cancel" && subAction !== "panel" && subAction !== "observe" && subAction !== "send"
|
|
155
|
+
const isEscalate = name === "subagent" && subAction === "escalate"
|
|
255
156
|
// Subagent complete: mark the earliest running child as done — the block
|
|
256
157
|
// persists (✓ frozen elapsed header, expandable) as the ONLY carrier of the
|
|
257
|
-
// child's activity
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
// child tool calls never enter the parent's history, so the block is the
|
|
261
|
-
// only trace of what the child did — memory bounded by the N2 line cap.
|
|
262
|
-
const isSubagent = name === "subagent"
|
|
158
|
+
// child's activity; memory bounded by the N2 line cap.
|
|
159
|
+
// §19.5: cancel 动作排除在 isSubagent 外——ack/错误 JSON 走普通工具块;区块冻结由
|
|
160
|
+
// ⟦ev⟧stopped settle 事件承担(此处 finishSubTask 会误冻最早 running 区块)。
|
|
263
161
|
if (isSubagent) {
|
|
264
162
|
// The dispatch-level tool-block carrier for this call would otherwise
|
|
265
163
|
// never be marked done (its result lands in the subagent block, not the
|
|
266
|
-
// carrier) and the turn sweep would mislabel it "(interrupted)"
|
|
267
|
-
// successful subagent call showed that banner (consult P1, 2026-08-30).
|
|
164
|
+
// carrier) and the turn sweep would mislabel it "(interrupted)".
|
|
268
165
|
settleToolBlock(state, name, toolId, "completed")
|
|
269
166
|
// Async spawn (§15 D-A1): the result is a status JSON, not a report — the
|
|
270
|
-
// child KEEPS running; skip the freeze (it would tombstone a live block
|
|
271
|
-
//
|
|
272
|
-
// emitted at turn-end collection.
|
|
167
|
+
// child KEEPS running; skip the freeze (it would tombstone a live block and
|
|
168
|
+
// drop its relay stream). The block freezes on the ⟦ev⟧done settle event.
|
|
273
169
|
if (!isAsyncSpawnResult(result)) {
|
|
274
|
-
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
|
|
170
|
+
// §7.2.3 sync spawn 完成精确冻结:结果按 key 归属三支——
|
|
171
|
+
// ① dispatch 同步成功路径带 subKey(ctx._subagentKey = relayPrefix 去尾):
|
|
172
|
+
// finishSubTaskKey 按 key 精确冻——不再落 finishSubTask 的"最早 started"
|
|
173
|
+
// 启发式(async eng-coder 先启动时 explore 完成会误冻其块——7.2.3.1/T-F2);
|
|
174
|
+
// ② spawn 门拒错误({status:"error"} JSON——auto-turn digest spawn 拒绝):
|
|
175
|
+
// 不冻结任何块(round1 #1——错误路径不冻结 running 块——T-F5);
|
|
176
|
+
// ③ subKey undefined 非错误(老回调/测试直调——成功路径未知工具)→ 启发式
|
|
177
|
+
// 兜底(既有行为不变——面板单块时与精确冻同效——T-F1)。
|
|
178
|
+
const lastError = result.includes(TURN_CAP_MARK) ? "turn cap reached — work may be partial" : null
|
|
179
|
+
const hasSubKey = subKey !== undefined && subKey !== null && subKey !== ""
|
|
180
|
+
if (hasSubKey) {
|
|
181
|
+
finishSubTaskKey(state, String(subKey), lastError)
|
|
182
|
+
freezeDoneSubTasks(state)
|
|
183
|
+
} else if (!isSpawnErrorResult(result)) {
|
|
184
|
+
finishSubTask(state, SUBAGENT_ROLES, lastError)
|
|
185
|
+
freezeDoneSubTasks(state)
|
|
186
|
+
}
|
|
280
187
|
// Subagent report preview (max 8 lines) displayed directly in conversation
|
|
281
188
|
const lines = result.split("\n")
|
|
282
189
|
const preview = lines.slice(0, SUBAGENT_PREVIEW_LINES).map((l) => l.slice(0, PREVIEW_LINE_CHARS)).join("\n")
|
|
283
190
|
if (preview) pushLine(preview, C.dim)
|
|
284
191
|
if (lines.length > SUBAGENT_PREVIEW_LINES) pushLine(` ... (${lines.length - SUBAGENT_PREVIEW_LINES} more lines)`, C.dim)
|
|
285
192
|
}
|
|
286
|
-
} else if (
|
|
287
|
-
// 飞刀 post-op report landed
|
|
193
|
+
} else if (isEscalate) {
|
|
194
|
+
// 飞刀 post-op report landed under the subagent tool name — freeze the
|
|
195
|
+
// escalate#N activity block (no preview; legacy surface).
|
|
288
196
|
settleToolBlock(state, name, toolId, "completed")
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const r = JSON.parse(result)
|
|
303
|
-
if (r?.done || r?.stopped !== undefined) {
|
|
304
|
-
finishSubTasksByRole(state, ["consult"], null)
|
|
305
|
-
freezeDoneSubTasks(state)
|
|
306
|
-
} else if (r?.model) {
|
|
307
|
-
finishSubTaskByModel(state, "consult", r.model)
|
|
308
|
-
freezeDoneSubTasks(state)
|
|
309
|
-
}
|
|
310
|
-
} catch {
|
|
311
|
-
if (name === "consult_stop") {
|
|
312
|
-
finishSubTasksByRole(state, ["consult"], null)
|
|
313
|
-
freezeDoneSubTasks(state)
|
|
197
|
+
// §25 D-R17b (R17): async escalate ack ({id, role:"escalate", status:
|
|
198
|
+
// running|queued}) — the child KEEPS running — skip the freeze like the
|
|
199
|
+
// async spawn path (the block freezes on the ⟦ev⟧done/stopped settle
|
|
200
|
+
// event at flight end). Sync results (async:false) freeze below.
|
|
201
|
+
if (!isAsyncSpawnResult(result)) {
|
|
202
|
+
// §7.2.3(round1 #2):escalate 成功返回带 subKey(escalate#N)→ 精确冻;
|
|
203
|
+
// 失败/老回调无 subKey → escalate 角色启发式兜底(escalate 串行 + 角色限定——
|
|
204
|
+
// 既有行为)。
|
|
205
|
+
const lastError = result.includes(TURN_CAP_MARK) ? "turn cap reached — work may be partial" : null
|
|
206
|
+
if (subKey !== undefined && subKey !== null && subKey !== "") {
|
|
207
|
+
finishSubTaskKey(state, String(subKey), lastError)
|
|
208
|
+
} else {
|
|
209
|
+
finishSubTask(state, ["escalate"], lastError)
|
|
314
210
|
}
|
|
315
|
-
|
|
211
|
+
freezeDoneSubTasks(state)
|
|
212
|
+
}
|
|
213
|
+
} else if (name === "consult_stop") {
|
|
214
|
+
// R17(§25 D-R17a——check 已删):consult_stop = 取消指定会诊会话。被 abort 的
|
|
215
|
+
// children 各自 settle(settleChild)时发 ⟦ev⟧done 冻结自己的卡——此处**不做按
|
|
216
|
+
// 角色整组清扫**(会诊会话可并发——按角色会误冻其他仍在运行的会话的卡,冻结即
|
|
217
|
+
// 截断其活动流——tool-events advisor 复评 🟡1 修正);取消的即时可见性由 abort
|
|
218
|
+
// settle 事件承担(abort 解绕通常在同回合内完成——卡片在其 child settle 即冻结)。
|
|
219
|
+
// 仅结果本身落本工具调用自己的载体块(下方通用分支)。
|
|
316
220
|
}
|
|
317
|
-
if (!isSubagent && name !== "advisor") {
|
|
221
|
+
if (!isSubagent && !isEscalate && name !== "advisor") {
|
|
318
222
|
// Result lands INSIDE the block (restore parity — the restored carrier
|
|
319
223
|
// carries the same fields). The done line is gone: status/elapsed live
|
|
320
224
|
// in the header now.
|
|
@@ -337,16 +241,11 @@ export function buildToolCallbacks(deps) {
|
|
|
337
241
|
// the frozen box, but the dispatch-level tool carrier must still be
|
|
338
242
|
// marked done (consult P1, 2026-08-30: sweep mislabeled it interrupted).
|
|
339
243
|
settleToolBlock(state, name, toolId, "completed")
|
|
340
|
-
// The review's thinking must survive into the conversation history like
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
// aligned with subagent blocks) instead of a flat auto-expanded line —
|
|
346
|
-
// the flat form flooded the conversation. Full text still lives in the
|
|
347
|
-
// tool result message; the box is the reviewable record. Live
|
|
348
|
-
// _advisorBlocks keep rendering the running view until cleared in the
|
|
349
|
-
// turn finally.
|
|
244
|
+
// The review's thinking must survive into the conversation history like the
|
|
245
|
+
// main agent's reasoning — flushed as a COLLAPSIBLE box before the done
|
|
246
|
+
// line (frozen-folded semantics, aligned with subagent blocks; the flat
|
|
247
|
+
// form flooded the conversation). Full text lives in the tool result;
|
|
248
|
+
// _advisorBlocks keep rendering the running view until cleared at turn end.
|
|
350
249
|
const blocks = state._advisorBlocks ?? []
|
|
351
250
|
if (blocks.length > 0) {
|
|
352
251
|
const text = blocks
|
|
@@ -368,17 +267,17 @@ export function buildToolCallbacks(deps) {
|
|
|
368
267
|
}
|
|
369
268
|
},
|
|
370
269
|
onToolOutput: (name, chunk, toolId) => {
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
270
|
+
// Subagent relays (name "role#id/tool", D1) route RAW — chunks are
|
|
271
|
+
// child-stdout/SSE fragments at arbitrary byte boundaries; trimEnd eats
|
|
272
|
+
// real trailing newlines and routeSubToolOutput's verbatim concat would
|
|
273
|
+
// glue lines (2026-09-03 修复轮; main path keeps the trimmed form below).
|
|
274
|
+
const isSubRelay = SUB_PREFIX_RE.test(name)
|
|
275
|
+
const rawText = typeof chunk === "string" ? chunk : String(chunk?.text ?? "")
|
|
276
|
+
const part = {
|
|
277
|
+
kind: typeof chunk === "string" ? "text" : (chunk?.kind ?? "text"),
|
|
278
|
+
text: isSubRelay ? rawText : rawText.trimEnd(),
|
|
279
|
+
}
|
|
377
280
|
if (!part.text) return
|
|
378
|
-
// Subagent tool output (D1: childCallbacks.onToolOutput relays under the
|
|
379
|
-
// prefixed name "role#id/toolName") → append into the CURRENT tool block of
|
|
380
|
-
// that child's activity buffer. Render throttled at 250ms (N1); the data
|
|
381
|
-
// append itself is never delayed.
|
|
382
281
|
if (routeSubToolOutput(state, name, part, scheduleRender)) return
|
|
383
282
|
if (name === "advisor") {
|
|
384
283
|
// Accumulate to buffer — formatTables + wrapText in render-conversation
|
|
@@ -386,13 +285,9 @@ export function buildToolCallbacks(deps) {
|
|
|
386
285
|
// NOTE: the advisor tool ALWAYS emits {kind, text} objects (run.mjs's
|
|
387
286
|
// emit() wrapper) — a raw string chunk is never think; if that ever
|
|
388
287
|
// changes, plain-string think would land in advisorStreaming.
|
|
389
|
-
// ORDERED block buffer — preserves the interleaved emission order
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
// regrouped ALL thinking above ALL tool progress — the alternating
|
|
393
|
-
// timeline was destroyed. Consecutive chunks of the same kind merge
|
|
394
|
-
// into one block; kind flips start a new block; render walks the
|
|
395
|
-
// blocks in order with per-kind colors.
|
|
288
|
+
// ORDERED block buffer — preserves the interleaved emission order (think →
|
|
289
|
+
// tool → think → … → final); consecutive chunks of the same kind merge
|
|
290
|
+
// into one block, kind flips start a new block, render walks them in order.
|
|
396
291
|
const isString = typeof chunk === "string"
|
|
397
292
|
const raw = isString ? chunk : String(chunk?.text ?? "")
|
|
398
293
|
const kind = isString ? "text" : (chunk?.kind ?? "text")
|
|
@@ -418,19 +313,20 @@ export function buildToolCallbacks(deps) {
|
|
|
418
313
|
}
|
|
419
314
|
scheduleRender()
|
|
420
315
|
},
|
|
421
|
-
|
|
316
|
+
// Manual-tier auto-turn digests (agent-turn.mjs suspension driver) pass null
|
|
317
|
+
// handlers — permission requests then deny WITHOUT a panel (§17 D-S7: no modal
|
|
318
|
+
// during unattended digestion) and question errors out instead of hanging.
|
|
319
|
+
...(askPermission ? { onPermissionRequest: (name, args) => askPermission(name, args) } : {}),
|
|
422
320
|
// Merged batch ask (§16 D-B1): one confirmation for N non-readonly tools in
|
|
423
321
|
// the same response — "approve all / one by one / deny" (key-handler resolves
|
|
424
322
|
// the verdict string; approveAll is batch-scope only, never the AUTO flag).
|
|
425
|
-
onBatchPermissionRequest: (req) => askBatchPermission(req),
|
|
426
|
-
onQuestion: (text, options) => askQuestion(text, options),
|
|
427
|
-
// Compression lifecycle (CONTEXT-COMPACTION.md §7 D-C2): the compression session
|
|
428
|
-
// as a subagent-style panel block — start → running panel ("Compressing
|
|
429
|
-
// "summarizing N messages" + elapsed ticker), fail → error text
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
// never enters the panel or the stream (the summary call is silent). Replaces the old
|
|
433
|
-
// one-line "[context] Context too long..." warn (user ruling: panel, not a status line).
|
|
323
|
+
...(askBatchPermission ? { onBatchPermissionRequest: (req) => askBatchPermission(req) } : {}),
|
|
324
|
+
...(askQuestion ? { onQuestion: (text, options) => askQuestion(text, options) } : {}),
|
|
325
|
+
// Compression lifecycle (CONTEXT-COMPACTION.md §7 D-C2): the compression session
|
|
326
|
+
// renders as a subagent-style panel block — start → running panel ("Compressing
|
|
327
|
+
// context…" + "summarizing N messages" + elapsed ticker), fail → error text only,
|
|
328
|
+
// success → frozen "Compressed: N tokens freed → summary (Xs)" / fallback →
|
|
329
|
+
// "truncated to N messages". The summary BODY never enters the panel or stream.
|
|
434
330
|
onCompressStart: (info) => {
|
|
435
331
|
ensureCompressPanel(state, info)
|
|
436
332
|
scheduleRender()
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* 超 500 行硬限拆分——启动序列、退出清理序列与退出闭包从 index.mjs 移入本模块)。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { appendFileSync } from "node:fs"
|
|
6
7
|
import { ansi } from "./ansi.mjs"
|
|
7
8
|
|
|
8
9
|
/** TUI 启动序列:alt buffer + 隐藏光标 + 鼠标/粘贴/键盘增强 + 禁环绕。
|
|
@@ -19,6 +20,33 @@ export function writeCleanupSequence(write = (s) => process.stdout.write(s)) {
|
|
|
19
20
|
write(ansi.clearScreen + ansi.mouseOff + ansi.bracketedPasteOff + ansi.keyboardPop + ansi.modifyOtherKeysOff + ansi.mainBuffer + ansi.showCursor + ansi.reset + ansi.wrapOn)
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
// R25(ARCHITECTURE.md §R25 F-R25a——复审 #1):TUI 活动态标志——index.mjs 在终端接管
|
|
24
|
+
// 处(writeStartupSequence 之后)经 setTuiActive(true) 置位,createExitCleanup 清理后清
|
|
25
|
+
// false;bin/thincoder.mjs 崩溃钩子经 restoreTerminalAfterCrash 读同一标志——chat 模式
|
|
26
|
+
// stdout 管道不得收 ANSI(误判即污染输出)。默认 false = 未启动(生产零变化——测试缝同源:
|
|
27
|
+
// T-R25a.2 注入 mock 走同一 setter)。
|
|
28
|
+
let tuiActive = false
|
|
29
|
+
|
|
30
|
+
/** 置/清 TUI 活动态(TUI 启动接管处与退出清理处调用;测试 env 门注入同走此 setter)。 */
|
|
31
|
+
export function setTuiActive(active) {
|
|
32
|
+
tuiActive = active === true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** R25 崩溃恢复:仅 TUI 活动态执行 writeCleanupSequence(复用本模块清理序列——符号锚)。
|
|
36
|
+
* 测试缝(T-R25a.2——env 门注入):THINCODER_TEST_CLEANUP_OUT 指向文件时序列写入该文件
|
|
37
|
+
* (观察恢复被调 + stdout 管道零 ANSI)——生产不设该 env → 恒 stdout(零变化)。
|
|
38
|
+
* @returns {boolean} 是否执行了恢复(false = TUI 未启动——调用方无需处理) */
|
|
39
|
+
export function restoreTerminalAfterCrash() {
|
|
40
|
+
if (!tuiActive) return false
|
|
41
|
+
const seamOut = process.env.THINCODER_TEST_CLEANUP_OUT
|
|
42
|
+
if (seamOut) {
|
|
43
|
+
writeCleanupSequence((s) => appendFileSync(seamOut, s, "utf8"))
|
|
44
|
+
} else {
|
|
45
|
+
writeCleanupSequence()
|
|
46
|
+
}
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
|
|
22
50
|
/** 退出清理闭包:保存会话(同步)+ 关闭 MCP 子进程 + 恢复终端。幂等(cleanedUp 守卫)。 */
|
|
23
51
|
export function createExitCleanup({ agent, saveSession, closeAllMcp }) {
|
|
24
52
|
let cleanedUp = false
|
|
@@ -41,5 +69,6 @@ export function createExitCleanup({ agent, saveSession, closeAllMcp }) {
|
|
|
41
69
|
}
|
|
42
70
|
process.stdin.setRawMode(false)
|
|
43
71
|
writeCleanupSequence()
|
|
72
|
+
setTuiActive(false) // R25:清理完成即清活动态(崩溃钩子不再收到误判)
|
|
44
73
|
}
|
|
45
74
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* update-notice.mjs — 后台升级提示(2026-09-03 D-S1c 自 index.mjs 拆出):
|
|
3
|
+
* upgradeFailureText / pendingNoticeReady(纯函数,index.mjs re-export——tui.test
|
|
4
|
+
* 动态 import 面零改动)+ createUpdateNotice 装配(升级提示 picker + 启动检查)。
|
|
5
|
+
* index.mjs 只保留装配调用(showPicker 闭包在此经 ctx 注入)。
|
|
6
|
+
*/
|
|
7
|
+
import { ansi, C } from "./ansi.mjs"
|
|
8
|
+
|
|
9
|
+
/** 升级失败提示文案:附 npm 输出尾部(最多 3 行),方便定位失败原因。 */
|
|
10
|
+
export function upgradeFailureText(code, output) {
|
|
11
|
+
const tail = (output ?? "").trimEnd().split("\n").slice(-3).join("\n")
|
|
12
|
+
return `✗ Upgrade failed (exit ${code}). Run \`thincoder upgrade\` manually.${tail ? `\n${tail}` : ""}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 后台更新提示可弹出的条件:无任何交互弹层(picker/permission/question)激活。 */
|
|
16
|
+
export function pendingNoticeReady(state) {
|
|
17
|
+
return Boolean(state.pendingNotice && !state.picker && !state.permission && !state.question)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 升级提示 + 启动检查装配。ctx: { state, showPicker, pushLine, pushLabel, render }
|
|
22
|
+
* 返回 { showUpdateNotice, checkUpdates }——checkUpdates 非阻塞(网络错误静默跳过);
|
|
23
|
+
* 有 picker 打开时提示挂到 state.pendingNotice,picker 全部关闭后由 doRender 弹出。
|
|
24
|
+
*/
|
|
25
|
+
export function createUpdateNotice(ctx) {
|
|
26
|
+
const { state, showPicker, pushLine, pushLabel, render } = ctx
|
|
27
|
+
|
|
28
|
+
const showUpdateNotice = async (result) => {
|
|
29
|
+
const sel = await showPicker(`Update available: ${result.local} → ${result.latest}`, [
|
|
30
|
+
{ type: "header", text: `thincoder ${result.latest} is available (current: ${result.local})` },
|
|
31
|
+
{ type: "item", text: "Upgrade now", action: "upgrade" },
|
|
32
|
+
{ type: "item", text: "Later", action: "later" },
|
|
33
|
+
])
|
|
34
|
+
if (sel?.action !== "upgrade") return
|
|
35
|
+
pushLabel(`❯ Upgrade`, ansi.bold + C.tool)
|
|
36
|
+
pushLine(`Upgrading to ${result.latest}...`, C.tool)
|
|
37
|
+
const { exec } = await import("node:child_process")
|
|
38
|
+
const cp = exec("npm install -g thincoder@latest", { windowsHide: true })
|
|
39
|
+
let stdout = ""
|
|
40
|
+
let done = false
|
|
41
|
+
cp.stdout?.on("data", (d) => { stdout += d })
|
|
42
|
+
cp.stderr?.on("data", (d) => { stdout += d })
|
|
43
|
+
cp.on("error", (e) => { if (done) return; done = true; pushLine(`✗ Upgrade failed (${e.message}). Run \`thincoder upgrade\` manually.`, C.error); render() })
|
|
44
|
+
cp.on("close", (code) => {
|
|
45
|
+
if (done) return
|
|
46
|
+
done = true
|
|
47
|
+
if (code === 0) {
|
|
48
|
+
pushLine(`✓ Upgraded to ${result.latest}. Restart to apply.`, C.tool)
|
|
49
|
+
} else {
|
|
50
|
+
pushLine(upgradeFailureText(code, stdout), C.error)
|
|
51
|
+
}
|
|
52
|
+
render()
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const checkUpdates = async () => {
|
|
57
|
+
try {
|
|
58
|
+
const { readFileSync } = await import("node:fs")
|
|
59
|
+
const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"))
|
|
60
|
+
const { checkForUpdate } = await import("../upgrade.mjs")
|
|
61
|
+
const result = await checkForUpdate(pkg.version)
|
|
62
|
+
if (result?.newer) {
|
|
63
|
+
// Defer: if wizard is still active, just show a dim line
|
|
64
|
+
if (state.wizard) {
|
|
65
|
+
pushLine(`Tip: thincoder ${result.latest} is available (run /upgrade later or restart)`, C.dim)
|
|
66
|
+
render()
|
|
67
|
+
} else {
|
|
68
|
+
state.pendingNotice = result
|
|
69
|
+
render()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch { /* network error or timeout — silently skip */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { showUpdateNotice, checkUpdates }
|
|
76
|
+
}
|