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/pickers.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { sliceByWidth } from "./render.mjs"
|
|
2
|
-
import { PROVIDER_PRESETS as PRESETS, providerSpec } from "../config.mjs"
|
|
3
1
|
import { computeLayout } from "./layout.mjs"
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { createModelPicker } from "./model-picker.mjs"
|
|
3
|
+
|
|
4
|
+
/** Generic list picker plumbing + /model two-level surface delegation.
|
|
5
|
+
* MODEL-MERGE-SESSION 拆分(评审 #9):/model 两级面(L1 provider → L2 模型行——
|
|
6
|
+
* 2026-09-10 MODEL-SELECTION v2 起 L2 行 = 运行期拉取候选)+ 渠道
|
|
7
|
+
* 管理流 + selectModel + pickModelForSlot 迁 model-picker.mjs——本文件只留通用 picker
|
|
8
|
+
* 栈(showPicker/closePicker/popPicker/rebuildLines)——createModelPicker 以 ctx 接收
|
|
9
|
+
* 通用绑定(showPicker/closePicker/renderPickerLines)再行装配——index.mjs 消费面不变。
|
|
6
10
|
* 单一 Promise API:showPicker(title, entries, { defaultIndex }) → Promise<entry|null>。
|
|
7
11
|
* picker 栈:state.pickerStack,state.picker 始终指向栈顶(layout/render/key-handler 都只读 state.picker)。
|
|
8
12
|
* 选中即关闭(Enter = resolve + pop);Esc = pop 当前层并 resolve(null)。菜单循环由调用方 while 重开。 */
|
|
@@ -37,6 +41,12 @@ export function createPickers(ctx) {
|
|
|
37
41
|
* 互斥保护:入栈前把现有挂起 picker 全部 resolve(null),消除 Promise 悬挂。
|
|
38
42
|
* (正常嵌套是先 await 上一层返回再开新的,栈深通常为 1。) */
|
|
39
43
|
function showPicker(title, entries, { defaultIndex = 0 } = {}) {
|
|
44
|
+
if (!Array.isArray(entries)) {
|
|
45
|
+
const got = entries && typeof entries.then === "function"
|
|
46
|
+
? "a Promise (missing `await`?)"
|
|
47
|
+
: `a ${entries === null ? "null" : typeof entries}`
|
|
48
|
+
throw new TypeError(`showPicker("${title}"): entries must be an array — got ${got}`)
|
|
49
|
+
}
|
|
40
50
|
closePicker()
|
|
41
51
|
return new Promise((resolve) => {
|
|
42
52
|
const itemCount = entries.filter((e) => e.type === "item").length
|
|
@@ -65,7 +75,12 @@ export function createPickers(ctx) {
|
|
|
65
75
|
const sel = row === p.index
|
|
66
76
|
if (sel) selLine = lines.length
|
|
67
77
|
const marker = e.marker ? ` ${e.marker}` : ""
|
|
68
|
-
|
|
78
|
+
// A1(第 20 批 §12.5——D-SS7):item 附注消费——渲染形态 {prefix}{text}{marker}{note};
|
|
79
|
+
// header 分支既有消费同形态,无 note 零追加(不产生尾随空格)。宽度预算 =
|
|
80
|
+
// renderPicker 既有 8 格余量 + 右截断——保序 prefix→text→marker→note,截断从行尾
|
|
81
|
+
// 开始 = note 段最先牺牲。
|
|
82
|
+
const note = e.note ? ` ${e.note}` : ""
|
|
83
|
+
lines.push({ text: `${sel ? " ▸ " : " "}${e.text}${marker}${note}`, color: sel ? ansi.bold + C.text : C.dim, _row: row })
|
|
69
84
|
row++
|
|
70
85
|
}
|
|
71
86
|
}
|
|
@@ -90,392 +105,13 @@ export function createPickers(ctx) {
|
|
|
90
105
|
|
|
91
106
|
function renderPickerLines() { rebuildLines() }
|
|
92
107
|
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
.replace(/-\d{4}-\d{2}-\d{2}$/, "") // date suffix like -2024-09-19
|
|
101
|
-
.replace(/-\d{8}$/, "") // date suffix like -20240919
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Dedupe model list: group by series, keep shortest name per group.
|
|
105
|
-
* Reduces "qwen-max, qwen-max-latest, qwen-max-2024-09-19" to just "qwen-max". */
|
|
106
|
-
function dedupeModels(models) {
|
|
107
|
-
const groups = new Map()
|
|
108
|
-
for (const m of models) {
|
|
109
|
-
const series = modelSeries(m)
|
|
110
|
-
const existing = groups.get(series)
|
|
111
|
-
if (!existing || m.length < existing.length) {
|
|
112
|
-
groups.set(series, m)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return [...groups.values()].sort()
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** entry 唯一标识:异步更新 entries 后按它恢复选中项 */
|
|
119
|
-
function entryKey(e) {
|
|
120
|
-
if (!e) return null
|
|
121
|
-
return e.action === "switch" ? `switch:${e.provider}:${e.model}` : `action:${e.action}`
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** 模型信息显示的 context 窗口(PROVIDER.md §15 D-C5):K 单位形态跟随覆盖值
|
|
125
|
-
* (config K 是二进制 K:128 → 128×1024 = 131072 → 显示 "128K");≥1M tokens 用 M 形态
|
|
126
|
-
* (1_000_000 → "1M"),与 spec 值的大窗口惯例一致。 */
|
|
127
|
-
function fmtContextK(tokens) {
|
|
128
|
-
if (tokens >= 1_000_000) return `${Math.round(tokens / 1_048_576)}M`
|
|
129
|
-
return `${Math.round(tokens / 1024)}K`
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Get API key for a provider (config.json only — env vars are not a key source) */
|
|
133
|
-
function getApiKey(providerName, providerConfig) {
|
|
134
|
-
return providerConfig.apiKey
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Level 1: Show provider list. Selecting a provider opens Level 2 (model list). */
|
|
138
|
-
async function openModelPicker() {
|
|
139
|
-
// 菜单循环:选中即关闭,子流程结束后重开主菜单;Esc 退出
|
|
140
|
-
for (;;) {
|
|
141
|
-
const entries = buildProviderEntries()
|
|
142
|
-
const items = entries.filter((e) => e.type === "item")
|
|
143
|
-
const current = items.findIndex(
|
|
144
|
-
(e) => e.action === "open-models" && e.provider === agent.activeProvider)
|
|
145
|
-
const picked = showPicker("Models & Providers", entries, { defaultIndex: Math.max(0, current) })
|
|
146
|
-
const e = await picked
|
|
147
|
-
if (!e) return
|
|
148
|
-
if (e.action === "open-models") {
|
|
149
|
-
// Level 2: open model list for this provider
|
|
150
|
-
const modelSelected = await openModelListForProvider(e.provider)
|
|
151
|
-
if (modelSelected) return // model selected, close picker
|
|
152
|
-
// Esc from model list → return to provider list
|
|
153
|
-
} else if (e.action === "add") {
|
|
154
|
-
await addProviderFlow()
|
|
155
|
-
} else if (e.action === "remove") {
|
|
156
|
-
await removeProviderFlow()
|
|
157
|
-
} else if (e.action === "key") {
|
|
158
|
-
await setKeyFlow()
|
|
159
|
-
} else if (e.action === "context") {
|
|
160
|
-
await setContextFlow()
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Level 2: Show model list for a specific provider. Returns true if a model was selected. */
|
|
166
|
-
async function openModelListForProvider(providerName) {
|
|
167
|
-
const providerConfig = agent.providers.find((p) => p.name === providerName)
|
|
168
|
-
if (!providerConfig) return false
|
|
169
|
-
|
|
170
|
-
const entries = buildModelEntriesForProvider(providerName, providerConfig)
|
|
171
|
-
const items = entries.filter((e) => e.type === "item")
|
|
172
|
-
const currentModel = providerName === agent.activeProvider
|
|
173
|
-
? (agent.activeModel || providerConfig.model)
|
|
174
|
-
: providerConfig.model
|
|
175
|
-
const current = items.findIndex((e) => e.model === currentModel)
|
|
176
|
-
const picked = showPicker(`${providerName} models`, entries, { defaultIndex: Math.max(0, current) })
|
|
177
|
-
|
|
178
|
-
// Async fetch models in background
|
|
179
|
-
fetchModelsForProvider(providerName, entries).catch((err) => {
|
|
180
|
-
pushLine(`[model] fetch models failed: ${err.message}`, C.error)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
const e = await picked
|
|
184
|
-
if (!e) return false // Esc → back to provider list
|
|
185
|
-
if (e.action === "switch") {
|
|
186
|
-
await selectModel(e).catch((err) => pushLine(`[error] ${err.message}`, C.error))
|
|
187
|
-
return true
|
|
188
|
-
}
|
|
189
|
-
return false
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** Build entries for Level 1: provider list */
|
|
193
|
-
function buildProviderEntries() {
|
|
194
|
-
const entries = []
|
|
195
|
-
for (const p of agent.providers) {
|
|
196
|
-
const active = p.name === agent.activeProvider
|
|
197
|
-
const currentModel = active ? (agent.activeModel || p.model) : p.model
|
|
198
|
-
const marker = active ? "●" : ""
|
|
199
|
-
const note = active ? " ← current" : ""
|
|
200
|
-
const keyStatus = p.apiKey ? "" : " (no key)"
|
|
201
|
-
// context 窗口显示(PROVIDER.md §15 D-C5/T-C6):跟随 providers[].context 覆盖
|
|
202
|
-
const ctxTag = ` (ctx ${fmtContextK(providerSpec(p).context)})`
|
|
203
|
-
entries.push({
|
|
204
|
-
type: "item",
|
|
205
|
-
text: `${p.name.padEnd(12)} ${currentModel}${ctxTag}${note}`,
|
|
206
|
-
action: "open-models",
|
|
207
|
-
provider: p.name,
|
|
208
|
-
marker,
|
|
209
|
-
note: `${p.baseURL}${keyStatus}`,
|
|
210
|
-
})
|
|
211
|
-
}
|
|
212
|
-
entries.push({ type: "header", text: "Management" })
|
|
213
|
-
entries.push({ type: "item", text: "Add provider…", action: "add" })
|
|
214
|
-
if (agent.providers.length > 1) entries.push({ type: "item", text: "Remove provider…", action: "remove" })
|
|
215
|
-
entries.push({ type: "item", text: "Set / change API key…", action: "key" })
|
|
216
|
-
entries.push({ type: "item", text: "Set context window (K units)…", action: "context" })
|
|
217
|
-
return entries
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Build entries for Level 2: model list for a specific provider */
|
|
221
|
-
function buildModelEntriesForProvider(providerName, providerConfig) {
|
|
222
|
-
const entries = []
|
|
223
|
-
const active = providerName === agent.activeProvider
|
|
224
|
-
const currentModel = active ? (agent.activeModel || providerConfig.model) : providerConfig.model
|
|
225
|
-
const isDefaultModel = currentModel === providerConfig.model
|
|
226
|
-
|
|
227
|
-
entries.push({ type: "header", text: "Current model" })
|
|
228
|
-
entries.push({
|
|
229
|
-
type: "item",
|
|
230
|
-
text: currentModel,
|
|
231
|
-
action: "switch",
|
|
232
|
-
provider: providerName,
|
|
233
|
-
model: currentModel,
|
|
234
|
-
marker: active && isDefaultModel ? "●" : "",
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
entries.push({ type: "header", text: "Available models (loading…)" })
|
|
238
|
-
return entries
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** Async fetch and splice models for a specific provider */
|
|
242
|
-
async function fetchModelsForProvider(providerName, entries) {
|
|
243
|
-
const { listModels } = await import("../provider/index.mjs")
|
|
244
|
-
const providerConfig = agent.providers.find((p) => p.name === providerName)
|
|
245
|
-
if (!providerConfig) return
|
|
246
|
-
|
|
247
|
-
let selKey = null
|
|
248
|
-
try {
|
|
249
|
-
const apiKey = getApiKey(providerName, providerConfig)
|
|
250
|
-
const models = await listModels(
|
|
251
|
-
{ baseURL: providerConfig.baseURL, apiKey: apiKey ?? "" },
|
|
252
|
-
{ signal: AbortSignal.timeout(10000) }
|
|
253
|
-
)
|
|
254
|
-
if (state.picker?.entries !== entries) return // picker closed or changed
|
|
255
|
-
selKey = entryKey(pickerItems(state.picker)[state.picker.index])
|
|
256
|
-
|
|
257
|
-
// Dedupe and filter
|
|
258
|
-
const deduped = dedupeModels(models)
|
|
259
|
-
const currentModel = providerName === agent.activeProvider
|
|
260
|
-
? (agent.activeModel || providerConfig.model)
|
|
261
|
-
: providerConfig.model
|
|
262
|
-
|
|
263
|
-
// Find the "Available models" header and splice after it
|
|
264
|
-
const headerIdx = entries.findIndex((e) => e.type === "header" && e.text.startsWith("Available models"))
|
|
265
|
-
if (headerIdx >= 0) {
|
|
266
|
-
// Update header with count info
|
|
267
|
-
const hint = deduped.length < models.length
|
|
268
|
-
? ` (${models.length} total, ${deduped.length} shown — type to filter)`
|
|
269
|
-
: ` (${deduped.length})`
|
|
270
|
-
entries[headerIdx].text = `Available models${hint}`
|
|
271
|
-
|
|
272
|
-
// Splice models after header (skip current model)
|
|
273
|
-
const newModels = deduped.filter((m) => m !== currentModel)
|
|
274
|
-
entries.splice(headerIdx + 1, 0, ...newModels.map((m) => ({
|
|
275
|
-
type: "item",
|
|
276
|
-
text: m,
|
|
277
|
-
action: "switch",
|
|
278
|
-
provider: providerName,
|
|
279
|
-
model: m,
|
|
280
|
-
})))
|
|
281
|
-
}
|
|
282
|
-
} catch (error) {
|
|
283
|
-
if (state.picker?.entries !== entries) return
|
|
284
|
-
const headerIdx = entries.findIndex((e) => e.type === "header" && e.text.startsWith("Available models"))
|
|
285
|
-
if (headerIdx >= 0) {
|
|
286
|
-
entries[headerIdx].text = `Available models (fetch failed: ${sliceByWidth(error.message, 30)})`
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Restore selection
|
|
291
|
-
const pk = state.picker
|
|
292
|
-
const items = pickerItems(pk)
|
|
293
|
-
const restored = selKey ? items.findIndex((e) => entryKey(e) === selKey) : -1
|
|
294
|
-
pk.index = restored >= 0 ? restored : Math.min(pk.index, Math.max(0, items.length - 1))
|
|
295
|
-
rebuildLines()
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
async function selectModel(item) {
|
|
299
|
-
closePicker()
|
|
300
|
-
const target = agent.providers.find((pp) => pp.name === item.provider)
|
|
301
|
-
if (!target) return
|
|
302
|
-
const providerDefault = target.model
|
|
303
|
-
target.model = item.model
|
|
304
|
-
agent.activeProvider = item.provider
|
|
305
|
-
// If selecting the provider's default model, clear activeModel; otherwise set it
|
|
306
|
-
agent.activeModel = item.model !== providerDefault ? item.model : null
|
|
307
|
-
agent.provider = { ...target }
|
|
308
|
-
if (agent.config?.agent?.compactThresholdAuto) {
|
|
309
|
-
const { resolveCompactThreshold } = await import("../config.mjs")
|
|
310
|
-
// provider 对象(非 model 字符串)——阈值跟随 providers[].context 覆盖(PROVIDER.md §15 T-C2)
|
|
311
|
-
agent.config.agent.compactThreshold = resolveCompactThreshold(null, target).value
|
|
312
|
-
}
|
|
313
|
-
await persistRaw((raw) => {
|
|
314
|
-
// 落盘前剥离运行时注入的 proxyUri(由 loadConfig + injectProxy 在加载时重建)
|
|
315
|
-
raw.providers = agent.providers.map(({ proxyUri: _, ...p }) => p)
|
|
316
|
-
raw.activeProvider = item.provider
|
|
317
|
-
raw.activeModel = agent.activeModel || undefined // null → omit from config
|
|
318
|
-
})
|
|
319
|
-
agent.config.activeProvider = item.provider
|
|
320
|
-
agent.config.activeModel = agent.activeModel
|
|
321
|
-
if (!agent.provider.apiKey) {
|
|
322
|
-
const selKey = await askQuestion(`Enter API key for ${item.provider} (leave empty to skip):`)
|
|
323
|
-
if (selKey) await setProviderKey(item.provider, selKey)
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async function addProviderFlow() {
|
|
328
|
-
const entries = [
|
|
329
|
-
{ type: "header", text: "Select a preset provider" },
|
|
330
|
-
...Object.entries(PRESETS).filter(([name]) => !agent.providers.some((p) => p.name === name))
|
|
331
|
-
.map(([name, p]) => ({ type: "item", text: `${name.padEnd(10)} ${p.desc ?? ""} (${p.model})`, name, kind: "preset" })),
|
|
332
|
-
{ type: "header", text: "Other" },
|
|
333
|
-
{ type: "item", text: "Custom (manual config)", name: "__custom__", kind: "custom" },
|
|
334
|
-
]
|
|
335
|
-
const se = await showPicker("Add Provider", entries)
|
|
336
|
-
if (!se) return // Esc → 返回上级(openModelPicker 循环会重开主菜单)
|
|
337
|
-
if (se.kind === "custom") {
|
|
338
|
-
const name = await askQuestion("Enter provider name:")
|
|
339
|
-
if (!name) return
|
|
340
|
-
if (agent.providers.some((p) => p.name === name)) return
|
|
341
|
-
const baseURL = (await askQuestion("Enter baseURL:")).replace(/\/+$/, "")
|
|
342
|
-
if (!baseURL) return
|
|
343
|
-
const model = await askQuestion("Enter model name:")
|
|
344
|
-
if (!model) return
|
|
345
|
-
// D-C1(TUI.md §10.6D):API format 改 picker 选择(openai/anthropic/google——默认 index=0
|
|
346
|
-
// openai)——固定枚举不再手输;Esc/取消 = 中止整个 Add Provider 流程(cfg 尚未落盘——
|
|
347
|
-
// 无半配置,与旧手输非法 abort 同语义)。与 preset 选择器同形态——零新机制。
|
|
348
|
-
const format = await showPicker("API format", [
|
|
349
|
-
{ type: "item", text: "openai", name: "openai" },
|
|
350
|
-
{ type: "item", text: "anthropic", name: "anthropic" },
|
|
351
|
-
{ type: "item", text: "google", name: "google" },
|
|
352
|
-
])
|
|
353
|
-
if (!format) return // Esc/取消 → 中止流程
|
|
354
|
-
const cfg = { name, baseURL, model }
|
|
355
|
-
if (format.name === "anthropic" || format.name === "google") cfg.format = format.name
|
|
356
|
-
else if (format.name !== "openai") return // 防御:未知格式(理论不可达——picker 枚举)
|
|
357
|
-
agent.providers.push(cfg)
|
|
358
|
-
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
359
|
-
const key = await askQuestion(`Enter API key for ${name} (skip if none):`)
|
|
360
|
-
if (key) await setProviderKey(name, key)
|
|
361
|
-
return
|
|
362
|
-
}
|
|
363
|
-
const preset = PRESETS[se.name]
|
|
364
|
-
if (!preset || agent.providers.some((p) => p.name === se.name)) return
|
|
365
|
-
const cfg = { name: se.name, baseURL: preset.baseURL, model: preset.model }
|
|
366
|
-
if (preset.thinking) cfg.thinking = preset.thinking
|
|
367
|
-
if (preset.reasoningEffort) cfg.reasoningEffort = preset.reasoningEffort
|
|
368
|
-
if (preset.maxTokens) cfg.maxTokens = preset.maxTokens
|
|
369
|
-
if (preset.chatPath) cfg.chatPath = preset.chatPath
|
|
370
|
-
if (preset.format) cfg.format = preset.format
|
|
371
|
-
agent.providers.push(cfg)
|
|
372
|
-
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
373
|
-
const key = await askQuestion(`Enter API key for ${se.name} (skip if none):`)
|
|
374
|
-
if (key) await setProviderKey(se.name, key)
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
async function removeProviderFlow() {
|
|
378
|
-
const candidates = agent.providers.filter((p) => p.name !== agent.activeProvider)
|
|
379
|
-
if (!candidates.length) return
|
|
380
|
-
const se = await showPicker("Remove Provider", [
|
|
381
|
-
{ type: "header", text: "Select provider to remove" },
|
|
382
|
-
...candidates.map((p) => ({ type: "item", text: `${p.name} (${p.model})`, name: p.name })),
|
|
383
|
-
])
|
|
384
|
-
if (!se) return
|
|
385
|
-
agent.providers.splice(agent.providers.findIndex((p) => p.name === se.name), 1)
|
|
386
|
-
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
async function setKeyFlow() {
|
|
390
|
-
const se = await showPicker("Configure API Key", [
|
|
391
|
-
{ type: "header", text: "Select provider" },
|
|
392
|
-
...agent.providers.map((p) => ({ type: "item", text: `${p.name} ${p.apiKey ? `(has key: ${maskKey(p.apiKey)})` : "(no key)"}`, name: p.name })),
|
|
393
|
-
])
|
|
394
|
-
if (!se) return
|
|
395
|
-
const key = await askQuestion(`Enter API key for ${se.name}:`)
|
|
396
|
-
if (key) await setProviderKey(se.name, key)
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
async function setProviderKey(name, key) {
|
|
400
|
-
const target = agent.providers.find((p) => p.name === name)
|
|
401
|
-
if (!target) return
|
|
402
|
-
target.apiKey = key
|
|
403
|
-
if (name === agent.activeProvider) agent.provider.apiKey = key
|
|
404
|
-
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/** /model provider 管理:context 窗口字段(K 单位,PROVIDER.md §15 D-C4/D-C5)——
|
|
408
|
-
* picker 选 provider + 表单输入(复用 syncProviderField 的落盘模式:改 agent.providers 目标项
|
|
409
|
-
* → persistRaw 全量写盘);空输入清空(回 spec 值);非法输入报错不落盘(D-C1 语义同 loadConfig)。 */
|
|
410
|
-
async function setContextFlow() {
|
|
411
|
-
const se = await showPicker("Set Context Window", [
|
|
412
|
-
{ type: "header", text: "Select provider" },
|
|
413
|
-
...agent.providers.map((p) => ({
|
|
414
|
-
type: "item",
|
|
415
|
-
text: `${p.name} (ctx ${fmtContextK(providerSpec(p).context)})`,
|
|
416
|
-
name: p.name,
|
|
417
|
-
})),
|
|
418
|
-
])
|
|
419
|
-
if (!se?.name) return // Esc
|
|
420
|
-
const target = agent.providers.find((p) => p.name === se.name)
|
|
421
|
-
if (!target) return
|
|
422
|
-
const current = Number.isInteger(target.context) && target.context > 0 ? target.context : null
|
|
423
|
-
const val = (await askQuestion(
|
|
424
|
-
`Context window for ${se.name} in K units (current: ${current ? `${current}K` : "spec default"} — e.g. 128 = 128K; empty to clear):`
|
|
425
|
-
))?.trim() ?? ""
|
|
426
|
-
if (val === "") {
|
|
427
|
-
delete target.context
|
|
428
|
-
} else {
|
|
429
|
-
const n = Number(val)
|
|
430
|
-
if (!Number.isInteger(n) || n <= 0) {
|
|
431
|
-
pushLine(`Invalid context: "${val}" — must be a positive integer in K units (e.g. 128 = 128K)`, C.error)
|
|
432
|
-
return
|
|
433
|
-
}
|
|
434
|
-
target.context = n
|
|
435
|
-
}
|
|
436
|
-
if (se.name === agent.activeProvider) {
|
|
437
|
-
// 运行时 provider 同步(同 setProviderKey 先例:只补 context,不重建对象以免丢 activeModel 覆盖)
|
|
438
|
-
if (target.context === undefined) delete agent.provider.context
|
|
439
|
-
else agent.provider.context = target.context
|
|
440
|
-
if (agent.config?.agent?.compactThresholdAuto) {
|
|
441
|
-
const { resolveCompactThreshold } = await import("../config.mjs")
|
|
442
|
-
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
446
|
-
pushLine(target.context !== undefined
|
|
447
|
-
? `ctx = ${target.context}K (${target.context * 1024} tokens)`
|
|
448
|
-
: `context cleared — using model spec (${fmtContextK(providerSpec(target).context)})`, C.tool)
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
/** Slot-bound model picker: two-level provider → model selection that RETURNS
|
|
453
|
-
* { provider, model } instead of writing main-session state — used by /submodel
|
|
454
|
-
* to write into a subagent slot (global or per-role). Esc from the model list
|
|
455
|
-
* returns to the provider list (openModelPicker parity); Esc from the provider
|
|
456
|
-
* list exits → null. */
|
|
457
|
-
async function pickModelForSlot() {
|
|
458
|
-
for (;;) {
|
|
459
|
-
const providers = agent.providers
|
|
460
|
-
if (!providers.length) return null
|
|
461
|
-
const e = await showPicker("Select provider", providers.map((p) => ({
|
|
462
|
-
type: "item",
|
|
463
|
-
text: `${p.name.padEnd(12)} ${p.model}`,
|
|
464
|
-
action: "open-models",
|
|
465
|
-
provider: p.name,
|
|
466
|
-
})))
|
|
467
|
-
if (!e?.provider) return null
|
|
468
|
-
const providerConfig = providers.find((p) => p.name === e.provider)
|
|
469
|
-
if (!providerConfig) return null
|
|
470
|
-
const entries = buildModelEntriesForProvider(e.provider, providerConfig)
|
|
471
|
-
fetchModelsForProvider(e.provider, entries).catch((err) => {
|
|
472
|
-
pushLine(`[model] fetch models failed: ${err.message}`, C.error)
|
|
473
|
-
})
|
|
474
|
-
const me = await showPicker(`${e.provider} models`, entries)
|
|
475
|
-
if (!me?.model) continue // Esc from model list → back to provider list
|
|
476
|
-
return { provider: e.provider, model: me.model }
|
|
477
|
-
}
|
|
478
|
-
}
|
|
108
|
+
// /model 两级面 + selectModel + 渠道管理 + pickModelForSlot —— 迁 model-picker.mjs
|
|
109
|
+
// (MODEL-MERGE-SESSION——pick 后本文件 ≤450)。createModelPicker 返回同名单函数,
|
|
110
|
+
// 经本层原样转发(index.mjs/wizard/slash-commands 消费面零变化)。
|
|
111
|
+
const modelPicker = createModelPicker({
|
|
112
|
+
agent, state, render, ansi, C, pushLine, persistRaw, askQuestion, maskKey,
|
|
113
|
+
showPicker, closePicker, renderPickerLines,
|
|
114
|
+
})
|
|
479
115
|
|
|
480
|
-
return { showPicker, closePicker, popPicker, renderPickerLines,
|
|
116
|
+
return { showPicker, closePicker, popPicker, renderPickerLines, ...modelPicker }
|
|
481
117
|
}
|
package/src/tui/render-frame.mjs
CHANGED
|
@@ -72,13 +72,6 @@ export function renderPermission(permPreviewLines) {
|
|
|
72
72
|
return [`${ansi.bold}${C.warn}❯ Permission Request${ansi.reset}`, ...permPreviewLines.map((w) => `${C.warn}${w}${ansi.reset}`)]
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
/** Queue preview (1 line when queue has items during processing). */
|
|
76
|
-
export function renderQueue(state, W) {
|
|
77
|
-
if (state.queue.length === 0 || !state.processing) return ""
|
|
78
|
-
const preview = sliceByWidth(state.queue[0].text, W - 20)
|
|
79
|
-
return `${C.dim}❯ Queue: ${state.queue.length} pending${state.queue.length > 1 ? ` (next: ${preview}…)` : ` (next: ${preview})`} — Ctrl+D delete │ Ctrl+I inject${ansi.reset}`
|
|
80
|
-
}
|
|
81
|
-
|
|
82
75
|
/** Picker/wizard overlay panel. Returns empty when no overlay. */
|
|
83
76
|
export function renderPicker(state, cols, panel, overlay) {
|
|
84
77
|
if (!panel || !overlay) return []
|
|
@@ -139,8 +132,8 @@ export function renderInputBox(state, W, boxLines, cols, inputLayout, inputOffse
|
|
|
139
132
|
? " ↑↓ select │ Enter confirm │ Esc: cancel "
|
|
140
133
|
: " Enter submit │ Esc: cancel │ Ctrl+U clear │ Ctrl+V paste ")
|
|
141
134
|
}
|
|
142
|
-
if (title === " Inject Message ") parts.push(" Enter send, Esc cancel ")
|
|
143
|
-
if (title
|
|
135
|
+
if (title === " Inject Message ") parts.push(" Enter send, Esc cancel │ ←→↑↓ Home/End move │ Ctrl+V paste ")
|
|
136
|
+
if (title === " Input " || title === " Processing... ") {
|
|
144
137
|
parts.push(" Shift+Enter / Ctrl+J newline ")
|
|
145
138
|
parts.push(" Ctrl+V paste ")
|
|
146
139
|
parts.push(" Ctrl+I inject ")
|
|
@@ -160,8 +153,8 @@ export function renderInputBox(state, W, boxLines, cols, inputLayout, inputOffse
|
|
|
160
153
|
const curLine = (!hasOverlay && inputLayout) ? inputLayout.cursorLine - (inputOffset ?? 0) : -1
|
|
161
154
|
const curCol = (!hasOverlay && inputLayout) ? inputLayout.cursorCol : -1
|
|
162
155
|
|
|
163
|
-
// Interrupt prompt
|
|
164
|
-
const isInterruptEmpty = state.interruptPrompt &&
|
|
156
|
+
// Interrupt prompt 空文本占位符:灰色提示用户该做什么(第 31 批:chars 数组判空——§8)
|
|
157
|
+
const isInterruptEmpty = state.interruptPrompt && state.interruptPrompt.chars.length === 0
|
|
165
158
|
const interruptPlaceholder = "Type message to inject (Enter send, Esc cancel)"
|
|
166
159
|
|
|
167
160
|
for (let li = 0; li < boxLines.length; li++) {
|
|
@@ -212,6 +205,15 @@ export function renderInputBox(state, W, boxLines, cols, inputLayout, inputOffse
|
|
|
212
205
|
return out
|
|
213
206
|
}
|
|
214
207
|
|
|
208
|
+
/** 第 33 批(TUI §14.3(a)——纯函数派生,无副作用):blocked = 审批/提问卡挂起(实时派生,
|
|
209
|
+
* 提示消解即消失);awaiting = 回合结束等待输入(链尾置位——processing / 挂起两态期间不派生)。
|
|
210
|
+
* @returns {"blocked"|"awaiting"|null} */
|
|
211
|
+
export function attentionKind(state) {
|
|
212
|
+
if (state.permission || state.question) return "blocked"
|
|
213
|
+
if (state.attentionAwaiting && !state.processing && !state.suspended && !state._suspPending) return "awaiting"
|
|
214
|
+
return null
|
|
215
|
+
}
|
|
216
|
+
|
|
215
217
|
/** Status bar (always 1 line). */
|
|
216
218
|
export function renderStatus(state, agent, cols, slashCommands) {
|
|
217
219
|
const statusLine = buildStatusLine(state, agent, { cols, slashCommands })
|
|
@@ -220,8 +222,20 @@ export function renderStatus(state, agent, cols, slashCommands) {
|
|
|
220
222
|
const advisorBanner = agent.config?.advisor?.guard === true ? `${C.advisor} ADVISOR${ansi.reset}${ansi.dim}│` : ""
|
|
221
223
|
const engBanner = agent.config?.agent?.engineering ? `${C.advisor} ENG${ansi.reset}${ansi.dim}│` : ""
|
|
222
224
|
const bannerPrefix = (agent.planMode ? " PLAN│ " : "") + (agent.autoApprove ? " AUTO│ " : "") + (agent.config?.advisor?.guard === true ? " ADVISOR│ " : "") + (agent.config?.agent?.engineering ? " ENG│ " : "")
|
|
223
|
-
|
|
224
|
-
|
|
225
|
+
// 第 33 批(TUI §14.3(b)/(c)):attention 态 = chip 行首 + 整行注意力色对包裹(blocked >
|
|
226
|
+
// awaiting;blocked 内 permission > question——与按键分发优先级同序);平态零注入。
|
|
227
|
+
const kind = attentionKind(state)
|
|
228
|
+
const chip = kind === "blocked" ? (state.permission ? "⚠ 等待你的审批" : "⚠ 等待你的回答")
|
|
229
|
+
: kind === "awaiting" ? "⚠ 等待你的输入" : ""
|
|
230
|
+
const attentionPad = chip ? `${chip} │ ` : ""
|
|
231
|
+
// 宽度预算(N9④):chip 占位计入——整行仍 ≤ cols − 1(既有口径)。
|
|
232
|
+
const statusMax = cols - 1 - (bannerPrefix ? stringWidth(bannerPrefix) : 0) - (attentionPad ? stringWidth(attentionPad) : 0)
|
|
233
|
+
const inner = `${ansi.dim}${planBanner}${autoBanner}${advisorBanner}${engBanner}${sliceByWidth(statusLine, Math.max(10, statusMax))}`
|
|
234
|
+
// 负向锁(§14.3):attention 为 null ⇒ 半态路径逐字节等价(不经包裹 / 零字节注入)。
|
|
235
|
+
if (!chip) return `${inner}${ansi.reset}`
|
|
236
|
+
// 底色存活(§14.3):既有内容含内部 ansi.reset(banner / ctx 警示段)——每次内部复位后
|
|
237
|
+
// 重施加注意力色对,再整体包裹(内容零省略)。
|
|
238
|
+
return `${C.attention}${(chip + " │ " + inner).replaceAll(ansi.reset, ansi.reset + C.attention)}${ansi.reset}`
|
|
225
239
|
}
|
|
226
240
|
|
|
227
241
|
// ====================================================================
|
|
@@ -266,7 +280,7 @@ export function renderRows(state, agent, opts) {
|
|
|
266
280
|
if (panels.todo) put(panels.todo.y, renderTodo(visibleTasks, cols))
|
|
267
281
|
if (panels.picker) put(panels.picker.y, renderPicker(state, cols, panels.picker, overlay))
|
|
268
282
|
if (panels.permission) put(panels.permission.y, renderPermission(permPreviewLines))
|
|
269
|
-
|
|
283
|
+
// 排队面板提示已撤(INPUT-LOCK-ASYNC F-7——2026-09-09——busy 提交吞——无排队展示)
|
|
270
284
|
// question 自由文本态:box 内容是 layoutAnswer 行——光标布局/滚动随 box 内容走
|
|
271
285
|
// (questionLayout/questionOffset);主输入 inputLayout 此时无意义(被 question 行替换)。
|
|
272
286
|
const qFree = Boolean(state.question && state.question.options.length === 0)
|
|
@@ -376,6 +390,8 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
376
390
|
const ctxTokensHint = state.ctxCache.tokens > 0 ? ` ${fmtK(state.ctxCache.tokens)}` : ""
|
|
377
391
|
const ctxHint = ctxPct > 0
|
|
378
392
|
? ctxPct >= 80 ? ` │ ${ansi.reset}${C.warn}context ${ctxPct}%${ctxTokensHint}${ansi.reset}${ansi.dim}` : ` │ context ${ctxPct}%${ctxTokensHint}` : ""
|
|
379
|
-
|
|
380
|
-
|
|
393
|
+
// INPUT-LOCK-ASYNC(C'——F-3):busy(processing 含 digest)状态栏提示——取代旧
|
|
394
|
+
// 排队提示(F-7 已删)——“主会话处理中——Enter 提交禁用”
|
|
395
|
+
const enterHint = state.processing ? `主会话处理中 — Enter 提交禁用(字符可输入,回合结束请重按 Enter)` : "Enter: send"
|
|
396
|
+
return ` ${statusText}${taskHint}${turnHint}${tokenHint}${ctxHint}${scrollHint} │ ${enterHint} │ /: commands │ wheel/PgUp/PgDn: scroll │ Ctrl+I: inject │ Ctrl+C: exit (×2)`
|
|
381
397
|
}
|
package/src/tui/render-loop.mjs
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { countConvLines, renderRows } from "./render-frame.mjs"
|
|
17
17
|
import { estimateTokens } from "../context.mjs"
|
|
18
18
|
import { ansi, C } from "./ansi.mjs"
|
|
19
|
+
import { isTuiActive } from "./tui-lifecycle.mjs" // F-2 渲染抑制守卫(RESIZE-MOUSE-LEAK-FIX)
|
|
19
20
|
|
|
20
21
|
const MIN_RENDER_INTERVAL_MS = 16
|
|
21
22
|
|
|
@@ -55,6 +56,7 @@ export function createRenderLoop(state, agent, ctx, pushLine, write = (s) => pro
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
function doRender() {
|
|
59
|
+
if (!isTuiActive()) return // F-2 渲染抑制(RESIZE-MOUSE-LEAK-FIX):cleanup 已清活动态——退出期不再把 TUI 帧重绘到已恢复的主屏
|
|
58
60
|
try {
|
|
59
61
|
// Single source (Windows ConPTY instability, 2026-08-30). CACHE ONLY —
|
|
60
62
|
// no per-frame refresh: during heavy streaming output ConPTY reports a
|
|
@@ -10,7 +10,6 @@ import { formatTables, sanitizeDisplay, sliceByWidth, wrapText } from "./render.
|
|
|
10
10
|
import {
|
|
11
11
|
foldTailLines, isExpanded, renderBlockTimeline, renderExpandedBlock, renderFoldedHead, renderMathAndMarkdown,
|
|
12
12
|
} from "./fold-block.mjs"
|
|
13
|
-
import { styleSubLabelRow } from "./subagent-panel.mjs"
|
|
14
13
|
import { SUBAGENT_ROLES } from "./subagent-blocks.mjs"
|
|
15
14
|
|
|
16
15
|
const _toolSegCache = new WeakMap()
|
|
@@ -70,7 +69,9 @@ export function toolSeg(state, l, i, cols, maxRows) {
|
|
|
70
69
|
|
|
71
70
|
/** 冻结子agent 活动块渲染(§7.2 D4):state.lines 载体 {_frozenSubTask: sub}(subagent-blocks.mjs
|
|
72
71
|
* freezeSubTaskLines 推入)。折叠 = 身份头 + tail 3;展开 = 共享组件(60% 屏幕封顶 + 底部可达
|
|
73
|
-
* 折叠控制)。折叠键 `sub-${key}`——与运行中面板区块同键,折叠态跨冻结边界无缝延续。
|
|
72
|
+
* 折叠控制)。折叠键 `sub-${key}`——与运行中面板区块同键,折叠态跨冻结边界无缝延续。
|
|
73
|
+
* SUBAGENT-TAIL:内层内容已并入 sub.blocks(数据层——取代小节;docs/design/TUI.md §6)
|
|
74
|
+
* ——本段只渲染单流(无子块段/无子块头)。 */
|
|
74
75
|
function frozenSubTaskLines(state, sub, cols, maxRows) {
|
|
75
76
|
const foldKey = `sub-${sub.key}`
|
|
76
77
|
const elapsed = Math.floor(((sub.doneAt ?? Date.now()) - sub.started) / 1000)
|
|
@@ -92,7 +93,7 @@ function frozenSubTaskLines(state, sub, cols, maxRows) {
|
|
|
92
93
|
if (isExpanded(state, foldKey)) {
|
|
93
94
|
// Expanded: shared component renders blank + ▼ control + full timeline,
|
|
94
95
|
// capped at 60% of the screen with a bottom collapse control.
|
|
95
|
-
const body = renderBlockTimeline(sub.blocks, cols)
|
|
96
|
+
const body = renderBlockTimeline(sub.blocks, cols)
|
|
96
97
|
out.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: "subagent activity" }))
|
|
97
98
|
} else {
|
|
98
99
|
// Folded: the header line itself is the control (▶ affordance), then tail 3.
|
|
@@ -109,18 +110,20 @@ function frozenSubTaskLines(state, sub, cols, maxRows) {
|
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
/** 冻结子agent 段入口(2026-08-31 段缓存:冻结后内容不变——签名含 sub.key + blocks 计数;
|
|
112
|
-
* 注:foldKey 由位置键升级为 sub.key(loadOlder unshift 后不重绑——sub 自身携带 key)。
|
|
113
|
+
* 注:foldKey 由位置键升级为 sub.key(loadOlder unshift 后不重绑——sub 自身携带 key)。
|
|
114
|
+
* SUBAGENT-TAIL:子块树签名退役(子块无内容行、无独立折叠键——渲染零读 children)。 */
|
|
113
115
|
export function frozenSubSeg(state, l, i, cols, maxRows) {
|
|
114
116
|
const fKey = `sub-${l._frozenSubTask.key}`
|
|
117
|
+
const sub = l._frozenSubTask
|
|
115
118
|
const fSig = [
|
|
116
|
-
cols, maxRows ?? 0,
|
|
117
|
-
|
|
119
|
+
cols, maxRows ?? 0, sub.key, sub.blocks?.length ?? 0,
|
|
120
|
+
sub.done ? 1 : 0, state.foldEnabled === false ? 0 : 1,
|
|
118
121
|
state.expandedBlocks?.has(fKey) ? 1 : 0, state._foldScroll?.get(fKey) ?? 0,
|
|
119
122
|
].join("|")
|
|
120
123
|
const hit = _frozenSubCache.get(l)
|
|
121
|
-
if (hit && hit.textRef ===
|
|
122
|
-
const rows = frozenSubTaskLines(state,
|
|
123
|
-
_frozenSubCache.set(l, { textRef:
|
|
124
|
+
if (hit && hit.textRef === sub && hit.sig === fSig) return hit.rows
|
|
125
|
+
const rows = frozenSubTaskLines(state, sub, cols, maxRows)
|
|
126
|
+
_frozenSubCache.set(l, { textRef: sub, sig: fSig, rows })
|
|
124
127
|
return rows
|
|
125
128
|
}
|
|
126
129
|
|