@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/src/app.ts
CHANGED
|
@@ -11,58 +11,129 @@ 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, type ApprovalDecision, type ApprovalRequest } 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
|
+
import type { CommandCompaction } from "./types.js";
|
|
43
|
+
function positiveInt(value, fallback) {
|
|
44
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
45
|
+
}
|
|
46
|
+
function streamingTail(text, maxLines = 6) {
|
|
47
|
+
const lines = String(text ?? "").split(/\r?\n/);
|
|
48
|
+
if (lines.length <= maxLines)
|
|
49
|
+
return String(text ?? "");
|
|
50
|
+
return "…\n" + lines.slice(-maxLines).join("\n");
|
|
51
|
+
}
|
|
52
|
+
function collapseToolOutput(output) {
|
|
53
|
+
const text = String(output ?? "");
|
|
54
|
+
const lines = text.split(/\r?\n/);
|
|
55
|
+
const first = (lines.find((line) => line.trim()) ?? "").trim();
|
|
56
|
+
const clipped = first.length > 160 ? first.slice(0, 157) + "..." : first;
|
|
57
|
+
const rest = Math.max(0, lines.length - 1);
|
|
58
|
+
return `${clipped}${clipped ? "\n" : ""}[${rest} more lines, ${text.length} chars]`;
|
|
59
|
+
}
|
|
60
|
+
function optionalBudget(name) {
|
|
61
|
+
const value = Number(process.env[name]);
|
|
62
|
+
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
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);
|
|
42
72
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
function unclosedNotice(session) {
|
|
76
|
+
const where = session.cwd || "(unknown directory)";
|
|
77
|
+
const files = session.activeFiles?.length ?? 0;
|
|
78
|
+
return `Last session did not close normally (${where}, ~${session.turns} turns, ${files} file${files === 1 ? "" : "s"} changed).\n` +
|
|
79
|
+
`Resume it with \`fastcode resume ${session.id}\`, or list sessions with \`fastcode sessions\`.`;
|
|
80
|
+
}
|
|
81
|
+
function streamErrorMessage(message, provider, model) {
|
|
82
|
+
if (/\b(401|403)\b/.test(message) || /invalid api key|unauthorized|authentication/i.test(message)) {
|
|
83
|
+
return `Authentication failed for ${provider.label} · ${model}. The key may be wrong or not allowed for this model.\n` +
|
|
84
|
+
`Check with \`/whoami\`, or re-save: \`/login ${provider.id} ${model} <key>\`.`;
|
|
47
85
|
}
|
|
48
|
-
|
|
86
|
+
if (/\b(402|429)\b/.test(message) || /quota|insufficient|rate limit|billing|balance/i.test(message)) {
|
|
87
|
+
return `Request blocked: ${provider.label} reports no remaining quota / rate limit for ${model}.\n` +
|
|
88
|
+
`Top up the account or switch model/provider. (${message.slice(0, 200)})`;
|
|
89
|
+
}
|
|
90
|
+
return `Error: ${message}`;
|
|
49
91
|
}
|
|
50
|
-
export const App = ({ initialPrompt, resume }) => {
|
|
92
|
+
export const App = ({ initialPrompt, resume, ui = "tty" }) => {
|
|
51
93
|
const { exit } = useApp();
|
|
52
94
|
const startupRef = useRef(null);
|
|
53
95
|
if (!startupRef.current) {
|
|
54
96
|
const config = loadConfig();
|
|
55
|
-
const
|
|
97
|
+
const resumed = resume ? resumeSession(resume) : undefined;
|
|
98
|
+
if (resumed) {
|
|
99
|
+
const provider = resumed.meta.providerId ? getProvider(resumed.meta.providerId) : undefined;
|
|
100
|
+
if (provider) {
|
|
101
|
+
config.providerId = provider.id;
|
|
102
|
+
if (resumed.meta.model)
|
|
103
|
+
config.model = resumed.meta.model;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const restored = resumed?.messages ?? [];
|
|
107
|
+
const transcript = restored.length > 0
|
|
108
|
+
? restored.map((message, index) => ({
|
|
109
|
+
id: index + 1,
|
|
110
|
+
role: message.role,
|
|
111
|
+
content: message.role === "assistant" ? stripToolCalls(message.content) : message.content,
|
|
112
|
+
markdown: message.role !== "user",
|
|
113
|
+
}))
|
|
114
|
+
: createStartupTranscript(config);
|
|
115
|
+
if (resumed) {
|
|
116
|
+
const push = (text) => transcript.push({ id: transcript.length + 1, role: "system", content: text, markdown: true });
|
|
117
|
+
if (resumed.meta.providerId && !getProvider(resumed.meta.providerId))
|
|
118
|
+
push(`The session used provider ${resumed.meta.providerId}, which is not available in this build.`);
|
|
119
|
+
push(`Resumed session: ${config.providerId}::${config.model}${resumed.rebuilt ? " (projection rebuilt)" : ""}.`);
|
|
120
|
+
for (const warning of resumed.warnings)
|
|
121
|
+
push(warning);
|
|
122
|
+
}
|
|
123
|
+
else if (!resume && loggingEnabled()) {
|
|
124
|
+
const unclosed = latestUnclosedSession();
|
|
125
|
+
if (unclosed) {
|
|
126
|
+
transcript.push({
|
|
127
|
+
id: transcript.length + 1,
|
|
128
|
+
role: "system",
|
|
129
|
+
content: unclosedNotice(unclosed),
|
|
130
|
+
markdown: true,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
56
134
|
startupRef.current = {
|
|
57
135
|
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),
|
|
136
|
+
transcript,
|
|
66
137
|
history: restored.map((message) => ({ role: message.role, content: message.content })),
|
|
67
138
|
};
|
|
68
139
|
}
|
|
@@ -70,43 +141,104 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
70
141
|
const sessionRef = useRef(null as any);
|
|
71
142
|
if (!sessionRef.current) {
|
|
72
143
|
sessionRef.current = resume
|
|
73
|
-
? openSessionLog(resume
|
|
144
|
+
? openSessionLog(resume, {
|
|
145
|
+
providerId: startup.config.providerId,
|
|
146
|
+
model: startup.config.model,
|
|
147
|
+
cwd: process.cwd(),
|
|
148
|
+
})
|
|
74
149
|
: createSessionLog({
|
|
75
150
|
providerId: startup.config.providerId,
|
|
76
151
|
model: startup.config.model,
|
|
77
152
|
cwd: process.cwd(),
|
|
78
153
|
});
|
|
79
154
|
}
|
|
80
|
-
const [
|
|
155
|
+
const [, setTrustEpoch] = useState(0);
|
|
156
|
+
const trusted = isTrusted(process.cwd());
|
|
81
157
|
const [config, setConfig] = useState(startup.config);
|
|
82
158
|
const [history, setHistory] = useState(startup.history ?? []);
|
|
83
159
|
const [transcript, setTranscript] = useState(startup.transcript);
|
|
84
160
|
const [streamingId, setStreamingId] = useState(null);
|
|
85
161
|
const [streamingText, setStreamingText] = useState("");
|
|
162
|
+
const [streamPhase, setStreamPhase] = useState("thinking");
|
|
86
163
|
const [input, setInput] = useState("");
|
|
87
164
|
const [pendingLogin, setPendingLogin] = useState(null);
|
|
165
|
+
const [picker, setPicker] = useState(null);
|
|
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);
|
|
88
179
|
const [busy, setBusy] = useState(false);
|
|
180
|
+
const [expanded, setExpanded] = useState(false);
|
|
181
|
+
const steerQueueRef = useRef([]);
|
|
89
182
|
const [totalIn, setTotalIn] = useState(0);
|
|
90
183
|
const [totalOut, setTotalOut] = useState(0);
|
|
91
184
|
const [totalCost, setTotalCost] = useState(0);
|
|
92
185
|
const [agentActivity, setAgentActivity] = useState(null);
|
|
93
186
|
const idRef = useRef(startup.transcript.length);
|
|
94
187
|
const abortRef = useRef(null);
|
|
188
|
+
const sawDeltaRef = useRef(false);
|
|
189
|
+
const lastStreamFlushRef = useRef(0);
|
|
95
190
|
const ctrlCRef = useRef(0);
|
|
191
|
+
const exitConfirmRef = useRef(0);
|
|
192
|
+
const busyRef = useRef(false);
|
|
96
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]);
|
|
97
225
|
const theme = getTheme(config.theme);
|
|
98
226
|
const stableUi = useMemo(() => shouldUseStableTerminalUi(), []);
|
|
99
227
|
const liveStreamingUi = useMemo(() => shouldUseLiveStreamingUi(), []);
|
|
100
228
|
// The banner picks a font that fits the terminal width (see ui/Banner.ts)
|
|
101
229
|
// and renders nothing when even the smallest font is too wide.
|
|
102
230
|
const showBanner = transcript.length <= startup.transcript.length && !busy;
|
|
103
|
-
const promptPlaceholder =
|
|
104
|
-
?
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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"
|
|
108
240
|
: pendingLogin.provider && getLegacy(pendingLogin.provider)
|
|
109
|
-
? "
|
|
241
|
+
? "API key (masked), or type 'keep' · Esc to cancel"
|
|
110
242
|
: "Paste API key (masked) · Esc to cancel"
|
|
111
243
|
: undefined;
|
|
112
244
|
const promptMask = pendingLogin?.step === "key" ? "*" : undefined;
|
|
@@ -114,13 +246,55 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
114
246
|
useEffect(() => {
|
|
115
247
|
saveConfig(config);
|
|
116
248
|
}, [config]);
|
|
249
|
+
const chooseItem = useCallback((request, index) => {
|
|
250
|
+
const item = request?.items?.[index];
|
|
251
|
+
setPicker(null);
|
|
252
|
+
if (item)
|
|
253
|
+
request.onSelect(item.value, item);
|
|
254
|
+
}, []);
|
|
117
255
|
// Esc cancels streaming. Double Ctrl+C quits.
|
|
118
256
|
useInput((inputChar, key) => {
|
|
119
|
-
if (
|
|
257
|
+
if (picker) {
|
|
258
|
+
if (key.escape || (key.ctrl && inputChar === "c")) {
|
|
259
|
+
const onCancel = picker.onCancel;
|
|
260
|
+
setPicker(null);
|
|
261
|
+
onCancel?.();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (picker.items.length === 0)
|
|
265
|
+
return;
|
|
266
|
+
if (key.upArrow) {
|
|
267
|
+
setPickerIndex((i) => Math.max(0, i - 1));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (key.downArrow) {
|
|
271
|
+
setPickerIndex((i) => Math.min(picker.items.length - 1, i + 1));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (key.return) {
|
|
275
|
+
chooseItem(picker, pickerIndex);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (/^[1-9]$/.test(inputChar)) {
|
|
279
|
+
chooseItem(picker, Number(inputChar) - 1);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
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
|
+
}
|
|
120
293
|
if (pendingLogin) {
|
|
294
|
+
modelSetupIdRef.current += 1;
|
|
121
295
|
setPendingLogin(null);
|
|
122
296
|
setInput("");
|
|
123
|
-
pushSystem("
|
|
297
|
+
pushSystem("Model configuration cancelled. Existing settings were not changed.");
|
|
124
298
|
return;
|
|
125
299
|
}
|
|
126
300
|
if (abortRef.current) {
|
|
@@ -131,8 +305,19 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
131
305
|
}
|
|
132
306
|
if (key.ctrl && inputChar === "c") {
|
|
133
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
|
+
}
|
|
134
319
|
if (ctrlCRef.current >= 2) {
|
|
135
|
-
|
|
320
|
+
void quitCleanly();
|
|
136
321
|
}
|
|
137
322
|
else {
|
|
138
323
|
pushSystem("Press Ctrl+C again to quit.");
|
|
@@ -144,27 +329,355 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
144
329
|
});
|
|
145
330
|
const pushSystem = useCallback((text) => {
|
|
146
331
|
idRef.current += 1;
|
|
147
|
-
sessionRef.current.append({ type: "
|
|
332
|
+
sessionRef.current.append({ type: "notice", content: text });
|
|
148
333
|
setTranscript((prev) => [
|
|
149
334
|
...prev,
|
|
150
335
|
{ id: idRef.current, role: "system", content: text, markdown: true },
|
|
151
336
|
]);
|
|
152
337
|
}, []);
|
|
153
|
-
const pushUser = useCallback((text) => {
|
|
338
|
+
const pushUser = useCallback((text, recordFact = true) => {
|
|
154
339
|
idRef.current += 1;
|
|
155
|
-
sessionRef.current.append(
|
|
340
|
+
sessionRef.current.append(recordFact
|
|
341
|
+
? { type: "user", content: text }
|
|
342
|
+
: { type: "notice", content: text });
|
|
156
343
|
setTranscript((prev) => [
|
|
157
344
|
...prev,
|
|
158
345
|
{ id: idRef.current, role: "user", content: text, markdown: false },
|
|
159
346
|
]);
|
|
160
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
|
+
|
|
161
669
|
const actions = useMemo(() => ({
|
|
162
670
|
setProvider: (id) => setConfig((c) => ({ ...c, providerId: id })),
|
|
163
671
|
setModel: (id) => setConfig((c) => ({ ...c, model: id })),
|
|
672
|
+
setReasoningEffort: (effort) => setConfig((c) => ({ ...c, reasoningEffort: effort })),
|
|
164
673
|
setTheme: (id) => setConfig((c) => ({ ...c, theme: id })),
|
|
165
674
|
setAgentMode: (enabled) => setConfig((c) => ({ ...c, agentMode: enabled })),
|
|
166
675
|
setSystemPrompt: (p) => setConfig((c) => ({ ...c, systemPrompt: p })),
|
|
167
676
|
setSkillsRegistry: (url) => setConfig((c) => ({ ...c, skillsRegistry: url })),
|
|
677
|
+
setShellPoolSize: (size) => {
|
|
678
|
+
const applied = applyShellPoolSize(size);
|
|
679
|
+
setConfig((c) => ({ ...c, shellPoolSize: applied }));
|
|
680
|
+
},
|
|
168
681
|
enableSkill: (name) => setConfig((c) => ({
|
|
169
682
|
...c,
|
|
170
683
|
enabledSkills: Array.from(new Set([...(c.enabledSkills ?? []), name])),
|
|
@@ -185,15 +698,29 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
185
698
|
setHistory([]);
|
|
186
699
|
setTranscript([]);
|
|
187
700
|
},
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
});
|
|
701
|
+
replaceHistory: (messages) => setHistory(messages),
|
|
702
|
+
cancelRun: () => {
|
|
703
|
+
if (abortRef.current) {
|
|
704
|
+
abortRef.current.abort();
|
|
705
|
+
abortRef.current = null;
|
|
706
|
+
}
|
|
195
707
|
},
|
|
196
|
-
|
|
708
|
+
steer: (text) => {
|
|
709
|
+
steerQueueRef.current.push(text);
|
|
710
|
+
},
|
|
711
|
+
setExpanded: (value) => setExpanded(value),
|
|
712
|
+
setInput: (value) => setInput(value),
|
|
713
|
+
pushSystemMessage: pushSystem,
|
|
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
|
+
}, []);
|
|
197
724
|
const runSlash = useCallback(async (raw) => {
|
|
198
725
|
const parsed = parseSlash(raw);
|
|
199
726
|
if (!parsed)
|
|
@@ -203,10 +730,24 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
203
730
|
pushSystem(`Unknown command "/${parsed.name}". Try /help.`);
|
|
204
731
|
return true;
|
|
205
732
|
}
|
|
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)) {
|
|
744
|
+
pushSystem(`/${cmd.name} is unavailable while a run is active. Try /cancel or /steer.`);
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
206
747
|
// Echo the slash invocation for transcript clarity, but never echo a
|
|
207
748
|
// secret: the login key is masked, while provider/model stay readable.
|
|
208
749
|
const echo = parsed.name === "login" ? maskLoginEcho(parsed.args) : raw;
|
|
209
|
-
pushUser(echo);
|
|
750
|
+
pushUser(echo, false);
|
|
210
751
|
try {
|
|
211
752
|
const res = await cmd.run({
|
|
212
753
|
args: parsed.args,
|
|
@@ -224,20 +765,37 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
224
765
|
enabledSkills: config.enabledSkills,
|
|
225
766
|
enabledPlugins: config.enabledPlugins,
|
|
226
767
|
skillsRegistry: config.skillsRegistry,
|
|
768
|
+
reasoningEffort: config.reasoningEffort,
|
|
769
|
+
expanded,
|
|
770
|
+
busy,
|
|
771
|
+
draft: input,
|
|
227
772
|
},
|
|
228
773
|
actions,
|
|
229
774
|
});
|
|
230
775
|
if (res?.message)
|
|
231
776
|
pushSystem(res.message);
|
|
777
|
+
if (res?.picker) {
|
|
778
|
+
const index = Math.max(0, res.picker.items.findIndex((item) => item.value === res.picker.current));
|
|
779
|
+
setPickerIndex(index);
|
|
780
|
+
setPicker(res.picker);
|
|
781
|
+
setInput("");
|
|
782
|
+
}
|
|
783
|
+
if (res?.compacted)
|
|
784
|
+
appendCompactionFact(res.compacted);
|
|
785
|
+
if (res?.cleared)
|
|
786
|
+
sessionRef.current.append({ type: "cleared" });
|
|
232
787
|
if (res?.quit)
|
|
233
|
-
|
|
788
|
+
void quitCleanly();
|
|
234
789
|
}
|
|
235
790
|
catch (err) {
|
|
236
|
-
pushSystem(
|
|
791
|
+
pushSystem(parsed.name === "login"
|
|
792
|
+
? "Login failed. The credential was not included in this error message."
|
|
793
|
+
: `Command failed: ${err.message}`);
|
|
237
794
|
}
|
|
238
795
|
return true;
|
|
239
796
|
}, [
|
|
240
797
|
actions,
|
|
798
|
+
appendCompactionFact,
|
|
241
799
|
config.agentMode,
|
|
242
800
|
config.enabledPlugins,
|
|
243
801
|
config.enabledSkills,
|
|
@@ -246,7 +804,9 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
246
804
|
config.skillsRegistry,
|
|
247
805
|
config.systemPrompt,
|
|
248
806
|
config.theme,
|
|
249
|
-
|
|
807
|
+
busy,
|
|
808
|
+
expanded,
|
|
809
|
+
quitCleanly,
|
|
250
810
|
history,
|
|
251
811
|
pushSystem,
|
|
252
812
|
pushUser,
|
|
@@ -256,8 +816,26 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
256
816
|
]);
|
|
257
817
|
const handleSubmit = useCallback(async (raw) => {
|
|
258
818
|
const text = raw.trim();
|
|
259
|
-
if (!text
|
|
819
|
+
if (!text)
|
|
820
|
+
return;
|
|
821
|
+
if (approvalFeedbackRef.current) {
|
|
822
|
+
approvalFeedbackRef.current.submit(text);
|
|
823
|
+
setInput("");
|
|
260
824
|
return;
|
|
825
|
+
}
|
|
826
|
+
if (plainApprovalRef.current) {
|
|
827
|
+
plainApprovalRef.current.submit(text);
|
|
828
|
+
setInput("");
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (busy) {
|
|
832
|
+
if (text.startsWith("/")) {
|
|
833
|
+
await runSlash(text);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
pushSystem("A run is active. Use /cancel to stop, /steer <text> to guide it, or wait.");
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
261
839
|
setInput("");
|
|
262
840
|
if (text.startsWith("/")) {
|
|
263
841
|
await runSlash(text);
|
|
@@ -274,114 +852,222 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
274
852
|
return;
|
|
275
853
|
}
|
|
276
854
|
if (lookup.status === "missing") {
|
|
277
|
-
pushSystem(`Cannot send: no API key for ${provider.label}
|
|
278
|
-
`Run \`/login ${provider.id} ${config.model} <key>\`
|
|
855
|
+
pushSystem(`Cannot send: no API key for ${provider.label}.\n` +
|
|
856
|
+
`Run \`/login ${provider.id} ${config.model} <key>\` (one key covers every ${provider.id} model), or set ${provider.authEnv[0]}.`);
|
|
279
857
|
return;
|
|
280
858
|
}
|
|
281
859
|
apiKey = lookup.key;
|
|
282
860
|
}
|
|
283
|
-
const nextHistory = [...history, { role: "user", content: text }];
|
|
284
|
-
setHistory(nextHistory);
|
|
285
861
|
idRef.current += 1;
|
|
286
862
|
const bubbleId = idRef.current;
|
|
287
863
|
setStreamingId(bubbleId);
|
|
288
864
|
setStreamingText("");
|
|
865
|
+
setStreamPhase("thinking");
|
|
866
|
+
steerQueueRef.current = [];
|
|
867
|
+
lastStreamFlushRef.current = 0;
|
|
289
868
|
const ctrl = new AbortController();
|
|
290
869
|
abortRef.current = ctrl;
|
|
870
|
+
busyRef.current = true;
|
|
291
871
|
setBusy(true);
|
|
872
|
+
let receivedDone = false;
|
|
292
873
|
try {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
874
|
+
const maxTurns = config.agentMode ? positiveInt(config.maxAgentTurns, 24) : 1;
|
|
875
|
+
const skillsBlock = buildSkillsPrompt(config.enabledSkills);
|
|
876
|
+
const pluginsBlock = buildPluginsPrompt(config.enabledPlugins);
|
|
877
|
+
const agentBlock = config.agentMode ? AGENT_SYSTEM_PROMPT : "";
|
|
878
|
+
const fullSystem = [config.systemPrompt, skillsBlock, pluginsBlock, agentBlock]
|
|
879
|
+
.filter((part) => part.trim())
|
|
880
|
+
.join("\n\n");
|
|
881
|
+
let compactionNotified = false;
|
|
882
|
+
let lastRawFact = "";
|
|
883
|
+
sawDeltaRef.current = false;
|
|
884
|
+
const protocol = config.agentMode ? resolveToolProtocol(config, config.providerId, config.model) : "legacy";
|
|
885
|
+
for await (const ev of runAgent({
|
|
886
|
+
provider,
|
|
887
|
+
apiKey,
|
|
888
|
+
model: config.model,
|
|
889
|
+
messages: history,
|
|
890
|
+
prompt: text,
|
|
891
|
+
system: fullSystem,
|
|
892
|
+
maxTurns,
|
|
893
|
+
allowTools: config.agentMode,
|
|
894
|
+
tokenBudget: optionalBudget("FASTCODE_TOKEN_BUDGET"),
|
|
895
|
+
costBudgetUsd: optionalBudget("FASTCODE_COST_BUDGET_USD"),
|
|
896
|
+
signal: ctrl.signal,
|
|
897
|
+
cwd: process.cwd(),
|
|
898
|
+
idleTimeoutMs: config.requestIdleTimeoutMs,
|
|
899
|
+
compaction: compactionConfigFromEnv(),
|
|
900
|
+
reasoningEffort: config.reasoningEffort,
|
|
901
|
+
protocol,
|
|
902
|
+
tools: protocol === "native" ? toolSchemas() : undefined,
|
|
903
|
+
onProtocolFallback: (message) => {
|
|
904
|
+
setConfig((current) => withToolCapability(current, config.providerId, config.model, "pseudo"));
|
|
905
|
+
pushSystem(message);
|
|
906
|
+
},
|
|
907
|
+
onNativeOk: () => {
|
|
908
|
+
if (protocol === "native")
|
|
909
|
+
setConfig((current) => withToolCapability(current, config.providerId, config.model, "native"));
|
|
910
|
+
},
|
|
911
|
+
takeSteering: () => {
|
|
912
|
+
const queued = steerQueueRef.current;
|
|
913
|
+
steerQueueRef.current = [];
|
|
914
|
+
return queued;
|
|
915
|
+
},
|
|
916
|
+
requestApproval,
|
|
917
|
+
onDelta: (delta, full) => {
|
|
918
|
+
if (!sawDeltaRef.current) {
|
|
919
|
+
sawDeltaRef.current = true;
|
|
920
|
+
setStreamPhase("streaming");
|
|
313
921
|
}
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
922
|
+
if (!liveStreamingUi)
|
|
923
|
+
return;
|
|
924
|
+
const now = Date.now();
|
|
925
|
+
if (now - lastStreamFlushRef.current < 60)
|
|
926
|
+
return;
|
|
927
|
+
lastStreamFlushRef.current = now;
|
|
928
|
+
setStreamingText(stripToolCalls(full));
|
|
929
|
+
},
|
|
930
|
+
})) {
|
|
931
|
+
switch (ev.type) {
|
|
932
|
+
case "compaction":
|
|
933
|
+
appendCompactionFact({
|
|
934
|
+
summaryRef: ev.summaryFile,
|
|
935
|
+
source: ev.source,
|
|
936
|
+
beforeTokens: ev.beforeTokens,
|
|
937
|
+
afterTokens: ev.afterTokens,
|
|
938
|
+
droppedMessages: ev.droppedMessages,
|
|
939
|
+
keepMessages: ev.keptMessages,
|
|
940
|
+
reason: ev.reason === "heap" ? "heap" : "threshold",
|
|
941
|
+
});
|
|
942
|
+
if (!compactionNotified) {
|
|
943
|
+
compactionNotified = true;
|
|
944
|
+
pushSystem(`[context compacted] ${ev.beforeTokens} → ${ev.afterTokens} tokens (${ev.reason}); recent turns kept.` +
|
|
945
|
+
(ev.summaryFile ? `\nSummary: ${ev.summaryFile}` : ""));
|
|
946
|
+
}
|
|
947
|
+
break;
|
|
948
|
+
case "assistant_raw":
|
|
949
|
+
lastRawFact = ev.text;
|
|
950
|
+
sessionRef.current.append({ type: "assistant_raw", content: ev.text, toolCalls: ev.toolCalls });
|
|
951
|
+
break;
|
|
952
|
+
case "assistant_text":
|
|
953
|
+
pushSystem(ev.text);
|
|
954
|
+
break;
|
|
955
|
+
case "tool_call":
|
|
956
|
+
sessionRef.current.append({ type: "tool_call", id: ev.id, tool: ev.call.tool, args: ev.call, native: Boolean(ev.native) });
|
|
957
|
+
setStreamPhase("tool");
|
|
958
|
+
setAgentActivity(`Coding: ${ev.call.tool}`);
|
|
959
|
+
pushSystem(`Running tool: \`${ev.call.tool}\`\n\n\`\`\`json\n${JSON.stringify(ev.call, null, 2)}\n\`\`\``);
|
|
960
|
+
break;
|
|
961
|
+
case "tool_result": {
|
|
962
|
+
sessionRef.current.append({
|
|
963
|
+
type: "tool_result",
|
|
964
|
+
id: ev.id,
|
|
965
|
+
tool: ev.call.tool,
|
|
966
|
+
ok: ev.ok,
|
|
967
|
+
output: ev.output,
|
|
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 } : {}),
|
|
979
|
+
});
|
|
980
|
+
const collapsed = stableUi && !expanded;
|
|
981
|
+
const body = collapsed ? collapseToolOutput(ev.output) : ev.output;
|
|
982
|
+
const hint = collapsed ? "\n\n_(output collapsed; /expand to show full output)_" : "";
|
|
983
|
+
pushSystem(`Tool ${ev.ok ? "completed" : "failed"}: \`${ev.call.tool}\`\n\n\`\`\`\n${body}\n\`\`\`${hint}`);
|
|
984
|
+
break;
|
|
321
985
|
}
|
|
986
|
+
case "usage":
|
|
987
|
+
if (ev.usage.inputTokens)
|
|
988
|
+
setTotalIn((n) => n + (ev.usage.inputTokens ?? 0));
|
|
989
|
+
if (ev.usage.outputTokens)
|
|
990
|
+
setTotalOut((n) => n + (ev.usage.outputTokens ?? 0));
|
|
991
|
+
if (ev.usage.costUsd)
|
|
992
|
+
setTotalCost((n) => n + (ev.usage.costUsd ?? 0));
|
|
993
|
+
break;
|
|
994
|
+
case "loop_detected":
|
|
995
|
+
pushSystem(`Loop detected: \`${ev.fingerprint.split(":")[0]}\` repeated ${ev.occurrences} times. Asking the model to change approach.`);
|
|
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
|
+
}
|
|
1016
|
+
case "notice":
|
|
1017
|
+
pushSystem(ev.text);
|
|
1018
|
+
break;
|
|
1019
|
+
case "checkpoint":
|
|
1020
|
+
sessionRef.current.append({
|
|
1021
|
+
type: "checkpoint",
|
|
1022
|
+
turn: ev.turn,
|
|
1023
|
+
cwd: ev.cwd,
|
|
1024
|
+
providerId: ev.providerId,
|
|
1025
|
+
model: ev.model,
|
|
1026
|
+
activeFiles: ev.activeFiles,
|
|
1027
|
+
lastFactSeq: sessionRef.current.count(),
|
|
1028
|
+
});
|
|
1029
|
+
break;
|
|
1030
|
+
case "error":
|
|
1031
|
+
if (ev.kind === "aborted")
|
|
1032
|
+
pushSystem("Cancelled current run; partial output kept. Type 继续 to continue.");
|
|
1033
|
+
else if (ev.kind === "stream_error")
|
|
1034
|
+
pushSystem(streamErrorMessage(ev.message, provider, config.model));
|
|
1035
|
+
else
|
|
1036
|
+
pushSystem(ev.message);
|
|
1037
|
+
break;
|
|
1038
|
+
case "done":
|
|
1039
|
+
receivedDone = true;
|
|
1040
|
+
setHistory(ev.history);
|
|
1041
|
+
if (ev.finalText) {
|
|
1042
|
+
idRef.current += 1;
|
|
1043
|
+
const finalId = idRef.current;
|
|
1044
|
+
if (ev.finalText !== lastRawFact)
|
|
1045
|
+
sessionRef.current.append({ type: "assistant_raw", content: ev.finalText });
|
|
1046
|
+
setTranscript((prev) => [
|
|
1047
|
+
...prev,
|
|
1048
|
+
{ id: finalId, role: "assistant", content: ev.finalText, markdown: true },
|
|
1049
|
+
]);
|
|
1050
|
+
}
|
|
1051
|
+
break;
|
|
322
1052
|
}
|
|
323
|
-
const visibleText = stripToolCalls(acc);
|
|
324
|
-
const calls = config.agentMode ? parseToolCalls(acc) : [];
|
|
325
|
-
if (calls.length === 0) {
|
|
326
|
-
finalText = visibleText || acc;
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
if (visibleText)
|
|
330
|
-
pushSystem(visibleText);
|
|
331
|
-
const results: any[] = [];
|
|
332
|
-
for (const call of calls) {
|
|
333
|
-
setAgentActivity(`Coding: ${call.tool}`);
|
|
334
|
-
pushSystem(`Running tool: \`${call.tool}\`\n\n\`\`\`json\n${JSON.stringify(call, null, 2)}\n\`\`\``);
|
|
335
|
-
const result: any = await runAgentTool(call, process.cwd());
|
|
336
|
-
results.push({ call, ...result });
|
|
337
|
-
pushSystem(`Tool ${result.ok ? "completed" : "failed"}: \`${call.tool}\`\n\n\`\`\`\n${result.output}\n\`\`\``);
|
|
338
|
-
}
|
|
339
|
-
const toolMessage = formatToolResults(results);
|
|
340
|
-
workingHistory = [
|
|
341
|
-
...workingHistory,
|
|
342
|
-
{ role: "assistant", content: acc },
|
|
343
|
-
{ role: "user", content: toolMessage },
|
|
344
|
-
];
|
|
345
|
-
setHistory(workingHistory);
|
|
346
|
-
setStreamingText("");
|
|
347
|
-
setAgentActivity(null);
|
|
348
|
-
}
|
|
349
|
-
if (!finalText) {
|
|
350
|
-
finalText = "Stopped after running the maximum number of agent tool turns.";
|
|
351
1053
|
}
|
|
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
1054
|
}
|
|
360
1055
|
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
|
-
}
|
|
1056
|
+
if (!receivedDone)
|
|
1057
|
+
setHistory([...history, { role: "user", content: text }]);
|
|
1058
|
+
pushSystem(`Error: ${String(err?.message ?? err)}`);
|
|
376
1059
|
}
|
|
377
1060
|
finally {
|
|
378
1061
|
setStreamingId(null);
|
|
379
1062
|
setStreamingText("");
|
|
380
1063
|
setAgentActivity(null);
|
|
1064
|
+
setStreamPhase("thinking");
|
|
1065
|
+
busyRef.current = false;
|
|
381
1066
|
setBusy(false);
|
|
382
1067
|
abortRef.current = null;
|
|
383
1068
|
}
|
|
384
1069
|
}, [
|
|
1070
|
+
appendCompactionFact,
|
|
385
1071
|
busy,
|
|
386
1072
|
config.agentMode,
|
|
387
1073
|
config.enabledSkills,
|
|
@@ -392,6 +1078,7 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
392
1078
|
history,
|
|
393
1079
|
pushSystem,
|
|
394
1080
|
pushUser,
|
|
1081
|
+
requestApproval,
|
|
395
1082
|
runSlash,
|
|
396
1083
|
]);
|
|
397
1084
|
// Guided `/login`: provider → model → key. Text prompts only; the key step
|
|
@@ -399,55 +1086,50 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
399
1086
|
const handleWizardSubmit = useCallback(async (raw) => {
|
|
400
1087
|
const value = raw.trim();
|
|
401
1088
|
setInput("");
|
|
402
|
-
if (!
|
|
1089
|
+
if (!pendingLogin)
|
|
403
1090
|
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);
|
|
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.");
|
|
413
1094
|
return;
|
|
414
1095
|
}
|
|
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}` : ""));
|
|
1096
|
+
continueModelSetup(pendingLogin, pendingLogin.provider, value);
|
|
421
1097
|
return;
|
|
422
1098
|
}
|
|
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.` : ""));
|
|
1099
|
+
if (pendingLogin.step !== "key")
|
|
432
1100
|
return;
|
|
433
|
-
}
|
|
434
1101
|
const provider = getProvider(pendingLogin.provider);
|
|
435
1102
|
if (!provider) {
|
|
436
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
|
+
}
|
|
437
1114
|
return;
|
|
438
1115
|
}
|
|
439
1116
|
let key = value;
|
|
440
1117
|
if (value.toLowerCase() === "keep") {
|
|
441
1118
|
const legacy = getLegacy(provider.id);
|
|
442
1119
|
if (!legacy) {
|
|
443
|
-
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.`);
|
|
444
1121
|
return;
|
|
445
1122
|
}
|
|
446
1123
|
key = legacy;
|
|
447
1124
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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]);
|
|
451
1133
|
useEffect(() => {
|
|
452
1134
|
if (!initialPrompt || initialPromptSubmittedRef.current)
|
|
453
1135
|
return;
|
|
@@ -456,6 +1138,7 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
456
1138
|
return () => clearTimeout(t);
|
|
457
1139
|
}, [handleSubmit, initialPrompt]);
|
|
458
1140
|
useEffect(() => () => {
|
|
1141
|
+
shutdownShells();
|
|
459
1142
|
if (sessionRef.current)
|
|
460
1143
|
sessionRef.current.close();
|
|
461
1144
|
}, []);
|
|
@@ -468,9 +1151,9 @@ export const App = ({ initialPrompt, resume }) => {
|
|
|
468
1151
|
setConfig((c) => ({ ...c, agentMode: false }));
|
|
469
1152
|
pushSystem("Trust declined: agent tools are off for this session (chat only).");
|
|
470
1153
|
}
|
|
471
|
-
|
|
1154
|
+
setTrustEpoch((n) => n + 1);
|
|
472
1155
|
};
|
|
473
1156
|
return _jsx(TrustGate, { theme: theme, cwd: process.cwd(), onDecision: decide });
|
|
474
1157
|
}
|
|
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:
|
|
1158
|
+
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
1159
|
};
|