thincoder 0.7.7 → 0.8.0
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 +36 -13
- package/bin/thincoder.mjs +27 -346
- package/package.json +1 -1
- package/src/agent/dispatch.mjs +98 -0
- package/src/agent/helpers.mjs +185 -0
- package/src/agent/setup.mjs +117 -0
- package/src/agent-tools/goal.mjs +71 -0
- package/src/agent-tools/plan.mjs +31 -0
- package/src/agent-tools/recent-changes.mjs +23 -0
- package/src/agent-tools/skill.mjs +46 -0
- package/src/agent-tools/subagent.mjs +113 -0
- package/src/agent-tools/task.mjs +67 -0
- package/src/agent-tools/verify.mjs +198 -0
- package/src/agent-tools.mjs +12 -0
- package/src/agent.mjs +90 -1040
- package/src/cli/distill-command.mjs +85 -0
- package/src/cli/make-agent.mjs +85 -0
- package/src/cli/memory-command.mjs +63 -0
- package/src/cli/permission.mjs +41 -0
- package/src/cli/setup-wizard.mjs +70 -0
- package/src/config.mjs +5 -8
- package/src/context.mjs +10 -13
- package/src/distill.mjs +4 -3
- package/src/embedding.mjs +4 -2
- package/src/{checkpoint.mjs → git/checkpoint.mjs} +1 -1
- package/src/mcp/helpers.mjs +37 -0
- package/src/mcp/transport-http.mjs +176 -0
- package/src/mcp/transport-stdio.mjs +84 -0
- package/src/mcp/transport-ws.mjs +87 -0
- package/src/mcp.mjs +4 -428
- package/src/memory/code-index.mjs +211 -0
- package/src/memory/code-sync.mjs +306 -0
- package/src/memory/core.mjs +277 -0
- package/src/memory/docs.mjs +262 -0
- package/src/memory/schema.mjs +426 -0
- package/src/memory.mjs +12 -1403
- package/src/provider/core.mjs +239 -0
- package/src/provider/index.mjs +6 -0
- package/src/provider/rate.mjs +104 -0
- package/src/session.mjs +18 -5
- package/src/tools/bash.md +1 -0
- package/src/tools/bash.mjs +144 -0
- package/src/tools/file.mjs +205 -0
- package/src/tools/git.mjs +166 -0
- package/src/tools/glob.mjs +51 -0
- package/src/tools/grep.mjs +100 -0
- package/src/tools/index.mjs +22 -0
- package/src/tools/ls.mjs +36 -0
- package/src/tools/patch.mjs +226 -0
- package/src/tools/repomap-parse.mjs +168 -0
- package/src/tools/shared.mjs +257 -0
- package/src/tools/system.mjs +336 -0
- package/src/tools/web.mjs +121 -0
- package/src/tools.mjs +2 -1188
- package/src/tui/agent-turn.mjs +254 -0
- package/src/tui/ansi.mjs +32 -0
- package/src/tui/clipboard.mjs +48 -0
- package/src/tui/cmd-auto.mjs +21 -0
- package/src/tui/cmd-clear.mjs +26 -0
- package/src/tui/cmd-config.mjs +72 -0
- package/src/tui/cmd-exit.mjs +5 -0
- package/src/tui/cmd-extract.mjs +5 -0
- package/src/tui/cmd-goal.mjs +47 -0
- package/src/tui/cmd-help.mjs +25 -0
- package/src/tui/cmd-init.mjs +91 -0
- package/src/tui/cmd-mcp.mjs +146 -0
- package/src/tui/cmd-model.mjs +7 -0
- package/src/tui/cmd-new.mjs +18 -0
- package/src/tui/cmd-plan.mjs +21 -0
- package/src/tui/cmd-reindex.mjs +44 -0
- package/src/tui/cmd-restore.mjs +39 -0
- package/src/tui/cmd-session.mjs +42 -0
- package/src/tui/cmd-skills.mjs +17 -0
- package/src/tui/cmd-think.mjs +56 -0
- package/src/tui/config-helpers.mjs +34 -0
- package/src/tui/distill-cmd.mjs +45 -0
- package/src/tui/index.mjs +330 -0
- package/src/tui/interaction.mjs +79 -0
- package/src/tui/key-handler.mjs +267 -0
- package/src/tui/layout.mjs +115 -0
- package/src/tui/pickers.mjs +279 -0
- package/src/tui/render-frame.mjs +304 -0
- package/src/tui/render.mjs +205 -0
- package/src/tui/slash-commands.mjs +138 -0
- package/src/tui/startup.mjs +113 -0
- package/src/tui/wizard.mjs +168 -0
- package/src/tui-render.mjs +4 -0
- package/src/tui.mjs +3 -2546
- package/src/provider.mjs +0 -383
- /package/src/{gitmem.mjs → git/gitmem.mjs} +0 -0
- /package/src/{coder-overlay.md → prompts/coder.md} +0 -0
- /package/src/{discipline-rules.md → prompts/discipline.md} +0 -0
- /package/src/{explore-overlay.md → prompts/explore.md} +0 -0
- /package/src/{main-overlay.md → prompts/main.md} +0 -0
- /package/src/{plan-overlay.md → prompts/plan.md} +0 -0
- /package/src/{SYSTEM_PROMPT.md → prompts/system.md} +0 -0
- /package/src/{repomap.mjs → tools/repomap.mjs} +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* layout.mjs — TUI 布局引擎(纯函数)
|
|
3
|
+
* 从 state + 终端尺寸计算每个面板的位置和高度。
|
|
4
|
+
* 不修改 state——副作用由调用方在渲染前执行。
|
|
5
|
+
*
|
|
6
|
+
* 面板布局(自上而下):
|
|
7
|
+
* header → conversation → picker → todo → subagent → permission → queue → input → status
|
|
8
|
+
* 固定面板先扣除,条件面板按优先级分配,剩余空间全给 conversation。
|
|
9
|
+
*/
|
|
10
|
+
import { layoutInput, wrapText } from "./render.mjs"
|
|
11
|
+
|
|
12
|
+
const MAX_INPUT_LINES = 5
|
|
13
|
+
const MAX_TASK_LINES = 5
|
|
14
|
+
export const MAX_SUB_LINES = 4
|
|
15
|
+
const QWIN = 5
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 计算布局。返回面板坐标 + 预计算的内容(影响高度的部分)。
|
|
19
|
+
* 纯函数:不修改 state。
|
|
20
|
+
*/
|
|
21
|
+
export function computeLayout(state, { cols, rows }) {
|
|
22
|
+
const W = Math.max(20, cols - 1)
|
|
23
|
+
|
|
24
|
+
// --- 输入框 ---
|
|
25
|
+
const inputLayout = layoutInput(state.input, state.cursor, W - 4)
|
|
26
|
+
let inputOffset = 0
|
|
27
|
+
if (inputLayout.lines.length > MAX_INPUT_LINES) {
|
|
28
|
+
inputOffset = Math.min(inputLayout.cursorLine, inputLayout.lines.length - MAX_INPUT_LINES)
|
|
29
|
+
}
|
|
30
|
+
const inputLines = inputLayout.lines.slice(inputOffset, inputOffset + MAX_INPUT_LINES)
|
|
31
|
+
let boxLines = inputLines
|
|
32
|
+
if (state.question) {
|
|
33
|
+
const q = state.question
|
|
34
|
+
if (q.options.length > 0) {
|
|
35
|
+
const sel = q.selected ?? 0
|
|
36
|
+
const start = Math.max(0, Math.min(sel - 2, q.options.length - QWIN))
|
|
37
|
+
boxLines = q.options.slice(start, start + QWIN).map((opt, i) => (start + i === sel ? "▸ " : " ") + opt)
|
|
38
|
+
} else {
|
|
39
|
+
boxLines = ["▸ " + (q.answer ?? "")]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const inputBoxH = boxLines.length + 2
|
|
43
|
+
|
|
44
|
+
// --- 固定面板 ---
|
|
45
|
+
const headerH = 1
|
|
46
|
+
const statusH = 1
|
|
47
|
+
|
|
48
|
+
// --- 条件面板 ---
|
|
49
|
+
const overlay = state.picker ?? state.wizard
|
|
50
|
+
const pickerH = overlay ? Math.min(overlay.lines.length + 1, Math.max(6, rows - 12)) : 0
|
|
51
|
+
|
|
52
|
+
// Todo
|
|
53
|
+
let visibleTasks = []
|
|
54
|
+
if (state.tasks.length <= MAX_TASK_LINES) {
|
|
55
|
+
visibleTasks = state.tasks
|
|
56
|
+
} else {
|
|
57
|
+
const inProgress = state.tasks.filter((t) => t.status === "in_progress")
|
|
58
|
+
const pending = state.tasks.filter((t) => t.status === "pending")
|
|
59
|
+
const done = state.tasks.filter((t) => t.status === "done")
|
|
60
|
+
visibleTasks = [...inProgress, ...pending, ...done].slice(0, MAX_TASK_LINES)
|
|
61
|
+
}
|
|
62
|
+
const taskPanelH = visibleTasks.length
|
|
63
|
+
|
|
64
|
+
// Subagent (only visible during processing; height matches what's actually rendered)
|
|
65
|
+
const allSubs = state.processing ? Object.values(state.subTasks) : []
|
|
66
|
+
const subPanelH = allSubs.length > 0
|
|
67
|
+
? Math.min(allSubs.length, MAX_SUB_LINES) + (allSubs.length > MAX_SUB_LINES ? 1 : 0)
|
|
68
|
+
: 0
|
|
69
|
+
|
|
70
|
+
// Permission preview (height depends on wrapped content)
|
|
71
|
+
let permPreviewLines = []
|
|
72
|
+
let permPreviewH = 0
|
|
73
|
+
if (state.permission) {
|
|
74
|
+
const maxLines = Math.max(1, rows - 8)
|
|
75
|
+
outer: for (const l of state.permissionPreview) {
|
|
76
|
+
for (const wrapped of wrapText(` ${l}`, W - 1)) {
|
|
77
|
+
if (permPreviewLines.length >= maxLines) break outer
|
|
78
|
+
permPreviewLines.push(wrapped)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
permPreviewH = 1 + permPreviewLines.length // 1 for header line
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Queue preview (1 line when queue has items and processing)
|
|
85
|
+
const queueH = state.queue.length > 0 && state.processing ? 1 : 0
|
|
86
|
+
|
|
87
|
+
// --- 弹性面板:conversation 占剩余空间 ---
|
|
88
|
+
const fixedH = headerH + inputBoxH + statusH + pickerH + taskPanelH + subPanelH + permPreviewH + queueH
|
|
89
|
+
const convH = Math.max(1, rows - fixedH)
|
|
90
|
+
|
|
91
|
+
// --- Y 坐标(0-indexed,ANSI 用时 +1)---
|
|
92
|
+
let y = 0
|
|
93
|
+
const header = { y, h: headerH }; y += headerH
|
|
94
|
+
const conversation = { y, h: convH }; y += convH
|
|
95
|
+
const picker = pickerH > 0 ? { y, h: pickerH } : null; y += pickerH
|
|
96
|
+
const todo = taskPanelH > 0 ? { y, h: taskPanelH } : null; y += taskPanelH
|
|
97
|
+
const subagent = subPanelH > 0 ? { y, h: subPanelH } : null; y += subPanelH
|
|
98
|
+
const permission = permPreviewH > 0 ? { y, h: permPreviewH } : null; y += permPreviewH
|
|
99
|
+
const queue = queueH > 0 ? { y, h: queueH } : null; y += queueH
|
|
100
|
+
const inputBox = { y, h: inputBoxH }; y += inputBoxH
|
|
101
|
+
const status = { y, h: statusH }
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
W, cols, rows,
|
|
105
|
+
panels: { header, conversation, picker, todo, subagent, permission, queue, inputBox, status },
|
|
106
|
+
// 预计算内容(影响高度,渲染时复用)
|
|
107
|
+
inputLayout,
|
|
108
|
+
inputOffset,
|
|
109
|
+
boxLines,
|
|
110
|
+
visibleTasks,
|
|
111
|
+
allSubs,
|
|
112
|
+
permPreviewLines,
|
|
113
|
+
overlay,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { sliceByWidth } from "./render.mjs"
|
|
2
|
+
import { PROVIDER_PRESETS as PRESETS } from "../config.mjs"
|
|
3
|
+
|
|
4
|
+
/** 通用列表选择器 + 统一模型/provider 管理 picker。
|
|
5
|
+
* 通过 createPickers(ctx) 接收闭包依赖,
|
|
6
|
+
* 返回 { openPicker, closePicker, renderPickerLines, openModelPicker, setProviderKey }。 */
|
|
7
|
+
export function createPickers(ctx) {
|
|
8
|
+
const { agent, state, render, ansi, C, pushLine, persistRaw, askQuestion, maskKey } = ctx
|
|
9
|
+
|
|
10
|
+
const pickerItems = () => state.picker?.entries.filter((e) => e.type === "item") ?? []
|
|
11
|
+
|
|
12
|
+
/** 打开通用列表选择器。entries 含 { type: "header"|"item", text, note?, ...extra },
|
|
13
|
+
* onSelect 拿到选中条目 (含 extra 字段透传),onCancel 在 Esc 时调。 */
|
|
14
|
+
function openPicker({ title, entries, onSelect, onCancel, defaultIndex = 0 }) {
|
|
15
|
+
state.picker = { title, entries, lines: [], index: defaultIndex, scroll: 0, selectedLine: 0, onSelect, onCancel }
|
|
16
|
+
renderPickerLines()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function closePicker() {
|
|
20
|
+
state.picker?.onCancel?.()
|
|
21
|
+
state.picker = null
|
|
22
|
+
render()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 按 entries 重建显示行并刷新 */
|
|
26
|
+
function renderPickerLines() {
|
|
27
|
+
const p = state.picker
|
|
28
|
+
if (!p) return
|
|
29
|
+
const lines = []
|
|
30
|
+
let row = 0
|
|
31
|
+
let selectedLine = 0
|
|
32
|
+
for (const e of p.entries) {
|
|
33
|
+
if (e.type === "header") {
|
|
34
|
+
lines.push({ text: ` ${e.text}${e.note ? ` ${e.note}` : ""}`, color: ansi.bold + C.tool })
|
|
35
|
+
} else {
|
|
36
|
+
const selected = row === p.index
|
|
37
|
+
if (selected) selectedLine = lines.length
|
|
38
|
+
const marker = e.marker ? ` ${e.marker}` : ""
|
|
39
|
+
lines.push({
|
|
40
|
+
text: `${selected ? " ▸ " : " "}${e.text}${marker}`,
|
|
41
|
+
color: selected ? ansi.bold + C.text : C.dim,
|
|
42
|
+
})
|
|
43
|
+
row++
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
p.lines = lines
|
|
47
|
+
p.selectedLine = selectedLine
|
|
48
|
+
render()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ========== 统一 picker:模型切换 + provider 管理 ==========
|
|
52
|
+
|
|
53
|
+
async function openModelPicker() {
|
|
54
|
+
const entries = buildModelEntries()
|
|
55
|
+
const onSelect = async (e) => {
|
|
56
|
+
if (e.action === "switch") {
|
|
57
|
+
await selectModel(e).catch((err) => pushLine(`[error] ${err.message}`, C.error))
|
|
58
|
+
} else if (e.action === "add") {
|
|
59
|
+
await addProviderFlow()
|
|
60
|
+
} else if (e.action === "remove") {
|
|
61
|
+
await removeProviderFlow()
|
|
62
|
+
} else if (e.action === "key") {
|
|
63
|
+
await setKeyFlow()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
openPicker({ title: "Models & Providers", entries, onSelect })
|
|
67
|
+
// 默认选中当前在用的模型
|
|
68
|
+
const current = pickerItems().findIndex(
|
|
69
|
+
(e) => e.action === "switch" && e.provider === agent.activeProvider && e.model === agent.provider.model,
|
|
70
|
+
)
|
|
71
|
+
if (current >= 0) state.picker.index = current
|
|
72
|
+
renderPickerLines()
|
|
73
|
+
|
|
74
|
+
// 异步拉取远端模型列表,追加到对应 provider 下
|
|
75
|
+
const { listModels } = await import("../provider/index.mjs")
|
|
76
|
+
await Promise.all(
|
|
77
|
+
agent.providers.map(async (p) => {
|
|
78
|
+
try {
|
|
79
|
+
const envKey = { deepseek: "DEEPSEEK_API_KEY", openai: "OPENAI_API_KEY" }[p.name]
|
|
80
|
+
let apiKey = p.apiKey
|
|
81
|
+
if (!apiKey && envKey && process.env[envKey]) apiKey = process.env[envKey]
|
|
82
|
+
if (!apiKey) apiKey = process.env.THINCODER_API_KEY
|
|
83
|
+
const models = await listModels(
|
|
84
|
+
{ baseURL: p.baseURL, apiKey: apiKey ?? "" },
|
|
85
|
+
{ signal: AbortSignal.timeout(10000) },
|
|
86
|
+
)
|
|
87
|
+
// 找到当前 provider 在 entries 里的模型 item 位置,在其后插入新模型
|
|
88
|
+
const at = entries.findLastIndex(
|
|
89
|
+
(e) => e.type === "item" && e.action === "switch" && e.provider === p.name,
|
|
90
|
+
)
|
|
91
|
+
if (at >= 0) {
|
|
92
|
+
entries.splice(
|
|
93
|
+
at + 1,
|
|
94
|
+
0,
|
|
95
|
+
...models
|
|
96
|
+
.filter((m) => m !== p.model)
|
|
97
|
+
.map((m) => ({ type: "item", text: m, action: "switch", provider: p.name, model: m })),
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
// 更新 header note 去掉 loading
|
|
101
|
+
const header = entries.find((e) => e.type === "header" && e.text === p.name)
|
|
102
|
+
if (header) header.note = `${p.baseURL}${p.apiKey ? "" : " (no key)"}`
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const header = entries.find((e) => e.type === "header" && e.text === p.name)
|
|
105
|
+
if (header) header.note = `${p.baseURL}${p.apiKey ? "" : " (no key)"} (fetch failed: ${sliceByWidth(error.message, 40)})`
|
|
106
|
+
}
|
|
107
|
+
if (state.picker?.entries === entries) renderPickerLines()
|
|
108
|
+
}),
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 构建 picker entries:每个 provider 一个 header + 模型列表,底部是管理操作 */
|
|
113
|
+
function buildModelEntries() {
|
|
114
|
+
const entries = []
|
|
115
|
+
for (const p of agent.providers) {
|
|
116
|
+
const active = p.name === agent.activeProvider
|
|
117
|
+
entries.push({
|
|
118
|
+
type: "header",
|
|
119
|
+
text: p.name,
|
|
120
|
+
note: `${p.baseURL}${p.apiKey ? "" : " (no key)"}${active ? " ← current" : ""} loading...`,
|
|
121
|
+
})
|
|
122
|
+
entries.push({
|
|
123
|
+
type: "item",
|
|
124
|
+
text: p.model,
|
|
125
|
+
action: "switch",
|
|
126
|
+
provider: p.name,
|
|
127
|
+
model: p.model,
|
|
128
|
+
marker: active ? "●" : "",
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
// 管理操作
|
|
132
|
+
entries.push({ type: "header", text: "Provider Management" })
|
|
133
|
+
entries.push({ type: "item", text: "Add provider…", action: "add" })
|
|
134
|
+
if (agent.providers.length > 1) {
|
|
135
|
+
entries.push({ type: "item", text: "Remove provider…", action: "remove" })
|
|
136
|
+
}
|
|
137
|
+
entries.push({ type: "item", text: "Set / change API key…", action: "key" })
|
|
138
|
+
return entries
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 切换 provider + 模型,持久化,阈值随模型走 */
|
|
142
|
+
async function selectModel(item) {
|
|
143
|
+
closePicker()
|
|
144
|
+
const target = agent.providers.find((pp) => pp.name === item.provider)
|
|
145
|
+
if (!target) return
|
|
146
|
+
target.model = item.model
|
|
147
|
+
agent.activeProvider = item.provider
|
|
148
|
+
agent.provider = { ...target }
|
|
149
|
+
if (!agent.provider.apiKey) {
|
|
150
|
+
const envKey = { deepseek: "DEEPSEEK_API_KEY", openai: "OPENAI_API_KEY" }[item.provider]
|
|
151
|
+
if (envKey && process.env[envKey]) agent.provider.apiKey = process.env[envKey]
|
|
152
|
+
}
|
|
153
|
+
if (!agent.provider.apiKey) agent.provider.apiKey = process.env.THINCODER_API_KEY
|
|
154
|
+
if (agent.config?.agent?.compactThresholdAuto) {
|
|
155
|
+
const { resolveCompactThreshold } = await import("../config.mjs")
|
|
156
|
+
const { value } = resolveCompactThreshold(null, item.model)
|
|
157
|
+
agent.config.agent.compactThreshold = value
|
|
158
|
+
}
|
|
159
|
+
await persistRaw((raw) => {
|
|
160
|
+
raw.providers = agent.providers
|
|
161
|
+
raw.activeProvider = item.provider
|
|
162
|
+
})
|
|
163
|
+
agent.config.activeProvider = item.provider
|
|
164
|
+
if (!agent.provider.apiKey) {
|
|
165
|
+
const selKey = await askQuestion(`Enter API key for ${item.provider} (leave empty to skip):`)
|
|
166
|
+
if (selKey) await setProviderKey(item.provider, selKey)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 添加 provider:预设菜单 → 输入 key → 完成,或自定义逐项输入 */
|
|
171
|
+
async function addProviderFlow() {
|
|
172
|
+
const presetEntries = [
|
|
173
|
+
{ type: "header", text: "Select a preset provider" },
|
|
174
|
+
...Object.entries(PRESETS)
|
|
175
|
+
.filter(([name]) => !agent.providers.some((p) => p.name === name))
|
|
176
|
+
.map(([name, p]) => ({
|
|
177
|
+
type: "item",
|
|
178
|
+
text: `${name.padEnd(10)} ${p.desc ?? ""} (${p.model})`,
|
|
179
|
+
name,
|
|
180
|
+
kind: "preset",
|
|
181
|
+
})),
|
|
182
|
+
{ type: "header", text: "Other" },
|
|
183
|
+
{ type: "item", text: "Custom (manual config)", name: "__custom__", kind: "custom" },
|
|
184
|
+
]
|
|
185
|
+
openPicker({
|
|
186
|
+
title: "Add Provider",
|
|
187
|
+
entries: presetEntries,
|
|
188
|
+
onCancel: () => openModelPicker().catch(() => {}),
|
|
189
|
+
onSelect: async (se) => {
|
|
190
|
+
if (se.kind === "custom") {
|
|
191
|
+
const name = await askQuestion("Enter provider name:")
|
|
192
|
+
if (!name) { openModelPicker().catch(() => {}); return }
|
|
193
|
+
if (agent.providers.some((p) => p.name === name)) { openModelPicker().catch(() => {}); return }
|
|
194
|
+
const baseURLRaw = await askQuestion("Enter baseURL (e.g. https://api.example.com/v1):")
|
|
195
|
+
if (!baseURLRaw) { openModelPicker().catch(() => {}); return }
|
|
196
|
+
const baseURL = baseURLRaw.replace(/\/+$/, "")
|
|
197
|
+
if (!/^https?:\/\//.test(baseURL)) { openModelPicker().catch(() => {}); return }
|
|
198
|
+
const model = await askQuestion("Enter model name:")
|
|
199
|
+
if (!model) { openModelPicker().catch(() => {}); return }
|
|
200
|
+
agent.providers.push({ name, baseURL, model })
|
|
201
|
+
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
202
|
+
const key = await askQuestion(`Enter API key for ${name} (leave empty to skip):`)
|
|
203
|
+
if (key) { await setProviderKey(name, key) }
|
|
204
|
+
openModelPicker().catch(() => {})
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
// 预设
|
|
208
|
+
if (agent.providers.some((p) => p.name === se.name)) { openModelPicker().catch(() => {}); return }
|
|
209
|
+
const preset = PRESETS[se.name]
|
|
210
|
+
const providerCfg = { name: se.name, baseURL: preset.baseURL, model: preset.model }
|
|
211
|
+
if (preset.thinking) providerCfg.thinking = preset.thinking
|
|
212
|
+
if (preset.reasoningEffort) providerCfg.reasoningEffort = preset.reasoningEffort
|
|
213
|
+
if (preset.maxTokens) providerCfg.maxTokens = preset.maxTokens
|
|
214
|
+
if (preset.chatPath) providerCfg.chatPath = preset.chatPath
|
|
215
|
+
if (preset.desc) providerCfg.desc = preset.desc
|
|
216
|
+
agent.providers.push(providerCfg)
|
|
217
|
+
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
218
|
+
const presetKey = await askQuestion(`Enter API key for ${se.name} (leave empty to skip):`)
|
|
219
|
+
if (presetKey) await setProviderKey(se.name, presetKey)
|
|
220
|
+
openModelPicker().catch(() => {})
|
|
221
|
+
},
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** 删除 provider(不能删当前激活的) */
|
|
226
|
+
async function removeProviderFlow() {
|
|
227
|
+
const candidates = agent.providers.filter((p) => p.name !== agent.activeProvider)
|
|
228
|
+
if (candidates.length === 0) { openModelPicker().catch(() => {}); return }
|
|
229
|
+
const removeEntries = [
|
|
230
|
+
{ type: "header", text: "Select provider to remove (current one cannot be removed)" },
|
|
231
|
+
...candidates.map((p) => ({ type: "item", text: `${p.name} (${p.model})`, name: p.name })),
|
|
232
|
+
]
|
|
233
|
+
openPicker({
|
|
234
|
+
title: "Remove Provider",
|
|
235
|
+
entries: removeEntries,
|
|
236
|
+
onCancel: () => openModelPicker().catch(() => {}),
|
|
237
|
+
onSelect: async (se) => {
|
|
238
|
+
const at = agent.providers.findIndex((p) => p.name === se.name)
|
|
239
|
+
agent.providers.splice(at, 1)
|
|
240
|
+
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
241
|
+
openModelPicker().catch(() => {})
|
|
242
|
+
},
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** 设置/更改 API key:选 provider → 输入 key */
|
|
247
|
+
async function setKeyFlow() {
|
|
248
|
+
const keyEntries = [
|
|
249
|
+
{ type: "header", text: "Select provider to configure key" },
|
|
250
|
+
...agent.providers.map((p) => ({
|
|
251
|
+
type: "item",
|
|
252
|
+
text: `${p.name} ${p.apiKey ? `(has key: ${maskKey(p.apiKey)})` : "(no key)"}`,
|
|
253
|
+
name: p.name,
|
|
254
|
+
})),
|
|
255
|
+
]
|
|
256
|
+
openPicker({
|
|
257
|
+
title: "Configure API Key",
|
|
258
|
+
entries: keyEntries,
|
|
259
|
+
onCancel: () => openModelPicker().catch(() => {}),
|
|
260
|
+
onSelect: async (se) => {
|
|
261
|
+
const key = await askQuestion(`Enter API key for ${se.name}:`)
|
|
262
|
+
if (!key) { openModelPicker().catch(() => {}); return }
|
|
263
|
+
await setProviderKey(se.name, key)
|
|
264
|
+
openModelPicker().catch(() => {})
|
|
265
|
+
},
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** 给指定 provider 写 key (内存 + Config文件);若它是current激活的,同步运行时 */
|
|
270
|
+
async function setProviderKey(name, key) {
|
|
271
|
+
const target = agent.providers.find((p) => p.name === name)
|
|
272
|
+
if (!target) return
|
|
273
|
+
target.apiKey = key
|
|
274
|
+
if (name === agent.activeProvider) agent.provider.apiKey = key
|
|
275
|
+
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return { openPicker, closePicker, renderPickerLines, openModelPicker, setProviderKey }
|
|
279
|
+
}
|