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/index.mjs
CHANGED
|
@@ -25,16 +25,16 @@ import { closeAllMcp } from "../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"
|
|
28
|
-
import { SLASH_COMMANDS,
|
|
28
|
+
import { SLASH_COMMANDS, createSlashCommands } from "./slash-commands.mjs"
|
|
29
29
|
import { createWizard } from "./wizard.mjs"
|
|
30
|
-
import { writeStartupSequence, createExitCleanup } from "./tui-lifecycle.mjs"
|
|
30
|
+
import { writeStartupSequence, setTuiActive, createExitCleanup } from "./tui-lifecycle.mjs"
|
|
31
31
|
import { createPickers } from "./pickers.mjs"
|
|
32
32
|
import { runDistill as runDistillImpl } from "./distill-cmd.mjs"
|
|
33
33
|
import { createInteraction } from "./interaction.mjs"
|
|
34
34
|
import { pasteClipboardImage as pasteClipboardImageImpl, insertPastedText, translateShiftEnter, stripKeyboardProtocol } from "./clipboard.mjs"
|
|
35
|
-
import { parseMouseClicks, handleWheel, createMouseDispatch } from "./mouse.mjs"
|
|
35
|
+
import { parseMouseClicks, handleWheel, createMouseDispatch, mouseOob } from "./mouse.mjs"
|
|
36
36
|
import { runAgentTurn } from "./agent-turn.mjs"
|
|
37
|
-
import { createKeyHandler, convMaxScroll } from "./key-handler.mjs"
|
|
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
40
|
import { createConfigHelpers } from "./config-helpers.mjs"
|
|
@@ -53,7 +53,12 @@ export async function promptProviderIfInvalid(agent, openModelPicker, pushLine)
|
|
|
53
53
|
if (!(agent._providerInvalid || !agent.provider)) return false
|
|
54
54
|
await openModelPicker()
|
|
55
55
|
if (!agent.provider) {
|
|
56
|
-
|
|
56
|
+
// MODEL-MERGE-SESSION 引导 A(F-6):空槽 + defaultModel 未设 → 提示 /config 默认模型入口
|
|
57
|
+
// (index.mjs 本行为 D-S2 取消提示扩展——设计文件清单外——随批上报)
|
|
58
|
+
const hasProviders = (agent.providers?.length ?? 0) > 0
|
|
59
|
+
pushLine(hasProviders && !agent.config?.defaultModel
|
|
60
|
+
? "尚未设置默认模型(config.defaultModel——新会话起点):/config → 默认模型 设置一次;/model 仅改本会话"
|
|
61
|
+
: "未配置有效 provider,可用 /model 选择或 /provider 配置", C.warn)
|
|
57
62
|
}
|
|
58
63
|
return true
|
|
59
64
|
}
|
|
@@ -106,12 +111,13 @@ export async function startTUI(agent, opts = {}) {
|
|
|
106
111
|
reasoning: "", // thinking stream buffer (dimmed display)
|
|
107
112
|
completion: null, // Tab completion state { candidates, index }
|
|
108
113
|
|
|
109
|
-
subTasks: {}, // sub-agent activity blocks (§7.2 D4): { "coder#1": { key, role, model, started, done, doneAt, blocks: [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError, dropped, blockEpoch, awaitingDigest(§17 挂起中间态), _freezeAt
|
|
114
|
+
subTasks: {}, // sub-agent activity blocks (§7.2 D4): { "coder#1": { key, role, model, started, done, doneAt, blocks: [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError, dropped, blockEpoch, awaitingDigest(§17 挂起中间态), _freezeAt(冻结锚点), stopped, children: [](SUBAGENT-TAIL:嵌套子代理**守护载体**——内容行并入本块 blocks——subagent-children.mjs) } } — rendered as collapsible in-conversation blocks; persists across turns (blocks are the child activity's ONLY carrier — child tool calls never enter the parent history); bounded by the N2 500-line per-child ring buffer(SUBAGENT-TAIL 单环:内层行同环计数、单载体最旧先行——TUI.md §6)
|
|
110
115
|
currentTool: null, // currently executing tool name (shown in status bar)
|
|
111
116
|
processingStarted: 0, // current turn start time (status bar timer)
|
|
112
117
|
status: "Ready",
|
|
113
|
-
queue: [], //
|
|
114
|
-
interruptPrompt: null, // Ctrl+I
|
|
118
|
+
queue: [], // 交接残项单容器 [{ text }](INPUT-LOCK:submit 不再排队——仅释放窗口兜底/挂起中止残余——回合尾队列循环续发,至多一条)
|
|
119
|
+
interruptPrompt: null, // Ctrl+I inject box(第 31 批——TUI-INPUT-BOX.md §8): { chars: string[], cursor: number } or null
|
|
120
|
+
attentionAwaiting: false, // 第 33 批(TUI §14.3(f)):回合结束等待输入——链尾置位 / 用户输入清位;不落盘、不进会话
|
|
115
121
|
search: null, // Ctrl+F search mode: { query: "", matches: [{lineIndex, charIndex}], index: 0 } or null
|
|
116
122
|
expandedBlocks: new Set(), // block hashes that are expanded (Enter toggles)
|
|
117
123
|
foldEnabled: true, // global fold toggle — /fold on|off
|
|
@@ -121,13 +127,15 @@ export async function startTUI(agent, opts = {}) {
|
|
|
121
127
|
_historyLoaded: 0, // messages loaded from the TAIL of _fullHistory
|
|
122
128
|
_historyTotal: 0, // total messages in the restored session
|
|
123
129
|
_hasOlder: false, // more earlier messages remain unloaded
|
|
124
|
-
_agent: null, //
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
130
|
+
_agent: null, // TUI state → agent 回指挂载点:startTUI 装配时置 agent 引用——
|
|
131
|
+
// ① SYNC-CANCEL ⏹ 门控读 state._agent._syncChildAborts(subagent-panel.mjs,2026-09-09);
|
|
132
|
+
// ② CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):agent._tuiState = state 反向挂载——
|
|
133
|
+
// action:"panel" 经 ctx.state(= agent._tuiState)**读时现算**面板块(computePanelBlocks
|
|
134
|
+
// ——subTasks 活值纯推导——单账本)。headless/VS Code 无此装配 = 无面板 → panel 动作
|
|
135
|
+
// 恒降级池视图(CLI-only 完整能力——AC-P4)。
|
|
128
136
|
}
|
|
129
137
|
state._agent = agent
|
|
130
|
-
agent.
|
|
138
|
+
agent._tuiState = state // F-3:面板视图现算通道(替代退役的手工面板镜像)
|
|
131
139
|
|
|
132
140
|
// On session restore, if all tasks are completed, auto-collapse the todo panel (match runtime behavior)
|
|
133
141
|
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
@@ -147,6 +155,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
147
155
|
// modifyOtherKeys lvl 2 (\x1b[>4;2m): Shift+Enter → \x1b[27;2;13~ (mintty / Git Bash)
|
|
148
156
|
// translateShiftEnter (stdin layer) maps both to \x1b\r → meta+return → multiline branch.
|
|
149
157
|
writeStartupSequence()
|
|
158
|
+
setTuiActive(true) // R25(F-R25a):终端接管完成——置 TUI 活动态(崩溃钩子恢复判定源)
|
|
150
159
|
|
|
151
160
|
const utf8Decoder = new TextDecoder("utf-8", { fatal: false })
|
|
152
161
|
|
|
@@ -159,6 +168,10 @@ export async function startTUI(agent, opts = {}) {
|
|
|
159
168
|
let text = mousePending + utf8Decoder.decode(chunk, { stream: true })
|
|
160
169
|
mousePending = ""
|
|
161
170
|
|
|
171
|
+
// 第 33 批(TUI §14.3(e) 鼠标点):stdin 数据到达即用户在场 ⇒ 清 attention 位
|
|
172
|
+
// (单点覆盖下方滚轮分支与 onMouseClick;键盘入口在 key-handler)。
|
|
173
|
+
clearAttention(state, render)
|
|
174
|
+
|
|
162
175
|
// Bracketed paste: terminal wraps pasted text in \x1b[200~ ... \x1b[201~
|
|
163
176
|
// Route pasted content to the active text target (question answer / input box) in one shot,
|
|
164
177
|
// avoiding slow char-by-char keypress render — see insertPastedText in clipboard.mjs
|
|
@@ -208,6 +221,10 @@ export async function startTUI(agent, opts = {}) {
|
|
|
208
221
|
for (const m of text.matchAll(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/g)) {
|
|
209
222
|
const button = Number(m[1])
|
|
210
223
|
if (button === 64 || button === 65) {
|
|
224
|
+
// F-3 sane-gate ③(RESIZE-MOUSE-LEAK-FIX):越界 wheel 禁止会话滚动——handleWheel
|
|
225
|
+
// 对越界返回未消费会穿到 fallback 滚动(mouse.mjs 内 gate 覆盖不到此路径)
|
|
226
|
+
const dims = state.dims ? state.dims.get() : { cols: process.stdout.columns || 80, rows: process.stdout.rows || 24 }
|
|
227
|
+
if (mouseOob(Number(m[2]), Number(m[3]), dims)) continue
|
|
211
228
|
const consumed = handleWheel(mouseCtx(), button, Number(m[2]), Number(m[3]))
|
|
212
229
|
if (!consumed) {
|
|
213
230
|
if (button === 64) {
|
|
@@ -313,6 +330,14 @@ export async function startTUI(agent, opts = {}) {
|
|
|
313
330
|
async function submit() {
|
|
314
331
|
const text = state.input.join("").trim()
|
|
315
332
|
if (!text) return
|
|
333
|
+
// INPUT-LOCK 防御(C'——2026-09-09)+ INPUT-LOCK-BEHAVIOR-REVISED(2026-09-09——白名单
|
|
334
|
+
// 已删——busy 全拒):提交吞的门禁在 key-handler(busy Enter 已吞 + 提示,文本保留)——
|
|
335
|
+
// submit 只在非 busy 期达此;防御直呼/上游改动:busy 期拒绝(不清输入框不吞内容)。
|
|
336
|
+
if (state.processing) {
|
|
337
|
+
pushLine(`[主会话处理中 —— 消息未发送(回合结束后请重按 Enter)]`, C.warn)
|
|
338
|
+
render()
|
|
339
|
+
return
|
|
340
|
+
}
|
|
316
341
|
state.input = []
|
|
317
342
|
state.cursor = 0
|
|
318
343
|
const wasInHistory = state.historyIndex !== -1
|
|
@@ -322,37 +347,14 @@ export async function startTUI(agent, opts = {}) {
|
|
|
322
347
|
state.scroll = 0
|
|
323
348
|
state._followTail = true // 2026-08-31 会诊 deepseek:新消息恢复跟随(注释曾承诺、实现缺漏)
|
|
324
349
|
|
|
325
|
-
// Slash commands: handled locally, don't enter agent loop
|
|
350
|
+
// Slash commands: handled locally, don't enter agent loop. busy 期提交在 key-handler
|
|
351
|
+
// 门禁被吞(白名单已删——斜杠同吞——INPUT-LOCK-BEHAVIOR-REVISED)——submit 仅非 busy 期可达。
|
|
326
352
|
if (text.startsWith("/")) {
|
|
327
|
-
if (state.processing) {
|
|
328
|
-
// While processing: allowlisted commands execute directly (they only touch
|
|
329
|
-
// local TUI/agent config, never the in-flight turn); the rest are queued
|
|
330
|
-
const cmd0 = text.split(/\s+/)[0].toLowerCase()
|
|
331
|
-
const resolved0 = SLASH_ALIASES[cmd0] ?? cmd0
|
|
332
|
-
const safeDuringProcessing = new Set(["/help", "/exit", "/model", "/submodel", "/shell", "/think", "/config", "/skills", "/mcp", "/goal", "/session"])
|
|
333
|
-
if (safeDuringProcessing.has(resolved0)) {
|
|
334
|
-
await handleSlash(text)
|
|
335
|
-
render()
|
|
336
|
-
} else {
|
|
337
|
-
state.queue.push({ text })
|
|
338
|
-
render()
|
|
339
|
-
}
|
|
340
|
-
return
|
|
341
|
-
}
|
|
342
353
|
await handleSlash(text)
|
|
343
354
|
render()
|
|
344
355
|
return
|
|
345
356
|
}
|
|
346
357
|
|
|
347
|
-
// While processing: queue for later, don't execute immediately.
|
|
348
|
-
// The queue panel (renderQueue) already shows pending items — don't also
|
|
349
|
-
// push to the conversation area, or the text scrolls up with streaming tokens.
|
|
350
|
-
if (state.processing) {
|
|
351
|
-
state.queue.push({ text })
|
|
352
|
-
render()
|
|
353
|
-
return
|
|
354
|
-
}
|
|
355
|
-
|
|
356
358
|
await turn(text)
|
|
357
359
|
}
|
|
358
360
|
|
package/src/tui/interaction.mjs
CHANGED
|
@@ -47,9 +47,9 @@ export function createInteraction(ctx) {
|
|
|
47
47
|
const action = String(args.action ?? "")
|
|
48
48
|
if (action === "put") return [`[${args.type ?? ""}] ${args.title ?? ""}`, ...cap(args.content ?? "", 500).split("\n")]
|
|
49
49
|
if (action === "delete") return args.id
|
|
50
|
-
? [`delete single: ${args.id} (
|
|
51
|
-
: [`batch delete
|
|
52
|
-
if (action === "clear") return [`clear personal memory`, `
|
|
50
|
+
? [`delete single: ${args.id}${args.layer ? ` (layer ${args.layer})` : ""}`]
|
|
51
|
+
: [`batch delete layer=${args.layer ?? ""} type=${args.type ?? ""} keyword=${args.keyword ?? ""}`, `confirm=${args.confirm}`]
|
|
52
|
+
if (action === "clear") return [`clear personal memory`, `layer=${args.layer ?? ""} confirm=${args.confirm}`]
|
|
53
53
|
return [cap(summarize(args), 300)]
|
|
54
54
|
}
|
|
55
55
|
return [cap(summarize(args), 300)]
|
package/src/tui/key-handler.mjs
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { ansi, C } from "./ansi.mjs"
|
|
2
2
|
import { readClipboardText, insertPastedText } from "./clipboard.mjs"
|
|
3
|
-
import { computeLayout } from "./layout.mjs"
|
|
3
|
+
import { computeLayout, inputContentWidth } from "./layout.mjs"
|
|
4
|
+
import { moveCursorVertical } from "./render.mjs"
|
|
4
5
|
import { handleSearchKey } from "./key-handler-search.mjs"
|
|
5
6
|
import { countConvLines } from "./render-conversation.mjs"
|
|
6
7
|
import { handlePermissionMode, handleQuestionMode, handleInterruptMode } from "./key-modes.mjs"
|
|
7
8
|
|
|
9
|
+
/** 第 33 批(TUI §14.3(e)——attention 清位):输入即在场——仅当原值为真时置假 + `render()`
|
|
10
|
+
* (已假则零副作用、零重绘)。键盘入口与本文件(`onKeypress`)与鼠标输入路径(index.mjs
|
|
11
|
+
* stdin data 处理器)两点共用;blocked 两态无需清位(实时派生)。
|
|
12
|
+
* @returns {boolean} 本次是否发生清位 */
|
|
13
|
+
export function clearAttention(state, render) {
|
|
14
|
+
if (!state.attentionAwaiting) return false
|
|
15
|
+
state.attentionAwaiting = false
|
|
16
|
+
render()
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
/** Current conversation max scroll offset (display lines beyond the visible panel). */
|
|
9
21
|
export function convMaxScroll(state) {
|
|
10
22
|
const d = state.dims ? state.dims.get() : {} // Single source (ConPTY instability, 2026-08-30) — cached dims
|
|
@@ -24,6 +36,9 @@ export function createKeyHandler(ctx) {
|
|
|
24
36
|
const { agent, state, render, popPicker, renderPickerLines, handleSlash, handleTab, submit, pasteClipboardImage, wizardChooseProvider, wizardSubmitText, cancelWizard, wizardProviderItems, renderWizard, pushLine, cleanup, showPicker, loadOlder } = ctx
|
|
25
37
|
|
|
26
38
|
return function onKeypress(str, key = {}) {
|
|
39
|
+
// 第 33 批(TUI §14.3(e) 键盘点):模态分派**之前**——任意按键 = 用户在场 ⇒ 清 attention
|
|
40
|
+
// 位(仅复位呈现态字段,按键语义 / 模态判定 / busy 门禁零改)。
|
|
41
|
+
clearAttention(state, render)
|
|
27
42
|
// permission confirm: y/n/a (a = approve + AUTO ON); batch (§16 D-B1): a/o/n (Esc = deny)
|
|
28
43
|
// ——模态实现 key-modes.mjs handlePermissionMode(2026-09-03 D-S4)
|
|
29
44
|
if (handlePermissionMode(str, key, { state, agent, pushLine, render })) return
|
|
@@ -59,12 +74,13 @@ export function createKeyHandler(ctx) {
|
|
|
59
74
|
state.suspended
|
|
60
75
|
if (hasStopTarget) {
|
|
61
76
|
// 当前回合平 abort(无 interrupt——命中 agent.mjs 回合收尾清池分支——全停)
|
|
62
|
-
|
|
77
|
+
// §20.3 站点 #11(第 24 批):整批 / 会话级停 = stop(reason 载荷)
|
|
78
|
+
if (state.processing && state.controller) state.controller.abort({ abortTrigger: "stop", abortDetail: "session-stop" })
|
|
63
79
|
// 会话 abort 集合 = 链条内全部 controller(含 Ctrl+I/ContinueError 重建的旧
|
|
64
80
|
// controller——children 不逃逸,round1 偏差 #3)。挂起态才标记 _suspAborted +
|
|
65
81
|
// 唤醒 driver(driver 收尾清池)——非挂起语境置位会粘滞阻塞未来挂起会话重入
|
|
66
82
|
// (round2 偏差 #1 语义)。
|
|
67
|
-
for (const c of agent._sessionAbortAll ?? (agent._sessionAbort ? [agent._sessionAbort] : [])) c?.abort()
|
|
83
|
+
for (const c of agent._sessionAbortAll ?? (agent._sessionAbort ? [agent._sessionAbort] : [])) c?.abort({ abortTrigger: "stop", abortDetail: "session-stop" })
|
|
68
84
|
if (state.suspended) {
|
|
69
85
|
state._suspAborted = true
|
|
70
86
|
state._suspWake?.()
|
|
@@ -159,7 +175,8 @@ export function createKeyHandler(ctx) {
|
|
|
159
175
|
// Ctrl+I (or Tab during processing): interrupt and inject a message
|
|
160
176
|
if ((key.ctrl && !key.alt && key.name === "i") || (key.name === "tab" && state.processing && !state.interruptPrompt)) {
|
|
161
177
|
if (state.processing && state.controller && !state.interruptPrompt) {
|
|
162
|
-
|
|
178
|
+
// 注入框状态模型(第 31 批——TUI-INPUT-BOX.md §1 不变量 9):{ chars, cursor },空态 = { chars: [], cursor: 0 }
|
|
179
|
+
state.interruptPrompt = { chars: [], cursor: 0 }
|
|
163
180
|
render()
|
|
164
181
|
}
|
|
165
182
|
return
|
|
@@ -263,17 +280,19 @@ export function createKeyHandler(ctx) {
|
|
|
263
280
|
}
|
|
264
281
|
|
|
265
282
|
if (state.processing) {
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
283
|
+
// busy 门禁(INPUT-LOCK C'——2026-09-09 + INPUT-LOCK-BEHAVIOR-REVISED——2026-09-09):
|
|
284
|
+
// processing(含 digest——单一判据)输入不禁——吞提交不吞字符(打字照常回显);Enter
|
|
285
|
+
// 提交吞 + busy 提示——斜杠同禁发(白名单已删——/exit 也发不出——退出靠 Ctrl+C 终端
|
|
286
|
+
// 层通道——门禁前不误伤);空 Enter 静默(text 非空才吞);多行换行
|
|
287
|
+
// (meta/enter——编辑)照常放行。**第 31 批**:↑↓ 不再在此全屏蔽——分流下沉到下方
|
|
288
|
+
// 三规则块(竖移放行 / 历史导航禁;TUI-INPUT-BOX.md §3);Tab 仍吞(死条件在案——原样保留)。
|
|
289
|
+
if (key.name === "tab") return
|
|
290
|
+
const isSend = (key.name === "return" && !key.meta) || (str === "\r" && !key.meta)
|
|
291
|
+
if (isSend && state.input.join("").trim()) {
|
|
292
|
+
pushLine(`[主会话处理中 —— 消息未发送(回合结束后请重按 Enter)]`, C.warn)
|
|
293
|
+
render()
|
|
274
294
|
return
|
|
275
295
|
}
|
|
276
|
-
// remaining printable characters go into input box normally
|
|
277
296
|
}
|
|
278
297
|
|
|
279
298
|
// Tab: slash-command completion (cycle candidates); other input ignored (\t would blow up input box, never inserted directly)
|
|
@@ -291,7 +310,24 @@ export function createKeyHandler(ctx) {
|
|
|
291
310
|
return
|
|
292
311
|
}
|
|
293
312
|
|
|
294
|
-
//
|
|
313
|
+
// ↑/↓ 三规则(TUI-INPUT-BOX.md §3——第 31 批):① 翻历史中恒历史(多行条目不竖移)
|
|
314
|
+
// ② 竖移优先(可视行口径——折行与 \n 同权;显示列保持 + 短行端钳制)③ 边界回落历史(↑)
|
|
315
|
+
// /无动作(↓)。processing 期:竖移放行(纯编辑——与字符/退格/←→ 同权)、历史导航禁
|
|
316
|
+
// (规则 ①/③ 的历史分支吞——不进入、不切换)。
|
|
317
|
+
if (key.name === "up" || key.name === "down") {
|
|
318
|
+
if (state.historyIndex === -1) { // 规则 ① 优先——翻历史中不竖移
|
|
319
|
+
const cols = (state.dims?.get() ?? {}).cols ?? (process.stdout.columns || 80)
|
|
320
|
+
const next = moveCursorVertical(state.input, state.cursor, inputContentWidth(cols), key.name)
|
|
321
|
+
if (next !== null) {
|
|
322
|
+
state.cursor = next // 规则 ②:竖移(草稿/历史指针零涉——D-31.6)
|
|
323
|
+
render()
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (state.processing) return // 规则 ①/③ 的历史分支吞——processing 期历史导航禁
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// input history(规则 ① 恒历史 + 规则 ③ 边界回落——历史语义零改)
|
|
295
331
|
if (key.name === "up") {
|
|
296
332
|
if (state.history.length) {
|
|
297
333
|
// Save current input as draft when entering history mode (historyIndex === -1).
|
|
@@ -372,15 +408,23 @@ export function createKeyHandler(ctx) {
|
|
|
372
408
|
render()
|
|
373
409
|
} else {
|
|
374
410
|
const text = state.input.join("").trim()
|
|
375
|
-
// §17 D-S5/F3/F7 + 偏差 #1
|
|
376
|
-
//
|
|
411
|
+
// §17 D-S5/F3/F7 + 偏差 #1 + INPUT-LOCK 单槽化(F-6——2026-09-09):挂起态
|
|
412
|
+
// (suspended 或释放窗口 _suspPending)Enter = 新回合输入(非打断)——填
|
|
413
|
+
// pendingInput 单槽(至多一条待交接——busy 提交吞在 L265 门禁先拦,digest 期
|
|
414
|
+
// 不会到这——仅挂起空闲/释放窗口触达)由挂起会话调度;输入框零干扰(F3)。
|
|
415
|
+
// 槽满(竞态防御——不覆盖不丢失)→ 吞 + 提示,文本保留在输入框。
|
|
377
416
|
if ((state.suspended || state._suspPending) && text && !text.startsWith("/")) {
|
|
417
|
+
state.pendingInput ??= []
|
|
418
|
+
if (state.pendingInput.length > 0) {
|
|
419
|
+
pushLine(`[主会话处理中 —— 已有一条消息待发送,请等其处理完成后再发送]`, C.warn)
|
|
420
|
+
render()
|
|
421
|
+
return
|
|
422
|
+
}
|
|
378
423
|
state.input = []
|
|
379
424
|
state.cursor = 0
|
|
380
425
|
state.history.push(text)
|
|
381
426
|
state.historyIndex = -1
|
|
382
427
|
state._draft = null
|
|
383
|
-
state.pendingInput ??= []
|
|
384
428
|
state.pendingInput.push(text)
|
|
385
429
|
state._suspWake?.()
|
|
386
430
|
render()
|
package/src/tui/key-modes.mjs
CHANGED
|
@@ -8,6 +8,31 @@
|
|
|
8
8
|
import { C } from "./ansi.mjs"
|
|
9
9
|
import { readClipboardText, insertPastedText } from "./clipboard.mjs"
|
|
10
10
|
import { QUESTION_CUSTOM } from "./interaction.mjs"
|
|
11
|
+
import { inputContentWidth } from "./layout.mjs"
|
|
12
|
+
import { moveCursorVertical } from "./render.mjs"
|
|
13
|
+
|
|
14
|
+
/** SYNC-CANCEL v2 模态 deny 解绕(用户裁——2026-09-09):⏹ 定向中止 sync child 时
|
|
15
|
+
* 顺带 deny 其 pending 权限/continue 模态——ask 不观 signal(R1——targeted abort 后
|
|
16
|
+
* child 停在模态则 runChildPipeline 不返回、父回合阻塞至模态回答)。owner 判定:
|
|
17
|
+
* 工具权限 ask name 前缀 `${key}/`(subagent-spawn.mjs 装配——owner key 标识);continue
|
|
18
|
+
* ask args.agent === key(subagent.mjs askSubagentContinue 既有参数)。副作用镜像
|
|
19
|
+
* handlePermissionMode deny 分支(清除模态 + deny 轨迹 + resolve(false)——子代理随即
|
|
20
|
+
* 在 abort 检出点解绕折叠)。返回 true = 本模态属该 child 且已 deny 解绕。鼠标 ⏹ 路径
|
|
21
|
+
* (mouse.mjs cancelSubagent)调用——key-modes 是权限模态归属语义的驻点。 */
|
|
22
|
+
export function denyModalForOwner(state, key, { pushLine, render } = {}) {
|
|
23
|
+
const p = state?.permission
|
|
24
|
+
if (!p) return false
|
|
25
|
+
const owned = (typeof p.name === "string" && p.name.startsWith(`${key}/`)) || p.args?.agent === key
|
|
26
|
+
if (!owned) return false
|
|
27
|
+
const { resolve, name } = p
|
|
28
|
+
state.permission = null
|
|
29
|
+
state.permissionPreview = []
|
|
30
|
+
state.status = "Processing..."
|
|
31
|
+
pushLine?.(` [denied] ${name}`, C.error)
|
|
32
|
+
resolve(false)
|
|
33
|
+
render?.()
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
11
36
|
|
|
12
37
|
/** permission 确认模态:y/n/a(a = approve + AUTO ON);batch(§16 D-B1):a/o/n(Esc = deny)。
|
|
13
38
|
* consume:valid 键或 Esc 走 resolve 分支;其余键静默吞掉(模态独占)。 */
|
|
@@ -68,10 +93,11 @@ export function handleQuestionMode(str, key, ctx) {
|
|
|
68
93
|
state.status = "Processing..."
|
|
69
94
|
render()
|
|
70
95
|
} else if (key.name === "up") {
|
|
71
|
-
|
|
96
|
+
// A4(第 20 批 §12.4 契约——同义键同形):↑↓ 环绕(原钳位——对齐 picker/wizard 既有语义)
|
|
97
|
+
q.selected = ((q.selected ?? 0) - 1 + q.options.length) % q.options.length
|
|
72
98
|
render()
|
|
73
99
|
} else if (key.name === "down") {
|
|
74
|
-
q.selected =
|
|
100
|
+
q.selected = ((q.selected ?? 0) + 1) % q.options.length
|
|
75
101
|
render()
|
|
76
102
|
} else if (key.name === "return") {
|
|
77
103
|
const answer = q.options[q.selected ?? 0]
|
|
@@ -182,16 +208,21 @@ export function handleQuestionMode(str, key, ctx) {
|
|
|
182
208
|
}
|
|
183
209
|
|
|
184
210
|
/** interruptPrompt 模态(Ctrl+I 后输入注入消息):Enter 提交 → controller.abort
|
|
185
|
-
* { interrupt: true, message };Esc
|
|
211
|
+
* { interrupt: true, message };Esc 取消;编辑键 = §7.2 最小集 + 四方向键(第 31 批:
|
|
212
|
+
* `{ chars, cursor }` codepoint 模型——TUI-INPUT-BOX.md §8.1);其余键静默吞(模态独占)。
|
|
186
213
|
* 返回 false 当模态未激活(key-handler 先检 state.interruptPrompt 入口再调)。 */
|
|
187
214
|
export function handleInterruptMode(str, key, ctx) {
|
|
188
215
|
const { state, pushLine, render } = ctx
|
|
189
216
|
if (!state.interruptPrompt) return false
|
|
217
|
+
const p = state.interruptPrompt
|
|
218
|
+
if (!Array.isArray(p.chars)) p.chars = p.chars ? [...p.chars] : [] // 形态防御(同 question 自由文本态)
|
|
219
|
+
p.cursor = Math.max(0, Math.min(p.cursor ?? p.chars.length, p.chars.length))
|
|
220
|
+
const len = p.chars.length
|
|
190
221
|
if (key.name === "escape") {
|
|
191
222
|
state.interruptPrompt = null
|
|
192
223
|
render()
|
|
193
224
|
} else if (key.name === "return") {
|
|
194
|
-
const msg =
|
|
225
|
+
const msg = p.chars.join("").trim()
|
|
195
226
|
state.interruptPrompt = null
|
|
196
227
|
if (msg) {
|
|
197
228
|
// Guard: if the turn already finished while the user was typing, the controller
|
|
@@ -204,12 +235,59 @@ export function handleInterruptMode(str, key, ctx) {
|
|
|
204
235
|
}
|
|
205
236
|
render()
|
|
206
237
|
}
|
|
207
|
-
} else if (key.name === "
|
|
208
|
-
|
|
238
|
+
} else if (key.name === "enter") {
|
|
239
|
+
// Ctrl+J(\n → name "enter")= no-op 吞——单行不变式:不插换行(§8.1)
|
|
240
|
+
} else if (key.name === "left") {
|
|
241
|
+
p.cursor = Math.max(0, p.cursor - 1)
|
|
242
|
+
render()
|
|
243
|
+
} else if (key.name === "right") {
|
|
244
|
+
p.cursor = Math.min(len, p.cursor + 1)
|
|
245
|
+
render()
|
|
246
|
+
} else if (key.name === "up" || key.name === "down") {
|
|
247
|
+
// 折行竖移(§8.1):显示列保持 + 短行端钳制;无邻行 = 吞(零副作用——无历史回落)
|
|
248
|
+
const cols = (state.dims?.get() ?? {}).cols ?? (process.stdout.columns || 80)
|
|
249
|
+
const next = moveCursorVertical(p.chars, p.cursor, inputContentWidth(cols), key.name)
|
|
250
|
+
if (next !== null) {
|
|
251
|
+
p.cursor = next
|
|
252
|
+
render()
|
|
253
|
+
}
|
|
254
|
+
} else if (key.name === "home") {
|
|
255
|
+
p.cursor = 0
|
|
209
256
|
render()
|
|
210
|
-
} else if (
|
|
211
|
-
|
|
257
|
+
} else if (key.name === "end") {
|
|
258
|
+
p.cursor = len
|
|
212
259
|
render()
|
|
260
|
+
} else if (key.ctrl && !key.alt && key.name === "u") {
|
|
261
|
+
p.chars = []
|
|
262
|
+
p.cursor = 0
|
|
263
|
+
render()
|
|
264
|
+
} else if (key.name === "backspace") {
|
|
265
|
+
if (p.cursor > 0) {
|
|
266
|
+
p.chars.splice(p.cursor - 1, 1) // codepoint 元素删除——emoji 不劈半
|
|
267
|
+
p.cursor--
|
|
268
|
+
render()
|
|
269
|
+
}
|
|
270
|
+
} else if (key.ctrl && !key.alt && !key.meta && key.name === "v") {
|
|
271
|
+
// Ctrl+V 粘贴:落 cursor 位置(\n/\r 去除、\t → 2 空格——insertPastedText 注入框分支)。
|
|
272
|
+
// 异步读剪贴板期间框可能已 Esc/Enter 关闭 → 回调守卫比对同一引用,防落主输入框
|
|
273
|
+
// (同 question 态 stale-paste 守卫语义;不加守卫会静默落 state.input)。
|
|
274
|
+
const target = p
|
|
275
|
+
readClipboardText().then((text) => {
|
|
276
|
+
if (text && state.interruptPrompt === target) {
|
|
277
|
+
insertPastedText(state, text)
|
|
278
|
+
render()
|
|
279
|
+
}
|
|
280
|
+
}).catch((e) => console.error(`[tui] clipboard paste failed: ${e.message}`))
|
|
281
|
+
} else if (str && !key.ctrl && !key.meta && key.name !== "tab" && key.name !== "enter") {
|
|
282
|
+
// 可打印字符插入光标位置(codepoint 拆字);\r\n 剥离 + \t→2 空格(单行不变式)
|
|
283
|
+
const chars = [...str.replace(/[\r\n]+/g, "").replace(/\t/g, " ")]
|
|
284
|
+
if (chars.length) {
|
|
285
|
+
p.chars.splice(p.cursor, 0, ...chars)
|
|
286
|
+
p.cursor += chars.length
|
|
287
|
+
render()
|
|
288
|
+
}
|
|
213
289
|
}
|
|
290
|
+
// 未列键(Delete/PgUp/PgDn/F1/…)一律 consume return——无 fall-through(模态独占;§8.1:
|
|
291
|
+
// Delete 不引入——与 §7.2 最小集对齐)
|
|
214
292
|
return true
|
|
215
293
|
}
|
package/src/tui/layout.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Computes position and height of each panel from state + terminal dimensions.
|
|
4
4
|
* Does not modify state — side effects are performed by the caller before rendering.
|
|
5
5
|
*
|
|
6
|
-
* header → conversation → subagent 面板 → todo → picker → permission →
|
|
6
|
+
* header → conversation → subagent 面板 → todo → picker → permission → input → status
|
|
7
7
|
* Running subagent activity renders in a FIXED bottom panel between the
|
|
8
8
|
* conversation and the todo panel (AGENT-LOOP.md §7.2.1) — full adaptive height
|
|
9
9
|
* (the conversation shrinks); compressed away first on small terminals (to 0 =
|
|
@@ -26,6 +26,13 @@ const MAX_INPUT_LINES = 5
|
|
|
26
26
|
const MAX_TASK_LINES = 5
|
|
27
27
|
const QWIN = 5
|
|
28
28
|
|
|
29
|
+
/** 输入框内容宽度单源(第 31 批——TUI-INPUT-BOX.md §9.3 #3):`computeLayout` 的输入框
|
|
30
|
+
* layoutInput 宽度与键处理侧竖移(`moveCursorVertical`)共用——布局与竖移零漂移。
|
|
31
|
+
* 含 2 列行前缀(`▸ ` / 续行 2 空格);layoutInput 内部再为前缀预留 2 列。 */
|
|
32
|
+
export function inputContentWidth(cols) {
|
|
33
|
+
return Math.max(20, cols - 1) - 4
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
/** question 自由文本态渲染布局:与主输入框同实现(TUI-INPUT-BOX.md §7.2 round2 #1/#6)——
|
|
30
37
|
* 折行展开(`▸ ` 首行 + 续行 2 空格缩进)+ 光标行列;answer 单行不变式(无 \n 字符)由
|
|
31
38
|
* 输入侧硬守卫(Ctrl+J no-op / 粘贴 \n→空格),此处只复用 layoutInput 的折行/光标语义,
|
|
@@ -40,11 +47,14 @@ export function layoutAnswer(chars, cursor, width) {
|
|
|
40
47
|
*/
|
|
41
48
|
export function computeLayout(state, { cols, rows }) {
|
|
42
49
|
const W = Math.max(20, cols - 1)
|
|
50
|
+
const inputW = inputContentWidth(cols) // 输入框内容宽(= W - 4)——单源:布局与竖移共用
|
|
43
51
|
|
|
44
52
|
// --- input box ---
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
53
|
+
// Ctrl+I 注入框(第 31 批):{ chars, cursor } codepoint 数组 + 光标——与主输入框同渲染路径(§8)。
|
|
54
|
+
const ip = state.interruptPrompt
|
|
55
|
+
const inputBuf = state.search ? [...state.search.query] : (ip ? ip.chars : state.input)
|
|
56
|
+
const inputCursor = state.search ? inputBuf.length : (ip ? Math.max(0, Math.min(ip.cursor ?? inputBuf.length, inputBuf.length)) : state.cursor)
|
|
57
|
+
const inputLayout = layoutInput(inputBuf, inputCursor, inputW)
|
|
48
58
|
let inputOffset = 0
|
|
49
59
|
if (inputLayout.lines.length > MAX_INPUT_LINES) {
|
|
50
60
|
inputOffset = Math.min(inputLayout.cursorLine, inputLayout.lines.length - MAX_INPUT_LINES)
|
|
@@ -66,7 +76,7 @@ export function computeLayout(state, { cols, rows }) {
|
|
|
66
76
|
} else {
|
|
67
77
|
const answerChars = Array.isArray(q.answer) ? q.answer : q.answer ? [...q.answer] : []
|
|
68
78
|
const qCursor = Math.max(0, Math.min(q.cursor ?? answerChars.length, answerChars.length))
|
|
69
|
-
questionLayout = layoutAnswer(answerChars, qCursor,
|
|
79
|
+
questionLayout = layoutAnswer(answerChars, qCursor, inputW)
|
|
70
80
|
if (questionLayout.lines.length > MAX_INPUT_LINES) {
|
|
71
81
|
questionOffset = Math.min(questionLayout.cursorLine, questionLayout.lines.length - MAX_INPUT_LINES)
|
|
72
82
|
}
|
|
@@ -127,11 +137,10 @@ export function computeLayout(state, { cols, rows }) {
|
|
|
127
137
|
permPreviewH = 1 + permPreviewLines.length // 1 for header line
|
|
128
138
|
}
|
|
129
139
|
|
|
130
|
-
// Queue
|
|
131
|
-
const queueH = state.queue.length > 0 && state.processing ? 1 : 0
|
|
140
|
+
// Queue 面板已撤(INPUT-LOCK-ASYNC F-7——2026-09-09——busy 提交吞——无排队展示)
|
|
132
141
|
|
|
133
142
|
// --- elastic panel: conversation takes remaining space ---
|
|
134
|
-
const fixedH = headerH + inputBoxH + statusH + pickerH + taskPanelH + permPreviewH +
|
|
143
|
+
const fixedH = headerH + inputBoxH + statusH + pickerH + taskPanelH + permPreviewH + subagentH
|
|
135
144
|
let convH = Math.max(1, rows - fixedH)
|
|
136
145
|
|
|
137
146
|
// 小终端高度补偿:subagent 面板最先让位(可至 0 隐藏,活动仍进缓冲区不丢),
|
|
@@ -183,13 +192,12 @@ export function computeLayout(state, { cols, rows }) {
|
|
|
183
192
|
const todo = todoFinalH > 0 ? { y, h: todoFinalH } : null; y += todoFinalH
|
|
184
193
|
const picker = pickerFinalH > 0 ? { y, h: pickerFinalH } : null; y += pickerFinalH
|
|
185
194
|
const permission = permFinalH > 0 ? { y, h: permFinalH } : null; y += permFinalH
|
|
186
|
-
const queue = queueH > 0 ? { y, h: queueH } : null; y += queueH
|
|
187
195
|
const inputBox = { y, h: inputBoxH }; y += inputBoxH
|
|
188
196
|
const status = { y, h: statusH }
|
|
189
197
|
|
|
190
198
|
return {
|
|
191
199
|
W, cols, rows,
|
|
192
|
-
panels: { header, conversation, subagent, picker, todo, permission,
|
|
200
|
+
panels: { header, conversation, subagent, picker, todo, permission, inputBox, status },
|
|
193
201
|
// precomputed content (affects height, reused during render)
|
|
194
202
|
inputLayout,
|
|
195
203
|
inputOffset,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-catalog.mjs — 会话级模型清单 helper(PROVIDER.md §16.2 M2/M9——2026-09-10 MODEL-SELECTION)。
|
|
3
|
+
*
|
|
4
|
+
* 清单唯一权威 = provider 运行期拉取(`GET /models`——provider/list-models.mjs 按 format 分派)。
|
|
5
|
+
* 本模块提供:
|
|
6
|
+
* - `getProviderModels(providerConfig)`:拉取 + **会话级缓存**(TTL 60s;失败不缓存——下次重试);
|
|
7
|
+
* - `probeChannelModels(providerConfig)`:配置阶段准入探(M9——探通入缓存候选直接可用 / 探不通返回
|
|
8
|
+
* 明示文案;调用方决定标注与「不入可选来源」——绝不阻断保存);
|
|
9
|
+
* - `modelListFailureText(error)`:M8 失败消息本体(逐字长句——界面明示的唯一断言对象);
|
|
10
|
+
* - `dedupeModels` / `modelSeries`:显示归并(自 model-picker.mjs 迁入——会话面与槽位面共用)。
|
|
11
|
+
*
|
|
12
|
+
* 缓存时钟可注入(`_catalogHooks.now`——先例 rate.mjs `_rateHooks`):测试假时钟确定性断言 T6 不依赖壁钟。
|
|
13
|
+
*/
|
|
14
|
+
import { listModels } from "../provider/list-models.mjs"
|
|
15
|
+
|
|
16
|
+
/** 会话级缓存 TTL(§16.4 a2:连续操作不重复付网络;60s 新鲜窗口)。 */
|
|
17
|
+
const CACHE_TTL_MS = 60_000
|
|
18
|
+
|
|
19
|
+
/** 测试钩子(先例 `rate.mjs` `_rateHooks`):时钟可注入——T6 假时钟断言。 */
|
|
20
|
+
export const _catalogHooks = { now: () => Date.now() }
|
|
21
|
+
|
|
22
|
+
/** 会话内缓存:key = name + baseURL(同渠道不同端点各自成条);value = { models, at }。 */
|
|
23
|
+
const _cache = new Map()
|
|
24
|
+
|
|
25
|
+
/** 测试缝:清空会话缓存(跨用例隔离——生产无调用点)。 */
|
|
26
|
+
export function _clearModelCatalogCache() { _cache.clear() }
|
|
27
|
+
|
|
28
|
+
function cacheKey(providerConfig) {
|
|
29
|
+
return `${providerConfig?.name ?? ""}\u0000${providerConfig?.baseURL ?? ""}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 拉取渠道模型清单(会话缓存 TTL 60s)。命中缓存(TTL 内)直接返回,否则走 M1 拉取;
|
|
34
|
+
* **失败不缓存**(下次调用重试——失败态由调用方降级展示)。
|
|
35
|
+
* `fresh: true` 跳过缓存读(仍写回——M9 准入探必须真发一次请求:换 key 不得被旧 key 的缓存短路)。
|
|
36
|
+
* 抛错契约与 listModels 一致。
|
|
37
|
+
*/
|
|
38
|
+
export async function getProviderModels(providerConfig, { signal, fresh = false } = {}) {
|
|
39
|
+
const key = cacheKey(providerConfig)
|
|
40
|
+
if (!fresh) {
|
|
41
|
+
const hit = _cache.get(key)
|
|
42
|
+
if (hit && _catalogHooks.now() - hit.at < CACHE_TTL_MS) return hit.models
|
|
43
|
+
}
|
|
44
|
+
const models = await listModels(providerConfig, { signal })
|
|
45
|
+
_cache.set(key, { models, at: _catalogHooks.now() })
|
|
46
|
+
return models
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** M8 失败消息本体(逐字长句——状态 = HTTP 状态码或网络错误摘要;无绕过指引)。
|
|
50
|
+
* 分工:消息本体 = 本长句;状态标签 = `不可用`(列表行内短标)。 */
|
|
51
|
+
export function modelListFailureText(error) {
|
|
52
|
+
const status = /GET \/models failed (\d+)/.exec(error?.message ?? "")?.[1]
|
|
53
|
+
?? String(error?.message ?? error)
|
|
54
|
+
return `该渠道不提供模型列表(GET /models ${status})——无法选择模型,请改用其他渠道`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* M9 配置阶段准入探:对目标渠道探一次 `GET /models`(复用 M1 + 既有超时;**fresh——真发一次
|
|
59
|
+
* 请求**(换 key 不被旧缓存短路);探通入会话缓存——该流内候选直接可用)。
|
|
60
|
+
* 返回 `{ ok:true, list }` | `{ ok:false, message }`——**不抛错**(探不通不是异常:调用方标注
|
|
61
|
+
* 不可用 + 明示原因,配置流不阻断)。
|
|
62
|
+
*/
|
|
63
|
+
export async function probeChannelModels(providerConfig) {
|
|
64
|
+
try {
|
|
65
|
+
return { ok: true, list: await getProviderModels(providerConfig, { fresh: true }) }
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return { ok: false, message: modelListFailureText(error) }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Strip known version/date suffixes to get the "series" name of a model.
|
|
72
|
+
* e.g. "qwen-max-latest" → "qwen-max", "qwen-max-2024-09-19" → "qwen-max" */
|
|
73
|
+
export function modelSeries(name) {
|
|
74
|
+
return name
|
|
75
|
+
.replace(/-latest$/, "")
|
|
76
|
+
.replace(/-\d{4}-\d{2}-\d{2}$/, "") // date suffix like -2024-09-19
|
|
77
|
+
.replace(/-\d{8}$/, "") // date suffix like -20240919
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Dedupe model list: group by series, keep shortest name per group. */
|
|
81
|
+
export function dedupeModels(models) {
|
|
82
|
+
const groups = new Map()
|
|
83
|
+
for (const m of models) {
|
|
84
|
+
const series = modelSeries(m)
|
|
85
|
+
const existing = groups.get(series)
|
|
86
|
+
if (!existing || m.length < existing.length) groups.set(series, m)
|
|
87
|
+
}
|
|
88
|
+
return [...groups.values()].sort()
|
|
89
|
+
}
|