@prestyj/cli 4.3.238 → 4.4.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 (146) hide show
  1. package/README.md +4 -2
  2. package/dist/cli/auth.d.ts.map +1 -1
  3. package/dist/cli/auth.js +17 -2
  4. package/dist/cli/auth.js.map +1 -1
  5. package/dist/cli/shared.d.ts.map +1 -1
  6. package/dist/cli/shared.js +27 -5
  7. package/dist/cli/shared.js.map +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +5 -4
  10. package/dist/cli.js.map +1 -1
  11. package/dist/core/agent-session.d.ts.map +1 -1
  12. package/dist/core/agent-session.js +26 -14
  13. package/dist/core/agent-session.js.map +1 -1
  14. package/dist/core/auth-storage.d.ts +18 -0
  15. package/dist/core/auth-storage.d.ts.map +1 -1
  16. package/dist/core/auth-storage.js +62 -8
  17. package/dist/core/auth-storage.js.map +1 -1
  18. package/dist/core/auto-update.js +2 -2
  19. package/dist/core/auto-update.js.map +1 -1
  20. package/dist/core/compaction/compactor.d.ts.map +1 -1
  21. package/dist/core/compaction/compactor.js +4 -0
  22. package/dist/core/compaction/compactor.js.map +1 -1
  23. package/dist/core/model-registry.d.ts +21 -0
  24. package/dist/core/model-registry.d.ts.map +1 -1
  25. package/dist/core/model-registry.js +58 -15
  26. package/dist/core/model-registry.js.map +1 -1
  27. package/dist/core/model-registry.test.js +13 -0
  28. package/dist/core/model-registry.test.js.map +1 -1
  29. package/dist/core/oauth/gemini.d.ts.map +1 -1
  30. package/dist/core/oauth/gemini.js +14 -5
  31. package/dist/core/oauth/gemini.js.map +1 -1
  32. package/dist/core/oauth/kimi.d.ts +46 -0
  33. package/dist/core/oauth/kimi.d.ts.map +1 -0
  34. package/dist/core/oauth/kimi.js +278 -0
  35. package/dist/core/oauth/kimi.js.map +1 -0
  36. package/dist/interactive.d.ts.map +1 -1
  37. package/dist/interactive.js +22 -2
  38. package/dist/interactive.js.map +1 -1
  39. package/dist/system-prompt.d.ts.map +1 -1
  40. package/dist/system-prompt.js +35 -16
  41. package/dist/system-prompt.js.map +1 -1
  42. package/dist/system-prompt.test.js +8 -1
  43. package/dist/system-prompt.test.js.map +1 -1
  44. package/dist/tools/goals.d.ts +1 -1
  45. package/dist/tools/index.d.ts +10 -0
  46. package/dist/tools/index.d.ts.map +1 -1
  47. package/dist/tools/index.js +9 -2
  48. package/dist/tools/index.js.map +1 -1
  49. package/dist/tools/prompt-hints.d.ts +23 -4
  50. package/dist/tools/prompt-hints.d.ts.map +1 -1
  51. package/dist/tools/prompt-hints.js +35 -10
  52. package/dist/tools/prompt-hints.js.map +1 -1
  53. package/dist/tools/read.d.ts +6 -1
  54. package/dist/tools/read.d.ts.map +1 -1
  55. package/dist/tools/read.js +70 -4
  56. package/dist/tools/read.js.map +1 -1
  57. package/dist/ui/App.d.ts +3 -0
  58. package/dist/ui/App.d.ts.map +1 -1
  59. package/dist/ui/App.js +164 -24
  60. package/dist/ui/App.js.map +1 -1
  61. package/dist/ui/app-items.d.ts +3 -1
  62. package/dist/ui/app-items.d.ts.map +1 -1
  63. package/dist/ui/app-items.js +13 -4
  64. package/dist/ui/app-items.js.map +1 -1
  65. package/dist/ui/chat-layout-pinning.test.js +1 -1
  66. package/dist/ui/chat-layout-pinning.test.js.map +1 -1
  67. package/dist/ui/components/ActivityIndicator.d.ts +7 -0
  68. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  69. package/dist/ui/components/ActivityIndicator.js +38 -12
  70. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  71. package/dist/ui/components/AssistantMessage.test.js +1 -1
  72. package/dist/ui/components/AssistantMessage.test.js.map +1 -1
  73. package/dist/ui/components/ChatInputStack.d.ts +2 -0
  74. package/dist/ui/components/ChatInputStack.d.ts.map +1 -1
  75. package/dist/ui/components/ChatInputStack.js.map +1 -1
  76. package/dist/ui/components/ChatStatusRow.d.ts +2 -0
  77. package/dist/ui/components/ChatStatusRow.d.ts.map +1 -1
  78. package/dist/ui/components/ChatStatusRow.js +40 -1
  79. package/dist/ui/components/ChatStatusRow.js.map +1 -1
  80. package/dist/ui/components/InputArea.d.ts.map +1 -1
  81. package/dist/ui/components/InputArea.js +17 -7
  82. package/dist/ui/components/InputArea.js.map +1 -1
  83. package/dist/ui/components/MessageResponse.d.ts +7 -1
  84. package/dist/ui/components/MessageResponse.d.ts.map +1 -1
  85. package/dist/ui/components/MessageResponse.js +2 -2
  86. package/dist/ui/components/MessageResponse.js.map +1 -1
  87. package/dist/ui/components/ToolExecution.d.ts.map +1 -1
  88. package/dist/ui/components/ToolExecution.js +72 -15
  89. package/dist/ui/components/ToolExecution.js.map +1 -1
  90. package/dist/ui/components/UserMessage.d.ts +2 -1
  91. package/dist/ui/components/UserMessage.d.ts.map +1 -1
  92. package/dist/ui/components/UserMessage.js +6 -2
  93. package/dist/ui/components/UserMessage.js.map +1 -1
  94. package/dist/ui/hooks/useAgentLoop.d.ts +16 -4
  95. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  96. package/dist/ui/hooks/useAgentLoop.js +46 -2
  97. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  98. package/dist/ui/layout-decisions.d.ts +4 -0
  99. package/dist/ui/layout-decisions.d.ts.map +1 -1
  100. package/dist/ui/layout-decisions.js.map +1 -1
  101. package/dist/ui/login.js +3 -3
  102. package/dist/ui/login.js.map +1 -1
  103. package/dist/ui/prompt-routing.d.ts +2 -2
  104. package/dist/ui/prompt-routing.d.ts.map +1 -1
  105. package/dist/ui/prompt-routing.js +50 -1
  106. package/dist/ui/prompt-routing.js.map +1 -1
  107. package/dist/ui/queued-message.test.js +5 -1
  108. package/dist/ui/queued-message.test.js.map +1 -1
  109. package/dist/ui/render.d.ts +1 -0
  110. package/dist/ui/render.d.ts.map +1 -1
  111. package/dist/ui/render.js +33 -0
  112. package/dist/ui/render.js.map +1 -1
  113. package/dist/ui/slash-command-images.test.js +55 -3
  114. package/dist/ui/slash-command-images.test.js.map +1 -1
  115. package/dist/ui/submit-prompt-command.d.ts.map +1 -1
  116. package/dist/ui/submit-prompt-command.js +6 -3
  117. package/dist/ui/submit-prompt-command.js.map +1 -1
  118. package/dist/ui/terminal-history.js +36 -10
  119. package/dist/ui/terminal-history.js.map +1 -1
  120. package/dist/ui/terminal-history.test.js +3 -3
  121. package/dist/ui/terminal-history.test.js.map +1 -1
  122. package/dist/ui/transcript/TranscriptRenderer.d.ts.map +1 -1
  123. package/dist/ui/transcript/TranscriptRenderer.js +12 -10
  124. package/dist/ui/transcript/TranscriptRenderer.js.map +1 -1
  125. package/dist/ui/transcript/presentation.d.ts.map +1 -1
  126. package/dist/ui/transcript/presentation.js +16 -2
  127. package/dist/ui/transcript/presentation.js.map +1 -1
  128. package/dist/ui/tui-history-parity.test.js +7 -3
  129. package/dist/ui/tui-history-parity.test.js.map +1 -1
  130. package/dist/ui/utils/assistant-stream-split.d.ts +10 -0
  131. package/dist/ui/utils/assistant-stream-split.d.ts.map +1 -1
  132. package/dist/ui/utils/assistant-stream-split.js +19 -0
  133. package/dist/ui/utils/assistant-stream-split.js.map +1 -1
  134. package/dist/ui/utils/assistant-stream-split.test.js +21 -1
  135. package/dist/ui/utils/assistant-stream-split.test.js.map +1 -1
  136. package/dist/utils/image.d.ts +37 -1
  137. package/dist/utils/image.d.ts.map +1 -1
  138. package/dist/utils/image.js +156 -1
  139. package/dist/utils/image.js.map +1 -1
  140. package/dist/utils/plan-steps.d.ts +29 -2
  141. package/dist/utils/plan-steps.d.ts.map +1 -1
  142. package/dist/utils/plan-steps.js +88 -16
  143. package/dist/utils/plan-steps.js.map +1 -1
  144. package/dist/utils/plan-steps.test.js +144 -1
  145. package/dist/utils/plan-steps.test.js.map +1 -1
  146. package/package.json +4 -4
