thincoder 0.12.61 → 0.12.63
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 +34 -1
- package/README.md +13 -12
- package/bin/thincoder.mjs +63 -28
- package/package.json +6 -5
- package/src/acp/bridge.mjs +35 -15
- package/src/acp/client-caps.mjs +86 -0
- package/src/acp/ext.mjs +86 -0
- package/src/acp/handlers-session.mjs +240 -0
- package/src/acp/handlers-slots.mjs +196 -0
- package/src/acp/login.mjs +48 -0
- package/src/acp/session.mjs +6 -4
- package/src/acp.mjs +67 -371
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +59 -17
- package/src/cli/memory-command.mjs +3 -3
- package/src/cli/permission.mjs +4 -48
- package/src/cli/setup-wizard.mjs +1 -1
- package/src/completions.mjs +3 -1
- package/src/crash-reports.mjs +32 -10
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +88 -0
- package/src/prompt-injections.mjs +20 -0
- package/src/tui/agent-turn.mjs +40 -9
- package/src/tui/cmd-advisor.mjs +5 -5
- package/src/tui/cmd-clear.mjs +2 -0
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +25 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +10 -5
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +31 -4
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +22 -9
- package/src/tui/config-helpers.mjs +1 -1
- package/src/tui/display-budget.mjs +206 -0
- package/src/tui/index.mjs +40 -12
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-handler-search.mjs +9 -1
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +85 -0
- package/src/tui/model-catalog.mjs +4 -4
- package/src/tui/model-picker.mjs +8 -7
- package/src/tui/mouse.mjs +11 -6
- package/src/tui/pickers.mjs +15 -2
- package/src/tui/render-conversation.mjs +1 -1
- package/src/tui/render-frame.mjs +17 -6
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render-segments.mjs +3 -1
- package/src/tui/slash-commands.mjs +1 -1
- package/src/tui/startup.mjs +49 -17
- package/src/tui/subagent-blocks.mjs +21 -3
- package/src/tui/subagent-children.mjs +86 -14
- package/src/tui/subagent-freeze.mjs +80 -3
- package/src/tui/suspension-drive.mjs +48 -22
- package/src/tui/tool-args.mjs +5 -2
- package/src/tui/tool-display.mjs +16 -2
- package/src/tui/tool-events.mjs +64 -22
- package/src/tui/tui-lifecycle.mjs +9 -2
- package/src/tui/wizard.mjs +3 -3
- package/src/tui/wrapped-spawn.mjs +21 -5
- package/src/abort-provenance.mjs +0 -116
- package/src/advisor/citations.mjs +0 -139
- package/src/advisor/compaction.mjs +0 -174
- package/src/advisor/convergence.mjs +0 -80
- package/src/advisor/history.mjs +0 -77
- package/src/advisor/loop.mjs +0 -293
- package/src/advisor/messages.mjs +0 -299
- package/src/advisor/project-context.mjs +0 -194
- package/src/advisor/repos.mjs +0 -150
- package/src/advisor/run.mjs +0 -293
- package/src/advisor/truncate.mjs +0 -57
- package/src/advisor.mjs +0 -290
- package/src/agent/completion.mjs +0 -146
- package/src/agent/dispatch.mjs +0 -489
- package/src/agent/helpers.mjs +0 -384
- package/src/agent/post-turn.mjs +0 -70
- package/src/agent/record-results.mjs +0 -174
- package/src/agent/relay-prefix.mjs +0 -39
- package/src/agent/run-stages.mjs +0 -242
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -228
- package/src/agent-tools/advisor-async.mjs +0 -346
- package/src/agent-tools/advisor-settle.mjs +0 -231
- package/src/agent-tools/advisor.mjs +0 -260
- package/src/agent-tools/async-settle.mjs +0 -191
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -468
- package/src/agent-tools/design-token.mjs +0 -117
- package/src/agent-tools/digest-budget.mjs +0 -76
- package/src/agent-tools/eng.mjs +0 -67
- package/src/agent-tools/escalate-async.mjs +0 -289
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -294
- package/src/agent-tools/recent-changes.mjs +0 -24
- package/src/agent-tools/review-streak.mjs +0 -93
- package/src/agent-tools/settings.mjs +0 -265
- package/src/agent-tools/skill.mjs +0 -47
- package/src/agent-tools/subagent-actions.mjs +0 -479
- package/src/agent-tools/subagent-async.mjs +0 -434
- package/src/agent-tools/subagent-panel.mjs +0 -160
- package/src/agent-tools/subagent-run.mjs +0 -205
- package/src/agent-tools/subagent-scheduler.mjs +0 -392
- package/src/agent-tools/subagent-spawn.mjs +0 -453
- package/src/agent-tools/subagent.mjs +0 -404
- package/src/agent-tools/task.mjs +0 -87
- package/src/agent-tools/timer.mjs +0 -46
- package/src/agent-tools/verify.mjs +0 -271
- package/src/agent-tools.mjs +0 -17
- package/src/agent.mjs +0 -413
- package/src/auto-think.mjs +0 -115
- package/src/config-migrate.mjs +0 -70
- package/src/config.mjs +0 -496
- package/src/context.mjs +0 -381
- package/src/conventions.mjs +0 -223
- package/src/embedding.mjs +0 -120
- package/src/escape.mjs +0 -152
- package/src/expand-home.mjs +0 -16
- package/src/explore-distill.mjs +0 -155
- package/src/generate-title.mjs +0 -83
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- package/src/log.mjs +0 -195
- package/src/markdown.mjs +0 -106
- package/src/mcp/helpers.mjs +0 -51
- package/src/mcp/transport-http.mjs +0 -248
- package/src/mcp/transport-stdio.mjs +0 -140
- package/src/mcp/transport-ws.mjs +0 -122
- package/src/mcp.mjs +0 -295
- package/src/memory/code-index.mjs +0 -219
- package/src/memory/code-sync.mjs +0 -413
- package/src/memory/core.mjs +0 -300
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -417
- package/src/memory/file-walk.mjs +0 -109
- package/src/memory/schema.mjs +0 -452
- package/src/memory.mjs +0 -21
- package/src/model-ref.mjs +0 -66
- package/src/model-specs.mjs +0 -179
- package/src/peer-domains.mjs +0 -265
- package/src/peer-instances.mjs +0 -231
- package/src/prompt-overlays.mjs +0 -82
- package/src/prompts/advisor-design.md +0 -41
- package/src/prompts/advisor-round1.md +0 -41
- package/src/prompts/advisor-round2.md +0 -46
- package/src/prompts/advisor-round3.md +0 -42
- package/src/prompts/common.md +0 -115
- package/src/prompts/consult-base.md +0 -19
- package/src/prompts/discipline-engineering.md +0 -217
- package/src/prompts/discipline-normal.md +0 -179
- package/src/prompts/persona-coder.md +0 -21
- package/src/prompts/persona-eng-coder.md +0 -37
- package/src/prompts/persona-eng-designer.md +0 -55
- package/src/prompts/persona-engineering.md +0 -54
- package/src/prompts/persona-explore.md +0 -15
- package/src/prompts/persona-normal.md +0 -27
- package/src/prompts/persona-plan.md +0 -26
- package/src/provider/anthropic.mjs +0 -225
- package/src/provider/core.mjs +0 -476
- package/src/provider/errors.mjs +0 -101
- package/src/provider/google.mjs +0 -257
- package/src/provider/index.mjs +0 -7
- package/src/provider/list-models.mjs +0 -93
- package/src/provider/normalize.mjs +0 -81
- package/src/provider/rate.mjs +0 -108
- package/src/provider/responses.mjs +0 -495
- package/src/provider/retry.mjs +0 -88
- package/src/provider/sse.mjs +0 -264
- package/src/proxy.mjs +0 -261
- package/src/rules.mjs +0 -53
- package/src/session-gc.mjs +0 -214
- package/src/session-guard.mjs +0 -47
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-slots.mjs +0 -489
- package/src/session.mjs +0 -475
- package/src/skills.mjs +0 -153
- package/src/token-ttl.mjs +0 -274
- package/src/tools/apply_patch.md +0 -15
- package/src/tools/bash.md +0 -37
- package/src/tools/bash.mjs +0 -268
- package/src/tools/checklist-sync.mjs +0 -181
- package/src/tools/checklist.md +0 -13
- package/src/tools/checklist.mjs +0 -299
- package/src/tools/delete.md +0 -13
- package/src/tools/edit-batch.mjs +0 -191
- package/src/tools/edit-diff.mjs +0 -348
- package/src/tools/edit.md +0 -30
- package/src/tools/execute.md +0 -21
- package/src/tools/execute.mjs +0 -228
- package/src/tools/fetch.md +0 -12
- package/src/tools/file.mjs +0 -469
- package/src/tools/file_ops.md +0 -17
- package/src/tools/get_current_time.md +0 -8
- package/src/tools/git-checkpoint.mjs +0 -143
- package/src/tools/git-ext.mjs +0 -173
- package/src/tools/git.md +0 -54
- package/src/tools/git.mjs +0 -356
- package/src/tools/glob-dialect.mjs +0 -130
- package/src/tools/glob.md +0 -11
- package/src/tools/grep.md +0 -19
- package/src/tools/hashline_edit.md +0 -14
- package/src/tools/index.mjs +0 -36
- package/src/tools/insert_after.md +0 -15
- package/src/tools/lint.md +0 -10
- package/src/tools/linter.mjs +0 -128
- package/src/tools/ls.md +0 -12
- package/src/tools/lsp.md +0 -10
- package/src/tools/lsp.mjs +0 -316
- package/src/tools/ops.mjs +0 -299
- package/src/tools/patch.mjs +0 -282
- package/src/tools/process.md +0 -10
- package/src/tools/question.md +0 -16
- package/src/tools/question.mjs +0 -26
- package/src/tools/read.md +0 -20
- package/src/tools/read_image.md +0 -8
- package/src/tools/repomap.mjs +0 -314
- package/src/tools/search.mjs +0 -236
- package/src/tools/shared.mjs +0 -446
- package/src/tools/tree.md +0 -14
- package/src/tools/tree.mjs +0 -66
- package/src/tools/wait_for.md +0 -22
- package/src/tools/web.mjs +0 -224
- package/src/tools/websearch.md +0 -16
- package/src/tools/write.md +0 -11
- package/src/traces/trace-store.mjs +0 -224
package/src/tui/index.mjs
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
import { emitKeypressEvents } from "node:readline"
|
|
22
22
|
import { PassThrough } from "node:stream"
|
|
23
|
-
import { saveSession } from "
|
|
24
|
-
import { closeAllMcp } from "
|
|
23
|
+
import { saveSession } from "@thincoder/core/session.mjs"
|
|
24
|
+
import { closeAllMcp } from "@thincoder/core/mcp.mjs"
|
|
25
25
|
import { ansi, C } from "./ansi.mjs"
|
|
26
26
|
import { createRenderLoop } from "./render-loop.mjs"
|
|
27
27
|
import { makeDimsState } from "./dims.mjs"
|
|
@@ -37,17 +37,19 @@ import { runAgentTurn } from "./agent-turn.mjs"
|
|
|
37
37
|
import { createKeyHandler, convMaxScroll, clearAttention } from "./key-handler.mjs"
|
|
38
38
|
import { showStartup, backgroundIndex, createLoadOlder } from "./startup.mjs"
|
|
39
39
|
import { shiftFreezeAnchors } from "./subagent-blocks.mjs"
|
|
40
|
+
import { capLine, accountLine, accountAll, syncLineBudget } from "./display-budget.mjs"
|
|
40
41
|
import { createConfigHelpers } from "./config-helpers.mjs"
|
|
41
42
|
import { createUpdateNotice, pendingNoticeReady } from "./update-notice.mjs"
|
|
43
|
+
import { startLedgerSurface } from "./ledger-surface.mjs"
|
|
42
44
|
|
|
43
45
|
export { upgradeFailureText, pendingNoticeReady } from "./update-notice.mjs"
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
|
-
* SESSION.md §8 D-S2 — TUI 启动首帧前的 provider 重选流程:
|
|
48
|
+
* SESSION.md §6.8 D-S2 — TUI 启动首帧前的 provider 重选流程:
|
|
47
49
|
* provider 无效(`_providerInvalid` 标记或 provider 为 null)→ 先弹模型选择 picker
|
|
48
50
|
* (复用 openModelPicker,展示当前可用 providers);用户选定后继续正常启动。
|
|
49
|
-
* 选择取消(Esc)→ 仍进入 TUI,推送提示行("未配置有效 provider,可用 /model
|
|
50
|
-
*
|
|
51
|
+
* 选择取消(Esc)→ 仍进入 TUI,推送提示行("未配置有效 provider,可用 /model 配置
|
|
52
|
+
* 渠道与模型")——绝不因无 provider 拒绝进入。返回 true 表示弹过选择流程。
|
|
51
53
|
*/
|
|
52
54
|
export async function promptProviderIfInvalid(agent, openModelPicker, pushLine) {
|
|
53
55
|
if (!(agent._providerInvalid || !agent.provider)) return false
|
|
@@ -58,7 +60,7 @@ export async function promptProviderIfInvalid(agent, openModelPicker, pushLine)
|
|
|
58
60
|
const hasProviders = (agent.providers?.length ?? 0) > 0
|
|
59
61
|
pushLine(hasProviders && !agent.config?.defaultModel
|
|
60
62
|
? "尚未设置默认模型(config.defaultModel——新会话起点):/config → 默认模型 设置一次;/model 仅改本会话"
|
|
61
|
-
: "未配置有效 provider,可用 /model
|
|
63
|
+
: "未配置有效 provider,可用 /model 配置渠道与模型", C.warn)
|
|
62
64
|
}
|
|
63
65
|
return true
|
|
64
66
|
}
|
|
@@ -115,6 +117,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
115
117
|
currentTool: null, // currently executing tool name (shown in status bar)
|
|
116
118
|
processingStarted: 0, // current turn start time (status bar timer)
|
|
117
119
|
status: "Ready",
|
|
120
|
+
ledger: { marker: null, warn: false, scannedAt: 0 }, // LEDGER-SURFACE(§2.30.3.4):L1 标记位——ledger-surface 写 / render-frame 读
|
|
118
121
|
queue: [], // 交接残项单容器 [{ text }](INPUT-LOCK:submit 不再排队——仅释放窗口兜底/挂起中止残余——回合尾队列循环续发,至多一条)
|
|
119
122
|
interruptPrompt: null, // Ctrl+I inject box(第 31 批——TUI-INPUT-BOX.md §8): { chars: string[], cursor: number } or null
|
|
120
123
|
attentionAwaiting: false, // 第 33 批(TUI §14.3(f)):回合结束等待输入——链尾置位 / 用户输入清位;不落盘、不进会话
|
|
@@ -127,6 +130,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
127
130
|
_historyLoaded: 0, // messages loaded from the TAIL of _fullHistory
|
|
128
131
|
_historyTotal: 0, // total messages in the restored session
|
|
129
132
|
_hasOlder: false, // more earlier messages remain unloaded
|
|
133
|
+
_linesChars: 0, // TUI-OOM-ROOTCAUSE(§15.3.2):state.lines 全部行与载体文本总量(字符账——唯一新增状态位)
|
|
130
134
|
_agent: null, // TUI state → agent 回指挂载点:startTUI 装配时置 agent 引用——
|
|
131
135
|
// ① SYNC-CANCEL ⏹ 门控读 state._agent._syncChildAborts(subagent-panel.mjs,2026-09-09);
|
|
132
136
|
// ② CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):agent._tuiState = state 反向挂载——
|
|
@@ -276,7 +280,11 @@ export async function startTUI(agent, opts = {}) {
|
|
|
276
280
|
process.on("exit", cleanup)
|
|
277
281
|
|
|
278
282
|
const pushLine = (text, color, kind) => {
|
|
279
|
-
|
|
283
|
+
// TUI-OOM-ROOTCAUSE(TUI.md §15.3.1/§15.3.3):行额度单点——行文本过 capLine
|
|
284
|
+
// (LINE_MAX_CHARS——单行巨内容/无换行巨 chunk 的堵口)+ 总量账 + 预算对账。
|
|
285
|
+
const line = { text: capLine(text), color, _kind: kind }
|
|
286
|
+
state.lines.push(line)
|
|
287
|
+
accountLine(state, line)
|
|
280
288
|
if (state.lines.length > 5000) {
|
|
281
289
|
state.lines.splice(0, 1000)
|
|
282
290
|
state.lines.unshift({ text: `... [earlier messages trimmed — ${state.lines.length} lines remaining]`, color: C.dim })
|
|
@@ -284,14 +292,24 @@ export async function startTUI(agent, opts = {}) {
|
|
|
284
292
|
// 流位置)——不校正则池空补发冻结(freezeSubTaskLines splice)落点漂移;校正量
|
|
285
293
|
// = 净位移(裁 1000 补 1 标记行 → −999,code review round1 #3)。
|
|
286
294
|
shiftFreezeAnchors(state, 1000)
|
|
295
|
+
accountAll(state) // 行对象集合已变(splice/unshift 直写)——直算重对账
|
|
287
296
|
}
|
|
297
|
+
syncLineBudget(state, { onTrim: (st, n) => shiftFreezeAnchors(st, n) })
|
|
288
298
|
render()
|
|
289
299
|
}
|
|
290
300
|
|
|
291
301
|
/** Message block label: blank line + label line. Breathing space between user/assistant messages */
|
|
292
302
|
const pushLabel = (text, color) => {
|
|
293
|
-
|
|
294
|
-
state.lines.
|
|
303
|
+
// 行额度同 pushLine(§15.3.1——恢复行/标签行同口径)
|
|
304
|
+
if (state.lines.length > 0) {
|
|
305
|
+
const blank = { text: "", color: C.dim }
|
|
306
|
+
state.lines.push(blank)
|
|
307
|
+
accountLine(state, blank)
|
|
308
|
+
}
|
|
309
|
+
const line = { text: capLine(text), color }
|
|
310
|
+
state.lines.push(line)
|
|
311
|
+
accountLine(state, line)
|
|
312
|
+
syncLineBudget(state, { onTrim: (st, n) => shiftFreezeAnchors(st, n) })
|
|
295
313
|
render()
|
|
296
314
|
}
|
|
297
315
|
|
|
@@ -316,6 +334,13 @@ export async function startTUI(agent, opts = {}) {
|
|
|
316
334
|
// 声明在此(render 已可用)——data 回调(滚轮分支)与 createKeyHandler ctx(PgUp)共用。
|
|
317
335
|
const loadOlder = createLoadOlder({ agent, state, render })
|
|
318
336
|
|
|
337
|
+
// TUI-OOM-ROOTCAUSE(CRASH-REPORTS.md §8.3 订阅接线):堆预警行入对话流(pushLine 已 render)——
|
|
338
|
+
// 不新增 TUI 定时器(采样定时器住 heap-watch 模块,bin 入口武装);订阅失败不影响启动。
|
|
339
|
+
try {
|
|
340
|
+
const { onHeapWarn } = await import("../heap-watch.mjs")
|
|
341
|
+
onHeapWarn((line) => pushLine(line, C.warn))
|
|
342
|
+
} catch { /* 尽力面 */ }
|
|
343
|
+
|
|
319
344
|
// Resize events are genuine dimension changes on every terminal (2026-08-31
|
|
320
345
|
// simplification: the earlier settle-timer/double-confirm machinery was built
|
|
321
346
|
// on the misdiagnosed ConPTY-stale hypothesis and even stalled drag-shrink).
|
|
@@ -381,7 +406,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
381
406
|
const { persistRaw, syncProviderField, maskKey } = createConfigHelpers(agent)
|
|
382
407
|
|
|
383
408
|
// Model picker + generic picker: implemented in pickers.mjs
|
|
384
|
-
const { closePicker, showPicker, popPicker, renderPickerLines, openModelPicker, selectModel, setProviderKey, pickModelForSlot } = createPickers({
|
|
409
|
+
const { closePicker, showPicker, popPicker, renderPickerLines, openModelPicker, selectModel, setProviderKey, pickModelForSlot, confirmDelete } = createPickers({
|
|
385
410
|
agent, state, render, ansi, C, pushLine, pushLabel, persistRaw, askQuestion, maskKey,
|
|
386
411
|
})
|
|
387
412
|
|
|
@@ -398,7 +423,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
398
423
|
const { handleSlash, completions, handleTab } = createSlashCommands({
|
|
399
424
|
agent, state, distillOpts,
|
|
400
425
|
pushLine, pushLabel, render,
|
|
401
|
-
showPicker, closePicker, askQuestion, askPermission,
|
|
426
|
+
showPicker, closePicker, askQuestion, askPermission, confirmDelete,
|
|
402
427
|
persistRaw, syncProviderField, maskKey,
|
|
403
428
|
openModelPicker: () => openModelPicker(),
|
|
404
429
|
selectModel,
|
|
@@ -433,11 +458,14 @@ export async function startTUI(agent, opts = {}) {
|
|
|
433
458
|
|
|
434
459
|
// ---------------------------------------------------------- Startup screen + background indexing
|
|
435
460
|
|
|
436
|
-
// SESSION.md §8 D-S2:startTUI 首帧前 —— provider 无效(_providerInvalid / provider 为 null)
|
|
461
|
+
// SESSION.md §6.8 D-S2:startTUI 首帧前 —— provider 无效(_providerInvalid / provider 为 null)
|
|
437
462
|
// → 先弹模型选择 picker(keyStream 已挂 keypress,Esc/Enter 可用);Esc → 提示行,仍进 TUI
|
|
438
463
|
await promptProviderIfInvalid(agent, () => openModelPicker(), pushLine)
|
|
439
464
|
|
|
440
465
|
showStartup({ agent, state, opts, pushLine, pushLabel, render, startWizard })
|
|
466
|
+
// LEDGER-SURFACE(§2.30.3.4):台账可见面——首扫(setImmediate)+ 周期;dispose 挂进程退出(:278 cleanup 先例)
|
|
467
|
+
const ledgerSurface = startLedgerSurface({ state, agent, pushLine, render })
|
|
468
|
+
process.on("exit", () => ledgerSurface.dispose())
|
|
441
469
|
backgroundIndex({ agent, state, render })
|
|
442
470
|
|
|
443
471
|
// Check for updates (non-blocking, after startup screen)——实现 update-notice.mjs
|
package/src/tui/interaction.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ansi, C } from "./ansi.mjs"
|
|
2
|
-
import { detectDanger } from "
|
|
2
|
+
import { detectDanger } from "@thincoder/core/tools/shared.mjs"
|
|
3
|
+
import { askPermission as coreAskPermission } from "@thincoder/core/permission.mjs"
|
|
3
4
|
|
|
4
5
|
/** Interaction primitives: permission approval + question input.
|
|
5
6
|
* Extracted from index.mjs, receives closure dependencies via createInteraction(ctx).
|
|
@@ -9,6 +10,11 @@ import { detectDanger } from "../tools/shared.mjs"
|
|
|
9
10
|
* answer mode (the user supplements/corrects the AI's preset choices). */
|
|
10
11
|
export const QUESTION_CUSTOM = "\u0001custom-answer"
|
|
11
12
|
|
|
13
|
+
/** y/n-only modal family(判据单源——key-modes / render-frame 同引):`continue`(回合帽)
|
|
14
|
+
* 与 `retry`(provider 失败面)同形 ⇒ 仅 `y` / `n`。`a` 不属该族键面(会翻会话级 AUTO)。 */
|
|
15
|
+
const YES_NO_MODALS = new Set(["continue", "retry"])
|
|
16
|
+
export function isYesNoModal(name) { return YES_NO_MODALS.has(name) }
|
|
17
|
+
|
|
12
18
|
export function createInteraction(ctx) {
|
|
13
19
|
const { agent, state, pushLine, pushLabel, render, summarize } = ctx
|
|
14
20
|
|
|
@@ -62,12 +68,15 @@ export function createInteraction(ctx) {
|
|
|
62
68
|
pushLine(` [auto] ${name}${argSummary ? ` ${argSummary}` : ""}`, C.warn)
|
|
63
69
|
return Promise.resolve(true)
|
|
64
70
|
}
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
// 闸语义 / 请示流程 = 核单源(P2 批 §2.20);本端只供展示面——卡片预览经 `io.ask` 缝
|
|
72
|
+
// 注入(permissionPreview 渲染在输入框上方,与 "Allow?" 提示同帧)。
|
|
73
|
+
return coreAskPermission(name, args, {
|
|
74
|
+
ask: ({ name: n, toolArgs }) => new Promise((resolve) => {
|
|
75
|
+
state.permissionPreview = formatPermission(n, toolArgs)
|
|
76
|
+
state.permission = { name: n, args: toolArgs, resolve }
|
|
77
|
+
state.status = `Waiting: ${n}`
|
|
78
|
+
render()
|
|
79
|
+
}),
|
|
71
80
|
})
|
|
72
81
|
}
|
|
73
82
|
|
|
@@ -1,20 +1,28 @@
|
|
|
1
|
-
/** Perform search on state.lines, update state.search.matches and clamp index
|
|
1
|
+
/** Perform search on state.lines, update state.search.matches and clamp index
|
|
2
|
+
* TUI-OOM-ROOTCAUSE(TUI.md §15.3.1 SEARCH_MATCH_CAP):匹配计数上限——超限截断 +
|
|
3
|
+
* `state.search.capped` 提示标志(渲染面提示行;次要无界面)。
|
|
4
|
+
*/
|
|
5
|
+
import { SEARCH_MATCH_CAP } from "./display-budget.mjs"
|
|
6
|
+
|
|
2
7
|
export function performSearch(state) {
|
|
3
8
|
if (!state.search) return
|
|
4
9
|
const query = state.search.query.toLowerCase()
|
|
5
10
|
state.search.matches = []
|
|
11
|
+
state.search.capped = false
|
|
6
12
|
// Clear old highlights
|
|
7
13
|
state.lines.forEach(l => delete l._searchMatches)
|
|
8
14
|
|
|
9
15
|
if (!query) { state.search.index = 0; return }
|
|
10
16
|
|
|
11
17
|
state.lines.forEach((line, lineIndex) => {
|
|
18
|
+
if (state.search.matches.length >= SEARCH_MATCH_CAP) { state.search.capped = true; return }
|
|
12
19
|
const text = (line.text || "").toLowerCase()
|
|
13
20
|
let charIndex = text.indexOf(query)
|
|
14
21
|
if (charIndex !== -1) {
|
|
15
22
|
line._searchMatches = []
|
|
16
23
|
}
|
|
17
24
|
while (charIndex !== -1) {
|
|
25
|
+
if (state.search.matches.length >= SEARCH_MATCH_CAP) { state.search.capped = true; break }
|
|
18
26
|
state.search.matches.push({ lineIndex, charIndex })
|
|
19
27
|
line._searchMatches.push(charIndex)
|
|
20
28
|
charIndex = text.indexOf(query, charIndex + 1)
|
package/src/tui/key-modes.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { C } from "./ansi.mjs"
|
|
9
9
|
import { readClipboardText, insertPastedText } from "./clipboard.mjs"
|
|
10
|
-
import { QUESTION_CUSTOM } from "./interaction.mjs"
|
|
10
|
+
import { QUESTION_CUSTOM, isYesNoModal } from "./interaction.mjs"
|
|
11
11
|
import { inputContentWidth } from "./layout.mjs"
|
|
12
12
|
import { moveCursorVertical } from "./render.mjs"
|
|
13
13
|
|
|
@@ -35,14 +35,19 @@ export function denyModalForOwner(state, key, { pushLine, render } = {}) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** permission 确认模态:y/n/a(a = approve + AUTO ON);batch(§16 D-B1):a/o/n(Esc = deny)。
|
|
38
|
+
* `continue` / `retry` = y/n-only 族(判据单源 `isYesNoModal`——X8 跟进①:`retry` 框不
|
|
39
|
+
* 广告 `a`,零会话级 AUTO 副作用)。
|
|
38
40
|
* consume:valid 键或 Esc 走 resolve 分支;其余键静默吞掉(模态独占)。 */
|
|
39
41
|
export function handlePermissionMode(str, key, ctx) {
|
|
40
42
|
const { state, agent, pushLine, render } = ctx
|
|
41
43
|
if (!state.permission) return false
|
|
42
44
|
const answer = (str || "").toLowerCase()
|
|
45
|
+
// `yOnly` = 本框键面仅 y/n(continue / retry);`isContinue` 仅保轨迹行豁免(continue 有
|
|
46
|
+
// 自有输出行——retry **保留** `[approved]` / `[denied]` 轨迹行)。
|
|
43
47
|
const isContinue = state.permission.name === "continue"
|
|
48
|
+
const yOnly = isYesNoModal(state.permission.name)
|
|
44
49
|
const isBatch = Boolean(state.permission.batch)
|
|
45
|
-
const validKeys =
|
|
50
|
+
const validKeys = yOnly ? ["y", "n"] : isBatch ? ["a", "o", "n"] : ["y", "n", "a"]
|
|
46
51
|
if (validKeys.includes(answer) || key.name === "escape") {
|
|
47
52
|
const { resolve, name } = state.permission
|
|
48
53
|
state.permission = null
|
|
@@ -58,13 +63,13 @@ export function handlePermissionMode(str, key, ctx) {
|
|
|
58
63
|
render()
|
|
59
64
|
return true
|
|
60
65
|
}
|
|
61
|
-
if (answer === "a" && !
|
|
66
|
+
if (answer === "a" && !yOnly) {
|
|
62
67
|
agent.autoApprove = true
|
|
63
68
|
agent._pendingReminders = agent._pendingReminders ?? []
|
|
64
69
|
agent._pendingReminders.push("[System reminder: AUTO mode is now ON. All tool calls are automatically approved. Use /auto to disable.]")
|
|
65
70
|
pushLine(` [auto] AUTO ON: tool calls no longer prompt for approval (/auto to disable)`, C.warn)
|
|
66
71
|
}
|
|
67
|
-
const approved = answer === "y" || (answer === "a" && !
|
|
72
|
+
const approved = answer === "y" || (answer === "a" && !yOnly)
|
|
68
73
|
// leave trail: record approval/denial in conversation (continue prompt has its own output, don't duplicate)
|
|
69
74
|
if (!isContinue) {
|
|
70
75
|
pushLine(` [${approved ? "approved" : "denied"}] ${name}`, approved ? C.dim : C.error)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ledger-surface.mjs — TUI 台账可见面胶水(LEDGER-SURFACE 批——设计档 §2.30.3.4)。
|
|
3
|
+
*
|
|
4
|
+
* 三面:① 启动行(首扫、**任一项目可动作**才出——明细行集每项目一行)② 变化行(老化首次
|
|
5
|
+
* 越线 / 池达阈值——一次性去重 + **送达门**:未送达不记账)③ 状态位(`state.ledger`——L1
|
|
6
|
+
* 常驻标记,`render-frame.mjs` 消费;**不受启动行门约束**——F2 按标记态照显)。
|
|
7
|
+
* 只读台账;唯一写面 = 去重档(`~/.thincoder/ledger-notify.json`)。
|
|
8
|
+
*
|
|
9
|
+
* M2/KD-M2-3:核台账面**动态 import**(`@thincoder/core/ledger.mjs` 静态链 `node:sqlite`——
|
|
10
|
+
* 静态 import 会把 `node:sqlite` 拽进端壳静态面);载入失败 = 降级不崩(N1),扫描拍重试。
|
|
11
|
+
*/
|
|
12
|
+
import { C } from "./ansi.mjs"
|
|
13
|
+
|
|
14
|
+
/** 核台账面惰性载入(KD-M2-3 动态 import——失败不缓存,下次扫描拍重试;N1 降级不崩)。 */
|
|
15
|
+
let _core = null
|
|
16
|
+
function loadCore() {
|
|
17
|
+
if (!_core) {
|
|
18
|
+
_core = import("@thincoder/core/ledger.mjs")
|
|
19
|
+
_core.catch(() => { _core = null })
|
|
20
|
+
}
|
|
21
|
+
return _core
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 单次扫描(直驱面——timer 包装见 `startLedgerSurface`)。
|
|
25
|
+
* `startup=true` = 会话首扫(补启动行;变化行在前、明细行在后)。 */
|
|
26
|
+
export async function runLedgerScan({ state, agent = null, anchor = null, notifyFile = undefined, pushLine = () => {}, render = () => {}, startup = false } = {}) {
|
|
27
|
+
const core = await loadCore()
|
|
28
|
+
const { buildScan, detailScans, discoverFamily, formatDetailLine, formatMarker, loadNotifyState, NOTIFY_FILE, notifyKey, planChangeLines, saveNotifyState } = core
|
|
29
|
+
const base = anchor ?? agent?.cwd ?? process.cwd()
|
|
30
|
+
const family = discoverFamily(base)
|
|
31
|
+
const scans = []
|
|
32
|
+
for (const p of family.projects) {
|
|
33
|
+
try { scans.push(buildScan({ cwd: p.root })) } catch { /* 台账不可读 → 该项目跳过(余者照常——N1/T110①) */ }
|
|
34
|
+
}
|
|
35
|
+
const current = family.current ? scans.find((s) => s.root === family.current.root) ?? null : null
|
|
36
|
+
const notify = loadNotifyState(notifyFile ?? NOTIFY_FILE)
|
|
37
|
+
const plans = scans.map((s) => ({ s, plan: planChangeLines(notify.ledgers[notifyKey(s.ledger)], s) }))
|
|
38
|
+
const changeLines = plans.flatMap(({ plan }) => plan.lines)
|
|
39
|
+
const showStartup = startup && scans.some((s) => s.actionable)
|
|
40
|
+
const rows = showStartup
|
|
41
|
+
? [...changeLines, ...detailScans(scans, current).map((s) => ({ text: formatDetailLine(s), warn: s.actionable }))]
|
|
42
|
+
: changeLines
|
|
43
|
+
let delivered = true
|
|
44
|
+
for (const line of rows) {
|
|
45
|
+
try { pushLine(line.text, line.warn ? C.warn : C.dim) } catch { delivered = false; break }
|
|
46
|
+
}
|
|
47
|
+
// 送达门(F5):有事件行 → 仅在送达后记账;无事件行 → 仅状态确有变化时静默同步(跌回 false 即重置)
|
|
48
|
+
const changed = plans.some(({ s, plan }) => {
|
|
49
|
+
const prev = notify.ledgers[notifyKey(s.ledger)]
|
|
50
|
+
return !prev || JSON.stringify(prev.aged ?? []) !== JSON.stringify(plan.next.aged) || Boolean(prev.threshold) !== plan.next.threshold
|
|
51
|
+
})
|
|
52
|
+
if (changeLines.length ? delivered : changed) {
|
|
53
|
+
const ledgers = { ...notify.ledgers }
|
|
54
|
+
for (const { s, plan } of plans) ledgers[notifyKey(s.ledger)] = { ...plan.next, updatedAt: Date.now() }
|
|
55
|
+
saveNotifyState(notifyFile ?? NOTIFY_FILE, { version: 1, ledgers })
|
|
56
|
+
}
|
|
57
|
+
state.ledger = { marker: current ? formatMarker(current) : null, warn: Boolean(current && current.aged > 0), scannedAt: Date.now() }
|
|
58
|
+
render()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** TUI 挂载:首扫(`setImmediate`——不抢首帧)+ 周期(`REFRESH_MS`;`state.processing` 期间跳过本轮)。 */
|
|
62
|
+
export function startLedgerSurface(ctx = {}) {
|
|
63
|
+
let disposed = false
|
|
64
|
+
let timer = null
|
|
65
|
+
const tick = async (startup) => {
|
|
66
|
+
if (disposed) return
|
|
67
|
+
if (!startup && ctx.state?.processing) return // 扫描不打断回合帧
|
|
68
|
+
try { await runLedgerScan({ ...ctx, startup }) } catch { /* 可见面尽力——不崩 TUI(N1) */ }
|
|
69
|
+
}
|
|
70
|
+
setImmediate(async () => {
|
|
71
|
+
if (disposed) return
|
|
72
|
+
tick(true)
|
|
73
|
+
const core = await loadCore().catch(() => null)
|
|
74
|
+
if (disposed || !core) return // 载入失败 → 无周期(N1——降级不崩)
|
|
75
|
+
timer = setInterval(() => tick(false), core.REFRESH_MS)
|
|
76
|
+
timer.unref?.()
|
|
77
|
+
})
|
|
78
|
+
return {
|
|
79
|
+
dispose() {
|
|
80
|
+
disposed = true
|
|
81
|
+
if (timer) clearInterval(timer)
|
|
82
|
+
timer = null
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* model-catalog.mjs — 会话级模型清单 helper(PROVIDER.md §16.2 M2/M9——2026-09-10 MODEL-SELECTION)。
|
|
3
3
|
*
|
|
4
|
-
* 清单唯一权威 = provider 运行期拉取(`GET /models`——provider/list-models.mjs 按 format 分派)。
|
|
4
|
+
* 清单唯一权威 = provider 运行期拉取(`GET /models`——thincoder-core/provider/list-models.mjs 按 format 分派)。
|
|
5
5
|
* 本模块提供:
|
|
6
6
|
* - `getProviderModels(providerConfig)`:拉取 + **会话级缓存**(TTL 60s;失败不缓存——下次重试);
|
|
7
7
|
* - `probeChannelModels(providerConfig)`:配置阶段准入探(M9——探通入缓存候选直接可用 / 探不通返回
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
* - `modelListFailureText(error)`:M8 失败消息本体(逐字长句——界面明示的唯一断言对象);
|
|
10
10
|
* - `dedupeModels` / `modelSeries`:显示归并(自 model-picker.mjs 迁入——会话面与槽位面共用)。
|
|
11
11
|
*
|
|
12
|
-
* 缓存时钟可注入(`_catalogHooks.now`——先例 rate.mjs `_rateHooks`):测试假时钟确定性断言 T6 不依赖壁钟。
|
|
12
|
+
* 缓存时钟可注入(`_catalogHooks.now`——先例 thincoder-core/provider/rate.mjs `_rateHooks`):测试假时钟确定性断言 T6 不依赖壁钟。
|
|
13
13
|
*/
|
|
14
|
-
import { listModels } from "
|
|
14
|
+
import { listModels } from "@thincoder/core/provider/list-models.mjs"
|
|
15
15
|
|
|
16
16
|
/** 会话级缓存 TTL(§16.4 a2:连续操作不重复付网络;60s 新鲜窗口)。 */
|
|
17
17
|
const CACHE_TTL_MS = 60_000
|
|
18
18
|
|
|
19
|
-
/** 测试钩子(先例
|
|
19
|
+
/** 测试钩子(先例 thincoder-core/provider/rate.mjs `_rateHooks`):时钟可注入——T6 假时钟断言。 */
|
|
20
20
|
export const _catalogHooks = { now: () => Date.now() }
|
|
21
21
|
|
|
22
22
|
/** 会话内缓存:key = name + baseURL(同渠道不同端点各自成条);value = { models, at }。 */
|
package/src/tui/model-picker.mjs
CHANGED
|
@@ -10,16 +10,16 @@
|
|
|
10
10
|
* 渠道管理流(add/remove/key/context)是 provider 级 config 写——语义不变。F-4 (ISSUE-FIX-BATCH):
|
|
11
11
|
* removeProviderFlow 级联清理悬挂引用(cascadeRemoveProvider——文件尾导出)。
|
|
12
12
|
* ctx: { agent, state, render, ansi, C, pushLine, persistRaw, askQuestion, maskKey, showPicker,
|
|
13
|
-
* closePicker, renderPickerLines }
|
|
13
|
+
* closePicker, renderPickerLines, confirmDelete }(后四者为 pickers.mjs 通用 picker 绑定——闭包入参,环安全)。
|
|
14
14
|
*/
|
|
15
15
|
import { sliceByWidth } from "./render.mjs"
|
|
16
|
-
import { PROVIDER_PRESETS as PRESETS, providerSpec, specMatch } from "
|
|
17
|
-
import { saveSession } from "
|
|
16
|
+
import { PROVIDER_PRESETS as PRESETS, providerSpec, specMatch } from "@thincoder/core/config.mjs"
|
|
17
|
+
import { saveSession } from "@thincoder/core/session.mjs"
|
|
18
18
|
import { getProviderModels, probeChannelModels, modelListFailureText, dedupeModels } from "./model-catalog.mjs"
|
|
19
19
|
|
|
20
20
|
/** createModelPicker(ctx) → { openModelPicker, selectModel, setProviderKey, setContextFlow, pickModelForSlot } */
|
|
21
21
|
export function createModelPicker(ctx) {
|
|
22
|
-
const { agent, state, pushLine, persistRaw, askQuestion, maskKey, showPicker, closePicker, renderPickerLines } = ctx
|
|
22
|
+
const { agent, state, pushLine, persistRaw, askQuestion, maskKey, showPicker, closePicker, renderPickerLines, confirmDelete = () => { throw new Error("ctx.confirmDelete missing — deletion refused (fail-closed)") } } = ctx
|
|
23
23
|
const { ansi, C } = ctx
|
|
24
24
|
|
|
25
25
|
/** entry 唯一标识:异步更新 entries 后按它恢复选中项 */
|
|
@@ -231,7 +231,7 @@ export function createModelPicker(ctx) {
|
|
|
231
231
|
agent.provider = { ...target }
|
|
232
232
|
agent.provider.model = item.model
|
|
233
233
|
if (agent.config?.agent?.compactThresholdAuto) {
|
|
234
|
-
const { resolveCompactThreshold } = await import("
|
|
234
|
+
const { resolveCompactThreshold } = await import("@thincoder/core/config.mjs")
|
|
235
235
|
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
236
236
|
}
|
|
237
237
|
// 写会话槽(saveSession——槽双字段恒非空)——config 文件零写
|
|
@@ -280,7 +280,7 @@ export function createModelPicker(ctx) {
|
|
|
280
280
|
/** Slot 面 fetch:模型直接并入可选行(旧 pickModelForSlot 语义——自由面;槽位面零改——
|
|
281
281
|
* 仅随 M1 获得三 format 支持 + 占位行(同会话面:0 item 的 picker 不会打开))。 */
|
|
282
282
|
async function fetchSlotModels(providerName, providerConfig, entries) {
|
|
283
|
-
const { listModels } = await import("
|
|
283
|
+
const { listModels } = await import("@thincoder/core/provider/index.mjs")
|
|
284
284
|
let selKey = null
|
|
285
285
|
try {
|
|
286
286
|
const models = await listModels(
|
|
@@ -379,6 +379,7 @@ export function createModelPicker(ctx) {
|
|
|
379
379
|
...candidates.map((p) => ({ type: "item", text: `${p.name} (${defaultModelLabel(p)})`, name: p.name })),
|
|
380
380
|
])
|
|
381
381
|
if (!se) return
|
|
382
|
+
if (!(await confirmDelete(`Remove provider ${se.name}?`))) return
|
|
382
383
|
await persistRaw((raw) => {
|
|
383
384
|
raw.providers ??= []
|
|
384
385
|
const idx = raw.providers.findIndex((p) => p?.name === se.name)
|
|
@@ -460,7 +461,7 @@ export function createModelPicker(ctx) {
|
|
|
460
461
|
if (newCtx === undefined) delete agent.provider.context
|
|
461
462
|
else agent.provider.context = newCtx
|
|
462
463
|
if (agent.config?.agent?.compactThresholdAuto) {
|
|
463
|
-
const { resolveCompactThreshold } = await import("
|
|
464
|
+
const { resolveCompactThreshold } = await import("@thincoder/core/config.mjs")
|
|
464
465
|
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
465
466
|
}
|
|
466
467
|
}
|
package/src/tui/mouse.mjs
CHANGED
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
import { computeLayout, subagentLineIndex } from "./layout.mjs"
|
|
18
18
|
import { buildConvLines, convViewport } from "./render-conversation.mjs"
|
|
19
19
|
import { toggleFoldBlock, scrollFoldBlock, foldScrollOffset } from "./fold-block.mjs"
|
|
20
|
-
import { cancelAsyncSubagent, cancelSyncChild } from "
|
|
21
|
-
import { cancelAsyncAdvisor } from "
|
|
22
|
-
import { maybeRefillAsync, refreshQueuedTokens } from "
|
|
20
|
+
import { cancelAsyncSubagent, cancelSyncChild } from "@thincoder/core/agent-tools/subagent-async.mjs"
|
|
21
|
+
import { cancelAsyncAdvisor, refreshAdvisorQueuedTokens } from "@thincoder/core/agent-tools/advisor-async.mjs" // ED-4:评审队列排队块刷新
|
|
22
|
+
import { maybeRefillAsync, refreshQueuedTokens } from "@thincoder/core/agent-tools/subagent-scheduler.mjs" // F-2:queued 取消后续(补位/位置刷新)——叶子模块
|
|
23
23
|
import { routeSubToken } from "./subagent-blocks.mjs" // F-2:queued 取消块移除(⟦ev⟧cancelled 就地路由——引擎动作路径同通道)
|
|
24
24
|
import { denyModalForOwner } from "./key-modes.mjs"
|
|
25
25
|
import { C } from "./ansi.mjs"
|
|
@@ -199,12 +199,14 @@ export function createMouseDispatch({ agent, state, pushLine, render, popPicker
|
|
|
199
199
|
try {
|
|
200
200
|
const id = key.slice(key.lastIndexOf("#") + 1)
|
|
201
201
|
const isAdvisorBlock = key.startsWith("advisor#")
|
|
202
|
+
// af 批 #1(发射单源化):`emit` = 本层通道,传进核调用供其单点发射(本层不另发)。
|
|
203
|
+
const emit = (t) => { if (!routeSubToken(state, t, render)) pushLine(t, C.dim) }
|
|
202
204
|
// §11.2 D-24b (②-6b): ⏹ on an advisor block cancels the background review
|
|
203
205
|
// (directed abort → cancelled settle: no pending entry / no token).
|
|
204
206
|
// SYNC-CANCEL F3: async 池/advisor miss 后查 sync registry(⏹ 门控已放开 sync——
|
|
205
207
|
// cancelSyncChild 与 async cancel 同模块同形态——subagent-async.mjs)。
|
|
206
208
|
let r = isAdvisorBlock
|
|
207
|
-
? cancelAsyncAdvisor(agent, id)
|
|
209
|
+
? cancelAsyncAdvisor(agent, id, emit)
|
|
208
210
|
: cancelAsyncSubagent(agent, id)
|
|
209
211
|
let syncStopped = false
|
|
210
212
|
if (!isAdvisorBlock && r?.status === "error") {
|
|
@@ -230,11 +232,14 @@ export function createMouseDispatch({ agent, state, pushLine, render, popPicker
|
|
|
230
232
|
// 无 settle 事件链)——UI 直连路径无回合 ctx 事件流——补 executeCancelAction
|
|
231
233
|
// 同款 TUI 维护:等待块移除(⟦ev⟧cancelled 就地路由——守卫同 routeSubToken
|
|
232
234
|
// cancelled 分支——不冻结)+ 补位 + 剩余排队头位置/依赖标注刷新(⟦ev⟧queued)。
|
|
235
|
+
// af 批去重(#1 单源化):共享行加 `!isAdvisorBlock` 守卫——评审族发射已由核单点经
|
|
236
|
+
// 上方 `emit` 通道完成;子代理族自持发射面零改(该族两路互斥、各发一次)。
|
|
233
237
|
if (r?.was === "queued") {
|
|
234
|
-
|
|
235
|
-
emit(`${key}/⟦ev⟧cancelled\x1e`)
|
|
238
|
+
if (!isAdvisorBlock) emit(`${key}/⟦ev⟧cancelled\x1e`)
|
|
236
239
|
maybeRefillAsync(agent)
|
|
237
240
|
refreshQueuedTokens(agent, emit)
|
|
241
|
+
// ED-4(评审池排队语义):advisor 队列余位位置刷新(subagent 队列同构面)。
|
|
242
|
+
if (isAdvisorBlock) refreshAdvisorQueuedTokens(agent, emit)
|
|
238
243
|
}
|
|
239
244
|
pushLine(`[subagent ${key} stop requested]`, C.warn)
|
|
240
245
|
}
|
package/src/tui/pickers.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { createModelPicker } from "./model-picker.mjs"
|
|
|
9
9
|
* 通用绑定(showPicker/closePicker/renderPickerLines)再行装配——index.mjs 消费面不变。
|
|
10
10
|
* 单一 Promise API:showPicker(title, entries, { defaultIndex }) → Promise<entry|null>。
|
|
11
11
|
* picker 栈:state.pickerStack,state.picker 始终指向栈顶(layout/render/key-handler 都只读 state.picker)。
|
|
12
|
+
* 删除类二次确认件:confirmDelete(TUI-COMMANDS.md §5.4 单源——形态同 cmd-clear / cmd-new 先例)。
|
|
12
13
|
* 选中即关闭(Enter = resolve + pop);Esc = pop 当前层并 resolve(null)。菜单循环由调用方 while 重开。 */
|
|
13
14
|
export function createPickers(ctx) {
|
|
14
15
|
const { agent, state, render, ansi, C, pushLine, persistRaw, askQuestion, maskKey } = ctx
|
|
@@ -105,13 +106,25 @@ export function createPickers(ctx) {
|
|
|
105
106
|
|
|
106
107
|
function renderPickerLines() { rebuildLines() }
|
|
107
108
|
|
|
109
|
+
/** 删除类入口二次确认件(单源——TUI-COMMANDS.md §5.4):形态 = 既有二次确认 picker
|
|
110
|
+
* (与 cmd-clear.mjs:6-9 · cmd-new.mjs:30-33 先例逐字同形,禁新造模态)。
|
|
111
|
+
* defaultIndex 恒指 Cancel(反误触——直接 Enter 不会删除);Esc / Cancel ⇒ false
|
|
112
|
+
* ⇒ 调用方零动作(取消先于落盘发生)。入口 ctx 缺此绑定 ⇒ 调用方报错(fail-closed)。 */
|
|
113
|
+
async function confirmDelete(question = "Delete?") {
|
|
114
|
+
const e = await showPicker(question, [
|
|
115
|
+
{ type: "item", text: "Yes, delete", action: "yes" },
|
|
116
|
+
{ type: "item", text: "Cancel", action: "no" },
|
|
117
|
+
], { defaultIndex: 1 })
|
|
118
|
+
return e?.action === "yes"
|
|
119
|
+
}
|
|
120
|
+
|
|
108
121
|
// /model 两级面 + selectModel + 渠道管理 + pickModelForSlot —— 迁 model-picker.mjs
|
|
109
122
|
// (MODEL-MERGE-SESSION——pick 后本文件 ≤450)。createModelPicker 返回同名单函数,
|
|
110
123
|
// 经本层原样转发(index.mjs/wizard/slash-commands 消费面零变化)。
|
|
111
124
|
const modelPicker = createModelPicker({
|
|
112
125
|
agent, state, render, ansi, C, pushLine, persistRaw, askQuestion, maskKey,
|
|
113
|
-
showPicker, closePicker, renderPickerLines,
|
|
126
|
+
showPicker, closePicker, renderPickerLines, confirmDelete,
|
|
114
127
|
})
|
|
115
128
|
|
|
116
|
-
return { showPicker, closePicker, popPicker, renderPickerLines, ...modelPicker }
|
|
129
|
+
return { showPicker, closePicker, popPicker, renderPickerLines, confirmDelete, ...modelPicker }
|
|
117
130
|
}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
isExpanded, foldHintLine, renderExpandedBlock, renderBlockTimeline,
|
|
16
16
|
renderMathAndMarkdown, foldCapRows, renderFoldedHead, foldTailLines,
|
|
17
17
|
} from "./fold-block.mjs"
|
|
18
|
-
import { ADVISOR_THINKING_PLACEHOLDER } from "
|
|
18
|
+
import { ADVISOR_THINKING_PLACEHOLDER } from "@thincoder/core/advisor/run.mjs"
|
|
19
19
|
import { frozenSubSeg, toolSeg, frozenAdvSeg } from "./render-segments.mjs"
|
|
20
20
|
|
|
21
21
|
|
package/src/tui/render-frame.mjs
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
import { ansi, C, ESC } from "./ansi.mjs"
|
|
10
10
|
import { convCacheKey, renderConversation, countConvLines } from "./render-conversation.mjs"
|
|
11
11
|
import { sliceByWidth, stringWidth } from "./render.mjs"
|
|
12
|
-
import { providerSpec } from "
|
|
12
|
+
import { providerSpec } from "@thincoder/core/config.mjs"
|
|
13
13
|
import { computeLayout, subagentVisibleLines } from "./layout.mjs"
|
|
14
14
|
import { basename } from "node:path"
|
|
15
15
|
import { readFileSync } from "node:fs"
|
|
16
|
+
import { isYesNoModal } from "./interaction.mjs" // X8 跟进①:y/n-only 族判据单源
|
|
16
17
|
|
|
17
18
|
export { convCacheKey, renderConversation, countConvLines } from "./render-conversation.mjs"
|
|
18
19
|
|
|
@@ -39,7 +40,7 @@ const SLASH_HINTS = {
|
|
|
39
40
|
|
|
40
41
|
/** Header panel (always 1 line). */
|
|
41
42
|
export function renderHeader(agent, cols) {
|
|
42
|
-
// 2026-09-02 Q1(SESSION.md §8):provider 可为 null(无效 provider 被清空后用户 Esc 取消重选)
|
|
43
|
+
// 2026-09-02 Q1(SESSION.md §6.8):provider 可为 null(无效 provider 被清空后用户 Esc 取消重选)
|
|
43
44
|
// —— 可选链守卫,头部显示 "no provider" 占位
|
|
44
45
|
const model = agent.provider?.model ?? "no provider"
|
|
45
46
|
// providerSpec(PROVIDER.md §15):模型信息显示跟随 provider 级 context 覆盖(D-C5);
|
|
@@ -323,7 +324,9 @@ function inputBoxStyle(state) {
|
|
|
323
324
|
borderColor = C.tool; title = " Question "
|
|
324
325
|
} else if (state.permission) {
|
|
325
326
|
borderColor = C.warn
|
|
326
|
-
|
|
327
|
+
// X8 跟进①:y/n-only 族(continue / retry)按名分流——`a` 不属该族键面
|
|
328
|
+
title = isYesNoModal(state.permission.name)
|
|
329
|
+
? (state.permission.name === "continue" ? " Continue? (y/n) " : " Retry? (y/n) ")
|
|
327
330
|
: state.permission.batch ? ` Allow ${state.permission.name}? (a/o/n) `
|
|
328
331
|
: ` Allow ${state.permission.name}? (y/n/a) `
|
|
329
332
|
} else if (state.picker) {
|
|
@@ -349,7 +352,9 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
349
352
|
: " Type answer then Enter │ Esc: cancel"
|
|
350
353
|
}
|
|
351
354
|
if (state.permission) {
|
|
352
|
-
if (state.permission.name
|
|
355
|
+
if (isYesNoModal(state.permission.name)) {
|
|
356
|
+
return state.permission.name === "continue" ? " y: continue │ n: stop" : " y: retry │ n: stop"
|
|
357
|
+
}
|
|
353
358
|
if (state.permission.batch) return " a: approve all │ o: one by one │ n: deny"
|
|
354
359
|
return " y: approve │ n: deny │ a: approve all (AUTO)"
|
|
355
360
|
}
|
|
@@ -383,7 +388,7 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
383
388
|
const elapsed = state.processing ? ` ${Math.floor((Date.now() - state.processingStarted) / 1000)}s` : ""
|
|
384
389
|
const toolHint = state.currentTool ? ` ${state.currentTool}…` : ""
|
|
385
390
|
const statusText = state.processing ? `${state.status}${toolHint}${elapsed}` : state.status
|
|
386
|
-
// 2026-09-02 Q1(SESSION.md §8):provider 可为 null —— providerSpec(null) 保守 128K 不抛错
|
|
391
|
+
// 2026-09-02 Q1(SESSION.md §6.8):provider 可为 null —— providerSpec(null) 保守 128K 不抛错
|
|
387
392
|
// 2026-09-02 §15:context 窗口跟随 providers[].context 覆盖(T-C6——百分比基于覆盖后的窗口)
|
|
388
393
|
const modelContext = providerSpec(agent.provider).context
|
|
389
394
|
const ctxPct = Math.round((state.ctxCache.tokens / modelContext) * 100)
|
|
@@ -393,5 +398,11 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
393
398
|
// INPUT-LOCK-ASYNC(C'——F-3):busy(processing 含 digest)状态栏提示——取代旧
|
|
394
399
|
// 排队提示(F-7 已删)——“主会话处理中——Enter 提交禁用”
|
|
395
400
|
const enterHint = state.processing ? `主会话处理中 — Enter 提交禁用(字符可输入,回合结束请重按 Enter)` : "Enter: send"
|
|
396
|
-
|
|
401
|
+
// LEDGER-SURFACE(§2.30.3.3/§2.30.3.4):L1 常驻标记——状态段簇尾(scrollHint 后、键位组前);
|
|
402
|
+
// 空标记零注入(半态逐字节等价——同 :233-238 纪律);warn = 当前项目老化 > 0(警示色段包裹)
|
|
403
|
+
const lg = state.ledger
|
|
404
|
+
const ledgerHint = lg?.marker
|
|
405
|
+
? ` │ ${lg.warn ? `${ansi.reset}${C.warn}${lg.marker}${ansi.reset}${ansi.dim}` : lg.marker}`
|
|
406
|
+
: ""
|
|
407
|
+
return ` ${statusText}${taskHint}${turnHint}${tokenHint}${ctxHint}${scrollHint}${ledgerHint} │ ${enterHint} │ /: commands │ wheel/PgUp/PgDn: scroll │ Ctrl+I: inject │ Ctrl+C: exit (×2)`
|
|
397
408
|
}
|
package/src/tui/render-loop.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* so recomputing the frame each paint is cheap.
|
|
15
15
|
*/
|
|
16
16
|
import { countConvLines, renderRows } from "./render-frame.mjs"
|
|
17
|
-
import { estimateTokens } from "
|
|
17
|
+
import { estimateTokens } from "@thincoder/core/context.mjs"
|
|
18
18
|
import { ansi, C } from "./ansi.mjs"
|
|
19
19
|
import { isTuiActive } from "./tui-lifecycle.mjs" // F-2 渲染抑制守卫(RESIZE-MOUSE-LEAK-FIX)
|
|
20
20
|
|
|
@@ -86,7 +86,9 @@ function frozenSubTaskLines(state, sub, cols, maxRows) {
|
|
|
86
86
|
const modelPart = sub.model ? ` · ${sub.model}` : ""
|
|
87
87
|
const turnPart = sub.maxTurns > 0 ? ` · turn ${sub.turn}/${sub.maxTurns}` : ""
|
|
88
88
|
const errPart = sub.lastError ? ` — ${sub.lastError}` : ""
|
|
89
|
-
|
|
89
|
+
// 图标三态互斥(2026-09-20 端差·显示面消差批 M5):修前 stopped 冻结头 `✓` 与 verb 相抵
|
|
90
|
+
// ⇒ 补齐 VSC `webview/activity-view.js:46-49` 已成的 cancelled → `⏹` 形态(VSC 标尺,CLI 缺面)。
|
|
91
|
+
const icon = sub.approval ? "⏸" : sub.stopped ? "⏹" : "✓"
|
|
90
92
|
const verb = sub.stopped ? "stopped" : "done" // §19.5: cancel 冻结标题 "stopped"
|
|
91
93
|
const header = `[${icon} ${sub.key}${modePart}${modelPart} · ${verb} ${elapsed}s${turnPart}${errPart}]`
|
|
92
94
|
const out = []
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { C } from "./ansi.mjs"
|
|
12
|
-
import { specForModel } from "
|
|
12
|
+
import { specForModel } from "@thincoder/core/config.mjs"
|
|
13
13
|
import { handleClearCommand } from "./cmd-clear.mjs"
|
|
14
14
|
import { handleNewCommand } from "./cmd-new.mjs"
|
|
15
15
|
import { handleExitCommand } from "./cmd-exit.mjs"
|