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.
Files changed (97) hide show
  1. package/README.md +36 -13
  2. package/bin/thincoder.mjs +27 -346
  3. package/package.json +1 -1
  4. package/src/agent/dispatch.mjs +98 -0
  5. package/src/agent/helpers.mjs +185 -0
  6. package/src/agent/setup.mjs +117 -0
  7. package/src/agent-tools/goal.mjs +71 -0
  8. package/src/agent-tools/plan.mjs +31 -0
  9. package/src/agent-tools/recent-changes.mjs +23 -0
  10. package/src/agent-tools/skill.mjs +46 -0
  11. package/src/agent-tools/subagent.mjs +113 -0
  12. package/src/agent-tools/task.mjs +67 -0
  13. package/src/agent-tools/verify.mjs +198 -0
  14. package/src/agent-tools.mjs +12 -0
  15. package/src/agent.mjs +90 -1040
  16. package/src/cli/distill-command.mjs +85 -0
  17. package/src/cli/make-agent.mjs +85 -0
  18. package/src/cli/memory-command.mjs +63 -0
  19. package/src/cli/permission.mjs +41 -0
  20. package/src/cli/setup-wizard.mjs +70 -0
  21. package/src/config.mjs +5 -8
  22. package/src/context.mjs +10 -13
  23. package/src/distill.mjs +4 -3
  24. package/src/embedding.mjs +4 -2
  25. package/src/{checkpoint.mjs → git/checkpoint.mjs} +1 -1
  26. package/src/mcp/helpers.mjs +37 -0
  27. package/src/mcp/transport-http.mjs +176 -0
  28. package/src/mcp/transport-stdio.mjs +84 -0
  29. package/src/mcp/transport-ws.mjs +87 -0
  30. package/src/mcp.mjs +4 -428
  31. package/src/memory/code-index.mjs +211 -0
  32. package/src/memory/code-sync.mjs +306 -0
  33. package/src/memory/core.mjs +277 -0
  34. package/src/memory/docs.mjs +262 -0
  35. package/src/memory/schema.mjs +426 -0
  36. package/src/memory.mjs +12 -1403
  37. package/src/provider/core.mjs +239 -0
  38. package/src/provider/index.mjs +6 -0
  39. package/src/provider/rate.mjs +104 -0
  40. package/src/session.mjs +18 -5
  41. package/src/tools/bash.md +1 -0
  42. package/src/tools/bash.mjs +144 -0
  43. package/src/tools/file.mjs +205 -0
  44. package/src/tools/git.mjs +166 -0
  45. package/src/tools/glob.mjs +51 -0
  46. package/src/tools/grep.mjs +100 -0
  47. package/src/tools/index.mjs +22 -0
  48. package/src/tools/ls.mjs +36 -0
  49. package/src/tools/patch.mjs +226 -0
  50. package/src/tools/repomap-parse.mjs +168 -0
  51. package/src/tools/shared.mjs +257 -0
  52. package/src/tools/system.mjs +336 -0
  53. package/src/tools/web.mjs +121 -0
  54. package/src/tools.mjs +2 -1188
  55. package/src/tui/agent-turn.mjs +254 -0
  56. package/src/tui/ansi.mjs +32 -0
  57. package/src/tui/clipboard.mjs +48 -0
  58. package/src/tui/cmd-auto.mjs +21 -0
  59. package/src/tui/cmd-clear.mjs +26 -0
  60. package/src/tui/cmd-config.mjs +72 -0
  61. package/src/tui/cmd-exit.mjs +5 -0
  62. package/src/tui/cmd-extract.mjs +5 -0
  63. package/src/tui/cmd-goal.mjs +47 -0
  64. package/src/tui/cmd-help.mjs +25 -0
  65. package/src/tui/cmd-init.mjs +91 -0
  66. package/src/tui/cmd-mcp.mjs +146 -0
  67. package/src/tui/cmd-model.mjs +7 -0
  68. package/src/tui/cmd-new.mjs +18 -0
  69. package/src/tui/cmd-plan.mjs +21 -0
  70. package/src/tui/cmd-reindex.mjs +44 -0
  71. package/src/tui/cmd-restore.mjs +39 -0
  72. package/src/tui/cmd-session.mjs +42 -0
  73. package/src/tui/cmd-skills.mjs +17 -0
  74. package/src/tui/cmd-think.mjs +56 -0
  75. package/src/tui/config-helpers.mjs +34 -0
  76. package/src/tui/distill-cmd.mjs +45 -0
  77. package/src/tui/index.mjs +330 -0
  78. package/src/tui/interaction.mjs +79 -0
  79. package/src/tui/key-handler.mjs +267 -0
  80. package/src/tui/layout.mjs +115 -0
  81. package/src/tui/pickers.mjs +279 -0
  82. package/src/tui/render-frame.mjs +304 -0
  83. package/src/tui/render.mjs +205 -0
  84. package/src/tui/slash-commands.mjs +138 -0
  85. package/src/tui/startup.mjs +113 -0
  86. package/src/tui/wizard.mjs +168 -0
  87. package/src/tui-render.mjs +4 -0
  88. package/src/tui.mjs +3 -2546
  89. package/src/provider.mjs +0 -383
  90. /package/src/{gitmem.mjs → git/gitmem.mjs} +0 -0
  91. /package/src/{coder-overlay.md → prompts/coder.md} +0 -0
  92. /package/src/{discipline-rules.md → prompts/discipline.md} +0 -0
  93. /package/src/{explore-overlay.md → prompts/explore.md} +0 -0
  94. /package/src/{main-overlay.md → prompts/main.md} +0 -0
  95. /package/src/{plan-overlay.md → prompts/plan.md} +0 -0
  96. /package/src/{SYSTEM_PROMPT.md → prompts/system.md} +0 -0
  97. /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
+ }