package/dist/ui/App.js CHANGED
@@ -12,7 +12,7 @@ import { useDoublePress } from "./hooks/useDoublePress.js";
12
12
  import { useTaskBarStore, useTaskBarPolling, focusTaskBar, exitTaskBar, expandTaskBar, collapseTaskBar, navigateTaskBar, killTask, } from "./stores/taskbar-store.js";
13
13
  import { playNotificationSound } from "../utils/sound.js";
14
14
  import {} from "@prestyj/ai";
15
- import { downscaleForPreview, extractImagePaths } from "../utils/image.js";
15
+ import { downscaleForPreview, extractMediaPaths } from "../utils/image.js";
16
16
  import { useAgentLoop } from "./hooks/useAgentLoop.js";
17
17
  import { useTranscriptHistory } from "./hooks/useTranscriptHistory.js";
18
18
  import { createWebSearchTool } from "../tools/web-search.js";
@@ -23,7 +23,7 @@ import { SessionSummaryDisplay } from "./components/SessionSummary.js";
23
23
  import { useTheme, useSetTheme } from "./theme/theme.js";
24
24
  import { useTerminalTitle } from "./hooks/useTerminalTitle.js";
25
25
  import { getGitBranch } from "../utils/git.js";
26
- import { getModel } from "../core/model-registry.js";
26
+ import { getModel, getVideoByteLimit } from "../core/model-registry.js";
27
27
  import { SessionManager } from "../core/session-manager.js";
