@datename/fastcode-termux 0.0.0-stage → 0.1.6-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +418 -2
- package/bin/fastcode.mjs +59 -0
- package/dist/agent/approval.d.ts +50 -0
- package/dist/agent/approval.js +7 -0
- package/dist/agent/compaction.d.ts +39 -0
- package/dist/agent/compaction.js +227 -0
- package/dist/agent/context.d.ts +29 -0
- package/dist/agent/context.js +62 -0
- package/dist/agent/guards.d.ts +43 -0
- package/dist/agent/guards.js +269 -0
- package/dist/agent/ignore.d.ts +11 -0
- package/dist/agent/ignore.js +129 -0
- package/dist/agent/limits.d.ts +24 -0
- package/dist/agent/limits.js +71 -0
- package/dist/agent/process-tree.d.ts +2 -0
- package/dist/agent/process-tree.js +31 -0
- package/dist/agent/protocol-mode.d.ts +6 -0
- package/dist/agent/protocol-mode.js +51 -0
- package/dist/agent/protocol.d.ts +4 -0
- package/dist/agent/protocol.js +129 -0
- package/dist/agent/registry.d.ts +17 -0
- package/dist/agent/registry.js +90 -0
- package/dist/agent/runner.d.ts +106 -0
- package/dist/agent/runner.js +352 -0
- package/dist/agent/shell-session.d.ts +37 -0
- package/dist/agent/shell-session.js +400 -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 +63 -0
- package/dist/agent/tools/append_file.d.ts +2 -0
- package/dist/agent/tools/append_file.js +26 -0
- package/dist/agent/tools/apply_patch.d.ts +10 -0
- package/dist/agent/tools/apply_patch.js +52 -0
- package/dist/agent/tools/copy_file.d.ts +2 -0
- package/dist/agent/tools/copy_file.js +30 -0
- package/dist/agent/tools/delete_file.d.ts +2 -0
- package/dist/agent/tools/delete_file.js +24 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +23 -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 +24 -0
- package/dist/agent/tools/move_path.d.ts +2 -0
- package/dist/agent/tools/move_path.js +47 -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 +21 -0
- package/dist/agent/tools/read_many.d.ts +2 -0
- package/dist/agent/tools/read_many.js +25 -0
- package/dist/agent/tools/readers.d.ts +6 -0
- package/dist/agent/tools/readers.js +216 -0
- package/dist/agent/tools/replace_in_file.d.ts +2 -0
- package/dist/agent/tools/replace_in_file.js +30 -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 +39 -0
- package/dist/agent/tools/types.js +1 -0
- package/dist/agent/tools/validate.d.ts +6 -0
- package/dist/agent/tools/validate.js +29 -0
- package/dist/agent/tools/write_file.d.ts +2 -0
- package/dist/agent/tools/write_file.js +26 -0
- package/dist/agent/tools.d.ts +3 -0
- package/dist/agent/tools.js +2 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.js +708 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +569 -0
- package/dist/commands/about.d.ts +8 -0
- package/dist/commands/about.js +16 -0
- package/dist/commands/agent.d.ts +11 -0
- package/dist/commands/agent.js +18 -0
- package/dist/commands/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 +11 -0
- package/dist/commands/clear.js +9 -0
- package/dist/commands/compact.d.ts +10 -0
- package/dist/commands/compact.js +22 -0
- package/dist/commands/config.d.ts +7 -0
- package/dist/commands/config.js +24 -0
- package/dist/commands/context.d.ts +7 -0
- package/dist/commands/context.js +24 -0
- package/dist/commands/cost.d.ts +9 -0
- package/dist/commands/cost.js +13 -0
- package/dist/commands/cwd.d.ts +7 -0
- package/dist/commands/cwd.js +7 -0
- package/dist/commands/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/exit.d.ts +9 -0
- package/dist/commands/exit.js +8 -0
- package/dist/commands/expand.d.ts +10 -0
- package/dist/commands/expand.js +8 -0
- package/dist/commands/help.d.ts +6 -0
- package/dist/commands/help.js +18 -0
- package/dist/commands/index.d.ts +9 -0
- package/dist/commands/index.js +80 -0
- package/dist/commands/jobs.d.ts +7 -0
- package/dist/commands/jobs.js +16 -0
- package/dist/commands/kill.d.ts +7 -0
- package/dist/commands/kill.js +11 -0
- package/dist/commands/login.d.ts +18 -0
- package/dist/commands/login.js +63 -0
- package/dist/commands/logout.d.ts +9 -0
- package/dist/commands/logout.js +45 -0
- package/dist/commands/model.d.ts +24 -0
- package/dist/commands/model.js +50 -0
- package/dist/commands/plugin.d.ts +11 -0
- package/dist/commands/plugin.js +74 -0
- package/dist/commands/provider.d.ts +24 -0
- package/dist/commands/provider.js +39 -0
- package/dist/commands/reset.d.ts +9 -0
- package/dist/commands/reset.js +13 -0
- package/dist/commands/skills.d.ts +11 -0
- package/dist/commands/skills.js +106 -0
- package/dist/commands/steer.d.ts +11 -0
- package/dist/commands/steer.js +13 -0
- package/dist/commands/system.d.ts +11 -0
- package/dist/commands/system.js +20 -0
- package/dist/commands/theme.d.ts +11 -0
- package/dist/commands/theme.js +22 -0
- package/dist/commands/tools.d.ts +7 -0
- package/dist/commands/tools.js +29 -0
- package/dist/commands/whoami.d.ts +9 -0
- package/dist/commands/whoami.js +52 -0
- package/dist/config/paths.d.ts +4 -0
- package/dist/config/paths.js +59 -0
- package/dist/config/store.d.ts +89 -0
- package/dist/config/store.js +260 -0
- package/dist/config/themes.d.ts +73 -0
- package/dist/config/themes.js +77 -0
- package/dist/config/trust.d.ts +22 -0
- package/dist/config/trust.js +153 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +31 -0
- package/dist/lib/diagnostics.d.ts +66 -0
- package/dist/lib/diagnostics.js +195 -0
- package/dist/lib/http.d.ts +23 -0
- package/dist/lib/http.js +128 -0
- package/dist/lib/markdown.d.ts +1 -0
- package/dist/lib/markdown.js +32 -0
- 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/lib/pkg.d.ts +5 -0
- package/dist/lib/pkg.js +27 -0
- package/dist/lib/platform.d.ts +7 -0
- package/dist/lib/platform.js +57 -0
- package/dist/lib/startup.d.ts +6 -0
- package/dist/lib/startup.js +25 -0
- package/dist/lib/uiMode.d.ts +2 -0
- package/dist/lib/uiMode.js +7 -0
- package/dist/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/plugins/manager.d.ts +23 -0
- package/dist/plugins/manager.js +101 -0
- package/dist/providers/anthropic.d.ts +22 -0
- package/dist/providers/anthropic.js +119 -0
- package/dist/providers/custom.d.ts +33 -0
- package/dist/providers/custom.js +25 -0
- package/dist/providers/deepseek.d.ts +34 -0
- package/dist/providers/deepseek.js +16 -0
- package/dist/providers/google.d.ts +33 -0
- package/dist/providers/google.js +19 -0
- package/dist/providers/groq.d.ts +34 -0
- package/dist/providers/groq.js +21 -0
- package/dist/providers/index.d.ts +10 -0
- package/dist/providers/index.js +67 -0
- package/dist/providers/mistral.d.ts +34 -0
- package/dist/providers/mistral.js +20 -0
- package/dist/providers/moonshot.d.ts +34 -0
- package/dist/providers/moonshot.js +23 -0
- package/dist/providers/ollama.d.ts +33 -0
- package/dist/providers/ollama.js +22 -0
- package/dist/providers/openai.d.ts +34 -0
- package/dist/providers/openai.js +26 -0
- package/dist/providers/openaiCompatible.d.ts +26 -0
- package/dist/providers/openaiCompatible.js +158 -0
- package/dist/providers/openrouter.d.ts +34 -0
- package/dist/providers/openrouter.js +27 -0
- package/dist/providers/qwen.d.ts +34 -0
- package/dist/providers/qwen.js +24 -0
- package/dist/providers/xai.d.ts +34 -0
- package/dist/providers/xai.js +19 -0
- package/dist/providers/zhipu.d.ts +34 -0
- package/dist/providers/zhipu.js +24 -0
- package/dist/session/log.d.ts +52 -0
- package/dist/session/log.js +331 -0
- package/dist/session/projection.d.ts +43 -0
- package/dist/session/projection.js +257 -0
- package/dist/skills/manager.d.ts +18 -0
- package/dist/skills/manager.js +201 -0
- package/dist/skills/parser.d.ts +18 -0
- package/dist/skills/parser.js +65 -0
- package/dist/types.d.ts +205 -0
- package/dist/types.js +1 -0
- package/dist/ui/Banner.d.ts +5 -0
- package/dist/ui/Banner.js +65 -0
- package/dist/ui/Message.d.ts +1 -0
- package/dist/ui/Message.js +24 -0
- package/dist/ui/Picker.d.ts +8 -0
- package/dist/ui/Picker.js +24 -0
- package/dist/ui/Prompt.d.ts +9 -0
- package/dist/ui/Prompt.js +38 -0
- package/dist/ui/Spinner.d.ts +5 -0
- package/dist/ui/Spinner.js +39 -0
- package/dist/ui/StatusBar.d.ts +11 -0
- package/dist/ui/StatusBar.js +20 -0
- package/dist/ui/TrustGate.d.ts +5 -0
- package/dist/ui/TrustGate.js +26 -0
- package/package.json +96 -4
- package/src/agent/approval.ts +47 -0
- package/src/agent/compaction.ts +276 -0
- package/src/agent/context.ts +97 -0
- package/src/agent/guards.ts +270 -0
- package/src/agent/ignore.ts +147 -0
- package/src/agent/limits.ts +71 -0
- package/src/agent/process-tree.ts +32 -0
- package/src/agent/protocol-mode.ts +59 -0
- package/src/agent/protocol.ts +129 -0
- package/src/agent/registry.ts +98 -0
- package/src/agent/runner.ts +443 -0
- package/src/agent/shell-session.ts +449 -0
- package/src/agent/stream.ts +143 -0
- package/src/agent/tool-schemas.ts +67 -0
- package/src/agent/tools/append_file.ts +28 -0
- package/src/agent/tools/apply_patch.ts +54 -0
- package/src/agent/tools/copy_file.ts +32 -0
- package/src/agent/tools/delete_file.ts +26 -0
- package/src/agent/tools/grep.ts +25 -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 +26 -0
- package/src/agent/tools/move_path.ts +50 -0
- package/src/agent/tools/paths.ts +92 -0
- package/src/agent/tools/pwd.ts +12 -0
- package/src/agent/tools/read_file.ts +23 -0
- package/src/agent/tools/read_many.ts +27 -0
- package/src/agent/tools/readers.ts +228 -0
- package/src/agent/tools/replace_in_file.ts +32 -0
- package/src/agent/tools/shell.ts +53 -0
- package/src/agent/tools/stat_path.ts +33 -0
- package/src/agent/tools/types.ts +42 -0
- package/src/agent/tools/validate.ts +35 -0
- package/src/agent/tools/write_file.ts +28 -0
- package/src/agent/tools.ts +3 -0
- package/src/app.ts +708 -0
- package/src/cli.ts +335 -0
- package/src/commands/about.ts +16 -0
- package/src/commands/agent.ts +18 -0
- 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 +9 -0
- package/src/commands/compact.ts +23 -0
- package/src/commands/config.ts +24 -0
- package/src/commands/context.ts +25 -0
- package/src/commands/cost.ts +13 -0
- package/src/commands/cwd.ts +7 -0
- package/src/commands/edit.ts +37 -0
- package/src/commands/effort.ts +49 -0
- package/src/commands/exit.ts +8 -0
- package/src/commands/expand.ts +8 -0
- package/src/commands/help.ts +18 -0
- package/src/commands/index.ts +80 -0
- package/src/commands/jobs.ts +17 -0
- package/src/commands/kill.ts +12 -0
- package/src/commands/login.ts +63 -0
- package/src/commands/logout.ts +45 -0
- package/src/commands/model.ts +50 -0
- package/src/commands/plugin.ts +74 -0
- package/src/commands/provider.ts +39 -0
- package/src/commands/reset.ts +13 -0
- package/src/commands/skills.ts +106 -0
- package/src/commands/steer.ts +13 -0
- package/src/commands/system.ts +20 -0
- package/src/commands/theme.ts +22 -0
- package/src/commands/tools.ts +29 -0
- package/src/commands/whoami.ts +52 -0
- package/src/config/paths.ts +59 -0
- package/src/config/store.ts +293 -0
- package/src/config/themes.ts +77 -0
- package/src/config/trust.ts +183 -0
- package/src/index.ts +31 -0
- package/src/lib/diagnostics.ts +195 -0
- package/src/lib/http.ts +132 -0
- package/src/lib/markdown.ts +32 -0
- 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/lib/pkg.ts +35 -0
- package/src/lib/platform.ts +57 -0
- package/src/lib/startup.ts +25 -0
- package/src/lib/uiMode.ts +7 -0
- package/src/patch/apply.ts +194 -0
- package/src/patch/parse.ts +131 -0
- package/src/patch/plan.ts +317 -0
- package/src/plugins/manager.ts +101 -0
- package/src/providers/anthropic.ts +119 -0
- package/src/providers/custom.ts +25 -0
- package/src/providers/deepseek.ts +16 -0
- package/src/providers/google.ts +19 -0
- package/src/providers/groq.ts +21 -0
- package/src/providers/index.ts +68 -0
- package/src/providers/mistral.ts +20 -0
- package/src/providers/moonshot.ts +23 -0
- package/src/providers/ollama.ts +22 -0
- package/src/providers/openai.ts +26 -0
- package/src/providers/openaiCompatible.ts +158 -0
- package/src/providers/openrouter.ts +27 -0
- package/src/providers/qwen.ts +24 -0
- package/src/providers/xai.ts +19 -0
- package/src/providers/zhipu.ts +24 -0
- package/src/session/log.ts +374 -0
- package/src/session/projection.ts +308 -0
- package/src/skills/manager.ts +201 -0
- package/src/skills/parser.ts +65 -0
- package/src/types.ts +205 -0
- package/src/ui/Banner.ts +68 -0
- package/src/ui/Message.ts +24 -0
- package/src/ui/Picker.ts +24 -0
- package/src/ui/Prompt.ts +38 -0
- package/src/ui/Spinner.ts +39 -0
- package/src/ui/StatusBar.ts +20 -0
- package/src/ui/TrustGate.ts +26 -0
- package/tsconfig.json +22 -0
package/src/app.ts
ADDED
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* FastCode App component — the heart of the TUI.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* • Render banner, transcript, prompt, and status bar.
|
|
7
|
+
* • Drive the chat loop: send user messages, stream provider responses,
|
|
8
|
+
* update token/cost state, and persist config across runs.
|
|
9
|
+
* • Dispatch slash commands.
|
|
10
|
+
* • Cancel streaming on Esc and quit cleanly on /exit.
|
|
11
|
+
*/
|
|
12
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
13
|
+
import { Box, Static, Text, useApp, useInput } from "ink";
|
|
14
|
+
import { AGENT_SYSTEM_PROMPT, stripToolCalls, } from "./agent/protocol.js";
|
|
15
|
+
import { compactionConfigFromEnv } from "./agent/compaction.js";
|
|
16
|
+
import { resolveToolProtocol, withToolCapability } from "./agent/protocol-mode.js";
|
|
17
|
+
import { runAgent } from "./agent/runner.js";
|
|
18
|
+
import { toolSchemas } from "./agent/tool-schemas.js";
|
|
19
|
+
import { shutdownShells } from "./agent/shell-session.js";
|
|
20
|
+
import { Banner } from "./ui/Banner.js";
|
|
21
|
+
import { Spinner } from "./ui/Spinner.js";
|
|
22
|
+
import { StatusBar } from "./ui/StatusBar.js";
|
|
23
|
+
import { MessageBubble } from "./ui/Message.js";
|
|
24
|
+
import { Prompt } from "./ui/Prompt.js";
|
|
25
|
+
import { Picker } from "./ui/Picker.js";
|
|
26
|
+
import { COMMANDS, findCommand, parseSlash } from "./commands/index.js";
|
|
27
|
+
import { saveScopedKey } from "./commands/login.js";
|
|
28
|
+
import { getProvider, getProviderOrThrow, PROVIDERS } from "./providers/index.js";
|
|
29
|
+
import { loadConfig, saveConfig, resolveApiKey, getLegacy, } from "./config/store.js";
|
|
30
|
+
import { getTheme } from "./config/themes.js";
|
|
31
|
+
import { createStartupTranscript } from "./lib/startup.js";
|
|
32
|
+
import { shouldUseLiveStreamingUi, shouldUseStableTerminalUi } from "./lib/uiMode.js";
|
|
33
|
+
import { buildPluginsPrompt } from "./plugins/manager.js";
|
|
34
|
+
import { buildSkillsPrompt } from "./skills/manager.js";
|
|
35
|
+
import { createSessionLog, latestUnclosedSession, loggingEnabled, openSessionLog } from "./session/log.js";
|
|
36
|
+
import { resumeSession } from "./session/projection.js";
|
|
37
|
+
import { isTrusted, trustProject } from "./config/trust.js";
|
|
38
|
+
import { TrustGate } from "./ui/TrustGate.js";
|
|
39
|
+
/**
|
|
40
|
+
* Mask the secret part of a `/login` invocation in the transcript:
|
|
41
|
+
* `/login prov` → unchanged, `/login prov <key>` → key masked,
|
|
42
|
+
* `/login prov model <key>` → provider/model shown, key masked.
|
|
43
|
+
*/
|
|
44
|
+
function maskLoginEcho(args) {
|
|
45
|
+
const parts = ["/login", args[0] ?? ""];
|
|
46
|
+
if (args.length === 2) {
|
|
47
|
+
parts.push("********");
|
|
48
|
+
}
|
|
49
|
+
else if (args.length >= 3) {
|
|
50
|
+
parts.push(args[1]);
|
|
51
|
+
for (let i = 2; i < args.length; i += 1)
|
|
52
|
+
parts.push("********");
|
|
53
|
+
}
|
|
54
|
+
return parts.filter((p) => p !== "").join(" ");
|
|
55
|
+
}
|
|
56
|
+
function positiveInt(value, fallback) {
|
|
57
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
58
|
+
}
|
|
59
|
+
function streamingTail(text, maxLines = 6) {
|
|
60
|
+
const lines = String(text ?? "").split(/\r?\n/);
|
|
61
|
+
if (lines.length <= maxLines)
|
|
62
|
+
return String(text ?? "");
|
|
63
|
+
return "…\n" + lines.slice(-maxLines).join("\n");
|
|
64
|
+
}
|
|
65
|
+
function collapseToolOutput(output) {
|
|
66
|
+
const text = String(output ?? "");
|
|
67
|
+
const lines = text.split(/\r?\n/);
|
|
68
|
+
const first = (lines.find((line) => line.trim()) ?? "").trim();
|
|
69
|
+
const clipped = first.length > 160 ? first.slice(0, 157) + "..." : first;
|
|
70
|
+
const rest = Math.max(0, lines.length - 1);
|
|
71
|
+
return `${clipped}${clipped ? "\n" : ""}[${rest} more lines, ${text.length} chars]`;
|
|
72
|
+
}
|
|
73
|
+
function optionalBudget(name) {
|
|
74
|
+
const value = Number(process.env[name]);
|
|
75
|
+
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
76
|
+
}
|
|
77
|
+
function unclosedNotice(session) {
|
|
78
|
+
const where = session.cwd || "(unknown directory)";
|
|
79
|
+
const files = session.activeFiles?.length ?? 0;
|
|
80
|
+
return `Last session did not close normally (${where}, ~${session.turns} turns, ${files} file${files === 1 ? "" : "s"} changed).\n` +
|
|
81
|
+
`Resume it with \`fastcode resume ${session.id}\`, or list sessions with \`fastcode sessions\`.`;
|
|
82
|
+
}
|
|
83
|
+
function streamErrorMessage(message, provider, model) {
|
|
84
|
+
if (/\b(401|403)\b/.test(message) || /invalid api key|unauthorized|authentication/i.test(message)) {
|
|
85
|
+
return `Authentication failed for ${provider.label} · ${model}. The key may be wrong or not allowed for this model.\n` +
|
|
86
|
+
`Check with \`/whoami\`, or re-save: \`/login ${provider.id} ${model} <key>\`.`;
|
|
87
|
+
}
|
|
88
|
+
if (/\b(402|429)\b/.test(message) || /quota|insufficient|rate limit|billing|balance/i.test(message)) {
|
|
89
|
+
return `Request blocked: ${provider.label} reports no remaining quota / rate limit for ${model}.\n` +
|
|
90
|
+
`Top up the account or switch model/provider. (${message.slice(0, 200)})`;
|
|
91
|
+
}
|
|
92
|
+
return `Error: ${message}`;
|
|
93
|
+
}
|
|
94
|
+
export const App = ({ initialPrompt, resume }) => {
|
|
95
|
+
const { exit } = useApp();
|
|
96
|
+
const startupRef = useRef(null);
|
|
97
|
+
if (!startupRef.current) {
|
|
98
|
+
const config = loadConfig();
|
|
99
|
+
const resumed = resume ? resumeSession(resume) : undefined;
|
|
100
|
+
if (resumed) {
|
|
101
|
+
const provider = resumed.meta.providerId ? getProvider(resumed.meta.providerId) : undefined;
|
|
102
|
+
if (provider) {
|
|
103
|
+
config.providerId = provider.id;
|
|
104
|
+
if (resumed.meta.model)
|
|
105
|
+
config.model = resumed.meta.model;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const restored = resumed?.messages ?? [];
|
|
109
|
+
const transcript = restored.length > 0
|
|
110
|
+
? restored.map((message, index) => ({
|
|
111
|
+
id: index + 1,
|
|
112
|
+
role: message.role,
|
|
113
|
+
content: message.role === "assistant" ? stripToolCalls(message.content) : message.content,
|
|
114
|
+
markdown: message.role !== "user",
|
|
115
|
+
}))
|
|
116
|
+
: createStartupTranscript(config);
|
|
117
|
+
if (resumed) {
|
|
118
|
+
const push = (text) => transcript.push({ id: transcript.length + 1, role: "system", content: text, markdown: true });
|
|
119
|
+
if (resumed.meta.providerId && !getProvider(resumed.meta.providerId))
|
|
120
|
+
push(`The session used provider ${resumed.meta.providerId}, which is not available in this build.`);
|
|
121
|
+
push(`Resumed session: ${config.providerId}::${config.model}${resumed.rebuilt ? " (projection rebuilt)" : ""}.`);
|
|
122
|
+
for (const warning of resumed.warnings)
|
|
123
|
+
push(warning);
|
|
124
|
+
}
|
|
125
|
+
else if (!resume && loggingEnabled()) {
|
|
126
|
+
const unclosed = latestUnclosedSession();
|
|
127
|
+
if (unclosed) {
|
|
128
|
+
transcript.push({
|
|
129
|
+
id: transcript.length + 1,
|
|
130
|
+
role: "system",
|
|
131
|
+
content: unclosedNotice(unclosed),
|
|
132
|
+
markdown: true,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
startupRef.current = {
|
|
137
|
+
config,
|
|
138
|
+
transcript,
|
|
139
|
+
history: restored.map((message) => ({ role: message.role, content: message.content })),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const startup = startupRef.current;
|
|
143
|
+
const sessionRef = useRef(null as any);
|
|
144
|
+
if (!sessionRef.current) {
|
|
145
|
+
sessionRef.current = resume
|
|
146
|
+
? openSessionLog(resume, {
|
|
147
|
+
providerId: startup.config.providerId,
|
|
148
|
+
model: startup.config.model,
|
|
149
|
+
cwd: process.cwd(),
|
|
150
|
+
})
|
|
151
|
+
: createSessionLog({
|
|
152
|
+
providerId: startup.config.providerId,
|
|
153
|
+
model: startup.config.model,
|
|
154
|
+
cwd: process.cwd(),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const [, setTrustEpoch] = useState(0);
|
|
158
|
+
const trusted = isTrusted(process.cwd());
|
|
159
|
+
const [config, setConfig] = useState(startup.config);
|
|
160
|
+
const [history, setHistory] = useState(startup.history ?? []);
|
|
161
|
+
const [transcript, setTranscript] = useState(startup.transcript);
|
|
162
|
+
const [streamingId, setStreamingId] = useState(null);
|
|
163
|
+
const [streamingText, setStreamingText] = useState("");
|
|
164
|
+
const [streamPhase, setStreamPhase] = useState("thinking");
|
|
165
|
+
const [input, setInput] = useState("");
|
|
166
|
+
const [pendingLogin, setPendingLogin] = useState(null);
|
|
167
|
+
const [picker, setPicker] = useState(null);
|
|
168
|
+
const [pickerIndex, setPickerIndex] = useState(0);
|
|
169
|
+
const [busy, setBusy] = useState(false);
|
|
170
|
+
const [expanded, setExpanded] = useState(false);
|
|
171
|
+
const steerQueueRef = useRef([]);
|
|
172
|
+
const [totalIn, setTotalIn] = useState(0);
|
|
173
|
+
const [totalOut, setTotalOut] = useState(0);
|
|
174
|
+
const [totalCost, setTotalCost] = useState(0);
|
|
175
|
+
const [agentActivity, setAgentActivity] = useState(null);
|
|
176
|
+
const idRef = useRef(startup.transcript.length);
|
|
177
|
+
const abortRef = useRef(null);
|
|
178
|
+
const sawDeltaRef = useRef(false);
|
|
179
|
+
const lastStreamFlushRef = useRef(0);
|
|
180
|
+
const ctrlCRef = useRef(0);
|
|
181
|
+
const initialPromptSubmittedRef = useRef(false);
|
|
182
|
+
const theme = getTheme(config.theme);
|
|
183
|
+
const stableUi = useMemo(() => shouldUseStableTerminalUi(), []);
|
|
184
|
+
const liveStreamingUi = useMemo(() => shouldUseLiveStreamingUi(), []);
|
|
185
|
+
// The banner picks a font that fits the terminal width (see ui/Banner.ts)
|
|
186
|
+
// and renders nothing when even the smallest font is too wide.
|
|
187
|
+
const showBanner = transcript.length <= startup.transcript.length && !busy;
|
|
188
|
+
const promptPlaceholder = pendingLogin
|
|
189
|
+
? pendingLogin.step === "provider"
|
|
190
|
+
? "Provider id (e.g. deepseek) · Esc to cancel"
|
|
191
|
+
: pendingLogin.step === "model"
|
|
192
|
+
? "Model id (e.g. deepseek-chat) · Esc to cancel"
|
|
193
|
+
: pendingLogin.provider && getLegacy(pendingLogin.provider)
|
|
194
|
+
? "Paste API key (masked), or type 'keep' · Esc to cancel"
|
|
195
|
+
: "Paste API key (masked) · Esc to cancel"
|
|
196
|
+
: undefined;
|
|
197
|
+
const promptMask = pendingLogin?.step === "key" ? "*" : undefined;
|
|
198
|
+
// Persist config whenever it changes.
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
saveConfig(config);
|
|
201
|
+
}, [config]);
|
|
202
|
+
const chooseItem = useCallback((request, index) => {
|
|
203
|
+
const item = request?.items?.[index];
|
|
204
|
+
setPicker(null);
|
|
205
|
+
if (item)
|
|
206
|
+
request.onSelect(item.value, item);
|
|
207
|
+
}, []);
|
|
208
|
+
// Esc cancels streaming. Double Ctrl+C quits.
|
|
209
|
+
useInput((inputChar, key) => {
|
|
210
|
+
if (picker) {
|
|
211
|
+
if (key.escape) {
|
|
212
|
+
picker.onCancel?.();
|
|
213
|
+
setPicker(null);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (key.upArrow) {
|
|
217
|
+
setPickerIndex((i) => Math.max(0, i - 1));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (key.downArrow) {
|
|
221
|
+
setPickerIndex((i) => Math.min(picker.items.length - 1, i + 1));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (key.return) {
|
|
225
|
+
chooseItem(picker, pickerIndex);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (/^[1-9]$/.test(inputChar)) {
|
|
229
|
+
chooseItem(picker, Number(inputChar) - 1);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (key.escape) {
|
|
235
|
+
if (pendingLogin) {
|
|
236
|
+
setPendingLogin(null);
|
|
237
|
+
setInput("");
|
|
238
|
+
pushSystem("API key entry cancelled.");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (abortRef.current) {
|
|
242
|
+
abortRef.current.abort();
|
|
243
|
+
abortRef.current = null;
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (key.ctrl && inputChar === "c") {
|
|
248
|
+
ctrlCRef.current += 1;
|
|
249
|
+
if (ctrlCRef.current >= 2) {
|
|
250
|
+
exit();
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
pushSystem("Press Ctrl+C again to quit.");
|
|
254
|
+
setTimeout(() => {
|
|
255
|
+
ctrlCRef.current = 0;
|
|
256
|
+
}, 1500);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
const pushSystem = useCallback((text) => {
|
|
261
|
+
idRef.current += 1;
|
|
262
|
+
sessionRef.current.append({ type: "notice", content: text });
|
|
263
|
+
setTranscript((prev) => [
|
|
264
|
+
...prev,
|
|
265
|
+
{ id: idRef.current, role: "system", content: text, markdown: true },
|
|
266
|
+
]);
|
|
267
|
+
}, []);
|
|
268
|
+
const pushUser = useCallback((text, recordFact = true) => {
|
|
269
|
+
idRef.current += 1;
|
|
270
|
+
sessionRef.current.append(recordFact
|
|
271
|
+
? { type: "user", content: text }
|
|
272
|
+
: { type: "notice", content: text });
|
|
273
|
+
setTranscript((prev) => [
|
|
274
|
+
...prev,
|
|
275
|
+
{ id: idRef.current, role: "user", content: text, markdown: false },
|
|
276
|
+
]);
|
|
277
|
+
}, []);
|
|
278
|
+
const actions = useMemo(() => ({
|
|
279
|
+
setProvider: (id) => setConfig((c) => ({ ...c, providerId: id })),
|
|
280
|
+
setModel: (id) => setConfig((c) => ({ ...c, model: id })),
|
|
281
|
+
setReasoningEffort: (effort) => setConfig((c) => ({ ...c, reasoningEffort: effort })),
|
|
282
|
+
setTheme: (id) => setConfig((c) => ({ ...c, theme: id })),
|
|
283
|
+
setAgentMode: (enabled) => setConfig((c) => ({ ...c, agentMode: enabled })),
|
|
284
|
+
setSystemPrompt: (p) => setConfig((c) => ({ ...c, systemPrompt: p })),
|
|
285
|
+
setSkillsRegistry: (url) => setConfig((c) => ({ ...c, skillsRegistry: url })),
|
|
286
|
+
enableSkill: (name) => setConfig((c) => ({
|
|
287
|
+
...c,
|
|
288
|
+
enabledSkills: Array.from(new Set([...(c.enabledSkills ?? []), name])),
|
|
289
|
+
})),
|
|
290
|
+
disableSkill: (name) => setConfig((c) => ({
|
|
291
|
+
...c,
|
|
292
|
+
enabledSkills: (c.enabledSkills ?? []).filter((n) => n !== name),
|
|
293
|
+
})),
|
|
294
|
+
enablePlugin: (name) => setConfig((c) => ({
|
|
295
|
+
...c,
|
|
296
|
+
enabledPlugins: Array.from(new Set([...(c.enabledPlugins ?? []), name])),
|
|
297
|
+
})),
|
|
298
|
+
disablePlugin: (name) => setConfig((c) => ({
|
|
299
|
+
...c,
|
|
300
|
+
enabledPlugins: (c.enabledPlugins ?? []).filter((n) => n !== name),
|
|
301
|
+
})),
|
|
302
|
+
clearHistory: () => {
|
|
303
|
+
setHistory([]);
|
|
304
|
+
setTranscript([]);
|
|
305
|
+
},
|
|
306
|
+
replaceHistory: (messages) => setHistory(messages),
|
|
307
|
+
cancelRun: () => {
|
|
308
|
+
if (abortRef.current) {
|
|
309
|
+
abortRef.current.abort();
|
|
310
|
+
abortRef.current = null;
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
steer: (text) => {
|
|
314
|
+
steerQueueRef.current.push(text);
|
|
315
|
+
},
|
|
316
|
+
setExpanded: (value) => setExpanded(value),
|
|
317
|
+
setInput: (value) => setInput(value),
|
|
318
|
+
pushSystemMessage: pushSystem,
|
|
319
|
+
startLogin: ({ provider, model }) => {
|
|
320
|
+
setPendingLogin({
|
|
321
|
+
step: provider ? (model ? "key" : "model") : "provider",
|
|
322
|
+
provider,
|
|
323
|
+
model,
|
|
324
|
+
});
|
|
325
|
+
},
|
|
326
|
+
}), [pushSystem]);
|
|
327
|
+
const runSlash = useCallback(async (raw) => {
|
|
328
|
+
const parsed = parseSlash(raw);
|
|
329
|
+
if (!parsed)
|
|
330
|
+
return false;
|
|
331
|
+
const cmd = findCommand(parsed.name);
|
|
332
|
+
if (!cmd) {
|
|
333
|
+
pushSystem(`Unknown command "/${parsed.name}". Try /help.`);
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
if (busy && !["cancel", "steer", "jobs", "kill", "attach", "expand", "context"].includes(cmd.name)) {
|
|
337
|
+
pushSystem(`/${cmd.name} is unavailable while a run is active. Try /cancel or /steer.`);
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
// Echo the slash invocation for transcript clarity, but never echo a
|
|
341
|
+
// secret: the login key is masked, while provider/model stay readable.
|
|
342
|
+
const echo = parsed.name === "login" ? maskLoginEcho(parsed.args) : raw;
|
|
343
|
+
pushUser(echo, false);
|
|
344
|
+
try {
|
|
345
|
+
const res = await cmd.run({
|
|
346
|
+
args: parsed.args,
|
|
347
|
+
raw: parsed.raw,
|
|
348
|
+
state: {
|
|
349
|
+
providerId: config.providerId,
|
|
350
|
+
model: config.model,
|
|
351
|
+
theme: config.theme,
|
|
352
|
+
agentMode: config.agentMode,
|
|
353
|
+
history,
|
|
354
|
+
systemPrompt: config.systemPrompt,
|
|
355
|
+
totalCostUsd: totalCost,
|
|
356
|
+
totalInputTokens: totalIn,
|
|
357
|
+
totalOutputTokens: totalOut,
|
|
358
|
+
enabledSkills: config.enabledSkills,
|
|
359
|
+
enabledPlugins: config.enabledPlugins,
|
|
360
|
+
skillsRegistry: config.skillsRegistry,
|
|
361
|
+
reasoningEffort: config.reasoningEffort,
|
|
362
|
+
expanded,
|
|
363
|
+
busy,
|
|
364
|
+
draft: input,
|
|
365
|
+
},
|
|
366
|
+
actions,
|
|
367
|
+
});
|
|
368
|
+
if (res?.message)
|
|
369
|
+
pushSystem(res.message);
|
|
370
|
+
if (res?.picker) {
|
|
371
|
+
const index = Math.max(0, res.picker.items.findIndex((item) => item.value === res.picker.current));
|
|
372
|
+
setPickerIndex(index);
|
|
373
|
+
setPicker(res.picker);
|
|
374
|
+
setInput("");
|
|
375
|
+
}
|
|
376
|
+
if (res?.quit)
|
|
377
|
+
exit();
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
pushSystem(`Command failed: ${err.message}`);
|
|
381
|
+
}
|
|
382
|
+
return true;
|
|
383
|
+
}, [
|
|
384
|
+
actions,
|
|
385
|
+
config.agentMode,
|
|
386
|
+
config.enabledPlugins,
|
|
387
|
+
config.enabledSkills,
|
|
388
|
+
config.model,
|
|
389
|
+
config.providerId,
|
|
390
|
+
config.skillsRegistry,
|
|
391
|
+
config.systemPrompt,
|
|
392
|
+
config.theme,
|
|
393
|
+
busy,
|
|
394
|
+
expanded,
|
|
395
|
+
exit,
|
|
396
|
+
history,
|
|
397
|
+
pushSystem,
|
|
398
|
+
pushUser,
|
|
399
|
+
totalCost,
|
|
400
|
+
totalIn,
|
|
401
|
+
totalOut,
|
|
402
|
+
]);
|
|
403
|
+
const handleSubmit = useCallback(async (raw) => {
|
|
404
|
+
const text = raw.trim();
|
|
405
|
+
if (!text)
|
|
406
|
+
return;
|
|
407
|
+
if (busy) {
|
|
408
|
+
if (text.startsWith("/")) {
|
|
409
|
+
await runSlash(text);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
pushSystem("A run is active. Use /cancel to stop, /steer <text> to guide it, or wait.");
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
setInput("");
|
|
416
|
+
if (text.startsWith("/")) {
|
|
417
|
+
await runSlash(text);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
pushUser(text);
|
|
421
|
+
const provider = getProviderOrThrow(config.providerId);
|
|
422
|
+
let apiKey;
|
|
423
|
+
if (provider.authEnv.length > 0) {
|
|
424
|
+
const lookup = resolveApiKey(provider.id, config.model, provider.authEnv);
|
|
425
|
+
if (lookup.status === "unbound") {
|
|
426
|
+
pushSystem(`Cannot send: your stored ${provider.label} key isn't bound to a model, so it won't be used.\n` +
|
|
427
|
+
`Re-bind it with \`/login ${provider.id} ${config.model} <key>\` (or \`/logout ${provider.id}\` to discard it).`);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (lookup.status === "missing") {
|
|
431
|
+
pushSystem(`Cannot send: no API key for ${provider.label}.\n` +
|
|
432
|
+
`Run \`/login ${provider.id} ${config.model} <key>\` (one key covers every ${provider.id} model), or set ${provider.authEnv[0]}.`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
apiKey = lookup.key;
|
|
436
|
+
}
|
|
437
|
+
idRef.current += 1;
|
|
438
|
+
const bubbleId = idRef.current;
|
|
439
|
+
setStreamingId(bubbleId);
|
|
440
|
+
setStreamingText("");
|
|
441
|
+
setStreamPhase("thinking");
|
|
442
|
+
steerQueueRef.current = [];
|
|
443
|
+
lastStreamFlushRef.current = 0;
|
|
444
|
+
const ctrl = new AbortController();
|
|
445
|
+
abortRef.current = ctrl;
|
|
446
|
+
setBusy(true);
|
|
447
|
+
let receivedDone = false;
|
|
448
|
+
try {
|
|
449
|
+
const maxTurns = config.agentMode ? positiveInt(config.maxAgentTurns, 24) : 1;
|
|
450
|
+
const skillsBlock = buildSkillsPrompt(config.enabledSkills);
|
|
451
|
+
const pluginsBlock = buildPluginsPrompt(config.enabledPlugins);
|
|
452
|
+
const agentBlock = config.agentMode ? AGENT_SYSTEM_PROMPT : "";
|
|
453
|
+
const fullSystem = [config.systemPrompt, skillsBlock, pluginsBlock, agentBlock]
|
|
454
|
+
.filter((part) => part.trim())
|
|
455
|
+
.join("\n\n");
|
|
456
|
+
let compactionNotified = false;
|
|
457
|
+
let lastRawFact = "";
|
|
458
|
+
sawDeltaRef.current = false;
|
|
459
|
+
const protocol = config.agentMode ? resolveToolProtocol(config, config.providerId, config.model) : "legacy";
|
|
460
|
+
for await (const ev of runAgent({
|
|
461
|
+
provider,
|
|
462
|
+
apiKey,
|
|
463
|
+
model: config.model,
|
|
464
|
+
messages: history,
|
|
465
|
+
prompt: text,
|
|
466
|
+
system: fullSystem,
|
|
467
|
+
maxTurns,
|
|
468
|
+
allowTools: config.agentMode,
|
|
469
|
+
tokenBudget: optionalBudget("FASTCODE_TOKEN_BUDGET"),
|
|
470
|
+
costBudgetUsd: optionalBudget("FASTCODE_COST_BUDGET_USD"),
|
|
471
|
+
signal: ctrl.signal,
|
|
472
|
+
cwd: process.cwd(),
|
|
473
|
+
idleTimeoutMs: config.requestIdleTimeoutMs,
|
|
474
|
+
compaction: compactionConfigFromEnv(),
|
|
475
|
+
reasoningEffort: config.reasoningEffort,
|
|
476
|
+
protocol,
|
|
477
|
+
tools: protocol === "native" ? toolSchemas() : undefined,
|
|
478
|
+
onProtocolFallback: (message) => {
|
|
479
|
+
setConfig((current) => withToolCapability(current, config.providerId, config.model, "pseudo"));
|
|
480
|
+
pushSystem(message);
|
|
481
|
+
},
|
|
482
|
+
onNativeOk: () => {
|
|
483
|
+
if (protocol === "native")
|
|
484
|
+
setConfig((current) => withToolCapability(current, config.providerId, config.model, "native"));
|
|
485
|
+
},
|
|
486
|
+
takeSteering: () => {
|
|
487
|
+
const queued = steerQueueRef.current;
|
|
488
|
+
steerQueueRef.current = [];
|
|
489
|
+
return queued;
|
|
490
|
+
},
|
|
491
|
+
onDelta: (delta, full) => {
|
|
492
|
+
if (!sawDeltaRef.current) {
|
|
493
|
+
sawDeltaRef.current = true;
|
|
494
|
+
setStreamPhase("streaming");
|
|
495
|
+
}
|
|
496
|
+
if (!liveStreamingUi)
|
|
497
|
+
return;
|
|
498
|
+
const now = Date.now();
|
|
499
|
+
if (now - lastStreamFlushRef.current < 60)
|
|
500
|
+
return;
|
|
501
|
+
lastStreamFlushRef.current = now;
|
|
502
|
+
setStreamingText(stripToolCalls(full));
|
|
503
|
+
},
|
|
504
|
+
})) {
|
|
505
|
+
switch (ev.type) {
|
|
506
|
+
case "compaction":
|
|
507
|
+
sessionRef.current.append({
|
|
508
|
+
type: "compacted",
|
|
509
|
+
summaryRef: ev.summaryFile,
|
|
510
|
+
source: ev.source,
|
|
511
|
+
beforeTokens: ev.beforeTokens,
|
|
512
|
+
afterTokens: ev.afterTokens,
|
|
513
|
+
droppedMessages: ev.droppedMessages,
|
|
514
|
+
keepMessages: ev.keptMessages,
|
|
515
|
+
});
|
|
516
|
+
if (!compactionNotified) {
|
|
517
|
+
compactionNotified = true;
|
|
518
|
+
pushSystem(`[context compacted] ${ev.beforeTokens} → ${ev.afterTokens} tokens (${ev.reason}); recent turns kept.` +
|
|
519
|
+
(ev.summaryFile ? `\nSummary: ${ev.summaryFile}` : ""));
|
|
520
|
+
}
|
|
521
|
+
break;
|
|
522
|
+
case "assistant_raw":
|
|
523
|
+
lastRawFact = ev.text;
|
|
524
|
+
sessionRef.current.append({ type: "assistant_raw", content: ev.text, toolCalls: ev.toolCalls });
|
|
525
|
+
break;
|
|
526
|
+
case "assistant_text":
|
|
527
|
+
pushSystem(ev.text);
|
|
528
|
+
break;
|
|
529
|
+
case "tool_call":
|
|
530
|
+
sessionRef.current.append({ type: "tool_call", id: ev.id, tool: ev.call.tool, args: ev.call, native: Boolean(ev.native) });
|
|
531
|
+
setStreamPhase("tool");
|
|
532
|
+
setAgentActivity(`Coding: ${ev.call.tool}`);
|
|
533
|
+
pushSystem(`Running tool: \`${ev.call.tool}\`\n\n\`\`\`json\n${JSON.stringify(ev.call, null, 2)}\n\`\`\``);
|
|
534
|
+
break;
|
|
535
|
+
case "tool_result": {
|
|
536
|
+
sessionRef.current.append({
|
|
537
|
+
type: "tool_result",
|
|
538
|
+
id: ev.id,
|
|
539
|
+
tool: ev.call.tool,
|
|
540
|
+
ok: ev.ok,
|
|
541
|
+
output: ev.output,
|
|
542
|
+
sideEffects: ev.sideEffects,
|
|
543
|
+
});
|
|
544
|
+
const collapsed = stableUi && !expanded;
|
|
545
|
+
const body = collapsed ? collapseToolOutput(ev.output) : ev.output;
|
|
546
|
+
const hint = collapsed ? "\n\n_(output collapsed; /expand to show full output)_" : "";
|
|
547
|
+
pushSystem(`Tool ${ev.ok ? "completed" : "failed"}: \`${ev.call.tool}\`\n\n\`\`\`\n${body}\n\`\`\`${hint}`);
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
case "usage":
|
|
551
|
+
if (ev.usage.inputTokens)
|
|
552
|
+
setTotalIn((n) => n + (ev.usage.inputTokens ?? 0));
|
|
553
|
+
if (ev.usage.outputTokens)
|
|
554
|
+
setTotalOut((n) => n + (ev.usage.outputTokens ?? 0));
|
|
555
|
+
if (ev.usage.costUsd)
|
|
556
|
+
setTotalCost((n) => n + (ev.usage.costUsd ?? 0));
|
|
557
|
+
break;
|
|
558
|
+
case "loop_detected":
|
|
559
|
+
pushSystem(`Loop detected: \`${ev.fingerprint.split(":")[0]}\` repeated ${ev.occurrences} times. Asking the model to change approach.`);
|
|
560
|
+
break;
|
|
561
|
+
case "notice":
|
|
562
|
+
pushSystem(ev.text);
|
|
563
|
+
break;
|
|
564
|
+
case "checkpoint":
|
|
565
|
+
sessionRef.current.append({
|
|
566
|
+
type: "checkpoint",
|
|
567
|
+
turn: ev.turn,
|
|
568
|
+
cwd: ev.cwd,
|
|
569
|
+
providerId: ev.providerId,
|
|
570
|
+
model: ev.model,
|
|
571
|
+
activeFiles: ev.activeFiles,
|
|
572
|
+
lastFactSeq: sessionRef.current.count(),
|
|
573
|
+
});
|
|
574
|
+
break;
|
|
575
|
+
case "error":
|
|
576
|
+
if (ev.kind === "aborted")
|
|
577
|
+
pushSystem("Cancelled.");
|
|
578
|
+
else if (ev.kind === "stream_error")
|
|
579
|
+
pushSystem(streamErrorMessage(ev.message, provider, config.model));
|
|
580
|
+
else
|
|
581
|
+
pushSystem(ev.message);
|
|
582
|
+
break;
|
|
583
|
+
case "done":
|
|
584
|
+
receivedDone = true;
|
|
585
|
+
setHistory(ev.history);
|
|
586
|
+
if (ev.finalText) {
|
|
587
|
+
idRef.current += 1;
|
|
588
|
+
const finalId = idRef.current;
|
|
589
|
+
if (ev.finalText !== lastRawFact)
|
|
590
|
+
sessionRef.current.append({ type: "assistant_raw", content: ev.finalText });
|
|
591
|
+
setTranscript((prev) => [
|
|
592
|
+
...prev,
|
|
593
|
+
{ id: finalId, role: "assistant", content: ev.finalText, markdown: true },
|
|
594
|
+
]);
|
|
595
|
+
}
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
catch (err) {
|
|
601
|
+
if (!receivedDone)
|
|
602
|
+
setHistory([...history, { role: "user", content: text }]);
|
|
603
|
+
pushSystem(`Error: ${String(err?.message ?? err)}`);
|
|
604
|
+
}
|
|
605
|
+
finally {
|
|
606
|
+
setStreamingId(null);
|
|
607
|
+
setStreamingText("");
|
|
608
|
+
setAgentActivity(null);
|
|
609
|
+
setStreamPhase("thinking");
|
|
610
|
+
setBusy(false);
|
|
611
|
+
abortRef.current = null;
|
|
612
|
+
}
|
|
613
|
+
}, [
|
|
614
|
+
busy,
|
|
615
|
+
config.agentMode,
|
|
616
|
+
config.enabledSkills,
|
|
617
|
+
config.enabledPlugins,
|
|
618
|
+
config.model,
|
|
619
|
+
config.providerId,
|
|
620
|
+
config.systemPrompt,
|
|
621
|
+
history,
|
|
622
|
+
pushSystem,
|
|
623
|
+
pushUser,
|
|
624
|
+
runSlash,
|
|
625
|
+
]);
|
|
626
|
+
// Guided `/login`: provider → model → key. Text prompts only; the key step
|
|
627
|
+
// is masked. `keep` re-binds an existing unbound (legacy) key to a model.
|
|
628
|
+
const handleWizardSubmit = useCallback(async (raw) => {
|
|
629
|
+
const value = raw.trim();
|
|
630
|
+
setInput("");
|
|
631
|
+
if (!value || !pendingLogin)
|
|
632
|
+
return;
|
|
633
|
+
if (pendingLogin.step === "provider") {
|
|
634
|
+
const provider = getProvider(value);
|
|
635
|
+
if (!provider) {
|
|
636
|
+
pushSystem(`Unknown provider "${value}". Available: ${PROVIDERS.map((p) => p.id).join(", ")}`);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (provider.authEnv.length === 0) {
|
|
640
|
+
pushSystem(`${provider.label} doesn't require an API key. Nothing to save.`);
|
|
641
|
+
setPendingLogin(null);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
setPendingLogin({ step: "model", provider: provider.id });
|
|
645
|
+
const models = await provider.listModels(undefined).catch(() => []);
|
|
646
|
+
const names = models.slice(0, 8).map((m) => m.id).join(", ");
|
|
647
|
+
pushSystem(`Provider: ${provider.label} (${provider.id}).\n` +
|
|
648
|
+
`Type the model id to bind this key to — e.g. \`${provider.defaultModel}\`.` +
|
|
649
|
+
(names ? `\n known models: ${names}` : ""));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (pendingLogin.step === "model") {
|
|
653
|
+
const provider = getProvider(pendingLogin.provider);
|
|
654
|
+
if (!provider) {
|
|
655
|
+
setPendingLogin(null);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
setPendingLogin({ step: "key", provider: provider.id, model: value });
|
|
659
|
+
pushSystem(`Saved from model: ${provider.id} · ${value}.\n` +
|
|
660
|
+
`This key will be reused by every ${provider.label} model.\n` +
|
|
661
|
+
`Paste the API key (input is masked).` +
|
|
662
|
+
(getLegacy(provider.id) ? `\nOr type \`keep\` to re-bind the stored ${provider.label} key.` : ""));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const provider = getProvider(pendingLogin.provider);
|
|
666
|
+
if (!provider) {
|
|
667
|
+
setPendingLogin(null);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
let key = value;
|
|
671
|
+
if (value.toLowerCase() === "keep") {
|
|
672
|
+
const legacy = getLegacy(provider.id);
|
|
673
|
+
if (!legacy) {
|
|
674
|
+
pushSystem(`No stored ${provider.label} key to reuse — paste the key instead.`);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
key = legacy;
|
|
678
|
+
}
|
|
679
|
+
pushSystem(saveScopedKey(provider.id, provider.label, pendingLogin.model, key));
|
|
680
|
+
setPendingLogin(null);
|
|
681
|
+
}, [pendingLogin, pushSystem]);
|
|
682
|
+
useEffect(() => {
|
|
683
|
+
if (!initialPrompt || initialPromptSubmittedRef.current)
|
|
684
|
+
return;
|
|
685
|
+
initialPromptSubmittedRef.current = true;
|
|
686
|
+
const t = setTimeout(() => void handleSubmit(initialPrompt), 50);
|
|
687
|
+
return () => clearTimeout(t);
|
|
688
|
+
}, [handleSubmit, initialPrompt]);
|
|
689
|
+
useEffect(() => () => {
|
|
690
|
+
shutdownShells();
|
|
691
|
+
if (sessionRef.current)
|
|
692
|
+
sessionRef.current.close();
|
|
693
|
+
}, []);
|
|
694
|
+
if (config.agentMode && !trusted) {
|
|
695
|
+
const decide = (allow) => {
|
|
696
|
+
if (allow) {
|
|
697
|
+
trustProject(process.cwd());
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
setConfig((c) => ({ ...c, agentMode: false }));
|
|
701
|
+
pushSystem("Trust declined: agent tools are off for this session (chat only).");
|
|
702
|
+
}
|
|
703
|
+
setTrustEpoch((n) => n + 1);
|
|
704
|
+
};
|
|
705
|
+
return _jsx(TrustGate, { theme: theme, cwd: process.cwd(), onDecision: decide });
|
|
706
|
+
}
|
|
707
|
+
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"] })] })] }));
|
|
708
|
+
};
|