@kenkaiiii/ggcoder 4.3.236 → 4.3.238

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 (107) hide show
  1. package/README.md +3 -1
  2. package/dist/cli.js +2 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +12 -4
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/loop-breaker.d.ts +46 -0
  8. package/dist/core/loop-breaker.d.ts.map +1 -0
  9. package/dist/core/loop-breaker.js +79 -0
  10. package/dist/core/loop-breaker.js.map +1 -0
  11. package/dist/core/loop-breaker.test.d.ts +2 -0
  12. package/dist/core/loop-breaker.test.d.ts.map +1 -0
  13. package/dist/core/loop-breaker.test.js +110 -0
  14. package/dist/core/loop-breaker.test.js.map +1 -0
  15. package/dist/core/model-registry.d.ts +1 -0
  16. package/dist/core/model-registry.d.ts.map +1 -1
  17. package/dist/core/model-registry.js +23 -16
  18. package/dist/core/model-registry.js.map +1 -1
  19. package/dist/core/model-registry.test.js +13 -0
  20. package/dist/core/model-registry.test.js.map +1 -1
  21. package/dist/core/oauth/gemini.d.ts.map +1 -1
  22. package/dist/core/oauth/gemini.js +10 -5
  23. package/dist/core/oauth/gemini.js.map +1 -1
  24. package/dist/core/regrounding.d.ts +23 -0
  25. package/dist/core/regrounding.d.ts.map +1 -0
  26. package/dist/core/regrounding.js +21 -0
  27. package/dist/core/regrounding.js.map +1 -0
  28. package/dist/core/regrounding.test.d.ts +2 -0
  29. package/dist/core/regrounding.test.d.ts.map +1 -0
  30. package/dist/core/regrounding.test.js +38 -0
  31. package/dist/core/regrounding.test.js.map +1 -0
  32. package/dist/interactive.d.ts.map +1 -1
  33. package/dist/interactive.js +17 -1
  34. package/dist/interactive.js.map +1 -1
  35. package/dist/system-prompt.d.ts.map +1 -1
  36. package/dist/system-prompt.js +22 -14
  37. package/dist/system-prompt.js.map +1 -1
  38. package/dist/system-prompt.test.js +8 -1
  39. package/dist/system-prompt.test.js.map +1 -1
  40. package/dist/tools/prompt-hints.d.ts +23 -4
  41. package/dist/tools/prompt-hints.d.ts.map +1 -1
  42. package/dist/tools/prompt-hints.js +35 -10
  43. package/dist/tools/prompt-hints.js.map +1 -1
  44. package/dist/ui/App.d.ts.map +1 -1
  45. package/dist/ui/App.js +124 -16
  46. package/dist/ui/App.js.map +1 -1
  47. package/dist/ui/app-items.d.ts +22 -3
  48. package/dist/ui/app-items.d.ts.map +1 -1
  49. package/dist/ui/app-items.js +23 -3
  50. package/dist/ui/app-items.js.map +1 -1
  51. package/dist/ui/components/ChatInputStack.js +1 -1
  52. package/dist/ui/components/ChatInputStack.js.map +1 -1
  53. package/dist/ui/components/IdealHookMessage.d.ts +3 -1
  54. package/dist/ui/components/IdealHookMessage.d.ts.map +1 -1
  55. package/dist/ui/components/IdealHookMessage.js +4 -2
  56. package/dist/ui/components/IdealHookMessage.js.map +1 -1
  57. package/dist/ui/components/InputArea.d.ts.map +1 -1
  58. package/dist/ui/components/InputArea.js +17 -7
  59. package/dist/ui/components/InputArea.js.map +1 -1
  60. package/dist/ui/components/UserMessage.d.ts +2 -1
  61. package/dist/ui/components/UserMessage.d.ts.map +1 -1
  62. package/dist/ui/components/UserMessage.js +6 -2
  63. package/dist/ui/components/UserMessage.js.map +1 -1
  64. package/dist/ui/hooks/useAgentLoop.d.ts +17 -2
  65. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  66. package/dist/ui/hooks/useAgentLoop.js +117 -9
  67. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  68. package/dist/ui/hooks/useChatLayoutMeasurements.d.ts.map +1 -1
  69. package/dist/ui/hooks/useChatLayoutMeasurements.js +6 -7
  70. package/dist/ui/hooks/useChatLayoutMeasurements.js.map +1 -1
  71. package/dist/ui/login.js +1 -1
  72. package/dist/ui/login.js.map +1 -1
  73. package/dist/ui/prompt-routing.d.ts +2 -2
  74. package/dist/ui/prompt-routing.d.ts.map +1 -1
  75. package/dist/ui/prompt-routing.js +26 -1
  76. package/dist/ui/prompt-routing.js.map +1 -1
  77. package/dist/ui/queued-message.test.js +5 -1
  78. package/dist/ui/queued-message.test.js.map +1 -1
  79. package/dist/ui/render.d.ts.map +1 -1
  80. package/dist/ui/render.js +38 -0
  81. package/dist/ui/render.js.map +1 -1
  82. package/dist/ui/slash-command-images.test.js +33 -3
  83. package/dist/ui/slash-command-images.test.js.map +1 -1
  84. package/dist/ui/submit-prompt-command.d.ts.map +1 -1
  85. package/dist/ui/submit-prompt-command.js +6 -3
  86. package/dist/ui/submit-prompt-command.js.map +1 -1
  87. package/dist/ui/terminal-history.js +13 -9
  88. package/dist/ui/terminal-history.js.map +1 -1
  89. package/dist/ui/transcript/TranscriptRenderer.d.ts.map +1 -1
  90. package/dist/ui/transcript/TranscriptRenderer.js +13 -11
  91. package/dist/ui/transcript/TranscriptRenderer.js.map +1 -1
  92. package/dist/ui/transcript/presentation.d.ts.map +1 -1
  93. package/dist/ui/transcript/presentation.js +7 -1
  94. package/dist/ui/transcript/presentation.js.map +1 -1
  95. package/dist/ui/tui-history-parity.test.js +6 -2
  96. package/dist/ui/tui-history-parity.test.js.map +1 -1
  97. package/dist/ui/utils/assistant-stream-split.d.ts +10 -0
  98. package/dist/ui/utils/assistant-stream-split.d.ts.map +1 -1
  99. package/dist/ui/utils/assistant-stream-split.js +19 -0
  100. package/dist/ui/utils/assistant-stream-split.js.map +1 -1
  101. package/dist/ui/utils/assistant-stream-split.test.js +21 -1
  102. package/dist/ui/utils/assistant-stream-split.test.js.map +1 -1
  103. package/dist/utils/image.d.ts +14 -1
  104. package/dist/utils/image.d.ts.map +1 -1
  105. package/dist/utils/image.js +63 -1
  106. package/dist/utils/image.js.map +1 -1
  107. package/package.json +3 -3
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 "@kenkaiiii/gg-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";
@@ -36,14 +36,16 @@ 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 { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
39
- import { getMCPServers } from "../core/mcp/index.js";
39
+ import { getAllMcpServers } from "../core/mcp/index.js";
40
40
  import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
41
- import { splitAssistantStreamingText } from "./utils/assistant-stream-split.js";
41
+ import { splitAssistantStreamingText, estimateRenderedRows, } from "./utils/assistant-stream-split.js";
42
42
  import { getNextPendingTask, markTaskInProgress } from "../core/tasks-store.js";
43
43
  import { buildUserContentWithAttachments } from "./prompt-routing.js";
44
44
  import { submitPromptCommand } from "./submit-prompt-command.js";
45
45
  import { handleUiSlashCommand } from "./submit-slash-commands.js";
46
46
  import { buildIdealReviewMessage, evaluateIdealReview } from "../core/ideal-review.js";
47
+ import { buildLoopBreakMessage, evaluateLoopBreak } from "../core/loop-breaker.js";
48
+ import { buildRegroundingMessage } from "../core/regrounding.js";
47
49
  import { getNextThinkingLevel, isThinkingLevelSupported } from "./thinking-level.js";
48
50
  import { getDoneFlushDecision, shouldTopSpaceAfterPrintedAgentBoundary, shouldTopSpaceStreamingAssistant, } from "./layout-decisions.js";
49
51
  import { isTranscriptSpacingItem } from "./transcript/spacing.js";
@@ -56,7 +58,7 @@ import { pickDurationVerb } from "./duration-summary.js";
56
58
  import { toErrorItem } from "./error-item.js";
57
59
  import { addLinesChanged, buildSessionSummary, createSessionStats, recordServerToolCall, recordToolEnd, recordTurnEnd, } from "./session-summary.js";
58
60
  import { compactHistory, getNextGeneratedItemId, isActiveItem, isSameAssistantText, normalizeAssistantText, partitionCompleted, pinStreamingTextBeforeToolBoundary, removeItemsWithIds, uniqueItemsById, } from "./item-helpers.js";
59
- import { IDEAL_HOOK_NOTICE_TEXT, lastVisibleTranscriptItem } from "./app-items.js";
61
+ import { IDEAL_HOOK_NOTICE_TEXT, LOOP_BREAK_NOTICE_TEXT, REGROUNDING_NOTICE_TEXT, lastVisibleTranscriptItem, } from "./app-items.js";
60
62
  export { buildUserContentWithAttachments, routePromptCommandInput } from "./prompt-routing.js";
61
63
  export { getNextThinkingLevel } from "./thinking-level.js";
62
64
  export { getChatControlsLayoutDecision, getDoneFlushDecision, getScrollStabilizationDecision, getStaticHistoryKey, hasParagraphBreakLiveUserMessage, isTallLiveUserMessage, shouldHideHistoryForOverlayView, shouldHideStaticItemsForOverlayView, shouldStabilizeOverlayPaneRerender, shouldTopSpaceAfterPrintedAgentBoundary, shouldTopSpaceAssistantAfterToolBoundary, shouldTopSpaceStreamingAssistant, } from "./layout-decisions.js";
@@ -561,6 +563,8 @@ export function App(props) {
561
563
  tools: currentTools,
562
564
  webSearch: props.webSearch,
563
565
  maxTokens: props.maxTokens,
566
+ supportsImages: getModel(currentModel)?.supportsImages ?? true,
567
+ supportsVideo: getModel(currentModel)?.supportsVideo ?? false,
564
568
  thinking: thinkingLevel,
565
569
  apiKey: activeApiKey,
566
570
  baseUrl: activeBaseUrl,
@@ -580,10 +584,35 @@ export function App(props) {
580
584
  });
581
585
  setLiveItems((prev) => [
582
586
  ...prev,
583
- { kind: "ideal_hook", text: IDEAL_HOOK_NOTICE_TEXT, id: getId() },
587
+ { kind: "ideal_hook", text: IDEAL_HOOK_NOTICE_TEXT, tone: "review", id: getId() },
584
588
  ]);
585
589
  return buildIdealReviewMessage(decision.reasons);
586
590
  },
