@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.
Files changed (303) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +67 -16
  3. package/dist/agent/approval-json.d.ts +43 -0
  4. package/dist/agent/approval-json.js +69 -0
  5. package/dist/agent/approval-native.d.ts +3 -0
  6. package/dist/agent/approval-native.js +165 -0
  7. package/dist/agent/approval-store.d.ts +16 -0
  8. package/dist/agent/approval-store.js +21 -0
  9. package/dist/agent/approval.d.ts +83 -0
  10. package/dist/agent/approval.js +42 -0
  11. package/dist/agent/compaction.d.ts +41 -0
  12. package/dist/agent/compaction.js +270 -0
  13. package/dist/agent/context.d.ts +29 -0
  14. package/dist/agent/context.js +62 -0
  15. package/dist/agent/guards.d.ts +24 -16
  16. package/dist/agent/guards.js +1167 -18
  17. package/dist/agent/ignore.d.ts +11 -0
  18. package/dist/agent/ignore.js +129 -0
  19. package/dist/agent/policy/defaults.d.ts +16 -0
  20. package/dist/agent/policy/defaults.js +17 -0
  21. package/dist/agent/policy/egress.d.ts +32 -0
  22. package/dist/agent/policy/egress.js +280 -0
  23. package/dist/agent/process-tree.d.ts +22 -0
  24. package/dist/agent/process-tree.js +102 -0
  25. package/dist/agent/protocol-mode.d.ts +6 -0
  26. package/dist/agent/protocol-mode.js +51 -0
  27. package/dist/agent/protocol.d.ts +20 -1
  28. package/dist/agent/protocol.js +169 -39
  29. package/dist/agent/registry.d.ts +19 -0
  30. package/dist/agent/registry.js +247 -0
  31. package/dist/agent/runner.d.ts +127 -0
  32. package/dist/agent/runner.js +391 -0
  33. package/dist/agent/shell-ast.d.ts +11 -0
  34. package/dist/agent/shell-ast.js +204 -0
  35. package/dist/agent/shell-session.d.ts +51 -0
  36. package/dist/agent/shell-session.js +611 -0
  37. package/dist/agent/stream.d.ts +29 -0
  38. package/dist/agent/stream.js +103 -0
  39. package/dist/agent/tool-schemas.d.ts +2 -0
  40. package/dist/agent/tool-schemas.js +82 -0
  41. package/dist/agent/tools/append_file.d.ts +2 -0
  42. package/dist/agent/tools/append_file.js +28 -0
  43. package/dist/agent/tools/apply_patch.d.ts +10 -0
  44. package/dist/agent/tools/apply_patch.js +92 -0
  45. package/dist/agent/tools/copy_file.d.ts +2 -0
  46. package/dist/agent/tools/copy_file.js +53 -0
  47. package/dist/agent/tools/delete_file.d.ts +2 -0
  48. package/dist/agent/tools/delete_file.js +43 -0
  49. package/dist/agent/tools/grep.d.ts +2 -0
  50. package/dist/agent/tools/grep.js +54 -0
  51. package/dist/agent/tools/index.d.ts +2 -0
  52. package/dist/agent/tools/index.js +32 -0
  53. package/dist/agent/tools/list_files.d.ts +2 -0
  54. package/dist/agent/tools/list_files.js +17 -0
  55. package/dist/agent/tools/make_dir.d.ts +2 -0
  56. package/dist/agent/tools/make_dir.js +26 -0
  57. package/dist/agent/tools/move_path.d.ts +9 -0
  58. package/dist/agent/tools/move_path.js +73 -0
  59. package/dist/agent/tools/paths.d.ts +7 -0
  60. package/dist/agent/tools/paths.js +84 -0
  61. package/dist/agent/tools/pwd.d.ts +2 -0
  62. package/dist/agent/tools/pwd.js +10 -0
  63. package/dist/agent/tools/read_file.d.ts +2 -0
  64. package/dist/agent/tools/read_file.js +81 -0
  65. package/dist/agent/tools/read_many.d.ts +2 -0
  66. package/dist/agent/tools/read_many.js +43 -0
  67. package/dist/agent/tools/readers.d.ts +58 -0
  68. package/dist/agent/tools/readers.js +426 -0
  69. package/dist/agent/tools/replace_in_file.d.ts +2 -0
  70. package/dist/agent/tools/replace_in_file.js +63 -0
  71. package/dist/agent/tools/shell.d.ts +2 -0
  72. package/dist/agent/tools/shell.js +48 -0
  73. package/dist/agent/tools/stat_path.d.ts +2 -0
  74. package/dist/agent/tools/stat_path.js +31 -0
  75. package/dist/agent/tools/types.d.ts +65 -0
  76. package/dist/agent/tools/types.js +1 -0
  77. package/dist/agent/tools/validate.d.ts +19 -0
  78. package/dist/agent/tools/validate.js +92 -0
  79. package/dist/agent/tools/write_file.d.ts +2 -0
  80. package/dist/agent/tools/write_file.js +28 -0
  81. package/dist/agent/tools.d.ts +3 -1
  82. package/dist/agent/tools.js +2 -377
  83. package/dist/app.d.ts +2 -1
  84. package/dist/app.js +846 -173
  85. package/dist/cli-main.js +363 -0
  86. package/dist/cli.js +444 -333
  87. package/dist/commands/about.js +1 -1
  88. package/dist/commands/attach.d.ts +9 -0
  89. package/dist/commands/attach.js +15 -0
  90. package/dist/commands/bg.d.ts +7 -0
  91. package/dist/commands/bg.js +9 -0
  92. package/dist/commands/cancel.d.ts +10 -0
  93. package/dist/commands/cancel.js +10 -0
  94. package/dist/commands/clear.d.ts +1 -0
  95. package/dist/commands/clear.js +1 -1
  96. package/dist/commands/compact.d.ts +22 -0
  97. package/dist/commands/compact.js +34 -0
  98. package/dist/commands/context.d.ts +7 -0
  99. package/dist/commands/context.js +25 -0
  100. package/dist/commands/edit.d.ts +10 -0
  101. package/dist/commands/edit.js +36 -0
  102. package/dist/commands/effort.d.ts +25 -0
  103. package/dist/commands/effort.js +48 -0
  104. package/dist/commands/expand.d.ts +10 -0
  105. package/dist/commands/expand.js +8 -0
  106. package/dist/commands/index.js +24 -0
  107. package/dist/commands/jobs.d.ts +7 -0
  108. package/dist/commands/jobs.js +17 -0
  109. package/dist/commands/kill.d.ts +7 -0
  110. package/dist/commands/kill.js +13 -0
  111. package/dist/commands/login.d.ts +6 -4
  112. package/dist/commands/login.js +24 -22
  113. package/dist/commands/logout.js +8 -16
  114. package/dist/commands/model.d.ts +1 -0
  115. package/dist/commands/model.js +10 -22
  116. package/dist/commands/provider.d.ts +1 -2
  117. package/dist/commands/provider.js +4 -11
  118. package/dist/commands/skills.js +34 -4
  119. package/dist/commands/steer.d.ts +11 -0
  120. package/dist/commands/steer.js +13 -0
  121. package/dist/commands/threads.d.ts +9 -0
  122. package/dist/commands/threads.js +26 -0
  123. package/dist/commands/whoami.js +6 -5
  124. package/dist/config/store.d.ts +53 -12
  125. package/dist/config/store.js +163 -73
  126. package/dist/config/trust.d.ts +30 -8
  127. package/dist/config/trust.js +139 -14
  128. package/dist/headless.d.ts +7 -0
  129. package/dist/headless.js +213 -0
  130. package/dist/index.js +19 -3
  131. package/dist/lib/debug.d.ts +2 -0
  132. package/dist/lib/debug.js +22 -0
  133. package/dist/lib/diagnostics.d.ts +48 -4
  134. package/dist/lib/diagnostics.js +90 -8
  135. package/dist/lib/highlight-subset.d.ts +21 -0
  136. package/dist/lib/highlight-subset.js +88 -0
  137. package/dist/lib/http.d.ts +11 -1
  138. package/dist/lib/http.js +62 -4
  139. package/dist/lib/lifecycle.d.ts +5 -0
  140. package/dist/lib/lifecycle.js +62 -0
  141. package/dist/lib/markdown.js +22 -4
  142. package/dist/lib/model-meta.d.ts +19 -0
  143. package/dist/lib/model-meta.js +113 -0
  144. package/dist/lib/native-tools.d.ts +12 -0
  145. package/dist/lib/native-tools.js +21 -0
  146. package/dist/lib/net.d.ts +33 -0
  147. package/dist/lib/net.js +168 -0
  148. package/dist/patch/apply.d.ts +15 -0
  149. package/dist/patch/apply.js +172 -0
  150. package/dist/patch/parse.d.ts +29 -0
  151. package/dist/patch/parse.js +112 -0
  152. package/dist/patch/plan.d.ts +49 -0
  153. package/dist/patch/plan.js +273 -0
  154. package/dist/providers/anthropic.d.ts +1 -0
  155. package/dist/providers/anthropic.js +12 -0
  156. package/dist/providers/custom.d.ts +9 -0
  157. package/dist/providers/deepseek.d.ts +10 -0
  158. package/dist/providers/deepseek.js +3 -2
  159. package/dist/providers/google.d.ts +9 -0
  160. package/dist/providers/groq.d.ts +10 -0
  161. package/dist/providers/groq.js +2 -1
  162. package/dist/providers/index.d.ts +10 -201
  163. package/dist/providers/index.js +30 -0
  164. package/dist/providers/mistral.d.ts +10 -0
  165. package/dist/providers/mistral.js +2 -1
  166. package/dist/providers/moonshot.d.ts +10 -0
  167. package/dist/providers/moonshot.js +2 -1
  168. package/dist/providers/ollama.d.ts +9 -0
  169. package/dist/providers/openai.d.ts +10 -0
  170. package/dist/providers/openai.js +2 -1
  171. package/dist/providers/openaiCompatible.d.ts +33 -2
  172. package/dist/providers/openaiCompatible.js +78 -16
  173. package/dist/providers/openrouter.d.ts +10 -0
  174. package/dist/providers/openrouter.js +2 -1
  175. package/dist/providers/qwen.d.ts +10 -0
  176. package/dist/providers/qwen.js +2 -1
  177. package/dist/providers/spec.d.ts +12 -0
  178. package/dist/providers/spec.js +29 -0
  179. package/dist/providers/xai.d.ts +10 -0
  180. package/dist/providers/xai.js +2 -1
  181. package/dist/providers/zhipu.d.ts +10 -0
  182. package/dist/providers/zhipu.js +2 -1
  183. package/dist/session/log.d.ts +49 -14
  184. package/dist/session/log.js +236 -11
  185. package/dist/session/projection.d.ts +76 -0
  186. package/dist/session/projection.js +526 -0
  187. package/dist/skills/manager.d.ts +15 -0
  188. package/dist/skills/manager.js +135 -18
  189. package/dist/types.d.ts +75 -1
  190. package/dist/ui/Message.d.ts +1 -7
  191. package/dist/ui/Message.js +13 -2
  192. package/dist/ui/Picker.d.ts +8 -0
  193. package/dist/ui/Picker.js +24 -0
  194. package/dist/ui/Prompt.d.ts +1 -2
  195. package/dist/ui/Prompt.js +2 -2
  196. package/dist/ui/Spinner.js +11 -5
  197. package/dist/ui/StatusBar.d.ts +2 -1
  198. package/dist/ui/StatusBar.js +6 -5
  199. package/package.json +3 -2
  200. package/src/agent/approval-json.ts +103 -0
  201. package/src/agent/approval-native.ts +168 -0
  202. package/src/agent/approval-store.ts +38 -0
  203. package/src/agent/approval.ts +94 -0
  204. package/src/agent/compaction.ts +321 -0
  205. package/src/agent/context.ts +97 -0
  206. package/src/agent/guards.ts +1239 -19
  207. package/src/agent/ignore.ts +147 -0
  208. package/src/agent/policy/defaults.ts +34 -0
  209. package/src/agent/policy/egress.ts +335 -0
  210. package/src/agent/process-tree.ts +114 -0
  211. package/src/agent/protocol-mode.ts +59 -0
  212. package/src/agent/protocol.ts +191 -40
  213. package/src/agent/registry.ts +270 -0
  214. package/src/agent/runner.ts +486 -0
  215. package/src/agent/shell-ast.ts +227 -0
  216. package/src/agent/shell-session.ts +673 -0
  217. package/src/agent/stream.ts +143 -0
  218. package/src/agent/tool-schemas.ts +86 -0
  219. package/src/agent/tools/append_file.ts +30 -0
  220. package/src/agent/tools/apply_patch.ts +95 -0
  221. package/src/agent/tools/copy_file.ts +56 -0
  222. package/src/agent/tools/delete_file.ts +45 -0
  223. package/src/agent/tools/grep.ts +57 -0
  224. package/src/agent/tools/index.ts +34 -0
  225. package/src/agent/tools/list_files.ts +19 -0
  226. package/src/agent/tools/make_dir.ts +28 -0
  227. package/src/agent/tools/move_path.ts +83 -0
  228. package/src/agent/tools/paths.ts +92 -0
  229. package/src/agent/tools/pwd.ts +12 -0
  230. package/src/agent/tools/read_file.ts +84 -0
  231. package/src/agent/tools/read_many.ts +45 -0
  232. package/src/agent/tools/readers.ts +491 -0
  233. package/src/agent/tools/replace_in_file.ts +66 -0
  234. package/src/agent/tools/shell.ts +53 -0
  235. package/src/agent/tools/stat_path.ts +33 -0
  236. package/src/agent/tools/types.ts +84 -0
  237. package/src/agent/tools/validate.ts +104 -0
  238. package/src/agent/tools/write_file.ts +30 -0
  239. package/src/agent/tools.ts +3 -377
  240. package/src/app.ts +853 -170
  241. package/src/{cli.ts → cli-main.ts} +64 -9
  242. package/src/commands/about.ts +1 -1
  243. package/src/commands/attach.ts +16 -0
  244. package/src/commands/bg.ts +9 -0
  245. package/src/commands/cancel.ts +10 -0
  246. package/src/commands/clear.ts +1 -1
  247. package/src/commands/compact.ts +35 -0
  248. package/src/commands/context.ts +26 -0
  249. package/src/commands/edit.ts +37 -0
  250. package/src/commands/effort.ts +49 -0
  251. package/src/commands/expand.ts +8 -0
  252. package/src/commands/index.ts +24 -0
  253. package/src/commands/jobs.ts +18 -0
  254. package/src/commands/kill.ts +14 -0
  255. package/src/commands/login.ts +24 -22
  256. package/src/commands/logout.ts +8 -16
  257. package/src/commands/model.ts +10 -22
  258. package/src/commands/provider.ts +4 -11
  259. package/src/commands/skills.ts +34 -4
  260. package/src/commands/steer.ts +13 -0
  261. package/src/commands/threads.ts +28 -0
  262. package/src/commands/whoami.ts +6 -5
  263. package/src/config/store.ts +168 -77
  264. package/src/config/trust.ts +181 -22
  265. package/src/headless.ts +225 -0
  266. package/src/index.ts +19 -3
  267. package/src/lib/debug.ts +24 -0
  268. package/src/lib/diagnostics.ts +90 -8
  269. package/src/lib/highlight-subset.ts +93 -0
  270. package/src/lib/http.ts +66 -4
  271. package/src/lib/lifecycle.ts +69 -0
  272. package/src/lib/markdown.ts +22 -4
  273. package/src/lib/model-meta.ts +138 -0
  274. package/src/lib/native-tools.ts +32 -0
  275. package/src/lib/net.ts +204 -0
  276. package/src/patch/apply.ts +194 -0
  277. package/src/patch/parse.ts +131 -0
  278. package/src/patch/plan.ts +317 -0
  279. package/src/providers/anthropic.ts +13 -1
  280. package/src/providers/deepseek.ts +3 -2
  281. package/src/providers/groq.ts +2 -1
  282. package/src/providers/index.ts +32 -1
  283. package/src/providers/mistral.ts +2 -1
  284. package/src/providers/moonshot.ts +2 -1
  285. package/src/providers/openai.ts +2 -1
  286. package/src/providers/openaiCompatible.ts +104 -19
  287. package/src/providers/openrouter.ts +2 -1
  288. package/src/providers/qwen.ts +2 -1
  289. package/src/providers/spec.ts +42 -0
  290. package/src/providers/xai.ts +2 -1
  291. package/src/providers/zhipu.ts +2 -1
  292. package/src/session/log.ts +311 -29
  293. package/src/session/projection.ts +621 -0
  294. package/src/skills/manager.ts +140 -18
  295. package/src/types/highlight-shims.d.ts +14 -0
  296. package/src/types.ts +79 -2
  297. package/src/ui/Message.ts +13 -2
  298. package/src/ui/Picker.ts +24 -0
  299. package/src/ui/Prompt.ts +2 -2
  300. package/src/ui/Spinner.ts +11 -5
  301. package/src/ui/StatusBar.ts +6 -5
  302. package/tsconfig.json +1 -0
  303. /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, formatToolResults, parseToolCalls, stripToolCalls, } from "./agent/protocol.js";
