@datename/fastcode-termux 0.0.0-stage → 0.1.6
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/LICENSE +21 -0
- package/README.md +400 -2
- package/bin/fastcode.mjs +59 -0
- package/dist/agent/guards.d.ts +35 -0
- package/dist/agent/guards.js +74 -0
- package/dist/agent/limits.d.ts +24 -0
- package/dist/agent/limits.js +71 -0
- package/dist/agent/protocol.d.ts +4 -0
- package/dist/agent/protocol.js +126 -0
- package/dist/agent/tools.d.ts +1 -0
- package/dist/agent/tools.js +377 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.js +476 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +518 -0
- package/dist/commands/about.d.ts +8 -0
- package/dist/commands/about.js +16 -0
- package/dist/commands/agent.d.ts +11 -0
- package/dist/commands/agent.js +18 -0
- package/dist/commands/clear.d.ts +11 -0
- package/dist/commands/clear.js +9 -0
- package/dist/commands/config.d.ts +7 -0
- package/dist/commands/config.js +24 -0
- package/dist/commands/cost.d.ts +9 -0
- package/dist/commands/cost.js +13 -0
- package/dist/commands/cwd.d.ts +7 -0
- package/dist/commands/cwd.js +7 -0
- package/dist/commands/exit.d.ts +9 -0
- package/dist/commands/exit.js +8 -0
- package/dist/commands/help.d.ts +6 -0
- package/dist/commands/help.js +18 -0
- package/dist/commands/index.d.ts +9 -0
- package/dist/commands/index.js +58 -0
- package/dist/commands/login.d.ts +16 -0
- package/dist/commands/login.js +61 -0
- package/dist/commands/logout.d.ts +9 -0
- package/dist/commands/logout.js +53 -0
- package/dist/commands/model.d.ts +11 -0
- package/dist/commands/model.js +30 -0
- package/dist/commands/plugin.d.ts +11 -0
- package/dist/commands/plugin.js +74 -0
- package/dist/commands/provider.d.ts +11 -0
- package/dist/commands/provider.js +28 -0
- package/dist/commands/reset.d.ts +9 -0
- package/dist/commands/reset.js +13 -0
- package/dist/commands/skills.d.ts +11 -0
- package/dist/commands/skills.js +106 -0
- package/dist/commands/system.d.ts +11 -0
- package/dist/commands/system.js +20 -0
- package/dist/commands/theme.d.ts +11 -0
- package/dist/commands/theme.js +22 -0
- package/dist/commands/tools.d.ts +7 -0
- package/dist/commands/tools.js +29 -0
- package/dist/commands/whoami.d.ts +9 -0
- package/dist/commands/whoami.js +51 -0
- package/dist/config/paths.d.ts +4 -0
- package/dist/config/paths.js +59 -0
- package/dist/config/store.d.ts +69 -0
- package/dist/config/store.js +208 -0
- package/dist/config/themes.d.ts +73 -0
- package/dist/config/themes.js +77 -0
- package/dist/config/trust.d.ts +9 -0
- package/dist/config/trust.js +67 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +31 -0
- package/dist/lib/diagnostics.d.ts +41 -0
- package/dist/lib/diagnostics.js +139 -0
- package/dist/lib/http.d.ts +13 -0
- package/dist/lib/http.js +70 -0
- package/dist/lib/markdown.d.ts +1 -0
- package/dist/lib/markdown.js +32 -0
- package/dist/lib/pkg.d.ts +5 -0
- package/dist/lib/pkg.js +27 -0
- package/dist/lib/platform.d.ts +7 -0
- package/dist/lib/platform.js +57 -0
- package/dist/lib/startup.d.ts +6 -0
- package/dist/lib/startup.js +25 -0
- package/dist/lib/uiMode.d.ts +2 -0
- package/dist/lib/uiMode.js +7 -0
- package/dist/plugins/manager.d.ts +23 -0
- package/dist/plugins/manager.js +101 -0
- package/dist/providers/anthropic.d.ts +21 -0
- package/dist/providers/anthropic.js +107 -0
- package/dist/providers/custom.d.ts +24 -0
- package/dist/providers/custom.js +25 -0
- package/dist/providers/deepseek.d.ts +24 -0
- package/dist/providers/deepseek.js +15 -0
- package/dist/providers/google.d.ts +24 -0
- package/dist/providers/google.js +19 -0
- package/dist/providers/groq.d.ts +24 -0
- package/dist/providers/groq.js +20 -0
- package/dist/providers/index.d.ts +201 -0
- package/dist/providers/index.js +37 -0
- package/dist/providers/mistral.d.ts +24 -0
- package/dist/providers/mistral.js +19 -0
- package/dist/providers/moonshot.d.ts +24 -0
- package/dist/providers/moonshot.js +22 -0
- package/dist/providers/ollama.d.ts +24 -0
- package/dist/providers/ollama.js +22 -0
- package/dist/providers/openai.d.ts +24 -0
- package/dist/providers/openai.js +25 -0
- package/dist/providers/openaiCompatible.d.ts +17 -0
- package/dist/providers/openaiCompatible.js +110 -0
- package/dist/providers/openrouter.d.ts +24 -0
- package/dist/providers/openrouter.js +26 -0
- package/dist/providers/qwen.d.ts +24 -0
- package/dist/providers/qwen.js +23 -0
- package/dist/providers/xai.d.ts +24 -0
- package/dist/providers/xai.js +18 -0
- package/dist/providers/zhipu.d.ts +24 -0
- package/dist/providers/zhipu.js +23 -0
- package/dist/session/log.d.ts +29 -0
- package/dist/session/log.js +135 -0
- package/dist/skills/manager.d.ts +18 -0
- package/dist/skills/manager.js +201 -0
- package/dist/skills/parser.d.ts +18 -0
- package/dist/skills/parser.js +65 -0
- package/dist/types.d.ts +146 -0
- package/dist/types.js +1 -0
- package/dist/ui/Banner.d.ts +5 -0
- package/dist/ui/Banner.js +65 -0
- package/dist/ui/Message.d.ts +7 -0
- package/dist/ui/Message.js +13 -0
- package/dist/ui/Prompt.d.ts +10 -0
- package/dist/ui/Prompt.js +38 -0
- package/dist/ui/Spinner.d.ts +5 -0
- package/dist/ui/Spinner.js +33 -0
- package/dist/ui/StatusBar.d.ts +10 -0
- package/dist/ui/StatusBar.js +19 -0
- package/dist/ui/TrustGate.d.ts +5 -0
- package/dist/ui/TrustGate.js +26 -0
- package/package.json +96 -4
- package/src/agent/guards.ts +74 -0
- package/src/agent/limits.ts +71 -0
- package/src/agent/protocol.ts +126 -0
- package/src/agent/tools.ts +377 -0
- package/src/app.ts +476 -0
- package/src/cli.ts +308 -0
- package/src/commands/about.ts +16 -0
- package/src/commands/agent.ts +18 -0
- package/src/commands/clear.ts +9 -0
- package/src/commands/config.ts +24 -0
- package/src/commands/cost.ts +13 -0
- package/src/commands/cwd.ts +7 -0
- package/src/commands/exit.ts +8 -0
- package/src/commands/help.ts +18 -0
- package/src/commands/index.ts +58 -0
- package/src/commands/login.ts +61 -0
- package/src/commands/logout.ts +53 -0
- package/src/commands/model.ts +30 -0
- package/src/commands/plugin.ts +74 -0
- package/src/commands/provider.ts +28 -0
- package/src/commands/reset.ts +13 -0
- package/src/commands/skills.ts +106 -0
- package/src/commands/system.ts +20 -0
- package/src/commands/theme.ts +22 -0
- package/src/commands/tools.ts +29 -0
- package/src/commands/whoami.ts +51 -0
- package/src/config/paths.ts +59 -0
- package/src/config/store.ts +240 -0
- package/src/config/themes.ts +77 -0
- package/src/config/trust.ts +67 -0
- package/src/index.ts +31 -0
- package/src/lib/diagnostics.ts +139 -0
- package/src/lib/http.ts +70 -0
- package/src/lib/markdown.ts +32 -0
- package/src/lib/pkg.ts +35 -0
- package/src/lib/platform.ts +57 -0
- package/src/lib/startup.ts +25 -0
- package/src/lib/uiMode.ts +7 -0
- package/src/plugins/manager.ts +101 -0
- package/src/providers/anthropic.ts +107 -0
- package/src/providers/custom.ts +25 -0
- package/src/providers/deepseek.ts +15 -0
- package/src/providers/google.ts +19 -0
- package/src/providers/groq.ts +20 -0
- package/src/providers/index.ts +37 -0
- package/src/providers/mistral.ts +19 -0
- package/src/providers/moonshot.ts +22 -0
- package/src/providers/ollama.ts +22 -0
- package/src/providers/openai.ts +25 -0
- package/src/providers/openaiCompatible.ts +110 -0
- package/src/providers/openrouter.ts +26 -0
- package/src/providers/qwen.ts +23 -0
- package/src/providers/xai.ts +18 -0
- package/src/providers/zhipu.ts +23 -0
- package/src/session/log.ts +135 -0
- package/src/skills/manager.ts +201 -0
- package/src/skills/parser.ts +65 -0
- package/src/types.ts +143 -0
- package/src/ui/Banner.ts +68 -0
- package/src/ui/Message.ts +13 -0
- package/src/ui/Prompt.ts +38 -0
- package/src/ui/Spinner.ts +33 -0
- package/src/ui/StatusBar.ts +19 -0
- package/src/ui/TrustGate.ts +26 -0
- package/tsconfig.json +22 -0
package/src/app.ts
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* FastCode App component — the heart of the TUI.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* • Render banner, transcript, prompt, and status bar.
|
|
7
|
+
* • Drive the chat loop: send user messages, stream provider responses,
|
|
8
|
+
* update token/cost state, and persist config across runs.
|
|
9
|
+
* • Dispatch slash commands.
|
|
10
|
+
* • Cancel streaming on Esc and quit cleanly on /exit.
|
|
11
|
+
*/
|
|
12
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
13
|
+
import { Box, Static, Text, useApp, useInput } from "ink";
|
|
14
|
+
import { AGENT_SYSTEM_PROMPT, formatToolResults, parseToolCalls, stripToolCalls, } from "./agent/protocol.js";
|
|
15
|
+
import { runAgentTool } from "./agent/tools.js";
|
|
16
|
+
import { Banner } from "./ui/Banner.js";
|
|
17
|
+
import { Spinner } from "./ui/Spinner.js";
|
|
18
|
+
import { StatusBar } from "./ui/StatusBar.js";
|
|
19
|
+
import { MessageBubble } from "./ui/Message.js";
|
|
20
|
+
import { Prompt } from "./ui/Prompt.js";
|
|
21
|
+
import { COMMANDS, findCommand, parseSlash } from "./commands/index.js";
|
|
22
|
+
import { saveScopedKey } from "./commands/login.js";
|
|
23
|
+
import { getProvider, getProviderOrThrow, PROVIDERS } from "./providers/index.js";
|
|
24
|
+
import { loadConfig, saveConfig, resolveApiKey, getLegacy, } from "./config/store.js";
|
|
25
|
+
import { getTheme } from "./config/themes.js";
|
|
26
|
+
import { createStartupTranscript } from "./lib/startup.js";
|
|
27
|
+
import { shouldUseLiveStreamingUi, shouldUseStableTerminalUi } from "./lib/uiMode.js";
|
|
28
|
+
import { buildPluginsPrompt } from "./plugins/manager.js";
|
|
29
|
+
import { buildSkillsPrompt } from "./skills/manager.js";
|
|
30
|
+
import { createSessionLog, loadSessionMessages, openSessionLog } from "./session/log.js";
|
|
31
|
+
import { isTrusted, trustProject } from "./config/trust.js";
|
|
32
|
+
import { TrustGate } from "./ui/TrustGate.js";
|
|
33
|
+
/**
|
|
34
|
+
* Mask the secret part of a `/login` invocation in the transcript:
|
|
35
|
+
* `/login prov` → unchanged, `/login prov <key>` → key masked,
|
|
36
|
+
* `/login prov model <key>` → provider/model shown, key masked.
|
|
37
|
+
*/
|
|
38
|
+
function maskLoginEcho(args) {
|
|
39
|
+
const parts = ["/login", args[0] ?? ""];
|
|
40
|
+
if (args.length === 2) {
|
|
41
|
+
parts.push("********");
|
|
42
|
+
}
|
|
43
|
+
else if (args.length >= 3) {
|
|
44
|
+
parts.push(args[1]);
|
|
45
|
+
for (let i = 2; i < args.length; i += 1)
|
|
46
|
+
parts.push("********");
|
|
47
|
+
}
|
|
48
|
+
return parts.filter((p) => p !== "").join(" ");
|
|
49
|
+
}
|
|
50
|
+
export const App = ({ initialPrompt, resume }) => {
|
|
51
|
+
const { exit } = useApp();
|
|
52
|
+
const startupRef = useRef(null);
|
|
53
|
+
if (!startupRef.current) {
|
|
54
|
+
const config = loadConfig();
|
|
55
|
+
const restored = resume ? loadSessionMessages(resume) : [];
|
|
56
|
+
startupRef.current = {
|
|
57
|
+
config,
|
|
58
|
+
transcript: restored.length > 0
|
|
59
|
+
? restored.map((message, index) => ({
|
|
60
|
+
id: index + 1,
|
|
61
|
+
role: message.role,
|
|
62
|
+
content: message.content,
|
|
63
|
+
markdown: message.role !== "user",
|
|
64
|
+
}))
|
|
65
|
+
: createStartupTranscript(config),
|
|
66
|
+
history: restored.map((message) => ({ role: message.role, content: message.content })),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const startup = startupRef.current;
|
|
70
|
+
const sessionRef = useRef(null as any);
|
|
71
|
+
if (!sessionRef.current) {
|
|
72
|
+
sessionRef.current = resume
|
|
73
|
+
? openSessionLog(resume)
|
|
74
|
+
: createSessionLog({
|
|
75
|
+
providerId: startup.config.providerId,
|
|
76
|
+
model: startup.config.model,
|
|
77
|
+
cwd: process.cwd(),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const [trusted, setTrusted] = useState(() => isTrusted(process.cwd()));
|
|
81
|
+
const [config, setConfig] = useState(startup.config);
|
|
82
|
+
const [history, setHistory] = useState(startup.history ?? []);
|
|
83
|
+
const [transcript, setTranscript] = useState(startup.transcript);
|
|
84
|
+
const [streamingId, setStreamingId] = useState(null);
|
|
85
|
+
const [streamingText, setStreamingText] = useState("");
|
|
86
|
+
const [input, setInput] = useState("");
|
|
87
|
+
const [pendingLogin, setPendingLogin] = useState(null);
|
|
88
|
+
const [busy, setBusy] = useState(false);
|
|
89
|
+
const [totalIn, setTotalIn] = useState(0);
|
|
90
|
+
const [totalOut, setTotalOut] = useState(0);
|
|
91
|
+
const [totalCost, setTotalCost] = useState(0);
|
|
92
|
+
const [agentActivity, setAgentActivity] = useState(null);
|
|
93
|
+
const idRef = useRef(startup.transcript.length);
|
|
94
|
+
const abortRef = useRef(null);
|
|
95
|
+
const ctrlCRef = useRef(0);
|
|
96
|
+
const initialPromptSubmittedRef = useRef(false);
|
|
97
|
+
const theme = getTheme(config.theme);
|
|
98
|
+
const stableUi = useMemo(() => shouldUseStableTerminalUi(), []);
|
|
99
|
+
const liveStreamingUi = useMemo(() => shouldUseLiveStreamingUi(), []);
|
|
100
|
+
// The banner picks a font that fits the terminal width (see ui/Banner.ts)
|
|
101
|
+
// and renders nothing when even the smallest font is too wide.
|
|
102
|
+
const showBanner = transcript.length <= startup.transcript.length && !busy;
|
|
103
|
+
const promptPlaceholder = pendingLogin
|
|
104
|
+
? pendingLogin.step === "provider"
|
|
105
|
+
? "Provider id (e.g. deepseek) · Esc to cancel"
|
|
106
|
+
: pendingLogin.step === "model"
|
|
107
|
+
? "Model id (e.g. deepseek-chat) · Esc to cancel"
|
|
108
|
+
: pendingLogin.provider && getLegacy(pendingLogin.provider)
|
|
109
|
+
? "Paste API key (masked), or type 'keep' · Esc to cancel"
|
|
110
|
+
: "Paste API key (masked) · Esc to cancel"
|
|
111
|
+
: undefined;
|
|
112
|
+
const promptMask = pendingLogin?.step === "key" ? "*" : undefined;
|
|
113
|
+
// Persist config whenever it changes.
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
saveConfig(config);
|
|
116
|
+
}, [config]);
|
|
117
|
+
// Esc cancels streaming. Double Ctrl+C quits.
|
|
118
|
+
useInput((inputChar, key) => {
|
|
119
|
+
if (key.escape) {
|
|
120
|
+
if (pendingLogin) {
|
|
121
|
+
setPendingLogin(null);
|
|
122
|
+
setInput("");
|
|
123
|
+
pushSystem("API key entry cancelled.");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (abortRef.current) {
|
|
127
|
+
abortRef.current.abort();
|
|
128
|
+
abortRef.current = null;
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (key.ctrl && inputChar === "c") {
|
|
133
|
+
ctrlCRef.current += 1;
|
|
134
|
+
if (ctrlCRef.current >= 2) {
|
|
135
|
+
exit();
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
pushSystem("Press Ctrl+C again to quit.");
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
ctrlCRef.current = 0;
|
|
141
|
+
}, 1500);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
const pushSystem = useCallback((text) => {
|
|
146
|
+
idRef.current += 1;
|
|
147
|
+
sessionRef.current.append({ type: "message", role: "system", content: text });
|
|
148
|
+
setTranscript((prev) => [
|
|
149
|
+
...prev,
|
|
150
|
+
{ id: idRef.current, role: "system", content: text, markdown: true },
|
|
151
|
+
]);
|
|
152
|
+
}, []);
|
|
153
|
+
const pushUser = useCallback((text) => {
|
|
154
|
+
idRef.current += 1;
|
|
155
|
+
sessionRef.current.append({ type: "message", role: "user", content: text });
|
|
156
|
+
setTranscript((prev) => [
|
|
157
|
+
...prev,
|
|
158
|
+
{ id: idRef.current, role: "user", content: text, markdown: false },
|
|
159
|
+
]);
|
|
160
|
+
}, []);
|
|
161
|
+
const actions = useMemo(() => ({
|
|
162
|
+
setProvider: (id) => setConfig((c) => ({ ...c, providerId: id })),
|
|
163
|
+
setModel: (id) => setConfig((c) => ({ ...c, model: id })),
|
|
164
|
+
setTheme: (id) => setConfig((c) => ({ ...c, theme: id })),
|
|
165
|
+
setAgentMode: (enabled) => setConfig((c) => ({ ...c, agentMode: enabled })),
|
|
166
|
+
setSystemPrompt: (p) => setConfig((c) => ({ ...c, systemPrompt: p })),
|
|
167
|
+
setSkillsRegistry: (url) => setConfig((c) => ({ ...c, skillsRegistry: url })),
|
|
168
|
+
enableSkill: (name) => setConfig((c) => ({
|
|
169
|
+
...c,
|
|
170
|
+
enabledSkills: Array.from(new Set([...(c.enabledSkills ?? []), name])),
|
|
171
|
+
})),
|
|
172
|
+
disableSkill: (name) => setConfig((c) => ({
|
|
173
|
+
...c,
|
|
174
|
+
enabledSkills: (c.enabledSkills ?? []).filter((n) => n !== name),
|
|
175
|
+
})),
|
|
176
|
+
enablePlugin: (name) => setConfig((c) => ({
|
|
177
|
+
...c,
|
|
178
|
+
enabledPlugins: Array.from(new Set([...(c.enabledPlugins ?? []), name])),
|
|
179
|
+
})),
|
|
180
|
+
disablePlugin: (name) => setConfig((c) => ({
|
|
181
|
+
...c,
|
|
182
|
+
enabledPlugins: (c.enabledPlugins ?? []).filter((n) => n !== name),
|
|
183
|
+
})),
|
|
184
|
+
clearHistory: () => {
|
|
185
|
+
setHistory([]);
|
|
186
|
+
setTranscript([]);
|
|
187
|
+
},
|
|
188
|
+
pushSystemMessage: pushSystem,
|
|
189
|
+
startLogin: ({ provider, model }) => {
|
|
190
|
+
setPendingLogin({
|
|
191
|
+
step: provider ? (model ? "key" : "model") : "provider",
|
|
192
|
+
provider,
|
|
193
|
+
model,
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
}), [pushSystem]);
|
|
197
|
+
const runSlash = useCallback(async (raw) => {
|
|
198
|
+
const parsed = parseSlash(raw);
|
|
199
|
+
if (!parsed)
|
|
200
|
+
return false;
|
|
201
|
+
const cmd = findCommand(parsed.name);
|
|
202
|
+
if (!cmd) {
|
|
203
|
+
pushSystem(`Unknown command "/${parsed.name}". Try /help.`);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
// Echo the slash invocation for transcript clarity, but never echo a
|
|
207
|
+
// secret: the login key is masked, while provider/model stay readable.
|
|
208
|
+
const echo = parsed.name === "login" ? maskLoginEcho(parsed.args) : raw;
|
|
209
|
+
pushUser(echo);
|
|
210
|
+
try {
|
|
211
|
+
const res = await cmd.run({
|
|
212
|
+
args: parsed.args,
|
|
213
|
+
raw: parsed.raw,
|
|
214
|
+
state: {
|
|
215
|
+
providerId: config.providerId,
|
|
216
|
+
model: config.model,
|
|
217
|
+
theme: config.theme,
|
|
218
|
+
agentMode: config.agentMode,
|
|
219
|
+
history,
|
|
220
|
+
systemPrompt: config.systemPrompt,
|
|
221
|
+
totalCostUsd: totalCost,
|
|
222
|
+
totalInputTokens: totalIn,
|
|
223
|
+
totalOutputTokens: totalOut,
|
|
224
|
+
enabledSkills: config.enabledSkills,
|
|
225
|
+
enabledPlugins: config.enabledPlugins,
|
|
226
|
+
skillsRegistry: config.skillsRegistry,
|
|
227
|
+
},
|
|
228
|
+
actions,
|
|
229
|
+
});
|
|
230
|
+
if (res?.message)
|
|
231
|
+
pushSystem(res.message);
|
|
232
|
+
if (res?.quit)
|
|
233
|
+
exit();
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
pushSystem(`Command failed: ${err.message}`);
|
|
237
|
+
}
|
|
238
|
+
return true;
|
|
239
|
+
}, [
|
|
240
|
+
actions,
|
|
241
|
+
config.agentMode,
|
|
242
|
+
config.enabledPlugins,
|
|
243
|
+
config.enabledSkills,
|
|
244
|
+
config.model,
|
|
245
|
+
config.providerId,
|
|
246
|
+
config.skillsRegistry,
|
|
247
|
+
config.systemPrompt,
|
|
248
|
+
config.theme,
|
|
249
|
+
exit,
|
|
250
|
+
history,
|
|
251
|
+
pushSystem,
|
|
252
|
+
pushUser,
|
|
253
|
+
totalCost,
|
|
254
|
+
totalIn,
|
|
255
|
+
totalOut,
|
|
256
|
+
]);
|
|
257
|
+
const handleSubmit = useCallback(async (raw) => {
|
|
258
|
+
const text = raw.trim();
|
|
259
|
+
if (!text || busy)
|
|
260
|
+
return;
|
|
261
|
+
setInput("");
|
|
262
|
+
if (text.startsWith("/")) {
|
|
263
|
+
await runSlash(text);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
pushUser(text);
|
|
267
|
+
const provider = getProviderOrThrow(config.providerId);
|
|
268
|
+
let apiKey;
|
|
269
|
+
if (provider.authEnv.length > 0) {
|
|
270
|
+
const lookup = resolveApiKey(provider.id, config.model, provider.authEnv);
|
|
271
|
+
if (lookup.status === "unbound") {
|
|
272
|
+
pushSystem(`Cannot send: your stored ${provider.label} key isn't bound to a model, so it won't be used.\n` +
|
|
273
|
+
`Re-bind it with \`/login ${provider.id} ${config.model} <key>\` (or \`/logout ${provider.id}\` to discard it).`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (lookup.status === "missing") {
|
|
277
|
+
pushSystem(`Cannot send: no API key for ${provider.label} · ${config.model}.\n` +
|
|
278
|
+
`Run \`/login ${provider.id} ${config.model} <key>\` for this model, or set ${provider.authEnv[0]} (provider-wide).`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
apiKey = lookup.key;
|
|
282
|
+
}
|
|
283
|
+
const nextHistory = [...history, { role: "user", content: text }];
|
|
284
|
+
setHistory(nextHistory);
|
|
285
|
+
idRef.current += 1;
|
|
286
|
+
const bubbleId = idRef.current;
|
|
287
|
+
setStreamingId(bubbleId);
|
|
288
|
+
setStreamingText("");
|
|
289
|
+
const ctrl = new AbortController();
|
|
290
|
+
abortRef.current = ctrl;
|
|
291
|
+
setBusy(true);
|
|
292
|
+
try {
|
|
293
|
+
let workingHistory = nextHistory;
|
|
294
|
+
let finalText = "";
|
|
295
|
+
for (let turn = 0; turn < (config.agentMode ? 8 : 1); turn += 1) {
|
|
296
|
+
const skillsBlock = buildSkillsPrompt(config.enabledSkills);
|
|
297
|
+
const pluginsBlock = buildPluginsPrompt(config.enabledPlugins);
|
|
298
|
+
const agentBlock = config.agentMode ? AGENT_SYSTEM_PROMPT : "";
|
|
299
|
+
const fullSystem = [config.systemPrompt, skillsBlock, pluginsBlock, agentBlock]
|
|
300
|
+
.filter((part) => part.trim())
|
|
301
|
+
.join("\n\n");
|
|
302
|
+
let acc = "";
|
|
303
|
+
for await (const chunk of provider.stream({
|
|
304
|
+
model: config.model,
|
|
305
|
+
messages: workingHistory,
|
|
306
|
+
system: fullSystem,
|
|
307
|
+
signal: ctrl.signal,
|
|
308
|
+
}, apiKey)) {
|
|
309
|
+
if (chunk.delta) {
|
|
310
|
+
acc += chunk.delta;
|
|
311
|
+
if (liveStreamingUi)
|
|
312
|
+
setStreamingText(stripToolCalls(acc));
|
|
313
|
+
}
|
|
314
|
+
if (chunk.done) {
|
|
315
|
+
if (chunk.usage?.inputTokens)
|
|
316
|
+
setTotalIn((n) => n + (chunk.usage.inputTokens ?? 0));
|
|
317
|
+
if (chunk.usage?.outputTokens)
|
|
318
|
+
setTotalOut((n) => n + (chunk.usage.outputTokens ?? 0));
|
|
319
|
+
if (chunk.usage?.costUsd)
|
|
320
|
+
setTotalCost((n) => n + (chunk.usage.costUsd ?? 0));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const visibleText = stripToolCalls(acc);
|
|
324
|
+
const calls = config.agentMode ? parseToolCalls(acc) : [];
|
|
325
|
+
if (calls.length === 0) {
|
|
326
|
+
finalText = visibleText || acc;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
if (visibleText)
|
|
330
|
+
pushSystem(visibleText);
|
|
331
|
+
const results: any[] = [];
|
|
332
|
+
for (const call of calls) {
|
|
333
|
+
setAgentActivity(`Coding: ${call.tool}`);
|
|
334
|
+
pushSystem(`Running tool: \`${call.tool}\`\n\n\`\`\`json\n${JSON.stringify(call, null, 2)}\n\`\`\``);
|
|
335
|
+
const result: any = await runAgentTool(call, process.cwd());
|
|
336
|
+
results.push({ call, ...result });
|
|
337
|
+
pushSystem(`Tool ${result.ok ? "completed" : "failed"}: \`${call.tool}\`\n\n\`\`\`\n${result.output}\n\`\`\``);
|
|
338
|
+
}
|
|
339
|
+
const toolMessage = formatToolResults(results);
|
|
340
|
+
workingHistory = [
|
|
341
|
+
...workingHistory,
|
|
342
|
+
{ role: "assistant", content: acc },
|
|
343
|
+
{ role: "user", content: toolMessage },
|
|
344
|
+
];
|
|
345
|
+
setHistory(workingHistory);
|
|
346
|
+
setStreamingText("");
|
|
347
|
+
setAgentActivity(null);
|
|
348
|
+
}
|
|
349
|
+
if (!finalText) {
|
|
350
|
+
finalText = "Stopped after running the maximum number of agent tool turns.";
|
|
351
|
+
}
|
|
352
|
+
setHistory([...workingHistory, { role: "assistant", content: finalText }]);
|
|
353
|
+
idRef.current = Math.max(idRef.current, bubbleId);
|
|
354
|
+
sessionRef.current.append({ type: "message", role: "assistant", content: finalText });
|
|
355
|
+
setTranscript((prev) => [
|
|
356
|
+
...prev,
|
|
357
|
+
{ id: bubbleId, role: "assistant", content: finalText, markdown: true },
|
|
358
|
+
]);
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
const message = String(err?.message ?? err);
|
|
362
|
+
if (err?.name === "AbortError") {
|
|
363
|
+
pushSystem("Cancelled.");
|
|
364
|
+
}
|
|
365
|
+
else if (/\b(401|403)\b/.test(message) || /invalid api key|unauthorized|authentication/i.test(message)) {
|
|
366
|
+
pushSystem(`Authentication failed for ${provider.label} · ${config.model}. The key may be wrong or not allowed for this model.\n` +
|
|
367
|
+
`Check with \`/whoami\`, or re-save: \`/login ${provider.id} ${config.model} <key>\`.`);
|
|
368
|
+
}
|
|
369
|
+
else if (/\b(402|429)\b/.test(message) || /quota|insufficient|rate limit|billing|balance/i.test(message)) {
|
|
370
|
+
pushSystem(`Request blocked: ${provider.label} reports no remaining quota / rate limit for ${config.model}.\n` +
|
|
371
|
+
`Top up the account or switch model/provider. (${message.slice(0, 200)})`);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
pushSystem(`Error: ${message}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
setStreamingId(null);
|
|
379
|
+
setStreamingText("");
|
|
380
|
+
setAgentActivity(null);
|
|
381
|
+
setBusy(false);
|
|
382
|
+
abortRef.current = null;
|
|
383
|
+
}
|
|
384
|
+
}, [
|
|
385
|
+
busy,
|
|
386
|
+
config.agentMode,
|
|
387
|
+
config.enabledSkills,
|
|
388
|
+
config.enabledPlugins,
|
|
389
|
+
config.model,
|
|
390
|
+
config.providerId,
|
|
391
|
+
config.systemPrompt,
|
|
392
|
+
history,
|
|
393
|
+
pushSystem,
|
|
394
|
+
pushUser,
|
|
395
|
+
runSlash,
|
|
396
|
+
]);
|
|
397
|
+
// Guided `/login`: provider → model → key. Text prompts only; the key step
|
|
398
|
+
// is masked. `keep` re-binds an existing unbound (legacy) key to a model.
|
|
399
|
+
const handleWizardSubmit = useCallback(async (raw) => {
|
|
400
|
+
const value = raw.trim();
|
|
401
|
+
setInput("");
|
|
402
|
+
if (!value || !pendingLogin)
|
|
403
|
+
return;
|
|
404
|
+
if (pendingLogin.step === "provider") {
|
|
405
|
+
const provider = getProvider(value);
|
|
406
|
+
if (!provider) {
|
|
407
|
+
pushSystem(`Unknown provider "${value}". Available: ${PROVIDERS.map((p) => p.id).join(", ")}`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (provider.authEnv.length === 0) {
|
|
411
|
+
pushSystem(`${provider.label} doesn't require an API key. Nothing to save.`);
|
|
412
|
+
setPendingLogin(null);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
setPendingLogin({ step: "model", provider: provider.id });
|
|
416
|
+
const models = await provider.listModels(undefined).catch(() => []);
|
|
417
|
+
const names = models.slice(0, 8).map((m) => m.id).join(", ");
|
|
418
|
+
pushSystem(`Provider: ${provider.label} (${provider.id}).\n` +
|
|
419
|
+
`Type the model id to bind this key to — e.g. \`${provider.defaultModel}\`.` +
|
|
420
|
+
(names ? `\n known models: ${names}` : ""));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (pendingLogin.step === "model") {
|
|
424
|
+
const provider = getProvider(pendingLogin.provider);
|
|
425
|
+
if (!provider) {
|
|
426
|
+
setPendingLogin(null);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
setPendingLogin({ step: "key", provider: provider.id, model: value });
|
|
430
|
+
pushSystem(`Scope: ${provider.id} · ${value}.\nPaste the API key for this model (input is masked).` +
|
|
431
|
+
(getLegacy(provider.id) ? `\nOr type \`keep\` to re-bind the stored ${provider.label} key.` : ""));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const provider = getProvider(pendingLogin.provider);
|
|
435
|
+
if (!provider) {
|
|
436
|
+
setPendingLogin(null);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
let key = value;
|
|
440
|
+
if (value.toLowerCase() === "keep") {
|
|
441
|
+
const legacy = getLegacy(provider.id);
|
|
442
|
+
if (!legacy) {
|
|
443
|
+
pushSystem(`No stored ${provider.label} key to reuse — paste the key instead.`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
key = legacy;
|
|
447
|
+
}
|
|
448
|
+
pushSystem(saveScopedKey(provider.id, provider.label, pendingLogin.model, key));
|
|
449
|
+
setPendingLogin(null);
|
|
450
|
+
}, [pendingLogin, pushSystem]);
|
|
451
|
+
useEffect(() => {
|
|
452
|
+
if (!initialPrompt || initialPromptSubmittedRef.current)
|
|
453
|
+
return;
|
|
454
|
+
initialPromptSubmittedRef.current = true;
|
|
455
|
+
const t = setTimeout(() => void handleSubmit(initialPrompt), 50);
|
|
456
|
+
return () => clearTimeout(t);
|
|
457
|
+
}, [handleSubmit, initialPrompt]);
|
|
458
|
+
useEffect(() => () => {
|
|
459
|
+
if (sessionRef.current)
|
|
460
|
+
sessionRef.current.close();
|
|
461
|
+
}, []);
|
|
462
|
+
if (config.agentMode && !trusted) {
|
|
463
|
+
const decide = (allow) => {
|
|
464
|
+
if (allow) {
|
|
465
|
+
trustProject(process.cwd());
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
setConfig((c) => ({ ...c, agentMode: false }));
|
|
469
|
+
pushSystem("Trust declined: agent tools are off for this session (chat only).");
|
|
470
|
+
}
|
|
471
|
+
setTrusted(true);
|
|
472
|
+
};
|
|
473
|
+
return _jsx(TrustGate, { theme: theme, cwd: process.cwd(), onDecision: decide });
|
|
474
|
+
}
|
|
475
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [showBanner ? _jsx(Banner, { theme: theme, stable: stableUi }) : null, _jsx(Static, { items: transcript, children: (entry) => (_jsx(Box, { marginTop: 1, children: _jsx(MessageBubble, { theme: theme, role: entry.role, content: entry.content, markdown: entry.markdown }) }, entry.id)) }), streamingId !== null && liveStreamingUi ? (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(MessageBubble, { theme: theme, role: "assistant", content: streamingText, markdown: true, streaming: true }) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [busy ? (_jsx(Box, { marginBottom: 1, children: _jsx(Spinner, { theme: theme, label: agentActivity ?? (streamingId !== null ? "Streaming" : "Thinking"), stable: stableUi && !agentActivity }) })) : null, _jsx(Prompt, { theme: theme, value: input, onChange: setInput, onSubmit: pendingLogin ? handleWizardSubmit : handleSubmit, disabled: busy, stable: stableUi, placeholder: promptPlaceholder, mask: promptMask }), _jsx(StatusBar, { theme: theme, providerId: config.providerId, model: config.model, inputTokens: totalIn, outputTokens: totalOut, costUsd: totalCost, enabledSkillCount: config.enabledSkills.length + config.enabledPlugins.length }), _jsxs(Text, { color: theme.muted, children: [COMMANDS.length, " commands \u00B7 ", config.enabledSkills.length, " skill(s) active"] })] })] }));
|
|
476
|
+
};
|