@datename/fastcode-termux 0.1.6-beta → 0.1.8-beta.0

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