thincoder 0.7.0 → 0.7.2
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/README.md +23 -1
- package/bin/thincoder.mjs +28 -1
- package/package.json +1 -1
- package/src/SYSTEM_PROMPT.md +1 -0
- package/src/agent.mjs +96 -64
- package/src/checkpoint.mjs +6 -3
- package/src/config.mjs +10 -4
- package/src/context.mjs +37 -8
- package/src/distill.mjs +6 -2
- package/src/embedding.mjs +11 -2
- package/src/gitmem.mjs +6 -2
- package/src/markdown.mjs +11 -4
- package/src/mcp.mjs +118 -36
- package/src/memory.mjs +214 -74
- package/src/provider.mjs +160 -21
- package/src/repomap.mjs +117 -16
- package/src/session.mjs +23 -9
- package/src/skills.mjs +6 -2
- package/src/tools/apply_patch.md +11 -0
- package/src/tools/checkpoint.md +11 -0
- package/src/tools.mjs +311 -25
- package/src/tui.mjs +506 -426
package/src/tui.mjs
CHANGED
|
@@ -206,7 +206,6 @@ export function layoutInput(chars, cursor, width) {
|
|
|
206
206
|
return { lines, cursorLine, cursorCol }
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
/** 文本按宽度折行(保留 \n),返回行数组 */
|
|
210
209
|
/**
|
|
211
210
|
* 显示净化:控制字符会破坏终端网格数学(\r 回车覆盖、\t 宽度误判致整帧错位、ANSI/响铃冲屏)。
|
|
212
211
|
* 只动显示层——模型看到的工具结果原文不变;session 里已存的脏 display 回放时也经此净化。
|
|
@@ -222,6 +221,7 @@ export function sanitizeDisplay(s) {
|
|
|
222
221
|
.replace(/\n+$/, "")
|
|
223
222
|
}
|
|
224
223
|
|
|
224
|
+
/** 文本按宽度折行(保留 \n),返回行数组 */
|
|
225
225
|
export function wrapText(text, width) {
|
|
226
226
|
const lines = []
|
|
227
227
|
for (const rawLine of text.split("\n")) {
|
|
@@ -406,9 +406,17 @@ export async function startTUI(agent, opts = {}) {
|
|
|
406
406
|
let boxLines = inputLines
|
|
407
407
|
if (state.question) {
|
|
408
408
|
const q = state.question
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
409
|
+
if (q.options.length > 0) {
|
|
410
|
+
// 选项窗口:只显示选中项 ±2,选项过多时防输入框无限增高撑破锚定布局
|
|
411
|
+
const sel = q.selected ?? 0
|
|
412
|
+
const QWIN = 5
|
|
413
|
+
const start = Math.max(0, Math.min(sel - 2, q.options.length - QWIN))
|
|
414
|
+
boxLines = q.options
|
|
415
|
+
.slice(start, start + QWIN)
|
|
416
|
+
.map((opt, i) => (start + i === sel ? "▸ " : " ") + opt)
|
|
417
|
+
} else {
|
|
418
|
+
boxLines = ["▸ " + (q.answer ?? "")]
|
|
419
|
+
}
|
|
412
420
|
}
|
|
413
421
|
const inputBoxH = boxLines.length + 2
|
|
414
422
|
|
|
@@ -434,8 +442,18 @@ export async function startTUI(agent, opts = {}) {
|
|
|
434
442
|
const taskPanelH = visibleTasks.length
|
|
435
443
|
// 子 agent 流式输出占位(显示时占最多 2 行)
|
|
436
444
|
const subOutLen = (state.subOutput && state.processing) ? wrapText(state.subOutput, W - 8).slice(-2).length : 0
|
|
437
|
-
//
|
|
438
|
-
|
|
445
|
+
// 权限预览占位:字符数之外再封顶显示行数(rows-8),多行短行也能把帧撑过终端高度,破坏锚定布局
|
|
446
|
+
let permPreviewLines = []
|
|
447
|
+
if (state.permission) {
|
|
448
|
+
const maxLines = Math.max(1, rows - 8)
|
|
449
|
+
outer: for (const l of state.permissionPreview) {
|
|
450
|
+
for (const wrapped of wrapText(` ${l}`, W - 1)) {
|
|
451
|
+
if (permPreviewLines.length >= maxLines) break outer
|
|
452
|
+
permPreviewLines.push(wrapped)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const permPreviewLen = state.permission ? 1 + permPreviewLines.length : 0
|
|
439
457
|
const convH = Math.max(1, rows - headerH - inputBoxH - statusH - pickerH - taskPanelH - subOutLen - permPreviewLen)
|
|
440
458
|
|
|
441
459
|
// 对话区内容行(含流式缓冲);markdown 表格先按显示宽度重排
|
|
@@ -495,7 +513,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
495
513
|
if (overlay.selectedLine >= overlay.scroll + winH) overlay.scroll = overlay.selectedLine - winH + 1
|
|
496
514
|
const start = Math.max(0, Math.min(overlay.scroll, Math.max(0, overlay.lines.length - winH)))
|
|
497
515
|
const shown = overlay.lines.slice(start, start + winH)
|
|
498
|
-
const overlayTitle = state.picker ?
|
|
516
|
+
const overlayTitle = state.picker ? ` ❯ ${state.picker.title} ` : " ❯ 初始配置 "
|
|
499
517
|
out.push(`${ansi.bold}${C.tool}${overlayTitle}${ansi.reset}${ansi.dim}${state.picker ? "(↑↓ 移动, Enter 确认, Esc 取消)" : ""}${ansi.reset}${ansi.clearLine}`)
|
|
500
518
|
for (const l of shown) {
|
|
501
519
|
out.push(`${l.color}${sliceByWidth(l.text, cols - 1)}${ansi.reset}${ansi.clearLine}`)
|
|
@@ -519,13 +537,11 @@ export async function startTUI(agent, opts = {}) {
|
|
|
519
537
|
}
|
|
520
538
|
}
|
|
521
539
|
|
|
522
|
-
//
|
|
540
|
+
// 权限审批内容预览(黄色,紧挨输入框上方);用上方已封顶的 permPreviewLines,渲染行数与占位一致
|
|
523
541
|
if (state.permission) {
|
|
524
542
|
out.push(`${ansi.bold}${C.warn}❯ 权限请求${ansi.reset}${ansi.clearLine}`)
|
|
525
|
-
for (const
|
|
526
|
-
|
|
527
|
-
out.push(`${C.warn}${wrapped}${ansi.reset}${ansi.clearLine}`)
|
|
528
|
-
}
|
|
543
|
+
for (const wrapped of permPreviewLines) {
|
|
544
|
+
out.push(`${C.warn}${wrapped}${ansi.reset}${ansi.clearLine}`)
|
|
529
545
|
}
|
|
530
546
|
}
|
|
531
547
|
|
|
@@ -543,7 +559,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
543
559
|
title = ` Allow ${state.permission.name}? (y/n/a) `
|
|
544
560
|
}
|
|
545
561
|
} else if (state.picker) {
|
|
546
|
-
title = "
|
|
562
|
+
title = " Select "
|
|
547
563
|
} else if (state.wizard) {
|
|
548
564
|
title = " Setup "
|
|
549
565
|
} else if (state.processing) {
|
|
@@ -584,13 +600,21 @@ export async function startTUI(agent, opts = {}) {
|
|
|
584
600
|
const cmds = SLASH_COMMANDS.filter((c) => c.name.startsWith(cmd))
|
|
585
601
|
const match = cmds.length === 1 ? cmds[0] : null
|
|
586
602
|
if (match?.name === "/config" && cmd === "/config") {
|
|
587
|
-
statusLine = " /config
|
|
603
|
+
statusLine = " /config 打开配置菜单"
|
|
588
604
|
} else if (match?.name === "/provider" && cmd === "/provider") {
|
|
589
|
-
statusLine = " /provider
|
|
605
|
+
statusLine = " /provider 打开 Provider 管理菜单"
|
|
590
606
|
} else if (match?.name === "/model" && cmd === "/model" && !sub) {
|
|
591
|
-
statusLine = " /model
|
|
607
|
+
statusLine = " /model 打开模型选择器"
|
|
592
608
|
} else if (match?.name === "/think" && cmd === "/think") {
|
|
593
|
-
statusLine = " /think
|
|
609
|
+
statusLine = " /think 打开思维模式菜单"
|
|
610
|
+
} else if (match?.name === "/mcp" && cmd === "/mcp") {
|
|
611
|
+
statusLine = " /mcp 打开 MCP 管理菜单"
|
|
612
|
+
} else if (match?.name === "/goal" && cmd === "/goal") {
|
|
613
|
+
statusLine = " /goal 打开目标管理菜单"
|
|
614
|
+
} else if (match?.name === "/session" && cmd === "/session") {
|
|
615
|
+
statusLine = " /session 选择归档会话"
|
|
616
|
+
} else if (match?.name === "/rewind" && cmd === "/rewind") {
|
|
617
|
+
statusLine = " /rewind 选择存档点回滚"
|
|
594
618
|
} else if (cmds.length > 0) {
|
|
595
619
|
if (cmds.length <= 4) {
|
|
596
620
|
statusLine = ` ${cmds.map((c) => `${c.name} ${c.desc}`).join(" │ ")}`
|
|
@@ -697,8 +721,8 @@ export async function startTUI(agent, opts = {}) {
|
|
|
697
721
|
|
|
698
722
|
const callbacks = {
|
|
699
723
|
onToken: (t) => {
|
|
700
|
-
// 子 agent 流式输出:前缀匹配 explore/coder/plan
|
|
701
|
-
const subMatch = t.match(/^(explore|coder|plan)\//)
|
|
724
|
+
// 子 agent 流式输出:前缀匹配 explore/coder/plan/sub(无角色子 agent 用 sub/)的 token 进 subOutput
|
|
725
|
+
const subMatch = t.match(/^(explore|coder|plan|sub)\//)
|
|
702
726
|
if (subMatch) {
|
|
703
727
|
state.currentSub = subMatch[1]
|
|
704
728
|
state.subOutput = (state.subOutput + t.slice(subMatch[0].length)).slice(-300)
|
|
@@ -710,6 +734,14 @@ export async function startTUI(agent, opts = {}) {
|
|
|
710
734
|
scheduleRender()
|
|
711
735
|
},
|
|
712
736
|
onReasoning: (t) => {
|
|
737
|
+
// 子 agent 的思考 token 同样带 role/ 前缀,进 subOutput 滚动区,不污染主思考流
|
|
738
|
+
const subMatch = t.match(/^(explore|coder|plan|sub)\//)
|
|
739
|
+
if (subMatch) {
|
|
740
|
+
state.currentSub = subMatch[1]
|
|
741
|
+
state.subOutput = (state.subOutput + t.slice(subMatch[0].length)).slice(-300)
|
|
742
|
+
scheduleRender()
|
|
743
|
+
return
|
|
744
|
+
}
|
|
713
745
|
ensureAssistantLabel()
|
|
714
746
|
state.reasoning += t
|
|
715
747
|
scheduleRender()
|
|
@@ -722,8 +754,9 @@ export async function startTUI(agent, opts = {}) {
|
|
|
722
754
|
},
|
|
723
755
|
onToolResult: (name, result) => {
|
|
724
756
|
state.currentTool = null
|
|
725
|
-
// 子 agent
|
|
726
|
-
|
|
757
|
+
// 子 agent 结束(父 agent 侧的 subagent 工具结果带着最终报告):清空流式缓冲,报告进对话区。
|
|
758
|
+
// 注意只能用精确匹配——子 agent 内部工具调用不 relay 到 TUI(刷了满屏的教训)
|
|
759
|
+
const isSubagent = name === "subagent"
|
|
727
760
|
if (isSubagent) {
|
|
728
761
|
state.subOutput = ""
|
|
729
762
|
state.currentSub = null
|
|
@@ -759,6 +792,11 @@ export async function startTUI(agent, opts = {}) {
|
|
|
759
792
|
state.tokens.cacheHit += usage.prompt_cache_hit_tokens ?? 0
|
|
760
793
|
state.tokens.cacheMiss += usage.prompt_cache_miss_tokens ?? 0
|
|
761
794
|
},
|
|
795
|
+
// 节流等待(主动闸门 / 429 退避):状态栏明示,防用户以为卡死
|
|
796
|
+
onWait: ({ phase, seconds }) => {
|
|
797
|
+
state.status = phase === "gate" ? `TPM 节流等待 ~${seconds}s` : `限流 429,${seconds}s 后重试`
|
|
798
|
+
render()
|
|
799
|
+
},
|
|
762
800
|
onTaskUpdate: (items) => {
|
|
763
801
|
state.tasks = items
|
|
764
802
|
const done = items.filter((i) => i.status === "done").length
|
|
@@ -877,6 +915,10 @@ export async function startTUI(agent, opts = {}) {
|
|
|
877
915
|
...cap(args.new_string ?? "", 500).split("\n").map((l) => `+ ${l}`),
|
|
878
916
|
]
|
|
879
917
|
}
|
|
918
|
+
if (base === "apply_patch") {
|
|
919
|
+
// 补丁本身就是可读的 diff,直接预览
|
|
920
|
+
return cap(args.patch ?? "", 1500).split("\n")
|
|
921
|
+
}
|
|
880
922
|
if (base === "delete") return [`${args.path}${args.force ? "(force:跟踪文件也删)" : ""}`]
|
|
881
923
|
if (base === "subagent") return cap(args.task ?? "", 500).split("\n")
|
|
882
924
|
if (base === "memory_put") return [`[${args.type ?? ""}] ${args.title ?? ""}`, ...cap(args.content ?? "", 500).split("\n")]
|
|
@@ -957,36 +999,35 @@ export async function startTUI(agent, opts = {}) {
|
|
|
957
999
|
setTimeout(() => process.exit(0), 100) // 延迟一拍:fetch 后立刻 exit 在 Windows/Node 24 会触发 libuv 断言
|
|
958
1000
|
return
|
|
959
1001
|
case "/session": {
|
|
960
|
-
const
|
|
961
|
-
if (
|
|
962
|
-
|
|
963
|
-
const data = switchToSlot(agent.cwd, slotNum)
|
|
964
|
-
if (!data) {
|
|
965
|
-
pushLine(`槽位 ${slotNum} 不存在`, C.dim)
|
|
966
|
-
} else {
|
|
967
|
-
applySession(agent, data)
|
|
968
|
-
state.lines = data.display.length
|
|
969
|
-
? data.display.map((l) => ({ text: l.text, color: l.color }))
|
|
970
|
-
: []
|
|
971
|
-
state.tasks = agent.tasks ?? []
|
|
972
|
-
// 切换过来的会话如果任务全完成,自动收起面板
|
|
973
|
-
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
974
|
-
state.tasks = []
|
|
975
|
-
}
|
|
976
|
-
pushLabel(`── 已切换到槽位 ${slotNum}(${data.history.length} 条消息)──`, C.warn)
|
|
977
|
-
render()
|
|
978
|
-
}
|
|
1002
|
+
const slots = listSlots(agent.cwd)
|
|
1003
|
+
if (slots.length === 0) {
|
|
1004
|
+
pushLine("没有归档会话(用 /new 后旧会话会自动归档)", C.dim)
|
|
979
1005
|
} else {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1006
|
+
const entries = [
|
|
1007
|
+
{ type: "header", text: "归档会话(↑↓ 选择, Enter 切换, Esc 取消)" },
|
|
1008
|
+
...slots.map((s) => ({ type: "item", text: `槽位 ${s.slot} — ${s.date}`, slot: s.slot })),
|
|
1009
|
+
]
|
|
1010
|
+
openPicker({
|
|
1011
|
+
title: "切换会话",
|
|
1012
|
+
entries,
|
|
1013
|
+
onSelect: (e) => {
|
|
1014
|
+
const data = switchToSlot(agent.cwd, e.slot)
|
|
1015
|
+
if (!data) {
|
|
1016
|
+
pushLine(`槽位 ${e.slot} 不存在`, C.dim)
|
|
1017
|
+
return
|
|
1018
|
+
}
|
|
1019
|
+
applySession(agent, data)
|
|
1020
|
+
state.lines = data.display.length
|
|
1021
|
+
? data.display.map((l) => ({ text: l.text, color: l.color }))
|
|
1022
|
+
: []
|
|
1023
|
+
state.tasks = agent.tasks ?? []
|
|
1024
|
+
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
1025
|
+
state.tasks = []
|
|
1026
|
+
}
|
|
1027
|
+
pushLabel(`── 已切换到槽位 ${e.slot}(${data.history.length} 条消息)──`, C.warn)
|
|
1028
|
+
render()
|
|
1029
|
+
},
|
|
1030
|
+
})
|
|
990
1031
|
}
|
|
991
1032
|
return
|
|
992
1033
|
}
|
|
@@ -1107,27 +1148,33 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1107
1148
|
pushLine("[rewind] 当前目录不是 git 仓库,无法使用存档点", C.error)
|
|
1108
1149
|
return
|
|
1109
1150
|
}
|
|
1110
|
-
const
|
|
1111
|
-
if (
|
|
1112
|
-
|
|
1113
|
-
pushLabel(`❯ Checkpoints`, ansi.bold + C.tool)
|
|
1114
|
-
if (cps.length === 0) {
|
|
1115
|
-
pushLine("(暂无存档点——每次提交任务前自动创建)", C.dim)
|
|
1116
|
-
}
|
|
1117
|
-
for (const cp of cps.slice(0, 10)) {
|
|
1118
|
-
pushLine(` ${cp.id} ${new Date(cp.time).toLocaleString()} (+${cp.untracked} 个未跟踪文件)`, C.dim)
|
|
1119
|
-
}
|
|
1120
|
-
pushLine("回滚: /rewind <id>(恢复前会先存当前状态,回滚可逆)", C.dim)
|
|
1151
|
+
const cps = await listCheckpoints(agent.cwd)
|
|
1152
|
+
if (cps.length === 0) {
|
|
1153
|
+
pushLine("(暂无存档点——每次提交任务前自动创建)", C.dim)
|
|
1121
1154
|
return
|
|
1122
1155
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1156
|
+
const entries = [
|
|
1157
|
+
{ type: "header", text: "存档点(↑↓ 选择, Enter 回滚, Esc 取消)" },
|
|
1158
|
+
...cps.slice(0, 12).map((cp) => ({
|
|
1159
|
+
type: "item",
|
|
1160
|
+
text: `${cp.id} ${new Date(cp.time).toLocaleString()} (+${cp.untracked} 个未跟踪文件)`,
|
|
1161
|
+
id: cp.id,
|
|
1162
|
+
})),
|
|
1163
|
+
]
|
|
1164
|
+
openPicker({
|
|
1165
|
+
title: "回滚存档点",
|
|
1166
|
+
entries,
|
|
1167
|
+
onSelect: async (e) => {
|
|
1168
|
+
try {
|
|
1169
|
+
const summary = await rewind(agent.cwd, e.id)
|
|
1170
|
+
pushLabel(`❯ Rewind`, ansi.bold + C.warn)
|
|
1171
|
+
pushLine(`已回滚到 ${e.id}:补丁${summary.patchApplied ? "已应用" : "无"},删除新建文件 ${summary.deleted} 个,还原文件 ${summary.restored} 个`, C.tool)
|
|
1172
|
+
pushLine("(当前状态已先存为新存档点,可再次 /rewind 回到刚才)", C.dim)
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
pushLine(`[rewind] ${error.message}`, C.error)
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
})
|
|
1131
1178
|
return
|
|
1132
1179
|
}
|
|
1133
1180
|
case "/plan": {
|
|
@@ -1148,37 +1195,46 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1148
1195
|
return
|
|
1149
1196
|
}
|
|
1150
1197
|
case "/goal": {
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
const semi = text.indexOf(";") >= 0 ? ";" : text.indexOf(";") >= 0 ? ";" : null
|
|
1156
|
-
const objective = semi ? text.slice(0, semi).trim() : text.trim()
|
|
1157
|
-
const criteria = semi ? text.slice(semi + 1).trim() : ""
|
|
1158
|
-
agent.goal = { objective, criteria, setAt: Date.now(), status: "active", turnsUsed: 0, _blockTally: null }
|
|
1159
|
-
pushLabel(`❯ Goal`, ansi.bold + C.warn)
|
|
1160
|
-
pushLine(`目标已设置: ${objective}`, C.tool)
|
|
1161
|
-
if (criteria) pushLine(` 完成条件: ${criteria}`, C.dim)
|
|
1162
|
-
else pushLine(` ⚠ 未完成条件——agent 用 goal set 设立时会被要求补上可验证的完成条件`, C.warn)
|
|
1163
|
-
return
|
|
1164
|
-
}
|
|
1165
|
-
if (sub === "cancel") {
|
|
1166
|
-
agent.goal = null
|
|
1167
|
-
pushLabel(`❯ Goal`, ansi.bold + C.dim)
|
|
1168
|
-
pushLine(`目标已取消。`, C.dim)
|
|
1169
|
-
return
|
|
1170
|
-
}
|
|
1198
|
+
const entries = [
|
|
1199
|
+
{ type: "header", text: agent.goal ? `当前目标: ${agent.goal.objective.slice(0, 60)}` : "操作" },
|
|
1200
|
+
{ type: "item", text: "设置新目标", action: "set" },
|
|
1201
|
+
]
|
|
1171
1202
|
if (agent.goal) {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
pushLine(`目标: ${agent.goal.objective}`, C.tool)
|
|
1175
|
-
if (agent.goal.criteria) pushLine(` 完成条件: ${agent.goal.criteria}`, C.dim)
|
|
1176
|
-
pushLine(` 状态: ${statusText ?? "进行中"} │ 已用轮数: ${agent.goal.turnsUsed ?? 0} │ 设置于: ${new Date(agent.goal.setAt).toLocaleString()}`, C.dim)
|
|
1177
|
-
pushLine("操作: /goal set <描述> 覆盖 | /goal cancel 取消", C.dim)
|
|
1178
|
-
} else {
|
|
1179
|
-
pushLabel(`❯ Goal`, ansi.bold + C.dim)
|
|
1180
|
-
pushLine("(无活跃目标——/goal set <描述> 设置)", C.dim)
|
|
1203
|
+
entries.push({ type: "item", text: "取消目标", action: "cancel" })
|
|
1204
|
+
entries.push({ type: "item", text: "查看详情", action: "view" })
|
|
1181
1205
|
}
|
|
1206
|
+
openPicker({
|
|
1207
|
+
title: "目标管理",
|
|
1208
|
+
entries,
|
|
1209
|
+
onSelect: (e) => {
|
|
1210
|
+
if (e.action === "view") {
|
|
1211
|
+
const statusText = { active: "进行中", complete: "已完成", blocked: "已阻塞" }[agent.goal.status] ?? agent.goal.status
|
|
1212
|
+
pushLabel(`❯ Goal`, ansi.bold + C.warn)
|
|
1213
|
+
pushLine(`目标: ${agent.goal.objective}`, C.tool)
|
|
1214
|
+
if (agent.goal.criteria) pushLine(` 完成条件: ${agent.goal.criteria}`, C.dim)
|
|
1215
|
+
pushLine(` 状态: ${statusText} │ 已用轮数: ${agent.goal.turnsUsed ?? 0} │ 设置于: ${new Date(agent.goal.setAt).toLocaleString()}`, C.dim)
|
|
1216
|
+
return
|
|
1217
|
+
}
|
|
1218
|
+
if (e.action === "cancel") {
|
|
1219
|
+
agent.goal = null
|
|
1220
|
+
pushLabel(`❯ Goal`, ansi.bold + C.dim)
|
|
1221
|
+
pushLine(`目标已取消。`, C.dim)
|
|
1222
|
+
return
|
|
1223
|
+
}
|
|
1224
|
+
// set — 需要输入目标文本
|
|
1225
|
+
askQuestion("请输入目标描述(; 分隔完成条件)").then((text) => {
|
|
1226
|
+
if (!text) return
|
|
1227
|
+
const semi = text.indexOf(";") >= 0 ? ";" : text.indexOf(";") >= 0 ? ";" : null
|
|
1228
|
+
const objective = semi ? text.slice(0, semi).trim() : text.trim()
|
|
1229
|
+
const criteria = semi ? text.slice(semi + 1).trim() : ""
|
|
1230
|
+
agent.goal = { objective, criteria, setAt: Date.now(), status: "active", turnsUsed: 0, _blockTally: null }
|
|
1231
|
+
pushLabel(`❯ Goal`, ansi.bold + C.warn)
|
|
1232
|
+
pushLine(`目标已设置: ${objective}`, C.tool)
|
|
1233
|
+
if (criteria) pushLine(` 完成条件: ${criteria}`, C.dim)
|
|
1234
|
+
else pushLine(` ⚠ 未完成条件——agent 用 goal set 设立时会被要求补上可验证的完成条件`, C.warn)
|
|
1235
|
+
})
|
|
1236
|
+
},
|
|
1237
|
+
})
|
|
1182
1238
|
return
|
|
1183
1239
|
}
|
|
1184
1240
|
case "/skills": {
|
|
@@ -1195,93 +1251,107 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1195
1251
|
return
|
|
1196
1252
|
}
|
|
1197
1253
|
case "/mcp": {
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const color = connected ? C.tool : C.dim
|
|
1210
|
-
const toolCount = agent.tools.filter((t) => t._mcpName === srv.name).length
|
|
1211
|
-
const desc = srv.wsUrl ? srv.wsUrl : srv.url ? srv.url : `${srv.command} ${(srv.args ?? []).join(" ")}`
|
|
1212
|
-
pushLine(` ${mark} ${srv.name}: ${desc} (${toolCount} tools)`, color)
|
|
1213
|
-
}
|
|
1214
|
-
pushLabel(`❯ 操作`, ansi.bold + C.tool)
|
|
1215
|
-
pushLine("/mcp add <name> <url|command> [args|headers...]", C.dim)
|
|
1216
|
-
pushLine(" URL 自动识别: https://… → HTTP, ws://… → WebSocket, 其他 → stdio 命令", C.dim)
|
|
1217
|
-
pushLine(" 例: /mcp add myapi https://api.example.com/mcp Authorization=\"Bearer x\"", C.dim)
|
|
1218
|
-
pushLine(" 例: /mcp add github npx -y @modelcontextprotocol/server-github", C.dim)
|
|
1219
|
-
pushLine(`/mcp remove <name> 断开并移除`, C.dim)
|
|
1220
|
-
pushLine(`/mcp connect <name> 重连已配置的 server`, C.dim)
|
|
1221
|
-
return
|
|
1222
|
-
}
|
|
1223
|
-
// ---- /mcp add <name> <url|command> [args|headers...] (统一入口,自动识别传输类型) ----
|
|
1224
|
-
// url / ws 子命令作为别名保留(兼容旧配置)
|
|
1225
|
-
if (sub === "add" || sub === "url" || sub === "ws") {
|
|
1226
|
-
const args = rest.slice(1)
|
|
1227
|
-
if (args.length < 2) {
|
|
1228
|
-
pushLine("用法: /mcp add <name> <url|command> [args|headers...]", C.error)
|
|
1229
|
-
pushLine(" URL 自动识别: https://… → HTTP, ws://… → WebSocket, 其他 → stdio 命令", C.dim)
|
|
1230
|
-
return
|
|
1231
|
-
}
|
|
1232
|
-
const name = args[0]
|
|
1233
|
-
const second = args[1]
|
|
1234
|
-
const extras = args.slice(2)
|
|
1235
|
-
const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
|
|
1236
|
-
if (existing) { pushLine(`[mcp] "${name}" 已存在,用 /mcp remove ${name} 先移除`, C.error); return }
|
|
1237
|
-
|
|
1238
|
-
const isWS = /^wss?:\/\//.test(second)
|
|
1239
|
-
const isHTTP = /^https?:\/\//.test(second)
|
|
1240
|
-
let srv
|
|
1241
|
-
if (isWS || sub === "ws") {
|
|
1242
|
-
const headers = parseHeaders(extras)
|
|
1243
|
-
srv = { name, wsUrl: second, headers: Object.keys(headers).length > 0 ? headers : undefined }
|
|
1244
|
-
} else if (isHTTP || sub === "url") {
|
|
1245
|
-
const headers = parseHeaders(extras)
|
|
1246
|
-
srv = { name, url: second, headers: Object.keys(headers).length > 0 ? headers : undefined }
|
|
1247
|
-
} else {
|
|
1248
|
-
srv = { name, command: second, args: extras.length > 0 ? extras : undefined }
|
|
1249
|
-
}
|
|
1250
|
-
await addAndConnect(srv)
|
|
1251
|
-
return
|
|
1252
|
-
}
|
|
1253
|
-
// ---- /mcp remove <name> ----
|
|
1254
|
-
if (sub === "remove") {
|
|
1255
|
-
const name = rest[1]
|
|
1256
|
-
if (!name) { pushLine("用法: /mcp remove <name>", C.error); return }
|
|
1257
|
-
const { removeMcpTools } = await import("./mcp.mjs")
|
|
1258
|
-
removeMcpTools(agent, name)
|
|
1259
|
-
await persistRaw((raw) => { raw.mcp ??= { servers: [] }; raw.mcp.servers = raw.mcp.servers.filter((s) => s.name !== name) })
|
|
1260
|
-
if (agent.config?.mcp?.servers) agent.config.mcp.servers = agent.config.mcp.servers.filter((s) => s.name !== name)
|
|
1261
|
-
pushLabel(`❯ MCP`, ansi.bold + C.tool)
|
|
1262
|
-
pushLine(`${name} 已断开并从配置移除。`, C.tool)
|
|
1263
|
-
return
|
|
1264
|
-
}
|
|
1265
|
-
// ---- /mcp connect <name> — 重连 ----
|
|
1266
|
-
if (sub === "connect") {
|
|
1267
|
-
const name = rest[1]
|
|
1268
|
-
if (!name) { pushLine("用法: /mcp connect <name>", C.error); return }
|
|
1269
|
-
const srv = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
|
|
1270
|
-
if (!srv) { pushLine(`[mcp] "${name}" 未在配置中找到(先用 /mcp add 添加)`, C.error); return }
|
|
1271
|
-
const { removeMcpTools, connectMcpServer } = await import("./mcp.mjs")
|
|
1272
|
-
removeMcpTools(agent, name)
|
|
1273
|
-
try {
|
|
1274
|
-
pushLine(`[mcp] 重连 ${name}...`, C.dim)
|
|
1275
|
-
const tools = await connectMcpServer(srv)
|
|
1276
|
-
agent.tools.push(...tools)
|
|
1277
|
-
pushLabel(`❯ MCP`, ansi.bold + C.tool)
|
|
1278
|
-
pushLine(`${name} 已重连,${tools.length} 个工具可用。`, C.tool)
|
|
1279
|
-
} catch (error) {
|
|
1280
|
-
pushLine(`[mcp] ${name}: ${error.message}`, C.error)
|
|
1281
|
-
}
|
|
1282
|
-
return
|
|
1254
|
+
const servers = agent.config?.mcp?.servers ?? []
|
|
1255
|
+
const entries = [
|
|
1256
|
+
{ type: "header", text: `已配置 ${servers.length} 个 MCP server` },
|
|
1257
|
+
{ type: "item", text: "查看列表", action: "list" },
|
|
1258
|
+
{ type: "item", text: "添加服务器", action: "add" },
|
|
1259
|
+
]
|
|
1260
|
+
if (servers.length > 0) {
|
|
1261
|
+
entries.push(
|
|
1262
|
+
{ type: "item", text: "移除服务器", action: "remove" },
|
|
1263
|
+
{ type: "item", text: "重连服务器", action: "connect" },
|
|
1264
|
+
)
|
|
1283
1265
|
}
|
|
1284
|
-
|
|
1266
|
+
openPicker({
|
|
1267
|
+
title: "MCP 管理",
|
|
1268
|
+
entries,
|
|
1269
|
+
onSelect: async (e) => {
|
|
1270
|
+
if (e.action === "list") {
|
|
1271
|
+
pushLabel(`❯ MCP Servers`, ansi.bold + C.tool)
|
|
1272
|
+
if (servers.length === 0) {
|
|
1273
|
+
pushLine("(无 MCP server)", C.dim)
|
|
1274
|
+
}
|
|
1275
|
+
for (const srv of servers) {
|
|
1276
|
+
const connected = agent.tools.some((t) => t._mcpName === srv.name)
|
|
1277
|
+
const mark = connected ? "●" : "○"
|
|
1278
|
+
const color = connected ? C.tool : C.dim
|
|
1279
|
+
const toolCount = agent.tools.filter((t) => t._mcpName === srv.name).length
|
|
1280
|
+
const desc = srv.wsUrl ? srv.wsUrl : srv.url ? srv.url : `${srv.command} ${(srv.args ?? []).join(" ")}`
|
|
1281
|
+
pushLine(` ${mark} ${srv.name}: ${desc} (${toolCount} tools)`, color)
|
|
1282
|
+
}
|
|
1283
|
+
return
|
|
1284
|
+
}
|
|
1285
|
+
if (e.action === "remove") {
|
|
1286
|
+
const removeEntries = [
|
|
1287
|
+
{ type: "header", text: "选择要移除的服务器" },
|
|
1288
|
+
...servers.map((s) => ({ type: "item", text: s.name, name: s.name })),
|
|
1289
|
+
]
|
|
1290
|
+
openPicker({
|
|
1291
|
+
title: "移除 MCP",
|
|
1292
|
+
entries: removeEntries,
|
|
1293
|
+
onSelect: async (se) => {
|
|
1294
|
+
const { removeMcpTools } = await import("./mcp.mjs")
|
|
1295
|
+
removeMcpTools(agent, se.name)
|
|
1296
|
+
await persistRaw((raw) => { raw.mcp ??= { servers: [] }; raw.mcp.servers = raw.mcp.servers.filter((s) => s.name !== se.name) })
|
|
1297
|
+
if (agent.config?.mcp?.servers) agent.config.mcp.servers = agent.config.mcp.servers.filter((s) => s.name !== se.name)
|
|
1298
|
+
pushLabel(`❯ MCP`, ansi.bold + C.tool)
|
|
1299
|
+
pushLine(`${se.name} 已断开并从配置移除。`, C.tool)
|
|
1300
|
+
},
|
|
1301
|
+
})
|
|
1302
|
+
return
|
|
1303
|
+
}
|
|
1304
|
+
if (e.action === "connect") {
|
|
1305
|
+
const connEntries = [
|
|
1306
|
+
{ type: "header", text: "选择要重连的服务器" },
|
|
1307
|
+
...servers.map((s) => ({ type: "item", text: s.name, name: s.name })),
|
|
1308
|
+
]
|
|
1309
|
+
openPicker({
|
|
1310
|
+
title: "重连 MCP",
|
|
1311
|
+
entries: connEntries,
|
|
1312
|
+
onSelect: async (se) => {
|
|
1313
|
+
const srv = servers.find((s) => s.name === se.name)
|
|
1314
|
+
if (!srv) return
|
|
1315
|
+
const { removeMcpTools, connectMcpServer } = await import("./mcp.mjs")
|
|
1316
|
+
removeMcpTools(agent, se.name)
|
|
1317
|
+
try {
|
|
1318
|
+
pushLine(`[mcp] 重连 ${se.name}...`, C.dim)
|
|
1319
|
+
const tools = await connectMcpServer(srv)
|
|
1320
|
+
agent.tools.push(...tools)
|
|
1321
|
+
pushLabel(`❯ MCP`, ansi.bold + C.tool)
|
|
1322
|
+
pushLine(`${se.name} 已重连,${tools.length} 个工具可用。`, C.tool)
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
pushLine(`[mcp] ${se.name}: ${error.message}`, C.error)
|
|
1325
|
+
}
|
|
1326
|
+
},
|
|
1327
|
+
})
|
|
1328
|
+
return
|
|
1329
|
+
}
|
|
1330
|
+
if (e.action === "add") {
|
|
1331
|
+
askQuestion("输入: <名称> <URL|命令> [参数...]\nURL 自动识别: https://… → HTTP, ws://… → WebSocket, 其他 → stdio 命令").then(async (text) => {
|
|
1332
|
+
if (!text) return
|
|
1333
|
+
const parts = text.split(/\s+/)
|
|
1334
|
+
if (parts.length < 2) { pushLine("用法: <名称> <URL|命令> [参数...]", C.error); return }
|
|
1335
|
+
const [name, second, ...extras] = parts
|
|
1336
|
+
const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
|
|
1337
|
+
if (existing) { pushLine(`[mcp] "${name}" 已存在`, C.error); return }
|
|
1338
|
+
const isWS = /^wss?:\/\//.test(second)
|
|
1339
|
+
const isHTTP = /^https?:\/\//.test(second)
|
|
1340
|
+
let srv
|
|
1341
|
+
if (isWS) {
|
|
1342
|
+
const headers = parseHeaders(extras)
|
|
1343
|
+
srv = { name, wsUrl: second, headers: Object.keys(headers).length > 0 ? headers : undefined }
|
|
1344
|
+
} else if (isHTTP) {
|
|
1345
|
+
const headers = parseHeaders(extras)
|
|
1346
|
+
srv = { name, url: second, headers: Object.keys(headers).length > 0 ? headers : undefined }
|
|
1347
|
+
} else {
|
|
1348
|
+
srv = { name, command: second, args: extras.length > 0 ? extras : undefined }
|
|
1349
|
+
}
|
|
1350
|
+
await addAndConnect(srv)
|
|
1351
|
+
})
|
|
1352
|
+
}
|
|
1353
|
+
},
|
|
1354
|
+
})
|
|
1285
1355
|
return
|
|
1286
1356
|
}
|
|
1287
1357
|
|
|
@@ -1338,235 +1408,235 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1338
1408
|
)
|
|
1339
1409
|
return
|
|
1340
1410
|
case "/think": {
|
|
1341
|
-
const sub = rest[0]
|
|
1342
1411
|
const cur = agent.provider
|
|
1343
1412
|
const thinkingEnabled = cur.thinking?.type === "enabled" || cur.thinking?.type === undefined
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
else
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1413
|
+
const { specForModel } = await import("./config.mjs")
|
|
1414
|
+
const spec = specForModel(cur.model)
|
|
1415
|
+
const isEffortOnly = spec.thinkApi === "effort"
|
|
1416
|
+
|
|
1417
|
+
const entries = [
|
|
1418
|
+
{ type: "header", text: "思维模式" },
|
|
1419
|
+
{ type: "item", text: `开启${thinkingEnabled ? " ← 当前" : ""}`, action: "on" },
|
|
1420
|
+
{ type: "item", text: `关闭${!thinkingEnabled ? " ← 当前" : ""}`, action: "off" },
|
|
1421
|
+
{ type: "header", text: "推理强度" },
|
|
1422
|
+
...["low", "high", "max"].map((l) => ({
|
|
1423
|
+
type: "item",
|
|
1424
|
+
text: `${l}${cur.reasoningEffort === l ? " ← 当前" : ""}`,
|
|
1425
|
+
action: "effort",
|
|
1426
|
+
level: l,
|
|
1427
|
+
})),
|
|
1428
|
+
]
|
|
1429
|
+
openPicker({
|
|
1430
|
+
title: "思维模式",
|
|
1431
|
+
entries,
|
|
1432
|
+
defaultIndex: thinkingEnabled ? 0 : 1,
|
|
1433
|
+
onSelect: async (e) => {
|
|
1434
|
+
if (e.action === "effort") {
|
|
1435
|
+
cur.reasoningEffort = e.level
|
|
1436
|
+
await syncProviderField("reasoningEffort", e.level)
|
|
1437
|
+
pushLabel(`❯ Think`, ansi.bold + C.tool)
|
|
1438
|
+
pushLine(`推理强度已设为 ${e.level}`, C.tool)
|
|
1439
|
+
} else {
|
|
1440
|
+
const enable = e.action === "on"
|
|
1441
|
+
if (isEffortOnly) {
|
|
1442
|
+
if (!enable) delete cur.reasoningEffort
|
|
1443
|
+
else if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
1444
|
+
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
1445
|
+
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
1446
|
+
} else {
|
|
1447
|
+
cur.thinking = enable ? { type: "enabled" } : { type: "disabled" }
|
|
1448
|
+
if (!enable) delete cur.reasoningEffort
|
|
1449
|
+
else if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
1450
|
+
await syncProviderField("thinking", cur.thinking)
|
|
1451
|
+
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
1452
|
+
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
1453
|
+
}
|
|
1454
|
+
pushLabel(`❯ Think`, ansi.bold + C.tool)
|
|
1455
|
+
pushLine(`思维模式已${enable ? "开启" : "关闭"}`, C.tool)
|
|
1456
|
+
if (enable) pushLine(`推理强度: ${cur.reasoningEffort}`, C.dim)
|
|
1457
|
+
}
|
|
1458
|
+
},
|
|
1459
|
+
})
|
|
1391
1460
|
return
|
|
1392
1461
|
}
|
|
1393
1462
|
case "/model": {
|
|
1394
|
-
|
|
1395
|
-
if (!arg) {
|
|
1396
|
-
// 打开交互选择器:全部 provider 的全部模型,方向键选择
|
|
1397
|
-
pushLabel(`❯ Model`, ansi.bold + C.tool)
|
|
1398
|
-
pushLine(`/model <名称> 直接切换 provider 或模型(如 /model deepseek-v4-pro)`, C.dim)
|
|
1399
|
-
pushLine(`/provider 管理 provider(添加/删除/配 key)`, C.dim)
|
|
1400
|
-
openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
|
|
1401
|
-
return
|
|
1402
|
-
}
|
|
1403
|
-
if (arg === "add" || arg === "--add") {
|
|
1404
|
-
pushLine(`添加 provider 已移到 /provider add(/provider 查看全部管理命令)`, C.warn)
|
|
1405
|
-
return
|
|
1406
|
-
}
|
|
1407
|
-
// 一个参数两种含义:先按 provider 名匹配,匹配不到就当模型名改当前 provider
|
|
1408
|
-
const { resolveCompactThreshold } = await import("./config.mjs")
|
|
1409
|
-
const p = agent.providers.find((pp) => pp.name === arg)
|
|
1410
|
-
const newModel = p ? p.model : arg
|
|
1411
|
-
let thresholdNote = ""
|
|
1412
|
-
if (agent.config?.agent?.compactThresholdAuto) {
|
|
1413
|
-
const { value } = resolveCompactThreshold(null, newModel)
|
|
1414
|
-
agent.config.agent.compactThreshold = value
|
|
1415
|
-
thresholdNote = `,压缩阈值随模型调整为 ${value}`
|
|
1416
|
-
}
|
|
1417
|
-
if (p) {
|
|
1418
|
-
agent.activeProvider = arg
|
|
1419
|
-
agent.provider = { ...p }
|
|
1420
|
-
// key 的环境变量兜底和 loadConfig 保持一致(提供商专用变量只对同名生效)
|
|
1421
|
-
if (!agent.provider.apiKey) {
|
|
1422
|
-
const envKey = { deepseek: "DEEPSEEK_API_KEY", openai: "OPENAI_API_KEY" }[arg]
|
|
1423
|
-
if (envKey && process.env[envKey]) agent.provider.apiKey = process.env[envKey]
|
|
1424
|
-
}
|
|
1425
|
-
if (!agent.provider.apiKey) agent.provider.apiKey = process.env.THINCODER_API_KEY
|
|
1426
|
-
await persistRaw((raw) => { raw.activeProvider = arg })
|
|
1427
|
-
agent.config.activeProvider = arg
|
|
1428
|
-
pushLabel(`❯ Model`, ansi.bold + C.tool)
|
|
1429
|
-
pushLine(`已切换到 ${arg} / ${p.model}${thresholdNote}(已持久化)`, C.tool)
|
|
1430
|
-
if (!agent.provider.apiKey) pushLine(`该 provider 还没配 key: /provider key <apikey>`, C.warn)
|
|
1431
|
-
} else {
|
|
1432
|
-
const target = agent.providers.find((pp) => pp.name === agent.activeProvider) ?? agent.providers[0]
|
|
1433
|
-
if (target) target.model = arg
|
|
1434
|
-
agent.provider.model = arg
|
|
1435
|
-
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
1436
|
-
pushLabel(`❯ Model`, ansi.bold + C.tool)
|
|
1437
|
-
pushLine(`已将 ${target?.name ?? agent.activeProvider} 的模型改为 ${arg}${thresholdNote}(已持久化)`, C.tool)
|
|
1438
|
-
}
|
|
1463
|
+
openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
|
|
1439
1464
|
return
|
|
1440
1465
|
}
|
|
1441
1466
|
case "/provider": {
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
if (agent.providers.some((p) => p.name === name)) {
|
|
1451
|
-
pushLine(`"${name}" 已存在;要重建可先 /provider remove ${name}`, C.warn)
|
|
1452
|
-
return
|
|
1453
|
-
}
|
|
1454
|
-
const preset = PRESETS[name]
|
|
1455
|
-
const baseURL = (rest[2] ?? preset?.baseURL)?.replace(/\/+$/, "")
|
|
1456
|
-
const model = rest[3] ?? preset?.model
|
|
1457
|
-
if (!baseURL || !model) {
|
|
1458
|
-
pushLine(`缺少参数: /provider add ${name} <baseURL> <模型>`, C.error)
|
|
1459
|
-
if (!preset) pushLine(`("${name}" 不是预设;预设: ${Object.keys(PRESETS).join(", ")})`, C.dim)
|
|
1460
|
-
return
|
|
1461
|
-
}
|
|
1462
|
-
if (!/^https?:\/\//.test(baseURL)) { pushLine(`baseURL 应以 http(s):// 开头`, C.error); return }
|
|
1463
|
-
agent.providers.push({ name, baseURL, model, ...(preset?.desc ? { desc: preset.desc } : {}) })
|
|
1464
|
-
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
1465
|
-
pushLabel(`❯ Provider`, ansi.bold + C.tool)
|
|
1466
|
-
pushLine(`已添加 ${name}(${baseURL} / ${model})`, C.tool)
|
|
1467
|
-
pushLine(`下一步: /provider key ${name} <apikey> 配 key,/model ${name} 切换`, C.dim)
|
|
1468
|
-
return
|
|
1469
|
-
}
|
|
1470
|
-
// ---- /provider remove <名称> ----
|
|
1471
|
-
if (sub === "remove" || sub === "rm") {
|
|
1472
|
-
const name = rest[1]
|
|
1473
|
-
if (!name) { pushLine("用法: /provider remove <名称>", C.error); return }
|
|
1474
|
-
const at = agent.providers.findIndex((p) => p.name === name)
|
|
1475
|
-
if (at < 0) { pushLine(`未找到 provider "${name}"`, C.error); return }
|
|
1476
|
-
if (name === agent.activeProvider) { pushLine(`"${name}" 正在使用中,先 /model 切换到别的 provider 再删`, C.warn); return }
|
|
1477
|
-
agent.providers.splice(at, 1)
|
|
1478
|
-
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
1479
|
-
pushLabel(`❯ Provider`, ansi.bold + C.tool)
|
|
1480
|
-
pushLine(`已删除 ${name}`, C.tool)
|
|
1481
|
-
return
|
|
1482
|
-
}
|
|
1483
|
-
// ---- /provider key [名称] <apikey> ----
|
|
1484
|
-
if (sub === "key") {
|
|
1485
|
-
let name = agent.activeProvider
|
|
1486
|
-
let keyParts = rest.slice(1)
|
|
1487
|
-
if (rest[1] && agent.providers.some((p) => p.name === rest[1])) {
|
|
1488
|
-
name = rest[1]
|
|
1489
|
-
keyParts = rest.slice(2)
|
|
1490
|
-
}
|
|
1491
|
-
const key = keyParts.join(" ")
|
|
1492
|
-
if (!key) { pushLine("用法: /provider key [名称] <apikey>(不填名称配当前 provider)", C.error); return }
|
|
1493
|
-
await setProviderKey(name, key)
|
|
1494
|
-
return
|
|
1495
|
-
}
|
|
1496
|
-
if (sub) { pushLine(`未知: ${sub}(/provider add | remove | key)`, C.error); return }
|
|
1497
|
-
// ---- /provider(无参): 列表 ----
|
|
1498
|
-
pushLabel(`❯ Providers (${agent.providers.length})`, ansi.bold + C.tool)
|
|
1499
|
-
for (const p of agent.providers) {
|
|
1500
|
-
const active = p.name === agent.activeProvider
|
|
1501
|
-
pushLine(
|
|
1502
|
-
`${active ? " ▸" : " "} ${p.name.padEnd(12)} ${p.model.padEnd(20)} ${p.baseURL}${p.apiKey ? " ●key" : " ○无key"}${active ? " ← 当前" : ""}`,
|
|
1503
|
-
active ? C.tool : C.dim,
|
|
1467
|
+
const entries = [
|
|
1468
|
+
{ type: "header", text: `已配置 ${agent.providers.length} 个 provider` },
|
|
1469
|
+
{ type: "item", text: "查看列表", action: "list" },
|
|
1470
|
+
{ type: "item", text: "添加 provider", action: "add" },
|
|
1471
|
+
]
|
|
1472
|
+
if (agent.providers.length > 0) {
|
|
1473
|
+
entries.push(
|
|
1474
|
+
{ type: "item", text: "移除 provider", action: "remove" },
|
|
1504
1475
|
)
|
|
1505
1476
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1477
|
+
if (!agent.provider.apiKey) {
|
|
1478
|
+
entries.push({ type: "item", text: "设置 API Key", action: "key" })
|
|
1479
|
+
} else {
|
|
1480
|
+
entries.push({ type: "item", text: "更换 API Key", action: "key" })
|
|
1481
|
+
}
|
|
1482
|
+
openPicker({
|
|
1483
|
+
title: "Provider 管理",
|
|
1484
|
+
entries,
|
|
1485
|
+
onSelect: async (e) => {
|
|
1486
|
+
if (e.action === "list") {
|
|
1487
|
+
pushLabel(`❯ Providers (${agent.providers.length})`, ansi.bold + C.tool)
|
|
1488
|
+
for (const p of agent.providers) {
|
|
1489
|
+
const active = p.name === agent.activeProvider
|
|
1490
|
+
pushLine(
|
|
1491
|
+
`${active ? " ▸" : " "} ${p.name.padEnd(12)} ${p.model.padEnd(20)} ${p.baseURL}${p.apiKey ? " ●key" : " ○无key"}${active ? " ← 当前" : ""}`,
|
|
1492
|
+
active ? C.tool : C.dim,
|
|
1493
|
+
)
|
|
1494
|
+
}
|
|
1495
|
+
return
|
|
1496
|
+
}
|
|
1497
|
+
if (e.action === "remove") {
|
|
1498
|
+
const candidates = agent.providers.filter((p) => p.name !== agent.activeProvider)
|
|
1499
|
+
if (candidates.length === 0) {
|
|
1500
|
+
pushLine("只有当前 provider,无法移除(先用 /model 切换到别的 provider)", C.warn)
|
|
1501
|
+
return
|
|
1502
|
+
}
|
|
1503
|
+
const removeEntries = [
|
|
1504
|
+
{ type: "header", text: "选择要移除的 provider(当前使用的不可移除)" },
|
|
1505
|
+
...candidates.map((p) => ({ type: "item", text: `${p.name} (${p.model})`, name: p.name })),
|
|
1506
|
+
]
|
|
1507
|
+
openPicker({
|
|
1508
|
+
title: "移除 Provider",
|
|
1509
|
+
entries: removeEntries,
|
|
1510
|
+
onSelect: async (se) => {
|
|
1511
|
+
const at = agent.providers.findIndex((p) => p.name === se.name)
|
|
1512
|
+
agent.providers.splice(at, 1)
|
|
1513
|
+
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
1514
|
+
pushLabel(`❯ Provider`, ansi.bold + C.tool)
|
|
1515
|
+
pushLine(`已删除 ${se.name}`, C.tool)
|
|
1516
|
+
},
|
|
1517
|
+
})
|
|
1518
|
+
return
|
|
1519
|
+
}
|
|
1520
|
+
if (e.action === "add") {
|
|
1521
|
+
// Add needs text input: name baseURL model
|
|
1522
|
+
askQuestion(
|
|
1523
|
+
`输入: <名称> <baseURL> <模型>\n预设可用: ${Object.keys(PRESETS).join(", ")}\n或只输预设名(如 deepseek)自动补全`,
|
|
1524
|
+
).then(async (text) => {
|
|
1525
|
+
if (!text) return
|
|
1526
|
+
const parts = text.split(/\s+/)
|
|
1527
|
+
const name = parts[0]
|
|
1528
|
+
if (!name) return
|
|
1529
|
+
if (agent.providers.some((p) => p.name === name)) {
|
|
1530
|
+
pushLine(`"${name}" 已存在;先 /provider → 移除`, C.warn)
|
|
1531
|
+
return
|
|
1532
|
+
}
|
|
1533
|
+
const preset = PRESETS[name]
|
|
1534
|
+
const baseURL = (parts[1] ?? preset?.baseURL)?.replace(/\/+$/, "")
|
|
1535
|
+
const model = parts[2] ?? preset?.model
|
|
1536
|
+
if (!baseURL || !model) {
|
|
1537
|
+
pushLine(`缺少参数: ${name} <baseURL> <模型>`, C.error)
|
|
1538
|
+
return
|
|
1539
|
+
}
|
|
1540
|
+
if (!/^https?:\/\//.test(baseURL)) { pushLine(`baseURL 应以 http(s):// 开头`, C.error); return }
|
|
1541
|
+
agent.providers.push({ name, baseURL, model, ...(preset?.desc ? { desc: preset.desc } : {}) })
|
|
1542
|
+
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
1543
|
+
pushLabel(`❯ Provider`, ansi.bold + C.tool)
|
|
1544
|
+
pushLine(`已添加 ${name}(${baseURL} / ${model})`, C.tool)
|
|
1545
|
+
pushLine(`下一步: /provider → 设置 Key`, C.dim)
|
|
1546
|
+
})
|
|
1547
|
+
return
|
|
1548
|
+
}
|
|
1549
|
+
if (e.action === "key") {
|
|
1550
|
+
// Key: pick which provider, then prompt for key
|
|
1551
|
+
const keyEntries = [
|
|
1552
|
+
{ type: "header", text: "选择要配 key 的 provider" },
|
|
1553
|
+
...agent.providers.map((p) => ({
|
|
1554
|
+
type: "item",
|
|
1555
|
+
text: `${p.name}${p.name === agent.activeProvider ? " ← 当前" : ""}${p.apiKey ? " ●已有key" : " ○无key"}`,
|
|
1556
|
+
name: p.name,
|
|
1557
|
+
})),
|
|
1558
|
+
]
|
|
1559
|
+
openPicker({
|
|
1560
|
+
title: "配置 API Key",
|
|
1561
|
+
entries: keyEntries,
|
|
1562
|
+
onSelect: (se) => {
|
|
1563
|
+
askQuestion(`为 ${se.name} 输入 API Key:`).then(async (key) => {
|
|
1564
|
+
if (!key) return
|
|
1565
|
+
await setProviderKey(se.name, key)
|
|
1566
|
+
})
|
|
1567
|
+
},
|
|
1568
|
+
})
|
|
1569
|
+
}
|
|
1570
|
+
},
|
|
1571
|
+
})
|
|
1511
1572
|
return
|
|
1512
1573
|
}
|
|
1513
1574
|
case "/config": {
|
|
1514
|
-
const
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1575
|
+
const entries = [
|
|
1576
|
+
{ type: "header", text: "配置管理" },
|
|
1577
|
+
{ type: "item", text: "查看当前配置", action: "view" },
|
|
1578
|
+
{ type: "item", text: "设置 embedding key(向量检索)", action: "embedkey" },
|
|
1579
|
+
{ type: "item", text: "高级设置(set path value)", action: "set" },
|
|
1580
|
+
]
|
|
1581
|
+
openPicker({
|
|
1582
|
+
title: "配置管理",
|
|
1583
|
+
entries,
|
|
1584
|
+
onSelect: async (e) => {
|
|
1585
|
+
if (e.action === "view") {
|
|
1586
|
+
const { configPath: cp } = await import("./config.mjs")
|
|
1587
|
+
pushLabel(`❯ 配置`, ansi.bold + C.tool)
|
|
1588
|
+
pushLine(`激活: ${agent.activeProvider} / ${agent.provider.model}`, C.dim)
|
|
1589
|
+
pushLine(`Key: ${maskKey(agent.provider.apiKey)}`, C.dim)
|
|
1590
|
+
const ac = agent.config?.agent ?? {}
|
|
1591
|
+
const tn = `${ac.compactThreshold ?? 100000}${ac.compactThresholdAuto ? " (auto)" : ""}`
|
|
1592
|
+
pushLine(`agent: maxTurns=${ac.maxTurns ?? 100} | compactThreshold=${tn}`, C.dim)
|
|
1593
|
+
pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${agent.config?.embedding?.model ?? ""})` : "disabled(纯 FTS 检索)"}`, C.dim)
|
|
1594
|
+
pushLine(`配置文件: ${cp}`, C.dim)
|
|
1595
|
+
return
|
|
1596
|
+
}
|
|
1597
|
+
if (e.action === "embedkey") {
|
|
1598
|
+
askQuestion("输入 embedding 服务的 API Key(默认 SiliconFlow bge-m3):").then(async (key) => {
|
|
1599
|
+
if (!key) return
|
|
1600
|
+
agent.config.embedding ??= {}
|
|
1601
|
+
agent.config.embedding.apiKey = key
|
|
1602
|
+
await persistRaw((raw) => { raw.embedding = { ...(raw.embedding ?? {}), apiKey: key } })
|
|
1603
|
+
if (agent.memory) {
|
|
1604
|
+
const { createEmbedder } = await import("./embedding.mjs")
|
|
1605
|
+
agent.memory.embedder = createEmbedder(agent.config.embedding)
|
|
1606
|
+
}
|
|
1607
|
+
pushLabel(`❯ Config`, ansi.bold + C.tool)
|
|
1608
|
+
pushLine(`embedding key 已保存,向量检索已启用`, C.tool)
|
|
1609
|
+
})
|
|
1610
|
+
return
|
|
1611
|
+
}
|
|
1612
|
+
if (e.action === "set") {
|
|
1613
|
+
askQuestion("输入: <path> <value>(如 agent.maxTurns 80,支持 a.b 嵌套):").then(async (text) => {
|
|
1614
|
+
if (!text) return
|
|
1615
|
+
const parts = text.split(/\s+/)
|
|
1616
|
+
const [path, value] = [parts[0], parts.slice(1).join(" ")]
|
|
1617
|
+
if (!path || !value) { pushLine("用法: <path> <value> 如 agent.maxTurns 80", C.error); return }
|
|
1618
|
+
try {
|
|
1619
|
+
const { configPath, loadConfig, saveConfig } = await import("./config.mjs")
|
|
1620
|
+
const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
|
|
1621
|
+
const keys = path.split(".")
|
|
1622
|
+
let obj = raw
|
|
1623
|
+
for (let i = 0; i < keys.length - 1; i++) { obj[keys[i]] ??= {}; obj = obj[keys[i]] }
|
|
1624
|
+
obj[keys[keys.length - 1]] = isNaN(value) ? value : Number(value)
|
|
1625
|
+
saveConfig(raw)
|
|
1626
|
+
const cfg = loadConfig()
|
|
1627
|
+
agent.provider = cfg.provider
|
|
1628
|
+
agent.providers = cfg.providersList
|
|
1629
|
+
agent.activeProvider = cfg.activeProvider
|
|
1630
|
+
agent.config = cfg
|
|
1631
|
+
pushLabel(`❯ Config`, ansi.bold + C.tool)
|
|
1632
|
+
pushLine(`已保存: ${path} = ${value}`, C.tool)
|
|
1633
|
+
} catch (error) {
|
|
1634
|
+
pushLine(`保存失败: ${error.message}`, C.error)
|
|
1635
|
+
}
|
|
1636
|
+
})
|
|
1637
|
+
}
|
|
1638
|
+
},
|
|
1639
|
+
})
|
|
1570
1640
|
return
|
|
1571
1641
|
}
|
|
1572
1642
|
case "/help": {
|
|
@@ -1674,9 +1744,22 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1674
1744
|
|
|
1675
1745
|
// ---------------------------------------------------------- 模型选择器(/model)
|
|
1676
1746
|
|
|
1677
|
-
const pickerItems = () => state.picker
|
|
1747
|
+
const pickerItems = () => state.picker?.entries.filter((e) => e.type === "item") ?? []
|
|
1748
|
+
|
|
1749
|
+
/** 打开通用列表选择器。entries 含 { type: "header"|"item", text, note?, ...extra },
|
|
1750
|
+
* onSelect 拿到选中条目(含 extra 字段透传),onCancel 在 Esc 时调。 */
|
|
1751
|
+
function openPicker({ title, entries, onSelect, onCancel, defaultIndex = 0 }) {
|
|
1752
|
+
state.picker = { title, entries, lines: [], index: defaultIndex, scroll: 0, selectedLine: 0, onSelect, onCancel }
|
|
1753
|
+
renderPickerLines()
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function closePicker() {
|
|
1757
|
+
state.picker?.onCancel?.()
|
|
1758
|
+
state.picker = null
|
|
1759
|
+
render()
|
|
1760
|
+
}
|
|
1678
1761
|
|
|
1679
|
-
/** 按 entries
|
|
1762
|
+
/** 按 entries 重建显示行并刷新 */
|
|
1680
1763
|
function renderPickerLines() {
|
|
1681
1764
|
const p = state.picker
|
|
1682
1765
|
if (!p) return
|
|
@@ -1685,13 +1768,13 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1685
1768
|
let selectedLine = 0
|
|
1686
1769
|
for (const e of p.entries) {
|
|
1687
1770
|
if (e.type === "header") {
|
|
1688
|
-
lines.push({ text: ` ${e.
|
|
1771
|
+
lines.push({ text: ` ${e.text}${e.note ? ` ${e.note}` : ""}`, color: ansi.bold + C.tool })
|
|
1689
1772
|
} else {
|
|
1690
1773
|
const selected = row === p.index
|
|
1691
1774
|
if (selected) selectedLine = lines.length
|
|
1692
|
-
const
|
|
1775
|
+
const marker = e.marker ? ` ${e.marker}` : ""
|
|
1693
1776
|
lines.push({
|
|
1694
|
-
text: `${selected ? " ▸ " : " "}${e.
|
|
1777
|
+
text: `${selected ? " ▸ " : " "}${e.text}${marker}`,
|
|
1695
1778
|
color: selected ? ansi.bold + C.text : C.dim,
|
|
1696
1779
|
})
|
|
1697
1780
|
row++
|
|
@@ -1702,14 +1785,16 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1702
1785
|
render()
|
|
1703
1786
|
}
|
|
1704
1787
|
|
|
1705
|
-
|
|
1788
|
+
// ========== 模型选择器(基于通用 picker,异步拉取远端模型列表) ==========
|
|
1789
|
+
|
|
1706
1790
|
async function openModelPicker() {
|
|
1707
1791
|
const entries = []
|
|
1708
1792
|
for (const p of agent.providers) {
|
|
1709
|
-
entries.push({ type: "header",
|
|
1710
|
-
entries.push({ type: "item", provider: p.name, model: p.model })
|
|
1793
|
+
entries.push({ type: "header", text: p.name, note: `${p.baseURL}${p.apiKey ? "" : "(未配 key)"} 加载中...` })
|
|
1794
|
+
entries.push({ type: "item", text: p.model, provider: p.name, model: p.model })
|
|
1711
1795
|
}
|
|
1712
|
-
|
|
1796
|
+
const onSelect = (e) => selectModel(e).catch((err) => pushLine(`[error] ${err.message}`, C.error))
|
|
1797
|
+
openPicker({ title: "选择模型", entries, onSelect })
|
|
1713
1798
|
// 默认选中当前在用的模型
|
|
1714
1799
|
const current = pickerItems().findIndex(
|
|
1715
1800
|
(e) => e.provider === agent.activeProvider && e.model === agent.provider.model,
|
|
@@ -1720,10 +1805,9 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1720
1805
|
const { listModels } = await import("./provider.mjs")
|
|
1721
1806
|
await Promise.all(
|
|
1722
1807
|
agent.providers.map(async (p) => {
|
|
1723
|
-
const header = entries.find((e) => e.type === "header" && e.
|
|
1808
|
+
const header = entries.find((e) => e.type === "header" && e.provider === undefined && e.text === p.name)
|
|
1724
1809
|
const noteBase = `${p.baseURL}${p.apiKey ? "" : "(未配 key)"}`
|
|
1725
1810
|
try {
|
|
1726
|
-
// key 的环境变量兜底和 loadConfig 保持一致(提供商专用变量只对同名生效)
|
|
1727
1811
|
const envKey = { deepseek: "DEEPSEEK_API_KEY", openai: "OPENAI_API_KEY" }[p.name]
|
|
1728
1812
|
let apiKey = p.apiKey
|
|
1729
1813
|
if (!apiKey && envKey && process.env[envKey]) apiKey = process.env[envKey]
|
|
@@ -1732,27 +1816,21 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1732
1816
|
{ baseURL: p.baseURL, apiKey: apiKey ?? "" },
|
|
1733
1817
|
{ signal: AbortSignal.timeout(10000) },
|
|
1734
1818
|
)
|
|
1735
|
-
// 展开到该 provider 已配置模型的后面(去重)
|
|
1736
1819
|
const at = entries.findIndex((e) => e.type === "item" && e.provider === p.name && e.model === p.model)
|
|
1737
1820
|
entries.splice(
|
|
1738
1821
|
at + 1,
|
|
1739
1822
|
0,
|
|
1740
|
-
...models.filter((m) => m !== p.model).map((m) => ({ type: "item", provider: p.name, model: m })),
|
|
1823
|
+
...models.filter((m) => m !== p.model).map((m) => ({ type: "item", text: m, provider: p.name, model: m })),
|
|
1741
1824
|
)
|
|
1742
|
-
header.note = noteBase
|
|
1825
|
+
if (header) header.note = noteBase
|
|
1743
1826
|
} catch (error) {
|
|
1744
|
-
header.note = `${noteBase} (拉取失败: ${sliceByWidth(error.message, 60)})`
|
|
1827
|
+
if (header) header.note = `${noteBase} (拉取失败: ${sliceByWidth(error.message, 60)})`
|
|
1745
1828
|
}
|
|
1746
|
-
if (state.picker?.entries === entries) renderPickerLines()
|
|
1829
|
+
if (state.picker?.entries === entries) renderPickerLines()
|
|
1747
1830
|
}),
|
|
1748
1831
|
)
|
|
1749
1832
|
}
|
|
1750
1833
|
|
|
1751
|
-
function closeModelPicker() {
|
|
1752
|
-
state.picker = null
|
|
1753
|
-
render()
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
1834
|
/** 给指定 provider 写 key(内存 + 配置文件);若它是当前激活的,同步运行时 */
|
|
1757
1835
|
async function setProviderKey(name, key) {
|
|
1758
1836
|
const target = agent.providers.find((p) => p.name === name)
|
|
@@ -1920,7 +1998,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1920
1998
|
|
|
1921
1999
|
/** 选中:切换 provider + 模型,持久化,阈值随模型走 */
|
|
1922
2000
|
async function selectModel(item) {
|
|
1923
|
-
|
|
2001
|
+
closePicker()
|
|
1924
2002
|
const target = agent.providers.find((pp) => pp.name === item.provider)
|
|
1925
2003
|
if (!target) return
|
|
1926
2004
|
target.model = item.model
|
|
@@ -2080,11 +2158,11 @@ export async function startTUI(agent, opts = {}) {
|
|
|
2080
2158
|
setTimeout(() => process.exit(0), 100)
|
|
2081
2159
|
}
|
|
2082
2160
|
|
|
2083
|
-
//
|
|
2161
|
+
// 通用列表选择器:↑↓ 移动,Enter 确认,Esc 取消
|
|
2084
2162
|
if (state.picker) {
|
|
2085
2163
|
const items = pickerItems()
|
|
2086
2164
|
if (key.name === "escape") {
|
|
2087
|
-
|
|
2165
|
+
closePicker()
|
|
2088
2166
|
} else if (key.name === "up" && items.length) {
|
|
2089
2167
|
state.picker.index = (state.picker.index - 1 + items.length) % items.length
|
|
2090
2168
|
renderPickerLines()
|
|
@@ -2092,7 +2170,9 @@ export async function startTUI(agent, opts = {}) {
|
|
|
2092
2170
|
state.picker.index = (state.picker.index + 1) % items.length
|
|
2093
2171
|
renderPickerLines()
|
|
2094
2172
|
} else if (key.name === "return" && items.length) {
|
|
2095
|
-
|
|
2173
|
+
const selected = items[state.picker.index]
|
|
2174
|
+
state.picker.onSelect?.(selected)
|
|
2175
|
+
closePicker()
|
|
2096
2176
|
}
|
|
2097
2177
|
return
|
|
2098
2178
|
}
|
|
@@ -2209,7 +2289,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
2209
2289
|
return
|
|
2210
2290
|
}
|
|
2211
2291
|
if (key.name === "return") {
|
|
2212
|
-
submit()
|
|
2292
|
+
submit().catch((e) => pushLine(`[error] ${e.message}`, C.error))
|
|
2213
2293
|
return
|
|
2214
2294
|
}
|
|
2215
2295
|
|