15
- import { runAgentTool } from "./agent/tools.js";
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 { getProvider, getProviderOrThrow, PROVIDERS } from "./providers/index.js";
24
- import { loadConfig, saveConfig, resolveApiKey, getLegacy, } from "./config/store.js";
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, loadSessionMessages, openSessionLog } from "./session/log.js";
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
- * Mask the secret part of a `/login` invocation in the transcript:
35
- * `/login prov` → unchanged, `/login prov <key>` → key masked,
36
- * `/login prov model <key>` → provider/model shown, key masked.
37
- */
38
- function maskLoginEcho(args) {
39
- const parts = ["/login", args[0] ?? ""];
40
- if (args.length === 2) {
41
- parts.push("********");
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
- else if (args.length >= 3) {
44
- parts.push(args[1]);
45
- for (let i = 2; i < args.length; i += 1)
46
- parts.push("********");
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
- return parts.filter((p) => p !== "").join(" ");
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 restored = resume ? loadSessionMessages(resume) : [];
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: restored.length > 0
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 [trusted, setTrusted] = useState(() => isTrusted(process.cwd()));
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 = pendingLogin
104
- ? pendingLogin.step === "provider"
105
- ? "Provider id (e.g. deepseek) · Esc to cancel"
106
- : pendingLogin.step === "model"
107
- ? "Model id (e.g. deepseek-chat) · Esc to cancel"
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
- ? "Paste API key (masked), or type 'keep' · Esc to cancel"
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 (key.escape) {
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("API key entry cancelled.");
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
- exit();
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: "message", role: "system", content: text });
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({ type: "message", role: "user", content: text });
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
- pushSystemMessage: pushSystem,
189
- startLogin: ({ provider, model }) => {
190
- setPendingLogin({
191
- step: provider ? (model ? "key" : "model") : "provider",
192
- provider,
193
- model,
194
- });
701
+ replaceHistory: (messages) => setHistory(messages),
702
+ cancelRun: () => {
703
+ if (abortRef.current) {
704
+ abortRef.current.abort();
705
+ abortRef.current = null;
706
+ }
195
707
  },
196
- }), [pushSystem]);
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
- exit();
788
+ void quitCleanly();
234
789
  }
