@datename/fastcode-termux 0.1.6 → 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 +67 -16
- 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 +83 -0
- package/dist/agent/approval.js +42 -0
- package/dist/agent/compaction.d.ts +41 -0
- package/dist/agent/compaction.js +270 -0
- package/dist/agent/context.d.ts +29 -0
- package/dist/agent/context.js +62 -0
- package/dist/agent/guards.d.ts +24 -16
- package/dist/agent/guards.js +1167 -18
- package/dist/agent/ignore.d.ts +11 -0
- package/dist/agent/ignore.js +129 -0
- 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 +22 -0
- package/dist/agent/process-tree.js +102 -0
- package/dist/agent/protocol-mode.d.ts +6 -0
- package/dist/agent/protocol-mode.js +51 -0
- package/dist/agent/protocol.d.ts +20 -1
- package/dist/agent/protocol.js +169 -39
- package/dist/agent/registry.d.ts +19 -0
- package/dist/agent/registry.js +247 -0
- package/dist/agent/runner.d.ts +127 -0
- package/dist/agent/runner.js +391 -0
- package/dist/agent/shell-ast.d.ts +11 -0
- package/dist/agent/shell-ast.js +204 -0
- package/dist/agent/shell-session.d.ts +51 -0
- package/dist/agent/shell-session.js +611 -0
- package/dist/agent/stream.d.ts +29 -0
- package/dist/agent/stream.js +103 -0
- package/dist/agent/tool-schemas.d.ts +2 -0
- package/dist/agent/tool-schemas.js +82 -0
- package/dist/agent/tools/append_file.d.ts +2 -0
- package/dist/agent/tools/append_file.js +28 -0
- package/dist/agent/tools/apply_patch.d.ts +10 -0
- package/dist/agent/tools/apply_patch.js +92 -0
- package/dist/agent/tools/copy_file.d.ts +2 -0
- package/dist/agent/tools/copy_file.js +53 -0
- package/dist/agent/tools/delete_file.d.ts +2 -0
- package/dist/agent/tools/delete_file.js +43 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +54 -0
- package/dist/agent/tools/index.d.ts +2 -0
- package/dist/agent/tools/index.js +32 -0
- package/dist/agent/tools/list_files.d.ts +2 -0
- package/dist/agent/tools/list_files.js +17 -0
- package/dist/agent/tools/make_dir.d.ts +2 -0
- package/dist/agent/tools/make_dir.js +26 -0
- package/dist/agent/tools/move_path.d.ts +9 -0
- package/dist/agent/tools/move_path.js +73 -0
- package/dist/agent/tools/paths.d.ts +7 -0
- package/dist/agent/tools/paths.js +84 -0
- package/dist/agent/tools/pwd.d.ts +2 -0
- package/dist/agent/tools/pwd.js +10 -0
- package/dist/agent/tools/read_file.d.ts +2 -0
- package/dist/agent/tools/read_file.js +81 -0
- package/dist/agent/tools/read_many.d.ts +2 -0
- package/dist/agent/tools/read_many.js +43 -0
- package/dist/agent/tools/readers.d.ts +58 -0
- package/dist/agent/tools/readers.js +426 -0
- package/dist/agent/tools/replace_in_file.d.ts +2 -0
- package/dist/agent/tools/replace_in_file.js +63 -0
- package/dist/agent/tools/shell.d.ts +2 -0
- package/dist/agent/tools/shell.js +48 -0
- package/dist/agent/tools/stat_path.d.ts +2 -0
- package/dist/agent/tools/stat_path.js +31 -0
- package/dist/agent/tools/types.d.ts +65 -0
- package/dist/agent/tools/types.js +1 -0
- package/dist/agent/tools/validate.d.ts +19 -0
- package/dist/agent/tools/validate.js +92 -0
- package/dist/agent/tools/write_file.d.ts +2 -0
- package/dist/agent/tools/write_file.js +28 -0
- package/dist/agent/tools.d.ts +3 -1
- package/dist/agent/tools.js +2 -377
- package/dist/app.d.ts +2 -1
- package/dist/app.js +846 -173
- package/dist/cli-main.js +363 -0
- package/dist/cli.js +444 -333
- package/dist/commands/about.js +1 -1
- package/dist/commands/attach.d.ts +9 -0
- package/dist/commands/attach.js +15 -0
- package/dist/commands/bg.d.ts +7 -0
- package/dist/commands/bg.js +9 -0
- package/dist/commands/cancel.d.ts +10 -0
- package/dist/commands/cancel.js +10 -0
- package/dist/commands/clear.d.ts +1 -0
- package/dist/commands/clear.js +1 -1
- package/dist/commands/compact.d.ts +22 -0
- package/dist/commands/compact.js +34 -0
- package/dist/commands/context.d.ts +7 -0
- package/dist/commands/context.js +25 -0
- package/dist/commands/edit.d.ts +10 -0
- package/dist/commands/edit.js +36 -0
- package/dist/commands/effort.d.ts +25 -0
- package/dist/commands/effort.js +48 -0
- package/dist/commands/expand.d.ts +10 -0
- package/dist/commands/expand.js +8 -0
- package/dist/commands/index.js +24 -0
- package/dist/commands/jobs.d.ts +7 -0
- package/dist/commands/jobs.js +17 -0
- package/dist/commands/kill.d.ts +7 -0
- package/dist/commands/kill.js +13 -0
- package/dist/commands/login.d.ts +6 -4
- package/dist/commands/login.js +24 -22
- package/dist/commands/logout.js +8 -16
- package/dist/commands/model.d.ts +1 -0
- package/dist/commands/model.js +10 -22
- package/dist/commands/provider.d.ts +1 -2
- package/dist/commands/provider.js +4 -11
- package/dist/commands/skills.js +34 -4
- package/dist/commands/steer.d.ts +11 -0
- package/dist/commands/steer.js +13 -0
- package/dist/commands/threads.d.ts +9 -0
- package/dist/commands/threads.js +26 -0
- package/dist/commands/whoami.js +6 -5
- package/dist/config/store.d.ts +53 -12
- package/dist/config/store.js +163 -73
- package/dist/config/trust.d.ts +30 -8
- package/dist/config/trust.js +139 -14
- 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 +48 -4
- package/dist/lib/diagnostics.js +90 -8
- package/dist/lib/highlight-subset.d.ts +21 -0
- package/dist/lib/highlight-subset.js +88 -0
- package/dist/lib/http.d.ts +11 -1
- package/dist/lib/http.js +62 -4
- package/dist/lib/lifecycle.d.ts +5 -0
- package/dist/lib/lifecycle.js +62 -0
- package/dist/lib/markdown.js +22 -4
- package/dist/lib/model-meta.d.ts +19 -0
- package/dist/lib/model-meta.js +113 -0
- package/dist/lib/native-tools.d.ts +12 -0
- package/dist/lib/native-tools.js +21 -0
- package/dist/lib/net.d.ts +33 -0
- package/dist/lib/net.js +168 -0
- package/dist/patch/apply.d.ts +15 -0
- package/dist/patch/apply.js +172 -0
- package/dist/patch/parse.d.ts +29 -0
- package/dist/patch/parse.js +112 -0
- package/dist/patch/plan.d.ts +49 -0
- package/dist/patch/plan.js +273 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +12 -0
- package/dist/providers/custom.d.ts +9 -0
- package/dist/providers/deepseek.d.ts +10 -0
- package/dist/providers/deepseek.js +3 -2
- package/dist/providers/google.d.ts +9 -0
- package/dist/providers/groq.d.ts +10 -0
- package/dist/providers/groq.js +2 -1
- package/dist/providers/index.d.ts +10 -201
- package/dist/providers/index.js +30 -0
- package/dist/providers/mistral.d.ts +10 -0
- package/dist/providers/mistral.js +2 -1
- package/dist/providers/moonshot.d.ts +10 -0
- package/dist/providers/moonshot.js +2 -1
- package/dist/providers/ollama.d.ts +9 -0
- package/dist/providers/openai.d.ts +10 -0
- package/dist/providers/openai.js +2 -1
- package/dist/providers/openaiCompatible.d.ts +33 -2
- package/dist/providers/openaiCompatible.js +78 -16
- package/dist/providers/openrouter.d.ts +10 -0
- package/dist/providers/openrouter.js +2 -1
- package/dist/providers/qwen.d.ts +10 -0
- package/dist/providers/qwen.js +2 -1
- package/dist/providers/spec.d.ts +12 -0
- package/dist/providers/spec.js +29 -0
- package/dist/providers/xai.d.ts +10 -0
- package/dist/providers/xai.js +2 -1
- package/dist/providers/zhipu.d.ts +10 -0
- package/dist/providers/zhipu.js +2 -1
- package/dist/session/log.d.ts +49 -14
- package/dist/session/log.js +236 -11
- package/dist/session/projection.d.ts +76 -0
- package/dist/session/projection.js +526 -0
- package/dist/skills/manager.d.ts +15 -0
- package/dist/skills/manager.js +135 -18
- package/dist/types.d.ts +75 -1
- package/dist/ui/Message.d.ts +1 -7
- package/dist/ui/Message.js +13 -2
- package/dist/ui/Picker.d.ts +8 -0
- package/dist/ui/Picker.js +24 -0
- package/dist/ui/Prompt.d.ts +1 -2
- package/dist/ui/Prompt.js +2 -2
- package/dist/ui/Spinner.js +11 -5
- package/dist/ui/StatusBar.d.ts +2 -1
- package/dist/ui/StatusBar.js +6 -5
- 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 +94 -0
- package/src/agent/compaction.ts +321 -0
- package/src/agent/context.ts +97 -0
- package/src/agent/guards.ts +1239 -19
- package/src/agent/ignore.ts +147 -0
- package/src/agent/policy/defaults.ts +34 -0
- package/src/agent/policy/egress.ts +335 -0
- package/src/agent/process-tree.ts +114 -0
- package/src/agent/protocol-mode.ts +59 -0
- package/src/agent/protocol.ts +191 -40
- package/src/agent/registry.ts +270 -0
- package/src/agent/runner.ts +486 -0
- package/src/agent/shell-ast.ts +227 -0
- package/src/agent/shell-session.ts +673 -0
- package/src/agent/stream.ts +143 -0
- package/src/agent/tool-schemas.ts +86 -0
- package/src/agent/tools/append_file.ts +30 -0
- package/src/agent/tools/apply_patch.ts +95 -0
- package/src/agent/tools/copy_file.ts +56 -0
- package/src/agent/tools/delete_file.ts +45 -0
- package/src/agent/tools/grep.ts +57 -0
- package/src/agent/tools/index.ts +34 -0
- package/src/agent/tools/list_files.ts +19 -0
- package/src/agent/tools/make_dir.ts +28 -0
- package/src/agent/tools/move_path.ts +83 -0
- package/src/agent/tools/paths.ts +92 -0
- package/src/agent/tools/pwd.ts +12 -0
- package/src/agent/tools/read_file.ts +84 -0
- package/src/agent/tools/read_many.ts +45 -0
- package/src/agent/tools/readers.ts +491 -0
- package/src/agent/tools/replace_in_file.ts +66 -0
- package/src/agent/tools/shell.ts +53 -0
- package/src/agent/tools/stat_path.ts +33 -0
- package/src/agent/tools/types.ts +84 -0
- package/src/agent/tools/validate.ts +104 -0
- package/src/agent/tools/write_file.ts +30 -0
- package/src/agent/tools.ts +3 -377
- package/src/app.ts +853 -170
- package/src/{cli.ts → cli-main.ts} +64 -9
- package/src/commands/about.ts +1 -1
- package/src/commands/attach.ts +16 -0
- package/src/commands/bg.ts +9 -0
- package/src/commands/cancel.ts +10 -0
- package/src/commands/clear.ts +1 -1
- package/src/commands/compact.ts +35 -0
- package/src/commands/context.ts +26 -0
- package/src/commands/edit.ts +37 -0
- package/src/commands/effort.ts +49 -0
- package/src/commands/expand.ts +8 -0
- package/src/commands/index.ts +24 -0
- package/src/commands/jobs.ts +18 -0
- package/src/commands/kill.ts +14 -0
- package/src/commands/login.ts +24 -22
- package/src/commands/logout.ts +8 -16
- package/src/commands/model.ts +10 -22
- package/src/commands/provider.ts +4 -11
- package/src/commands/skills.ts +34 -4
- package/src/commands/steer.ts +13 -0
- package/src/commands/threads.ts +28 -0
- package/src/commands/whoami.ts +6 -5
- package/src/config/store.ts +168 -77
- package/src/config/trust.ts +181 -22
- package/src/headless.ts +225 -0
- package/src/index.ts +19 -3
- package/src/lib/debug.ts +24 -0
- package/src/lib/diagnostics.ts +90 -8
- package/src/lib/highlight-subset.ts +93 -0
- package/src/lib/http.ts +66 -4
- package/src/lib/lifecycle.ts +69 -0
- package/src/lib/markdown.ts +22 -4
- package/src/lib/model-meta.ts +138 -0
- package/src/lib/native-tools.ts +32 -0
- package/src/lib/net.ts +204 -0
- package/src/patch/apply.ts +194 -0
- package/src/patch/parse.ts +131 -0
- package/src/patch/plan.ts +317 -0
- package/src/providers/anthropic.ts +13 -1
- package/src/providers/deepseek.ts +3 -2
- package/src/providers/groq.ts +2 -1
- package/src/providers/index.ts +32 -1
- package/src/providers/mistral.ts +2 -1
- package/src/providers/moonshot.ts +2 -1
- package/src/providers/openai.ts +2 -1
- package/src/providers/openaiCompatible.ts +104 -19
- package/src/providers/openrouter.ts +2 -1
- package/src/providers/qwen.ts +2 -1
- package/src/providers/spec.ts +42 -0
- package/src/providers/xai.ts +2 -1
- package/src/providers/zhipu.ts +2 -1
- package/src/session/log.ts +311 -29
- package/src/session/projection.ts +621 -0
- package/src/skills/manager.ts +140 -18
- package/src/types/highlight-shims.d.ts +14 -0
- package/src/types.ts +79 -2
- package/src/ui/Message.ts +13 -2
- package/src/ui/Picker.ts +24 -0
- package/src/ui/Prompt.ts +2 -2
- package/src/ui/Spinner.ts +11 -5
- package/src/ui/StatusBar.ts +6 -5
- package/tsconfig.json +1 -0
- /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
package/dist/app.js
CHANGED
|
@@ -11,58 +11,128 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
*/
|
|
12
12
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
13
13
|
import { Box, Static, Text, useApp, useInput } from "ink";
|
|
14
|
-
import { AGENT_SYSTEM_PROMPT,
|
|
15
|
-
import {
|
|
14
|
+
import { AGENT_SYSTEM_PROMPT, stripToolCalls, } from "./agent/protocol.js";
|
|
15
|
+
import { compactionConfigFromEnv, removeSummaryFile } from "./agent/compaction.js";
|
|
16
|
+
import { resolveToolProtocol, withToolCapability } from "./agent/protocol-mode.js";
|
|
17
|
+
import { runAgent } from "./agent/runner.js";
|
|
18
|
+
import { decisionForOption, parseApprovalChoice } from "./agent/approval.js";
|
|
19
|
+
import { nativeApproval } from "./agent/approval-native.js";
|
|
20
|
+
import { toolSchemas } from "./agent/tool-schemas.js";
|
|
21
|
+
import { setShellPoolSize as applyShellPoolSize, shutdownShells, shutdownShellsAsync } from "./agent/shell-session.js";
|
|
16
22
|
import { Banner } from "./ui/Banner.js";
|
|
17
23
|
import { Spinner } from "./ui/Spinner.js";
|
|
18
24
|
import { StatusBar } from "./ui/StatusBar.js";
|
|
19
25
|
import { MessageBubble } from "./ui/Message.js";
|
|
20
26
|
import { Prompt } from "./ui/Prompt.js";
|
|
27
|
+
import { Picker } from "./ui/Picker.js";
|
|
21
28
|
import { COMMANDS, findCommand, parseSlash } from "./commands/index.js";
|
|
22
|
-
import { saveScopedKey } from "./commands/login.js";
|
|
23
|
-
import {
|
|
24
|
-
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";
|
|
25
33
|
import { getTheme } from "./config/themes.js";
|
|
26
34
|
import { createStartupTranscript } from "./lib/startup.js";
|
|
27
35
|
import { shouldUseLiveStreamingUi, shouldUseStableTerminalUi } from "./lib/uiMode.js";
|
|
28
36
|
import { buildPluginsPrompt } from "./plugins/manager.js";
|
|
29
37
|
import { buildSkillsPrompt } from "./skills/manager.js";
|
|
30
|
-
import { createSessionLog,
|
|
38
|
+
import { createSessionLog, latestUnclosedSession, loggingEnabled, openSessionLog, staleSummaryRefs } from "./session/log.js";
|
|
39
|
+
import { resumeSession } from "./session/projection.js";
|
|
31
40
|
import { isTrusted, trustProject } from "./config/trust.js";
|
|
32
41
|
import { TrustGate } from "./ui/TrustGate.js";
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
function positiveInt(value, fallback) {
|
|
43
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
44
|
+
}
|
|
45
|
+
function streamingTail(text, maxLines = 6) {
|
|
46
|
+
const lines = String(text ?? "").split(/\r?\n/);
|
|
47
|
+
if (lines.length <= maxLines)
|
|
48
|
+
return String(text ?? "");
|
|
49
|
+
return "…\n" + lines.slice(-maxLines).join("\n");
|
|
50
|
+
}
|
|
51
|
+
function collapseToolOutput(output) {
|
|
52
|
+
const text = String(output ?? "");
|
|
53
|
+
const lines = text.split(/\r?\n/);
|
|
54
|
+
const first = (lines.find((line) => line.trim()) ?? "").trim();
|
|
55
|
+
const clipped = first.length > 160 ? first.slice(0, 157) + "..." : first;
|
|
56
|
+
const rest = Math.max(0, lines.length - 1);
|
|
57
|
+
return `${clipped}${clipped ? "\n" : ""}[${rest} more lines, ${text.length} chars]`;
|
|
58
|
+
}
|
|
59
|
+
function optionalBudget(name) {
|
|
60
|
+
const value = Number(process.env[name]);
|
|
61
|
+
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
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);
|
|
42
71
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
function unclosedNotice(session) {
|
|
75
|
+
const where = session.cwd || "(unknown directory)";
|
|
76
|
+
const files = session.activeFiles?.length ?? 0;
|
|
77
|
+
return `Last session did not close normally (${where}, ~${session.turns} turns, ${files} file${files === 1 ? "" : "s"} changed).\n` +
|
|
78
|
+
`Resume it with \`fastcode resume ${session.id}\`, or list sessions with \`fastcode sessions\`.`;
|
|
79
|
+
}
|
|
80
|
+
function streamErrorMessage(message, provider, model) {
|
|
81
|
+
if (/\b(401|403)\b/.test(message) || /invalid api key|unauthorized|authentication/i.test(message)) {
|
|
82
|
+
return `Authentication failed for ${provider.label} · ${model}. The key may be wrong or not allowed for this model.\n` +
|
|
83
|
+
`Check with \`/whoami\`, or re-save: \`/login ${provider.id} ${model} <key>\`.`;
|
|
47
84
|
}
|
|
48
|
-
|
|
85
|
+
if (/\b(402|429)\b/.test(message) || /quota|insufficient|rate limit|billing|balance/i.test(message)) {
|
|
86
|
+
return `Request blocked: ${provider.label} reports no remaining quota / rate limit for ${model}.\n` +
|
|
87
|
+
`Top up the account or switch model/provider. (${message.slice(0, 200)})`;
|
|
88
|
+
}
|
|
89
|
+
return `Error: ${message}`;
|
|
49
90
|
}
|
|
50
|
-
export const App = ({ initialPrompt, resume }) => {
|
|
91
|
+
export const App = ({ initialPrompt, resume, ui = "tty" }) => {
|
|
51
92
|
const { exit } = useApp();
|
|
52
93
|
const startupRef = useRef(null);
|
|
53
94
|
if (!startupRef.current) {
|
|
54
95
|
const config = loadConfig();
|
|
55
|
-
const
|
|
96
|
+
const resumed = resume ? resumeSession(resume) : undefined;
|
|
97
|
+
if (resumed) {
|
|
98
|
+
const provider = resumed.meta.providerId ? getProvider(resumed.meta.providerId) : undefined;
|
|
99
|
+
if (provider) {
|
|
100
|
+
config.providerId = provider.id;
|
|
101
|
+
if (resumed.meta.model)
|
|
102
|
+
config.model = resumed.meta.model;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const restored = resumed?.messages ?? [];
|
|
106
|
+
const transcript = restored.length > 0
|
|
107
|
+
? restored.map((message, index) => ({
|
|
108
|
+
id: index + 1,
|
|
109
|
+
role: message.role,
|
|
110
|
+
content: message.role === "assistant" ? stripToolCalls(message.content) : message.content,
|
|
111
|
+
markdown: message.role !== "user",
|
|
112
|
+
}))
|
|
113
|
+
: createStartupTranscript(config);
|
|
114
|
+
if (resumed) {
|
|
115
|
+
const push = (text) => transcript.push({ id: transcript.length + 1, role: "system", content: text, markdown: true });
|
|
116
|
+
if (resumed.meta.providerId && !getProvider(resumed.meta.providerId))
|
|
117
|
+
push(`The session used provider ${resumed.meta.providerId}, which is not available in this build.`);
|
|
118
|
+
push(`Resumed session: ${config.providerId}::${config.model}${resumed.rebuilt ? " (projection rebuilt)" : ""}.`);
|
|
119
|
+
for (const warning of resumed.warnings)
|
|
120
|
+
push(warning);
|
|
121
|
+
}
|
|
122
|
+
else if (!resume && loggingEnabled()) {
|
|
123
|
+
const unclosed = latestUnclosedSession();
|
|
124
|
+
if (unclosed) {
|
|
125
|
+
transcript.push({
|
|
126
|
+
id: transcript.length + 1,
|
|
127
|
+
role: "system",
|
|
128
|
+
content: unclosedNotice(unclosed),
|
|
129
|
+
markdown: true,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
56
133
|
startupRef.current = {
|
|
57
134
|
config,
|
|
58
|
-
transcript
|
|
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),
|
|
135
|
+
transcript,
|
|
66
136
|
history: restored.map((message) => ({ role: message.role, content: message.content })),
|
|
67
137
|
};
|
|
68
138
|
}
|
|
@@ -70,57 +140,153 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
70
140
|
const sessionRef = useRef(null);
|
|
71
141
|
if (!sessionRef.current) {
|
|
72
142
|
sessionRef.current = resume
|
|
73
|
-
? openSessionLog(resume
|
|
143
|
+
? openSessionLog(resume, {
|
|
144
|
+
providerId: startup.config.providerId,
|
|
145
|
+
model: startup.config.model,
|
|
146
|
+
cwd: process.cwd(),
|
|
147
|
+
})
|
|
74
148
|
: createSessionLog({
|
|
75
149
|
providerId: startup.config.providerId,
|
|
76
150
|
model: startup.config.model,
|
|
77
151
|
cwd: process.cwd(),
|
|
78
152
|
});
|
|
79
153
|
}
|
|
80
|
-
const [
|
|
154
|
+
const [, setTrustEpoch] = useState(0);
|
|
155
|
+
const trusted = isTrusted(process.cwd());
|
|
81
156
|
const [config, setConfig] = useState(startup.config);
|
|
82
157
|
const [history, setHistory] = useState(startup.history ?? []);
|
|
83
158
|
const [transcript, setTranscript] = useState(startup.transcript);
|
|
84
159
|
const [streamingId, setStreamingId] = useState(null);
|
|
85
160
|
const [streamingText, setStreamingText] = useState("");
|
|
161
|
+
const [streamPhase, setStreamPhase] = useState("thinking");
|
|
86
162
|
const [input, setInput] = useState("");
|
|
87
163
|
const [pendingLogin, setPendingLogin] = useState(null);
|
|
164
|
+
const [picker, setPicker] = useState(null);
|
|
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);
|
|
88
171
|
const [busy, setBusy] = useState(false);
|
|
172
|
+
const [expanded, setExpanded] = useState(false);
|
|
173
|
+
const steerQueueRef = useRef([]);
|
|
89
174
|
const [totalIn, setTotalIn] = useState(0);
|
|
90
175
|
const [totalOut, setTotalOut] = useState(0);
|
|
91
176
|
const [totalCost, setTotalCost] = useState(0);
|
|
92
177
|
const [agentActivity, setAgentActivity] = useState(null);
|
|
93
178
|
const idRef = useRef(startup.transcript.length);
|
|
94
179
|
const abortRef = useRef(null);
|
|
180
|
+
const sawDeltaRef = useRef(false);
|
|
181
|
+
const lastStreamFlushRef = useRef(0);
|
|
95
182
|
const ctrlCRef = useRef(0);
|
|
183
|
+
const exitConfirmRef = useRef(0);
|
|
184
|
+
const busyRef = useRef(false);
|
|
96
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]);
|
|
97
217
|
const theme = getTheme(config.theme);
|
|
98
218
|
const stableUi = useMemo(() => shouldUseStableTerminalUi(), []);
|
|
99
219
|
const liveStreamingUi = useMemo(() => shouldUseLiveStreamingUi(), []);
|
|
100
220
|
// The banner picks a font that fits the terminal width (see ui/Banner.ts)
|
|
101
221
|
// and renders nothing when even the smallest font is too wide.
|
|
102
222
|
const showBanner = transcript.length <= startup.transcript.length && !busy;
|
|
103
|
-
const promptPlaceholder =
|
|
104
|
-
?
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
? "
|
|
108
|
-
: pendingLogin.
|
|
109
|
-
? "
|
|
110
|
-
:
|
|
111
|
-
|
|
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;
|
|
112
236
|
const promptMask = pendingLogin?.step === "key" ? "*" : undefined;
|
|
113
237
|
// Persist config whenever it changes.
|
|
114
238
|
useEffect(() => {
|
|
115
239
|
saveConfig(config);
|
|
116
240
|
}, [config]);
|
|
241
|
+
const chooseItem = useCallback((request, index) => {
|
|
242
|
+
const item = request?.items?.[index];
|
|
243
|
+
setPicker(null);
|
|
244
|
+
if (item)
|
|
245
|
+
request.onSelect(item.value, item);
|
|
246
|
+
}, []);
|
|
117
247
|
// Esc cancels streaming. Double Ctrl+C quits.
|
|
118
248
|
useInput((inputChar, key) => {
|
|
119
|
-
if (
|
|
249
|
+
if (picker) {
|
|
250
|
+
if (key.escape || (key.ctrl && inputChar === "c")) {
|
|
251
|
+
const onCancel = picker.onCancel;
|
|
252
|
+
setPicker(null);
|
|
253
|
+
onCancel?.();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (picker.items.length === 0)
|
|
257
|
+
return;
|
|
258
|
+
if (key.upArrow) {
|
|
259
|
+
setPickerIndex((i) => Math.max(0, i - 1));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (key.downArrow) {
|
|
263
|
+
setPickerIndex((i) => Math.min(picker.items.length - 1, i + 1));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (key.return) {
|
|
267
|
+
chooseItem(picker, pickerIndex);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (/^[1-9]$/.test(inputChar)) {
|
|
271
|
+
chooseItem(picker, Number(inputChar) - 1);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
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
|
+
}
|
|
120
285
|
if (pendingLogin) {
|
|
286
|
+
modelSetupIdRef.current += 1;
|
|
121
287
|
setPendingLogin(null);
|
|
122
288
|
setInput("");
|
|
123
|
-
pushSystem("
|
|
289
|
+
pushSystem("Model configuration cancelled. Existing settings were not changed.");
|
|
124
290
|
return;
|
|
125
291
|
}
|
|
126
292
|
if (abortRef.current) {
|
|
@@ -131,8 +297,19 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
131
297
|
}
|
|
132
298
|
if (key.ctrl && inputChar === "c") {
|
|
133
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
|
+
}
|
|
134
311
|
if (ctrlCRef.current >= 2) {
|
|
135
|
-
|
|
312
|
+
void quitCleanly();
|
|
136
313
|
}
|
|
137
314
|
else {
|
|
138
315
|
pushSystem("Press Ctrl+C again to quit.");
|
|
@@ -144,27 +321,353 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
144
321
|
});
|
|
145
322
|
const pushSystem = useCallback((text) => {
|
|
146
323
|
idRef.current += 1;
|
|
147
|
-
sessionRef.current.append({ type: "
|
|
324
|
+
sessionRef.current.append({ type: "notice", content: text });
|
|
148
325
|
setTranscript((prev) => [
|
|
149
326
|
...prev,
|
|
150
327
|
{ id: idRef.current, role: "system", content: text, markdown: true },
|
|
151
328
|
]);
|
|
152
329
|
}, []);
|
|
153
|
-
const pushUser = useCallback((text) => {
|
|
330
|
+
const pushUser = useCallback((text, recordFact = true) => {
|
|
154
331
|
idRef.current += 1;
|
|
155
|
-
sessionRef.current.append(
|
|
332
|
+
sessionRef.current.append(recordFact
|
|
333
|
+
? { type: "user", content: text }
|
|
334
|
+
: { type: "notice", content: text });
|
|
156
335
|
setTranscript((prev) => [
|
|
157
336
|
...prev,
|
|
158
337
|
{ id: idRef.current, role: "user", content: text, markdown: false },
|
|
159
338
|
]);
|
|
160
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]);
|
|
161
659
|
const actions = useMemo(() => ({
|
|
162
660
|
setProvider: (id) => setConfig((c) => ({ ...c, providerId: id })),
|
|
163
661
|
setModel: (id) => setConfig((c) => ({ ...c, model: id })),
|
|
662
|
+
setReasoningEffort: (effort) => setConfig((c) => ({ ...c, reasoningEffort: effort })),
|
|
164
663
|
setTheme: (id) => setConfig((c) => ({ ...c, theme: id })),
|
|
165
664
|
setAgentMode: (enabled) => setConfig((c) => ({ ...c, agentMode: enabled })),
|
|
166
665
|
setSystemPrompt: (p) => setConfig((c) => ({ ...c, systemPrompt: p })),
|
|
167
666
|
setSkillsRegistry: (url) => setConfig((c) => ({ ...c, skillsRegistry: url })),
|
|
667
|
+
setShellPoolSize: (size) => {
|
|
668
|
+
const applied = applyShellPoolSize(size);
|
|
669
|
+
setConfig((c) => ({ ...c, shellPoolSize: applied }));
|
|
670
|
+
},
|
|
168
671
|
enableSkill: (name) => setConfig((c) => ({
|
|
169
672
|
...c,
|
|
170
673
|
enabledSkills: Array.from(new Set([...(c.enabledSkills ?? []), name])),
|
|
@@ -185,15 +688,29 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
185
688
|
setHistory([]);
|
|
186
689
|
setTranscript([]);
|
|
187
690
|
},
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
});
|
|
691
|
+
replaceHistory: (messages) => setHistory(messages),
|
|
692
|
+
cancelRun: () => {
|
|
693
|
+
if (abortRef.current) {
|
|
694
|
+
abortRef.current.abort();
|
|
695
|
+
abortRef.current = null;
|
|
696
|
+
}
|
|
195
697
|
},
|
|
196
|
-
|
|
698
|
+
steer: (text) => {
|
|
699
|
+
steerQueueRef.current.push(text);
|
|
700
|
+
},
|
|
701
|
+
setExpanded: (value) => setExpanded(value),
|
|
702
|
+
setInput: (value) => setInput(value),
|
|
703
|
+
pushSystemMessage: pushSystem,
|
|
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
|
+
}, []);
|
|
197
714
|
const runSlash = useCallback(async (raw) => {
|
|
198
715
|
const parsed = parseSlash(raw);
|
|
199
716
|
if (!parsed)
|
|
@@ -203,10 +720,24 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
203
720
|
pushSystem(`Unknown command "/${parsed.name}". Try /help.`);
|
|
204
721
|
return true;
|
|
205
722
|
}
|
|
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)) {
|
|
734
|
+
pushSystem(`/${cmd.name} is unavailable while a run is active. Try /cancel or /steer.`);
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
206
737
|
// Echo the slash invocation for transcript clarity, but never echo a
|
|
207
738
|
// secret: the login key is masked, while provider/model stay readable.
|
|
208
739
|
const echo = parsed.name === "login" ? maskLoginEcho(parsed.args) : raw;
|
|
209
|
-
pushUser(echo);
|
|
740
|
+
pushUser(echo, false);
|
|
210
741
|
try {
|
|
211
742
|
const res = await cmd.run({
|
|
212
743
|
args: parsed.args,
|
|
@@ -224,20 +755,37 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
224
755
|
enabledSkills: config.enabledSkills,
|
|
225
756
|
enabledPlugins: config.enabledPlugins,
|
|
226
757
|
skillsRegistry: config.skillsRegistry,
|
|
758
|
+
reasoningEffort: config.reasoningEffort,
|
|
759
|
+
expanded,
|
|
760
|
+
busy,
|
|
761
|
+
draft: input,
|
|
227
762
|
},
|
|
228
763
|
actions,
|
|
229
764
|
});
|
|
230
765
|
if (res?.message)
|
|
231
766
|
pushSystem(res.message);
|
|
767
|
+
if (res?.picker) {
|
|
768
|
+
const index = Math.max(0, res.picker.items.findIndex((item) => item.value === res.picker.current));
|
|
769
|
+
setPickerIndex(index);
|
|
770
|
+
setPicker(res.picker);
|
|
771
|
+
setInput("");
|
|
772
|
+
}
|
|
773
|
+
if (res?.compacted)
|
|
774
|
+
appendCompactionFact(res.compacted);
|
|
775
|
+
if (res?.cleared)
|
|
776
|
+
sessionRef.current.append({ type: "cleared" });
|
|
232
777
|
if (res?.quit)
|
|
233
|
-
|
|
778
|
+
void quitCleanly();
|
|
234
779
|
}
|
|
235
780
|
catch (err) {
|
|
236
|
-
pushSystem(
|
|
781
|
+
pushSystem(parsed.name === "login"
|
|
782
|
+
? "Login failed. The credential was not included in this error message."
|
|
783
|
+
: `Command failed: ${err.message}`);
|
|
237
784
|
}
|
|
238
785
|
return true;
|
|
239
786
|
}, [
|
|
240
787
|
actions,
|
|
788
|
+
appendCompactionFact,
|
|
241
789
|
config.agentMode,
|
|
242
790
|
config.enabledPlugins,
|
|
243
791
|
config.enabledSkills,
|
|
@@ -246,7 +794,9 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
246
794
|
config.skillsRegistry,
|
|
247
795
|
config.systemPrompt,
|
|
248
796
|
config.theme,
|
|
249
|
-
|
|
797
|
+
busy,
|
|
798
|
+
expanded,
|
|
799
|
+
quitCleanly,
|
|
250
800
|
history,
|
|
251
801
|
pushSystem,
|
|
252
802
|
pushUser,
|
|
@@ -256,8 +806,26 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
256
806
|
]);
|
|
257
807
|
const handleSubmit = useCallback(async (raw) => {
|
|
258
808
|
const text = raw.trim();
|
|
259
|
-
if (!text
|
|
809
|
+
if (!text)
|
|
810
|
+
return;
|
|
811
|
+
if (approvalFeedbackRef.current) {
|
|
812
|
+
approvalFeedbackRef.current.submit(text);
|
|
813
|
+
setInput("");
|
|
260
814
|
return;
|
|
815
|
+
}
|
|
816
|
+
if (plainApprovalRef.current) {
|
|
817
|
+
plainApprovalRef.current.submit(text);
|
|
818
|
+
setInput("");
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (busy) {
|
|
822
|
+
if (text.startsWith("/")) {
|
|
823
|
+
await runSlash(text);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
pushSystem("A run is active. Use /cancel to stop, /steer <text> to guide it, or wait.");
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
261
829
|
setInput("");
|
|
262
830
|
if (text.startsWith("/")) {
|
|
263
831
|
await runSlash(text);
|
|
@@ -274,114 +842,222 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
274
842
|
return;
|
|
275
843
|
}
|
|
276
844
|
if (lookup.status === "missing") {
|
|
277
|
-
pushSystem(`Cannot send: no API key for ${provider.label}
|
|
278
|
-
`Run \`/login ${provider.id} ${config.model} <key>\`
|
|
845
|
+
pushSystem(`Cannot send: no API key for ${provider.label}.\n` +
|
|
846
|
+
`Run \`/login ${provider.id} ${config.model} <key>\` (one key covers every ${provider.id} model), or set ${provider.authEnv[0]}.`);
|
|
279
847
|
return;
|
|
280
848
|
}
|
|
281
849
|
apiKey = lookup.key;
|
|
282
850
|
}
|
|
283
|
-
const nextHistory = [...history, { role: "user", content: text }];
|
|
284
|
-
setHistory(nextHistory);
|
|
285
851
|
idRef.current += 1;
|
|
286
852
|
const bubbleId = idRef.current;
|
|
287
853
|
setStreamingId(bubbleId);
|
|
288
854
|
setStreamingText("");
|
|
855
|
+
setStreamPhase("thinking");
|
|
856
|
+
steerQueueRef.current = [];
|
|
857
|
+
lastStreamFlushRef.current = 0;
|
|
289
858
|
const ctrl = new AbortController();
|
|
290
859
|
abortRef.current = ctrl;
|
|
860
|
+
busyRef.current = true;
|
|
291
861
|
setBusy(true);
|
|
862
|
+
let receivedDone = false;
|
|
292
863
|
try {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
864
|
+
const maxTurns = config.agentMode ? positiveInt(config.maxAgentTurns, 24) : 1;
|
|
865
|
+
const skillsBlock = buildSkillsPrompt(config.enabledSkills);
|
|
866
|
+
const pluginsBlock = buildPluginsPrompt(config.enabledPlugins);
|
|
867
|
+
const agentBlock = config.agentMode ? AGENT_SYSTEM_PROMPT : "";
|
|
868
|
+
const fullSystem = [config.systemPrompt, skillsBlock, pluginsBlock, agentBlock]
|
|
869
|
+
.filter((part) => part.trim())
|
|
870
|
+
.join("\n\n");
|
|
871
|
+
let compactionNotified = false;
|
|
872
|
+
let lastRawFact = "";
|
|
873
|
+
sawDeltaRef.current = false;
|
|
874
|
+
const protocol = config.agentMode ? resolveToolProtocol(config, config.providerId, config.model) : "legacy";
|
|
875
|
+
for await (const ev of runAgent({
|
|
876
|
+
provider,
|
|
877
|
+
apiKey,
|
|
878
|
+
model: config.model,
|
|
879
|
+
messages: history,
|
|
880
|
+
prompt: text,
|
|
881
|
+
system: fullSystem,
|
|
882
|
+
maxTurns,
|
|
883
|
+
allowTools: config.agentMode,
|
|
884
|
+
tokenBudget: optionalBudget("FASTCODE_TOKEN_BUDGET"),
|
|
885
|
+
costBudgetUsd: optionalBudget("FASTCODE_COST_BUDGET_USD"),
|
|
886
|
+
signal: ctrl.signal,
|
|
887
|
+
cwd: process.cwd(),
|
|
888
|
+
idleTimeoutMs: config.requestIdleTimeoutMs,
|
|
889
|
+
compaction: compactionConfigFromEnv(),
|
|
890
|
+
reasoningEffort: config.reasoningEffort,
|
|
891
|
+
protocol,
|
|
892
|
+
tools: protocol === "native" ? toolSchemas() : undefined,
|
|
893
|
+
onProtocolFallback: (message) => {
|
|
894
|
+
setConfig((current) => withToolCapability(current, config.providerId, config.model, "pseudo"));
|
|
895
|
+
pushSystem(message);
|
|
896
|
+
},
|
|
897
|
+
onNativeOk: () => {
|
|
898
|
+
if (protocol === "native")
|
|
899
|
+
setConfig((current) => withToolCapability(current, config.providerId, config.model, "native"));
|
|
900
|
+
},
|
|
901
|
+
takeSteering: () => {
|
|
902
|
+
const queued = steerQueueRef.current;
|
|
903
|
+
steerQueueRef.current = [];
|
|
904
|
+
return queued;
|
|
905
|
+
},
|
|
906
|
+
requestApproval,
|
|
907
|
+
onDelta: (delta, full) => {
|
|
908
|
+
if (!sawDeltaRef.current) {
|
|
909
|
+
sawDeltaRef.current = true;
|
|
910
|
+
setStreamPhase("streaming");
|
|
313
911
|
}
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
912
|
+
if (!liveStreamingUi)
|
|
913
|
+
return;
|
|
914
|
+
const now = Date.now();
|
|
915
|
+
if (now - lastStreamFlushRef.current < 60)
|
|
916
|
+
return;
|
|
917
|
+
lastStreamFlushRef.current = now;
|
|
918
|
+
setStreamingText(stripToolCalls(full));
|
|
919
|
+
},
|
|
920
|
+
})) {
|
|
921
|
+
switch (ev.type) {
|
|
922
|
+
case "compaction":
|
|
923
|
+
appendCompactionFact({
|
|
924
|
+
summaryRef: ev.summaryFile,
|
|
925
|
+
source: ev.source,
|
|
926
|
+
beforeTokens: ev.beforeTokens,
|
|
927
|
+
afterTokens: ev.afterTokens,
|
|
928
|
+
droppedMessages: ev.droppedMessages,
|
|
929
|
+
keepMessages: ev.keptMessages,
|
|
930
|
+
reason: ev.reason === "heap" ? "heap" : "threshold",
|
|
931
|
+
});
|
|
932
|
+
if (!compactionNotified) {
|
|
933
|
+
compactionNotified = true;
|
|
934
|
+
pushSystem(`[context compacted] ${ev.beforeTokens} → ${ev.afterTokens} tokens (${ev.reason}); recent turns kept.` +
|
|
935
|
+
(ev.summaryFile ? `\nSummary: ${ev.summaryFile}` : ""));
|
|
936
|
+
}
|
|
937
|
+
break;
|
|
938
|
+
case "assistant_raw":
|
|
939
|
+
lastRawFact = ev.text;
|
|
940
|
+
sessionRef.current.append({ type: "assistant_raw", content: ev.text, toolCalls: ev.toolCalls });
|
|
941
|
+
break;
|
|
942
|
+
case "assistant_text":
|
|
943
|
+
pushSystem(ev.text);
|
|
944
|
+
break;
|
|
945
|
+
case "tool_call":
|
|
946
|
+
sessionRef.current.append({ type: "tool_call", id: ev.id, tool: ev.call.tool, args: ev.call, native: Boolean(ev.native) });
|
|
947
|
+
setStreamPhase("tool");
|
|
948
|
+
setAgentActivity(`Coding: ${ev.call.tool}`);
|
|
949
|
+
pushSystem(`Running tool: \`${ev.call.tool}\`\n\n\`\`\`json\n${JSON.stringify(ev.call, null, 2)}\n\`\`\``);
|
|
950
|
+
break;
|
|
951
|
+
case "tool_result": {
|
|
952
|
+
sessionRef.current.append({
|
|
953
|
+
type: "tool_result",
|
|
954
|
+
id: ev.id,
|
|
955
|
+
tool: ev.call.tool,
|
|
956
|
+
ok: ev.ok,
|
|
957
|
+
output: ev.output,
|
|
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 } : {}),
|
|
969
|
+
});
|
|
970
|
+
const collapsed = stableUi && !expanded;
|
|
971
|
+
const body = collapsed ? collapseToolOutput(ev.output) : ev.output;
|
|
972
|
+
const hint = collapsed ? "\n\n_(output collapsed; /expand to show full output)_" : "";
|
|
973
|
+
pushSystem(`Tool ${ev.ok ? "completed" : "failed"}: \`${ev.call.tool}\`\n\n\`\`\`\n${body}\n\`\`\`${hint}`);
|
|
974
|
+
break;
|
|
321
975
|
}
|
|
976
|
+
case "usage":
|
|
977
|
+
if (ev.usage.inputTokens)
|
|
978
|
+
setTotalIn((n) => n + (ev.usage.inputTokens ?? 0));
|
|
979
|
+
if (ev.usage.outputTokens)
|
|
980
|
+
setTotalOut((n) => n + (ev.usage.outputTokens ?? 0));
|
|
981
|
+
if (ev.usage.costUsd)
|
|
982
|
+
setTotalCost((n) => n + (ev.usage.costUsd ?? 0));
|
|
983
|
+
break;
|
|
984
|
+
case "loop_detected":
|
|
985
|
+
pushSystem(`Loop detected: \`${ev.fingerprint.split(":")[0]}\` repeated ${ev.occurrences} times. Asking the model to change approach.`);
|
|
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
|
+
}
|
|
1006
|
+
case "notice":
|
|
1007
|
+
pushSystem(ev.text);
|
|
1008
|
+
break;
|
|
1009
|
+
case "checkpoint":
|
|
1010
|
+
sessionRef.current.append({
|
|
1011
|
+
type: "checkpoint",
|
|
1012
|
+
turn: ev.turn,
|
|
1013
|
+
cwd: ev.cwd,
|
|
1014
|
+
providerId: ev.providerId,
|
|
1015
|
+
model: ev.model,
|
|
1016
|
+
activeFiles: ev.activeFiles,
|
|
1017
|
+
lastFactSeq: sessionRef.current.count(),
|
|
1018
|
+
});
|
|
1019
|
+
break;
|
|
1020
|
+
case "error":
|
|
1021
|
+
if (ev.kind === "aborted")
|
|
1022
|
+
pushSystem("Cancelled current run; partial output kept. Type 继续 to continue.");
|
|
1023
|
+
else if (ev.kind === "stream_error")
|
|
1024
|
+
pushSystem(streamErrorMessage(ev.message, provider, config.model));
|
|
1025
|
+
else
|
|
1026
|
+
pushSystem(ev.message);
|
|
1027
|
+
break;
|
|
1028
|
+
case "done":
|
|
1029
|
+
receivedDone = true;
|
|
1030
|
+
setHistory(ev.history);
|
|
1031
|
+
if (ev.finalText) {
|
|
1032
|
+
idRef.current += 1;
|
|
1033
|
+
const finalId = idRef.current;
|
|
1034
|
+
if (ev.finalText !== lastRawFact)
|
|
1035
|
+
sessionRef.current.append({ type: "assistant_raw", content: ev.finalText });
|
|
1036
|
+
setTranscript((prev) => [
|
|
1037
|
+
...prev,
|
|
1038
|
+
{ id: finalId, role: "assistant", content: ev.finalText, markdown: true },
|
|
1039
|
+
]);
|
|
1040
|
+
}
|
|
1041
|
+
break;
|
|
322
1042
|
}
|
|
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 = [];
|
|
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 = 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
1043
|
}
|
|
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
1044
|
}
|
|
360
1045
|
catch (err) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
}
|
|
1046
|
+
if (!receivedDone)
|
|
1047
|
+
setHistory([...history, { role: "user", content: text }]);
|
|
1048
|
+
pushSystem(`Error: ${String(err?.message ?? err)}`);
|
|
376
1049
|
}
|
|
377
1050
|
finally {
|
|
378
1051
|
setStreamingId(null);
|
|
379
1052
|
setStreamingText("");
|
|
380
1053
|
setAgentActivity(null);
|
|
1054
|
+
setStreamPhase("thinking");
|
|
1055
|
+
busyRef.current = false;
|
|
381
1056
|
setBusy(false);
|
|
382
1057
|
abortRef.current = null;
|
|
383
1058
|
}
|
|
384
1059
|
}, [
|
|
1060
|
+
appendCompactionFact,
|
|
385
1061
|
busy,
|
|
386
1062
|
config.agentMode,
|
|
387
1063
|
config.enabledSkills,
|
|
@@ -392,6 +1068,7 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
392
1068
|
history,
|
|
393
1069
|
pushSystem,
|
|
394
1070
|
pushUser,
|
|
1071
|
+
requestApproval,
|
|
395
1072
|
runSlash,
|
|
396
1073
|
]);
|
|
397
1074
|
// Guided `/login`: provider → model → key. Text prompts only; the key step
|
|
@@ -399,55 +1076,50 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
399
1076
|
const handleWizardSubmit = useCallback(async (raw) => {
|
|
400
1077
|
const value = raw.trim();
|
|
401
1078
|
setInput("");
|
|
402
|
-
if (!
|
|
1079
|
+
if (!pendingLogin)
|
|
403
1080
|
return;
|
|
404
|
-
if (pendingLogin.step === "
|
|
405
|
-
|
|
406
|
-
|
|
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);
|
|
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.");
|
|
413
1084
|
return;
|
|
414
1085
|
}
|
|
415
|
-
|
|
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}` : ""));
|
|
1086
|
+
continueModelSetup(pendingLogin, pendingLogin.provider, value);
|
|
421
1087
|
return;
|
|
422
1088
|
}
|
|
423
|
-
if (pendingLogin.step
|
|
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.` : ""));
|
|
1089
|
+
if (pendingLogin.step !== "key")
|
|
432
1090
|
return;
|
|
433
|
-
}
|
|
434
1091
|
const provider = getProvider(pendingLogin.provider);
|
|
435
1092
|
if (!provider) {
|
|
436
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
|
+
}
|
|
437
1104
|
return;
|
|
438
1105
|
}
|
|
439
1106
|
let key = value;
|
|
440
1107
|
if (value.toLowerCase() === "keep") {
|
|
441
1108
|
const legacy = getLegacy(provider.id);
|
|
442
1109
|
if (!legacy) {
|
|
443
|
-
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.`);
|
|
444
1111
|
return;
|
|
445
1112
|
}
|
|
446
1113
|
key = legacy;
|
|
447
1114
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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]);
|
|
451
1123
|
useEffect(() => {
|
|
452
1124
|
if (!initialPrompt || initialPromptSubmittedRef.current)
|
|
453
1125
|
return;
|
|
@@ -456,6 +1128,7 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
456
1128
|
return () => clearTimeout(t);
|
|
457
1129
|
}, [handleSubmit, initialPrompt]);
|
|
458
1130
|
useEffect(() => () => {
|
|
1131
|
+
shutdownShells();
|
|
459
1132
|
if (sessionRef.current)
|
|
460
1133
|
sessionRef.current.close();
|
|
461
1134
|
}, []);
|
|
@@ -468,9 +1141,9 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
468
1141
|
setConfig((c) => ({ ...c, agentMode: false }));
|
|
469
1142
|
pushSystem("Trust declined: agent tools are off for this session (chat only).");
|
|
470
1143
|
}
|
|
471
|
-
|
|
1144
|
+
setTrustEpoch((n) => n + 1);
|
|
472
1145
|
};
|
|
473
1146
|
return _jsx(TrustGate, { theme: theme, cwd: process.cwd(), onDecision: decide });
|
|
474
1147
|
}
|
|
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:
|
|
1148
|
+
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: streamingTail(streamingText, 6), markdown: false, streaming: true }) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [busy ? (_jsx(Box, { marginBottom: 1, children: _jsx(Spinner, { theme: theme, label: agentActivity ?? (streamPhase === "streaming" ? "Streaming" : streamPhase === "tool" ? "Waiting for tool" : "Thinking"), stable: stableUi }) })) : null, picker ? _jsx(Picker, { theme: theme, title: picker.title, items: picker.items, index: pickerIndex, current: picker.current }) : _jsx(Prompt, { theme: theme, value: input, onChange: setInput, onSubmit: pendingLogin ? handleWizardSubmit : handleSubmit, disabled: false, 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, reasoningEffort: config.reasoningEffort }), _jsxs(Text, { color: theme.muted, children: [COMMANDS.length, " commands \u00B7 ", config.enabledSkills.length, " skill(s) active"] })] })] }));
|
|
476
1149
|
};
|