@datename/fastcode-termux 0.1.6-beta → 0.1.8-beta.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/LICENSE +4 -0
- package/README.md +50 -17
- package/dist/agent/approval-json.d.ts +43 -0
- package/dist/agent/approval-json.js +69 -0
- package/dist/agent/approval-native.d.ts +3 -0
- package/dist/agent/approval-native.js +165 -0
- package/dist/agent/approval-store.d.ts +16 -0
- package/dist/agent/approval-store.js +21 -0
- package/dist/agent/approval.d.ts +33 -0
- package/dist/agent/approval.js +40 -5
- package/dist/agent/compaction.d.ts +2 -0
- package/dist/agent/compaction.js +60 -17
- package/dist/agent/guards.d.ts +16 -16
- package/dist/agent/guards.js +978 -24
- package/dist/agent/policy/defaults.d.ts +16 -0
- package/dist/agent/policy/defaults.js +17 -0
- package/dist/agent/policy/egress.d.ts +32 -0
- package/dist/agent/policy/egress.js +280 -0
- package/dist/agent/process-tree.d.ts +20 -0
- package/dist/agent/process-tree.js +81 -10
- package/dist/agent/protocol.d.ts +20 -1
- package/dist/agent/protocol.js +165 -38
- package/dist/agent/registry.d.ts +3 -1
- package/dist/agent/registry.js +168 -11
- package/dist/agent/runner.d.ts +21 -0
- package/dist/agent/runner.js +44 -5
- package/dist/agent/shell-ast.d.ts +11 -0
- package/dist/agent/shell-ast.js +204 -0
- package/dist/agent/shell-session.d.ts +14 -0
- package/dist/agent/shell-session.js +251 -40
- package/dist/agent/tool-schemas.js +26 -7
- package/dist/agent/tools/append_file.js +3 -1
- package/dist/agent/tools/apply_patch.js +44 -4
- package/dist/agent/tools/copy_file.js +26 -3
- package/dist/agent/tools/delete_file.js +22 -3
- package/dist/agent/tools/grep.js +35 -4
- package/dist/agent/tools/make_dir.js +3 -1
- package/dist/agent/tools/move_path.d.ts +7 -0
- package/dist/agent/tools/move_path.js +31 -5
- package/dist/agent/tools/read_file.js +63 -3
- package/dist/agent/tools/read_many.js +21 -3
- package/dist/agent/tools/readers.d.ts +53 -1
- package/dist/agent/tools/readers.js +236 -26
- package/dist/agent/tools/replace_in_file.js +38 -5
- package/dist/agent/tools/shell.js +2 -2
- package/dist/agent/tools/types.d.ts +26 -0
- package/dist/agent/tools/validate.d.ts +15 -2
- package/dist/agent/tools/validate.js +66 -3
- package/dist/agent/tools/write_file.js +3 -1
- package/dist/app.d.ts +2 -1
- package/dist/app.js +526 -85
- package/dist/cli-main.js +363 -0
- package/dist/cli.js +397 -337
- package/dist/commands/about.js +1 -1
- package/dist/commands/clear.d.ts +1 -0
- package/dist/commands/clear.js +1 -1
- package/dist/commands/compact.d.ts +12 -0
- package/dist/commands/compact.js +14 -2
- package/dist/commands/context.js +5 -4
- package/dist/commands/index.js +2 -0
- package/dist/commands/jobs.js +3 -2
- package/dist/commands/kill.js +3 -1
- package/dist/commands/login.d.ts +2 -2
- package/dist/commands/login.js +15 -15
- package/dist/commands/model.d.ts +1 -13
- package/dist/commands/model.js +6 -38
- package/dist/commands/provider.d.ts +1 -15
- package/dist/commands/provider.js +4 -22
- package/dist/commands/skills.js +34 -4
- package/dist/commands/threads.d.ts +9 -0
- package/dist/commands/threads.js +26 -0
- package/dist/config/store.d.ts +21 -0
- package/dist/config/store.js +47 -9
- package/dist/config/trust.d.ts +9 -0
- package/dist/config/trust.js +41 -2
- package/dist/headless.d.ts +7 -0
- package/dist/headless.js +213 -0
- package/dist/index.js +19 -3
- package/dist/lib/debug.d.ts +2 -0
- package/dist/lib/debug.js +22 -0
- package/dist/lib/diagnostics.d.ts +19 -0
- package/dist/lib/diagnostics.js +32 -6
- package/dist/lib/highlight-subset.d.ts +21 -0
- package/dist/lib/highlight-subset.js +88 -0
- package/dist/lib/lifecycle.d.ts +5 -0
- package/dist/lib/lifecycle.js +62 -0
- package/dist/lib/markdown.js +22 -4
- package/dist/providers/deepseek.js +1 -1
- package/dist/providers/openaiCompatible.d.ts +24 -2
- package/dist/providers/openaiCompatible.js +20 -6
- package/dist/providers/spec.d.ts +12 -0
- package/dist/providers/spec.js +29 -0
- package/dist/session/log.d.ts +14 -2
- package/dist/session/log.js +56 -27
- package/dist/session/projection.d.ts +34 -1
- package/dist/session/projection.js +315 -46
- package/dist/skills/manager.d.ts +15 -0
- package/dist/skills/manager.js +135 -18
- package/dist/types.d.ts +15 -0
- package/dist/ui/StatusBar.js +1 -1
- package/package.json +3 -2
- package/src/agent/approval-json.ts +103 -0
- package/src/agent/approval-native.ts +168 -0
- package/src/agent/approval-store.ts +38 -0
- package/src/agent/approval.ts +58 -11
- package/src/agent/compaction.ts +62 -17
- package/src/agent/guards.ts +1049 -25
- package/src/agent/policy/defaults.ts +34 -0
- package/src/agent/policy/egress.ts +335 -0
- package/src/agent/process-tree.ts +105 -23
- package/src/agent/protocol.ts +187 -39
- package/src/agent/registry.ts +186 -14
- package/src/agent/runner.ts +50 -7
- package/src/agent/shell-ast.ts +227 -0
- package/src/agent/shell-session.ts +265 -41
- package/src/agent/tool-schemas.ts +26 -7
- package/src/agent/tools/append_file.ts +3 -1
- package/src/agent/tools/apply_patch.ts +45 -4
- package/src/agent/tools/copy_file.ts +27 -3
- package/src/agent/tools/delete_file.ts +22 -3
- package/src/agent/tools/grep.ts +36 -4
- package/src/agent/tools/make_dir.ts +3 -1
- package/src/agent/tools/move_path.ts +38 -5
- package/src/agent/tools/read_file.ts +64 -3
- package/src/agent/tools/read_many.ts +21 -3
- package/src/agent/tools/readers.ts +289 -26
- package/src/agent/tools/replace_in_file.ts +39 -5
- package/src/agent/tools/shell.ts +2 -2
- package/src/agent/tools/types.ts +43 -1
- package/src/agent/tools/validate.ts +73 -4
- package/src/agent/tools/write_file.ts +3 -1
- package/src/app.ts +533 -82
- package/src/{cli.ts → cli-main.ts} +34 -6
- package/src/commands/about.ts +1 -1
- package/src/commands/clear.ts +1 -1
- package/src/commands/compact.ts +14 -2
- package/src/commands/context.ts +5 -4
- package/src/commands/index.ts +2 -0
- package/src/commands/jobs.ts +3 -2
- package/src/commands/kill.ts +3 -1
- package/src/commands/login.ts +15 -15
- package/src/commands/model.ts +6 -38
- package/src/commands/provider.ts +4 -22
- package/src/commands/skills.ts +34 -4
- package/src/commands/threads.ts +28 -0
- package/src/config/store.ts +47 -9
- package/src/config/trust.ts +45 -2
- package/src/headless.ts +225 -0
- package/src/index.ts +19 -3
- package/src/lib/debug.ts +24 -0
- package/src/lib/diagnostics.ts +32 -6
- package/src/lib/highlight-subset.ts +93 -0
- package/src/lib/lifecycle.ts +69 -0
- package/src/lib/markdown.ts +22 -4
- package/src/providers/deepseek.ts +1 -1
- package/src/providers/openaiCompatible.ts +47 -10
- package/src/providers/spec.ts +42 -0
- package/src/session/log.ts +70 -27
- package/src/session/projection.ts +360 -47
- package/src/skills/manager.ts +140 -18
- package/src/types/highlight-shims.d.ts +14 -0
- package/src/types.ts +16 -1
- package/src/ui/StatusBar.ts +1 -1
- package/tsconfig.json +1 -0
- /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
package/dist/app.js
CHANGED
|
@@ -12,11 +12,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
13
13
|
import { Box, Static, Text, useApp, useInput } from "ink";
|
|
14
14
|
import { AGENT_SYSTEM_PROMPT, stripToolCalls, } from "./agent/protocol.js";
|
|
15
|
-
import { compactionConfigFromEnv } from "./agent/compaction.js";
|
|
15
|
+
import { compactionConfigFromEnv, removeSummaryFile } from "./agent/compaction.js";
|
|
16
16
|
import { resolveToolProtocol, withToolCapability } from "./agent/protocol-mode.js";
|
|
17
17
|
import { runAgent } from "./agent/runner.js";
|
|
18
|
+
import { decisionForOption, parseApprovalChoice } from "./agent/approval.js";
|
|
19
|
+
import { nativeApproval } from "./agent/approval-native.js";
|
|
18
20
|
import { toolSchemas } from "./agent/tool-schemas.js";
|
|
19
|
-
import { shutdownShells } from "./agent/shell-session.js";
|
|
21
|
+
import { setShellPoolSize as applyShellPoolSize, shutdownShells, shutdownShellsAsync } from "./agent/shell-session.js";
|
|
20
22
|
import { Banner } from "./ui/Banner.js";
|
|
21
23
|
import { Spinner } from "./ui/Spinner.js";
|
|
22
24
|
import { StatusBar } from "./ui/StatusBar.js";
|
|
@@ -24,35 +26,19 @@ import { MessageBubble } from "./ui/Message.js";
|
|
|
24
26
|
import { Prompt } from "./ui/Prompt.js";
|
|
25
27
|
import { Picker } from "./ui/Picker.js";
|
|
26
28
|
import { COMMANDS, findCommand, parseSlash } from "./commands/index.js";
|
|
27
|
-
import { saveScopedKey } from "./commands/login.js";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
29
|
+
import { maskLoginEcho, saveScopedKey } from "./commands/login.js";
|
|
30
|
+
import { isValidModelId } from "./commands/model.js";
|
|
31
|
+
import { getProvider, getProviderOrThrow, probeProvider, PROVIDERS } from "./providers/index.js";
|
|
32
|
+
import { loadConfig, saveConfig, resolveApiKey, resolveProviderKey, getLegacy, } from "./config/store.js";
|
|
30
33
|
import { getTheme } from "./config/themes.js";
|
|
31
34
|
import { createStartupTranscript } from "./lib/startup.js";
|
|
32
35
|
import { shouldUseLiveStreamingUi, shouldUseStableTerminalUi } from "./lib/uiMode.js";
|
|
33
36
|
import { buildPluginsPrompt } from "./plugins/manager.js";
|
|
34
37
|
import { buildSkillsPrompt } from "./skills/manager.js";
|
|
35
|
-
import { createSessionLog, latestUnclosedSession, loggingEnabled, openSessionLog } from "./session/log.js";
|
|
38
|
+
import { createSessionLog, latestUnclosedSession, loggingEnabled, openSessionLog, staleSummaryRefs } from "./session/log.js";
|
|
36
39
|
import { resumeSession } from "./session/projection.js";
|
|
37
40
|
import { isTrusted, trustProject } from "./config/trust.js";
|
|
38
41
|
import { TrustGate } from "./ui/TrustGate.js";
|
|
39
|
-
/**
|
|
40
|
-
* Mask the secret part of a `/login` invocation in the transcript:
|
|
41
|
-
* `/login prov` → unchanged, `/login prov <key>` → key masked,
|
|
42
|
-
* `/login prov model <key>` → provider/model shown, key masked.
|
|
43
|
-
*/
|
|
44
|
-
function maskLoginEcho(args) {
|
|
45
|
-
const parts = ["/login", args[0] ?? ""];
|
|
46
|
-
if (args.length === 2) {
|
|
47
|
-
parts.push("********");
|
|
48
|
-
}
|
|
49
|
-
else if (args.length >= 3) {
|
|
50
|
-
parts.push(args[1]);
|
|
51
|
-
for (let i = 2; i < args.length; i += 1)
|
|
52
|
-
parts.push("********");
|
|
53
|
-
}
|
|
54
|
-
return parts.filter((p) => p !== "").join(" ");
|
|
55
|
-
}
|
|
56
42
|
function positiveInt(value, fallback) {
|
|
57
43
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
58
44
|
}
|
|
@@ -74,6 +60,17 @@ function optionalBudget(name) {
|
|
|
74
60
|
const value = Number(process.env[name]);
|
|
75
61
|
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
76
62
|
}
|
|
63
|
+
function uniqueModelList(models) {
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
const result = [];
|
|
66
|
+
for (const model of models) {
|
|
67
|
+
if (!model?.id || seen.has(model.id))
|
|
68
|
+
continue;
|
|
69
|
+
seen.add(model.id);
|
|
70
|
+
result.push(model);
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
77
74
|
function unclosedNotice(session) {
|
|
78
75
|
const where = session.cwd || "(unknown directory)";
|
|
79
76
|
const files = session.activeFiles?.length ?? 0;
|
|
@@ -91,7 +88,7 @@ function streamErrorMessage(message, provider, model) {
|
|
|
91
88
|
}
|
|
92
89
|
return `Error: ${message}`;
|
|
93
90
|
}
|
|
94
|
-
export const App = ({ initialPrompt, resume }) => {
|
|
91
|
+
export const App = ({ initialPrompt, resume, ui = "tty" }) => {
|
|
95
92
|
const { exit } = useApp();
|
|
96
93
|
const startupRef = useRef(null);
|
|
97
94
|
if (!startupRef.current) {
|
|
@@ -166,6 +163,11 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
166
163
|
const [pendingLogin, setPendingLogin] = useState(null);
|
|
167
164
|
const [picker, setPicker] = useState(null);
|
|
168
165
|
const [pickerIndex, setPickerIndex] = useState(0);
|
|
166
|
+
const modelSetupIdRef = useRef(0);
|
|
167
|
+
const openModelSetupPickerRef = useRef(null);
|
|
168
|
+
const plainApprovalRef = useRef(null);
|
|
169
|
+
const [approvalFeedbackPrompt, setApprovalFeedbackPrompt] = useState(null);
|
|
170
|
+
const approvalFeedbackRef = useRef(null);
|
|
169
171
|
const [busy, setBusy] = useState(false);
|
|
170
172
|
const [expanded, setExpanded] = useState(false);
|
|
171
173
|
const steerQueueRef = useRef([]);
|
|
@@ -178,22 +180,59 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
178
180
|
const sawDeltaRef = useRef(false);
|
|
179
181
|
const lastStreamFlushRef = useRef(0);
|
|
180
182
|
const ctrlCRef = useRef(0);
|
|
183
|
+
const exitConfirmRef = useRef(0);
|
|
184
|
+
const busyRef = useRef(false);
|
|
181
185
|
const initialPromptSubmittedRef = useRef(false);
|
|
186
|
+
const quittingRef = useRef(false);
|
|
187
|
+
const quitCleanly = useCallback(async () => {
|
|
188
|
+
if (quittingRef.current)
|
|
189
|
+
return;
|
|
190
|
+
quittingRef.current = true;
|
|
191
|
+
if (abortRef.current) {
|
|
192
|
+
abortRef.current.abort();
|
|
193
|
+
abortRef.current = null;
|
|
194
|
+
}
|
|
195
|
+
// Give the aborted run a chance to append its final facts before the
|
|
196
|
+
// session save point is closed.
|
|
197
|
+
const idleDeadline = Date.now() + 1000;
|
|
198
|
+
while (busyRef.current && Date.now() < idleDeadline)
|
|
199
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
200
|
+
try {
|
|
201
|
+
await shutdownShellsAsync(400);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
shutdownShells();
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
sessionRef.current?.close?.({ reason: "exit" });
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Session close is best effort during exit.
|
|
211
|
+
}
|
|
212
|
+
exit();
|
|
213
|
+
}, [exit]);
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
applyShellPoolSize(config.shellPoolSize);
|
|
216
|
+
}, [config.shellPoolSize]);
|
|
182
217
|
const theme = getTheme(config.theme);
|
|
183
218
|
const stableUi = useMemo(() => shouldUseStableTerminalUi(), []);
|
|
184
219
|
const liveStreamingUi = useMemo(() => shouldUseLiveStreamingUi(), []);
|
|
185
220
|
// The banner picks a font that fits the terminal width (see ui/Banner.ts)
|
|
186
221
|
// and renders nothing when even the smallest font is too wide.
|
|
187
222
|
const showBanner = transcript.length <= startup.transcript.length && !busy;
|
|
188
|
-
const promptPlaceholder =
|
|
189
|
-
?
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
? "
|
|
193
|
-
: pendingLogin.
|
|
194
|
-
? "
|
|
195
|
-
:
|
|
196
|
-
|
|
223
|
+
const promptPlaceholder = approvalFeedbackPrompt
|
|
224
|
+
? `${approvalFeedbackPrompt} · Esc to cancel`
|
|
225
|
+
: pendingLogin
|
|
226
|
+
? pendingLogin.step === "loading-models"
|
|
227
|
+
? "Loading available models…"
|
|
228
|
+
: pendingLogin.step === "model-manual"
|
|
229
|
+
? "Model id (e.g. deepseek-flash) · Esc to cancel"
|
|
230
|
+
: pendingLogin.hasExistingKey
|
|
231
|
+
? "API key (masked), or Enter to keep existing key · Esc to cancel"
|
|
232
|
+
: pendingLogin.provider && getLegacy(pendingLogin.provider)
|
|
233
|
+
? "API key (masked), or type 'keep' · Esc to cancel"
|
|
234
|
+
: "Paste API key (masked) · Esc to cancel"
|
|
235
|
+
: undefined;
|
|
197
236
|
const promptMask = pendingLogin?.step === "key" ? "*" : undefined;
|
|
198
237
|
// Persist config whenever it changes.
|
|
199
238
|
useEffect(() => {
|
|
@@ -208,11 +247,14 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
208
247
|
// Esc cancels streaming. Double Ctrl+C quits.
|
|
209
248
|
useInput((inputChar, key) => {
|
|
210
249
|
if (picker) {
|
|
211
|
-
if (key.escape) {
|
|
212
|
-
picker.onCancel
|
|
250
|
+
if (key.escape || (key.ctrl && inputChar === "c")) {
|
|
251
|
+
const onCancel = picker.onCancel;
|
|
213
252
|
setPicker(null);
|
|
253
|
+
onCancel?.();
|
|
214
254
|
return;
|
|
215
255
|
}
|
|
256
|
+
if (picker.items.length === 0)
|
|
257
|
+
return;
|
|
216
258
|
if (key.upArrow) {
|
|
217
259
|
setPickerIndex((i) => Math.max(0, i - 1));
|
|
218
260
|
return;
|
|
@@ -231,11 +273,20 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
231
273
|
}
|
|
232
274
|
return;
|
|
233
275
|
}
|
|
234
|
-
if (key.escape) {
|
|
276
|
+
if (key.escape || (key.ctrl && inputChar === "c" && (approvalFeedbackRef.current || plainApprovalRef.current || pendingLogin))) {
|
|
277
|
+
if (approvalFeedbackRef.current) {
|
|
278
|
+
approvalFeedbackRef.current.cancel();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (plainApprovalRef.current) {
|
|
282
|
+
plainApprovalRef.current.cancel();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
235
285
|
if (pendingLogin) {
|
|
286
|
+
modelSetupIdRef.current += 1;
|
|
236
287
|
setPendingLogin(null);
|
|
237
288
|
setInput("");
|
|
238
|
-
pushSystem("
|
|
289
|
+
pushSystem("Model configuration cancelled. Existing settings were not changed.");
|
|
239
290
|
return;
|
|
240
291
|
}
|
|
241
292
|
if (abortRef.current) {
|
|
@@ -246,8 +297,19 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
246
297
|
}
|
|
247
298
|
if (key.ctrl && inputChar === "c") {
|
|
248
299
|
ctrlCRef.current += 1;
|
|
300
|
+
if (busy && ctrlCRef.current === 1) {
|
|
301
|
+
if (abortRef.current) {
|
|
302
|
+
abortRef.current.abort();
|
|
303
|
+
abortRef.current = null;
|
|
304
|
+
}
|
|
305
|
+
pushSystem("Cancelled current run; partial output kept. Press Ctrl+C again to quit, or type 继续 to continue.");
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
ctrlCRef.current = 0;
|
|
308
|
+
}, 1500);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
249
311
|
if (ctrlCRef.current >= 2) {
|
|
250
|
-
|
|
312
|
+
void quitCleanly();
|
|
251
313
|
}
|
|
252
314
|
else {
|
|
253
315
|
pushSystem("Press Ctrl+C again to quit.");
|
|
@@ -275,6 +337,325 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
275
337
|
{ id: idRef.current, role: "user", content: text, markdown: false },
|
|
276
338
|
]);
|
|
277
339
|
}, []);
|
|
340
|
+
const finishModelSetup = useCallback((setup, providerId, modelId, keyReceipt = undefined) => {
|
|
341
|
+
if (setup.id !== modelSetupIdRef.current)
|
|
342
|
+
return;
|
|
343
|
+
const provider = getProvider(providerId);
|
|
344
|
+
if (!provider) {
|
|
345
|
+
setPendingLogin(null);
|
|
346
|
+
setInput("");
|
|
347
|
+
pushSystem("Provider is no longer available. Configuration was not changed.");
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
setConfig((current) => ({ ...current, providerId: provider.id, model: modelId }));
|
|
351
|
+
setPendingLogin(null);
|
|
352
|
+
setInput("");
|
|
353
|
+
modelSetupIdRef.current += 1;
|
|
354
|
+
pushSystem(keyReceipt
|
|
355
|
+
? `${keyReceipt}\nActive model: ${provider.id} · ${modelId}.`
|
|
356
|
+
: `Provider/model set to ${provider.id} · ${modelId}. ${provider.authEnv.length === 0 ? "No API key is required." : ""}`);
|
|
357
|
+
}, [pushSystem]);
|
|
358
|
+
const continueModelSetup = useCallback((setup, providerId, modelId) => {
|
|
359
|
+
if (setup.id !== modelSetupIdRef.current)
|
|
360
|
+
return;
|
|
361
|
+
const provider = getProvider(providerId);
|
|
362
|
+
if (!provider) {
|
|
363
|
+
setPendingLogin(null);
|
|
364
|
+
pushSystem("Unknown provider. Configuration was not changed.");
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (provider.authEnv.length === 0) {
|
|
368
|
+
finishModelSetup(setup, provider.id, modelId);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const existing = resolveApiKey(provider.id, modelId, provider.authEnv);
|
|
372
|
+
setPendingLogin({
|
|
373
|
+
...setup,
|
|
374
|
+
step: "key",
|
|
375
|
+
provider: provider.id,
|
|
376
|
+
model: modelId,
|
|
377
|
+
hasExistingKey: existing.status === "ok",
|
|
378
|
+
});
|
|
379
|
+
setPicker(null);
|
|
380
|
+
setInput("");
|
|
381
|
+
pushSystem(`Selected ${provider.label} · ${modelId}. Enter the API key for this provider. ` +
|
|
382
|
+
"The key is provider-wide and will be reused for its models." +
|
|
383
|
+
(existing.status === "ok" ? " Press Enter on an empty input to keep the existing key." : ""));
|
|
384
|
+
}, [finishModelSetup, pushSystem]);
|
|
385
|
+
const openModelSetupPicker = useCallback(async (setup, step, providerId = undefined) => {
|
|
386
|
+
if (setup.id !== modelSetupIdRef.current)
|
|
387
|
+
return;
|
|
388
|
+
if (step === "provider") {
|
|
389
|
+
setPendingLogin({ ...setup, step: "provider", provider: undefined, model: undefined });
|
|
390
|
+
const currentProvider = setup.provider ?? config.providerId;
|
|
391
|
+
setPickerIndex(Math.max(0, PROVIDERS.findIndex((provider) => provider.id === currentProvider)));
|
|
392
|
+
setPicker({
|
|
393
|
+
title: "Configure provider · choose a provider",
|
|
394
|
+
current: currentProvider,
|
|
395
|
+
items: PROVIDERS.map((provider) => ({
|
|
396
|
+
value: provider.id,
|
|
397
|
+
label: `${provider.id.padEnd(12)} ${provider.label}`,
|
|
398
|
+
hint: provider.authEnv.length === 0 ? "no API key required" : provider.defaultModel,
|
|
399
|
+
})),
|
|
400
|
+
onSelect: (id) => {
|
|
401
|
+
const selected = getProvider(id);
|
|
402
|
+
if (!selected)
|
|
403
|
+
return;
|
|
404
|
+
const nextSetup = {
|
|
405
|
+
...setup,
|
|
406
|
+
provider: selected.id,
|
|
407
|
+
model: selected.id === config.providerId ? config.model : selected.defaultModel,
|
|
408
|
+
};
|
|
409
|
+
void openModelSetupPickerRef.current?.(nextSetup, "model", selected.id);
|
|
410
|
+
},
|
|
411
|
+
onCancel: () => {
|
|
412
|
+
if (setup.id !== modelSetupIdRef.current)
|
|
413
|
+
return;
|
|
414
|
+
modelSetupIdRef.current += 1;
|
|
415
|
+
setPendingLogin(null);
|
|
416
|
+
setInput("");
|
|
417
|
+
pushSystem("Model configuration cancelled. Existing settings were not changed.");
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const provider = getProvider(providerId);
|
|
423
|
+
if (!provider) {
|
|
424
|
+
setPendingLogin(null);
|
|
425
|
+
pushSystem("Unknown provider. Configuration was not changed.");
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
setPendingLogin({ ...setup, step: "loading-models", provider: provider.id });
|
|
429
|
+
setInput("");
|
|
430
|
+
setPickerIndex(0);
|
|
431
|
+
setPicker({
|
|
432
|
+
title: `Loading models · ${provider.label}`,
|
|
433
|
+
current: undefined,
|
|
434
|
+
items: [],
|
|
435
|
+
onSelect: () => { },
|
|
436
|
+
onCancel: () => {
|
|
437
|
+
if (setup.id !== modelSetupIdRef.current)
|
|
438
|
+
return;
|
|
439
|
+
modelSetupIdRef.current += 1;
|
|
440
|
+
setPendingLogin(null);
|
|
441
|
+
setInput("");
|
|
442
|
+
pushSystem("Model configuration cancelled. Existing settings were not changed.");
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
const { key } = resolveProviderKey(provider.id, provider.authEnv);
|
|
446
|
+
let models = [];
|
|
447
|
+
let online = false;
|
|
448
|
+
try {
|
|
449
|
+
models = uniqueModelList(await provider.listModels(key));
|
|
450
|
+
online = await probeProvider(provider, key);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
online = false;
|
|
454
|
+
}
|
|
455
|
+
if (setup.id !== modelSetupIdRef.current)
|
|
456
|
+
return;
|
|
457
|
+
if (!models.some((model) => model.id === provider.defaultModel))
|
|
458
|
+
models.unshift({ id: provider.defaultModel, label: "provider default" });
|
|
459
|
+
const manualModelValue = "__fastcode_manual_model_id__";
|
|
460
|
+
const items = [
|
|
461
|
+
...models.slice(0, 49).map((model) => ({
|
|
462
|
+
value: model.id,
|
|
463
|
+
label: model.id,
|
|
464
|
+
hint: model.label && model.label !== model.id ? model.label : undefined,
|
|
465
|
+
})),
|
|
466
|
+
{ value: manualModelValue, label: "Enter model ID manually…" },
|
|
467
|
+
];
|
|
468
|
+
const currentModel = setup.model ?? provider.defaultModel;
|
|
469
|
+
const currentIndex = items.findIndex((item) => item.value === currentModel);
|
|
470
|
+
setPendingLogin({ ...setup, step: "model", provider: provider.id, model: currentModel });
|
|
471
|
+
setPickerIndex(currentIndex >= 0 ? currentIndex : 0);
|
|
472
|
+
pushSystem(`Models for ${provider.label}: ${models.length}${online ? "" : " (offline — showing built-in/cached models)"}.`);
|
|
473
|
+
setPicker({
|
|
474
|
+
title: `Configure model · ${provider.label}${online ? "" : " · offline list"}`,
|
|
475
|
+
current: currentModel,
|
|
476
|
+
items,
|
|
477
|
+
onSelect: (id) => {
|
|
478
|
+
if (setup.id !== modelSetupIdRef.current)
|
|
479
|
+
return;
|
|
480
|
+
if (id === manualModelValue) {
|
|
481
|
+
setPendingLogin({ ...setup, step: "model-manual", provider: provider.id });
|
|
482
|
+
setInput("");
|
|
483
|
+
pushSystem("Enter a model ID (letters, numbers, . _ : / + -; up to 200 characters).");
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
continueModelSetup(setup, provider.id, id);
|
|
487
|
+
},
|
|
488
|
+
onCancel: () => {
|
|
489
|
+
if (setup.id === modelSetupIdRef.current)
|
|
490
|
+
void openModelSetupPickerRef.current?.(setup, "provider");
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
}, [config.model, config.providerId, continueModelSetup, pushSystem]);
|
|
494
|
+
openModelSetupPickerRef.current = openModelSetupPicker;
|
|
495
|
+
const startModelSetup = useCallback(({ provider, model, flow = "login" }) => {
|
|
496
|
+
const setup = {
|
|
497
|
+
id: ++modelSetupIdRef.current,
|
|
498
|
+
flow,
|
|
499
|
+
originalProvider: config.providerId,
|
|
500
|
+
originalModel: config.model,
|
|
501
|
+
provider,
|
|
502
|
+
model,
|
|
503
|
+
};
|
|
504
|
+
setPendingLogin(setup);
|
|
505
|
+
setInput("");
|
|
506
|
+
if (flow === "model" && provider) {
|
|
507
|
+
void openModelSetupPicker(setup, "model", provider);
|
|
508
|
+
}
|
|
509
|
+
else if (provider && model) {
|
|
510
|
+
continueModelSetup(setup, provider, model);
|
|
511
|
+
}
|
|
512
|
+
else if (provider) {
|
|
513
|
+
void openModelSetupPicker(setup, "model", provider);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
void openModelSetupPicker(setup, "provider");
|
|
517
|
+
}
|
|
518
|
+
}, [config.model, config.providerId, continueModelSetup, openModelSetupPicker]);
|
|
519
|
+
const requestApproval = useCallback(async (request) => {
|
|
520
|
+
const defaultDecision = () => {
|
|
521
|
+
const option = request.options[request.defaultIndex ?? 0];
|
|
522
|
+
if (!option || option.kind === "allow")
|
|
523
|
+
return { kind: "deny", reason: "approval timeout/cancel (fail closed)" };
|
|
524
|
+
return decisionForOption(option);
|
|
525
|
+
};
|
|
526
|
+
if (ui === "native" && process.platform !== "win32") {
|
|
527
|
+
const native = await nativeApproval(request, abortRef.current?.signal);
|
|
528
|
+
if (native)
|
|
529
|
+
return native;
|
|
530
|
+
}
|
|
531
|
+
const preview = request.preview?.kind === "diff" && request.preview.text
|
|
532
|
+
? `\n\n${request.preview.text.slice(0, 4000)}`
|
|
533
|
+
: "";
|
|
534
|
+
pushSystem(`[approval] Tier ${request.tier ?? 2} · ${request.tool}\n${request.prompt ?? "Approval required"}${preview}`);
|
|
535
|
+
if (ui === "json") {
|
|
536
|
+
return { kind: "deny", reason: "JSON approval mode requires the headless protocol channel (not wired in this build)" };
|
|
537
|
+
}
|
|
538
|
+
const approvalSignal = abortRef.current?.signal;
|
|
539
|
+
if (ui === "plain") {
|
|
540
|
+
return await new Promise((resolve) => {
|
|
541
|
+
let settled = false;
|
|
542
|
+
let timer;
|
|
543
|
+
const onAbort = () => finish({ kind: "deny", reason: "run cancelled" });
|
|
544
|
+
const finish = (decision) => {
|
|
545
|
+
if (settled)
|
|
546
|
+
return;
|
|
547
|
+
settled = true;
|
|
548
|
+
if (timer)
|
|
549
|
+
clearTimeout(timer);
|
|
550
|
+
approvalSignal?.removeEventListener("abort", onAbort);
|
|
551
|
+
plainApprovalRef.current = null;
|
|
552
|
+
resolve(decision);
|
|
553
|
+
};
|
|
554
|
+
approvalSignal?.addEventListener("abort", onAbort, { once: true });
|
|
555
|
+
const lines = request.options.map((option, index) => `${index + 1}. ${option.label}`);
|
|
556
|
+
pushSystem(`Choose an option:\n${lines.join("\n")}\nEnter a number (1-${request.options.length}); Esc cancels.`);
|
|
557
|
+
plainApprovalRef.current = {
|
|
558
|
+
request,
|
|
559
|
+
submit: (text) => {
|
|
560
|
+
const option = parseApprovalChoice(request, text);
|
|
561
|
+
if (!option) {
|
|
562
|
+
pushSystem(`Invalid approval choice. Enter 1-${request.options.length}.`);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (option.kind === "feedback") {
|
|
566
|
+
const prompt = option.prompt || "Why should this be denied?";
|
|
567
|
+
pushSystem(`${prompt}\nType your reason and press Enter; Esc cancels.`);
|
|
568
|
+
plainApprovalRef.current = {
|
|
569
|
+
request,
|
|
570
|
+
submit: (feedbackText) => {
|
|
571
|
+
const feedback = feedbackText.trim();
|
|
572
|
+
if (!feedback) {
|
|
573
|
+
pushSystem("Feedback cannot be empty. Type a reason, or Esc to cancel.");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
finish({ kind: "feedback", text: feedback });
|
|
577
|
+
},
|
|
578
|
+
cancel: () => finish({ kind: "deny", reason: "cancelled during feedback" }),
|
|
579
|
+
};
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
finish(decisionForOption(option));
|
|
583
|
+
},
|
|
584
|
+
cancel: () => finish({ kind: "deny", reason: "cancelled" }),
|
|
585
|
+
};
|
|
586
|
+
timer = setTimeout(() => {
|
|
587
|
+
pushSystem("Approval timed out; applying the most conservative option.");
|
|
588
|
+
finish(defaultDecision());
|
|
589
|
+
}, Math.max(1000, request.timeoutMs ?? 60_000));
|
|
590
|
+
timer.unref?.();
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
return await new Promise((resolve) => {
|
|
594
|
+
let settled = false;
|
|
595
|
+
let timer;
|
|
596
|
+
const onAbort = () => finish({ kind: "deny", reason: "run cancelled" });
|
|
597
|
+
const finish = (decision) => {
|
|
598
|
+
if (settled)
|
|
599
|
+
return;
|
|
600
|
+
settled = true;
|
|
601
|
+
if (timer)
|
|
602
|
+
clearTimeout(timer);
|
|
603
|
+
approvalSignal?.removeEventListener("abort", onAbort);
|
|
604
|
+
if (approvalFeedbackRef.current) {
|
|
605
|
+
approvalFeedbackRef.current = null;
|
|
606
|
+
setApprovalFeedbackPrompt(null);
|
|
607
|
+
}
|
|
608
|
+
setPicker(null);
|
|
609
|
+
resolve(decision);
|
|
610
|
+
};
|
|
611
|
+
approvalSignal?.addEventListener("abort", onAbort, { once: true });
|
|
612
|
+
const items = request.options.map((option, index) => ({
|
|
613
|
+
value: option.id ?? String(index),
|
|
614
|
+
label: option.label,
|
|
615
|
+
}));
|
|
616
|
+
const defaultIndex = Math.max(0, Math.min(request.defaultIndex ?? 0, items.length - 1));
|
|
617
|
+
setPickerIndex(defaultIndex);
|
|
618
|
+
setPicker({
|
|
619
|
+
title: `Approval · Tier ${request.tier ?? 2}`,
|
|
620
|
+
items,
|
|
621
|
+
current: items[defaultIndex]?.value,
|
|
622
|
+
onSelect: (value) => {
|
|
623
|
+
const index = items.findIndex((item) => item.value === value);
|
|
624
|
+
const option = request.options[index];
|
|
625
|
+
if (!option) {
|
|
626
|
+
finish({ kind: "deny", reason: "unknown approval option" });
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (option.kind === "feedback") {
|
|
630
|
+
const prompt = option.prompt || "Why should this be denied?";
|
|
631
|
+
setPicker(null);
|
|
632
|
+
setInput("");
|
|
633
|
+
setApprovalFeedbackPrompt(prompt);
|
|
634
|
+
pushSystem(`${prompt}\nType your reason and press Enter; Esc cancels.`);
|
|
635
|
+
approvalFeedbackRef.current = {
|
|
636
|
+
submit: (text) => {
|
|
637
|
+
const feedback = text.trim();
|
|
638
|
+
if (!feedback) {
|
|
639
|
+
pushSystem("Feedback cannot be empty. Type a reason, or Esc to cancel.");
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
finish({ kind: "feedback", text: feedback });
|
|
643
|
+
},
|
|
644
|
+
cancel: () => finish({ kind: "deny", reason: "cancelled during feedback" }),
|
|
645
|
+
};
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
finish(decisionForOption(option));
|
|
649
|
+
},
|
|
650
|
+
onCancel: () => finish({ kind: "deny", reason: "cancelled" }),
|
|
651
|
+
});
|
|
652
|
+
timer = setTimeout(() => {
|
|
653
|
+
pushSystem("Approval timed out; applying the most conservative option.");
|
|
654
|
+
finish(defaultDecision());
|
|
655
|
+
}, Math.max(1000, request.timeoutMs ?? 60_000));
|
|
656
|
+
timer.unref?.();
|
|
657
|
+
});
|
|
658
|
+
}, [pushSystem, ui]);
|
|
278
659
|
const actions = useMemo(() => ({
|
|
279
660
|
setProvider: (id) => setConfig((c) => ({ ...c, providerId: id })),
|
|
280
661
|
setModel: (id) => setConfig((c) => ({ ...c, model: id })),
|
|
@@ -283,6 +664,10 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
283
664
|
setAgentMode: (enabled) => setConfig((c) => ({ ...c, agentMode: enabled })),
|
|
284
665
|
setSystemPrompt: (p) => setConfig((c) => ({ ...c, systemPrompt: p })),
|
|
285
666
|
setSkillsRegistry: (url) => setConfig((c) => ({ ...c, skillsRegistry: url })),
|
|
667
|
+
setShellPoolSize: (size) => {
|
|
668
|
+
const applied = applyShellPoolSize(size);
|
|
669
|
+
setConfig((c) => ({ ...c, shellPoolSize: applied }));
|
|
670
|
+
},
|
|
286
671
|
enableSkill: (name) => setConfig((c) => ({
|
|
287
672
|
...c,
|
|
288
673
|
enabledSkills: Array.from(new Set([...(c.enabledSkills ?? []), name])),
|
|
@@ -316,14 +701,16 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
316
701
|
setExpanded: (value) => setExpanded(value),
|
|
317
702
|
setInput: (value) => setInput(value),
|
|
318
703
|
pushSystemMessage: pushSystem,
|
|
319
|
-
startLogin: (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
704
|
+
startLogin: (options) => startModelSetup(options),
|
|
705
|
+
}), [pushSystem, startModelSetup]);
|
|
706
|
+
const appendCompactionFact = useCallback((info) => {
|
|
707
|
+
const file = sessionRef.current?.file;
|
|
708
|
+
sessionRef.current.append({ type: "compacted", ...info });
|
|
709
|
+
if (typeof file === "string") {
|
|
710
|
+
for (const ref of staleSummaryRefs(file))
|
|
711
|
+
removeSummaryFile(ref);
|
|
712
|
+
}
|
|
713
|
+
}, []);
|
|
327
714
|
const runSlash = useCallback(async (raw) => {
|
|
328
715
|
const parsed = parseSlash(raw);
|
|
329
716
|
if (!parsed)
|
|
@@ -333,7 +720,17 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
333
720
|
pushSystem(`Unknown command "/${parsed.name}". Try /help.`);
|
|
334
721
|
return true;
|
|
335
722
|
}
|
|
336
|
-
if (
|
|
723
|
+
if (cmd.name === "exit" && busy) {
|
|
724
|
+
const now = Date.now();
|
|
725
|
+
if (now - exitConfirmRef.current > 5000) {
|
|
726
|
+
exitConfirmRef.current = now;
|
|
727
|
+
pushSystem("A run is active. /exit will stop it, keep the partial output, and save the session. " +
|
|
728
|
+
"Type /exit again within 5s to confirm, or /cancel to stop first.");
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
731
|
+
exitConfirmRef.current = 0;
|
|
732
|
+
}
|
|
733
|
+
if (busy && !["cancel", "steer", "jobs", "kill", "attach", "expand", "context", "threads", "exit"].includes(cmd.name)) {
|
|
337
734
|
pushSystem(`/${cmd.name} is unavailable while a run is active. Try /cancel or /steer.`);
|
|
338
735
|
return true;
|
|
339
736
|
}
|
|
@@ -373,15 +770,22 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
373
770
|
setPicker(res.picker);
|
|
374
771
|
setInput("");
|
|
375
772
|
}
|
|
773
|
+
if (res?.compacted)
|
|
774
|
+
appendCompactionFact(res.compacted);
|
|
775
|
+
if (res?.cleared)
|
|
776
|
+
sessionRef.current.append({ type: "cleared" });
|
|
376
777
|
if (res?.quit)
|
|
377
|
-
|
|
778
|
+
void quitCleanly();
|
|
378
779
|
}
|
|
379
780
|
catch (err) {
|
|
380
|
-
pushSystem(
|
|
781
|
+
pushSystem(parsed.name === "login"
|
|
782
|
+
? "Login failed. The credential was not included in this error message."
|
|
783
|
+
: `Command failed: ${err.message}`);
|
|
381
784
|
}
|
|
382
785
|
return true;
|
|
383
786
|
}, [
|
|
384
787
|
actions,
|
|
788
|
+
appendCompactionFact,
|
|
385
789
|
config.agentMode,
|
|
386
790
|
config.enabledPlugins,
|
|
387
791
|
config.enabledSkills,
|
|
@@ -392,7 +796,7 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
392
796
|
config.theme,
|
|
393
797
|
busy,
|
|
394
798
|
expanded,
|
|
395
|
-
|
|
799
|
+
quitCleanly,
|
|
396
800
|
history,
|
|
397
801
|
pushSystem,
|
|
398
802
|
pushUser,
|
|
@@ -404,6 +808,16 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
404
808
|
const text = raw.trim();
|
|
405
809
|
if (!text)
|
|
406
810
|
return;
|
|
811
|
+
if (approvalFeedbackRef.current) {
|
|
812
|
+
approvalFeedbackRef.current.submit(text);
|
|
813
|
+
setInput("");
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (plainApprovalRef.current) {
|
|
817
|
+
plainApprovalRef.current.submit(text);
|
|
818
|
+
setInput("");
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
407
821
|
if (busy) {
|
|
408
822
|
if (text.startsWith("/")) {
|
|
409
823
|
await runSlash(text);
|
|
@@ -443,6 +857,7 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
443
857
|
lastStreamFlushRef.current = 0;
|
|
444
858
|
const ctrl = new AbortController();
|
|
445
859
|
abortRef.current = ctrl;
|
|
860
|
+
busyRef.current = true;
|
|
446
861
|
setBusy(true);
|
|
447
862
|
let receivedDone = false;
|
|
448
863
|
try {
|
|
@@ -488,6 +903,7 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
488
903
|
steerQueueRef.current = [];
|
|
489
904
|
return queued;
|
|
490
905
|
},
|
|
906
|
+
requestApproval,
|
|
491
907
|
onDelta: (delta, full) => {
|
|
492
908
|
if (!sawDeltaRef.current) {
|
|
493
909
|
sawDeltaRef.current = true;
|
|
@@ -504,14 +920,14 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
504
920
|
})) {
|
|
505
921
|
switch (ev.type) {
|
|
506
922
|
case "compaction":
|
|
507
|
-
|
|
508
|
-
type: "compacted",
|
|
923
|
+
appendCompactionFact({
|
|
509
924
|
summaryRef: ev.summaryFile,
|
|
510
925
|
source: ev.source,
|
|
511
926
|
beforeTokens: ev.beforeTokens,
|
|
512
927
|
afterTokens: ev.afterTokens,
|
|
513
928
|
droppedMessages: ev.droppedMessages,
|
|
514
929
|
keepMessages: ev.keptMessages,
|
|
930
|
+
reason: ev.reason === "heap" ? "heap" : "threshold",
|
|
515
931
|
});
|
|
516
932
|
if (!compactionNotified) {
|
|
517
933
|
compactionNotified = true;
|
|
@@ -540,6 +956,16 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
540
956
|
ok: ev.ok,
|
|
541
957
|
output: ev.output,
|
|
542
958
|
sideEffects: ev.sideEffects,
|
|
959
|
+
...(ev.code ? { code: ev.code } : {}),
|
|
960
|
+
...(ev.hint ? { hint: ev.hint } : {}),
|
|
961
|
+
...(ev.truncated !== undefined ? { truncated: ev.truncated } : {}),
|
|
962
|
+
...(ev.totalLines !== undefined ? { totalLines: ev.totalLines } : {}),
|
|
963
|
+
...(ev.returnedLines !== undefined ? { returnedLines: ev.returnedLines } : {}),
|
|
964
|
+
...(ev.totalBytes !== undefined ? { totalBytes: ev.totalBytes } : {}),
|
|
965
|
+
...(ev.returnedBytes !== undefined ? { returnedBytes: ev.returnedBytes } : {}),
|
|
966
|
+
...(ev.filesRead !== undefined ? { filesRead: ev.filesRead } : {}),
|
|
967
|
+
...(ev.filesSkipped !== undefined ? { filesSkipped: ev.filesSkipped } : {}),
|
|
968
|
+
...(ev.matches !== undefined ? { matches: ev.matches } : {}),
|
|
543
969
|
});
|
|
544
970
|
const collapsed = stableUi && !expanded;
|
|
545
971
|
const body = collapsed ? collapseToolOutput(ev.output) : ev.output;
|
|
@@ -558,6 +984,25 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
558
984
|
case "loop_detected":
|
|
559
985
|
pushSystem(`Loop detected: \`${ev.fingerprint.split(":")[0]}\` repeated ${ev.occurrences} times. Asking the model to change approach.`);
|
|
560
986
|
break;
|
|
987
|
+
case "egress_denied": {
|
|
988
|
+
idRef.current += 1;
|
|
989
|
+
sessionRef.current.append({
|
|
990
|
+
type: "egress_denied",
|
|
991
|
+
outlet: ev.outlet,
|
|
992
|
+
target: ev.target,
|
|
993
|
+
rule: ev.rule,
|
|
994
|
+
mode: ev.mode,
|
|
995
|
+
wouldBlock: ev.wouldBlock,
|
|
996
|
+
code: ev.code,
|
|
997
|
+
reason: ev.reason,
|
|
998
|
+
});
|
|
999
|
+
const target = ev.target ? ` target=${JSON.stringify(ev.target)}` : "";
|
|
1000
|
+
setTranscript((prev) => [
|
|
1001
|
+
...prev,
|
|
1002
|
+
{ id: idRef.current, role: "system", content: `[egress] ${ev.mode} rule=${ev.rule}${target} would_block=${ev.wouldBlock}\n${ev.reason}`, markdown: true },
|
|
1003
|
+
]);
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
561
1006
|
case "notice":
|
|
562
1007
|
pushSystem(ev.text);
|
|
563
1008
|
break;
|
|
@@ -574,7 +1019,7 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
574
1019
|
break;
|
|
575
1020
|
case "error":
|
|
576
1021
|
if (ev.kind === "aborted")
|
|
577
|
-
pushSystem("Cancelled.");
|
|
1022
|
+
pushSystem("Cancelled current run; partial output kept. Type 继续 to continue.");
|
|
578
1023
|
else if (ev.kind === "stream_error")
|
|
579
1024
|
pushSystem(streamErrorMessage(ev.message, provider, config.model));
|
|
580
1025
|
else
|
|
@@ -607,10 +1052,12 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
607
1052
|
setStreamingText("");
|
|
608
1053
|
setAgentActivity(null);
|
|
609
1054
|
setStreamPhase("thinking");
|
|
1055
|
+
busyRef.current = false;
|
|
610
1056
|
setBusy(false);
|
|
611
1057
|
abortRef.current = null;
|
|
612
1058
|
}
|
|
613
1059
|
}, [
|
|
1060
|
+
appendCompactionFact,
|
|
614
1061
|
busy,
|
|
615
1062
|
config.agentMode,
|
|
616
1063
|
config.enabledSkills,
|
|
@@ -621,6 +1068,7 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
621
1068
|
history,
|
|
622
1069
|
pushSystem,
|
|
623
1070
|
pushUser,
|
|
1071
|
+
requestApproval,
|
|
624
1072
|
runSlash,
|
|
625
1073
|
]);
|
|
626
1074
|
// Guided `/login`: provider → model → key. Text prompts only; the key step
|
|
@@ -628,57 +1076,50 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
628
1076
|
const handleWizardSubmit = useCallback(async (raw) => {
|
|
629
1077
|
const value = raw.trim();
|
|
630
1078
|
setInput("");
|
|
631
|
-
if (!
|
|
1079
|
+
if (!pendingLogin)
|
|
632
1080
|
return;
|
|
633
|
-
if (pendingLogin.step === "
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
pushSystem(`Unknown provider "${value}". Available: ${PROVIDERS.map((p) => p.id).join(", ")}`);
|
|
1081
|
+
if (pendingLogin.step === "model-manual") {
|
|
1082
|
+
if (!isValidModelId(value)) {
|
|
1083
|
+
pushSystem("Invalid model ID. Use 1-200 letters, numbers, or . _ : / + -; configuration was not changed.");
|
|
637
1084
|
return;
|
|
638
1085
|
}
|
|
639
|
-
|
|
640
|
-
pushSystem(`${provider.label} doesn't require an API key. Nothing to save.`);
|
|
641
|
-
setPendingLogin(null);
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
setPendingLogin({ step: "model", provider: provider.id });
|
|
645
|
-
const models = await provider.listModels(undefined).catch(() => []);
|
|
646
|
-
const names = models.slice(0, 8).map((m) => m.id).join(", ");
|
|
647
|
-
pushSystem(`Provider: ${provider.label} (${provider.id}).\n` +
|
|
648
|
-
`Type the model id to bind this key to — e.g. \`${provider.defaultModel}\`.` +
|
|
649
|
-
(names ? `\n known models: ${names}` : ""));
|
|
1086
|
+
continueModelSetup(pendingLogin, pendingLogin.provider, value);
|
|
650
1087
|
return;
|
|
651
1088
|
}
|
|
652
|
-
if (pendingLogin.step
|
|
653
|
-
const provider = getProvider(pendingLogin.provider);
|
|
654
|
-
if (!provider) {
|
|
655
|
-
setPendingLogin(null);
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
setPendingLogin({ step: "key", provider: provider.id, model: value });
|
|
659
|
-
pushSystem(`Saved from model: ${provider.id} · ${value}.\n` +
|
|
660
|
-
`This key will be reused by every ${provider.label} model.\n` +
|
|
661
|
-
`Paste the API key (input is masked).` +
|
|
662
|
-
(getLegacy(provider.id) ? `\nOr type \`keep\` to re-bind the stored ${provider.label} key.` : ""));
|
|
1089
|
+
if (pendingLogin.step !== "key")
|
|
663
1090
|
return;
|
|
664
|
-
}
|
|
665
1091
|
const provider = getProvider(pendingLogin.provider);
|
|
666
1092
|
if (!provider) {
|
|
667
1093
|
setPendingLogin(null);
|
|
1094
|
+
pushSystem("Provider is no longer available. Configuration was not changed.");
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (!value) {
|
|
1098
|
+
if (pendingLogin.hasExistingKey) {
|
|
1099
|
+
finishModelSetup(pendingLogin, provider.id, pendingLogin.model);
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
pushSystem("An API key is required for this provider. Configuration was not changed.");
|
|
1103
|
+
}
|
|
668
1104
|
return;
|
|
669
1105
|
}
|
|
670
1106
|
let key = value;
|
|
671
1107
|
if (value.toLowerCase() === "keep") {
|
|
672
1108
|
const legacy = getLegacy(provider.id);
|
|
673
1109
|
if (!legacy) {
|
|
674
|
-
pushSystem(`No stored ${provider.label} key to reuse
|
|
1110
|
+
pushSystem(`No stored ${provider.label} key to reuse. Paste the API key; configuration was not changed.`);
|
|
675
1111
|
return;
|
|
676
1112
|
}
|
|
677
1113
|
key = legacy;
|
|
678
1114
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1115
|
+
try {
|
|
1116
|
+
const receipt = saveScopedKey(provider.id, provider.label, pendingLogin.model, key);
|
|
1117
|
+
finishModelSetup(pendingLogin, provider.id, pendingLogin.model, receipt);
|
|
1118
|
+
}
|
|
1119
|
+
catch {
|
|
1120
|
+
pushSystem("Could not save the API key. Credential details were omitted; model settings were not changed.");
|
|
1121
|
+
}
|
|
1122
|
+
}, [continueModelSetup, finishModelSetup, pendingLogin, pushSystem]);
|
|
682
1123
|
useEffect(() => {
|
|
683
1124
|
if (!initialPrompt || initialPromptSubmittedRef.current)
|
|
684
1125
|
return;
|