thincoder 0.12.59 → 0.12.61
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 +62 -4
- package/README.md +10 -8
- package/bin/thincoder.mjs +99 -133
- package/package.json +6 -4
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +45 -21
- package/src/acp.mjs +6 -1
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +59 -137
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +156 -359
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +27 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +145 -27
- package/src/agent/helpers.mjs +107 -13
- package/src/agent/record-results.mjs +55 -11
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +242 -0
- package/src/agent/setup-reminders.mjs +69 -0
- package/src/agent/setup.mjs +107 -127
- package/src/agent/spawn-child.mjs +55 -13
- package/src/agent-tools/advisor-async.mjs +346 -0
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +167 -116
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +139 -107
- package/src/agent-tools/design-token.mjs +117 -0
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +289 -0
- package/src/agent-tools/read-history.mjs +166 -32
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +137 -34
- package/src/agent-tools/subagent-actions.mjs +180 -133
- package/src/agent-tools/subagent-async.mjs +184 -177
- package/src/agent-tools/subagent-panel.mjs +160 -0
- package/src/agent-tools/subagent-run.mjs +205 -0
- package/src/agent-tools/subagent-scheduler.mjs +100 -27
- package/src/agent-tools/subagent-spawn.mjs +453 -0
- package/src/agent-tools/subagent.mjs +256 -578
- package/src/agent-tools/verify.mjs +119 -292
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +89 -205
- package/src/cli/distill-command.mjs +12 -6
- package/src/cli/make-agent.mjs +26 -8
- package/src/cli/memory-command.mjs +4 -3
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +42 -17
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +180 -63
- package/src/context.mjs +5 -147
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +128 -0
- package/src/distill.mjs +11 -11
- package/src/expand-home.mjs +16 -0
- package/src/explore-distill.mjs +155 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +72 -32
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +236 -0
- package/src/memory/docs.mjs +68 -54
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/memory.mjs +3 -1
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +82 -0
- package/src/prompts/advisor-design.md +18 -75
- package/src/prompts/advisor-round1.md +14 -67
- package/src/prompts/advisor-round2.md +15 -51
- package/src/prompts/advisor-round3.md +15 -51
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +5 -23
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +55 -0
- package/src/prompts/persona-engineering.md +54 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +18 -98
- package/src/provider/errors.mjs +101 -0
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/retry.mjs +8 -45
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- 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 +188 -60
- package/src/session.mjs +104 -124
- package/src/token-ttl.mjs +274 -0
- package/src/tools/{system.mjs → bash.mjs} +19 -221
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +41 -16
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +23 -34
- 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/index.mjs +9 -8
- package/src/tools/ops.mjs +188 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +4 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -2
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tools/shared.mjs +14 -13
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +36 -228
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-config.mjs +142 -30
- package/src/tui/cmd-eng.mjs +28 -40
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-undo.mjs +4 -3
- 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 +40 -38
- package/src/tui/interaction.mjs +3 -3
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +52 -9
- package/src/tui/pickers.mjs +28 -392
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +12 -9
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +106 -295
- package/src/tui/subagent-children.mjs +162 -0
- package/src/tui/subagent-freeze.mjs +169 -0
- package/src/tui/subagent-panel.mjs +24 -31
- package/src/tui/suspension-drive.mjs +297 -0
- package/src/tui/tool-args.mjs +7 -5
- package/src/tui/tool-display.mjs +143 -0
- package/src/tui/tool-events.mjs +56 -185
- package/src/tui/tui-lifecycle.mjs +46 -4
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +61 -21
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -56
- package/src/prompts/discipline.md +0 -102
- package/src/prompts/eng-coder.md +0 -44
- package/src/prompts/engineering-sub.md +0 -34
- package/src/prompts/engineering.md +0 -340
- package/src/prompts/explore.md +0 -21
- package/src/prompts/main.md +0 -56
- package/src/prompts/methodology-template.md +0 -58
- package/src/prompts/plan.md +0 -16
- package/src/prompts/system.md +0 -63
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
package/src/tui/tool-events.mjs
CHANGED
|
@@ -4,163 +4,39 @@
|
|
|
4
4
|
*
|
|
5
5
|
* - onToken/onReasoning : 子agent 前缀分流(routeSub*)→ 主流 streaming/reasoning
|
|
6
6
|
* - onToolCall : 状态栏 + `❯ name args` 标题行 + 计时 + §19 action 记录
|
|
7
|
-
* - onToolResult : 子agent 完成冻结(
|
|
7
|
+
* - onToolResult : 子agent 完成冻结(finishSubTaskKey 精确冻 + freezeDoneSubTasks——
|
|
8
|
+
* F-2 后 finishSubTask 为 no-op 兼容保留)、
|
|
8
9
|
* 工具块结果入块、advisor 评审冻结框
|
|
9
10
|
* - onToolOutput : advisor 有序块缓冲 / 工具块输出流
|
|
10
11
|
* - 其余 : usage 累计、等待提示、task 面板、回合末增量落盘
|
|
11
12
|
*
|
|
12
13
|
* flushStream 同时返回给调用方(回合循环 / onTurnEnd 共用)。纯回调装配,无终端副作用
|
|
13
|
-
* (除经 deps 注入的 pushLine/render)。§19: subagent_check/escalate
|
|
14
|
-
* subagent 家族全部调用以工具名 "subagent" +
|
|
15
|
-
*
|
|
14
|
+
* (除经 deps 注入的 pushLine/render)。§19: subagent_check/escalate 工具退役后
|
|
15
|
+
* (§19.8——check 动作已删),subagent 家族全部调用以工具名 "subagent" +
|
|
16
|
+
* action 到达——完成路由按 onToolCall 时记录的 action 分流(spawn 区块 / escalate
|
|
17
|
+
* 区块 / status/observe/send 普通工具块——observe/send 不建子代理区块)。
|
|
16
18
|
*/
|
|
17
19
|
import { C } from "./ansi.mjs"
|
|
18
20
|
import { formatToolSummary } from "./tool-summaries.mjs"
|
|
19
21
|
import { describeToolArgs, toolArgsLines } from "./tool-args.mjs"
|
|
20
22
|
import { ADVISOR_THINKING_PLACEHOLDER, resolveAdvisorProvider } from "../advisor/run.mjs"
|
|
21
23
|
import {
|
|
22
|
-
|
|
23
|
-
routeSubToolOutput, finishSubTask, finishSubTaskKey, finishSubTasksByRole,
|
|
24
|
+
SUBAGENT_ROLES, routeSubToken, routeSubReasoning, routeSubToolCall,
|
|
25
|
+
routeSubToolOutput, finishSubTask, finishSubTaskKey, finishSubTasksByRole, freezeDoneSubTasks,
|
|
24
26
|
ensureCompressPanel, markCompressFailed, markCompressDone, markCompressFallback,
|
|
25
27
|
} from "./subagent-blocks.mjs"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const PREVIEW_LINE_CHARS = 120 // per-line preview slice
|
|
39
|
-
const REMINDER_CAP = 3 // max pending reminders shown on turn end
|
|
40
|
-
const REMINDER_PERSIST_TURNS = 5 // persist reminders every N turns
|
|
41
|
-
|
|
42
|
-
const _toolTicks = new Map()
|
|
43
|
-
// §19 action registry: tool_call id → subagent action (non-spawn only — spawn is
|
|
44
|
-
// the default when no record exists). onToolCall sees the args, onToolResult only
|
|
45
|
-
// the name; without the record every subagent result would route as a spawn.
|
|
46
|
-
const _subActions = new Map()
|
|
47
|
-
const _subActionQ = [] // FIFO for subagent calls without a tool id (same fallback as tick queue)
|
|
48
|
-
|
|
49
|
-
function tickStart(name, toolId) {
|
|
50
|
-
const key = toolId ?? name
|
|
51
|
-
if (toolId) { _toolTicks.set(key, performance.now()); return }
|
|
52
|
-
const q = _toolTicks.get(key) ?? []
|
|
53
|
-
q.push(performance.now())
|
|
54
|
-
_toolTicks.set(key, q)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Settle one pending tick: by tool_call id, or per-name FIFO. Returns start or null. */
|
|
58
|
-
function tickTake(name, toolId) {
|
|
59
|
-
const key = toolId ?? name
|
|
60
|
-
const v = _toolTicks.get(key)
|
|
61
|
-
if (v === undefined) return null
|
|
62
|
-
if (toolId) { _toolTicks.delete(key); return v }
|
|
63
|
-
if (v.length === 0) { _toolTicks.delete(key); return null }
|
|
64
|
-
const started = v.shift()
|
|
65
|
-
if (v.length === 0) _toolTicks.delete(key)
|
|
66
|
-
return started
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** P0-2 sweep (2026-08-30 consult): an interrupted turn (Ctrl+C / error) leaves
|
|
70
|
-
* running tool-block carriers without an onToolResult — their header would say
|
|
71
|
-
* "running" forever. runAgentTurn's finally calls this: mark them done with an
|
|
72
|
-
* "(interrupted)" status and clear the tick table so no stale start time leaks
|
|
73
|
-
* into the next turn. Mirrors freezeAllSubTasks for the tool-block family. */
|
|
74
|
-
export function sweepToolBlocks(state) {
|
|
75
|
-
for (const l of state.lines ?? []) {
|
|
76
|
-
const b = l._toolBlock
|
|
77
|
-
if (b && !b.done) {
|
|
78
|
-
b.done = true
|
|
79
|
-
b.summary = "(interrupted)"
|
|
80
|
-
b.interrupted = true
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
_toolTicks.clear()
|
|
84
|
-
_subActions.clear()
|
|
85
|
-
_subActionQ.length = 0
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Shared display guard for tool results — LIVE and RESTORE use the same
|
|
89
|
-
* function (P1, 2026-08-30 consult: restore lacked the live path's guards).
|
|
90
|
-
* 1) Multimodal results (read_image) embed FULL base64 images in the JSON —
|
|
91
|
-
* the human needs only the text part (model gets images via the multimodal
|
|
92
|
-
* channel); 2) results beyond maxRows are truncated in the block (full
|
|
93
|
-
* text always lives in history for the model). Returns row array. */
|
|
94
|
-
export function slimToolResultForDisplay(result, maxRows = 400) {
|
|
95
|
-
let displayResult = result
|
|
96
|
-
try {
|
|
97
|
-
const parsed = JSON.parse(result)
|
|
98
|
-
if (parsed?.images?.length) displayResult = parsed.text ?? result
|
|
99
|
-
} catch { /* not JSON — show as-is */ }
|
|
100
|
-
const rows = String(displayResult).split("\n").filter((l) => l.trim())
|
|
101
|
-
return rows.length > maxRows
|
|
102
|
-
? [...rows.slice(0, maxRows), `… (result truncated at ${maxRows} rows — full text in history)`]
|
|
103
|
-
: rows
|
|
104
|
-
}
|
|
105
|
-
/** Mark the dispatch-level tool carrier done when its result is consumed by a
|
|
106
|
-
* dedicated branch (subagent/escalate/advisor blocks) — without this the turn
|
|
107
|
-
* sweep mislabels successful calls as "(interrupted)" (consult P1, 2026-08-30). */
|
|
108
|
-
function settleToolBlock(state, name, toolId, summary) {
|
|
109
|
-
const block = findToolBlock(state, name, toolId)
|
|
110
|
-
if (block) {
|
|
111
|
-
block.done = true
|
|
112
|
-
block.summary = summary
|
|
113
|
-
const started = tickTake(name, toolId)
|
|
114
|
-
block.elapsed = started !== null ? Math.round(performance.now() - started) : null
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Async spawn detection (§15 D-A1): the subagent tool's async:true result is a
|
|
119
|
-
* status JSON ({id, role, status: running|queued}), NOT a report — the child
|
|
120
|
-
* keeps running, so its activity block must not be frozen at spawn time (it
|
|
121
|
-
* freezes via the ⟦ev⟧done event at settle — §15 D-A3). */
|
|
122
|
-
function isAsyncSpawnResult(result) {
|
|
123
|
-
try {
|
|
124
|
-
const o = JSON.parse(result)
|
|
125
|
-
return Boolean(o && typeof o === "object" && typeof o.id !== "undefined" && (o.status === "running" || o.status === "queued"))
|
|
126
|
-
} catch {
|
|
127
|
-
return false
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** §7.2.3 spawn 门拒错误探测(T-F5):subagent spawn 的机械拒绝以 {status:"error"}
|
|
132
|
-
* JSON 返回(例:manual auto-turn digest spawn 门——digest 语义 organize-only)——
|
|
133
|
-
* 错误路径不得触发完成冻结(round1 #1——错误路径不冻结任何 running 块)。该形态只
|
|
134
|
-
* 出现在无 subKey 的拒绝路径(成功路径恒带 dispatch 传的 ctx._subagentKey)。 */
|
|
135
|
-
function isSpawnErrorResult(result) {
|
|
136
|
-
try {
|
|
137
|
-
const o = JSON.parse(result)
|
|
138
|
-
return Boolean(o && typeof o === "object" && o.status === "error")
|
|
139
|
-
} catch {
|
|
140
|
-
return false
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
/** Find the live tool-block carrier for a tool event: exact id match when the
|
|
147
|
-
* callback carries one (P0-3 — parallel same-name tools route to their own
|
|
148
|
-
* block); falls back to the last unfinished block of that name. */
|
|
149
|
-
function findToolBlock(state, name, toolId) {
|
|
150
|
-
const lines = state.lines ?? []
|
|
151
|
-
if (toolId !== undefined && toolId !== null) {
|
|
152
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
153
|
-
if (lines[i]._toolBlock?.id === toolId) return lines[i]._toolBlock
|
|
154
|
-
}
|
|
155
|
-
return null
|
|
156
|
-
}
|
|
157
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
158
|
-
const b = lines[i]._toolBlock
|
|
159
|
-
if (b && b.name === name && !b.done) return b
|
|
160
|
-
}
|
|
161
|
-
return null
|
|
162
|
-
}
|
|
163
|
-
|
|
28
|
+
// 第 27 批 §12.3②:前缀正则换名 + import 源改文法模块(纯换名——语义零改)。
|
|
29
|
+
import { RELAY_PREFIX_RE } from "../agent/relay-prefix.mjs"
|
|
30
|
+
import { TURN_CAP_MARK, STOPPED_MARK } from "../agent/spawn-child.mjs"
|
|
31
|
+
// 2026-09-05 module-split:ticks/maps/sweep/slim/settle/探测/find 族迁 tool-display.mjs——
|
|
32
|
+
// buildToolCallbacks 内部引用用本地 import;sweepToolBlocks re-export(agent-turn 消费面)
|
|
33
|
+
import {
|
|
34
|
+
TOOL_OUTPUT_LINE_CAP, REMINDER_CAP, REMINDER_PERSIST_TURNS,
|
|
35
|
+
_toolTicks, _subActions, _subActionQ,
|
|
36
|
+
tickStart, tickTake, settleToolBlock, isAsyncSpawnResult, isSpawnErrorResult,
|
|
37
|
+
findToolBlock, slimToolResultForDisplay, sweepToolBlocks,
|
|
38
|
+
} from "./tool-display.mjs"
|
|
39
|
+
export { sweepToolBlocks, slimToolResultForDisplay } from "./tool-display.mjs"
|
|
164
40
|
|
|
165
41
|
/** Build the agent callbacks + the shared flushStream for one turn.
|
|
166
42
|
* askPermission, askQuestion, saveSessionImpl } */
|
|
@@ -278,7 +154,7 @@ export function buildToolCallbacks(deps) {
|
|
|
278
154
|
: _subActionQ.shift() ?? null
|
|
279
155
|
if (toolId !== undefined && toolId !== null) _subActions.delete(toolId)
|
|
280
156
|
}
|
|
281
|
-
const isSubagent = name === "subagent" && subAction !== "
|
|
157
|
+
const isSubagent = name === "subagent" && subAction !== "status" && subAction !== "escalate" && subAction !== "cancel" && subAction !== "panel" && subAction !== "observe" && subAction !== "send"
|
|
282
158
|
const isEscalate = name === "subagent" && subAction === "escalate"
|
|
283
159
|
// Subagent complete: mark the earliest running child as done — the block
|
|
284
160
|
// persists (✓ frozen elapsed header, expandable) as the ONLY carrier of the
|
|
@@ -300,9 +176,12 @@ export function buildToolCallbacks(deps) {
|
|
|
300
176
|
// 启发式(async eng-coder 先启动时 explore 完成会误冻其块——7.2.3.1/T-F2);
|
|
301
177
|
// ② spawn 门拒错误({status:"error"} JSON——auto-turn digest spawn 拒绝):
|
|
302
178
|
// 不冻结任何块(round1 #1——错误路径不冻结 running 块——T-F5);
|
|
303
|
-
// ③ subKey undefined 非错误(老回调/测试直调——成功路径未知工具)→
|
|
304
|
-
//
|
|
305
|
-
|
|
179
|
+
// ③ subKey undefined 非错误(老回调/测试直调——成功路径未知工具)→ 不冻结
|
|
180
|
+
// 任何块(CLI-ACTIVITY-DEBLOAT F-2 收窄:finishSubTask 恒 no-op——无 key
|
|
181
|
+
// 无从精确归属,宁可 no-op 不误冻;块由回合尾 freezeAllSubTasks 兜底清场)。
|
|
182
|
+
// SYNC-CANCEL(R6):⏹ 折叠报告带 STOPPED_MARK——块冻结标 stopped 而非 done
|
|
183
|
+
// (lastError 注记 + 事件定格——兜底竞态窗口的 dispatch 精确冻路径)
|
|
184
|
+
const lastError = result.includes(TURN_CAP_MARK) ? "turn cap reached — work may be partial" : result.includes(STOPPED_MARK) ? "stopped by user — work may be partial" : null
|
|
306
185
|
const hasSubKey = subKey !== undefined && subKey !== null && subKey !== ""
|
|
307
186
|
if (hasSubKey) {
|
|
308
187
|
finishSubTaskKey(state, String(subKey), lastError)
|
|
@@ -311,48 +190,40 @@ export function buildToolCallbacks(deps) {
|
|
|
311
190
|
finishSubTask(state, SUBAGENT_ROLES, lastError)
|
|
312
191
|
freezeDoneSubTasks(state)
|
|
313
192
|
}
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (lines.length > SUBAGENT_PREVIEW_LINES) pushLine(` ... (${lines.length - SUBAGENT_PREVIEW_LINES} more lines)`, C.dim)
|
|
193
|
+
// CLI-ACTIVITY-DEBLOAT F-1 (2026-09-10): the conversation-stream report
|
|
194
|
+
// preview (max 8 dim lines) is deleted — the frozen block is the ONLY
|
|
195
|
+
// carrier of the child's report (full text also lives in history for the
|
|
196
|
+
// model; escalate#N keeps its no-preview surface unchanged).
|
|
319
197
|
}
|
|
320
198
|
} else if (isEscalate) {
|
|
321
199
|
// 飞刀 post-op report landed under the subagent tool name — freeze the
|
|
322
200
|
// escalate#N activity block (no preview; legacy surface).
|
|
323
201
|
settleToolBlock(state, name, toolId, "completed")
|
|
324
|
-
// §
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// - individual reply (done:false): settle precisely by r.model.
|
|
340
|
-
// - done:true / stopped: settle every remaining consult block.
|
|
341
|
-
try {
|
|
342
|
-
const r = JSON.parse(result)
|
|
343
|
-
if (r?.done || r?.stopped !== undefined) {
|
|
344
|
-
finishSubTasksByRole(state, ["consult"], null)
|
|
345
|
-
freezeDoneSubTasks(state)
|
|
346
|
-
} else if (r?.model) {
|
|
347
|
-
finishSubTaskByModel(state, "consult", r.model)
|
|
348
|
-
freezeDoneSubTasks(state)
|
|
349
|
-
}
|
|
350
|
-
} catch {
|
|
351
|
-
if (name === "consult_stop") {
|
|
352
|
-
finishSubTasksByRole(state, ["consult"], null)
|
|
353
|
-
freezeDoneSubTasks(state)
|
|
202
|
+
// §25 D-R17b (R17): async escalate ack ({id, role:"escalate", status:
|
|
203
|
+
// running|queued}) — the child KEEPS running — skip the freeze like the
|
|
204
|
+
// async spawn path (the block freezes on the ⟦ev⟧done/stopped settle
|
|
205
|
+
// event at flight end). Sync results (async:false) freeze below.
|
|
206
|
+
if (!isAsyncSpawnResult(result)) {
|
|
207
|
+
// §7.2.3(round1 #2):escalate 成功返回带 subKey(escalate#N)→ 精确冻;
|
|
208
|
+
// 失败/老回调无 subKey → 不冻结任何块(F-2 收窄——finishSubTask 恒 no-op,
|
|
209
|
+
// 块由回合尾 freezeAllSubTasks 兜底清场)。
|
|
210
|
+
// SYNC-CANCEL(R6):同上——escalate 路径同款扩展(sync escalate 无 registry——
|
|
211
|
+
// 恒不折叠——扩展仅口径一致——零行为变化)
|
|
212
|
+
const lastError = result.includes(TURN_CAP_MARK) ? "turn cap reached — work may be partial" : result.includes(STOPPED_MARK) ? "stopped by user — work may be partial" : null
|
|
213
|
+
if (subKey !== undefined && subKey !== null && subKey !== "") {
|
|
214
|
+
finishSubTaskKey(state, String(subKey), lastError)
|
|
215
|
+
} else {
|
|
216
|
+
finishSubTask(state, ["escalate"], lastError)
|
|
354
217
|
}
|
|
355
|
-
|
|
218
|
+
freezeDoneSubTasks(state)
|
|
219
|
+
}
|
|
220
|
+
} else if (name === "consult_stop") {
|
|
221
|
+
// R17(§25 D-R17a——check 已删):consult_stop = 取消指定会诊会话。被 abort 的
|
|
222
|
+
// children 各自 settle(settleChild)时发 ⟦ev⟧done 冻结自己的卡——此处**不做按
|
|
223
|
+
// 角色整组清扫**(会诊会话可并发——按角色会误冻其他仍在运行的会话的卡,冻结即
|
|
224
|
+
// 截断其活动流——tool-events advisor 复评 🟡1 修正);取消的即时可见性由 abort
|
|
225
|
+
// settle 事件承担(abort 解绕通常在同回合内完成——卡片在其 child settle 即冻结)。
|
|
226
|
+
// 仅结果本身落本工具调用自己的载体块(下方通用分支)。
|
|
356
227
|
}
|
|
357
228
|
if (!isSubagent && !isEscalate && name !== "advisor") {
|
|
358
229
|
// Result lands INSIDE the block (restore parity — the restored carrier
|
|
@@ -407,7 +278,7 @@ export function buildToolCallbacks(deps) {
|
|
|
407
278
|
// child-stdout/SSE fragments at arbitrary byte boundaries; trimEnd eats
|
|
408
279
|
// real trailing newlines and routeSubToolOutput's verbatim concat would
|
|
409
280
|
// glue lines (2026-09-03 修复轮; main path keeps the trimmed form below).
|
|
410
|
-
const isSubRelay =
|
|
281
|
+
const isSubRelay = RELAY_PREFIX_RE.test(name)
|
|
411
282
|
const rawText = typeof chunk === "string" ? chunk : String(chunk?.text ?? "")
|
|
412
283
|
const part = {
|
|
413
284
|
kind: typeof chunk === "string" ? "text" : (chunk?.kind ?? "text"),
|
|
@@ -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,8 +20,42 @@ 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
|
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
/** 读 TUI 活动态(F-2 渲染抑制守卫——render-loop doRender 前查——cleanup 清 false 后不再重绘已恢复的主屏)。 */
|
|
36
|
+
export function isTuiActive() {
|
|
37
|
+
return tuiActive
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** R25 崩溃恢复:仅 TUI 活动态执行 writeCleanupSequence(复用本模块清理序列——符号锚)。
|
|
41
|
+
* 测试缝(T-R25a.2——env 门注入):THINCODER_TEST_CLEANUP_OUT 指向文件时序列写入该文件
|
|
42
|
+
* (观察恢复被调 + stdout 管道零 ANSI)——生产不设该 env → 恒 stdout(零变化)。
|
|
43
|
+
* @returns {boolean} 是否执行了恢复(false = TUI 未启动——调用方无需处理) */
|
|
44
|
+
export function restoreTerminalAfterCrash() {
|
|
45
|
+
if (!tuiActive) return false
|
|
46
|
+
const seamOut = process.env.THINCODER_TEST_CLEANUP_OUT
|
|
47
|
+
if (seamOut) {
|
|
48
|
+
writeCleanupSequence((s) => appendFileSync(seamOut, s, "utf8"))
|
|
49
|
+
} else {
|
|
50
|
+
writeCleanupSequence()
|
|
51
|
+
}
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 退出清理闭包:保存会话 + 关闭 MCP + 恢复终端。幂等(cleanedUp 守卫)。stdin/write 注入缝(测试锁序——缺省生产零变化)。
|
|
56
|
+
* F-1 唯一权威序(RESIZE-MOUSE-LEAK-FIX):① mouseOff DECRST → ② settle ~20ms(DECRST 往返——raw 仍开回显仍关)→
|
|
57
|
+
* ③ raw off → ④ stdin 排空 → ⑤ 恢复屏幕 → ⑥ 清 TUI 活动态。 */
|
|
58
|
+
export function createExitCleanup({ agent, saveSession, closeAllMcp, stdin = process.stdin, write = (s) => process.stdout.write(s) }) {
|
|
24
59
|
let cleanedUp = false
|
|
25
60
|
return () => {
|
|
26
61
|
if (cleanedUp) return
|
|
@@ -39,7 +74,14 @@ export function createExitCleanup({ agent, saveSession, closeAllMcp }) {
|
|
|
39
74
|
} catch {
|
|
40
75
|
// Can't close? fine, process is exiting anyway
|
|
41
76
|
}
|
|
42
|
-
|
|
43
|
-
|
|
77
|
+
// ① mouseOff 单独写(DECRST——不整包 writeCleanupSequence):先于 raw off 停鼠标——消除「回显开而鼠标未停」暴露窗口
|
|
78
|
+
write(ansi.mouseOff)
|
|
79
|
+
// ② settle 20ms:Atomics.wait 同步等(不拆注册点——cleanup 亦挂 process.on("exit") 兜底异常退出——exit 事件仅同步合法)
|
|
80
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20)
|
|
81
|
+
// ③ raw off(DECRST 已被终端处理——无新上报可回显)→ ④ 排空:摘 data 监听 + pause——不依赖限时读——在途已读字节丢弃不归 shell
|
|
82
|
+
stdin.setRawMode(false); stdin.removeAllListeners("data"); stdin.pause()
|
|
83
|
+
// ⑤ writeCleanupSequence 余部(mouseOff 已单写——clearScreen/bracketedPasteOff/…/wrapOn 恢复屏幕)
|
|
84
|
+
write(ansi.clearScreen + ansi.bracketedPasteOff + ansi.keyboardPop + ansi.modifyOtherKeysOff + ansi.mainBuffer + ansi.showCursor + ansi.reset + ansi.wrapOn)
|
|
85
|
+
setTuiActive(false) // ⑥ R25:清理完即清活动态(崩溃钩子不再误判——渲染抑制锚)
|
|
44
86
|
}
|
|
45
87
|
}
|
|
@@ -37,9 +37,13 @@ export function createUpdateNotice(ctx) {
|
|
|
37
37
|
const { exec } = await import("node:child_process")
|
|
38
38
|
const cp = exec("npm install -g thincoder@latest", { windowsHide: true })
|
|
39
39
|
let stdout = ""
|
|
40
|
+
let done = false
|
|
40
41
|
cp.stdout?.on("data", (d) => { stdout += d })
|
|
41
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() })
|
|
42
44
|
cp.on("close", (code) => {
|
|
45
|
+
if (done) return
|
|
46
|
+
done = true
|
|
43
47
|
if (code === 0) {
|
|
44
48
|
pushLine(`✓ Upgraded to ${result.latest}. Restart to apply.`, C.tool)
|
|
45
49
|
} else {
|
package/src/tui/wizard.mjs
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { PROVIDER_PRESETS as PRESETS } from "../config.mjs"
|
|
10
10
|
import { ansi, C } from "./ansi.mjs"
|
|
11
|
+
import { computeLayout } from "./layout.mjs"
|
|
12
|
+
import { probeChannelModels } from "./model-catalog.mjs"
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Creates the wizard controller.
|
|
@@ -16,16 +18,17 @@ import { ansi, C } from "./ansi.mjs"
|
|
|
16
18
|
export function createWizard(ctx) {
|
|
17
19
|
const { agent, state, pushLine, pushLabel, render, persistRaw } = ctx
|
|
18
20
|
|
|
19
|
-
/** Candidates for the menu step: existing providers (marked "no key" if missing), unadded presets, custom
|
|
21
|
+
/** Candidates for the menu step: existing providers (marked "no key" if missing), unadded presets, custom
|
|
22
|
+
* MODEL-SELECTION v2:渠道默认模型 = 单值 `model`(preset 自带;候选清单运行期拉取) */
|
|
20
23
|
function wizardProviderItems() {
|
|
21
24
|
const items = []
|
|
22
25
|
for (const p of agent.providers) {
|
|
23
|
-
items.push({ kind: "existing", name: p.name, baseURL: p.baseURL, model: p.model, label: `${p.name} (added${p.apiKey ? "" : ", no key"})` })
|
|
26
|
+
items.push({ kind: "existing", name: p.name, baseURL: p.baseURL, model: p.model ?? "", label: `${p.name} (added${p.apiKey ? "" : ", no key"})` })
|
|
24
27
|
}
|
|
25
28
|
for (const [name, p] of Object.entries(PRESETS)) {
|
|
26
29
|
if (!agent.providers.some((x) => x.name === name)) {
|
|
27
30
|
items.push({
|
|
28
|
-
kind: "preset", name, baseURL: p.baseURL, model: p.model, label: `${name} (${p.desc})`,
|
|
31
|
+
kind: "preset", name, baseURL: p.baseURL, model: p.model ?? "", label: `${name} (${p.desc})`,
|
|
29
32
|
// 预设自身声明的扩展字段随 preset 直达落盘(code review 🟡——与 pickers preset 路径同构;
|
|
30
33
|
// claude/gemini 缺 format、deepseek/glm 缺 thinking/maxTokens 会静默错配);不新增提问步。
|
|
31
34
|
format: p.format, thinking: p.thinking, reasoningEffort: p.reasoningEffort,
|
|
@@ -100,6 +103,19 @@ export function createWizard(ctx) {
|
|
|
100
103
|
}
|
|
101
104
|
if (w.error) lines.push({ text: ` ${w.error}`, color: C.error })
|
|
102
105
|
w.lines = lines
|
|
106
|
+
// A4(第 20 批 §12.5——D-SS6):provider 步选中行自动滚入可视窗——renderWizard 是索引变化的单一路径(产出即一致);
|
|
107
|
+
// winH 走 computeLayout(同 pickers.mjs 口径)+ try/catch 兜底 8(无 dims/测试环境不崩)。
|
|
108
|
+
if (w.step === "provider") {
|
|
109
|
+
let winH
|
|
110
|
+
try {
|
|
111
|
+
winH = Math.max(1, (computeLayout(state, { cols: (state.dims?.get() ?? {}).cols ?? (process.stdout.columns || 80), rows: (state.dims?.get() ?? {}).rows ?? (process.stdout.rows || 24) }).panels.picker?.h ?? lines.length + 1) - 1)
|
|
112
|
+
} catch {
|
|
113
|
+
winH = 8 // safe fallback for mocks without dims(同 pickers.mjs 兜底口径)
|
|
114
|
+
}
|
|
115
|
+
if (w.selectedLine < w.scroll) w.scroll = w.selectedLine
|
|
116
|
+
if (w.selectedLine >= w.scroll + winH) w.scroll = w.selectedLine - winH + 1
|
|
117
|
+
w.scroll = Math.max(0, Math.min(w.scroll, Math.max(0, lines.length - winH)))
|
|
118
|
+
}
|
|
103
119
|
render()
|
|
104
120
|
}
|
|
105
121
|
|
|
@@ -109,8 +125,7 @@ export function createWizard(ctx) {
|
|
|
109
125
|
w.step = "name"
|
|
110
126
|
} else {
|
|
111
127
|
w.fields = { name: item.name, baseURL: item.baseURL, model: item.model }
|
|
112
|
-
// preset
|
|
113
|
-
// 无 format 提问步(T-C4)但落盘不丢字段(code review 🟡——picker preset 路径同款复制)。
|
|
128
|
+
// preset 直达:其余扩展字段照旧(渠道默认模型 = 单值 model——随 fields 落盘)
|
|
114
129
|
for (const k of ["format", "thinking", "reasoningEffort", "maxTokens", "chatPath"]) {
|
|
115
130
|
if (item[k]) w.fields[k] = item[k]
|
|
116
131
|
}
|
|
@@ -145,46 +160,71 @@ export function createWizard(ctx) {
|
|
|
145
160
|
|
|
146
161
|
function cancelWizard() {
|
|
147
162
|
state.wizard = null
|
|
148
|
-
|
|
163
|
+
// MODEL-MERGE-SESSION 引导 A(F-6):有 provider 但 defaultModel 未设时指引 /config 入口
|
|
164
|
+
const hint = (agent.providers?.length ?? 0) > 0 && !agent.config?.defaultModel
|
|
165
|
+
? "Skipped initial setup. 已配置渠道但 config.defaultModel 未设——新会话无起点:/config → 默认模型 设置一次(或 /model 仅改本会话)。"
|
|
166
|
+
: "Skipped initial setup. Use /model to add providers and configure API keys anytime."
|
|
167
|
+
pushLine(hint, C.dim)
|
|
149
168
|
render()
|
|
150
169
|
}
|
|
151
170
|
|
|
152
|
-
/** Wizard complete: write provider (update if exists)
|
|
171
|
+
/** Wizard complete: write provider (update if exists) with its single default model (`model`),
|
|
172
|
+
* set config.defaultModel(裁定⑦——首配模型即写 defaultModel——新会话起点), then open the
|
|
173
|
+
* session model picker. 加渠道 = 配置写入面——落盘后探一次 `/models`(M9:探不通标「不可用」
|
|
174
|
+
* + 明示原因,不阻断保存)。 */
|
|
153
175
|
async function finishWizard() {
|
|
154
176
|
const f = state.wizard.fields
|
|
155
177
|
state.wizard = null
|
|
156
178
|
// D-C2:format 非默认(anthropic/google)时落盘;openai = 默认省略(与 D-C1 picker 路径同构)
|
|
179
|
+
// MODEL-SELECTION v2:渠道默认模型 = 单值 model
|
|
157
180
|
const providerRec = { name: f.name, baseURL: f.baseURL, model: f.model, apiKey: f.key }
|
|
158
181
|
if (f.format && f.format !== "openai") providerRec.format = f.format
|
|
159
|
-
// code review 🟡:preset 直达带来的扩展字段一并落盘(truthy 语义与 pickers preset 分支一致——
|
|
160
|
-
// thinking: null 不落;Custom 路径无这些字段不受影响)
|
|
161
182
|
for (const k of ["thinking", "reasoningEffort", "maxTokens", "chatPath"]) {
|
|
162
183
|
if (f[k]) providerRec[k] = f[k]
|
|
163
184
|
}
|
|
185
|
+
// D-F5a(wizard finishWizard——清单外同型写回补正)先盘后存:磁盘 fresh raw 单操作
|
|
186
|
+
// (upsert 目标项 + defaultModel + 清 legacy 字段)——冲突放弃不留下内存 ghost(F5 约定)
|
|
187
|
+
await persistRaw((raw) => {
|
|
188
|
+
raw.providers ??= []
|
|
189
|
+
const existing = raw.providers.find((p) => p?.name === f.name)
|
|
190
|
+
if (existing) Object.assign(existing, providerRec)
|
|
191
|
+
else raw.providers.push(providerRec)
|
|
192
|
+
raw.defaultModel = `${f.name}:${providerRec.model}`
|
|
193
|
+
delete raw.activeProvider
|
|
194
|
+
delete raw.activeModel
|
|
195
|
+
// 渠道老字段(models 候选清单)由 config-migrate 在下次 load 统一清理(迁移唯一权威)
|
|
196
|
+
})
|
|
164
197
|
const existing = agent.providers.find((p) => p.name === f.name)
|
|
165
198
|
if (existing) Object.assign(existing, providerRec)
|
|
166
199
|
else agent.providers.push(providerRec)
|
|
167
200
|
agent.activeProvider = f.name
|
|
168
|
-
agent.activeModel =
|
|
201
|
+
agent.activeModel = providerRec.model
|
|
169
202
|
agent.provider = { ...agent.providers.find((p) => p.name === f.name) }
|
|
203
|
+
agent.provider.model = agent.activeModel
|
|
170
204
|
if (agent.config?.agent?.compactThresholdAuto) {
|
|
171
205
|
const { resolveCompactThreshold } = await import("../config.mjs")
|
|
172
|
-
agent.config.agent.compactThreshold = resolveCompactThreshold(null,
|
|
206
|
+
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
173
207
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
raw.activeProvider = f.name
|
|
177
|
-
raw.activeModel = undefined // reset to default model
|
|
178
|
-
})
|
|
179
|
-
agent.config.activeProvider = f.name
|
|
180
|
-
agent.config.activeModel = null
|
|
208
|
+
// agent.config 是 loadConfig merged——无 active* 键可写——defaultModel 随内存 merged 更新
|
|
209
|
+
agent.config.defaultModel = `${f.name}:${agent.activeModel}`
|
|
181
210
|
pushLabel(`❯ Setup`, ansi.bold + C.tool)
|
|
182
|
-
pushLine(`Setup complete: ${f.name} / ${
|
|
211
|
+
pushLine(`Setup complete: ${f.name} / ${agent.activeModel} (defaultModel 已设——新会话起点)`, C.tool)
|
|
212
|
+
// M9 配置阶段准入:加渠道属配置写入面——保存已落,探一次 `/models`(探不通标「不可用」+ 明示原因;不阻断)
|
|
213
|
+
const channel = agent.providers.find((p) => p.name === f.name) ?? providerRec
|
|
214
|
+
const probe = await probeChannelModels(channel)
|
|
215
|
+
if (probe.ok) {
|
|
216
|
+
delete channel._unavailable
|
|
217
|
+
pushLine(`${f.name}: /models 可用(${probe.list.length} 个模型可候选)`, C.tool)
|
|
218
|
+
} else {
|
|
219
|
+
channel._unavailable = true
|
|
220
|
+
pushLine(`${f.name} 不可用 — ${probe.message}`, C.error)
|
|
221
|
+
}
|
|
183
222
|
// embedding key: if provided, enable vector search; if not, show how to enable later
|
|
184
223
|
if (f.embedkey) {
|
|
224
|
+
// D-F5b 语义先盘后存(embedding 单键补丁——冲突放弃不留 ghost)
|
|
225
|
+
await persistRaw((raw) => { raw.embedding = { ...(raw.embedding ?? {}), apiKey: f.embedkey } })
|
|
185
226
|
agent.config.embedding ??= {}
|
|
186
227
|
agent.config.embedding.apiKey = f.embedkey
|
|
187
|
-
await persistRaw((raw) => { raw.embedding = { ...(raw.embedding ?? {}), apiKey: f.embedkey } })
|
|
188
228
|
if (agent.memory && !agent.memory.embedder) {
|
|
189
229
|
const { createEmbedder } = await import("../embedding.mjs")
|
|
190
230
|
agent.memory.embedder = createEmbedder(agent.config.embedding)
|
|
@@ -193,7 +233,7 @@ export function createWizard(ctx) {
|
|
|
193
233
|
} else {
|
|
194
234
|
pushLine(`Vector search disabled (memory falls back to text-only search). Run /config embedkey <key> to enable.`, C.dim)
|
|
195
235
|
}
|
|
196
|
-
pushLine(`Select model (Esc to keep ${
|
|
236
|
+
pushLine(`Select model (Esc to keep ${agent.activeModel})`, C.dim)
|
|
197
237
|
ctx.openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
|
|
198
238
|
}
|
|
199
239
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** wrapped-spawn.mjs — TUI-STDERR-CAPTURE F-1/F-3:包装父
|
|
2
|
+
* spawn 子(自身 bin)tee stderr → 终端 + crash-reports/tui-stderr-<ts>-<pid>.log(外部终止/
|
|
3
|
+
* native abort——fd 2 进程内不可改——诊断唯一默认捕获路)。子死 → 日志收尾 → 同码退(null 映射
|
|
4
|
+
* code??(signal?1:0)——评审 #1);spawn error → 注日志 + exit 1(评审 #5——不挂死)。 */
|
|
5
|
+
import { appendFileSync, mkdirSync, writeFileSync } from "node:fs"
|
|
6
|
+
import { spawn } from "node:child_process"
|
|
7
|
+
import { fileURLToPath } from "node:url"
|
|
8
|
+
import { crashReportsDir } from "../crash-reports.mjs"
|
|
9
|
+
|
|
10
|
+
// F-3 信号语义(raw mode 既有 key-handler 双按语义——Ctrl+C = stdin 字节不产生信号 → 子正常退 → 父收
|
|
11
|
+
// exit 同码退):父忽略 SIGINT/SIGTERM——tee 不被打断。2026-09-09 实测:Windows process.kill(SIGINT)
|
|
12
|
+
// = 硬杀 ≠ 控制台 Ctrl+C 事件(handler 不触发)——信号面测试走 mock(真控制台端到端留发布前手动 QA)。
|
|
13
|
+
export const ignoreSignal = () => {} // no-op 单一引用——父信号 + stderr-error 监听共用——测试可精确复原
|
|
14
|
+
export function spawnTuiWrapped({ dir = crashReportsDir(), script = fileURLToPath(new URL("../../bin/thincoder.mjs", import.meta.url)), spawnImpl = spawn, exitImpl = (code) => process.exit(code) } = {}) {
|
|
15
|
+
// ① mkdir 前置(评审 #2——prepareCrashReporting 只在子内跑——首启目录缺失会静默不包装)+ 开日志
|
|
16
|
+
//(文件头元信息行:时间/pid/argv——F-2——0600 append)——失败 → false:不包装直接跑现逻辑(尽力面)
|
|
17
|
+
let logPath = null
|
|
18
|
+
try {
|
|
19
|
+
mkdirSync(dir, { recursive: true })
|
|
20
|
+
logPath = `${dir}/tui-stderr-${Date.now()}-${process.pid}.log`
|
|
21
|
+
writeFileSync(logPath, `# tui-stderr ${new Date().toISOString()} pid=${process.pid} argv=${JSON.stringify(process.argv)}\n`, { flag: "a", mode: 0o600 })
|
|
22
|
+
} catch { return false }
|
|
23
|
+
process.on("SIGINT", ignoreSignal); process.on("SIGTERM", ignoreSignal) // 父忽略信号(F-3)——tee 不被打断
|
|
24
|
+
process.stderr.on("error", ignoreSignal) // 父 stderr 异步 EPIPE(终端/管道已关)→ 不炸父——tee 继续
|
|
25
|
+
const note = (s) => { try { appendFileSync(logPath, s) } catch { /* 落盘尽力面——不阻断 tee */ } }
|
|
26
|
+
let exitCode = null, exitSignal = null, settled = false
|
|
27
|
+
const finish = (code) => { if (!settled) { settled = true; exitImpl(code) } }
|
|
28
|
+
const child = spawnImpl(process.execPath, [script, ...process.argv.slice(2)], {
|
|
29
|
+
stdio: ["inherit", "inherit", "pipe"], // 子 stderr pipe → tee;stdin/stdout 继承(TTY 原样)
|
|
30
|
+
env: { ...process.env, THINCODER_TUI_WRAPPED: "1" }, // env 门——子内判定不包装——纯现逻辑(红线)
|
|
31
|
+
windowsHide: false,
|
|
32
|
+
})
|
|
33
|
+
child.stderr.on("data", (chunk) => { try { process.stderr.write(chunk) } catch { /* 终端已关——日志仍落 */ } note(chunk) }) // ② tee 双写:终端实时 + 日志
|
|
34
|
+
child.on("exit", (code, signal) => { exitCode = code; exitSignal = signal; setTimeout(() => finish(exitCode ?? (exitSignal ? 1 : 0)), 30_000).unref() }) // ③ exit 记码;兜底 30s 强退
|
|
35
|
+
child.on("close", () => finish(exitCode ?? (exitSignal ? 1 : 0))) // ④ close = stderr 尾数据全收(AC-2)→ 同码退
|
|
36
|
+
child.on("error", (err) => { note(`# spawn error: ${err.message}\n`); finish(1) }) // ⑤ spawn 失败不发 exit 只发 error
|
|
37
|
+
return true
|
|
38
|
+
}
|
package/src/prompts/coder.md
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
You are a coding subagent. The parent agent dispatched you to handle a self-contained coding task. The parent CANNOT see your context — it only sees your final report.
|
|
2
|
-
|
|
3
|
-
## Your role (identity — read before you code)
|
|
4
|
-
|
|
5
|
-
You are an IMPLEMENTER with independent judgment — not a typewriter.
|
|
6
|
-
|
|
7
|
-
1. **Evidence discipline**: every factual/behavioral assertion you make MUST be
|
|
8
|
-
verified from the code/docs in front of you (read them, cite file:line) —
|
|
9
|
-
or explicitly marked `unverified`. NEVER assert "Known behavior…",
|
|
10
|
-
"I'm confident…", or rely on remembered API semantics when the source is
|
|
11
|
-
readable — a behavioral question is an EVIDENCE question, not a reasoning
|
|
12
|
-
question.
|
|
13
|
-
2. **Neutrality**: you implement the design; you are not the designer. If the
|
|
14
|
-
design conflicts with what you find in the code (an interface change broke a
|
|
15
|
-
caller, a referenced symbol does not exist), STOP and report the conflict
|
|
16
|
-
to the parent — do not silently adapt. The parent decides; you surface.
|
|
17
|
-
3. **Boundary**: your task = the parent's task brief (files, acceptance
|
|
18
|
-
criteria). Do not expand it. Findings that touch things outside the brief
|
|
19
|
-
(other modules, parent-side docs) go in a trailing "out-of-scope note" in
|
|
20
|
-
your report — no action without the parent's word.
|
|
21
|
-
|
|
22
|
-
- before you start coding, locate the owning design doc for this change (docs/design/ — via the doc map); if it exists, note the change in it (变更记录/设计注); if not, create it and register it in the map. Then code. No exemption — even one-line fixes.
|
|
23
|
-
|
|
24
|
-
Guidelines:
|
|
25
|
-
- Work independently: use doc_search to learn project conventions and design, repo_outline to understand structure, then code_search to find implementations.
|
|
26
|
-
Don't write code until you know what the project intends.
|
|
27
|
-
- COMPLETE delivery: solve the ENTIRE task the parent gave you — every requirement, every file, every acceptance criterion. Nothing less. Do what was asked, fully. No opportunistic cleanup, no speculative generality, no half-finished refactors. When you finish, include a delivery table (see Discipline rules) — every requirement either Done, Simplified, or Not done. The parent doesn't read your diff; it reads your report.
|
|
28
|
-
- Write code one file at a time, verify each before moving on — don't write multiple files at once without checking each along the way:
|
|
29
|
-
1. After every write/edit of a file: run a syntax/lint check to catch parse errors immediately
|
|
30
|
-
2. After a logical group of changes: run the relevant tests to confirm behavior
|
|
31
|
-
3. Before finishing: run tests relevant to your changes; run the full test suite only if you changed core infrastructure (agent loop, provider protocol, config schema, tool execution, memory schema)
|
|
32
|
-
- Be thorough: include what you did, which files you changed, why, and any caveats
|
|
33
|
-
- If the task is ambiguous, note the ambiguity in your report; do not ask the user
|
|
34
|
-
- It is always OK to say "this is too hard for me." Bad work is worse than no work — you will not be penalized for escalating
|
|
35
|
-
- BEFORE finishing, do a final review of your work:
|
|
36
|
-
1. Run relevant tests — confirm all pass
|
|
37
|
-
2. If no existing test covers your change, add at least one test
|
|
38
|
-
3. Read every file you changed — catch leftover debug code, stale comments, or incomplete edits
|
|
39
|
-
4. Check that comments and docstrings match what the code actually does
|
|
40
|
-
5. Verify imports/dependencies are correct — no stale or missing references
|
|
41
|
-
- Your last message IS the report the parent sees — it is the ONLY thing the parent receives. Make it complete and self-contained. A report that fails this checklist is sent back for expansion, costing an extra turn:
|
|
42
|
-
1. What you changed and why
|
|
43
|
-
2. The path of every file you touched
|
|
44
|
-
3. How you verified the change (tests run, commands executed, with results)
|
|
45
|
-
4. **Delivery transparency table** — mandatory. Format:
|
|
46
|
-
| # | Status | Requirement |
|
|
47
|
-
|---|--------|-------------|
|
|
48
|
-
| 1 | ✅ Done | (fully covered) |
|
|
49
|
-
| 2 | ⚠️ Simplified | (delivered but simpler — explain the gap) |
|
|
50
|
-
| 3 | ❌ Not done | (NOT implemented — including anything you wanted to defer) |
|
|
51
|
-
Every requirement point from the parent's task must appear in exactly one row. There is no "deferred" or "later" column — pushing to later means "not done now," so it goes under ❌.
|
|
52
|
-
5. consistency self-check: does the delivery match the task instruction and the board design doc (if any)? Report deviations explicitly. Fix implementation deviations (partial implementation / silent simplification) so the delivery matches the doc before reporting; report genuine doc drift or out-of-scope changes.
|
|
53
|
-
|
|
54
|
-
IMPORTANT — Tool permissions: when you see "permission denied by user" for a tool, it means the parent has not granted that tool.
|
|
55
|
-
This is expected: your job is to write a detailed report of what SHOULD be done, not to force tool execution.
|
|
56
|
-
Describe the needed changes clearly in your report so the parent agent can apply them.
|