thincoder 0.12.67 → 0.12.69
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 +32 -0
- package/README.md +21 -5
- package/bin/thincoder.mjs +41 -344
- package/package.json +2 -2
- package/src/acp/bridge.mjs +5 -2
- package/src/acp/handlers-session.mjs +12 -1
- package/src/cli/make-agent.mjs +44 -119
- package/src/cli/memory-command.mjs +45 -3
- package/src/command-interactive.mjs +196 -0
- package/src/command-table.mjs +185 -0
- package/src/completions.mjs +36 -12
- package/src/crash-reports.mjs +42 -20
- package/src/distill.mjs +3 -3
- package/src/heap-watch.mjs +5 -14
- package/src/tui/agent-turn.mjs +88 -50
- package/src/tui/clipboard.mjs +74 -30
- package/src/tui/cmd-advisor.mjs +39 -13
- package/src/tui/cmd-clear.mjs +2 -2
- package/src/tui/cmd-config.mjs +17 -1
- package/src/tui/cmd-eng.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +1 -1
- package/src/tui/cmd-new.mjs +2 -2
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-session.mjs +13 -5
- package/src/tui/cmd-think.mjs +35 -17
- package/src/tui/cmd-timers.mjs +33 -0
- package/src/tui/conversation-writer.mjs +2 -2
- package/src/tui/display-budget.mjs +9 -9
- package/src/tui/fold-block.mjs +1 -1
- package/src/tui/index.mjs +54 -18
- package/src/tui/interaction.mjs +4 -2
- package/src/tui/key-handler-ctrlc.mjs +1 -1
- package/src/tui/key-handler-search.mjs +1 -1
- package/src/tui/key-handler.mjs +1 -1
- package/src/tui/key-modes.mjs +1 -1
- package/src/tui/layout.mjs +5 -5
- package/src/tui/lifecycle-records.mjs +199 -0
- package/src/tui/model-picker.mjs +16 -199
- package/src/tui/mouse.mjs +3 -3
- package/src/tui/pickers.mjs +6 -2
- package/src/tui/provider-admin.mjs +213 -0
- package/src/tui/queued-merge.mjs +4 -60
- package/src/tui/queued-pickup.mjs +3 -3
- package/src/tui/render-conversation.mjs +13 -4
- package/src/tui/render-frame.mjs +43 -6
- package/src/tui/render-loop.mjs +2 -3
- package/src/tui/render-segments.mjs +2 -2
- package/src/tui/render.mjs +1 -1
- package/src/tui/slash-commands.mjs +3 -0
- package/src/tui/startup.mjs +28 -3
- package/src/tui/subagent-blocks.mjs +27 -19
- package/src/tui/subagent-children.mjs +5 -5
- package/src/tui/subagent-freeze.mjs +39 -37
- package/src/tui/subagent-panel.mjs +7 -7
- package/src/tui/suspension-drive.mjs +172 -259
- package/src/tui/timer-watch.mjs +64 -0
- package/src/tui/tool-args.mjs +10 -71
- package/src/tui/tool-display.mjs +6 -6
- package/src/tui/tool-events.mjs +33 -28
- package/src/tui/tui-lifecycle.mjs +13 -4
- package/src/tui/tui-state.mjs +3 -2
- package/src/tui/turn-face.mjs +4 -1
- package/src/tui/wizard.mjs +7 -2
- package/src/tui/wrapped-spawn.mjs +3 -3
package/src/tui/model-picker.mjs
CHANGED
|
@@ -7,20 +7,24 @@
|
|
|
7
7
|
* 拉到的行直接可选);显式 `provider:model` 一律放行(M4——仅[空值/裸值/未知 provider]无效)。
|
|
8
8
|
* 切换成功回显规格来源(M6);渠道默认模型 = `providers[].model` 单值(M3——显示回退读取)。
|
|
9
9
|
* 配置写入面(加渠道 / 设 key)探一次 `/models`(M9——探不通标「不可用」+ 明示原因,不阻断保存)。
|
|
10
|
-
* 渠道管理流(add/remove/key/context)是 provider 级 config
|
|
11
|
-
* removeProviderFlow
|
|
10
|
+
* 渠道管理流(add/remove/key/context)是 provider 级 config 写——语义不变;2026-09-29 结构拆分起
|
|
11
|
+
* 四流居 provider-admin.mjs(本档装配 ∥ 尾同名再出口)。F-4 (ISSUE-FIX-BATCH):removeProviderFlow
|
|
12
|
+
* 级联清理悬挂引用(cascadeRemoveProvider——定义同在 provider-admin.mjs)。
|
|
12
13
|
* ctx: { agent, state, render, ansi, C, pushLine, persistRaw, askQuestion, maskKey, showPicker,
|
|
13
14
|
* closePicker, renderPickerLines, confirmDelete }(后四者为 pickers.mjs 通用 picker 绑定——闭包入参,环安全)。
|
|
14
15
|
*/
|
|
15
16
|
import { sliceByWidth } from "./render.mjs"
|
|
16
|
-
import {
|
|
17
|
+
import { providerSpec, specMatch } from "@thincoder/core/config.mjs"
|
|
17
18
|
import { saveSession } from "@thincoder/core/session.mjs"
|
|
18
|
-
import { getProviderModels,
|
|
19
|
+
import { getProviderModels, modelListFailureText, dedupeModels } from "./model-catalog.mjs"
|
|
20
|
+
import { createProviderAdmin } from "./provider-admin.mjs"
|
|
19
21
|
|
|
20
22
|
/** createModelPicker(ctx) → { openModelPicker, selectModel, setProviderKey, setContextFlow, pickModelForSlot } */
|
|
21
23
|
export function createModelPicker(ctx) {
|
|
22
24
|
const { agent, state, pushLine, persistRaw, askQuestion, maskKey, showPicker, closePicker, renderPickerLines, confirmDelete = () => { throw new Error("ctx.confirmDelete missing — deletion refused (fail-closed)") } } = ctx
|
|
23
25
|
const { ansi, C } = ctx
|
|
26
|
+
// 渠道管理四流 + 级联清理(provider-admin.mjs——structure-split-2 迁出:本档装配 ∥ 尾同名再出口)
|
|
27
|
+
const admin = createProviderAdmin({ agent, pushLine, persistRaw, askQuestion, maskKey, showPicker, confirmDelete, C, fmtContextK, defaultModelLabel })
|
|
24
28
|
|
|
25
29
|
/** entry 唯一标识:异步更新 entries 后按它恢复选中项 */
|
|
26
30
|
function entryKey(e) {
|
|
@@ -68,13 +72,13 @@ export function createModelPicker(ctx) {
|
|
|
68
72
|
if (modelSelected) return // model selected, close picker
|
|
69
73
|
// Esc from model list → return to provider list
|
|
70
74
|
} else if (e.action === "add") {
|
|
71
|
-
await addProviderFlow()
|
|
75
|
+
await admin.addProviderFlow()
|
|
72
76
|
} else if (e.action === "remove") {
|
|
73
|
-
await removeProviderFlow()
|
|
77
|
+
await admin.removeProviderFlow()
|
|
74
78
|
} else if (e.action === "key") {
|
|
75
|
-
await setKeyFlow()
|
|
79
|
+
await admin.setKeyFlow()
|
|
76
80
|
} else if (e.action === "context") {
|
|
77
|
-
await setContextFlow()
|
|
81
|
+
await admin.setContextFlow()
|
|
78
82
|
}
|
|
79
83
|
}
|
|
80
84
|
}
|
|
@@ -113,7 +117,7 @@ export function createModelPicker(ctx) {
|
|
|
113
117
|
const shown = active ? (agent.activeModel ?? defaultModelLabel(p)) : defaultModelLabel(p)
|
|
114
118
|
const marker = active ? "●" : ""
|
|
115
119
|
// A1(第 20 批 §12.5):渠道警示((no key) / (不可用))上移 text——与既有状态标
|
|
116
|
-
// ((ctx …) / ← session)同簇(先例 =
|
|
120
|
+
// ((ctx …) / ← session)同簇(先例 = provider-admin.mjs setKeyFlow / wizard 的 (added, no key)):
|
|
117
121
|
// 警示不再位于最先牺牲段。note 收窄为 baseURL(补充信息——超宽随行右截断可接受)。
|
|
118
122
|
const sessionNote = active ? " ← session" : ""
|
|
119
123
|
const keyStatus = p.apiKey ? "" : " (no key)"
|
|
@@ -247,7 +251,7 @@ export function createModelPicker(ctx) {
|
|
|
247
251
|
)
|
|
248
252
|
if (!agent.provider.apiKey) {
|
|
249
253
|
const selKey = await askQuestion(`Enter API key for ${item.provider} (leave empty to skip):`)
|
|
250
|
-
if (selKey) await setProviderKey(item.provider, selKey)
|
|
254
|
+
if (selKey) await admin.setProviderKey(item.provider, selKey)
|
|
251
255
|
}
|
|
252
256
|
}
|
|
253
257
|
|
|
@@ -306,194 +310,7 @@ export function createModelPicker(ctx) {
|
|
|
306
310
|
renderPickerLines()
|
|
307
311
|
}
|
|
308
312
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
/** M9 配置阶段准入探:探通 → 清标记;探不通 → 会话内存标「不可用」+ 明示原因(绝不落 config;
|
|
312
|
-
* 不阻断保存——条目已保存,仅标注)。 */
|
|
313
|
-
async function probeChannelFlow(cfg, label) {
|
|
314
|
-
const r = await probeChannelModels(cfg)
|
|
315
|
-
if (r.ok) {
|
|
316
|
-
delete cfg._unavailable
|
|
317
|
-
pushLine(`${label}: /models 可用(${r.list.length} 个模型可候选)`, C.tool)
|
|
318
|
-
} else {
|
|
319
|
-
cfg._unavailable = true
|
|
320
|
-
pushLine(`${label} 不可用 — ${r.message}`, C.error)
|
|
321
|
-
}
|
|
322
|
-
return r
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
async function addProviderFlow() {
|
|
326
|
-
const entries = [
|
|
327
|
-
{ type: "header", text: "Select a preset provider" },
|
|
328
|
-
...Object.entries(PRESETS).filter(([name]) => !agent.providers.some((p) => p.name === name))
|
|
329
|
-
.map(([name, p]) => ({ type: "item", text: `${name.padEnd(10)} ${p.desc ?? ""} (${p.model ?? ""})`, name, kind: "preset" })),
|
|
330
|
-
{ type: "header", text: "Other" },
|
|
331
|
-
{ type: "item", text: "Custom (manual config)", name: "__custom__", kind: "custom" },
|
|
332
|
-
]
|
|
333
|
-
const se = await showPicker("Add Provider", entries)
|
|
334
|
-
if (!se) return // Esc → 返回上级(openModelPicker 循环会重开主菜单)
|
|
335
|
-
if (se.kind === "custom") {
|
|
336
|
-
const name = await askQuestion("Enter provider name:")
|
|
337
|
-
if (!name) return
|
|
338
|
-
if (agent.providers.some((p) => p.name === name)) return
|
|
339
|
-
const baseURL = (await askQuestion("Enter baseURL:")).replace(/\/+$/, "")
|
|
340
|
-
if (!baseURL) return
|
|
341
|
-
const model = await askQuestion("Enter model name:")
|
|
342
|
-
if (!model) return
|
|
343
|
-
const format = await showPicker("API format", [
|
|
344
|
-
{ type: "item", text: "openai", name: "openai" },
|
|
345
|
-
{ type: "item", text: "anthropic", name: "anthropic" },
|
|
346
|
-
{ type: "item", text: "google", name: "google" },
|
|
347
|
-
])
|
|
348
|
-
if (!format) return // Esc/取消 → 中止流程
|
|
349
|
-
const cfg = { name, baseURL, model }
|
|
350
|
-
if (format.name === "anthropic" || format.name === "google") cfg.format = format.name
|
|
351
|
-
else if (format.name !== "openai") return // 防御:未知格式(理论不可达——picker 枚举)
|
|
352
|
-
await persistRaw((raw) => { raw.providers ??= []; raw.providers.push(cfg) }) // D-F5a 先盘后存
|
|
353
|
-
agent.providers.push(cfg)
|
|
354
|
-
const key = await askQuestion(`Enter API key for ${name} (skip if none):`)
|
|
355
|
-
if (key) await setProviderKey(name, key, { probe: false }) // 探统一在流尾(M9——精确一次)
|
|
356
|
-
await probeChannelFlow(cfg, name) // M9 准入探(加渠道配置写入面;保存已落——不阻断)
|
|
357
|
-
return
|
|
358
|
-
}
|
|
359
|
-
const preset = PRESETS[se.name]
|
|
360
|
-
if (!preset || agent.providers.some((p) => p.name === se.name)) return
|
|
361
|
-
const cfg = { name: se.name, baseURL: preset.baseURL, model: preset.model }
|
|
362
|
-
if (preset.thinking) cfg.thinking = preset.thinking
|
|
363
|
-
if (preset.reasoningEffort) cfg.reasoningEffort = preset.reasoningEffort
|
|
364
|
-
if (preset.maxTokens) cfg.maxTokens = preset.maxTokens
|
|
365
|
-
if (preset.chatPath) cfg.chatPath = preset.chatPath
|
|
366
|
-
if (preset.format) cfg.format = preset.format
|
|
367
|
-
await persistRaw((raw) => { raw.providers ??= []; raw.providers.push(cfg) }) // D-F5a 先盘后存
|
|
368
|
-
agent.providers.push(cfg)
|
|
369
|
-
const key = await askQuestion(`Enter API key for ${se.name} (skip if none):`)
|
|
370
|
-
if (key) await setProviderKey(se.name, key, { probe: false }) // 探统一在流尾(M9——精确一次)
|
|
371
|
-
await probeChannelFlow(cfg, se.name) // M9 准入探(加渠道配置写入面;保存已落——不阻断)
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
async function removeProviderFlow() {
|
|
375
|
-
const candidates = agent.providers.filter((p) => p.name !== agent.activeProvider)
|
|
376
|
-
if (!candidates.length) return
|
|
377
|
-
const se = await showPicker("Remove Provider", [
|
|
378
|
-
{ type: "header", text: "Select provider to remove" },
|
|
379
|
-
...candidates.map((p) => ({ type: "item", text: `${p.name} (${defaultModelLabel(p)})`, name: p.name })),
|
|
380
|
-
])
|
|
381
|
-
if (!se) return
|
|
382
|
-
if (!(await confirmDelete(`Remove provider ${se.name}?`))) return
|
|
383
|
-
await persistRaw((raw) => {
|
|
384
|
-
raw.providers ??= []
|
|
385
|
-
const idx = raw.providers.findIndex((p) => p?.name === se.name)
|
|
386
|
-
if (idx !== -1) raw.providers.splice(idx, 1)
|
|
387
|
-
// F-4 级联(IKCDMR——AC-4):同盘清 consultModels/subagentModels/advisor.provider 悬挂引用
|
|
388
|
-
cascadeRemoveProvider(raw, se.name)
|
|
389
|
-
})
|
|
390
|
-
agent.providers.splice(agent.providers.findIndex((p) => p.name === se.name), 1)
|
|
391
|
-
// 会话内存镜像同清(agent.config = loadConfig 产物——consult/escalate 同会话读
|
|
392
|
-
// agent.config.agent.*;merged.advisor 为 agent.advisor 的提升拷贝——双处清理)
|
|
393
|
-
if (agent.config) {
|
|
394
|
-
cascadeRemoveProvider(agent.config, se.name)
|
|
395
|
-
if (agent.config.advisor?.provider === se.name) delete agent.config.advisor.provider
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
async function setKeyFlow() {
|
|
400
|
-
const se = await showPicker("Configure API Key", [
|
|
401
|
-
{ type: "header", text: "Select provider" },
|
|
402
|
-
...agent.providers.map((p) => ({ type: "item", text: `${p.name} ${p.apiKey ? `(has key: ${maskKey(p.apiKey)})` : "(no key)"}`, name: p.name })),
|
|
403
|
-
])
|
|
404
|
-
if (!se) return
|
|
405
|
-
const key = await askQuestion(`Enter API key for ${se.name}:`)
|
|
406
|
-
if (key) await setProviderKey(se.name, key)
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/** 设 API key(可以来自渠道管理流 / 加渠道流内的 key 步):写盘 + 内存镜像;
|
|
410
|
-
* M9:设 key 属配置写入面——默认探一次 `/models`(不阻断——key 已保存);
|
|
411
|
-
* 加渠道流内调用传 `{ probe: false }`(探由该流尾部统一执行——精确一次)。 */
|
|
412
|
-
async function setProviderKey(name, key, { probe = true } = {}) {
|
|
413
|
-
const target = agent.providers.find((p) => p.name === name)
|
|
414
|
-
if (!target) return
|
|
415
|
-
await persistRaw((raw) => {
|
|
416
|
-
raw.providers ??= []
|
|
417
|
-
const t = raw.providers.find((p) => p?.name === name)
|
|
418
|
-
if (t) t.apiKey = key
|
|
419
|
-
})
|
|
420
|
-
target.apiKey = key
|
|
421
|
-
if (name === agent.activeProvider) agent.provider.apiKey = key
|
|
422
|
-
if (probe) await probeChannelFlow(target, name)
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/** /model provider 管理:context 窗口字段(K 单位)——picker 选 provider + 表单输入。 */
|
|
426
|
-
async function setContextFlow() {
|
|
427
|
-
const se = await showPicker("Set Context Window", [
|
|
428
|
-
{ type: "header", text: "Select provider" },
|
|
429
|
-
...agent.providers.map((p) => ({
|
|
430
|
-
type: "item",
|
|
431
|
-
text: `${p.name} (ctx ${fmtContextK(providerSpec(p).context)})`,
|
|
432
|
-
name: p.name,
|
|
433
|
-
})),
|
|
434
|
-
])
|
|
435
|
-
if (!se?.name) return // Esc
|
|
436
|
-
const target = agent.providers.find((p) => p.name === se.name)
|
|
437
|
-
if (!target) return
|
|
438
|
-
const current = Number.isInteger(target.context) && target.context > 0 ? target.context : null
|
|
439
|
-
const val = (await askQuestion(
|
|
440
|
-
`Context window for ${se.name} in K units (current: ${current ? `${current}K` : "spec default"} — e.g. 128 = 128K; empty to clear):`
|
|
441
|
-
))?.trim() ?? ""
|
|
442
|
-
let newCtx // undefined = 清空(回 spec 值)
|
|
443
|
-
if (val !== "") {
|
|
444
|
-
const n = Number(val)
|
|
445
|
-
if (!Number.isInteger(n) || n <= 0) {
|
|
446
|
-
pushLine(`Invalid context: "${val}" — must be a positive integer in K units (e.g. 128 = 128K)`, C.error)
|
|
447
|
-
return
|
|
448
|
-
}
|
|
449
|
-
newCtx = n
|
|
450
|
-
}
|
|
451
|
-
await persistRaw((raw) => {
|
|
452
|
-
raw.providers ??= []
|
|
453
|
-
const t = raw.providers.find((p) => p?.name === se.name)
|
|
454
|
-
if (!t) return
|
|
455
|
-
if (newCtx === undefined) delete t.context
|
|
456
|
-
else t.context = newCtx
|
|
457
|
-
})
|
|
458
|
-
if (newCtx === undefined) delete target.context
|
|
459
|
-
else target.context = newCtx
|
|
460
|
-
if (se.name === agent.activeProvider) {
|
|
461
|
-
if (newCtx === undefined) delete agent.provider.context
|
|
462
|
-
else agent.provider.context = newCtx
|
|
463
|
-
if (agent.config?.agent?.compactThresholdAuto) {
|
|
464
|
-
const { resolveCompactThreshold } = await import("@thincoder/core/config.mjs")
|
|
465
|
-
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
pushLine(target.context !== undefined
|
|
469
|
-
? `ctx = ${target.context}K (${target.context * 1024} tokens)`
|
|
470
|
-
: `context cleared — using model spec (${fmtContextK(providerSpec(target).context)})`, C.tool)
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
return { openModelPicker, selectModel, setProviderKey, setContextFlow, pickModelForSlot }
|
|
313
|
+
return { openModelPicker, selectModel, setProviderKey: admin.setProviderKey, setContextFlow: admin.setContextFlow, pickModelForSlot }
|
|
474
314
|
}
|
|
475
315
|
|
|
476
|
-
|
|
477
|
-
* agent.consultModels 条目 / agent.subagentModels 角色值(=== name 或 "name:…" 前缀——
|
|
478
|
-
* 角色值是 "provider:model" | 裸渠道名 | 裸模型名)/ agent.advisor.provider。
|
|
479
|
-
* 纯 mutate(removeProviderFlow persistRaw 的 D-F5 新鲜 raw 上调用;agent.config 内存镜像
|
|
480
|
-
* 子树与 raw.agent 同构可复用)。空数组/空对象键删除(规范形态)。 */
|
|
481
|
-
export function cascadeRemoveProvider(raw, name) {
|
|
482
|
-
const a = raw?.agent
|
|
483
|
-
if (!a || typeof a !== "object" || Array.isArray(a)) return
|
|
484
|
-
if (Array.isArray(a.consultModels)) {
|
|
485
|
-
const keep = a.consultModels.filter((m) => m?.provider !== name)
|
|
486
|
-
if (keep.length) a.consultModels = keep
|
|
487
|
-
else delete a.consultModels
|
|
488
|
-
}
|
|
489
|
-
if (a.subagentModels && typeof a.subagentModels === "object" && !Array.isArray(a.subagentModels)) {
|
|
490
|
-
for (const role of Object.keys(a.subagentModels)) {
|
|
491
|
-
const v = a.subagentModels[role]
|
|
492
|
-
if (typeof v === "string" && (v === name || v.startsWith(`${name}:`))) delete a.subagentModels[role]
|
|
493
|
-
}
|
|
494
|
-
if (Object.keys(a.subagentModels).length === 0) delete a.subagentModels
|
|
495
|
-
}
|
|
496
|
-
if (a.advisor && typeof a.advisor === "object" && !Array.isArray(a.advisor) && a.advisor.provider === name) {
|
|
497
|
-
delete a.advisor.provider
|
|
498
|
-
}
|
|
499
|
-
}
|
|
316
|
+
export { cascadeRemoveProvider } from "./provider-admin.mjs"
|
package/src/tui/mouse.mjs
CHANGED
|
@@ -36,7 +36,7 @@ export function handleWheel(ctx, button, col, row) {
|
|
|
36
36
|
if (mouseOob(col, row, dims)) return false // F-3 sane-gate ②:越界滚轮丢弃——不落面板(> 非 >=——末行列合法)
|
|
37
37
|
const layout = computeLayout(state, dims)
|
|
38
38
|
const P = layout.panels
|
|
39
|
-
// §
|
|
39
|
+
// docs/cli/design/TUI.md §6.8 D4: 固定子agent 面板(conversation 与 todo 之间)——面板行默认穿出
|
|
40
40
|
// 滚会话(F3,与 todo 面板同型);命中展开区块内容行(_foldBlock 标记)→
|
|
41
41
|
// 块内滚动(现状能力不丢)。
|
|
42
42
|
if (P.subagent && r >= P.subagent.y && r < P.subagent.y + P.subagent.h) {
|
|
@@ -134,7 +134,7 @@ export function handleMouseClick(ctx, col, row) {
|
|
|
134
134
|
return true
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
// ── §
|
|
137
|
+
// ── docs/cli/design/TUI.md §6.8 D4: 固定子agent 面板(conversation 与 todo 之间)——折叠/展开/翻窗
|
|
138
138
|
// 坐标映射到面板行(与 todo 面板同型;layout.subagentLines = 面板渲染行)。
|
|
139
139
|
if (P.subagent && r >= P.subagent.y && r < P.subagent.y + P.subagent.h) {
|
|
140
140
|
// 评审 #4:保底截断后可见行 ≠ 前 h 行——命中映射与 render-frame 同一几何契约
|
|
@@ -201,7 +201,7 @@ export function createMouseDispatch({ agent, state, pushLine, render, popPicker
|
|
|
201
201
|
const isAdvisorBlock = key.startsWith("advisor#")
|
|
202
202
|
// af 批 #1(发射单源化):`emit` = 本层通道,传进核调用供其单点发射(本层不另发)。
|
|
203
203
|
const emit = (t) => { if (!routeSubToken(state, t, render)) pushLine(t, C.dim) }
|
|
204
|
-
// §
|
|
204
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.10 D-24b (②-6b): ⏹ on an advisor block cancels the background review
|
|
205
205
|
// (directed abort → cancelled settle: no pending entry / no token).
|
|
206
206
|
// SYNC-CANCEL F3: async 池/advisor miss 后查 sync registry(⏹ 门控已放开 sync——
|
|
207
207
|
// cancelSyncChild 与 async cancel 同模块同形态——subagent-async.mjs)。
|
package/src/tui/pickers.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { createModelPicker } from "./model-picker.mjs"
|
|
|
12
12
|
* 删除类二次确认件:confirmDelete(TUI-COMMANDS.md §5.4 单源——形态同 cmd-clear / cmd-new 先例)。
|
|
13
13
|
* 选中即关闭(Enter = resolve + pop);Esc = pop 当前层并 resolve(null)。菜单循环由调用方 while 重开。 */
|
|
14
14
|
export function createPickers(ctx) {
|
|
15
|
-
const { agent, state, render, ansi, C, pushLine, persistRaw, askQuestion, maskKey } = ctx
|
|
15
|
+
const { agent, state, render, ansi, C, pushLine, persistRaw, askQuestion, maskKey, onModalClose = null } = ctx
|
|
16
16
|
|
|
17
17
|
state.pickerStack ??= []
|
|
18
18
|
|
|
@@ -22,7 +22,9 @@ export function createPickers(ctx) {
|
|
|
22
22
|
return p.entries.filter((e) => e.type === "item" && (!f || e.text.toLowerCase().includes(f)))
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/** 弹出栈顶 picker 并 resolve 其 Promise。返回是否有 picker 被弹出。
|
|
25
|
+
/** 弹出栈顶 picker 并 resolve 其 Promise。返回是否有 picker 被弹出。
|
|
26
|
+
* #448①「关闭后补评估」:本弹出使 picker 面退场(栈空 ∧ 无 wizard)⇒ 通知链尾(闩重武装——模态期
|
|
27
|
+
* 被抑制的到期件即达);其余模态仍在场 ⇒ 零动作(交其关闭点)。 */
|
|
26
28
|
function popPicker(value) {
|
|
27
29
|
const p = state.pickerStack.pop()
|
|
28
30
|
if (!p) return false
|
|
@@ -30,6 +32,7 @@ export function createPickers(ctx) {
|
|
|
30
32
|
if (state.picker) rebuildLines()
|
|
31
33
|
else render()
|
|
32
34
|
p.resolve(value)
|
|
35
|
+
if (state.picker === null && state.wizard == null) onModalClose?.()
|
|
33
36
|
return true
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -119,6 +122,7 @@ export function createPickers(ctx) {
|
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
// /model 两级面 + selectModel + 渠道管理 + pickModelForSlot —— 迁 model-picker.mjs
|
|
125
|
+
// (2026-09-29 结构拆分:渠道管理四流再迁 provider-admin.mjs——本层与会话面转发零改。)
|
|
122
126
|
// (MODEL-MERGE-SESSION——pick 后本文件 ≤450)。createModelPicker 返回同名单函数,
|
|
123
127
|
// 经本层原样转发(index.mjs/wizard/slash-commands 消费面零变化)。
|
|
124
128
|
const modelPicker = createModelPicker({
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provider-admin.mjs — 渠道管理四流 + 级联清理出档(structure-split-2 · 2026-09-29):自
|
|
3
|
+
* `model-picker.mjs` 迁出〔原 `:309-471`(probeChannelFlow ∥ addProviderFlow ∥ removeProviderFlow ∥
|
|
4
|
+
* setKeyFlow ∥ setProviderKey ∥ setContextFlow)+ `:476-499`(cascadeRemoveProvider)〕——
|
|
5
|
+
* 结构拆分零语义(面不变 ∕ 判据不变,只换宿主档);切点 ∕ 缝单源 = 批档
|
|
6
|
+
* `docs/batches/2026-09-29-structure-split-2.md` §2.2-A。
|
|
7
|
+
*
|
|
8
|
+
* 装配 = `createProviderAdmin(ctx)`(ctx = `{ agent, showPicker, askQuestion, pushLine, persistRaw,
|
|
9
|
+
* maskKey, confirmDelete, C, fmtContextK, defaultModelLabel }`——后两名 = 主档纯 helper,ctx 注入
|
|
10
|
+
* 同 `maskKey` 先例)→ `{ addProviderFlow, removeProviderFlow, setKeyFlow, setProviderKey,
|
|
11
|
+
* setContextFlow }`;`cascadeRemoveProvider` 模块级导出(宿主 `model-picker.mjs` 尾同名再出口——
|
|
12
|
+
* 历史导出面零改)。零环:本档不 import 主档(宿主 → 本档单向)。
|
|
13
|
+
* 语义(迁出原文照录):四流 = provider 级 config 写——M9 配置阶段准入探一次 `/models`(不阻断保存)
|
|
14
|
+
* ∥ F-4 (IKCDMR):removeProviderFlow 级联清理悬挂引用(consultModels ∕ subagentModels ∕ advisor.provider)。
|
|
15
|
+
*/
|
|
16
|
+
import { PROVIDER_PRESETS as PRESETS, providerSpec } from "@thincoder/core/config.mjs"
|
|
17
|
+
import { probeChannelModels } from "./model-catalog.mjs"
|
|
18
|
+
|
|
19
|
+
/** createProviderAdmin(ctx) → { addProviderFlow, removeProviderFlow, setKeyFlow, setProviderKey, setContextFlow } */
|
|
20
|
+
export function createProviderAdmin(ctx) {
|
|
21
|
+
const { agent, showPicker, askQuestion, pushLine, persistRaw, maskKey, confirmDelete, C, fmtContextK, defaultModelLabel } = ctx
|
|
22
|
+
|
|
23
|
+
// ═══ 渠道管理流(provider 级 config 写——语义不变——与 /model 会话选择分离)═══
|
|
24
|
+
|
|
25
|
+
/** M9 配置阶段准入探:探通 → 清标记;探不通 → 会话内存标「不可用」+ 明示原因(绝不落 config;
|
|
26
|
+
* 不阻断保存——条目已保存,仅标注)。 */
|
|
27
|
+
async function probeChannelFlow(cfg, label) {
|
|
28
|
+
const r = await probeChannelModels(cfg)
|
|
29
|
+
if (r.ok) {
|
|
30
|
+
delete cfg._unavailable
|
|
31
|
+
pushLine(`${label}: /models 可用(${r.list.length} 个模型可候选)`, C.tool)
|
|
32
|
+
} else {
|
|
33
|
+
cfg._unavailable = true
|
|
34
|
+
pushLine(`${label} 不可用 — ${r.message}`, C.error)
|
|
35
|
+
}
|
|
36
|
+
return r
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function addProviderFlow() {
|
|
40
|
+
const entries = [
|
|
41
|
+
{ type: "header", text: "Select a preset provider" },
|
|
42
|
+
...Object.entries(PRESETS).filter(([name]) => !agent.providers.some((p) => p.name === name))
|
|
43
|
+
.map(([name, p]) => ({ type: "item", text: `${name.padEnd(10)} ${p.desc ?? ""} (${p.model ?? ""})`, name, kind: "preset" })),
|
|
44
|
+
{ type: "header", text: "Other" },
|
|
45
|
+
{ type: "item", text: "Custom (manual config)", name: "__custom__", kind: "custom" },
|
|
46
|
+
]
|
|
47
|
+
const se = await showPicker("Add Provider", entries)
|
|
48
|
+
if (!se) return // Esc → 返回上级(openModelPicker 循环会重开主菜单)
|
|
49
|
+
if (se.kind === "custom") {
|
|
50
|
+
const name = await askQuestion("Enter provider name:")
|
|
51
|
+
if (!name) return
|
|
52
|
+
if (agent.providers.some((p) => p.name === name)) return
|
|
53
|
+
const baseURL = (await askQuestion("Enter baseURL:")).replace(/\/+$/, "")
|
|
54
|
+
if (!baseURL) return
|
|
55
|
+
const model = await askQuestion("Enter model name:")
|
|
56
|
+
if (!model) return
|
|
57
|
+
const format = await showPicker("API format", [
|
|
58
|
+
{ type: "item", text: "openai", name: "openai" },
|
|
59
|
+
{ type: "item", text: "anthropic", name: "anthropic" },
|
|
60
|
+
{ type: "item", text: "google", name: "google" },
|
|
61
|
+
])
|
|
62
|
+
if (!format) return // Esc/取消 → 中止流程
|
|
63
|
+
const cfg = { name, baseURL, model }
|
|
64
|
+
if (format.name === "anthropic" || format.name === "google") cfg.format = format.name
|
|
65
|
+
else if (format.name !== "openai") return // 防御:未知格式(理论不可达——picker 枚举)
|
|
66
|
+
await persistRaw((raw) => { raw.providers ??= []; raw.providers.push(cfg) }) // D-F5a 先盘后存
|
|
67
|
+
agent.providers.push(cfg)
|
|
68
|
+
const key = await askQuestion(`Enter API key for ${name} (skip if none):`)
|
|
69
|
+
if (key) await setProviderKey(name, key, { probe: false }) // 探统一在流尾(M9——精确一次)
|
|
70
|
+
await probeChannelFlow(cfg, name) // M9 准入探(加渠道配置写入面;保存已落——不阻断)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
const preset = PRESETS[se.name]
|
|
74
|
+
if (!preset || agent.providers.some((p) => p.name === se.name)) return
|
|
75
|
+
const cfg = { name: se.name, baseURL: preset.baseURL, model: preset.model }
|
|
76
|
+
if (preset.thinking) cfg.thinking = preset.thinking
|
|
77
|
+
if (preset.reasoningEffort) cfg.reasoningEffort = preset.reasoningEffort
|
|
78
|
+
if (preset.maxTokens) cfg.maxTokens = preset.maxTokens
|
|
79
|
+
if (preset.chatPath) cfg.chatPath = preset.chatPath
|
|
80
|
+
if (preset.format) cfg.format = preset.format
|
|
81
|
+
await persistRaw((raw) => { raw.providers ??= []; raw.providers.push(cfg) }) // D-F5a 先盘后存
|
|
82
|
+
agent.providers.push(cfg)
|
|
83
|
+
const key = await askQuestion(`Enter API key for ${se.name} (skip if none):`)
|
|
84
|
+
if (key) await setProviderKey(se.name, key, { probe: false }) // 探统一在流尾(M9——精确一次)
|
|
85
|
+
await probeChannelFlow(cfg, se.name) // M9 准入探(加渠道配置写入面;保存已落——不阻断)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function removeProviderFlow() {
|
|
89
|
+
const candidates = agent.providers.filter((p) => p.name !== agent.activeProvider)
|
|
90
|
+
if (!candidates.length) return
|
|
91
|
+
const se = await showPicker("Remove Provider", [
|
|
92
|
+
{ type: "header", text: "Select provider to remove" },
|
|
93
|
+
...candidates.map((p) => ({ type: "item", text: `${p.name} (${defaultModelLabel(p)})`, name: p.name })),
|
|
94
|
+
])
|
|
95
|
+
if (!se) return
|
|
96
|
+
if (!(await confirmDelete(`Remove provider ${se.name}?`))) return
|
|
97
|
+
await persistRaw((raw) => {
|
|
98
|
+
raw.providers ??= []
|
|
99
|
+
const idx = raw.providers.findIndex((p) => p?.name === se.name)
|
|
100
|
+
if (idx !== -1) raw.providers.splice(idx, 1)
|
|
101
|
+
// F-4 级联(IKCDMR——AC-4):同盘清 consultModels/subagentModels/advisor.provider 悬挂引用
|
|
102
|
+
cascadeRemoveProvider(raw, se.name)
|
|
103
|
+
})
|
|
104
|
+
agent.providers.splice(agent.providers.findIndex((p) => p.name === se.name), 1)
|
|
105
|
+
// 会话内存镜像同清(agent.config = loadConfig 产物——consult/escalate 同会话读
|
|
106
|
+
// agent.config.agent.*;merged.advisor 为 agent.advisor 的提升拷贝——双处清理)
|
|
107
|
+
if (agent.config) {
|
|
108
|
+
cascadeRemoveProvider(agent.config, se.name)
|
|
109
|
+
if (agent.config.advisor?.provider === se.name) delete agent.config.advisor.provider
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function setKeyFlow() {
|
|
114
|
+
const se = await showPicker("Configure API Key", [
|
|
115
|
+
{ type: "header", text: "Select provider" },
|
|
116
|
+
...agent.providers.map((p) => ({ type: "item", text: `${p.name} ${p.apiKey ? `(has key: ${maskKey(p.apiKey)})` : "(no key)"}`, name: p.name })),
|
|
117
|
+
])
|
|
118
|
+
if (!se) return
|
|
119
|
+
const key = await askQuestion(`Enter API key for ${se.name}:`)
|
|
120
|
+
if (key) await setProviderKey(se.name, key)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 设 API key(可以来自渠道管理流 / 加渠道流内的 key 步):写盘 + 内存镜像;
|
|
124
|
+
* M9:设 key 属配置写入面——默认探一次 `/models`(不阻断——key 已保存);
|
|
125
|
+
* 加渠道流内调用传 `{ probe: false }`(探由该流尾部统一执行——精确一次)。 */
|
|
126
|
+
async function setProviderKey(name, key, { probe = true } = {}) {
|
|
127
|
+
const target = agent.providers.find((p) => p.name === name)
|
|
128
|
+
if (!target) return
|
|
129
|
+
await persistRaw((raw) => {
|
|
130
|
+
raw.providers ??= []
|
|
131
|
+
const t = raw.providers.find((p) => p?.name === name)
|
|
132
|
+
if (t) t.apiKey = key
|
|
133
|
+
})
|
|
134
|
+
target.apiKey = key
|
|
135
|
+
if (name === agent.activeProvider) agent.provider.apiKey = key
|
|
136
|
+
if (probe) await probeChannelFlow(target, name)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** /model provider 管理:context 窗口字段(K 单位)——picker 选 provider + 表单输入。 */
|
|
140
|
+
async function setContextFlow() {
|
|
141
|
+
const se = await showPicker("Set Context Window", [
|
|
142
|
+
{ type: "header", text: "Select provider" },
|
|
143
|
+
...agent.providers.map((p) => ({
|
|
144
|
+
type: "item",
|
|
145
|
+
text: `${p.name} (ctx ${fmtContextK(providerSpec(p).context)})`,
|
|
146
|
+
name: p.name,
|
|
147
|
+
})),
|
|
148
|
+
])
|
|
149
|
+
if (!se?.name) return // Esc
|
|
150
|
+
const target = agent.providers.find((p) => p.name === se.name)
|
|
151
|
+
if (!target) return
|
|
152
|
+
const current = Number.isInteger(target.context) && target.context > 0 ? target.context : null
|
|
153
|
+
const val = (await askQuestion(
|
|
154
|
+
`Context window for ${se.name} in K units (current: ${current ? `${current}K` : "spec default"} — e.g. 128 = 128K; empty to clear):`
|
|
155
|
+
))?.trim() ?? ""
|
|
156
|
+
let newCtx // undefined = 清空(回 spec 值)
|
|
157
|
+
if (val !== "") {
|
|
158
|
+
const n = Number(val)
|
|
159
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
160
|
+
pushLine(`Invalid context: "${val}" — must be a positive integer in K units (e.g. 128 = 128K)`, C.error)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
newCtx = n
|
|
164
|
+
}
|
|
165
|
+
await persistRaw((raw) => {
|
|
166
|
+
raw.providers ??= []
|
|
167
|
+
const t = raw.providers.find((p) => p?.name === se.name)
|
|
168
|
+
if (!t) return
|
|
169
|
+
if (newCtx === undefined) delete t.context
|
|
170
|
+
else t.context = newCtx
|
|
171
|
+
})
|
|
172
|
+
if (newCtx === undefined) delete target.context
|
|
173
|
+
else target.context = newCtx
|
|
174
|
+
if (se.name === agent.activeProvider) {
|
|
175
|
+
if (newCtx === undefined) delete agent.provider.context
|
|
176
|
+
else agent.provider.context = newCtx
|
|
177
|
+
if (agent.config?.agent?.compactThresholdAuto) {
|
|
178
|
+
const { resolveCompactThreshold } = await import("@thincoder/core/config.mjs")
|
|
179
|
+
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
pushLine(target.context !== undefined
|
|
183
|
+
? `ctx = ${target.context}K (${target.context * 1024} tokens)`
|
|
184
|
+
: `context cleared — using model spec (${fmtContextK(providerSpec(target).context)})`, C.tool)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { addProviderFlow, removeProviderFlow, setKeyFlow, setProviderKey, setContextFlow }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** F-4 (IKCDMR) 级联清理(删渠道共享写点):raw/merged config 移除 name 渠道后清悬挂引用——
|
|
191
|
+
* agent.consultModels 条目 / agent.subagentModels 角色值(=== name 或 "name:…" 前缀——
|
|
192
|
+
* 角色值是 "provider:model" | 裸渠道名 | 裸模型名)/ agent.advisor.provider。
|
|
193
|
+
* 纯 mutate(removeProviderFlow persistRaw 的 D-F5 新鲜 raw 上调用;agent.config 内存镜像
|
|
194
|
+
* 子树与 raw.agent 同构可复用)。空数组/空对象键删除(规范形态)。 */
|
|
195
|
+
export function cascadeRemoveProvider(raw, name) {
|
|
196
|
+
const a = raw?.agent
|
|
197
|
+
if (!a || typeof a !== "object" || Array.isArray(a)) return
|
|
198
|
+
if (Array.isArray(a.consultModels)) {
|
|
199
|
+
const keep = a.consultModels.filter((m) => m?.provider !== name)
|
|
200
|
+
if (keep.length) a.consultModels = keep
|
|
201
|
+
else delete a.consultModels
|
|
202
|
+
}
|
|
203
|
+
if (a.subagentModels && typeof a.subagentModels === "object" && !Array.isArray(a.subagentModels)) {
|
|
204
|
+
for (const role of Object.keys(a.subagentModels)) {
|
|
205
|
+
const v = a.subagentModels[role]
|
|
206
|
+
if (typeof v === "string" && (v === name || v.startsWith(`${name}:`))) delete a.subagentModels[role]
|
|
207
|
+
}
|
|
208
|
+
if (Object.keys(a.subagentModels).length === 0) delete a.subagentModels
|
|
209
|
+
}
|
|
210
|
+
if (a.advisor && typeof a.advisor === "object" && !Array.isArray(a.advisor) && a.advisor.provider === name) {
|
|
211
|
+
delete a.advisor.provider
|
|
212
|
+
}
|
|
213
|
+
}
|
package/src/tui/queued-merge.mjs
CHANGED
|
@@ -1,68 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* queued-merge.mjs — busy 排队消息的合并消费纯函数族(R15 攒批恢复——queue-visible 批 2026-09-24)。
|
|
3
3
|
*
|
|
4
|
-
* 机制单源 = `docs/cli/design/TUI.md` §7.5「合并消费」+ `docs/cli/design/TUI-INPUT-BOX.md` §4.1
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* §11.3;原实现 = git `3e1234b7` 树内 `src/tui/suspension-drive.mjs:21-68`)——常量名 / 值 /
|
|
8
|
-
* 形态文案 / 批语义逐字一致。
|
|
9
|
-
*
|
|
4
|
+
* 机制单源 = `docs/cli/design/TUI.md` §7.5「合并消费」+ `docs/cli/design/TUI-INPUT-BOX.md` §4.1「送达链路」。
|
|
5
|
+
* **核单源(转口)**:五名(常量 ∕ 形态 ∕ 计划)自 `@thincoder/core/queued.mjs` 转口(同一绑定——
|
|
6
|
+
* 对拍锁 = `docs/batches/2026-09-29-parity-b2-queued.test.mjs`(批次本地件))。
|
|
10
7
|
* 取批四支共用同一事实源(`planQueuedInput`;队列容器仍归会话层 `state.pendingInput`——核只给缝):
|
|
11
8
|
* ① 步边界 pickup(`queued-pickup.mjs`)· ② 回合尾兜底(`agent-turn.mjs`)
|
|
12
9
|
* · ③ driver 输入优先(`suspension-drive.mjs`)· ④ 中止残余转 queue(`suspension-drive.mjs`)。
|
|
13
10
|
*/
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
// (双端逐字一致——VSC 对位 `thincoder-vscode/src/extension/queued-merge.mjs` 同值同名);
|
|
17
|
-
// 超限截批先行(余下留待下批——不丢不截断单条);单条 > MAX_MERGE_CHARS 直发不进批;
|
|
18
|
-
// /cmd 不进合并(逐条保序即时)。──
|
|
19
|
-
export const MAX_MERGE_ITEMS = 8
|
|
20
|
-
export const MAX_MERGE_CHARS = 2000
|
|
21
|
-
|
|
22
|
-
/** 队容量(条)——**常量单源**(显示面 / 输入门禁同源 import;与合并批上限 `MAX_MERGE_ITEMS`(8)
|
|
23
|
-
* **同值不同名**:常规满队 ⇒ 恰一批一次消费)。判据面 = `docs/cli/design/TUI-INPUT-BOX.md` §4.1
|
|
24
|
-
* 条件 5(第 9 条 = 拒 + 提示 + 文本保留)。 */
|
|
25
|
-
export const QUEUED_MAX_ITEMS = 8
|
|
26
|
-
|
|
27
|
-
/** 合并注入形态(编号逐条——模型一次处理全部——设计措辞锚;`docs/cli/design/TUI.md` §7.5 逐字)。 */
|
|
28
|
-
export function formatMergedMessages(items) {
|
|
29
|
-
return `你排队了 ${items.length} 条消息:\n${items.map((m, i) => `${i + 1}. ${m}`).join("\n")}\n——一次处理`
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* R15 攒批计划(纯函数——消费点共享同一事实源):按队列顺序产出动作序列。
|
|
34
|
-
* - { kind:"slash", text, count:1 }——/cmd 逐条即时执行(保序——不进合并缓冲);
|
|
35
|
-
* - { kind:"turn", text, count, merged }——文本批次:连续非 / 条目攒批(单批 ≤ MAX_MERGE_ITEMS 条;
|
|
36
|
-
* 合并注入(格式化后)≤ MAX_MERGE_CHARS 字符;count ≥2 → merged 编号注入;count === 1 → 原文
|
|
37
|
-
* 直发(含单条 > MAX_MERGE_CHARS——不进批);超限截批先行——余下条目留待下批(不丢)。
|
|
38
|
-
* 消费者每次取动作 [0] 并按 count 从源队列移除——下轮重新计划余项(边界幂等)。
|
|
39
|
-
*/
|
|
40
|
-
export function planQueuedInput(items) {
|
|
41
|
-
const actions = []
|
|
42
|
-
const n = items.length
|
|
43
|
-
let i = 0
|
|
44
|
-
while (i < n) {
|
|
45
|
-
const cur = String(items[i])
|
|
46
|
-
if (cur.startsWith("/")) { actions.push({ kind: "slash", text: cur, count: 1 }); i++; continue }
|
|
47
|
-
let j = i
|
|
48
|
-
for (;;) {
|
|
49
|
-
if (j - i >= MAX_MERGE_ITEMS || j >= n) break
|
|
50
|
-
const it = String(items[j])
|
|
51
|
-
if (it.startsWith("/")) break
|
|
52
|
-
if (it.length > MAX_MERGE_CHARS) break // 单条超长——不进批(留作直发)
|
|
53
|
-
const cand = items.slice(i, j + 1)
|
|
54
|
-
if (cand.length > 1 && formatMergedMessages(cand).length > MAX_MERGE_CHARS) break // 批总长超限——截批
|
|
55
|
-
j++
|
|
56
|
-
}
|
|
57
|
-
// 超长条目堵头(内环一步未进)→ 该条直发(不进批——也不与前后合并)
|
|
58
|
-
if (j === i) { actions.push({ kind: "turn", text: String(items[i]), count: 1, merged: false }); i++; continue }
|
|
59
|
-
const taken = items.slice(i, j)
|
|
60
|
-
if (taken.length === 1) {
|
|
61
|
-
actions.push({ kind: "turn", text: String(taken[0]), count: 1, merged: false })
|
|
62
|
-
} else {
|
|
63
|
-
actions.push({ kind: "turn", text: formatMergedMessages(taken), count: taken.length, merged: true })
|
|
64
|
-
}
|
|
65
|
-
i = j
|
|
66
|
-
}
|
|
67
|
-
return actions
|
|
68
|
-
}
|
|
12
|
+
export { MAX_MERGE_ITEMS, MAX_MERGE_CHARS, QUEUED_MAX_ITEMS, formatMergedMessages, planQueuedInput } from "@thincoder/core/queued.mjs"
|
|
@@ -14,9 +14,9 @@ import { planQueuedInput } from "./queued-merge.mjs"
|
|
|
14
14
|
/**
|
|
15
15
|
* 步边界取批(核循环头回调——空队列 no-op,headless / 直连面零开销)。
|
|
16
16
|
* 取数 = 计划首动作(`planQueuedInput`——2 条短消息 ⇒ 合并批一次消费);`/cmd` 首动作 =
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* `agent-turn.mjs` 回合起点同形——下一步 LLM 请求即含该消息)。
|
|
17
|
+
* 不可达支(斜杠 busy 禁发——`key-handler-busy.mjs:24` ∕ `key-handler-edit.mjs:59` 双门禁;
|
|
18
|
+
* `TUI-INPUT-BOX.md` §4.1 条件 3)⇒ 不消费,留给既有消费点(回合尾 queue / driver)。呈现 =
|
|
19
|
+
* 回执行 + `❯ You:` + 本批文本(与 `agent-turn.mjs` 回合起点同形——下一步 LLM 请求即含该消息)。
|
|
20
20
|
* @param {object} ctx TUI 上下文(agent / state / pushLine / pushLabel / render)
|
|
21
21
|
*/
|
|
22
22
|
export function pickupQueuedAtStepBoundary(ctx) {
|