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.
- package/.acmecode/config.json +6 -0
- package/README.md +124 -0
- package/dist/agent/index.js +161 -0
- package/dist/cli/bin/acmecode.js +3 -0
- package/dist/cli/package.json +25 -0
- package/dist/cli/src/index.d.ts +1 -0
- package/dist/cli/src/index.js +53 -0
- package/dist/config/index.js +92 -0
- package/dist/context/index.js +30 -0
- package/dist/core/src/agent/index.d.ts +52 -0
- package/dist/core/src/agent/index.js +476 -0
- package/dist/core/src/config/index.d.ts +83 -0
- package/dist/core/src/config/index.js +318 -0
- package/dist/core/src/context/index.d.ts +1 -0
- package/dist/core/src/context/index.js +30 -0
- package/dist/core/src/llm/provider.d.ts +27 -0
- package/dist/core/src/llm/provider.js +202 -0
- package/dist/core/src/llm/vision.d.ts +7 -0
- package/dist/core/src/llm/vision.js +37 -0
- package/dist/core/src/mcp/index.d.ts +10 -0
- package/dist/core/src/mcp/index.js +84 -0
- package/dist/core/src/prompt/anthropic.d.ts +1 -0
- package/dist/core/src/prompt/anthropic.js +32 -0
- package/dist/core/src/prompt/architect.d.ts +1 -0
- package/dist/core/src/prompt/architect.js +17 -0
- package/dist/core/src/prompt/autopilot.d.ts +1 -0
- package/dist/core/src/prompt/autopilot.js +18 -0
- package/dist/core/src/prompt/beast.d.ts +1 -0
- package/dist/core/src/prompt/beast.js +83 -0
- package/dist/core/src/prompt/gemini.d.ts +1 -0
- package/dist/core/src/prompt/gemini.js +45 -0
- package/dist/core/src/prompt/index.d.ts +18 -0
- package/dist/core/src/prompt/index.js +239 -0
- package/dist/core/src/prompt/zen.d.ts +1 -0
- package/dist/core/src/prompt/zen.js +13 -0
- package/dist/core/src/session/index.d.ts +18 -0
- package/dist/core/src/session/index.js +97 -0
- package/dist/core/src/skills/index.d.ts +6 -0
- package/dist/core/src/skills/index.js +72 -0
- package/dist/core/src/tools/batch.d.ts +2 -0
- package/dist/core/src/tools/batch.js +65 -0
- package/dist/core/src/tools/browser.d.ts +7 -0
- package/dist/core/src/tools/browser.js +86 -0
- package/dist/core/src/tools/edit.d.ts +11 -0
- package/dist/core/src/tools/edit.js +312 -0
- package/dist/core/src/tools/index.d.ts +13 -0
- package/dist/core/src/tools/index.js +980 -0
- package/dist/core/src/tools/lsp-client.d.ts +11 -0
- package/dist/core/src/tools/lsp-client.js +224 -0
- package/dist/index.js +41 -0
- package/dist/llm/provider.js +34 -0
- package/dist/mcp/index.js +84 -0
- package/dist/session/index.js +74 -0
- package/dist/skills/index.js +32 -0
- package/dist/tools/index.js +96 -0
- package/dist/tui/App.js +297 -0
- package/dist/tui/Spinner.js +16 -0
- package/dist/tui/TextInput.js +98 -0
- package/dist/tui/src/App.d.ts +11 -0
- package/dist/tui/src/App.js +1211 -0
- package/dist/tui/src/CatLogo.d.ts +10 -0
- package/dist/tui/src/CatLogo.js +99 -0
- package/dist/tui/src/OptionList.d.ts +15 -0
- package/dist/tui/src/OptionList.js +60 -0
- package/dist/tui/src/Spinner.d.ts +7 -0
- package/dist/tui/src/Spinner.js +18 -0
- package/dist/tui/src/TextInput.d.ts +28 -0
- package/dist/tui/src/TextInput.js +139 -0
- package/dist/tui/src/Tips.d.ts +2 -0
- package/dist/tui/src/Tips.js +62 -0
- package/dist/tui/src/Toast.d.ts +19 -0
- package/dist/tui/src/Toast.js +39 -0
- package/dist/tui/src/TodoItem.d.ts +7 -0
- package/dist/tui/src/TodoItem.js +21 -0
- package/dist/tui/src/i18n.d.ts +172 -0
- package/dist/tui/src/i18n.js +189 -0
- package/dist/tui/src/markdown.d.ts +6 -0
- package/dist/tui/src/markdown.js +356 -0
- package/dist/tui/src/theme.d.ts +31 -0
- package/dist/tui/src/theme.js +239 -0
- package/output.txt +0 -0
- package/package.json +44 -0
- package/packages/cli/package.json +25 -0
- package/packages/cli/src/index.ts +59 -0
- package/packages/cli/tsconfig.json +26 -0
- package/packages/core/package.json +39 -0
- package/packages/core/src/agent/index.ts +588 -0
- package/packages/core/src/config/index.ts +383 -0
- package/packages/core/src/context/index.ts +34 -0
- package/packages/core/src/llm/provider.ts +237 -0
- package/packages/core/src/llm/vision.ts +43 -0
- package/packages/core/src/mcp/index.ts +110 -0
- package/packages/core/src/prompt/anthropic.ts +32 -0
- package/packages/core/src/prompt/architect.ts +17 -0
- package/packages/core/src/prompt/autopilot.ts +18 -0
- package/packages/core/src/prompt/beast.ts +83 -0
- package/packages/core/src/prompt/gemini.ts +45 -0
- package/packages/core/src/prompt/index.ts +267 -0
- package/packages/core/src/prompt/zen.ts +13 -0
- package/packages/core/src/session/index.ts +129 -0
- package/packages/core/src/skills/index.ts +86 -0
- package/packages/core/src/tools/batch.ts +73 -0
- package/packages/core/src/tools/browser.ts +95 -0
- package/packages/core/src/tools/edit.ts +317 -0
- package/packages/core/src/tools/index.ts +1112 -0
- package/packages/core/src/tools/lsp-client.ts +303 -0
- package/packages/core/tsconfig.json +19 -0
- package/packages/tui/package.json +24 -0
- package/packages/tui/src/App.tsx +1702 -0
- package/packages/tui/src/CatLogo.tsx +134 -0
- package/packages/tui/src/OptionList.tsx +95 -0
- package/packages/tui/src/Spinner.tsx +28 -0
- package/packages/tui/src/TextInput.tsx +202 -0
- package/packages/tui/src/Tips.tsx +64 -0
- package/packages/tui/src/Toast.tsx +60 -0
- package/packages/tui/src/TodoItem.tsx +29 -0
- package/packages/tui/src/i18n.ts +203 -0
- package/packages/tui/src/markdown.ts +403 -0
- package/packages/tui/src/theme.ts +287 -0
- package/packages/tui/tsconfig.json +24 -0
- package/tsconfig.json +18 -0
- package/vscode-acmecode/.vscodeignore +11 -0
- package/vscode-acmecode/README.md +57 -0
- package/vscode-acmecode/esbuild.js +46 -0
- package/vscode-acmecode/images/button-dark.svg +5 -0
- package/vscode-acmecode/images/button-light.svg +5 -0
- package/vscode-acmecode/images/icon.png +1 -0
- package/vscode-acmecode/package-lock.json +490 -0
- package/vscode-acmecode/package.json +87 -0
- package/vscode-acmecode/src/extension.ts +128 -0
- 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
|
+
}
|