28
28
  import { log } from "../core/logger.js";
29
29
  import { getPendingUpdate, startPeriodicUpdateCheck, stopPeriodicUpdateCheck, } from "../core/auto-update.js";
@@ -36,10 +36,10 @@ import { detectLanguages } from "../core/language-detector.js";
36
36
  import { detectVerifyCommands } from "../core/verify-commands.js";
37
37
  import { RewindOverlay } from "./components/RewindOverlay.js";
38
38
  import { reconcileGoalStatusEntriesWithRuns, removeGoalStatusEntry, syncGoalStatusEntries, } from "./components/GoalStatusBar.js";
39
- import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
40
- import { getMCPServers } from "../core/mcp/index.js";
39
+ import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, rebasePlanSteps, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
40
+ import { getAllMcpServers } from "../core/mcp/index.js";
41
41
  import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
42
- import { splitAssistantStreamingText } from "./utils/assistant-stream-split.js";
42
+ import { splitAssistantStreamingText, estimateRenderedRows, } from "./utils/assistant-stream-split.js";
43
43
  import { getNextPendingTask, markTaskInProgress } from "../core/task-store.js";
44
44
  import { buildUserContentWithAttachments } from "./prompt-routing.js";
45
45
  import { submitPromptCommand } from "./submit-prompt-command.js";
