acmecode 1.0.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 (131) hide show
  1. package/.acmecode/config.json +6 -0
  2. package/README.md +124 -0
  3. package/dist/agent/index.js +161 -0
  4. package/dist/cli/bin/acmecode.js +3 -0
  5. package/dist/cli/package.json +25 -0
  6. package/dist/cli/src/index.d.ts +1 -0
  7. package/dist/cli/src/index.js +53 -0
  8. package/dist/config/index.js +92 -0
  9. package/dist/context/index.js +30 -0
  10. package/dist/core/src/agent/index.d.ts +52 -0
  11. package/dist/core/src/agent/index.js +476 -0
  12. package/dist/core/src/config/index.d.ts +83 -0
  13. package/dist/core/src/config/index.js +318 -0
  14. package/dist/core/src/context/index.d.ts +1 -0
  15. package/dist/core/src/context/index.js +30 -0
  16. package/dist/core/src/llm/provider.d.ts +27 -0
  17. package/dist/core/src/llm/provider.js +202 -0
  18. package/dist/core/src/llm/vision.d.ts +7 -0
  19. package/dist/core/src/llm/vision.js +37 -0
  20. package/dist/core/src/mcp/index.d.ts +10 -0
  21. package/dist/core/src/mcp/index.js +84 -0
  22. package/dist/core/src/prompt/anthropic.d.ts +1 -0
  23. package/dist/core/src/prompt/anthropic.js +32 -0
  24. package/dist/core/src/prompt/architect.d.ts +1 -0
  25. package/dist/core/src/prompt/architect.js +17 -0
  26. package/dist/core/src/prompt/autopilot.d.ts +1 -0
  27. package/dist/core/src/prompt/autopilot.js +18 -0
  28. package/dist/core/src/prompt/beast.d.ts +1 -0
  29. package/dist/core/src/prompt/beast.js +83 -0
  30. package/dist/core/src/prompt/gemini.d.ts +1 -0
  31. package/dist/core/src/prompt/gemini.js +45 -0
  32. package/dist/core/src/prompt/index.d.ts +18 -0
  33. package/dist/core/src/prompt/index.js +239 -0
  34. package/dist/core/src/prompt/zen.d.ts +1 -0
  35. package/dist/core/src/prompt/zen.js +13 -0
  36. package/dist/core/src/session/index.d.ts +18 -0
  37. package/dist/core/src/session/index.js +97 -0
  38. package/dist/core/src/skills/index.d.ts +6 -0
  39. package/dist/core/src/skills/index.js +72 -0
  40. package/dist/core/src/tools/batch.d.ts +2 -0
  41. package/dist/core/src/tools/batch.js +65 -0
  42. package/dist/core/src/tools/browser.d.ts +7 -0
  43. package/dist/core/src/tools/browser.js +86 -0
  44. package/dist/core/src/tools/edit.d.ts +11 -0
  45. package/dist/core/src/tools/edit.js +312 -0
  46. package/dist/core/src/tools/index.d.ts +13 -0
  47. package/dist/core/src/tools/index.js +980 -0
  48. package/dist/core/src/tools/lsp-client.d.ts +11 -0
  49. package/dist/core/src/tools/lsp-client.js +224 -0
  50. package/dist/index.js +41 -0
  51. package/dist/llm/provider.js +34 -0
  52. package/dist/mcp/index.js +84 -0
  53. package/dist/session/index.js +74 -0
  54. package/dist/skills/index.js +32 -0
  55. package/dist/tools/index.js +96 -0
  56. package/dist/tui/App.js +297 -0
  57. package/dist/tui/Spinner.js +16 -0
  58. package/dist/tui/TextInput.js +98 -0
  59. package/dist/tui/src/App.d.ts +11 -0
  60. package/dist/tui/src/App.js +1211 -0
  61. package/dist/tui/src/CatLogo.d.ts +10 -0
  62. package/dist/tui/src/CatLogo.js +99 -0
  63. package/dist/tui/src/OptionList.d.ts +15 -0
  64. package/dist/tui/src/OptionList.js +60 -0
  65. package/dist/tui/src/Spinner.d.ts +7 -0
  66. package/dist/tui/src/Spinner.js +18 -0
  67. package/dist/tui/src/TextInput.d.ts +28 -0
  68. package/dist/tui/src/TextInput.js +139 -0
  69. package/dist/tui/src/Tips.d.ts +2 -0
  70. package/dist/tui/src/Tips.js +62 -0
  71. package/dist/tui/src/Toast.d.ts +19 -0
  72. package/dist/tui/src/Toast.js +39 -0
  73. package/dist/tui/src/TodoItem.d.ts +7 -0
  74. package/dist/tui/src/TodoItem.js +21 -0
  75. package/dist/tui/src/i18n.d.ts +172 -0
  76. package/dist/tui/src/i18n.js +189 -0
  77. package/dist/tui/src/markdown.d.ts +6 -0
  78. package/dist/tui/src/markdown.js +356 -0
  79. package/dist/tui/src/theme.d.ts +31 -0
  80. package/dist/tui/src/theme.js +239 -0
  81. package/output.txt +0 -0
  82. package/package.json +44 -0
  83. package/packages/cli/package.json +25 -0
  84. package/packages/cli/src/index.ts +59 -0
  85. package/packages/cli/tsconfig.json +26 -0
  86. package/packages/core/package.json +39 -0
  87. package/packages/core/src/agent/index.ts +588 -0
  88. package/packages/core/src/config/index.ts +383 -0
  89. package/packages/core/src/context/index.ts +34 -0
  90. package/packages/core/src/llm/provider.ts +237 -0
  91. package/packages/core/src/llm/vision.ts +43 -0
  92. package/packages/core/src/mcp/index.ts +110 -0
  93. package/packages/core/src/prompt/anthropic.ts +32 -0
  94. package/packages/core/src/prompt/architect.ts +17 -0
  95. package/packages/core/src/prompt/autopilot.ts +18 -0
  96. package/packages/core/src/prompt/beast.ts +83 -0
  97. package/packages/core/src/prompt/gemini.ts +45 -0
  98. package/packages/core/src/prompt/index.ts +267 -0
  99. package/packages/core/src/prompt/zen.ts +13 -0
  100. package/packages/core/src/session/index.ts +129 -0
  101. package/packages/core/src/skills/index.ts +86 -0
  102. package/packages/core/src/tools/batch.ts +73 -0
  103. package/packages/core/src/tools/browser.ts +95 -0
  104. package/packages/core/src/tools/edit.ts +317 -0
  105. package/packages/core/src/tools/index.ts +1112 -0
  106. package/packages/core/src/tools/lsp-client.ts +303 -0
  107. package/packages/core/tsconfig.json +19 -0
  108. package/packages/tui/package.json +24 -0
  109. package/packages/tui/src/App.tsx +1702 -0
  110. package/packages/tui/src/CatLogo.tsx +134 -0
  111. package/packages/tui/src/OptionList.tsx +95 -0
  112. package/packages/tui/src/Spinner.tsx +28 -0
  113. package/packages/tui/src/TextInput.tsx +202 -0
  114. package/packages/tui/src/Tips.tsx +64 -0
  115. package/packages/tui/src/Toast.tsx +60 -0
  116. package/packages/tui/src/TodoItem.tsx +29 -0
  117. package/packages/tui/src/i18n.ts +203 -0
  118. package/packages/tui/src/markdown.ts +403 -0
  119. package/packages/tui/src/theme.ts +287 -0
  120. package/packages/tui/tsconfig.json +24 -0
  121. package/tsconfig.json +18 -0
  122. package/vscode-acmecode/.vscodeignore +11 -0
  123. package/vscode-acmecode/README.md +57 -0
  124. package/vscode-acmecode/esbuild.js +46 -0
  125. package/vscode-acmecode/images/button-dark.svg +5 -0
  126. package/vscode-acmecode/images/button-light.svg +5 -0
  127. package/vscode-acmecode/images/icon.png +1 -0
  128. package/vscode-acmecode/package-lock.json +490 -0
  129. package/vscode-acmecode/package.json +87 -0
  130. package/vscode-acmecode/src/extension.ts +128 -0
  131. package/vscode-acmecode/tsconfig.json +16 -0
