thincoder 0.12.60 → 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 +24 -1
- package/README.md +8 -6
- package/bin/thincoder.mjs +27 -122
- package/package.json +3 -2
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +38 -17
- 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 +36 -134
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +124 -329
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +3 -2
- package/src/agent/completion.mjs +1 -1
- package/src/agent/dispatch.mjs +47 -12
- package/src/agent/helpers.mjs +71 -13
- package/src/agent/record-results.mjs +13 -5
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +21 -6
- package/src/agent/setup-reminders.mjs +16 -9
- package/src/agent/setup.mjs +92 -128
- package/src/agent/spawn-child.mjs +26 -9
- package/src/agent-tools/advisor-async.mjs +70 -180
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +69 -20
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +23 -10
- package/src/agent-tools/design-token.mjs +14 -1
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +3 -3
- package/src/agent-tools/escalate-async.mjs +16 -13
- package/src/agent-tools/read-history.mjs +13 -3
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +130 -17
- package/src/agent-tools/subagent-actions.mjs +15 -6
- package/src/agent-tools/subagent-async.mjs +66 -14
- package/src/agent-tools/subagent-panel.mjs +22 -15
- package/src/agent-tools/subagent-run.mjs +9 -6
- package/src/agent-tools/subagent-scheduler.mjs +57 -8
- package/src/agent-tools/subagent-spawn.mjs +63 -16
- package/src/agent-tools/subagent.mjs +175 -49
- package/src/agent-tools/verify.mjs +13 -34
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +38 -21
- package/src/cli/distill-command.mjs +2 -2
- package/src/cli/make-agent.mjs +23 -7
- package/src/cli/memory-command.mjs +2 -2
- package/src/cli/setup-wizard.mjs +29 -9
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +132 -63
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +7 -2
- package/src/expand-home.mjs +16 -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 +70 -31
- package/src/memory/delete.mjs +2 -0
- package/src/memory/docs.mjs +10 -6
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/prompt-overlays.mjs +73 -16
- package/src/prompts/advisor-design.md +18 -8
- package/src/prompts/advisor-round1.md +7 -1
- package/src/prompts/advisor-round2.md +13 -2
- package/src/prompts/advisor-round3.md +13 -2
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +2 -0
- 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 +13 -32
- package/src/provider/errors.mjs +26 -1
- 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/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-slots.mjs +7 -2
- package/src/session.mjs +59 -38
- package/src/token-ttl.mjs +2 -1
- package/src/tools/{system.mjs → bash.mjs} +6 -243
- package/src/tools/file.mjs +30 -10
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +15 -34
- package/src/tools/index.mjs +4 -2
- package/src/tools/ops.mjs +20 -7
- package/src/tools/question.md +1 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -1
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tui/agent-turn.mjs +32 -13
- 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 +108 -37
- package/src/tui/cmd-eng.mjs +11 -27
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/index.mjs +38 -37
- 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 +47 -10
- package/src/tui/pickers.mjs +28 -410
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +5 -19
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/subagent-blocks.mjs +69 -90
- package/src/tui/subagent-children.mjs +50 -64
- package/src/tui/subagent-freeze.mjs +40 -43
- package/src/tui/subagent-panel.mjs +21 -66
- package/src/tui/suspension-drive.mjs +28 -82
- package/src/tui/tool-args.mjs +4 -2
- package/src/tui/tool-display.mjs +4 -3
- package/src/tui/tool-events.mjs +23 -16
- package/src/tui/tui-lifecycle.mjs +18 -5
- package/src/tui/wizard.mjs +52 -18
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -13
- package/src/prompts/discipline.md +0 -84
- package/src/prompts/eng-coder.md +0 -19
- package/src/prompts/engineering-sub.md +0 -14
- package/src/prompts/engineering.md +0 -87
- package/src/prompts/explore.md +0 -12
- package/src/prompts/main.md +0 -34
- package/src/prompts/methodology-template.md +0 -38
- package/src/prompts/plan.md +0 -9
- package/src/prompts/system.md +0 -44
package/src/tui/mouse.mjs
CHANGED
|
@@ -17,8 +17,11 @@
|
|
|
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 } from "../agent-tools/subagent-async.mjs"
|
|
20
|
+
import { cancelAsyncSubagent, cancelSyncChild } from "../agent-tools/subagent-async.mjs"
|
|
21
21
|
import { cancelAsyncAdvisor } from "../agent-tools/advisor-async.mjs"
|
|
22
|
+
import { maybeRefillAsync, refreshQueuedTokens } from "../agent-tools/subagent-scheduler.mjs" // F-2:queued 取消后续(补位/位置刷新)——叶子模块
|
|
23
|
+
import { routeSubToken } from "./subagent-blocks.mjs" // F-2:queued 取消块移除(⟦ev⟧cancelled 就地路由——引擎动作路径同通道)
|
|
24
|
+
import { denyModalForOwner } from "./key-modes.mjs"
|
|
22
25
|
import { C } from "./ansi.mjs"
|
|
23
26
|
|
|
24
27
|
/** 2026-08-31 滚轮事件分派(用户需求"展开块能滚动阅读全文"):坐标命中展开块内容行 →
|
|
@@ -30,6 +33,7 @@ export function handleWheel(ctx, button, col, row) {
|
|
|
30
33
|
const r = row - 1
|
|
31
34
|
if (r < 0) return false
|
|
32
35
|
const dims = state.dims ? state.dims.get() : { cols: process.stdout.columns || 80, rows: process.stdout.rows || 24 }
|
|
36
|
+
if (mouseOob(col, row, dims)) return false // F-3 sane-gate ②:越界滚轮丢弃——不落面板(> 非 >=——末行列合法)
|
|
33
37
|
const layout = computeLayout(state, dims)
|
|
34
38
|
const P = layout.panels
|
|
35
39
|
// §7.2.1 D4: 固定子agent 面板(conversation 与 todo 之间)——面板行默认穿出
|
|
@@ -82,6 +86,13 @@ export function parseMouseClicks(text) {
|
|
|
82
86
|
return out
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
/** F-3 sane-gate(RESIZE-MOUSE-LEAK-FIX):越界坐标判定——resize 后短窗口越界上报丢弃
|
|
90
|
+
* (col > dims.cols || row > dims.rows——> 非 >=——末行列合法——与 dims.mjs 同哲学)。
|
|
91
|
+
* 落点:① handleMouseClick 入口 ② handleWheel 入口 ③ index.mjs 滚轮 fallback 前(评审 #3 定稿)。 */
|
|
92
|
+
export function mouseOob(col, row, dims) {
|
|
93
|
+
return col > dims.cols || row > dims.rows
|
|
94
|
+
}
|
|
95
|
+
|
|
85
96
|
/** Map a 0-based screen row to a conversation line index (same math as renderConversation). */
|
|
86
97
|
export function convGlobalIndex(convLen, convH, scroll) {
|
|
87
98
|
const { start, pad } = convViewport(convLen, convH, scroll)
|
|
@@ -105,6 +116,7 @@ export function handleMouseClick(ctx, col, row) {
|
|
|
105
116
|
// Single source (Windows ConPTY instability, 2026-08-30): the cached dims,
|
|
106
117
|
// never a live read that can flip between stale and fresh values.
|
|
107
118
|
const dims = state.dims ? state.dims.get() : { cols: process.stdout.columns || 80, rows: process.stdout.rows || 24 }
|
|
119
|
+
if (mouseOob(col, row, dims)) return false // F-3 sane-gate ①:越界点击丢弃——resize 后短窗口越界上报不落应用(> 非 >=——末行列合法)
|
|
108
120
|
const layout = computeLayout(state, dims)
|
|
109
121
|
const P = layout.panels
|
|
110
122
|
|
|
@@ -128,10 +140,11 @@ export function handleMouseClick(ctx, col, row) {
|
|
|
128
140
|
// 评审 #4:保底截断后可见行 ≠ 前 h 行——命中映射与 render-frame 同一几何契约
|
|
129
141
|
// (subagentLineIndex:分隔线 + 末尾区块行优先)。
|
|
130
142
|
const lineEl = layout.subagentLines[subagentLineIndex(layout.subagentLines, P.subagent.h, r - P.subagent.y)]
|
|
131
|
-
// §19.5 D-M7/D-M7b ⏹ 停止标记(round1 #6 + 用户裁定 B
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
143
|
+
// §19.5 D-M7/D-M7b ⏹ 停止标记(round1 #6 + 用户裁定 B 形态)+ SYNC-CANCEL F3
|
|
144
|
+
// (2026-09-09):列级命中——⏹ 列点击 = cancel(定向该子代理——ctx.cancelSubagent
|
|
145
|
+
// 直连池/registry abort 路径,不经模型回合),不触发折叠翻转;**async 区块与 registry
|
|
146
|
+
// live 的 sync 区块带 _stopSub 元数据**(headless 无 _agent → sync 不钉——零回归);
|
|
147
|
+
// ⏹ 区外点击照常走折叠/翻窗。
|
|
135
148
|
if (lineEl?._stopSub && col >= (lineEl._stopCol ?? Infinity)) {
|
|
136
149
|
ctx.cancelSubagent?.(lineEl._stopSub)
|
|
137
150
|
render()
|
|
@@ -186,19 +199,43 @@ export function createMouseDispatch({ agent, state, pushLine, render, popPicker
|
|
|
186
199
|
try {
|
|
187
200
|
const id = key.slice(key.lastIndexOf("#") + 1)
|
|
188
201
|
const isAdvisorBlock = key.startsWith("advisor#")
|
|
189
|
-
// §
|
|
202
|
+
// §11.2 D-24b (②-6b): ⏹ on an advisor block cancels the background review
|
|
190
203
|
// (directed abort → cancelled settle: no pending entry / no token).
|
|
191
|
-
|
|
204
|
+
// SYNC-CANCEL F3: async 池/advisor miss 后查 sync registry(⏹ 门控已放开 sync——
|
|
205
|
+
// cancelSyncChild 与 async cancel 同模块同形态——subagent-async.mjs)。
|
|
206
|
+
let r = isAdvisorBlock
|
|
192
207
|
? cancelAsyncAdvisor(agent, id)
|
|
193
208
|
: cancelAsyncSubagent(agent, id)
|
|
209
|
+
let syncStopped = false
|
|
210
|
+
if (!isAdvisorBlock && r?.status === "error") {
|
|
211
|
+
r = cancelSyncChild(agent, key)
|
|
212
|
+
syncStopped = r?.status === "cancelled"
|
|
213
|
+
}
|
|
194
214
|
if (r?.status === "error") {
|
|
195
|
-
//
|
|
196
|
-
//
|
|
215
|
+
// 拒绝文案改(SYNC-CANCEL):registry/池 miss + live 块 → 定向中止窗口已过
|
|
216
|
+
// (成功/折叠/整回合停后 finally 注销——"finished or stop no longer applies"——
|
|
217
|
+
// 原 "blocking (sync) child mid-call — targeted stop not available" 文案退役——
|
|
218
|
+
// sync 现已可中止);无 live 块(真 miss/未知 key)→ 原错误直显。
|
|
197
219
|
const liveBlock = state.subTasks?.[key] && !state.subTasks[key].done
|
|
198
220
|
pushLine(liveBlock
|
|
199
|
-
? `[subagent stop] ${key}
|
|
221
|
+
? `[subagent stop] ${key} has finished or stop no longer applies`
|
|
200
222
|
: `[subagent stop] ${r.error}`, C.error)
|
|
201
223
|
} else {
|
|
224
|
+
// SYNC-CANCEL v2 模态 deny(用户裁):**sync ⏹** 顺带 deny 该 child 的 pending
|
|
225
|
+
// ask(权限/continue 模态)——模态立即解除——child 解绕折叠(async cancel 的
|
|
226
|
+
// 同类缺陷现状已知——非本批引入——v2 deny 机制后续可复用到 async——此处仅
|
|
227
|
+
// syncStopped 路径 deny——async 成功取消不 deny 模态——零回归)。
|
|
228
|
+
if (syncStopped) denyModalForOwner(state, key, { pushLine, render })
|
|
229
|
+
// F-2(QUEUED-VISIBILITY——2026-09-09):queued 出队(引擎返回 was:"queued"——
|
|
230
|
+
// 无 settle 事件链)——UI 直连路径无回合 ctx 事件流——补 executeCancelAction
|
|
231
|
+
// 同款 TUI 维护:等待块移除(⟦ev⟧cancelled 就地路由——守卫同 routeSubToken
|
|
232
|
+
// cancelled 分支——不冻结)+ 补位 + 剩余排队头位置/依赖标注刷新(⟦ev⟧queued)。
|
|
233
|
+
if (r?.was === "queued") {
|
|
234
|
+
const emit = (t) => { if (!routeSubToken(state, t, render)) pushLine(t, C.dim) }
|
|
235
|
+
emit(`${key}/⟦ev⟧cancelled\x1e`)
|
|
236
|
+
maybeRefillAsync(agent)
|
|
237
|
+
refreshQueuedTokens(agent, emit)
|
|
238
|
+
}
|
|
202
239
|
pushLine(`[subagent ${key} stop requested]`, C.warn)
|
|
203
240
|
}
|
|
204
241
|
} catch (e) {
|
package/src/tui/pickers.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { sliceByWidth } from "./render.mjs"
|
|
2
|
-
import { PROVIDER_PRESETS as PRESETS, providerSpec } from "../config.mjs"
|
|
3
1
|
import { computeLayout } from "./layout.mjs"
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { createModelPicker } from "./model-picker.mjs"
|
|
3
|
+
|
|
4
|
+
/** Generic list picker plumbing + /model two-level surface delegation.
|
|
5
|
+
* MODEL-MERGE-SESSION 拆分(评审 #9):/model 两级面(L1 provider → L2 模型行——
|
|
6
|
+
* 2026-09-10 MODEL-SELECTION v2 起 L2 行 = 运行期拉取候选)+ 渠道
|
|
7
|
+
* 管理流 + selectModel + pickModelForSlot 迁 model-picker.mjs——本文件只留通用 picker
|
|
8
|
+
* 栈(showPicker/closePicker/popPicker/rebuildLines)——createModelPicker 以 ctx 接收
|
|
9
|
+
* 通用绑定(showPicker/closePicker/renderPickerLines)再行装配——index.mjs 消费面不变。
|
|
6
10
|
* 单一 Promise API:showPicker(title, entries, { defaultIndex }) → Promise<entry|null>。
|
|
7
11
|
* picker 栈:state.pickerStack,state.picker 始终指向栈顶(layout/render/key-handler 都只读 state.picker)。
|
|
8
12
|
* 选中即关闭(Enter = resolve + pop);Esc = pop 当前层并 resolve(null)。菜单循环由调用方 while 重开。 */
|
|
@@ -37,6 +41,12 @@ export function createPickers(ctx) {
|
|
|
37
41
|
* 互斥保护:入栈前把现有挂起 picker 全部 resolve(null),消除 Promise 悬挂。
|
|
38
42
|
* (正常嵌套是先 await 上一层返回再开新的,栈深通常为 1。) */
|
|
39
43
|
function showPicker(title, entries, { defaultIndex = 0 } = {}) {
|
|
44
|
+
if (!Array.isArray(entries)) {
|
|
45
|
+
const got = entries && typeof entries.then === "function"
|
|
46
|
+
? "a Promise (missing `await`?)"
|
|
47
|
+
: `a ${entries === null ? "null" : typeof entries}`
|
|
48
|
+
throw new TypeError(`showPicker("${title}"): entries must be an array — got ${got}`)
|
|
49
|
+
}
|
|
40
50
|
closePicker()
|
|
41
51
|
return new Promise((resolve) => {
|
|
42
52
|
const itemCount = entries.filter((e) => e.type === "item").length
|
|
@@ -65,7 +75,12 @@ export function createPickers(ctx) {
|
|
|
65
75
|
const sel = row === p.index
|
|
66
76
|
if (sel) selLine = lines.length
|
|
67
77
|
const marker = e.marker ? ` ${e.marker}` : ""
|
|
68
|
-
|
|
78
|
+
// A1(第 20 批 §12.5——D-SS7):item 附注消费——渲染形态 {prefix}{text}{marker}{note};
|
|
79
|
+
// header 分支既有消费同形态,无 note 零追加(不产生尾随空格)。宽度预算 =
|
|
80
|
+
// renderPicker 既有 8 格余量 + 右截断——保序 prefix→text→marker→note,截断从行尾
|
|
81
|
+
// 开始 = note 段最先牺牲。
|
|
82
|
+
const note = e.note ? ` ${e.note}` : ""
|
|
83
|
+
lines.push({ text: `${sel ? " ▸ " : " "}${e.text}${marker}${note}`, color: sel ? ansi.bold + C.text : C.dim, _row: row })
|
|
69
84
|
row++
|
|
70
85
|
}
|
|
71
86
|
}
|
|
@@ -90,410 +105,13 @@ export function createPickers(ctx) {
|
|
|
90
105
|
|
|
91
106
|
function renderPickerLines() { rebuildLines() }
|
|
92
107
|
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
.replace(/-\d{4}-\d{2}-\d{2}$/, "") // date suffix like -2024-09-19
|
|
101
|
-
.replace(/-\d{8}$/, "") // date suffix like -20240919
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Dedupe model list: group by series, keep shortest name per group.
|
|
105
|
-
* Reduces "qwen-max, qwen-max-latest, qwen-max-2024-09-19" to just "qwen-max". */
|
|
106
|
-
function dedupeModels(models) {
|
|
107
|
-
const groups = new Map()
|
|
108
|
-
for (const m of models) {
|
|
109
|
-
const series = modelSeries(m)
|
|
110
|
-
const existing = groups.get(series)
|
|
111
|
-
if (!existing || m.length < existing.length) {
|
|
112
|
-
groups.set(series, m)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return [...groups.values()].sort()
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** entry 唯一标识:异步更新 entries 后按它恢复选中项 */
|
|
119
|
-
function entryKey(e) {
|
|
120
|
-
if (!e) return null
|
|
121
|
-
return e.action === "switch" ? `switch:${e.provider}:${e.model}` : `action:${e.action}`
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** 模型信息显示的 context 窗口(PROVIDER.md §15 D-C5):K 单位形态跟随覆盖值
|
|
125
|
-
* (config K 是二进制 K:128 → 128×1024 = 131072 → 显示 "128K");≥1M tokens 用 M 形态
|
|
126
|
-
* (1_000_000 → "1M"),与 spec 值的大窗口惯例一致。 */
|
|
127
|
-
function fmtContextK(tokens) {
|
|
128
|
-
if (tokens >= 1_000_000) return `${Math.round(tokens / 1_048_576)}M`
|
|
129
|
-
return `${Math.round(tokens / 1024)}K`
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Get API key for a provider (config.json only — env vars are not a key source) */
|
|
133
|
-
function getApiKey(providerName, providerConfig) {
|
|
134
|
-
return providerConfig.apiKey
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Level 1: Show provider list. Selecting a provider opens Level 2 (model list). */
|
|
138
|
-
async function openModelPicker() {
|
|
139
|
-
// 菜单循环:选中即关闭,子流程结束后重开主菜单;Esc 退出
|
|
140
|
-
for (;;) {
|
|
141
|
-
const entries = buildProviderEntries()
|
|
142
|
-
const items = entries.filter((e) => e.type === "item")
|
|
143
|
-
const current = items.findIndex(
|
|
144
|
-
(e) => e.action === "open-models" && e.provider === agent.activeProvider)
|
|
145
|
-
const picked = showPicker("Models & Providers", entries, { defaultIndex: Math.max(0, current) })
|
|
146
|
-
const e = await picked
|
|
147
|
-
if (!e) return
|
|
148
|
-
if (e.action === "open-models") {
|
|
149
|
-
// Level 2: open model list for this provider
|
|
150
|
-
const modelSelected = await openModelListForProvider(e.provider)
|
|
151
|
-
if (modelSelected) return // model selected, close picker
|
|
152
|
-
// Esc from model list → return to provider list
|
|
153
|
-
} else if (e.action === "add") {
|
|
154
|
-
await addProviderFlow()
|
|
155
|
-
} else if (e.action === "remove") {
|
|
156
|
-
await removeProviderFlow()
|
|
157
|
-
} else if (e.action === "key") {
|
|
158
|
-
await setKeyFlow()
|
|
159
|
-
} else if (e.action === "context") {
|
|
160
|
-
await setContextFlow()
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Level 2: Show model list for a specific provider. Returns true if a model was selected. */
|
|
166
|
-
async function openModelListForProvider(providerName) {
|
|
167
|
-
const providerConfig = agent.providers.find((p) => p.name === providerName)
|
|
168
|
-
if (!providerConfig) return false
|
|
169
|
-
|
|
170
|
-
const entries = buildModelEntriesForProvider(providerName, providerConfig)
|
|
171
|
-
const items = entries.filter((e) => e.type === "item")
|
|
172
|
-
const currentModel = providerName === agent.activeProvider
|
|
173
|
-
? (agent.activeModel || providerConfig.model)
|
|
174
|
-
: providerConfig.model
|
|
175
|
-
const current = items.findIndex((e) => e.model === currentModel)
|
|
176
|
-
const picked = showPicker(`${providerName} models`, entries, { defaultIndex: Math.max(0, current) })
|
|
177
|
-
|
|
178
|
-
// Async fetch models in background
|
|
179
|
-
fetchModelsForProvider(providerName, entries).catch((err) => {
|
|
180
|
-
pushLine(`[model] fetch models failed: ${err.message}`, C.error)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
const e = await picked
|
|
184
|
-
if (!e) return false // Esc → back to provider list
|
|
185
|
-
if (e.action === "switch") {
|
|
186
|
-
await selectModel(e).catch((err) => pushLine(`[error] ${err.message}`, C.error))
|
|
187
|
-
return true
|
|
188
|
-
}
|
|
189
|
-
return false
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** Build entries for Level 1: provider list */
|
|
193
|
-
function buildProviderEntries() {
|
|
194
|
-
const entries = []
|
|
195
|
-
for (const p of agent.providers) {
|
|
196
|
-
const active = p.name === agent.activeProvider
|
|
197
|
-
const currentModel = active ? (agent.activeModel || p.model) : p.model
|
|
198
|
-
const marker = active ? "●" : ""
|
|
199
|
-
const note = active ? " ← current" : ""
|
|
200
|
-
const keyStatus = p.apiKey ? "" : " (no key)"
|
|
201
|
-
// context 窗口显示(PROVIDER.md §15 D-C5/T-C6):跟随 providers[].context 覆盖
|
|
202
|
-
const ctxTag = ` (ctx ${fmtContextK(providerSpec(p).context)})`
|
|
203
|
-
entries.push({
|
|
204
|
-
type: "item",
|
|
205
|
-
text: `${p.name.padEnd(12)} ${currentModel}${ctxTag}${note}`,
|
|
206
|
-
action: "open-models",
|
|
207
|
-
provider: p.name,
|
|
208
|
-
marker,
|
|
209
|
-
note: `${p.baseURL}${keyStatus}`,
|
|
210
|
-
})
|
|
211
|
-
}
|
|
212
|
-
entries.push({ type: "header", text: "Management" })
|
|
213
|
-
entries.push({ type: "item", text: "Add provider…", action: "add" })
|
|
214
|
-
if (agent.providers.length > 1) entries.push({ type: "item", text: "Remove provider…", action: "remove" })
|
|
215
|
-
entries.push({ type: "item", text: "Set / change API key…", action: "key" })
|
|
216
|
-
entries.push({ type: "item", text: "Set context window (K units)…", action: "context" })
|
|
217
|
-
return entries
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Build entries for Level 2: model list for a specific provider */
|
|
221
|
-
function buildModelEntriesForProvider(providerName, providerConfig) {
|
|
222
|
-
const entries = []
|
|
223
|
-
const active = providerName === agent.activeProvider
|
|
224
|
-
const currentModel = active ? (agent.activeModel || providerConfig.model) : providerConfig.model
|
|
225
|
-
const isDefaultModel = currentModel === providerConfig.model
|
|
226
|
-
|
|
227
|
-
entries.push({ type: "header", text: "Current model" })
|
|
228
|
-
entries.push({
|
|
229
|
-
type: "item",
|
|
230
|
-
text: currentModel,
|
|
231
|
-
action: "switch",
|
|
232
|
-
provider: providerName,
|
|
233
|
-
model: currentModel,
|
|
234
|
-
marker: active && isDefaultModel ? "●" : "",
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
entries.push({ type: "header", text: "Available models (loading…)" })
|
|
238
|
-
return entries
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** Async fetch and splice models for a specific provider */
|
|
242
|
-
async function fetchModelsForProvider(providerName, entries) {
|
|
243
|
-
const { listModels } = await import("../provider/index.mjs")
|
|
244
|
-
const providerConfig = agent.providers.find((p) => p.name === providerName)
|
|
245
|
-
if (!providerConfig) return
|
|
246
|
-
|
|
247
|
-
let selKey = null
|
|
248
|
-
try {
|
|
249
|
-
const apiKey = getApiKey(providerName, providerConfig)
|
|
250
|
-
const models = await listModels(
|
|
251
|
-
{ baseURL: providerConfig.baseURL, apiKey: apiKey ?? "" },
|
|
252
|
-
{ signal: AbortSignal.timeout(10000) }
|
|
253
|
-
)
|
|
254
|
-
if (state.picker?.entries !== entries) return // picker closed or changed
|
|
255
|
-
selKey = entryKey(pickerItems(state.picker)[state.picker.index])
|
|
256
|
-
|
|
257
|
-
// Dedupe and filter
|
|
258
|
-
const deduped = dedupeModels(models)
|
|
259
|
-
const currentModel = providerName === agent.activeProvider
|
|
260
|
-
? (agent.activeModel || providerConfig.model)
|
|
261
|
-
: providerConfig.model
|
|
262
|
-
|
|
263
|
-
// Find the "Available models" header and splice after it
|
|
264
|
-
const headerIdx = entries.findIndex((e) => e.type === "header" && e.text.startsWith("Available models"))
|
|
265
|
-
if (headerIdx >= 0) {
|
|
266
|
-
// Update header with count info
|
|
267
|
-
const hint = deduped.length < models.length
|
|
268
|
-
? ` (${models.length} total, ${deduped.length} shown — type to filter)`
|
|
269
|
-
: ` (${deduped.length})`
|
|
270
|
-
entries[headerIdx].text = `Available models${hint}`
|
|
271
|
-
|
|
272
|
-
// Splice models after header (skip current model)
|
|
273
|
-
const newModels = deduped.filter((m) => m !== currentModel)
|
|
274
|
-
entries.splice(headerIdx + 1, 0, ...newModels.map((m) => ({
|
|
275
|
-
type: "item",
|
|
276
|
-
text: m,
|
|
277
|
-
action: "switch",
|
|
278
|
-
provider: providerName,
|
|
279
|
-
model: m,
|
|
280
|
-
})))
|
|
281
|
-
}
|
|
282
|
-
} catch (error) {
|
|
283
|
-
if (state.picker?.entries !== entries) return
|
|
284
|
-
const headerIdx = entries.findIndex((e) => e.type === "header" && e.text.startsWith("Available models"))
|
|
285
|
-
if (headerIdx >= 0) {
|
|
286
|
-
entries[headerIdx].text = `Available models (fetch failed: ${sliceByWidth(error.message, 30)})`
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Restore selection
|
|
291
|
-
const pk = state.picker
|
|
292
|
-
const items = pickerItems(pk)
|
|
293
|
-
const restored = selKey ? items.findIndex((e) => entryKey(e) === selKey) : -1
|
|
294
|
-
pk.index = restored >= 0 ? restored : Math.min(pk.index, Math.max(0, items.length - 1))
|
|
295
|
-
rebuildLines()
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
async function selectModel(item) {
|
|
299
|
-
closePicker()
|
|
300
|
-
const target = agent.providers.find((pp) => pp.name === item.provider)
|
|
301
|
-
if (!target) return
|
|
302
|
-
const providerDefault = target.model
|
|
303
|
-
const nextActiveModel = item.model !== providerDefault ? item.model : null // default model → clear activeModel
|
|
304
|
-
// D-F5a(selectModel——清单外同型写回补正)先盘后存:磁盘 fresh raw 单操作(model + active 指针)——冲突放弃不留 ghost
|
|
305
|
-
await persistRaw((raw) => {
|
|
306
|
-
raw.providers ??= []
|
|
307
|
-
const t = raw.providers.find((p) => p?.name === item.provider)
|
|
308
|
-
if (t) t.model = item.model
|
|
309
|
-
raw.activeProvider = item.provider
|
|
310
|
-
raw.activeModel = nextActiveModel || undefined // null → omit from config
|
|
311
|
-
})
|
|
312
|
-
target.model = item.model
|
|
313
|
-
agent.activeProvider = item.provider
|
|
314
|
-
agent.activeModel = nextActiveModel
|
|
315
|
-
agent.provider = { ...target }
|
|
316
|
-
if (agent.config?.agent?.compactThresholdAuto) {
|
|
317
|
-
const { resolveCompactThreshold } = await import("../config.mjs")
|
|
318
|
-
// provider 对象(非 model 字符串)——阈值跟随 providers[].context 覆盖(PROVIDER.md §15 T-C2)
|
|
319
|
-
agent.config.agent.compactThreshold = resolveCompactThreshold(null, target).value
|
|
320
|
-
}
|
|
321
|
-
agent.config.activeProvider = item.provider
|
|
322
|
-
agent.config.activeModel = agent.activeModel
|
|
323
|
-
if (!agent.provider.apiKey) {
|
|
324
|
-
const selKey = await askQuestion(`Enter API key for ${item.provider} (leave empty to skip):`)
|
|
325
|
-
if (selKey) await setProviderKey(item.provider, selKey)
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
async function addProviderFlow() {
|
|
330
|
-
const entries = [
|
|
331
|
-
{ type: "header", text: "Select a preset provider" },
|
|
332
|
-
...Object.entries(PRESETS).filter(([name]) => !agent.providers.some((p) => p.name === name))
|
|
333
|
-
.map(([name, p]) => ({ type: "item", text: `${name.padEnd(10)} ${p.desc ?? ""} (${p.model})`, name, kind: "preset" })),
|
|
334
|
-
{ type: "header", text: "Other" },
|
|
335
|
-
{ type: "item", text: "Custom (manual config)", name: "__custom__", kind: "custom" },
|
|
336
|
-
]
|
|
337
|
-
const se = await showPicker("Add Provider", entries)
|
|
338
|
-
if (!se) return // Esc → 返回上级(openModelPicker 循环会重开主菜单)
|
|
339
|
-
if (se.kind === "custom") {
|
|
340
|
-
const name = await askQuestion("Enter provider name:")
|
|
341
|
-
if (!name) return
|
|
342
|
-
if (agent.providers.some((p) => p.name === name)) return
|
|
343
|
-
const baseURL = (await askQuestion("Enter baseURL:")).replace(/\/+$/, "")
|
|
344
|
-
if (!baseURL) return
|
|
345
|
-
const model = await askQuestion("Enter model name:")
|
|
346
|
-
if (!model) return
|
|
347
|
-
// D-C1(TUI.md §10.6D):API format 改 picker 选择(openai/anthropic/google——默认 index=0
|
|
348
|
-
// openai)——固定枚举不再手输;Esc/取消 = 中止整个 Add Provider 流程(cfg 尚未落盘——
|
|
349
|
-
// 无半配置,与旧手输非法 abort 同语义)。与 preset 选择器同形态——零新机制。
|
|
350
|
-
const format = await showPicker("API format", [
|
|
351
|
-
{ type: "item", text: "openai", name: "openai" },
|
|
352
|
-
{ type: "item", text: "anthropic", name: "anthropic" },
|
|
353
|
-
{ type: "item", text: "google", name: "google" },
|
|
354
|
-
])
|
|
355
|
-
if (!format) return // Esc/取消 → 中止流程
|
|
356
|
-
const cfg = { name, baseURL, model }
|
|
357
|
-
if (format.name === "anthropic" || format.name === "google") cfg.format = format.name
|
|
358
|
-
else if (format.name !== "openai") return // 防御:未知格式(理论不可达——picker 枚举)
|
|
359
|
-
await persistRaw((raw) => { raw.providers ??= []; raw.providers.push(cfg) }) // D-F5a(#3)先盘后存
|
|
360
|
-
agent.providers.push(cfg)
|
|
361
|
-
const key = await askQuestion(`Enter API key for ${name} (skip if none):`)
|
|
362
|
-
if (key) await setProviderKey(name, key)
|
|
363
|
-
return
|
|
364
|
-
}
|
|
365
|
-
const preset = PRESETS[se.name]
|
|
366
|
-
if (!preset || agent.providers.some((p) => p.name === se.name)) return
|
|
367
|
-
const cfg = { name: se.name, baseURL: preset.baseURL, model: preset.model }
|
|
368
|
-
if (preset.thinking) cfg.thinking = preset.thinking
|
|
369
|
-
if (preset.reasoningEffort) cfg.reasoningEffort = preset.reasoningEffort
|
|
370
|
-
if (preset.maxTokens) cfg.maxTokens = preset.maxTokens
|
|
371
|
-
if (preset.chatPath) cfg.chatPath = preset.chatPath
|
|
372
|
-
if (preset.format) cfg.format = preset.format
|
|
373
|
-
await persistRaw((raw) => { raw.providers ??= []; raw.providers.push(cfg) }) // D-F5a(#2)先盘后存
|
|
374
|
-
agent.providers.push(cfg)
|
|
375
|
-
const key = await askQuestion(`Enter API key for ${se.name} (skip if none):`)
|
|
376
|
-
if (key) await setProviderKey(se.name, key)
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
async function removeProviderFlow() {
|
|
380
|
-
const candidates = agent.providers.filter((p) => p.name !== agent.activeProvider)
|
|
381
|
-
if (!candidates.length) return
|
|
382
|
-
const se = await showPicker("Remove Provider", [
|
|
383
|
-
{ type: "header", text: "Select provider to remove" },
|
|
384
|
-
...candidates.map((p) => ({ type: "item", text: `${p.name} (${p.model})`, name: p.name })),
|
|
385
|
-
])
|
|
386
|
-
if (!se) return
|
|
387
|
-
// D-F5a(#4)先盘后存:磁盘 fresh raw 上 splice;冲突放弃不留下内存 ghost
|
|
388
|
-
await persistRaw((raw) => {
|
|
389
|
-
raw.providers ??= []
|
|
390
|
-
const idx = raw.providers.findIndex((p) => p?.name === se.name)
|
|
391
|
-
if (idx !== -1) raw.providers.splice(idx, 1)
|
|
392
|
-
})
|
|
393
|
-
agent.providers.splice(agent.providers.findIndex((p) => p.name === se.name), 1)
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
async function setKeyFlow() {
|
|
397
|
-
const se = await showPicker("Configure API Key", [
|
|
398
|
-
{ type: "header", text: "Select provider" },
|
|
399
|
-
...agent.providers.map((p) => ({ type: "item", text: `${p.name} ${p.apiKey ? `(has key: ${maskKey(p.apiKey)})` : "(no key)"}`, name: p.name })),
|
|
400
|
-
])
|
|
401
|
-
if (!se) return
|
|
402
|
-
const key = await askQuestion(`Enter API key for ${se.name}:`)
|
|
403
|
-
if (key) await setProviderKey(se.name, key)
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
async function setProviderKey(name, key) {
|
|
407
|
-
const target = agent.providers.find((p) => p.name === name)
|
|
408
|
-
if (!target) return
|
|
409
|
-
// D-F5a(#5)先盘后存:磁盘 fresh raw 上改目标项 apiKey;冲突放弃不留下内存 ghost
|
|
410
|
-
await persistRaw((raw) => {
|
|
411
|
-
raw.providers ??= []
|
|
412
|
-
const t = raw.providers.find((p) => p?.name === name)
|
|
413
|
-
if (t) t.apiKey = key
|
|
414
|
-
})
|
|
415
|
-
target.apiKey = key
|
|
416
|
-
if (name === agent.activeProvider) agent.provider.apiKey = key
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/** /model provider 管理:context 窗口字段(K 单位,PROVIDER.md §15 D-C4/D-C5)——picker 选
|
|
420
|
-
* provider + 表单输入;空清空/非法不落盘(D-C1);落盘 = D-F5a(#6)fresh 单字段补丁——先盘后存。 */
|
|
421
|
-
async function setContextFlow() {
|
|
422
|
-
const se = await showPicker("Set Context Window", [
|
|
423
|
-
{ type: "header", text: "Select provider" },
|
|
424
|
-
...agent.providers.map((p) => ({
|
|
425
|
-
type: "item",
|
|
426
|
-
text: `${p.name} (ctx ${fmtContextK(providerSpec(p).context)})`,
|
|
427
|
-
name: p.name,
|
|
428
|
-
})),
|
|
429
|
-
])
|
|
430
|
-
if (!se?.name) return // Esc
|
|
431
|
-
const target = agent.providers.find((p) => p.name === se.name)
|
|
432
|
-
if (!target) return
|
|
433
|
-
const current = Number.isInteger(target.context) && target.context > 0 ? target.context : null
|
|
434
|
-
const val = (await askQuestion(
|
|
435
|
-
`Context window for ${se.name} in K units (current: ${current ? `${current}K` : "spec default"} — e.g. 128 = 128K; empty to clear):`
|
|
436
|
-
))?.trim() ?? ""
|
|
437
|
-
let newCtx // undefined = 清空(回 spec 值)
|
|
438
|
-
if (val !== "") {
|
|
439
|
-
const n = Number(val)
|
|
440
|
-
if (!Number.isInteger(n) || n <= 0) {
|
|
441
|
-
pushLine(`Invalid context: "${val}" — must be a positive integer in K units (e.g. 128 = 128K)`, C.error)
|
|
442
|
-
return
|
|
443
|
-
}
|
|
444
|
-
newCtx = n
|
|
445
|
-
}
|
|
446
|
-
await persistRaw((raw) => {
|
|
447
|
-
raw.providers ??= []
|
|
448
|
-
const t = raw.providers.find((p) => p?.name === se.name)
|
|
449
|
-
if (!t) return
|
|
450
|
-
if (newCtx === undefined) delete t.context
|
|
451
|
-
else t.context = newCtx
|
|
452
|
-
})
|
|
453
|
-
if (newCtx === undefined) delete target.context
|
|
454
|
-
else target.context = newCtx
|
|
455
|
-
if (se.name === agent.activeProvider) {
|
|
456
|
-
// 运行时 provider 同步(同 setProviderKey 先例:只补 context,不重建对象以免丢 activeModel 覆盖)
|
|
457
|
-
if (newCtx === undefined) delete agent.provider.context
|
|
458
|
-
else agent.provider.context = newCtx
|
|
459
|
-
if (agent.config?.agent?.compactThresholdAuto) {
|
|
460
|
-
const { resolveCompactThreshold } = await import("../config.mjs")
|
|
461
|
-
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
pushLine(target.context !== undefined
|
|
465
|
-
? `ctx = ${target.context}K (${target.context * 1024} tokens)`
|
|
466
|
-
: `context cleared — using model spec (${fmtContextK(providerSpec(target).context)})`, C.tool)
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
/** Slot-bound model picker: two-level provider → model selection that RETURNS
|
|
471
|
-
* { provider, model } instead of writing main-session state — used by /submodel
|
|
472
|
-
* to write into a subagent slot (global or per-role). Esc from the model list
|
|
473
|
-
* returns to the provider list (openModelPicker parity); Esc from the provider
|
|
474
|
-
* list exits → null. */
|
|
475
|
-
async function pickModelForSlot() {
|
|
476
|
-
for (;;) {
|
|
477
|
-
const providers = agent.providers
|
|
478
|
-
if (!providers.length) return null
|
|
479
|
-
const e = await showPicker("Select provider", providers.map((p) => ({
|
|
480
|
-
type: "item",
|
|
481
|
-
text: `${p.name.padEnd(12)} ${p.model}`,
|
|
482
|
-
action: "open-models",
|
|
483
|
-
provider: p.name,
|
|
484
|
-
})))
|
|
485
|
-
if (!e?.provider) return null
|
|
486
|
-
const providerConfig = providers.find((p) => p.name === e.provider)
|
|
487
|
-
if (!providerConfig) return null
|
|
488
|
-
const entries = buildModelEntriesForProvider(e.provider, providerConfig)
|
|
489
|
-
fetchModelsForProvider(e.provider, entries).catch((err) => {
|
|
490
|
-
pushLine(`[model] fetch models failed: ${err.message}`, C.error)
|
|
491
|
-
})
|
|
492
|
-
const me = await showPicker(`${e.provider} models`, entries)
|
|
493
|
-
if (!me?.model) continue // Esc from model list → back to provider list
|
|
494
|
-
return { provider: e.provider, model: me.model }
|
|
495
|
-
}
|
|
496
|
-
}
|
|
108
|
+
// /model 两级面 + selectModel + 渠道管理 + pickModelForSlot —— 迁 model-picker.mjs
|
|
109
|
+
// (MODEL-MERGE-SESSION——pick 后本文件 ≤450)。createModelPicker 返回同名单函数,
|
|
110
|
+
// 经本层原样转发(index.mjs/wizard/slash-commands 消费面零变化)。
|
|
111
|
+
const modelPicker = createModelPicker({
|
|
112
|
+
agent, state, render, ansi, C, pushLine, persistRaw, askQuestion, maskKey,
|
|
113
|
+
showPicker, closePicker, renderPickerLines,
|
|
114
|
+
})
|
|
497
115
|
|
|
498
|
-
return { showPicker, closePicker, popPicker, renderPickerLines,
|
|
116
|
+
return { showPicker, closePicker, popPicker, renderPickerLines, ...modelPicker }
|
|
499
117
|
}
|