@@ -772,6 +772,8 @@ export function App(props) {
772
772
  tools: currentTools,
773
773
  webSearch: props.webSearch,
774
774
  maxTokens: props.maxTokens,
775
+ supportsImages: getModel(currentModel)?.supportsImages ?? true,
776
+ supportsVideo: getModel(currentModel)?.supportsVideo ?? false,
775
777
  thinking: thinkingLevel,
776
778
  apiKey: activeApiKey,
777
779
  baseUrl: activeBaseUrl,
@@ -889,10 +891,32 @@ export function App(props) {
889
891
  if (planStepsRef.current.length > 0) {
890
892
  const completed = findCompletedMarkers(text);
891
893
  if (completed.size > 0) {
892
- const updated = markStepsCompleted(planStepsRef.current, completed);
893
- if (updated !== planStepsRef.current) {
894
- planStepsRef.current = updated;
895
- setPlanSteps(updated);
894
+ const planPath = approvedPlanPathRef.current;
895
+ // The agent can rewrite/expand the approved plan mid-run, so the
896
+ // snapshot captured at approval goes stale (wrong total, and new
897
+ // [DONE:n] markers match nothing). Re-extract from the live plan
898
+ // file and re-base onto it before applying markers. The file read
899
+ // is async; apply markers inside the same async step to avoid a
900
+ // race with the snapshot. Fall back to the in-memory snapshot if
901
+ // there is no plan path or the read fails.
902
+ const applyMarkers = (base) => {
903
+ const updated = markStepsCompleted(base, completed);
904
+ if (updated !== planStepsRef.current) {
905
+ planStepsRef.current = updated;
906
+ setPlanSteps(updated);
907
+ }
908
+ };
909
+ if (planPath) {
910
+ void import("node:fs/promises")
911
+ .then(({ readFile }) => readFile(planPath, "utf-8"))
912
+ .then((planContent) => {
913
+ const fresh = extractPlanSteps(planContent);
914
+ applyMarkers(rebasePlanSteps(planStepsRef.current, fresh));
915
+ })
916
+ .catch(() => applyMarkers(planStepsRef.current));
917
+ }
918
+ else {
919
+ applyMarkers(planStepsRef.current);
896
920
  }
897
921
  // Real progress happened — reset the stuck-guard so the next
898
922
  // step gets its own fresh nudge budget.
@@ -1229,6 +1253,18 @@ export function App(props) {
1229
1253
  log("INFO", "server_tool", `Server tool call: ${name}`, { id });
1230
1254
  const startedAt = Date.now();
1231
1255
  const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
1256
+ // Feed the pinned LiveToolPanel so provider-side tools (Anthropic's
1257
+ // native web_search) appear in the same rolling window as client
1258
+ // tools. `input` carries the tool args (e.g. { query }) the row reads.
1259
+ setLiveToolFeed((prev) => [
1260
+ ...prev,
1261
+ {
1262
+ id,
1263
+ name,
1264
+ args: (input ?? {}),
1265
+ status: "running",
1266
+ },
1267
+ ].slice(-(LIVE_TOOL_PANEL_ROWS * 2)));
1232
1268
  // Flush completed items (including assistant text) before adding server
1233
1269
  // tool UI — same rationale as onToolStart.
1234
1270
  setLiveItems((prev) => {
@@ -1243,6 +1279,11 @@ export function App(props) {
1243
1279
  if (flushed.length > 0) {
1244
1280
  queueFlush(flushed);
1245
1281
  }
1282
+ // The pre-tool text was just pinned; the hook resets its streaming
1283
+ // buffer at this same boundary. Reset the progressive-flush offset
1284
+ // so post-tool text is measured from zero (stale flushedChars would
1285
+ // otherwise slice into the fresh, shorter buffer).
1286
+ streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
1246
1287
  return [
1247
1288
  ...remaining,
1248
1289
  {
@@ -1259,6 +1300,9 @@ export function App(props) {
1259
1300
  }, [queueFlush]),
1260
1301
  onServerToolResult: useCallback((toolUseId, resultType, data) => {
1261
1302
  log("INFO", "server_tool", `Server tool result`, { toolUseId, resultType });
1303
+ // Mark the panel entry done. Aborts never reach here (handled in
1304
+ // onAborted), so a result that arrives is always a normal completion.
1305
+ setLiveToolFeed((prev) => prev.map((entry) => entry.id === toolUseId ? { ...entry, status: "done" } : entry));
1262
1306
  setLiveItems((prev) => {
1263
1307
  let updated;
1264
1308
  const startIdx = prev.findIndex((item) => item.kind === "server_tool_start" && item.serverToolCallId === toolUseId);
@@ -1324,7 +1368,7 @@ export function App(props) {
1324
1368
  return remaining;
1325
1369
  });
1326
1370
  }, [queueFlush]),
1327
- onDone: useCallback((durationMs, toolsUsed) => {
1371
+ onDone: useCallback((durationMs, toolsUsed, runStats) => {
1328
1372
  log("INFO", "agent", `Agent done`, {
1329
1373
  duration: `${durationMs}ms`,
1330
1374
  toolsUsed: toolsUsed.join(",") || "none",
@@ -1337,7 +1381,13 @@ export function App(props) {
1337
1381
  // Still flush live transcript rows before the pane remounts; otherwise
1338
1382
  // setup output remains in ephemeral liveItems and appears to vanish.
1339
1383
  if (doneDecision.showDoneStatus) {
1340
- setDoneStatus({ durationMs, toolsUsed, verb: pickDurationVerb(toolsUsed) });
1384
+ setDoneStatus({
1385
+ durationMs,
1386
+ toolsUsed,
1387
+ verb: pickDurationVerb(toolsUsed),
1388
+ counts: runStats?.counts,
1389
+ tokens: runStats?.tokens,
1390
+ });
1341
1391
  playNotificationSound();
1342
1392
  }
1343
1393
  // Keep the final assistant response mounted in the live frame after a
@@ -1465,10 +1515,14 @@ export function App(props) {
1465
1515
  const imageCount = typeof content === "string"
1466
1516
  ? undefined
1467
1517
  : content.filter((c) => c.type === "image").length || undefined;
1518
+ const videoCount = typeof content === "string"
1519
+ ? undefined
1520
+ : content.filter((c) => c.type === "video").length || undefined;
1468
1521
  const userItem = {
1469
1522
  kind: "user",
1470
1523
  text: displayText,
1471
1524
  imageCount,
1525
+ videoCount,
1472
1526
  id: getId(),
1473
1527
  };
1474
1528
  setLastUserMessage(displayText);
@@ -1505,6 +1559,13 @@ export function App(props) {
1505
1559
  },
1506
1560
  ];
1507
1561
  }, []),
1562
+ onRetry: useCallback(() => {
1563
+ // Roll back any pending progressive flushes from the aborted attempt.
1564
+ // Without this, a stall retry regenerates the preamble and the old
1565
+ // flushed paragraph + the new one both end up in terminal history.
1566
+ pendingHistoryFlushRef.current = pendingHistoryFlushRef.current.filter((item) => item.kind !== "assistant");
1567
+ streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
1568
+ }, []),
1508
1569
  });
1509
1570
  const continueGoalRun = useCallback((run) => {
1510
1571
  const cwd = cwdRef.current;
@@ -1891,6 +1952,9 @@ export function App(props) {
1891
1952
  void agentLoop.run(action.prompt).catch((err) => {
1892
1953
  const errMsg = err instanceof Error ? err.message : String(err);
1893
1954
  log("ERROR", "error", errMsg);
1955
+ if (agentLoop.isRunning) {
1956
+ agentLoop.reset();
1957
+ }
1894
1958
  setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
1895
1959
  });
1896
1960
  // Intentional one-shot: run once on mount, never re-fire on re-render.
@@ -2056,31 +2120,35 @@ export function App(props) {
2056
2120
  }
2057
2121
  // ── Build user content (shared by normal + queued paths) ──
2058
2122
  const hasImages = inputImages.length > 0;
2123
+ const imageCount = inputImages.filter((img) => img.kind === "image").length;
2124
+ const videoCount = inputImages.filter((img) => img.kind === "video").length;
2059
2125
  const modelInfo = getModel(currentModel);
2060
2126
  const modelSupportsImages = modelInfo?.supportsImages ?? true;
2061
- const userContent = buildUserContentWithAttachments(input, inputImages, modelSupportsImages);
2127
+ const modelSupportsVideo = modelInfo?.supportsVideo ?? false;
2128
+ const userContent = buildUserContentWithAttachments(input, inputImages, modelSupportsImages, modelSupportsVideo);
2062
2129
  // ── Queue message if agent is already running ──
2063
2130
  if (agentLoop.isRunning) {
2064
2131
  log("INFO", "queue", `Queued message: ${trimmed.length > 80 ? trimmed.slice(0, 80) + "..." : trimmed}`);
2065
2132
  agentLoop.queueMessage(userContent, input);
2066
2133
  let displayText = input;
2067
2134
  if (hasImages) {
2068
- const { cleanText } = await extractImagePaths(input, props.cwd);
2135
+ const { cleanText } = await extractMediaPaths(input, props.cwd);
2069
2136
  displayText = cleanText;
2070
2137
  }
2071
2138
  const queuedItem = {
2072
2139
  kind: "queued",
2073
2140
  text: displayText,
2074
- imageCount: hasImages ? inputImages.length : undefined,
2141
+ imageCount: imageCount > 0 ? imageCount : undefined,
2142
+ videoCount: videoCount > 0 ? videoCount : undefined,
2075
2143
  id: getId(),
2076
2144
  };
2077
2145
  setLiveItems((prev) => [...prev, queuedItem]);
2078
2146
  return;
2079
2147
  }
2080
- // Build display text — strip image paths, show badges instead
2148
+ // Build display text — strip image/video paths, show badges instead
2081
2149
  let displayText = input;
2082
2150
  if (hasImages) {
2083
- const { cleanText } = await extractImagePaths(input, props.cwd);
2151
+ const { cleanText } = await extractMediaPaths(input, props.cwd);
2084
2152
  displayText = cleanText;
2085
2153
  }
2086
2154
  let imagePreviews;
@@ -2096,7 +2164,8 @@ export function App(props) {
2096
2164
  const userItem = {
2097
2165
  kind: "user",
2098
2166
  text: displayText,
2099
- imageCount: hasImages ? inputImages.length : undefined,
2167
+ imageCount: imageCount > 0 ? imageCount : undefined,
2168
+ videoCount: videoCount > 0 ? videoCount : undefined,
2100
2169
  imagePreviews,
2101
2170
  pasteInfo,
2102
2171
  id: getId(),
@@ -2130,6 +2199,12 @@ export function App(props) {
2130
2199
  const msg = err instanceof Error ? err.message : String(err);
2131
2200
  log("ERROR", "error", msg);
2132
2201
  const isAbort = msg.includes("aborted") || msg.includes("abort");
2202
+ // If the agent loop threw but left isRunning in a stale true state
2203
+ // (can happen when the finally block hasn't been processed by React
2204
+ // yet), reset it so the user isn't deadlocked with a non-working UI.
2205
+ if (agentLoop.isRunning) {
2206
+ agentLoop.reset();
2207
+ }
2133
2208
  setLiveItems((prev) => [
2134
2209
  ...prev,
2135
2210
  isAbort
@@ -2220,8 +2295,14 @@ export function App(props) {
2220
2295
  rebuildPromptWithTools(next);
2221
2296
  return next;
2222
2297
  });
2223
- // Reconnect MCP servers
2224
- if (props.mcpManager) {
2298
+ // Reconnect MCP servers ONLY when the resolved server set actually
2299
+ // changes. GLM is the only provider with a different set (Z.AI
2300
+ // servers), so a switch that doesn't involve GLM on either side
2301
+ // keeps the identical set — tearing down a live stdio child (e.g.
2302
+ // kencode-search) and re-spawning `npx` there only risks a failed
2303
+ // re-spawn that would silently drop the tools.
2304
+ const glmInvolved = newProvider === "glm" || prevProvider === "glm";
2305
+ if (props.mcpManager && glmInvolved) {
2225
2306
  void (async () => {
2226
2307
  // Disconnect old MCP servers
2227
2308
  await props.mcpManager.dispose();
@@ -2240,7 +2321,11 @@ export function App(props) {
2240
2321
  apiKey = props.credentialsByProvider?.["glm"]?.accessToken;
2241
2322
  }
2242
2323
  try {
2243
- const mcpTools = await props.mcpManager.connectAll(getMCPServers(newProvider, apiKey));
2324
+ // Use getAllMcpServers so user-configured servers (from
2325
+ // ~/.ezcoder/mcp.json and ./.gg/mcp.json) survive the reconnect —
2326
+ // getMCPServers returns provider defaults only.
2327
+ const servers = await getAllMcpServers(newProvider, apiKey, props.cwd);
2328
+ const mcpTools = await props.mcpManager.connectAll(servers);
2244
2329
  setCurrentTools((prev) => {
2245
2330
  const next = [...prev.filter((t) => !t.name.startsWith("mcp__")), ...mcpTools];
2246
2331
  rebuildPromptWithTools(next);
@@ -2262,7 +2347,26 @@ export function App(props) {
2262
2347
  }
2263
2348
  return newProvider;
2264
2349
  });
2265
- setCurrentModel(newModelId);
2350
+ // The `read` tool's video capability (its description + native-video
2351
+ // execute path) is baked in at creation from the model's `maxVideoBytes`.
2352
+ // Switching to/from a video-capable model (e.g. text-only MiMo-V2.5-Pro →
2353
+ // omnimodal MiMo-V2.5) must rebuild it, or the tool keeps telling the model
2354
+ // it can't watch video. Rebuild reuses the read tracker, so read-before-edit
2355
+ // history survives. Provider-change rebuilds the prompt above; this covers
2356
+ // same-provider model switches too.
2357
+ setCurrentModel((prevModel) => {
2358
+ if (props.rebuildReadTool &&
2359
+ getVideoByteLimit(prevModel) !== getVideoByteLimit(newModelId)) {
2360
+ const newReadTool = props.rebuildReadTool(newModelId);
2361
+ setCurrentTools((prev) => {
2362
+ const next = prev.map((tool) => (tool.name === "read" ? newReadTool : tool));
2363
+ currentToolsRef.current = next;
2364
+ void replaceSystemPrompt({ tools: next });
2365
+ return next;
2366
+ });
2367
+ }
2368
+ return newModelId;
2369
+ });
2266
2370
  const modelInfo = getModel(newModelId);
2267
2371
  const displayName = modelInfo?.name ?? newModelId;
2268
2372
  setLiveItems((prev) => [
@@ -2277,7 +2381,14 @@ export function App(props) {
2277
2381
  await sm.set("defaultModel", newModelId);
2278
2382
  });
2279
2383
  }
2280
- }, [props.settingsFile, props.mcpManager, props.credentialsByProvider, props.authStorage]);
2384
+ }, [
2385
+ props.settingsFile,
2386
+ props.mcpManager,
2387
+ props.credentialsByProvider,
2388
+ props.authStorage,
2389
+ props.rebuildReadTool,
2390
+ replaceSystemPrompt,
2391
+ ]);
2281
2392
  const handleThemeSelect = useCallback((name) => {
2282
2393
  setOverlay(null);
2283
2394
  if (switchTheme) {
@@ -2497,6 +2608,9 @@ export function App(props) {
2497
2608
  setDoneStatus(null);
2498
2609
  setLiveItems([taskItem]);
2499
2610
  void agentLoop.run(fullPrompt).catch((err) => {
2611
+ if (agentLoop.isRunning) {
2612
+ agentLoop.reset();
2613
+ }
2500
2614
  setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
2501
2615
  });
2502
2616
  }, [agentLoop, currentModel, currentProvider, props]);
@@ -2545,7 +2659,23 @@ export function App(props) {
2545
2659
  // every chunk boundary. Slicing by the prospective flush here keeps the live
2546
2660
  // frame height monotonic, so the footer never bounces.
2547
2661
  const alreadyFlushedChars = streamedAssistantFlushRef.current.flushedChars;
2548
- const pendingFlushChars = rawVisibleStreamingText
2662
+ // Retry-safety gate: don't commit streamed paragraphs to permanent scrollback
2663
+ // while the text is still small enough to live entirely in the live region.
2664
+ // A silent stall-retry (agent-loop.ts) restarts the LLM call from scratch and
2665
+ // regenerates the opening text — reworded, so the byte-identical dedup can't
2666
+ // catch it. Anything already printed to scrollback can't be un-written, so the
2667
+ // regen appends as a second ⏺ bullet that paraphrases the first. Keeping short
2668
+ // streamed text live (it clears via setStreamingText("") on retry) closes that
2669
+ // hole. We only start flushing once the unflushed text would overflow the live
2670
+ // area — the original anti-jump purpose, which only matters for long responses
2671
+ // that are far less likely to be a stalled preamble. Once flushing has begun
2672
+ // for this turn (alreadyFlushedChars > 0) we keep flushing every boundary so
2673
+ // committed continuation paragraphs stay consistent with the live tail.
2674
+ const unflushedStreamingRows = rawVisibleStreamingText
2675
+ ? estimateRenderedRows(rawVisibleStreamingText.slice(alreadyFlushedChars), columns)
2676
+ : 0;
2677
+ const shouldFlushStreamedText = alreadyFlushedChars > 0 || unflushedStreamingRows > measuredLiveAreaRows;
2678
+ const pendingFlushChars = rawVisibleStreamingText && shouldFlushStreamedText
2549
2679
  ? splitAssistantStreamingText(rawVisibleStreamingText.slice(alreadyFlushedChars)).flushedText
2550
2680
  .length
2551
2681
  : 0;
@@ -2556,6 +2686,13 @@ export function App(props) {
2556
2686
  }
2557
2687
  if (rawVisibleStreamingText === streamedAssistantFlushRef.current.text)
2558
2688
  return;
2689
+ if (!shouldFlushStreamedText) {
2690
+ streamedAssistantFlushRef.current = {
2691
+ ...streamedAssistantFlushRef.current,
2692
+ text: rawVisibleStreamingText,
2693
+ };
2694
+ return;
2695
+ }
2559
2696
  const alreadyFlushed = streamedAssistantFlushRef.current.flushedChars;
2560
2697
  const unflushedText = rawVisibleStreamingText.slice(alreadyFlushed);
2561
2698
  const split = splitAssistantStreamingText(unflushedText);
@@ -2578,7 +2715,7 @@ export function App(props) {
2578
2715
  ...streamedAssistantFlushRef.current,
2579
2716
  text: rawVisibleStreamingText,
2580
2717
  };
2581
- }, [rawVisibleStreamingText, queueFlush]);
2718
+ }, [rawVisibleStreamingText, shouldFlushStreamedText, queueFlush]);
2582
2719
  const visibleStreamingText = stripDoneMarkers(rawVisibleStreamingText.slice(alreadyFlushedChars + pendingFlushChars));
2583
2720
  const lastLiveItem = liveItems.at(-1);
2584
2721
  // For spacing decisions, the previous row is the last item that actually
@@ -3018,6 +3155,9 @@ export function App(props) {
3018
3155
  void agentLoop.run(rejectionMsg).catch((err) => {
3019
3156
  const errMsg = err instanceof Error ? err.message : String(err);
3020
3157
  log("ERROR", "error", errMsg);
3158
+ if (agentLoop.isRunning) {
3159
+ agentLoop.reset();
3160
+ }
3021
3161
  setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3022
3162
  });
3023
3163
  };