591
+ getLoopBreakMessage: (stats) => {
592
+ if (!idealReviewEnabledRef.current)
593
+ return null;
594
+ const decision = evaluateLoopBreak(stats);
595
+ if (!decision.shouldBreak)
596
+ return null;
597
+ log("INFO", "loop-break", "Injecting loop-break nudge", {
598
+ reasons: decision.reasons.join(", "),
599
+ });
600
+ setLiveItems((prev) => [
601
+ ...prev,
602
+ { kind: "ideal_hook", text: LOOP_BREAK_NOTICE_TEXT, tone: "warning", id: getId() },
603
+ ]);
604
+ return buildLoopBreakMessage(decision.reasons);
605
+ },
606
+ getRegroundingMessage: (originalRequest) => {
607
+ if (!idealReviewEnabledRef.current)
608
+ return null;
609
+ log("INFO", "reground", "Injecting re-grounding after compaction", {});
610
+ setLiveItems((prev) => [
611
+ ...prev,
612
+ { kind: "ideal_hook", text: REGROUNDING_NOTICE_TEXT, tone: "info", id: getId() },
613
+ ]);
614
+ return buildRegroundingMessage(originalRequest);
615
+ },
587
616
  }, {
588
617
  onComplete: useCallback(() => {
589
618
  persistNewMessages();
@@ -993,6 +1022,18 @@ export function App(props) {
993
1022
  log("INFO", "server_tool", `Server tool call: ${name}`, { id });
994
1023
  const startedAt = Date.now();
995
1024
  const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
1025
+ // Feed the pinned LiveToolPanel so provider-side tools (Anthropic's
1026
+ // native web_search) appear in the same rolling window as client
1027
+ // tools. `input` carries the tool args (e.g. { query }) the row reads.
1028
+ setLiveToolFeed((prev) => [
1029
+ ...prev,
1030
+ {
1031
+ id,
1032
+ name,
1033
+ args: (input ?? {}),
1034
+ status: "running",
1035
+ },
1036
+ ].slice(-(LIVE_TOOL_PANEL_ROWS * 2)));
996
1037
  // Flush completed items (including assistant text) before adding server
997
1038
  // tool UI — same rationale as onToolStart.
998
1039
  setLiveItems((prev) => {
@@ -1023,6 +1064,9 @@ export function App(props) {
1023
1064
  }, [queueFlush]),
1024
1065
  onServerToolResult: useCallback((toolUseId, resultType, data) => {
1025
1066
  log("INFO", "server_tool", `Server tool result`, { toolUseId, resultType });
1067
+ // Mark the panel entry done. Aborts never reach here (handled in
1068
+ // onAborted), so a result that arrives is always a normal completion.
1069
+ setLiveToolFeed((prev) => prev.map((entry) => entry.id === toolUseId ? { ...entry, status: "done" } : entry));
1026
1070
  setLiveItems((prev) => {
1027
1071
  let updated;
1028
1072
  const startIdx = prev.findIndex((item) => item.kind === "server_tool_start" && item.serverToolCallId === toolUseId);
@@ -1228,10 +1272,14 @@ export function App(props) {
1228
1272
  const imageCount = typeof content === "string"
1229
1273
  ? undefined
1230
1274
  : content.filter((c) => c.type === "image").length || undefined;
1275
+ const videoCount = typeof content === "string"
1276
+ ? undefined
1277
+ : content.filter((c) => c.type === "video").length || undefined;
1231
1278
  const userItem = {
1232
1279
  kind: "user",
1233
1280
  text: displayText,
1234
1281
  imageCount,
1282
+ videoCount,
1235
1283
  id: getId(),
1236
1284
  };
1237
1285
  setLastUserMessage(displayText);
@@ -1268,6 +1316,13 @@ export function App(props) {
1268
1316
  },
1269
1317
  ];
1270
1318
  }, []),
1319
+ onRetry: useCallback(() => {
1320
+ // Roll back any pending progressive flushes from the aborted attempt.
1321
+ // Without this, a stall retry regenerates the preamble and the old
1322
+ // flushed paragraph + the new one both end up in terminal history.
1323
+ pendingHistoryFlushRef.current = pendingHistoryFlushRef.current.filter((item) => item.kind !== "assistant");
1324
+ streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
1325
+ }, []),
1271
1326
  });
1272
1327
  // First-time-per-project auto-run of /setup. Bound after `agentLoop` is in
1273
1328
  // scope so the ref closure can dispatch to it. Called from the initial
@@ -1383,6 +1438,9 @@ export function App(props) {
1383
1438
  void agentLoop.run(action.prompt).catch((err) => {
1384
1439
  const errMsg = err instanceof Error ? err.message : String(err);
1385
1440
  log("ERROR", "error", errMsg);
1441
+ if (agentLoop.isRunning) {
1442
+ agentLoop.reset();
1443
+ }
1386
1444
  setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
1387
1445
  });
1388
1446
  // Intentional one-shot: run once on mount, never re-fire on re-render.
@@ -1537,31 +1595,35 @@ export function App(props) {
1537
1595
  }
1538
1596
  // ── Build user content (shared by normal + queued paths) ──
1539
1597
  const hasImages = inputImages.length > 0;
1598
+ const imageCount = inputImages.filter((img) => img.kind === "image").length;
1599
+ const videoCount = inputImages.filter((img) => img.kind === "video").length;
1540
1600
  const modelInfo = getModel(currentModel);
1541
1601
  const modelSupportsImages = modelInfo?.supportsImages ?? true;
1542
- const userContent = buildUserContentWithAttachments(input, inputImages, modelSupportsImages);
1602
+ const modelSupportsVideo = modelInfo?.supportsVideo ?? false;
1603
+ const userContent = buildUserContentWithAttachments(input, inputImages, modelSupportsImages, modelSupportsVideo);
1543
1604
  // ── Queue message if agent is already running ──
1544
1605
  if (agentLoop.isRunning) {
1545
1606
  log("INFO", "queue", `Queued message: ${trimmed.length > 80 ? trimmed.slice(0, 80) + "..." : trimmed}`);
1546
1607
  agentLoop.queueMessage(userContent, input);
1547
1608
  let displayText = input;
1548
1609
  if (hasImages) {
1549
- const { cleanText } = await extractImagePaths(input, props.cwd);
1610
+ const { cleanText } = await extractMediaPaths(input, props.cwd);
1550
1611
  displayText = cleanText;
1551
1612
  }
1552
1613
  const queuedItem = {
1553
1614
  kind: "queued",
1554
1615
  text: displayText,
1555
- imageCount: hasImages ? inputImages.length : undefined,
1616
+ imageCount: imageCount > 0 ? imageCount : undefined,
1617
+ videoCount: videoCount > 0 ? videoCount : undefined,
1556
1618
  id: getId(),
1557
1619
  };
1558
1620
  setLiveItems((prev) => [...prev, queuedItem]);
1559
1621
  return;
1560
1622
  }
1561
- // Build display text — strip image paths, show badges instead
1623
+ // Build display text — strip image/video paths, show badges instead
1562
1624
  let displayText = input;
1563
1625
  if (hasImages) {
1564
- const { cleanText } = await extractImagePaths(input, props.cwd);
1626
+ const { cleanText } = await extractMediaPaths(input, props.cwd);
1565
1627
  displayText = cleanText;
1566
1628
  }
1567
1629
  let imagePreviews;
@@ -1577,7 +1639,8 @@ export function App(props) {
1577
1639
  const userItem = {
1578
1640
  kind: "user",
1579
1641
  text: displayText,
1580
- imageCount: hasImages ? inputImages.length : undefined,
1642
+ imageCount: imageCount > 0 ? imageCount : undefined,
1643
+ videoCount: videoCount > 0 ? videoCount : undefined,
1581
1644
  imagePreviews,
1582
1645
  pasteInfo,
1583
1646
  id: getId(),
@@ -1611,6 +1674,12 @@ export function App(props) {
1611
1674
  const msg = err instanceof Error ? err.message : String(err);
1612
1675
  log("ERROR", "error", msg);
1613
1676
  const isAbort = msg.includes("aborted") || msg.includes("abort");
1677
+ // If the agent loop threw but left isRunning in a stale true state
1678
+ // (can happen when the finally block hasn't been processed by React
1679
+ // yet), reset it so the user isn't deadlocked with a non-working UI.
1680
+ if (agentLoop.isRunning) {
1681
+ agentLoop.reset();
1682
+ }
1614
1683
  setLiveItems((prev) => [
1615
1684
  ...prev,
1616
1685
  isAbort
@@ -1701,8 +1770,14 @@ export function App(props) {
1701
1770
  rebuildPromptWithTools(next);
1702
1771
  return next;
1703
1772
  });
1704
- // Reconnect MCP servers
1705
- if (props.mcpManager) {
1773
+ // Reconnect MCP servers ONLY when the resolved server set actually
1774
+ // changes. GLM is the only provider with a different set (Z.AI
1775
+ // servers), so a switch that doesn't involve GLM on either side
1776
+ // keeps the identical set — tearing down a live stdio child (e.g.
1777
+ // kencode-search) and re-spawning `npx` there only risks a failed
1778
+ // re-spawn that would silently drop the tools.
1779
+ const glmInvolved = newProvider === "glm" || prevProvider === "glm";
1780
+ if (props.mcpManager && glmInvolved) {
1706
1781
  void (async () => {
1707
1782
  // Disconnect old MCP servers
1708
1783
  await props.mcpManager.dispose();
@@ -1721,7 +1796,11 @@ export function App(props) {
1721
1796
  apiKey = props.credentialsByProvider?.["glm"]?.accessToken;
1722
1797
  }
1723
1798
  try {
1724
- const mcpTools = await props.mcpManager.connectAll(getMCPServers(newProvider, apiKey));
1799
+ // Use getAllMcpServers so user-configured servers (from
1800
+ // ~/.gg/mcp.json and ./.gg/mcp.json) survive the reconnect —
1801
+ // getMCPServers returns provider defaults only.
1802
+ const servers = await getAllMcpServers(newProvider, apiKey, props.cwd);
1803
+ const mcpTools = await props.mcpManager.connectAll(servers);
1725
1804
  setCurrentTools((prev) => {
1726
1805
  const next = [...prev.filter((t) => !t.name.startsWith("mcp__")), ...mcpTools];
1727
1806
  rebuildPromptWithTools(next);
@@ -1972,6 +2051,9 @@ export function App(props) {
1972
2051
  setDoneStatus(null);
1973
2052
  setLiveItems([taskItem]);
1974
2053
  void agentLoop.run(fullPrompt).catch((err) => {
2054
+ if (agentLoop.isRunning) {
2055
+ agentLoop.reset();
2056
+ }
1975
2057
  setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
1976
2058
  });
1977
2059
  }, [agentLoop, currentModel, currentProvider, props]);
@@ -2020,7 +2102,23 @@ export function App(props) {
2020
2102
  // every chunk boundary. Slicing by the prospective flush here keeps the live
2021
2103
  // frame height monotonic, so the footer never bounces.
2022
2104
  const alreadyFlushedChars = streamedAssistantFlushRef.current.flushedChars;
2023
- const pendingFlushChars = rawVisibleStreamingText
2105
+ // Retry-safety gate: don't commit streamed paragraphs to permanent scrollback
2106
+ // while the text is still small enough to live entirely in the live region.
2107
+ // A silent stall-retry (agent-loop.ts) restarts the LLM call from scratch and
2108
+ // regenerates the opening text — reworded, so the byte-identical dedup can't
2109
+ // catch it. Anything already printed to scrollback can't be un-written, so the
2110
+ // regen appends as a second ⏺ bullet that paraphrases the first. Keeping short
2111
+ // streamed text live (it clears via setStreamingText("") on retry) closes that
2112
+ // hole. We only start flushing once the unflushed text would overflow the live
2113
+ // area — the original anti-jump purpose, which only matters for long responses
2114
+ // that are far less likely to be a stalled preamble. Once flushing has begun
2115
+ // for this turn (alreadyFlushedChars > 0) we keep flushing every boundary so
2116
+ // committed continuation paragraphs stay consistent with the live tail.
2117
+ const unflushedStreamingRows = rawVisibleStreamingText
2118
+ ? estimateRenderedRows(rawVisibleStreamingText.slice(alreadyFlushedChars), columns)
2119
+ : 0;
2120
+ const shouldFlushStreamedText = alreadyFlushedChars > 0 || unflushedStreamingRows > measuredLiveAreaRows;
2121
+ const pendingFlushChars = rawVisibleStreamingText && shouldFlushStreamedText
2024
2122
  ? splitAssistantStreamingText(rawVisibleStreamingText.slice(alreadyFlushedChars)).flushedText
2025
2123
  .length
2026
2124
  : 0;
@@ -2031,6 +2129,13 @@ export function App(props) {
2031
2129
  }
2032
2130
  if (rawVisibleStreamingText === streamedAssistantFlushRef.current.text)
2033
2131
  return;
2132
+ if (!shouldFlushStreamedText) {
2133
+ streamedAssistantFlushRef.current = {
2134
+ ...streamedAssistantFlushRef.current,
2135
+ text: rawVisibleStreamingText,
2136
+ };
2137
+ return;
2138
+ }
2034
2139
  const alreadyFlushed = streamedAssistantFlushRef.current.flushedChars;
2035
2140
  const unflushedText = rawVisibleStreamingText.slice(alreadyFlushed);
2036
2141
  const split = splitAssistantStreamingText(unflushedText);
@@ -2053,7 +2158,7 @@ export function App(props) {
2053
2158
  ...streamedAssistantFlushRef.current,
2054
2159
  text: rawVisibleStreamingText,
2055
2160
  };
2056
- }, [rawVisibleStreamingText, queueFlush]);
2161
+ }, [rawVisibleStreamingText, shouldFlushStreamedText, queueFlush]);
2057
2162
  const visibleStreamingText = stripDoneMarkers(rawVisibleStreamingText.slice(alreadyFlushedChars + pendingFlushChars));
2058
2163
  const lastLiveItem = liveItems.at(-1);
2059
2164
  // For spacing decisions, the previous row is the last item that actually
@@ -2357,6 +2462,9 @@ export function App(props) {
2357
2462
  void agentLoop.run(rejectionMsg).catch((err) => {
2358
2463
  const errMsg = err instanceof Error ? err.message : String(err);
2359
2464
  log("ERROR", "error", errMsg);
2465
+ if (agentLoop.isRunning) {
2466
+ agentLoop.reset();
2467
+ }
2360
2468
  setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
2361
2469
  });
2362
2470
  };