235
790
  catch (err) {
236
- pushSystem(`Command failed: ${err.message}`);
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
- exit,
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 || busy)
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} · ${config.model}.\n` +
278
- `Run \`/login ${provider.id} ${config.model} <key>\` for this model, or set ${provider.authEnv[0]} (provider-wide).`);
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
- let workingHistory = nextHistory;
294
- let finalText = "";
295
- for (let turn = 0; turn < (config.agentMode ? 8 : 1); turn += 1) {
296
- const skillsBlock = buildSkillsPrompt(config.enabledSkills);
297
- const pluginsBlock = buildPluginsPrompt(config.enabledPlugins);
298
- const agentBlock = config.agentMode ? AGENT_SYSTEM_PROMPT : "";
299
- const fullSystem = [config.systemPrompt, skillsBlock, pluginsBlock, agentBlock]
300
- .filter((part) => part.trim())
301
- .join("\n\n");
302
- let acc = "";
303
- for await (const chunk of provider.stream({
304
- model: config.model,
305
- messages: workingHistory,
306
- system: fullSystem,
307
- signal: ctrl.signal,
308
- }, apiKey)) {
309
- if (chunk.delta) {
310
- acc += chunk.delta;
311
- if (liveStreamingUi)
312
- setStreamingText(stripToolCalls(acc));
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 (chunk.done) {
315
- if (chunk.usage?.inputTokens)
316
- setTotalIn((n) => n + (chunk.usage.inputTokens ?? 0));
317
- if (chunk.usage?.outputTokens)
318
- setTotalOut((n) => n + (chunk.usage.outputTokens ?? 0));
319
- if (chunk.usage?.costUsd)
320
- setTotalCost((n) => n + (chunk.usage.costUsd ?? 0));
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
- const message = String(err?.message ?? err);
362
- if (err?.name === "AbortError") {
363
- pushSystem("Cancelled.");
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 (!value || !pendingLogin)
1089
+ if (!pendingLogin)
403
1090
  return;
404
- if (pendingLogin.step === "provider") {
405
- const provider = getProvider(value);
406
- if (!provider) {
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
- setPendingLogin({ step: "model", provider: provider.id });
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 === "model") {
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 — paste the key instead.`);
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
- pushSystem(saveScopedKey(provider.id, provider.label, pendingLogin.model, key));
449
- setPendingLogin(null);
450
- }, [pendingLogin, pushSystem]);
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
- setTrusted(true);
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: true, streaming: true }) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [busy ? (_jsx(Box, { marginBottom: 1, children: _jsx(Spinner, { theme: theme, label: agentActivity ?? (streamingId !== null ? "Streaming" : "Thinking"), stable: stableUi && !agentActivity }) })) : null, _jsx(Prompt, { theme: theme, value: input, onChange: setInput, onSubmit: pendingLogin ? handleWizardSubmit : handleSubmit, disabled: busy, stable: stableUi, 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 }), _jsxs(Text, { color: theme.muted, children: [COMMANDS.length, " commands \u00B7 ", config.enabledSkills.length, " skill(s) active"] })] })] }));
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
  };