@@ -0,0 +1,1211 @@
1
+ import React, { useState, useRef, useEffect } from "react";
2
+ import { Box, Text, Static } from "ink";
3
+ import TextInput from "./TextInput.js";
4
+ import Spinner from "./Spinner.js";
5
+ import Tips from "./Tips.js";
6
+ import OptionList from "./OptionList.js";
7
+ import Toast, { useToast } from "./Toast.js";
8
+ import { renderMarkdown } from "./markdown.js";
9
+ import { runAgent } from "@acmecode/core/agent/index.js";
10
+ import { getContextWindow, fetchModelsDevCache, getProviders, } from "@acmecode/core/llm/provider.js";
11
+ import { loadSession, saveMessages } from "@acmecode/core/session/index.js";
12
+ import { loadSkills } from "@acmecode/core/skills/index.js";
13
+ import { loadModelConfig, loadReasoningLevel, loadAgentModeConfig, saveProjectModelConfig, saveAgentModeConfig, saveGlobalReasoningLevel, saveProviderConfig, getProviderKey, getProviderBaseUrl, getProviderProtocol, } from "@acmecode/core/config/index.js";
14
+ import { theme, formatToolIcon, formatToolName, formatBorder, setTheme, listThemes, getThemeName, onThemeChange, } from "./theme.js";
15
+ import { t, setLang, listLangs, getLang } from "./i18n.js";
16
+ let cachedModels = [];
17
+ let itemCounter = 0;
18
+ function windowText(text, maxLines) {
19
+ const lines = text.split("\n");
20
+ if (lines.length <= maxLines)
21
+ return { content: text, truncated: false };
22
+ let content = lines.slice(-maxLines).join("\n");
23
+ // Check for open code blocks to maintain Markdown integrity
24
+ const codeBlockCount = (content.match(/^```/gm) || []).length;
25
+ if (codeBlockCount % 2 !== 0) {
26
+ // Find if the original text had an opening block that was truncated
27
+ const originalCodeBlocks = (text.match(/^```/gm) || []).length;
28
+ if (originalCodeBlocks % 2 === 0) {
29
+ // We are inside a block that started earlier, prepend a fake opener to satisfy parser
30
+ content = "```\n" + content;
31
+ }
32
+ else {
33
+ // The block is genuinely open and should be closed for safety
34
+ content = content + "\n```";
35
+ }
36
+ }
37
+ return { content, truncated: true };
38
+ }
39
+ // Tools that only read data — show a brief call line, suppress result output
40
+ const READ_ONLY_TOOLS = new Set([
41
+ "read_file",
42
+ "list_dir",
43
+ "grep_search",
44
+ "codesearch",
45
+ "webfetch",
46
+ "websearch",
47
+ "lsp",
48
+ "batch",
49
+ ]);
50
+ function isReadOnlyTool(name) {
51
+ return READ_ONLY_TOOLS.has(name);
52
+ }
53
+ function truncateToolResult(result, maxLen = 500) {
54
+ // If result contains a diff block, show it fully — that's the whole point
55
+ if (result.includes("```diff") || result.includes("```patch"))
56
+ return result;
57
+ if (result.length <= maxLen)
58
+ return result;
59
+ return result.slice(0, maxLen) + `\n${theme.muted("... (truncated)")}`;
60
+ }
61
+ function extractToolDetail(args) {
62
+ let argsObj = args || {};
63
+ if (typeof argsObj === "string") {
64
+ try {
65
+ argsObj = JSON.parse(argsObj);
66
+ }
67
+ catch {
68
+ return argsObj ? ` ${argsObj}` : "";
69
+ }
70
+ }
71
+ // Primary argument — shown inline after tool name
72
+ const primaryKeys = [
73
+ "command",
74
+ "commandLine",
75
+ "CommandLine",
76
+ "cmd",
77
+ "filePath",
78
+ "path",
79
+ "url",
80
+ "query",
81
+ "pattern",
82
+ "prompt",
83
+ ];
84
+ let primary = "";
85
+ const extras = [];
86
+ // Special handling for batch tool - check for array format first
87
+ if (Array.isArray(argsObj)) {
88
+ return `${argsObj.length}个工具调用`;
89
+ }
90
+ // Special handling for batch tool
91
+ if (argsObj.tools || argsObj.calls) {
92
+ let callsArray = [];
93
+ const toolsData = argsObj.tools || argsObj.calls;
94
+ if (typeof toolsData === "string") {
95
+ try {
96
+ callsArray = JSON.parse(toolsData);
97
+ }
98
+ catch {
99
+ callsArray = [];
100
+ }
101
+ }
102
+ else if (Array.isArray(toolsData)) {
103
+ callsArray = toolsData;
104
+ }
105
+ if (callsArray.length > 0) {
106
+ return `${callsArray.length}个工具调用`;
107
+ }
108
+ }
109
+ for (const key of primaryKeys) {
110
+ if (argsObj[key]) {
111
+ primary = String(argsObj[key]);
112
+ break;
113
+ }
114
+ }
115
+ // Secondary arguments — shown in [key=val, ...] brackets
116
+ const secondaryKeys = [
117
+ "limit",
118
+ "offset",
119
+ "recursive",
120
+ "replaceAll",
121
+ "include",
122
+ "exclude",
123
+ ];
124
+ for (const key of secondaryKeys) {
125
+ if (argsObj[key] !== undefined && argsObj[key] !== null) {
126
+ extras.push(`${key}=${argsObj[key]}`);
127
+ }
128
+ }
129
+ // Fallback: if no primary found, use first key's value
130
+ if (!primary && Object.keys(argsObj).length > 0) {
131
+ const firstKey = Object.keys(argsObj)[0];
132
+ const firstVal = argsObj[firstKey];
133
+ if (typeof firstVal === "string" || typeof firstVal === "number") {
134
+ primary = String(firstVal);
135
+ }
136
+ }
137
+ if (!primary && extras.length === 0)
138
+ return "";
139
+ const extrasStr = extras.length > 0 ? ` [${extras.join(", ")}]` : "";
140
+ return primary ? ` ${primary}${extrasStr}` : extrasStr;
141
+ }
142
+ function extractContent(msg) {
143
+ if (typeof msg.content === "string")
144
+ return msg.content;
145
+ if (Array.isArray(msg.content)) {
146
+ return msg.content
147
+ .map((c) => {
148
+ if (c.type === "text")
149
+ return c.text;
150
+ if (c.type === "tool-call") {
151
+ const icon = formatToolIcon(c.toolName);
152
+ const name = formatToolName(c.toolName);
153
+ const detail = extractToolDetail(c.args);
154
+ return `${icon} ${theme.muted(name)}${detail ? " " + theme.muted("→") + " " + theme.muted(detail.trim()) : ""}\n`;
155
+ }
156
+ if (c.type === "tool-result") {
157
+ const r = typeof c.result === "string"
158
+ ? c.result
159
+ : JSON.stringify(c.result, null, 2);
160
+ return `${theme.success("✔")} ${theme.muted(truncateToolResult(r))}\n`;
161
+ }
162
+ return "";
163
+ })
164
+ .join("");
165
+ }
166
+ return "";
167
+ }
168
+ export default function App({ sessionId, onExit, }) {
169
+ const [themeName, setThemeName] = useState(() => getThemeName());
170
+ useEffect(() => {
171
+ onThemeChange((name) => {
172
+ setThemeName(name);
173
+ });
174
+ }, []);
175
+ const [displayItems, setDisplayItems] = useState(() => {
176
+ try {
177
+ const saved = loadSession(sessionId) || [];
178
+ return saved
179
+ .filter((m) => m.role !== "tool" && m.role !== "system")
180
+ .map((m) => {
181
+ const text = extractContent(m);
182
+ if (!text && m.role === "assistant")
183
+ return null;
184
+ return { id: String(itemCounter++), role: m.role, text };
185
+ })
186
+ .filter(Boolean);
187
+ }
188
+ catch (e) {
189
+ return [];
190
+ }
191
+ });
192
+ const pendingToolCallsRef = useRef(new Map());
193
+ // Refresh pre-rendered Markdown when theme changes
194
+ useEffect(() => {
195
+ setDisplayItems((prev) => prev.map((item) => {
196
+ if (item.role === "assistant" || item.role === "user") {
197
+ return {
198
+ ...item,
199
+ rendered: renderMarkdown(item.text),
200
+ };
201
+ }
202
+ return item;
203
+ }));
204
+ }, [themeName]);
205
+ const [currentInput, setCurrentInput] = useState("");
206
+ // Stores collapsed paste parts: label → full text mapping
207
+ const [pastedParts, setPastedParts] = useState(new Map());
208
+ // Clear paste tokens whenever the input is fully cleared
209
+ useEffect(() => {
210
+ if (currentInput === "")
211
+ setPastedParts(new Map());
212
+ }, [currentInput]);
213
+ const [isGenerating, setIsGenerating] = useState(false);
214
+ const [streamingText, setStreamingText] = useState("");
215
+ const [statusText, setStatusText] = useState("");
216
+ const [activeSkill, setActiveSkill] = useState(null);
217
+ const [tempProvider, setTempProvider] = useState(null);
218
+ useEffect(() => {
219
+ // Fetch dynamic model limits from models.dev in the background
220
+ fetchModelsDevCache().catch(() => { });
221
+ }, []);
222
+ // Support aborting generation
223
+ const abortRef = useRef(null);
224
+ const [provider, setProvider] = useState(() => loadModelConfig().provider);
225
+ const [modelName, setModelName] = useState(() => loadModelConfig().model);
226
+ const [visionProvider, setVisionProvider] = useState(() => loadModelConfig().visionProvider);
227
+ const [visionModel, setVisionModel] = useState(() => loadModelConfig().visionModel);
228
+ const [reasoningLevel, setReasoningLevel] = useState(loadReasoningLevel());
229
+ const [agentMode, setAgentMode] = useState(() => loadAgentModeConfig().mode);
230
+ const [activePlanFile, setActivePlanFile] = useState(() => loadAgentModeConfig().planFile);
231
+ const [activeSelection, setActiveSelection] = useState(null);
232
+ const [setupStep, setSetupStep] = useState("none");
233
+ const [setupContext, setSetupContext] = useState(null);
234
+ // Exact length of the system prompt (including hidden specs/instructions)
235
+ const [activePromptLength, setActivePromptLength] = useState(0);
236
+ // List of available slash commands for autocomplete
237
+ const slashCommands = [
238
+ { id: "/mode", label: t("command.mode") },
239
+ { id: "/model", label: t("command.model") },
240
+ { id: "/vision", label: "Configure Vision Model (AcmeVision)" },
241
+ { id: "/config", label: t("command.config") },
242
+ { id: "/reason", label: t("command.reason") },
243
+ { id: "/skill", label: t("command.skill") },
244
+ { id: "/theme", label: t("command.theme") },
245
+ { id: "/lang", label: t("command.lang") },
246
+ { id: "/clear", label: t("command.clear") },
247
+ { id: "/exit", label: t("command.exit") },
248
+ ];
249
+ // Autocomplete state
250
+ const [autocompleteIndex, setAutocompleteIndex] = useState(0);
251
+ const visibleCommands = currentInput.startsWith("/") && !currentInput.includes(" ")
252
+ ? slashCommands.filter((cmd) => cmd.id.startsWith(currentInput.toLowerCase()))
253
+ : [];
254
+ const messagesRef = useRef((() => {
255
+ try {
256
+ return loadSession(sessionId) || [];
257
+ }
258
+ catch {
259
+ return [];
260
+ }
261
+ })());
262
+ const addDisplayItem = (role, text) => {
263
+ // Pre-render Markdown for assistant/user messages to prevent re-parsing in Static
264
+ const rendered = role === "assistant" || role === "user"
265
+ ? renderMarkdown(text)
266
+ : undefined;
267
+ const item = {
268
+ id: String(itemCounter++),
269
+ role,
270
+ text,
271
+ rendered,
272
+ };
273
+ setDisplayItems((prev) => [...prev, item]);
274
+ return item;
275
+ };
276
+ const { toast, show: showToast, dismiss: dismissToast } = useToast();
277
+ const handleSubmit = async (value) => {
278
+ // Expand any collapsed paste tokens before processing
279
+ let expandedValue = value;
280
+ if (pastedParts.size > 0) {
281
+ for (const [label, text] of pastedParts.entries()) {
282
+ expandedValue = expandedValue.split(label).join(text);
283
+ }
284
+ setPastedParts(new Map());
285
+ }
286
+ const input = expandedValue.trim();
287
+ // If the autocomplete menu is open and the user presses Enter, autocomplete instead of submitting
288
+ // EXCEPT if the input is already an exact match of a command
289
+ if (visibleCommands.length > 0) {
290
+ const selected = visibleCommands[Math.min(autocompleteIndex, visibleCommands.length - 1)];
291
+ if (selected && selected.id !== input.toLowerCase()) {
292
+ setCurrentInput(selected.id + " ");
293
+ setAutocompleteIndex(0);
294
+ return;
295
+ }
296
+ }
297
+ if (input === "/exit" || input === "/quit") {
298
+ onExit();
299
+ return;
300
+ }
301
+ if (input === "/clear") {
302
+ setDisplayItems([]);
303
+ setCurrentInput("");
304
+ setActiveSkill(null);
305
+ messagesRef.current = [];
306
+ showToast("info", t("command.clear"));
307
+ return;
308
+ }
309
+ if (input === "/cancel" && isGenerating) {
310
+ if (abortRef.current) {
311
+ abortRef.current.abort();
312
+ }
313
+ setCurrentInput("");
314
+ return;
315
+ }
316
+ const [cmdRaw, ...argsParts] = input.split(/\s+/);
317
+ const cmd = cmdRaw.toLowerCase();
318
+ const args = argsParts.join(" ").trim();
319
+ if (cmd === "/skill") {
320
+ setCurrentInput("");
321
+ if (!args) {
322
+ const skills = await loadSkills();
323
+ addDisplayItem("assistant", `${t("command.skill")}:\n${skills.length ? skills.map((s) => `- ${theme.primary(s.name)}: ${s.description}`).join("\n") : t("status.no_skills")}`);
324
+ return;
325
+ }
326
+ const skills = await loadSkills();
327
+ const skill = skills.find((s) => s.name === args);
328
+ if (skill) {
329
+ setActiveSkill(skill);
330
+ addDisplayItem("assistant", t("status.skill_activated", { name: theme.primary(skill.name) }));
331
+ }
332
+ else {
333
+ addDisplayItem("assistant", t("status.skill_not_found", { name: args }));
334
+ }
335
+ return;
336
+ }
337
+ if (cmd === "/lang") {
338
+ setCurrentInput("");
339
+ const all = listLangs();
340
+ if (!args) {
341
+ setActiveSelection({
342
+ type: "lang",
343
+ options: all.map((l) => ({ id: l, label: l })),
344
+ title: t("status.available_langs"),
345
+ currentId: getLang(),
346
+ });
347
+ }
348
+ else if (setLang(args)) {
349
+ showToast("success", t("status.switched_lang", { name: args }), t("command.lang"));
350
+ addDisplayItem("assistant", `${theme.success("✔")} ${t("status.switched_lang", { name: theme.primary(args) })}`);
351
+ }
352
+ else {
353
+ addDisplayItem("assistant", t("status.lang_not_found", { name: args }));
354
+ }
355
+ return;
356
+ }
357
+ if (cmd === "/theme") {
358
+ setCurrentInput("");
359
+ const all = listThemes();
360
+ if (!args) {
361
+ setActiveSelection({
362
+ type: "theme",
363
+ options: all.map((tn) => ({ id: tn, label: tn })),
364
+ title: t("status.available_themes"),
365
+ currentId: getThemeName(),
366
+ });
367
+ }
368
+ else if (setTheme(args)) {
369
+ showToast("success", t("status.switched_theme", { name: args }), t("command.theme"));
370
+ addDisplayItem("assistant", `${theme.success("✔")} ${t("status.switched_theme", { name: theme.primary(args) })}`);
371
+ }
372
+ else {
373
+ addDisplayItem("assistant", t("status.theme_not_found", { name: args }));
374
+ }
375
+ return;
376
+ }
377
+ if (cmd === "/model") {
378
+ setCurrentInput("");
379
+ const all = getProviders();
380
+ setActiveSelection({
381
+ type: "provider",
382
+ options: all.map((p) => ({ id: p.id, label: p.name })),
383
+ title: t("status.select_provider"),
384
+ currentId: provider,
385
+ });
386
+ return;
387
+ }
388
+ if (cmd === "/vision") {
389
+ setCurrentInput("");
390
+ const all = getProviders();
391
+ setActiveSelection({
392
+ type: "vision_provider",
393
+ options: all.map((p) => ({ id: p.id, label: p.name })),
394
+ title: "Select Vision Delegate Provider",
395
+ currentId: visionProvider,
396
+ });
397
+ return;
398
+ }
399
+ if (cmd === "/reason") {
400
+ setCurrentInput("");
401
+ const levels = [
402
+ "low",
403
+ "medium",
404
+ "high",
405
+ "max",
406
+ "xhigh",
407
+ ];
408
+ if (!args) {
409
+ setActiveSelection({
410
+ type: "reason",
411
+ options: levels.map((l) => ({ id: l, label: l })),
412
+ title: t("status.available_reasoning"),
413
+ currentId: reasoningLevel,
414
+ });
415
+ }
416
+ else if (levels.includes(args)) {
417
+ setReasoningLevel(args);
418
+ saveGlobalReasoningLevel(args);
419
+ showToast("success", t("status.switched_reasoning", { name: args }), t("command.reason"));
420
+ addDisplayItem("assistant", `${theme.success("✔")} ${t("status.switched_reasoning", { name: theme.primary(args) })}`);
421
+ }
422
+ else {
423
+ addDisplayItem("assistant", t("status.reasoning_not_found", { name: args }));
424
+ }
425
+ return;
426
+ }
427
+ if (cmd === "/mode") {
428
+ setCurrentInput("");
429
+ const modes = ["plan", "code", "agent", "zen"];
430
+ if (!args) {
431
+ setActiveSelection({
432
+ type: "mode",
433
+ options: modes.map((m) => ({ id: m, label: t(`mode.${m}`) })),
434
+ title: t("status.available_modes"),
435
+ currentId: agentMode,
436
+ });
437
+ }
438
+ else if (modes.includes(args)) {
439
+ setAgentMode(args);
440
+ setActivePlanFile(undefined);
441
+ saveAgentModeConfig(args, undefined);
442
+ showToast("success", t("status.switched_mode", { name: args }), t("command.mode"));
443
+ addDisplayItem("assistant", `${theme.success("✔")} ${t("status.switched_mode", { name: theme.primary(args) })}`);
444
+ }
445
+ else {
446
+ addDisplayItem("assistant", `Invalid mode: ${args}`);
447
+ }
448
+ return;
449
+ }
450
+ if (cmd === "/config") {
451
+ setCurrentInput("");
452
+ const providers = getProviders();
453
+ setActiveSelection({
454
+ type: "config_provider",
455
+ options: [
456
+ ...providers.map((p) => ({ id: p.id, label: p.name })),
457
+ { id: "ADD_CUSTOM", label: t("ui.add_custom_provider") },
458
+ ],
459
+ title: t("ui.config_title"),
460
+ });
461
+ return;
462
+ }
463
+ // Handle Setup Wizard Input
464
+ if (setupStep !== "none") {
465
+ const val = input.trim();
466
+ if (!val)
467
+ return;
468
+ setCurrentInput("");
469
+ if (setupStep === "naming") {
470
+ setSetupContext((prev) => ({
471
+ ...prev,
472
+ providerId: val.toLowerCase(),
473
+ name: val,
474
+ isCustom: true,
475
+ }));
476
+ // Show protocol selection
477
+ setActiveSelection({
478
+ type: "config_protocol",
479
+ options: [
480
+ { id: "openai", label: "OpenAI (Standard)" },
481
+ { id: "anthropic", label: "Anthropic" },
482
+ { id: "google", label: "Google (Gemini)" },
483
+ { id: "mistral", label: "Mistral" },
484
+ { id: "groq", label: "Groq" },
485
+ ],
486
+ title: t("ui.select_protocol", { name: val }),
487
+ });
488
+ setSetupStep("entering_protocol");
489
+ }
490
+ else if (setupStep === "entering_key") {
491
+ if (setupContext?.isCustom) {
492
+ setSetupContext((prev) => ({ ...prev, apiKey: val }));
493
+ setSetupStep("entering_url");
494
+ }
495
+ else {
496
+ // Official provider - save immediately
497
+ saveProviderConfig(setupContext.providerId, { apiKey: val });
498
+ addDisplayItem("assistant", `${theme.success("✔")} ${t("status.config_saved", { name: theme.primary(setupContext.providerId) })}`);
499
+ setSetupStep("none");
500
+ setSetupContext(null);
501
+ }
502
+ }
503
+ else if (setupStep === "entering_url") {
504
+ saveProviderConfig(setupContext.providerId, {
505
+ apiKey: setupContext.apiKey,
506
+ baseUrl: val,
507
+ isCustom: true,
508
+ protocol: setupContext.protocol,
509
+ });
510
+ addDisplayItem("assistant", `${theme.success("✔")} Custom provider ${theme.primary(setupContext.name)} configured.`);
511
+ setSetupStep("none");
512
+ setSetupContext(null);
513
+ }
514
+ return;
515
+ }
516
+ if (value.trim() && !isGenerating) {
517
+ addDisplayItem("user", value);
518
+ setCurrentInput("");
519
+ setIsGenerating(true);
520
+ setStreamingText("");
521
+ const currentMessages = Array.isArray(messagesRef.current)
522
+ ? messagesRef.current
523
+ : [];
524
+ const newMessages = [
525
+ ...currentMessages,
526
+ { role: "user", content: value },
527
+ ];
528
+ try {
529
+ abortRef.current = new AbortController();
530
+ // Auto-select first model if not set
531
+ let effectiveModelName = modelName;
532
+ if (!effectiveModelName) {
533
+ try {
534
+ const apiKey = getProviderKey(provider);
535
+ const baseUrl = getProviderBaseUrl(provider);
536
+ const res = await fetch(`${baseUrl}/models`, {
537
+ headers: { Authorization: `Bearer ${apiKey}` },
538
+ });
539
+ if (res.ok) {
540
+ const data = await res.json();
541
+ const models = (data.data || data.models || []);
542
+ if (models.length > 0) {
543
+ effectiveModelName =
544
+ models[0].id ||
545
+ models[0].name?.replace("models/", "") ||
546
+ models[0].model;
547
+ setModelName(effectiveModelName);
548
+ }
549
+ }
550
+ }
551
+ catch {
552
+ // ignore fetch error
553
+ }
554
+ }
555
+ if (!effectiveModelName) {
556
+ addDisplayItem("assistant", theme.danger("No model available. Please select a provider and model."));
557
+ return;
558
+ }
559
+ const generator = runAgent(provider, effectiveModelName, newMessages, undefined, // Let runAgent build system prompt dynamically
560
+ abortRef.current.signal, reasoningLevel, agentMode, activePlanFile, activeSkill?.content);
561
+ let fullResponse = "";
562
+ let lastFlush = Date.now();
563
+ let didGetMessages = false;
564
+ let generatorInstance = generator;
565
+ let nextApproval = undefined;
566
+ let lastToolName = "";
567
+ setStatusText(t("status.thinking"));
568
+ let idleTimer = null;
569
+ const resetIdle = (forceThinking = true) => {
570
+ if (idleTimer)
571
+ clearTimeout(idleTimer);
572
+ idleTimer = setTimeout(() => {
573
+ setStreamingText(fullResponse);
574
+ if (forceThinking)
575
+ setStatusText(t("status.thinking"));
576
+ }, 500);
577
+ };
578
+ resetIdle();
579
+ while (true) {
580
+ const next = await generatorInstance.next(nextApproval);
581
+ nextApproval = undefined; // Reset
582
+ if (next.done) {
583
+ break;
584
+ }
585
+ const event = next.value;
586
+ resetIdle();
587
+ if (event.type === "text") {
588
+ setStatusText("");
589
+ fullResponse += event.text;
590
+ const now = Date.now();
591
+ // Throttle streaming updates to 200ms to reduce render-flicker
592
+ if (now - lastFlush > 200) {
593
+ setStreamingText(fullResponse);
594
+ lastFlush = now;
595
+ }
596
+ }
597
+ else if (event.type === "tool-approval-required") {
598
+ const icon = formatToolIcon(event.name);
599
+ const humanName = formatToolName(event.name);
600
+ const detail = extractToolDetail(event.args);
601
+ const riskLabel = event.riskLevel
602
+ ? t(`ui.risk_${event.riskLevel}`)
603
+ : "";
604
+ const localWarning = event.isLocalGuard
605
+ ? `\n${theme.danger("! " + t("ui.local_guard_hit"))}`
606
+ : "";
607
+ setStatusText(`${theme.danger("⚠ ")} ${humanName} ${theme.danger(t("ui.approval_required"))}`);
608
+ const approved = await new Promise((resolve) => {
609
+ setActiveSelection({
610
+ type: "mode",
611
+ title: `${icon} ${theme.danger(t("ui.dangerous_command_warning"))}${localWarning}\n${theme.muted(formatBorder(40))}\n${theme.primary(detail)}\n${theme.muted(formatBorder(40))}\n${theme.highlight(t("ui.risk_level", { level: riskLabel }))}\n${t("ui.approve_execution_q")}`,
612
+ options: [
613
+ { id: "deny", label: theme.danger(t("ui.deny")) },
614
+ { id: "approve", label: theme.success(t("ui.approve")) },
615
+ ],
616
+ currentId: "deny",
617
+ });
618
+ globalThis._approvalResolver = (opt) => {
619
+ setActiveSelection(null);
620
+ resolve(opt.id === "approve");
621
+ };
622
+ });
623
+ nextApproval = approved;
624
+ }
625
+ else if (event.type === "tool-call") {
626
+ const icon = formatToolIcon(event.name);
627
+ const humanName = formatToolName(event.name);
628
+ let detail = extractToolDetail(event.args);
629
+ // Default detail for tools without arguments
630
+ if (!detail) {
631
+ if (event.name === "list_dir")
632
+ detail = ".";
633
+ else if (event.name === "read_file")
634
+ detail = "(未指定文件)";
635
+ }
636
+ lastToolName = event.name;
637
+ setStatusText(`${icon} ${humanName}${detail}`);
638
+ if (idleTimer)
639
+ clearTimeout(idleTimer);
640
+ if (fullResponse.trim()) {
641
+ addDisplayItem("assistant", fullResponse);
642
+ fullResponse = "";
643
+ setStreamingText("");
644
+ }
645
+ const toolDetail = detail;
646
+ const key = event.toolCallId || `${event.name}:${toolDetail}`;
647
+ pendingToolCallsRef.current.set(key, {
648
+ icon,
649
+ humanName,
650
+ detail: toolDetail,
651
+ timestamp: Date.now(),
652
+ });
653
+ }
654
+ else if (event.type === "tool-call-delta") {
655
+ const icon = formatToolIcon(event.name);
656
+ const humanName = formatToolName(event.name);
657
+ const detail = extractToolDetail(event.args);
658
+ setStatusText(`${icon} ${humanName}${detail} ${theme.muted("(streaming...)")}`);
659
+ }
660
+ else if (event.type === "tool-generating") {
661
+ const icon = formatToolIcon(event.name);
662
+ const humanName = formatToolName(event.name);
663
+ const detail = extractToolDetail(event.args);
664
+ // Only update the spinner status — tool-call already added the display item
665
+ setStatusText(`${icon} ${humanName}${detail}`);
666
+ }
667
+ else if (event.type === "tool-result") {
668
+ const resultStr = typeof event.result === "string"
669
+ ? event.result
670
+ : JSON.stringify(event.result, null, 2);
671
+ let matchedEntry;
672
+ let matchedKey;
673
+ // 先用 toolCallId 匹配 key
674
+ if (event.toolCallId) {
675
+ const entry = Array.from(pendingToolCallsRef.current.entries()).find(([k]) => k.startsWith(event.toolCallId));
676
+ if (entry) {
677
+ matchedEntry = entry;
678
+ matchedKey = entry[0];
679
+ }
680
+ }
681
+ // 否则用工具名匹配 key(key 格式是 "toolName:detail")
682
+ if (!matchedEntry) {
683
+ for (const [key, value,] of pendingToolCallsRef.current.entries()) {
684
+ if (key.startsWith(event.name + ":") ||
685
+ key.startsWith(event.name.toLowerCase() + ":")) {
686
+ matchedEntry = [key, value];
687
+ matchedKey = key;
688
+ break;
689
+ }
690
+ }
691
+ }
692
+ if (matchedEntry) {
693
+ const { humanName, detail } = matchedEntry[1];
694
+ const isReadOnly = isReadOnlyTool(event.name);
695
+ const isReadFile = event.name === "read_file";
696
+ const isBatch = event.name === "batch";
697
+ const separator = isReadFile ? " ~ " : " → ";
698
+ if (isBatch) {
699
+ // Batch tool: show summary
700
+ const callCount = (resultStr.match(/\[Call \d+:/g) || [])
701
+ .length;
702
+ const errorCount = (resultStr.match(/Error:/g) || []).length;
703
+ const summary = errorCount > 0
704
+ ? `${callCount}个调用, ${errorCount}个失败`
705
+ : `${callCount}个调用成功`;
706
+ addDisplayItem("tool", `${theme.muted(humanName)}${detail ? theme.muted(separator) + theme.muted(detail.trim()) : ""} ${theme.success("✔")} ${theme.muted(summary)}`);
707
+ }
708
+ else if (isReadOnly) {
709
+ addDisplayItem("tool", `${theme.muted(humanName)}${detail ? theme.muted(separator) + theme.muted(detail.trim()) : ""} ${theme.success("✔")}`);
710
+ }
711
+ else {
712
+ addDisplayItem("tool", `${theme.muted(humanName)}${detail ? theme.muted(separator) + theme.muted(detail.trim()) : ""}\n${theme.success("✔")} ${theme.muted(truncateToolResult(resultStr || "OK"))}`);
713
+ }
714
+ pendingToolCallsRef.current.delete(matchedKey);
715
+ }
716
+ else {
717
+ addDisplayItem("tool", `${theme.success("✔")} ${theme.muted(truncateToolResult(resultStr || "OK"))}`);
718
+ }
719
+ setStatusText(t("status.thinking"));
720
+ resetIdle();
721
+ }
722
+ else if (event.type === "step") {
723
+ setStatusText(`${t("status.thinking")} (${event.step}/${event.maxSteps})`);
724
+ }
725
+ else if (event.type === "mode-changed") {
726
+ // Autonomous mode switch initiated by the agent
727
+ setAgentMode(event.mode);
728
+ setActivePlanFile(event.planFile);
729
+ saveAgentModeConfig(event.mode, event.planFile);
730
+ showToast("success", t("status.switched_mode", { name: event.mode }), t("command.mode"));
731
+ addDisplayItem("assistant", `${theme.success("✔")} ${t("status.switched_mode", { name: theme.primary(event.mode) })}${event.planFile ? ` (${event.planFile})` : ""}`);
732
+ }
733
+ else if (event.type === "messages") {
734
+ messagesRef.current = event.messages;
735
+ if (event.promptLength !== undefined) {
736
+ setActivePromptLength(event.promptLength);
737
+ }
738
+ saveMessages(sessionId, messagesRef.current);
739
+ didGetMessages = true;
740
+ }
741
+ }
742
+ if (idleTimer)
743
+ clearTimeout(idleTimer);
744
+ setStatusText("");
745
+ if (fullResponse) {
746
+ addDisplayItem("assistant", fullResponse);
747
+ }
748
+ if (!didGetMessages) {
749
+ const fallback = [
750
+ ...newMessages,
751
+ { role: "assistant", content: fullResponse },
752
+ ];
753
+ messagesRef.current = fallback;
754
+ saveMessages(sessionId, fallback);
755
+ }
756
+ setStreamingText("");
757
+ }
758
+ catch (err) {
759
+ addDisplayItem("assistant", theme.danger(`Error: ${err.message}`));
760
+ setStreamingText("");
761
+ }
762
+ finally {
763
+ setIsGenerating(false);
764
+ abortRef.current = null;
765
+ }
766
+ }
767
+ };
768
+ // ── Context Usage Calculation ──
769
+ const contextUsage = (() => {
770
+ const windowSize = getContextWindow(modelName);
771
+ let historyLength = 0;
772
+ if (messagesRef.current && Array.isArray(messagesRef.current)) {
773
+ for (const msg of messagesRef.current) {
774
+ if (typeof msg.content === "string") {
775
+ historyLength += msg.content.length;
776
+ }
777
+ else if (Array.isArray(msg.content)) {
778
+ historyLength += JSON.stringify(msg.content).length;
779
+ }
780
+ }
781
+ }
782
+ // Account for hidden system prompts, specs, and environment details
783
+ // activePromptLength covers selectBasePrompt + mode_instructions + language_preference + environment + specs
784
+ const totalTextLength = historyLength + streamingText.length + (activePromptLength || 4000);
785
+ // 1 token ≈ 3.0 characters (safer ratio for heavily formatted code/Chinese)
786
+ const tokens = Math.ceil(totalTextLength / 3.0);
787
+ let windowStr = windowSize >= 1000000
788
+ ? `${(windowSize / 1000000).toFixed(1).replace(".0", "")}M`
789
+ : `${Math.round(windowSize / 1000)}k`;
790
+ const percent = Math.min((tokens / windowSize) * 100, 100);
791
+ const pStr = percent > 0 && percent < 0.01 ? "<0.01" : percent.toFixed(2);
792
+ return `${tokens}/${windowStr} (${pStr}%)`;
793
+ })();
794
+ return (React.createElement(Box, { flexDirection: "column", width: "100%" },
795
+ React.createElement(Static, { items: displayItems }, (item) => {
796
+ const hasText = !!item.text.trim();
797
+ if (!hasText && item.role !== "user")
798
+ return null;
799
+ return (React.createElement(Box, { key: item.id, flexDirection: "column", marginBottom: !hasText || item.role === "tool" ? 0 : 1, width: "100%" }, item.role !== "tool" ? (React.createElement(Box, { flexDirection: "row" },
800
+ React.createElement(Text, { bold: true, color: item.role === "user" ? "green" : "blue" }, item.role === "user" ? "❯ " : "󱨊 "),
801
+ React.createElement(Box, { paddingLeft: 1, flexDirection: "column", width: "100%" },
802
+ React.createElement(Text, null, item.rendered || renderMarkdown(item.text))))) : (React.createElement(Box, { paddingLeft: 3 },
803
+ React.createElement(Text, null, item.text)))));
804
+ }),
805
+ React.createElement(Box, { flexDirection: "column", marginTop: 1, width: "100%" },
806
+ React.createElement(Box, { flexDirection: "row", justifyContent: "space-between", paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1, marginBottom: 1, backgroundColor: "#1a1a2e", width: "100%" },
807
+ React.createElement(Box, { flexDirection: "row", gap: 1 },
808
+ React.createElement(Text, { bold: true, color: "cyan" }, "AcmeCode"),
809
+ React.createElement(Text, { color: "gray" }, "\u00B7"),
810
+ React.createElement(Text, null, agentMode === "plan"
811
+ ? theme.highlight(agentMode)
812
+ : agentMode === "code"
813
+ ? theme.success(agentMode)
814
+ : theme.primary(agentMode)),
815
+ activePlanFile && (React.createElement(React.Fragment, null,
816
+ React.createElement(Text, { color: "gray" }, "\u00B7"),
817
+ React.createElement(Text, { color: "yellow" }, activePlanFile))),
818
+ React.createElement(Text, { color: "gray" }, "\u00B7"),
819
+ React.createElement(Text, { color: "white" }, modelName),
820
+ activeSkill ? (React.createElement(React.Fragment, null,
821
+ React.createElement(Text, { color: "gray" }, "\u00B7"),
822
+ React.createElement(Text, { color: "yellow" }, activeSkill.name))) : null),
823
+ React.createElement(Box, { flexDirection: "row", gap: 1 },
824
+ React.createElement(Text, { color: "gray" }, reasoningLevel),
825
+ React.createElement(Text, { color: "gray" }, "\u00B7"),
826
+ React.createElement(Text, { color: "gray" }, contextUsage))),
827
+ activeSelection && (React.createElement(OptionList, { title: activeSelection.title, options: activeSelection.options, currentId: activeSelection.currentId, onSelect: (opt) => {
828
+ if (activeSelection.type === "lang") {
829
+ setLang(opt.id);
830
+ showToast("success", t("status.switched_lang", { name: opt.id }), t("command.lang"));
831
+ addDisplayItem("assistant", `${theme.success("✔")} ${t("status.switched_lang", { name: theme.primary(opt.id) })}`);
832
+ }
833
+ else if (activeSelection.type === "theme") {
834
+ setTheme(opt.id);
835
+ showToast("success", t("status.switched_theme", { name: opt.id }), t("command.theme"));
836
+ addDisplayItem("assistant", `${theme.success("✔")} ${t("status.switched_theme", { name: theme.primary(opt.id) })}`);
837
+ }
838
+ else if (activeSelection.type === "reason") {
839
+ setReasoningLevel(opt.id);
840
+ saveGlobalReasoningLevel(opt.id);
841
+ showToast("success", t("status.switched_reasoning", { name: opt.id }), t("command.reason"));
842
+ addDisplayItem("assistant", `${theme.success("✔")} ${t("status.switched_reasoning", { name: theme.primary(opt.id) })}`);
843
+ }
844
+ else if (activeSelection.type === "mode") {
845
+ if (globalThis._approvalResolver) {
846
+ globalThis._approvalResolver(opt);
847
+ delete globalThis._approvalResolver;
848
+ return;
849
+ }
850
+ setAgentMode(opt.id);
851
+ setActivePlanFile(undefined);
852
+ saveAgentModeConfig(opt.id, undefined);
853
+ showToast("success", t("status.switched_mode", { name: opt.id }), t("command.mode"));
854
+ addDisplayItem("assistant", `${theme.success("✔")} ${t("status.switched_mode", { name: theme.primary(opt.id) })}`);
855
+ }
856
+ else if (activeSelection.type === "provider") {
857
+ const selectedProvider = opt.id;
858
+ setActiveSelection(null);
859
+ setTempProvider(selectedProvider);
860
+ // Fetch models for the selected provider
861
+ addDisplayItem("assistant", `Fetching available models (${selectedProvider})...`);
862
+ (async () => {
863
+ try {
864
+ const apiKey = getProviderKey(selectedProvider);
865
+ const baseUrl = getProviderBaseUrl(selectedProvider);
866
+ const protocol = getProviderProtocol(selectedProvider);
867
+ if (!apiKey) {
868
+ addDisplayItem("assistant", theme.danger(`Error: API key for ${selectedProvider} not found.`));
869
+ return;
870
+ }
871
+ let models = [];
872
+ let fetchError = "";
873
+ // 1. Try Official Endpoint
874
+ try {
875
+ let url = "";
876
+ let headers = {};
877
+ // Special handling for Extralink: fetch from Git repo
878
+ if (selectedProvider === "extralink") {
879
+ url =
880
+ "https://cnb.cool/acmecloud/acmecode-models/-/git/raw/main/v1/models";
881
+ const res = await fetch(url);
882
+ if (res.ok) {
883
+ const data = await res.json();
884
+ const list = (data.data || []);
885
+ models = list.map((m) => ({
886
+ id: m.id,
887
+ label: m.name || m.id,
888
+ }));
889
+ }
890
+ else {
891
+ fetchError = `HTTP ${res.status}`;
892
+ }
893
+ }
894
+ else {
895
+ // Standard provider model fetching
896
+ url = `${baseUrl}/models`;
897
+ headers = { Authorization: `Bearer ${apiKey}` };
898
+ if (protocol === "google") {
899
+ // Google uses key in query param
900
+ const cleanBase = baseUrl?.replace(/\/+$/, "");
901
+ url = `${cleanBase}/models?key=${apiKey}`;
902
+ headers = {};
903
+ }
904
+ else if (protocol === "anthropic") {
905
+ // Anthropic doesn't have a public model list API usually,
906
+ // but some proxies might. We'll skip official and hit fallback directly.
907
+ throw new Error("Anthropic official has no list API");
908
+ }
909
+ const res = await fetch(url, { headers });
910
+ if (res.ok) {
911
+ const data = await res.json();
912
+ const list = (data.data ||
913
+ data.models ||
914
+ []);
915
+ models = list.map((m) => ({
916
+ id: m.id || m.name?.replace("models/", "") || m.model,
917
+ }));
918
+ }
919
+ else {
920
+ fetchError = `HTTP ${res.status}`;
921
+ }
922
+ }
923
+ }
924
+ catch (e) {
925
+ fetchError = e.message;
926
+ }
927
+ // 2. Fallback: Try multiple OpenAI-compatible candidates if first attempt failed or was empty
928
+ if (models.length === 0 &&
929
+ selectedProvider !== "extralink") {
930
+ const cleanBase = baseUrl
931
+ ?.replace(/\/+$/, "")
932
+ .replace(/\/(v1|v1beta)$/, "");
933
+ const candidates = [
934
+ `${cleanBase}/models`,
935
+ `${cleanBase}/v1/models`,
936
+ `${cleanBase}/v1beta/models`,
937
+ ];
938
+ for (const fallbackUrl of candidates) {
939
+ try {
940
+ const res = await fetch(fallbackUrl, {
941
+ headers: { Authorization: `Bearer ${apiKey}` },
942
+ });
943
+ if (res.ok) {
944
+ const data = await res.json();
945
+ const list = (data.data ||
946
+ data.models ||
947
+ []);
948
+ if (list.length > 0) {
949
+ models = list.map((m) => ({
950
+ id: m.id ||
951
+ m.name?.replace("models/", "") ||
952
+ m.model,
953
+ }));
954
+ break; // Success!
955
+ }
956
+ }
957
+ }
958
+ catch (e) {
959
+ // Continue to next candidate
960
+ }
961
+ }
962
+ }
963
+ if (models.length === 0) {
964
+ addDisplayItem("assistant", theme.danger(`No models found or fetch failed: ${fetchError}`));
965
+ }
966
+ else {
967
+ // Auto-select first model if none selected
968
+ const currentModelId = selectedProvider === provider
969
+ ? modelName || models[0]?.id
970
+ : undefined;
971
+ setActiveSelection({
972
+ type: "model",
973
+ options: models.map((m) => ({
974
+ id: m.id,
975
+ label: m.label || m.id,
976
+ })),
977
+ title: `${t("status.available_models")} (${selectedProvider})`,
978
+ currentId: currentModelId,
979
+ });
980
+ }
981
+ }
982
+ catch (err) {
983
+ addDisplayItem("assistant", theme.danger(`Error fetching models: ${err.message}`));
984
+ }
985
+ })();
986
+ return; // Don't close selection yet, we're transitioning
987
+ }
988
+ else if (activeSelection.type === "vision_provider") {
989
+ const selectedProvider = opt.id;
990
+ setActiveSelection(null);
991
+ setTempProvider(selectedProvider);
992
+ addDisplayItem("assistant", `Fetching vision models (${selectedProvider})...`);
993
+ (async () => {
994
+ try {
995
+ const apiKey = getProviderKey(selectedProvider);
996
+ const baseUrl = getProviderBaseUrl(selectedProvider);
997
+ const protocol = getProviderProtocol(selectedProvider);
998
+ if (!apiKey) {
999
+ addDisplayItem("assistant", theme.danger(`Error: API key for ${selectedProvider} not found.`));
1000
+ return;
1001
+ }
1002
+ // Reuse model fetching logic (extracted or duplicated for now)
1003
+ let models = [];
1004
+ const cleanBase = baseUrl
1005
+ ?.replace(/\/+$/, "")
1006
+ .replace(/\/(v1|v1beta)$/, "");
1007
+ const candidates = [
1008
+ `${cleanBase}/models`,
1009
+ `${cleanBase}/v1/models`,
1010
+ `${cleanBase}/v1beta/models`,
1011
+ ];
1012
+ for (const fallbackUrl of candidates) {
1013
+ try {
1014
+ const res = await fetch(fallbackUrl, {
1015
+ headers: { Authorization: `Bearer ${apiKey}` },
1016
+ });
1017
+ if (res.ok) {
1018
+ const data = await res.json();
1019
+ const list = (data.data ||
1020
+ data.models ||
1021
+ []);
1022
+ if (list.length > 0) {
1023
+ models = list.map((m) => ({
1024
+ id: m.id ||
1025
+ m.name?.replace("models/", "") ||
1026
+ m.model,
1027
+ }));
1028
+ break;
1029
+ }
1030
+ }
1031
+ }
1032
+ catch { }
1033
+ }
1034
+ if (models.length === 0) {
1035
+ addDisplayItem("assistant", theme.danger(`No vision models found for ${selectedProvider}.`));
1036
+ }
1037
+ else {
1038
+ setActiveSelection({
1039
+ type: "vision_model",
1040
+ options: models.map((m) => ({ id: m.id, label: m.id })),
1041
+ title: `Select Vision Model (${selectedProvider})`,
1042
+ currentId: visionModel,
1043
+ });
1044
+ }
1045
+ }
1046
+ catch (err) {
1047
+ addDisplayItem("assistant", theme.danger(`Error: ${err.message}`));
1048
+ }
1049
+ })();
1050
+ return;
1051
+ }
1052
+ else if (activeSelection.type === "vision_model") {
1053
+ const p = tempProvider || provider;
1054
+ setVisionProvider(p);
1055
+ setVisionModel(opt.id);
1056
+ saveProjectModelConfig(provider, modelName, p, opt.id);
1057
+ showToast("success", `Vision delegate set to ${opt.id}`);
1058
+ addDisplayItem("assistant", `${theme.success("✔")} Vision Delegate: ${theme.primary(opt.id)}`);
1059
+ setActiveSelection(null);
1060
+ setTempProvider(null);
1061
+ return;
1062
+ }
1063
+ else if (activeSelection.type === "model") {
1064
+ const p = tempProvider || provider;
1065
+ setProvider(p);
1066
+ setModelName(opt.id);
1067
+ saveProjectModelConfig(p, opt.id);
1068
+ addDisplayItem("assistant", `${theme.success("✔")} ${t("status.switched_model", { name: theme.primary(`${p}:${opt.id}`) })}`);
1069
+ setTempProvider(null);
1070
+ }
1071
+ else if (activeSelection.type === "config_provider") {
1072
+ if (opt.id === "ADD_CUSTOM") {
1073
+ setSetupStep("naming");
1074
+ setSetupContext({ providerId: "", isCustom: true });
1075
+ addDisplayItem("assistant", t("ui.enter_custom_name"));
1076
+ }
1077
+ else {
1078
+ const providerInfo = getProviders().find((p) => p.id === opt.id);
1079
+ const isCustom = providerInfo?.envKey === "CUSTOM";
1080
+ if (isCustom) {
1081
+ setSetupContext({
1082
+ providerId: opt.id,
1083
+ name: opt.label,
1084
+ isCustom: true,
1085
+ });
1086
+ setActiveSelection({
1087
+ type: "config_protocol",
1088
+ options: [
1089
+ { id: "openai", label: "OpenAI (Standard)" },
1090
+ { id: "anthropic", label: "Anthropic" },
1091
+ { id: "google", label: "Google (Gemini)" },
1092
+ { id: "mistral", label: "Mistral" },
1093
+ { id: "groq", label: "Groq" },
1094
+ ],
1095
+ title: t("ui.select_protocol", { name: opt.label }),
1096
+ });
1097
+ setSetupStep("entering_protocol");
1098
+ return; // Don't close selection, transitioning to protocols
1099
+ }
1100
+ else {
1101
+ setSetupContext({ providerId: opt.id, isCustom: false });
1102
+ setSetupStep("entering_key");
1103
+ addDisplayItem("assistant", t("ui.enter_api_key", { name: opt.label }));
1104
+ }
1105
+ }
1106
+ }
1107
+ else if (activeSelection.type === "config_protocol") {
1108
+ setSetupContext((prev) => ({ ...prev, protocol: opt.id }));
1109
+ setSetupStep("entering_key");
1110
+ addDisplayItem("assistant", t("ui.enter_api_key", { name: setupContext?.name || "" }));
1111
+ }
1112
+ setActiveSelection(null);
1113
+ }, onCancel: () => {
1114
+ setActiveSelection(null);
1115
+ setTempProvider(null);
1116
+ setSetupStep("none");
1117
+ setSetupContext(null);
1118
+ } })),
1119
+ setupStep !== "none" && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
1120
+ setupStep === "naming" && (React.createElement(Text, { color: "yellow" },
1121
+ "\uDB85\uDD17 ",
1122
+ t("ui.enter_custom_name"))),
1123
+ setupStep === "entering_key" && (React.createElement(Text, { color: "yellow" },
1124
+ "\uDB85\uDD17",
1125
+ " ",
1126
+ t("ui.enter_api_key", {
1127
+ name: setupContext?.name || setupContext?.providerId || "",
1128
+ }))),
1129
+ setupStep === "entering_url" && (React.createElement(Text, { color: "yellow" },
1130
+ "\uDB85\uDD17",
1131
+ " ",
1132
+ t("ui.enter_base_url", {
1133
+ name: setupContext?.name || setupContext?.providerId || "",
1134
+ example: setupContext?.protocol === "anthropic"
1135
+ ? "https://api.anthropic.com"
1136
+ : setupContext?.protocol === "google"
1137
+ ? "https://generativelanguage.googleapis.com"
1138
+ : setupContext?.protocol === "mistral"
1139
+ ? "https://api.mistral.ai"
1140
+ : setupContext?.protocol === "groq"
1141
+ ? "https://api.groq.com/openai/v1"
1142
+ : "https://api.openai.com/v1",
1143
+ }))))),
1144
+ streamingText ? (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
1145
+ React.createElement(Box, null,
1146
+ React.createElement(Text, { bold: true, color: "blue" },
1147
+ "\uDB86\uDE0A",
1148
+ " "),
1149
+ React.createElement(Box, { paddingLeft: 1, flexDirection: "column" }, (() => {
1150
+ const { content, truncated } = windowText(streamingText, 25);
1151
+ return (React.createElement(React.Fragment, null,
1152
+ truncated && (React.createElement(Text, { dimColor: true, italic: true },
1153
+ "... (",
1154
+ t("ui.output_truncated") ||
1155
+ "Previous lines hidden for stability",
1156
+ ")")),
1157
+ React.createElement(Text, null, renderMarkdown(content))));
1158
+ })())))) : null,
1159
+ isGenerating && statusText ? (React.createElement(Box, { marginBottom: 1, paddingLeft: 3 },
1160
+ React.createElement(Spinner, { label: statusText, color: "cyan" }))) : null),
1161
+ !activeSelection ? (React.createElement(Box, { flexDirection: "column" },
1162
+ !isGenerating && visibleCommands.length > 0 && (React.createElement(Box, { marginBottom: 1, paddingX: 2, flexDirection: "column" }, visibleCommands.map((cmd, i) => {
1163
+ const isSelected = i === Math.min(autocompleteIndex, visibleCommands.length - 1);
1164
+ return (React.createElement(Text, { key: cmd.id },
1165
+ isSelected ? theme.success("❯ ") : " ",
1166
+ isSelected
1167
+ ? theme.highlight(cmd.id)
1168
+ : theme.primary(cmd.id),
1169
+ " ",
1170
+ theme.muted(`- ${cmd.label}`)));
1171
+ }))),
1172
+ React.createElement(Box, { flexDirection: "row", width: "100%" },
1173
+ React.createElement(Box, { width: 1, backgroundColor: isGenerating ? "yellow" : "cyan" }),
1174
+ React.createElement(Box, { flexDirection: "column", flexGrow: 1, paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1, backgroundColor: "#16213e" },
1175
+ React.createElement(TextInput, { value: currentInput, focus: !activeSelection, onChange: (val) => {
1176
+ setCurrentInput(val);
1177
+ setAutocompleteIndex(0);
1178
+ }, onSubmit: handleSubmit, onUp: () => setAutocompleteIndex((prev) => Math.max(0, prev - 1)), onDown: () => setAutocompleteIndex((prev) => Math.min(visibleCommands.length - 1, prev + 1)), onTab: () => {
1179
+ if (visibleCommands.length > 0) {
1180
+ const selected = visibleCommands[Math.min(autocompleteIndex, visibleCommands.length - 1)];
1181
+ if (selected)
1182
+ setCurrentInput(selected.id + " ");
1183
+ }
1184
+ }, placeholder: isGenerating ? t("command.cancel") : t("ui.input_placeholder"), onPaste: (part) => {
1185
+ setPastedParts((prev) => {
1186
+ const next = new Map(prev);
1187
+ next.set(part.label, part.text);
1188
+ return next;
1189
+ });
1190
+ } }),
1191
+ React.createElement(Box, { flexDirection: "row", marginTop: 1, gap: 1 },
1192
+ React.createElement(Text, { color: "cyan" }, agentMode.toUpperCase()),
1193
+ React.createElement(Text, { color: "gray" }, "\u00B7"),
1194
+ React.createElement(Text, { color: "white" }, modelName),
1195
+ React.createElement(Text, { color: "gray" }, provider),
1196
+ activeSkill && (React.createElement(React.Fragment, null,
1197
+ React.createElement(Text, { color: "gray" }, "\u00B7"),
1198
+ React.createElement(Text, { color: "yellow" }, activeSkill.name)))))),
1199
+ React.createElement(Box, { flexDirection: "row", justifyContent: "space-between", marginTop: 1, paddingLeft: 2, paddingRight: 2 },
1200
+ React.createElement(Text, { color: "gray" }, process.cwd()),
1201
+ React.createElement(Box, { flexDirection: "row", gap: 2 }, isGenerating ? (React.createElement(Text, { color: "gray" },
1202
+ "esc ",
1203
+ React.createElement(Text, { color: "gray" }, "interrupt"))) : (React.createElement(React.Fragment, null,
1204
+ React.createElement(Text, { color: "gray" },
1205
+ "/ ",
1206
+ React.createElement(Text, { color: "gray" }, "commands")),
1207
+ React.createElement(Text, { color: "gray" }, "/exit"))))),
1208
+ !isGenerating && (React.createElement(Box, { marginTop: 1, paddingLeft: 2 },
1209
+ React.createElement(Tips, null))))) : null,
1210
+ React.createElement(Toast, { toast: toast, onDismiss: dismissToast })));
1211
+ }