@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/src/app.ts
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, type ApprovalDecision, type ApprovalRequest } 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,20 @@ 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
|
-
}
|
|
42
|
+
import type { CommandCompaction } from "./types.js";
|
|
56
43
|
function positiveInt(value, fallback) {
|
|
57
44
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
58
45
|
}
|
|
@@ -74,6 +61,17 @@ function optionalBudget(name) {
|
|
|
74
61
|
const value = Number(process.env[name]);
|
|
75
62
|
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
76
63
|
}
|
|
64
|
+
function uniqueModelList(models) {
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
const result = [];
|
|
67
|
+
for (const model of models) {
|
|
68
|
+
if (!model?.id || seen.has(model.id))
|
|
69
|
+
continue;
|
|
70
|
+
seen.add(model.id);
|
|
71
|
+
result.push(model);
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
77
75
|
function unclosedNotice(session) {
|
|
78
76
|
const where = session.cwd || "(unknown directory)";
|
|
79
77
|
const files = session.activeFiles?.length ?? 0;
|
|
@@ -91,7 +89,7 @@ function streamErrorMessage(message, provider, model) {
|
|
|
91
89
|
}
|
|
92
90
|
return `Error: ${message}`;
|
|
93
91
|
}
|
|
94
|
-
export const App = ({ initialPrompt, resume }) => {
|
|
92
|
+
export const App = ({ initialPrompt, resume, ui = "tty" }) => {
|
|
95
93
|
const { exit } = useApp();
|
|
96
94
|
const startupRef = useRef(null);
|
|
97
95
|
if (!startupRef.current) {
|
|
@@ -166,6 +164,18 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
166
164
|
const [pendingLogin, setPendingLogin] = useState(null);
|
|
167
165
|
const [picker, setPicker] = useState(null);
|
|
168
166
|
const [pickerIndex, setPickerIndex] = useState(0);
|
|
167
|
+
const modelSetupIdRef = useRef(0);
|
|
168
|
+
const openModelSetupPickerRef = useRef(null);
|
|
169
|
+
const plainApprovalRef = useRef<null | {
|
|
170
|
+
request: ApprovalRequest;
|
|
171
|
+
submit(text: string): void;
|
|
172
|
+
cancel(): void;
|
|
173
|
+
}>(null);
|
|
174
|
+
const [approvalFeedbackPrompt, setApprovalFeedbackPrompt] = useState<string | null>(null);
|
|
175
|
+
const approvalFeedbackRef = useRef<null | {
|
|
176
|
+
submit(text: string): void;
|
|
177
|
+
cancel(): void;
|
|
178
|
+
}>(null);
|
|
169
179
|
const [busy, setBusy] = useState(false);
|
|
170
180
|
const [expanded, setExpanded] = useState(false);
|
|
171
181
|
const steerQueueRef = useRef([]);
|
|
@@ -178,20 +188,57 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
178
188
|
const sawDeltaRef = useRef(false);
|
|
179
189
|
const lastStreamFlushRef = useRef(0);
|
|
180
190
|
const ctrlCRef = useRef(0);
|
|
191
|
+
const exitConfirmRef = useRef(0);
|
|
192
|
+
const busyRef = useRef(false);
|
|
181
193
|
const initialPromptSubmittedRef = useRef(false);
|
|
194
|
+
const quittingRef = useRef(false);
|
|
195
|
+
const quitCleanly = useCallback(async () => {
|
|
196
|
+
if (quittingRef.current)
|
|
197
|
+
return;
|
|
198
|
+
quittingRef.current = true;
|
|
199
|
+
if (abortRef.current) {
|
|
200
|
+
abortRef.current.abort();
|
|
201
|
+
abortRef.current = null;
|
|
202
|
+
}
|
|
203
|
+
// Give the aborted run a chance to append its final facts before the
|
|
204
|
+
// session save point is closed.
|
|
205
|
+
const idleDeadline = Date.now() + 1000;
|
|
206
|
+
while (busyRef.current && Date.now() < idleDeadline)
|
|
207
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
208
|
+
try {
|
|
209
|
+
await shutdownShellsAsync(400);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
shutdownShells();
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
sessionRef.current?.close?.({ reason: "exit" });
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// Session close is best effort during exit.
|
|
219
|
+
}
|
|
220
|
+
exit();
|
|
221
|
+
}, [exit]);
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
applyShellPoolSize(config.shellPoolSize);
|
|
224
|
+
}, [config.shellPoolSize]);
|
|
182
225
|
const theme = getTheme(config.theme);
|
|
183
226
|
const stableUi = useMemo(() => shouldUseStableTerminalUi(), []);
|
|
184
227
|
const liveStreamingUi = useMemo(() => shouldUseLiveStreamingUi(), []);
|
|
185
228
|
// The banner picks a font that fits the terminal width (see ui/Banner.ts)
|
|
186
229
|
// and renders nothing when even the smallest font is too wide.
|
|
187
230
|
const showBanner = transcript.length <= startup.transcript.length && !busy;
|
|
188
|
-
const promptPlaceholder =
|
|
189
|
-
?
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
231
|
+
const promptPlaceholder = approvalFeedbackPrompt
|
|
232
|
+
? `${approvalFeedbackPrompt} · Esc to cancel`
|
|
233
|
+
: pendingLogin
|
|
234
|
+
? pendingLogin.step === "loading-models"
|
|
235
|
+
? "Loading available models…"
|
|
236
|
+
: pendingLogin.step === "model-manual"
|
|
237
|
+
? "Model id (e.g. deepseek-flash) · Esc to cancel"
|
|
238
|
+
: pendingLogin.hasExistingKey
|
|
239
|
+
? "API key (masked), or Enter to keep existing key · Esc to cancel"
|
|
193
240
|
: pendingLogin.provider && getLegacy(pendingLogin.provider)
|
|
194
|
-
? "
|
|
241
|
+
? "API key (masked), or type 'keep' · Esc to cancel"
|
|
195
242
|
: "Paste API key (masked) · Esc to cancel"
|
|
196
243
|
: undefined;
|
|
197
244
|
const promptMask = pendingLogin?.step === "key" ? "*" : undefined;
|
|
@@ -208,11 +255,14 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
208
255
|
// Esc cancels streaming. Double Ctrl+C quits.
|
|
209
256
|
useInput((inputChar, key) => {
|
|
210
257
|
if (picker) {
|
|
211
|
-
if (key.escape) {
|
|
212
|
-
picker.onCancel
|
|
258
|
+
if (key.escape || (key.ctrl && inputChar === "c")) {
|
|
259
|
+
const onCancel = picker.onCancel;
|
|
213
260
|
setPicker(null);
|
|
261
|
+
onCancel?.();
|
|
214
262
|
return;
|
|
215
263
|
}
|
|
264
|
+
if (picker.items.length === 0)
|
|
265
|
+
return;
|
|
216
266
|
if (key.upArrow) {
|
|
217
267
|
setPickerIndex((i) => Math.max(0, i - 1));
|
|
218
268
|
return;
|
|
@@ -231,11 +281,20 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
231
281
|
}
|
|
232
282
|
return;
|
|
233
283
|
}
|
|
234
|
-
if (key.escape) {
|
|
284
|
+
if (key.escape || (key.ctrl && inputChar === "c" && (approvalFeedbackRef.current || plainApprovalRef.current || pendingLogin))) {
|
|
285
|
+
if (approvalFeedbackRef.current) {
|
|
286
|
+
approvalFeedbackRef.current.cancel();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (plainApprovalRef.current) {
|
|
290
|
+
plainApprovalRef.current.cancel();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
235
293
|
if (pendingLogin) {
|
|
294
|
+
modelSetupIdRef.current += 1;
|
|
236
295
|
setPendingLogin(null);
|
|
237
296
|
setInput("");
|
|
238
|
-
pushSystem("
|
|
297
|
+
pushSystem("Model configuration cancelled. Existing settings were not changed.");
|
|
239
298
|
return;
|
|
240
299
|
}
|
|
241
300
|
if (abortRef.current) {
|
|
@@ -246,8 +305,19 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
246
305
|
}
|
|
247
306
|
if (key.ctrl && inputChar === "c") {
|
|
248
307
|
ctrlCRef.current += 1;
|
|
308
|
+
if (busy && ctrlCRef.current === 1) {
|
|
309
|
+
if (abortRef.current) {
|
|
310
|
+
abortRef.current.abort();
|
|
311
|
+
abortRef.current = null;
|
|
312
|
+
}
|
|
313
|
+
pushSystem("Cancelled current run; partial output kept. Press Ctrl+C again to quit, or type 继续 to continue.");
|
|
314
|
+
setTimeout(() => {
|
|
315
|
+
ctrlCRef.current = 0;
|
|
316
|
+
}, 1500);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
249
319
|
if (ctrlCRef.current >= 2) {
|
|
250
|
-
|
|
320
|
+
void quitCleanly();
|
|
251
321
|
}
|
|
252
322
|
else {
|
|
253
323
|
pushSystem("Press Ctrl+C again to quit.");
|
|
@@ -275,6 +345,327 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
275
345
|
{ id: idRef.current, role: "user", content: text, markdown: false },
|
|
276
346
|
]);
|
|
277
347
|
}, []);
|
|
348
|
+
const finishModelSetup = useCallback((setup, providerId, modelId, keyReceipt = undefined) => {
|
|
349
|
+
if (setup.id !== modelSetupIdRef.current)
|
|
350
|
+
return;
|
|
351
|
+
const provider = getProvider(providerId);
|
|
352
|
+
if (!provider) {
|
|
353
|
+
setPendingLogin(null);
|
|
354
|
+
setInput("");
|
|
355
|
+
pushSystem("Provider is no longer available. Configuration was not changed.");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
setConfig((current) => ({ ...current, providerId: provider.id, model: modelId }));
|
|
359
|
+
setPendingLogin(null);
|
|
360
|
+
setInput("");
|
|
361
|
+
modelSetupIdRef.current += 1;
|
|
362
|
+
pushSystem(keyReceipt
|
|
363
|
+
? `${keyReceipt}\nActive model: ${provider.id} · ${modelId}.`
|
|
364
|
+
: `Provider/model set to ${provider.id} · ${modelId}. ${provider.authEnv.length === 0 ? "No API key is required." : ""}`);
|
|
365
|
+
}, [pushSystem]);
|
|
366
|
+
const continueModelSetup = useCallback((setup, providerId, modelId) => {
|
|
367
|
+
if (setup.id !== modelSetupIdRef.current)
|
|
368
|
+
return;
|
|
369
|
+
const provider = getProvider(providerId);
|
|
370
|
+
if (!provider) {
|
|
371
|
+
setPendingLogin(null);
|
|
372
|
+
pushSystem("Unknown provider. Configuration was not changed.");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (provider.authEnv.length === 0) {
|
|
376
|
+
finishModelSetup(setup, provider.id, modelId);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const existing = resolveApiKey(provider.id, modelId, provider.authEnv);
|
|
380
|
+
setPendingLogin({
|
|
381
|
+
...setup,
|
|
382
|
+
step: "key",
|
|
383
|
+
provider: provider.id,
|
|
384
|
+
model: modelId,
|
|
385
|
+
hasExistingKey: existing.status === "ok",
|
|
386
|
+
});
|
|
387
|
+
setPicker(null);
|
|
388
|
+
setInput("");
|
|
389
|
+
pushSystem(`Selected ${provider.label} · ${modelId}. Enter the API key for this provider. ` +
|
|
390
|
+
"The key is provider-wide and will be reused for its models." +
|
|
391
|
+
(existing.status === "ok" ? " Press Enter on an empty input to keep the existing key." : ""));
|
|
392
|
+
}, [finishModelSetup, pushSystem]);
|
|
393
|
+
const openModelSetupPicker = useCallback(async (setup, step, providerId = undefined) => {
|
|
394
|
+
if (setup.id !== modelSetupIdRef.current)
|
|
395
|
+
return;
|
|
396
|
+
if (step === "provider") {
|
|
397
|
+
setPendingLogin({ ...setup, step: "provider", provider: undefined, model: undefined });
|
|
398
|
+
const currentProvider = setup.provider ?? config.providerId;
|
|
399
|
+
setPickerIndex(Math.max(0, PROVIDERS.findIndex((provider) => provider.id === currentProvider)));
|
|
400
|
+
setPicker({
|
|
401
|
+
title: "Configure provider · choose a provider",
|
|
402
|
+
current: currentProvider,
|
|
403
|
+
items: PROVIDERS.map((provider) => ({
|
|
404
|
+
value: provider.id,
|
|
405
|
+
label: `${provider.id.padEnd(12)} ${provider.label}`,
|
|
406
|
+
hint: provider.authEnv.length === 0 ? "no API key required" : provider.defaultModel,
|
|
407
|
+
})),
|
|
408
|
+
onSelect: (id) => {
|
|
409
|
+
const selected = getProvider(id);
|
|
410
|
+
if (!selected)
|
|
411
|
+
return;
|
|
412
|
+
const nextSetup = {
|
|
413
|
+
...setup,
|
|
414
|
+
provider: selected.id,
|
|
415
|
+
model: selected.id === config.providerId ? config.model : selected.defaultModel,
|
|
416
|
+
};
|
|
417
|
+
void openModelSetupPickerRef.current?.(nextSetup, "model", selected.id);
|
|
418
|
+
},
|
|
419
|
+
onCancel: () => {
|
|
420
|
+
if (setup.id !== modelSetupIdRef.current)
|
|
421
|
+
return;
|
|
422
|
+
modelSetupIdRef.current += 1;
|
|
423
|
+
setPendingLogin(null);
|
|
424
|
+
setInput("");
|
|
425
|
+
pushSystem("Model configuration cancelled. Existing settings were not changed.");
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const provider = getProvider(providerId);
|
|
432
|
+
if (!provider) {
|
|
433
|
+
setPendingLogin(null);
|
|
434
|
+
pushSystem("Unknown provider. Configuration was not changed.");
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
setPendingLogin({ ...setup, step: "loading-models", provider: provider.id });
|
|
438
|
+
setInput("");
|
|
439
|
+
setPickerIndex(0);
|
|
440
|
+
setPicker({
|
|
441
|
+
title: `Loading models · ${provider.label}`,
|
|
442
|
+
current: undefined,
|
|
443
|
+
items: [],
|
|
444
|
+
onSelect: () => { },
|
|
445
|
+
onCancel: () => {
|
|
446
|
+
if (setup.id !== modelSetupIdRef.current)
|
|
447
|
+
return;
|
|
448
|
+
modelSetupIdRef.current += 1;
|
|
449
|
+
setPendingLogin(null);
|
|
450
|
+
setInput("");
|
|
451
|
+
pushSystem("Model configuration cancelled. Existing settings were not changed.");
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
const { key } = resolveProviderKey(provider.id, provider.authEnv);
|
|
455
|
+
let models = [];
|
|
456
|
+
let online = false;
|
|
457
|
+
try {
|
|
458
|
+
models = uniqueModelList(await provider.listModels(key));
|
|
459
|
+
online = await probeProvider(provider, key);
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
online = false;
|
|
463
|
+
}
|
|
464
|
+
if (setup.id !== modelSetupIdRef.current)
|
|
465
|
+
return;
|
|
466
|
+
if (!models.some((model) => model.id === provider.defaultModel))
|
|
467
|
+
models.unshift({ id: provider.defaultModel, label: "provider default" });
|
|
468
|
+
const manualModelValue = "__fastcode_manual_model_id__";
|
|
469
|
+
const items = [
|
|
470
|
+
...models.slice(0, 49).map((model) => ({
|
|
471
|
+
value: model.id,
|
|
472
|
+
label: model.id,
|
|
473
|
+
hint: model.label && model.label !== model.id ? model.label : undefined,
|
|
474
|
+
})),
|
|
475
|
+
{ value: manualModelValue, label: "Enter model ID manually…" },
|
|
476
|
+
];
|
|
477
|
+
const currentModel = setup.model ?? provider.defaultModel;
|
|
478
|
+
const currentIndex = items.findIndex((item) => item.value === currentModel);
|
|
479
|
+
setPendingLogin({ ...setup, step: "model", provider: provider.id, model: currentModel });
|
|
480
|
+
setPickerIndex(currentIndex >= 0 ? currentIndex : 0);
|
|
481
|
+
pushSystem(`Models for ${provider.label}: ${models.length}${online ? "" : " (offline — showing built-in/cached models)"}.`);
|
|
482
|
+
setPicker({
|
|
483
|
+
title: `Configure model · ${provider.label}${online ? "" : " · offline list"}`,
|
|
484
|
+
current: currentModel,
|
|
485
|
+
items,
|
|
486
|
+
onSelect: (id) => {
|
|
487
|
+
if (setup.id !== modelSetupIdRef.current)
|
|
488
|
+
return;
|
|
489
|
+
if (id === manualModelValue) {
|
|
490
|
+
setPendingLogin({ ...setup, step: "model-manual", provider: provider.id });
|
|
491
|
+
setInput("");
|
|
492
|
+
pushSystem("Enter a model ID (letters, numbers, . _ : / + -; up to 200 characters).");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
continueModelSetup(setup, provider.id, id);
|
|
496
|
+
},
|
|
497
|
+
onCancel: () => {
|
|
498
|
+
if (setup.id === modelSetupIdRef.current)
|
|
499
|
+
void openModelSetupPickerRef.current?.(setup, "provider");
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
}, [config.model, config.providerId, continueModelSetup, pushSystem]);
|
|
503
|
+
openModelSetupPickerRef.current = openModelSetupPicker;
|
|
504
|
+
const startModelSetup = useCallback(({ provider, model, flow = "login" }) => {
|
|
505
|
+
const setup = {
|
|
506
|
+
id: ++modelSetupIdRef.current,
|
|
507
|
+
flow,
|
|
508
|
+
originalProvider: config.providerId,
|
|
509
|
+
originalModel: config.model,
|
|
510
|
+
provider,
|
|
511
|
+
model,
|
|
512
|
+
};
|
|
513
|
+
setPendingLogin(setup);
|
|
514
|
+
setInput("");
|
|
515
|
+
if (flow === "model" && provider) {
|
|
516
|
+
void openModelSetupPicker(setup, "model", provider);
|
|
517
|
+
}
|
|
518
|
+
else if (provider && model) {
|
|
519
|
+
continueModelSetup(setup, provider, model);
|
|
520
|
+
}
|
|
521
|
+
else if (provider) {
|
|
522
|
+
void openModelSetupPicker(setup, "model", provider);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
void openModelSetupPicker(setup, "provider");
|
|
526
|
+
}
|
|
527
|
+
}, [config.model, config.providerId, continueModelSetup, openModelSetupPicker]);
|
|
528
|
+
const requestApproval = useCallback(async (request: ApprovalRequest): Promise<ApprovalDecision> => {
|
|
529
|
+
const defaultDecision = (): ApprovalDecision => {
|
|
530
|
+
const option = request.options[request.defaultIndex ?? 0];
|
|
531
|
+
if (!option || option.kind === "allow")
|
|
532
|
+
return { kind: "deny", reason: "approval timeout/cancel (fail closed)" };
|
|
533
|
+
return decisionForOption(option);
|
|
534
|
+
};
|
|
535
|
+
if (ui === "native" && process.platform !== "win32") {
|
|
536
|
+
const native = await nativeApproval(request, abortRef.current?.signal);
|
|
537
|
+
if (native)
|
|
538
|
+
return native;
|
|
539
|
+
}
|
|
540
|
+
const preview = request.preview?.kind === "diff" && request.preview.text
|
|
541
|
+
? `\n\n${request.preview.text.slice(0, 4000)}`
|
|
542
|
+
: "";
|
|
543
|
+
pushSystem(`[approval] Tier ${request.tier ?? 2} · ${request.tool}\n${request.prompt ?? "Approval required"}${preview}`);
|
|
544
|
+
if (ui === "json") {
|
|
545
|
+
return { kind: "deny", reason: "JSON approval mode requires the headless protocol channel (not wired in this build)" };
|
|
546
|
+
}
|
|
547
|
+
const approvalSignal = abortRef.current?.signal;
|
|
548
|
+
if (ui === "plain") {
|
|
549
|
+
return await new Promise((resolve) => {
|
|
550
|
+
let settled = false;
|
|
551
|
+
let timer;
|
|
552
|
+
const onAbort = () => finish({ kind: "deny", reason: "run cancelled" });
|
|
553
|
+
const finish = (decision: ApprovalDecision) => {
|
|
554
|
+
if (settled)
|
|
555
|
+
return;
|
|
556
|
+
settled = true;
|
|
557
|
+
if (timer)
|
|
558
|
+
clearTimeout(timer);
|
|
559
|
+
approvalSignal?.removeEventListener("abort", onAbort);
|
|
560
|
+
plainApprovalRef.current = null;
|
|
561
|
+
resolve(decision);
|
|
562
|
+
};
|
|
563
|
+
approvalSignal?.addEventListener("abort", onAbort, { once: true });
|
|
564
|
+
const lines = request.options.map((option, index) => `${index + 1}. ${option.label}`);
|
|
565
|
+
pushSystem(`Choose an option:\n${lines.join("\n")}\nEnter a number (1-${request.options.length}); Esc cancels.`);
|
|
566
|
+
plainApprovalRef.current = {
|
|
567
|
+
request,
|
|
568
|
+
submit: (text) => {
|
|
569
|
+
const option = parseApprovalChoice(request, text);
|
|
570
|
+
if (!option) {
|
|
571
|
+
pushSystem(`Invalid approval choice. Enter 1-${request.options.length}.`);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (option.kind === "feedback") {
|
|
575
|
+
const prompt = option.prompt || "Why should this be denied?";
|
|
576
|
+
pushSystem(`${prompt}\nType your reason and press Enter; Esc cancels.`);
|
|
577
|
+
plainApprovalRef.current = {
|
|
578
|
+
request,
|
|
579
|
+
submit: (feedbackText) => {
|
|
580
|
+
const feedback = feedbackText.trim();
|
|
581
|
+
if (!feedback) {
|
|
582
|
+
pushSystem("Feedback cannot be empty. Type a reason, or Esc to cancel.");
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
finish({ kind: "feedback", text: feedback });
|
|
586
|
+
},
|
|
587
|
+
cancel: () => finish({ kind: "deny", reason: "cancelled during feedback" }),
|
|
588
|
+
};
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
finish(decisionForOption(option));
|
|
592
|
+
},
|
|
593
|
+
cancel: () => finish({ kind: "deny", reason: "cancelled" }),
|
|
594
|
+
};
|
|
595
|
+
timer = setTimeout(() => {
|
|
596
|
+
pushSystem("Approval timed out; applying the most conservative option.");
|
|
597
|
+
finish(defaultDecision());
|
|
598
|
+
}, Math.max(1000, request.timeoutMs ?? 60_000));
|
|
599
|
+
timer.unref?.();
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
return await new Promise((resolve) => {
|
|
603
|
+
let settled = false;
|
|
604
|
+
let timer;
|
|
605
|
+
const onAbort = () => finish({ kind: "deny", reason: "run cancelled" });
|
|
606
|
+
const finish = (decision: ApprovalDecision) => {
|
|
607
|
+
if (settled)
|
|
608
|
+
return;
|
|
609
|
+
settled = true;
|
|
610
|
+
if (timer)
|
|
611
|
+
clearTimeout(timer);
|
|
612
|
+
approvalSignal?.removeEventListener("abort", onAbort);
|
|
613
|
+
if (approvalFeedbackRef.current) {
|
|
614
|
+
approvalFeedbackRef.current = null;
|
|
615
|
+
setApprovalFeedbackPrompt(null);
|
|
616
|
+
}
|
|
617
|
+
setPicker(null);
|
|
618
|
+
resolve(decision);
|
|
619
|
+
};
|
|
620
|
+
approvalSignal?.addEventListener("abort", onAbort, { once: true });
|
|
621
|
+
const items = request.options.map((option, index) => ({
|
|
622
|
+
value: option.id ?? String(index),
|
|
623
|
+
label: option.label,
|
|
624
|
+
}));
|
|
625
|
+
const defaultIndex = Math.max(0, Math.min(request.defaultIndex ?? 0, items.length - 1));
|
|
626
|
+
setPickerIndex(defaultIndex);
|
|
627
|
+
setPicker({
|
|
628
|
+
title: `Approval · Tier ${request.tier ?? 2}`,
|
|
629
|
+
items,
|
|
630
|
+
current: items[defaultIndex]?.value,
|
|
631
|
+
onSelect: (value) => {
|
|
632
|
+
const index = items.findIndex((item) => item.value === value);
|
|
633
|
+
const option = request.options[index];
|
|
634
|
+
if (!option) {
|
|
635
|
+
finish({ kind: "deny", reason: "unknown approval option" });
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (option.kind === "feedback") {
|
|
639
|
+
const prompt = option.prompt || "Why should this be denied?";
|
|
640
|
+
setPicker(null);
|
|
641
|
+
setInput("");
|
|
642
|
+
setApprovalFeedbackPrompt(prompt);
|
|
643
|
+
pushSystem(`${prompt}\nType your reason and press Enter; Esc cancels.`);
|
|
644
|
+
approvalFeedbackRef.current = {
|
|
645
|
+
submit: (text) => {
|
|
646
|
+
const feedback = text.trim();
|
|
647
|
+
if (!feedback) {
|
|
648
|
+
pushSystem("Feedback cannot be empty. Type a reason, or Esc to cancel.");
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
finish({ kind: "feedback", text: feedback });
|
|
652
|
+
},
|
|
653
|
+
cancel: () => finish({ kind: "deny", reason: "cancelled during feedback" }),
|
|
654
|
+
};
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
finish(decisionForOption(option));
|
|
658
|
+
},
|
|
659
|
+
onCancel: () => finish({ kind: "deny", reason: "cancelled" }),
|
|
660
|
+
});
|
|
661
|
+
timer = setTimeout(() => {
|
|
662
|
+
pushSystem("Approval timed out; applying the most conservative option.");
|
|
663
|
+
finish(defaultDecision());
|
|
664
|
+
}, Math.max(1000, request.timeoutMs ?? 60_000));
|
|
665
|
+
timer.unref?.();
|
|
666
|
+
});
|
|
667
|
+
}, [pushSystem, ui]);
|
|
668
|
+
|
|
278
669
|
const actions = useMemo(() => ({
|
|
279
670
|
setProvider: (id) => setConfig((c) => ({ ...c, providerId: id })),
|
|
280
671
|
setModel: (id) => setConfig((c) => ({ ...c, model: id })),
|
|
@@ -283,6 +674,10 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
283
674
|
setAgentMode: (enabled) => setConfig((c) => ({ ...c, agentMode: enabled })),
|
|
284
675
|
setSystemPrompt: (p) => setConfig((c) => ({ ...c, systemPrompt: p })),
|
|
285
676
|
setSkillsRegistry: (url) => setConfig((c) => ({ ...c, skillsRegistry: url })),
|
|
677
|
+
setShellPoolSize: (size) => {
|
|
678
|
+
const applied = applyShellPoolSize(size);
|
|
679
|
+
setConfig((c) => ({ ...c, shellPoolSize: applied }));
|
|
680
|
+
},
|
|
286
681
|
enableSkill: (name) => setConfig((c) => ({
|
|
287
682
|
...c,
|
|
288
683
|
enabledSkills: Array.from(new Set([...(c.enabledSkills ?? []), name])),
|
|
@@ -316,14 +711,16 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
316
711
|
setExpanded: (value) => setExpanded(value),
|
|
317
712
|
setInput: (value) => setInput(value),
|
|
318
713
|
pushSystemMessage: pushSystem,
|
|
319
|
-
startLogin: (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
714
|
+
startLogin: (options) => startModelSetup(options),
|
|
715
|
+
}), [pushSystem, startModelSetup]);
|
|
716
|
+
const appendCompactionFact = useCallback((info: CommandCompaction) => {
|
|
717
|
+
const file = sessionRef.current?.file;
|
|
718
|
+
sessionRef.current.append({ type: "compacted", ...info });
|
|
719
|
+
if (typeof file === "string") {
|
|
720
|
+
for (const ref of staleSummaryRefs(file))
|
|
721
|
+
removeSummaryFile(ref);
|
|
722
|
+
}
|
|
723
|
+
}, []);
|
|
327
724
|
const runSlash = useCallback(async (raw) => {
|
|
328
725
|
const parsed = parseSlash(raw);
|
|
329
726
|
if (!parsed)
|
|
@@ -333,7 +730,17 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
333
730
|
pushSystem(`Unknown command "/${parsed.name}". Try /help.`);
|
|
334
731
|
return true;
|
|
335
732
|
}
|
|
336
|
-
if (
|
|
733
|
+
if (cmd.name === "exit" && busy) {
|
|
734
|
+
const now = Date.now();
|
|
735
|
+
if (now - exitConfirmRef.current > 5000) {
|
|
736
|
+
exitConfirmRef.current = now;
|
|
737
|
+
pushSystem("A run is active. /exit will stop it, keep the partial output, and save the session. " +
|
|
738
|
+
"Type /exit again within 5s to confirm, or /cancel to stop first.");
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
exitConfirmRef.current = 0;
|
|
742
|
+
}
|
|
743
|
+
if (busy && !["cancel", "steer", "jobs", "kill", "attach", "expand", "context", "threads", "exit"].includes(cmd.name)) {
|
|
337
744
|
pushSystem(`/${cmd.name} is unavailable while a run is active. Try /cancel or /steer.`);
|
|
338
745
|
return true;
|
|
339
746
|
}
|
|
@@ -373,15 +780,22 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
373
780
|
setPicker(res.picker);
|
|
374
781
|
setInput("");
|
|
375
782
|
}
|
|
783
|
+
if (res?.compacted)
|
|
784
|
+
appendCompactionFact(res.compacted);
|
|
785
|
+
if (res?.cleared)
|
|
786
|
+
sessionRef.current.append({ type: "cleared" });
|
|
376
787
|
if (res?.quit)
|
|
377
|
-
|
|
788
|
+
void quitCleanly();
|
|
378
789
|
}
|
|
379
790
|
catch (err) {
|
|
380
|
-
pushSystem(
|
|
791
|
+
pushSystem(parsed.name === "login"
|
|
792
|
+
? "Login failed. The credential was not included in this error message."
|
|
793
|
+
: `Command failed: ${err.message}`);
|
|
381
794
|
}
|
|
382
795
|
return true;
|
|
383
796
|
}, [
|
|
384
797
|
actions,
|
|
798
|
+
appendCompactionFact,
|
|
385
799
|
config.agentMode,
|
|
386
800
|
config.enabledPlugins,
|
|
387
801
|
config.enabledSkills,
|
|
@@ -392,7 +806,7 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
392
806
|
config.theme,
|
|
393
807
|
busy,
|
|
394
808
|
expanded,
|
|
395
|
-
|
|
809
|
+
quitCleanly,
|
|
396
810
|
history,
|
|
397
811
|
pushSystem,
|
|
398
812
|
pushUser,
|
|
@@ -404,6 +818,16 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
404
818
|
const text = raw.trim();
|
|
405
819
|
if (!text)
|
|
406
820
|
return;
|
|
821
|
+
if (approvalFeedbackRef.current) {
|
|
822
|
+
approvalFeedbackRef.current.submit(text);
|
|
823
|
+
setInput("");
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (plainApprovalRef.current) {
|
|
827
|
+
plainApprovalRef.current.submit(text);
|
|
828
|
+
setInput("");
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
407
831
|
if (busy) {
|
|
408
832
|
if (text.startsWith("/")) {
|
|
409
833
|
await runSlash(text);
|
|
@@ -443,6 +867,7 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
443
867
|
lastStreamFlushRef.current = 0;
|
|
444
868
|
const ctrl = new AbortController();
|
|
445
869
|
abortRef.current = ctrl;
|
|
870
|
+
busyRef.current = true;
|
|
446
871
|
setBusy(true);
|
|
447
872
|
let receivedDone = false;
|
|
448
873
|
try {
|
|
@@ -488,6 +913,7 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
488
913
|
steerQueueRef.current = [];
|
|
489
914
|
return queued;
|
|
490
915
|
},
|
|
916
|
+
requestApproval,
|
|
491
917
|
onDelta: (delta, full) => {
|
|
492
918
|
if (!sawDeltaRef.current) {
|
|
493
919
|
sawDeltaRef.current = true;
|
|
@@ -504,14 +930,14 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
504
930
|
})) {
|
|
505
931
|
switch (ev.type) {
|
|
506
932
|
case "compaction":
|
|
507
|
-
|
|
508
|
-
type: "compacted",
|
|
933
|
+
appendCompactionFact({
|
|
509
934
|
summaryRef: ev.summaryFile,
|
|
510
935
|
source: ev.source,
|
|
511
936
|
beforeTokens: ev.beforeTokens,
|
|
512
937
|
afterTokens: ev.afterTokens,
|
|
513
938
|
droppedMessages: ev.droppedMessages,
|
|
514
939
|
keepMessages: ev.keptMessages,
|
|
940
|
+
reason: ev.reason === "heap" ? "heap" : "threshold",
|
|
515
941
|
});
|
|
516
942
|
if (!compactionNotified) {
|
|
517
943
|
compactionNotified = true;
|
|
@@ -540,6 +966,16 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
540
966
|
ok: ev.ok,
|
|
541
967
|
output: ev.output,
|
|
542
968
|
sideEffects: ev.sideEffects,
|
|
969
|
+
...(ev.code ? { code: ev.code } : {}),
|
|
970
|
+
...(ev.hint ? { hint: ev.hint } : {}),
|
|
971
|
+
...(ev.truncated !== undefined ? { truncated: ev.truncated } : {}),
|
|
972
|
+
...(ev.totalLines !== undefined ? { totalLines: ev.totalLines } : {}),
|
|
973
|
+
...(ev.returnedLines !== undefined ? { returnedLines: ev.returnedLines } : {}),
|
|
974
|
+
...(ev.totalBytes !== undefined ? { totalBytes: ev.totalBytes } : {}),
|
|
975
|
+
...(ev.returnedBytes !== undefined ? { returnedBytes: ev.returnedBytes } : {}),
|
|
976
|
+
...(ev.filesRead !== undefined ? { filesRead: ev.filesRead } : {}),
|
|
977
|
+
...(ev.filesSkipped !== undefined ? { filesSkipped: ev.filesSkipped } : {}),
|
|
978
|
+
...(ev.matches !== undefined ? { matches: ev.matches } : {}),
|
|
543
979
|
});
|
|
544
980
|
const collapsed = stableUi && !expanded;
|
|
545
981
|
const body = collapsed ? collapseToolOutput(ev.output) : ev.output;
|
|
@@ -558,6 +994,25 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
558
994
|
case "loop_detected":
|
|
559
995
|
pushSystem(`Loop detected: \`${ev.fingerprint.split(":")[0]}\` repeated ${ev.occurrences} times. Asking the model to change approach.`);
|
|
560
996
|
break;
|
|
997
|
+
case "egress_denied": {
|
|
998
|
+
idRef.current += 1;
|
|
999
|
+
sessionRef.current.append({
|
|
1000
|
+
type: "egress_denied",
|
|
1001
|
+
outlet: ev.outlet,
|
|
1002
|
+
target: ev.target,
|
|
1003
|
+
rule: ev.rule,
|
|
1004
|
+
mode: ev.mode,
|
|
1005
|
+
wouldBlock: ev.wouldBlock,
|
|
1006
|
+
code: ev.code,
|
|
1007
|
+
reason: ev.reason,
|
|
1008
|
+
});
|
|
1009
|
+
const target = ev.target ? ` target=${JSON.stringify(ev.target)}` : "";
|
|
1010
|
+
setTranscript((prev) => [
|
|
1011
|
+
...prev,
|
|
1012
|
+
{ id: idRef.current, role: "system", content: `[egress] ${ev.mode} rule=${ev.rule}${target} would_block=${ev.wouldBlock}\n${ev.reason}`, markdown: true },
|
|
1013
|
+
]);
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
561
1016
|
case "notice":
|
|
562
1017
|
pushSystem(ev.text);
|
|
563
1018
|
break;
|
|
@@ -574,7 +1029,7 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
574
1029
|
break;
|
|
575
1030
|
case "error":
|
|
576
1031
|
if (ev.kind === "aborted")
|
|
577
|
-
pushSystem("Cancelled.");
|
|
1032
|
+
pushSystem("Cancelled current run; partial output kept. Type 继续 to continue.");
|
|
578
1033
|
else if (ev.kind === "stream_error")
|
|
579
1034
|
pushSystem(streamErrorMessage(ev.message, provider, config.model));
|
|
580
1035
|
else
|
|
@@ -607,10 +1062,12 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
607
1062
|
setStreamingText("");
|
|
608
1063
|
setAgentActivity(null);
|
|
609
1064
|
setStreamPhase("thinking");
|
|
1065
|
+
busyRef.current = false;
|
|
610
1066
|
setBusy(false);
|
|
611
1067
|
abortRef.current = null;
|
|
612
1068
|
}
|
|
613
1069
|
}, [
|
|
1070
|
+
appendCompactionFact,
|
|
614
1071
|
busy,
|
|
615
1072
|
config.agentMode,
|
|
616
1073
|
config.enabledSkills,
|
|
@@ -621,6 +1078,7 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
621
1078
|
history,
|
|
622
1079
|
pushSystem,
|
|
623
1080
|
pushUser,
|
|
1081
|
+
requestApproval,
|
|
624
1082
|
runSlash,
|
|
625
1083
|
]);
|
|
626
1084
|
// Guided `/login`: provider → model → key. Text prompts only; the key step
|
|
@@ -628,57 +1086,50 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
628
1086
|
const handleWizardSubmit = useCallback(async (raw) => {
|
|
629
1087
|
const value = raw.trim();
|
|
630
1088
|
setInput("");
|
|
631
|
-
if (!
|
|
1089
|
+
if (!pendingLogin)
|
|
632
1090
|
return;
|
|
633
|
-
if (pendingLogin.step === "
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
pushSystem(`Unknown provider "${value}". Available: ${PROVIDERS.map((p) => p.id).join(", ")}`);
|
|
1091
|
+
if (pendingLogin.step === "model-manual") {
|
|
1092
|
+
if (!isValidModelId(value)) {
|
|
1093
|
+
pushSystem("Invalid model ID. Use 1-200 letters, numbers, or . _ : / + -; configuration was not changed.");
|
|
637
1094
|
return;
|
|
638
1095
|
}
|
|
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}` : ""));
|
|
1096
|
+
continueModelSetup(pendingLogin, pendingLogin.provider, value);
|
|
650
1097
|
return;
|
|
651
1098
|
}
|
|
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.` : ""));
|
|
1099
|
+
if (pendingLogin.step !== "key")
|
|
663
1100
|
return;
|
|
664
|
-
}
|
|
665
1101
|
const provider = getProvider(pendingLogin.provider);
|
|
666
1102
|
if (!provider) {
|
|
667
1103
|
setPendingLogin(null);
|
|
1104
|
+
pushSystem("Provider is no longer available. Configuration was not changed.");
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (!value) {
|
|
1108
|
+
if (pendingLogin.hasExistingKey) {
|
|
1109
|
+
finishModelSetup(pendingLogin, provider.id, pendingLogin.model);
|
|
1110
|
+
}
|
|
1111
|
+
else {
|
|
1112
|
+
pushSystem("An API key is required for this provider. Configuration was not changed.");
|
|
1113
|
+
}
|
|
668
1114
|
return;
|
|
669
1115
|
}
|
|
670
1116
|
let key = value;
|
|
671
1117
|
if (value.toLowerCase() === "keep") {
|
|
672
1118
|
const legacy = getLegacy(provider.id);
|
|
673
1119
|
if (!legacy) {
|
|
674
|
-
pushSystem(`No stored ${provider.label} key to reuse
|
|
1120
|
+
pushSystem(`No stored ${provider.label} key to reuse. Paste the API key; configuration was not changed.`);
|
|
675
1121
|
return;
|
|
676
1122
|
}
|
|
677
1123
|
key = legacy;
|
|
678
1124
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1125
|
+
try {
|
|
1126
|
+
const receipt = saveScopedKey(provider.id, provider.label, pendingLogin.model, key);
|
|
1127
|
+
finishModelSetup(pendingLogin, provider.id, pendingLogin.model, receipt);
|
|
1128
|
+
}
|
|
1129
|
+
catch {
|
|
1130
|
+
pushSystem("Could not save the API key. Credential details were omitted; model settings were not changed.");
|
|
1131
|
+
}
|
|
1132
|
+
}, [continueModelSetup, finishModelSetup, pendingLogin, pushSystem]);
|
|
682
1133
|
useEffect(() => {
|
|
683
1134
|
if (!initialPrompt || initialPromptSubmittedRef.current)
|
|
684
1135
|
return;
|