@agent-native/core 0.12.36 → 0.12.38

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 (56) hide show
  1. package/dist/agent/engine/credential-errors.d.ts +1 -1
  2. package/dist/agent/engine/credential-errors.d.ts.map +1 -1
  3. package/dist/agent/engine/credential-errors.js +2 -2
  4. package/dist/agent/engine/credential-errors.js.map +1 -1
  5. package/dist/cli/workspace-dev.d.ts.map +1 -1
  6. package/dist/cli/workspace-dev.js +1 -0
  7. package/dist/cli/workspace-dev.js.map +1 -1
  8. package/dist/client/AgentPanel.d.ts.map +1 -1
  9. package/dist/client/AgentPanel.js +5 -5
  10. package/dist/client/AgentPanel.js.map +1 -1
  11. package/dist/client/AssistantChat.d.ts.map +1 -1
  12. package/dist/client/AssistantChat.js +252 -229
  13. package/dist/client/AssistantChat.js.map +1 -1
  14. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  15. package/dist/client/ConnectBuilderCard.js +23 -5
  16. package/dist/client/ConnectBuilderCard.js.map +1 -1
  17. package/dist/client/components/CodeRequiredDialog.d.ts.map +1 -1
  18. package/dist/client/components/CodeRequiredDialog.js +11 -4
  19. package/dist/client/components/CodeRequiredDialog.js.map +1 -1
  20. package/dist/client/frame.d.ts.map +1 -1
  21. package/dist/client/frame.js +25 -9
  22. package/dist/client/frame.js.map +1 -1
  23. package/dist/client/integrations/IntegrationsPanel.d.ts.map +1 -1
  24. package/dist/client/integrations/IntegrationsPanel.js +28 -2
  25. package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
  26. package/dist/client/settings/BackgroundAgentSection.d.ts.map +1 -1
  27. package/dist/client/settings/BackgroundAgentSection.js +2 -1
  28. package/dist/client/settings/BackgroundAgentSection.js.map +1 -1
  29. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  30. package/dist/client/settings/SettingsPanel.js +13 -1
  31. package/dist/client/settings/SettingsPanel.js.map +1 -1
  32. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  33. package/dist/client/settings/useBuilderStatus.js +4 -0
  34. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  35. package/dist/client/sse-event-processor.js +1 -1
  36. package/dist/client/sse-event-processor.js.map +1 -1
  37. package/dist/db/client.d.ts +6 -0
  38. package/dist/db/client.d.ts.map +1 -1
  39. package/dist/db/client.js +50 -25
  40. package/dist/db/client.js.map +1 -1
  41. package/dist/db/index.d.ts +1 -1
  42. package/dist/db/index.d.ts.map +1 -1
  43. package/dist/db/index.js +1 -1
  44. package/dist/db/index.js.map +1 -1
  45. package/dist/deploy/workspace-deploy.js +7 -0
  46. package/dist/deploy/workspace-deploy.js.map +1 -1
  47. package/dist/onboarding/default-steps.js +1 -1
  48. package/dist/onboarding/default-steps.js.map +1 -1
  49. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  50. package/dist/server/agent-chat-plugin.js +11 -5
  51. package/dist/server/agent-chat-plugin.js.map +1 -1
  52. package/dist/server/google-oauth.d.ts +8 -7
  53. package/dist/server/google-oauth.d.ts.map +1 -1
  54. package/dist/server/google-oauth.js +56 -54
  55. package/dist/server/google-oauth.js.map +1 -1
  56. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantChat.d.ts","sourceRoot":"","sources":["../../src/client/AssistantChat.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AA4Bf,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAinFrE,MAAM,WAAW,mBAAmB;IAClC,qDAAqD;IACrD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,6DAA6D;IAC7D,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,4CAA4C;IAC5C,SAAS,IAAI,OAAO,CAAC;IACrB,+BAA+B;IAC/B,aAAa,IAAI,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wGAAwG;IACxG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,oDAAoD;IACpD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,0CAA0C;IAC1C,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,8EAA8E;IAC9E,YAAY,CAAC,EAAE,CACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;QACJ,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;KACtB,KACE,IAAI,CAAC;IACV,+DAA+D;IAC/D,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9D,8DAA8D;IAC9D,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC/B,sFAAsF;IACtF,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,8EAA8E;IAC9E,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,+FAA+F;IAC/F,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,mEAAmE;IACnE,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC5B,wCAAwC;IACxC,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;IACpD,0DAA0D;IAC1D,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,0DAA0D;IAC1D,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qFAAqF;IACrF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+DAA+D;IAC/D,cAAc,CAAC,EAAE,eAAe,CAAC;IACjC,uDAAuD;IACvD,eAAe,CAAC,EAAE,KAAK,CAAC;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,UAAU,EAAE,OAAO,CAAC;KACrB,CAAC,CAAC;IACH,uDAAuD;IACvD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxD,kEAAkE;IAClE,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IACnD,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AAED,eAAO,MAAM,mBAAmB,gBAAgB,CAAC;AAEjD,8DAA8D;AAC9D,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,EAAE,MAAM,QAI9C;AAqCD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAm9C7B,eAAO,MAAM,aAAa,gGA4DxB,CAAC"}
1
+ {"version":3,"file":"AssistantChat.d.ts","sourceRoot":"","sources":["../../src/client/AssistantChat.tsx"],"names":[],"mappings":"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"}
@@ -229,6 +229,15 @@ const markdownStyles = `
229
229
  const PENDING_SELECTION_KEY = "pending-selection-context";
230
230
  const ACTIVE_RUN_CLEAR_TIMEOUT_MS = 5_000;
231
231
  const ACTIVE_RUN_POLL_INTERVAL_MS = 150;
232
+ function activeRunLooksStale(runInfo) {
233
+ const heartbeatAt = typeof runInfo.heartbeatAt === "number" ? runInfo.heartbeatAt : null;
234
+ return (runInfo.status === "running" &&
235
+ heartbeatAt != null &&
236
+ Date.now() - heartbeatAt > 5000);
237
+ }
238
+ function repoHasAssistantMessage(repo) {
239
+ return repo?.messages?.some((m) => (m.message?.role ?? m.role) === "assistant");
240
+ }
232
241
  function clearPendingSelection() {
233
242
  fetch(agentNativePath(`/_agent-native/application-state/${PENDING_SELECTION_KEY}`), {
234
243
  method: "DELETE",
@@ -1452,6 +1461,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1452
1461
  const [isReconnecting, setIsReconnecting] = useState(false);
1453
1462
  const [reconnectContent, setReconnectContent] = useState([]);
1454
1463
  const [activityLabel, setActivityLabel] = useState(null);
1464
+ const activityStepIdCounter = useRef(0);
1455
1465
  // When stop is clicked during reconnect, keep content visible (don't wipe it)
1456
1466
  const [reconnectFrozen, setReconnectFrozen] = useState(false);
1457
1467
  const reconnectRunIdRef = useRef(null);
@@ -1487,6 +1497,201 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1487
1497
  const onGenerateTitleRef = useRef(onGenerateTitle);
1488
1498
  onGenerateTitleRef.current = onGenerateTitle;
1489
1499
  const titleGeneratedRef = useRef(false);
1500
+ const importThreadData = useCallback((threadData, options) => {
1501
+ const repo = typeof threadData === "string" ? JSON.parse(threadData) : threadData;
1502
+ if (repo?.messages?.length > 0) {
1503
+ if (options?.markTitleGenerated) {
1504
+ titleGeneratedRef.current = true;
1505
+ }
1506
+ threadRuntime.import(ensureMessageMetadata(repo));
1507
+ }
1508
+ if (Array.isArray(repo?.queuedMessages)) {
1509
+ setQueuedMessages(repo.queuedMessages);
1510
+ lastPersistedQueueRef.current = JSON.stringify(repo.queuedMessages);
1511
+ }
1512
+ return repo;
1513
+ }, [threadRuntime]);
1514
+ const refreshThreadFromServer = useCallback(async () => {
1515
+ if (!threadId)
1516
+ return null;
1517
+ try {
1518
+ const refreshRes = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}`);
1519
+ if (!refreshRes.ok)
1520
+ return null;
1521
+ const refreshData = await refreshRes.json();
1522
+ if (!refreshData.threadData)
1523
+ return null;
1524
+ return importThreadData(refreshData.threadData);
1525
+ }
1526
+ catch {
1527
+ return null;
1528
+ }
1529
+ }, [apiUrl, importThreadData, threadId]);
1530
+ const wasRecentlyStoppedRun = useCallback((runId) => {
1531
+ const stopped = userStoppedRunRef.current;
1532
+ return Boolean(stopped &&
1533
+ Date.now() - stopped.at < 10_000 &&
1534
+ (!stopped.runId || !runId || stopped.runId === runId));
1535
+ }, []);
1536
+ const startReconnectToRun = useCallback((runInfo) => {
1537
+ if (!threadId || !runInfo.runId || runInfo.status !== "running") {
1538
+ return false;
1539
+ }
1540
+ const runId = String(runInfo.runId);
1541
+ if (wasRecentlyStoppedRun(runId))
1542
+ return false;
1543
+ if (reconnectRunIdRef.current === runId)
1544
+ return true;
1545
+ reconnectRunIdRef.current = runId;
1546
+ setIsReconnecting(true);
1547
+ setReconnectFrozen(false);
1548
+ setReconnectContent([]);
1549
+ window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
1550
+ detail: { isRunning: true, tabId: tabId || threadId },
1551
+ }));
1552
+ const abortCtrl = new AbortController();
1553
+ reconnectAbortRef.current = abortCtrl;
1554
+ const watchdog = setInterval(async () => {
1555
+ try {
1556
+ const res = await fetch(`${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`);
1557
+ if (!res.ok) {
1558
+ abortCtrl.abort();
1559
+ clearInterval(watchdog);
1560
+ return;
1561
+ }
1562
+ const info = (await res.json());
1563
+ if (info.status !== "running" || activeRunLooksStale(info)) {
1564
+ abortCtrl.abort();
1565
+ clearInterval(watchdog);
1566
+ }
1567
+ }
1568
+ catch {
1569
+ // Network blip — keep polling.
1570
+ }
1571
+ }, 1000);
1572
+ let reconnectTimedOut = false;
1573
+ const maxReconnectTimer = setTimeout(() => {
1574
+ reconnectTimedOut = true;
1575
+ abortCtrl.abort();
1576
+ clearInterval(watchdog);
1577
+ }, 20_000);
1578
+ const streamReconnect = async () => {
1579
+ let noProgressDuringReconnect = false;
1580
+ let latestContent = [];
1581
+ try {
1582
+ const sseRes = await fetch(`${apiUrl}/runs/${encodeURIComponent(runId)}/events?after=0`, { signal: abortCtrl.signal });
1583
+ if (sseRes.ok && sseRes.body) {
1584
+ const content = [];
1585
+ latestContent = content;
1586
+ const toolCallCounter = { value: 0 };
1587
+ let rafPending = false;
1588
+ let latestSnapshot = [];
1589
+ const scheduleUpdate = (snapshot) => {
1590
+ latestSnapshot = snapshot;
1591
+ if (rafPending)
1592
+ return;
1593
+ rafPending = true;
1594
+ requestAnimationFrame(() => {
1595
+ rafPending = false;
1596
+ setReconnectContent(latestSnapshot);
1597
+ });
1598
+ };
1599
+ await readSSEStreamRaw(sseRes.body, content, toolCallCounter, tabId, scheduleUpdate);
1600
+ setReconnectContent([...content]);
1601
+ }
1602
+ }
1603
+ catch (err) {
1604
+ if (err instanceof AgentAutoContinueSignal &&
1605
+ err.reason === "no_progress") {
1606
+ noProgressDuringReconnect = true;
1607
+ }
1608
+ else if (reconnectTimedOut &&
1609
+ err instanceof Error &&
1610
+ err.name === "AbortError") {
1611
+ noProgressDuringReconnect = true;
1612
+ }
1613
+ }
1614
+ finally {
1615
+ clearInterval(watchdog);
1616
+ clearTimeout(maxReconnectTimer);
1617
+ }
1618
+ if (noProgressDuringReconnect && reconnectRunIdRef.current === runId) {
1619
+ try {
1620
+ await fetch(`${apiUrl}/runs/${encodeURIComponent(runId)}/abort`, {
1621
+ method: "POST",
1622
+ headers: { "Content-Type": "application/json" },
1623
+ body: JSON.stringify({ reason: "no_progress" }),
1624
+ });
1625
+ }
1626
+ catch {
1627
+ // Best effort — the important part is unwinding the UI.
1628
+ }
1629
+ setReconnectContent([...latestContent]);
1630
+ setReconnectFrozen(latestContent.length > 0);
1631
+ setRunErrorInfo({
1632
+ message: "The previous agent run stopped producing visible progress while reconnecting, so it was stopped before it could keep looping.",
1633
+ errorCode: "reconnect_no_progress",
1634
+ recoverable: true,
1635
+ runId,
1636
+ });
1637
+ setDismissedRunErrorKey(null);
1638
+ reconnectAbortRef.current = null;
1639
+ setIsReconnecting(false);
1640
+ reconnectRunIdRef.current = null;
1641
+ window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
1642
+ detail: { isRunning: false, tabId: tabId || threadId },
1643
+ }));
1644
+ return;
1645
+ }
1646
+ setReconnectFrozen(true);
1647
+ let loaded = false;
1648
+ for (let attempt = 0; attempt < 10; attempt++) {
1649
+ await new Promise((r) => setTimeout(r, 500));
1650
+ if (reconnectRunIdRef.current !== runId)
1651
+ break;
1652
+ const repo = await refreshThreadFromServer();
1653
+ if (repoHasAssistantMessage(repo)) {
1654
+ setReconnectContent([]);
1655
+ setReconnectFrozen(false);
1656
+ loaded = true;
1657
+ break;
1658
+ }
1659
+ }
1660
+ if (reconnectRunIdRef.current === runId) {
1661
+ reconnectAbortRef.current = null;
1662
+ setIsReconnecting(false);
1663
+ reconnectRunIdRef.current = null;
1664
+ window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
1665
+ detail: { isRunning: false, tabId: tabId || threadId },
1666
+ }));
1667
+ }
1668
+ if (!loaded) {
1669
+ await refreshThreadFromServer();
1670
+ }
1671
+ };
1672
+ void streamReconnect();
1673
+ return true;
1674
+ }, [apiUrl, refreshThreadFromServer, tabId, threadId, wasRecentlyStoppedRun]);
1675
+ const reconnectActiveRunForThread = useCallback(async () => {
1676
+ if (!threadId)
1677
+ return false;
1678
+ try {
1679
+ const runRes = await fetch(`${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`);
1680
+ if (!runRes.ok)
1681
+ return false;
1682
+ const runInfo = (await runRes.json());
1683
+ if (!runInfo.active ||
1684
+ runInfo.status !== "running" ||
1685
+ activeRunLooksStale(runInfo)) {
1686
+ await refreshThreadFromServer();
1687
+ return false;
1688
+ }
1689
+ return startReconnectToRun(runInfo);
1690
+ }
1691
+ catch {
1692
+ return false;
1693
+ }
1694
+ }, [apiUrl, refreshThreadFromServer, startReconnectToRun, threadId]);
1490
1695
  // Restore messages from server on mount (when threadId is set)
1491
1696
  useEffect(() => {
1492
1697
  if (hasRestoredRef.current)
@@ -1501,238 +1706,15 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1501
1706
  return;
1502
1707
  const data = await res.json();
1503
1708
  if (data.threadData) {
1504
- const repo = typeof data.threadData === "string"
1505
- ? JSON.parse(data.threadData)
1506
- : data.threadData;
1507
- if (repo?.messages?.length > 0) {
1508
- titleGeneratedRef.current = true; // Don't re-generate for restored threads
1509
- threadRuntime.import(ensureMessageMetadata(repo));
1510
- }
1511
- // Restore user-queued messages that were persisted before reload.
1512
- if (Array.isArray(repo?.queuedMessages)) {
1513
- setQueuedMessages(repo.queuedMessages);
1514
- // Mark as restored so the debounced save effect doesn't write
1515
- // the same data back to the server on mount.
1516
- lastPersistedQueueRef.current = JSON.stringify(repo.queuedMessages);
1517
- }
1709
+ importThreadData(data.threadData, { markTitleGenerated: true });
1518
1710
  }
1519
1711
  // Also skip title generation if thread already has a title
1520
1712
  if (data.title) {
1521
1713
  titleGeneratedRef.current = true;
1522
1714
  }
1523
- // Check if there's an active run for this thread (e.g. after hot reload)
1524
- try {
1525
- const runRes = await fetch(`${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`);
1526
- if (runRes.ok) {
1527
- const runInfo = await runRes.json();
1528
- // Defense in depth: if the server says status="running" but the
1529
- // heartbeat is stale (producer died before the server-side reap
1530
- // sweep noticed), treat it as dead. 5s tolerates normal jitter
1531
- // around the 1.5s heartbeat without false positives.
1532
- const heartbeatAt = typeof runInfo.heartbeatAt === "number"
1533
- ? runInfo.heartbeatAt
1534
- : null;
1535
- const looksStale = runInfo.status === "running" &&
1536
- heartbeatAt != null &&
1537
- Date.now() - heartbeatAt > 5000;
1538
- // If the run already completed or looks stale, just re-fetch
1539
- // thread data (don't enter "Thinking." reconnection mode).
1540
- if (runInfo.status !== "running" || looksStale) {
1541
- try {
1542
- const refreshRes = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}`);
1543
- if (refreshRes.ok) {
1544
- const refreshData = await refreshRes.json();
1545
- if (refreshData.threadData) {
1546
- const repo = typeof refreshData.threadData === "string"
1547
- ? JSON.parse(refreshData.threadData)
1548
- : refreshData.threadData;
1549
- if (repo?.messages?.length > 0) {
1550
- threadRuntime.import(ensureMessageMetadata(repo));
1551
- }
1552
- if (Array.isArray(repo?.queuedMessages)) {
1553
- setQueuedMessages(repo.queuedMessages);
1554
- lastPersistedQueueRef.current = JSON.stringify(repo.queuedMessages);
1555
- }
1556
- }
1557
- }
1558
- }
1559
- catch { }
1560
- // Skip reconnection entirely
1561
- }
1562
- else {
1563
- // Agent is still running — subscribe to live SSE stream
1564
- reconnectRunIdRef.current = runInfo.runId;
1565
- setIsReconnecting(true);
1566
- setReconnectContent([]);
1567
- // Signal tab running indicator
1568
- window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
1569
- detail: { isRunning: true, tabId: tabId || threadId },
1570
- }));
1571
- // Create AbortController before the async call so stop button
1572
- // can abort it even if clicked before the function body runs.
1573
- const abortCtrl = new AbortController();
1574
- reconnectAbortRef.current = abortCtrl;
1575
- // Watchdog: poll /runs/active every 1s to detect when the run
1576
- // is no longer running server-side, or the heartbeat has gone
1577
- // stale (producer died). Aborts the SSE fetch so we fall
1578
- // through to thread refresh instead of showing "Thinking..."
1579
- // forever.
1580
- const watchdog = setInterval(async () => {
1581
- try {
1582
- const res = await fetch(`${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`);
1583
- if (!res.ok) {
1584
- abortCtrl.abort();
1585
- clearInterval(watchdog);
1586
- return;
1587
- }
1588
- const info = await res.json();
1589
- const hb = typeof info.heartbeatAt === "number"
1590
- ? info.heartbeatAt
1591
- : null;
1592
- const stale = info.status === "running" &&
1593
- hb != null &&
1594
- Date.now() - hb > 5000;
1595
- if (info.status !== "running" || stale) {
1596
- abortCtrl.abort();
1597
- clearInterval(watchdog);
1598
- }
1599
- }
1600
- catch {
1601
- // Network blip — keep polling
1602
- }
1603
- }, 1000);
1604
- // Hard cap: no single reconnect should wedge the UI for
1605
- // more than 20s. With the 1s watchdog + stale-heartbeat
1606
- // detection + startup reap, this only triggers in truly
1607
- // pathological cases. Keeps "Reconnecting…" from feeling
1608
- // infinite.
1609
- let reconnectTimedOut = false;
1610
- const maxReconnectTimer = setTimeout(() => {
1611
- reconnectTimedOut = true;
1612
- abortCtrl.abort();
1613
- clearInterval(watchdog);
1614
- }, 20_000);
1615
- const streamReconnect = async () => {
1616
- let noProgressDuringReconnect = false;
1617
- let latestContent = [];
1618
- try {
1619
- const sseRes = await fetch(`${apiUrl}/runs/${encodeURIComponent(runInfo.runId)}/events?after=0`, { signal: abortCtrl.signal });
1620
- if (sseRes.ok && sseRes.body) {
1621
- const content = [];
1622
- latestContent = content;
1623
- const toolCallCounter = { value: 0 };
1624
- // Throttle React state updates via requestAnimationFrame
1625
- let rafPending = false;
1626
- let latestSnapshot = [];
1627
- const scheduleUpdate = (snapshot) => {
1628
- latestSnapshot = snapshot;
1629
- if (!rafPending) {
1630
- rafPending = true;
1631
- requestAnimationFrame(() => {
1632
- rafPending = false;
1633
- setReconnectContent(latestSnapshot);
1634
- });
1635
- }
1636
- };
1637
- await readSSEStreamRaw(sseRes.body, content, toolCallCounter, tabId, scheduleUpdate);
1638
- // Final update with complete content
1639
- setReconnectContent([...content]);
1640
- }
1641
- }
1642
- catch (err) {
1643
- if (err instanceof AgentAutoContinueSignal &&
1644
- err.reason === "no_progress") {
1645
- noProgressDuringReconnect = true;
1646
- }
1647
- else if (reconnectTimedOut &&
1648
- err instanceof Error &&
1649
- err.name === "AbortError") {
1650
- noProgressDuringReconnect = true;
1651
- }
1652
- // Other stream errors/aborts fall through to re-fetch.
1653
- }
1654
- finally {
1655
- clearInterval(watchdog);
1656
- clearTimeout(maxReconnectTimer);
1657
- }
1658
- if (noProgressDuringReconnect && reconnectRunIdRef.current) {
1659
- try {
1660
- await fetch(`${apiUrl}/runs/${encodeURIComponent(runInfo.runId)}/abort`, {
1661
- method: "POST",
1662
- headers: { "Content-Type": "application/json" },
1663
- body: JSON.stringify({ reason: "no_progress" }),
1664
- });
1665
- }
1666
- catch {
1667
- // Best effort — the important part is unwinding the UI.
1668
- }
1669
- setReconnectContent([...latestContent]);
1670
- setReconnectFrozen(latestContent.length > 0);
1671
- setRunErrorInfo({
1672
- message: "The previous agent run stopped producing visible progress while reconnecting, so it was stopped before it could keep looping.",
1673
- errorCode: "reconnect_no_progress",
1674
- recoverable: true,
1675
- runId: runInfo.runId,
1676
- });
1677
- setDismissedRunErrorKey(null);
1678
- reconnectAbortRef.current = null;
1679
- setIsReconnecting(false);
1680
- reconnectRunIdRef.current = null;
1681
- window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
1682
- detail: { isRunning: false, tabId: tabId || threadId },
1683
- }));
1684
- return;
1685
- }
1686
- // Poll for thread data — server's updateThreadData may not have
1687
- // committed yet when the SSE `done` event fires, so retry until
1688
- // an assistant message appears (up to ~5 s) before clearing.
1689
- setReconnectFrozen(true);
1690
- let loaded = false;
1691
- for (let attempt = 0; attempt < 10; attempt++) {
1692
- await new Promise((r) => setTimeout(r, 500));
1693
- // If the stop button fired mid-poll, bail out
1694
- if (!reconnectRunIdRef.current)
1695
- break;
1696
- try {
1697
- const refreshRes = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}`);
1698
- if (refreshRes.ok) {
1699
- const refreshData = await refreshRes.json();
1700
- if (refreshData.threadData) {
1701
- const repo = typeof refreshData.threadData === "string"
1702
- ? JSON.parse(refreshData.threadData)
1703
- : refreshData.threadData;
1704
- const hasAssistant = repo?.messages?.some((m) => (m.message?.role ?? m.role) === "assistant");
1705
- if (hasAssistant) {
1706
- threadRuntime.import(ensureMessageMetadata(repo));
1707
- setReconnectContent([]);
1708
- setReconnectFrozen(false);
1709
- loaded = true;
1710
- break;
1711
- }
1712
- }
1713
- }
1714
- }
1715
- catch { }
1716
- }
1717
- // Only clean up if the stop button hasn't already done it
1718
- if (reconnectRunIdRef.current) {
1719
- reconnectAbortRef.current = null;
1720
- // If loaded=true, reconnectContent already cleared above.
1721
- // If loaded=false (timeout), keep content frozen so user sees what happened.
1722
- setIsReconnecting(false);
1723
- reconnectRunIdRef.current = null;
1724
- window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
1725
- detail: { isRunning: false, tabId: tabId || threadId },
1726
- }));
1727
- }
1728
- };
1729
- streamReconnect();
1730
- } // end else (running)
1731
- }
1732
- }
1733
- catch {
1734
- // No active run — nothing to reconnect to
1735
- }
1715
+ // Check if there's an active run for this thread (e.g. after hot
1716
+ // reload), and reconnect to it if it is still running.
1717
+ await reconnectActiveRunForThread();
1736
1718
  }
1737
1719
  catch {
1738
1720
  // Start fresh
@@ -1757,7 +1739,48 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1757
1739
  catch { }
1758
1740
  setIsRestoring(false);
1759
1741
  }
1760
- }, [threadId, tabId, apiUrl, threadRuntime]);
1742
+ }, [
1743
+ threadId,
1744
+ tabId,
1745
+ apiUrl,
1746
+ threadRuntime,
1747
+ importThreadData,
1748
+ reconnectActiveRunForThread,
1749
+ ]);
1750
+ // If assistant-ui stops the local runtime while the background server run is
1751
+ // still alive, immediately switch into the same reconnect path used after a
1752
+ // reload. Otherwise the composer unlocks, the next send hits a 409, and the
1753
+ // user sees "still working" even though the UI stopped updating.
1754
+ const prevRuntimeRunningForReconnectRef = useRef(isRuntimeRunning);
1755
+ useEffect(() => {
1756
+ const wasRuntimeRunning = prevRuntimeRunningForReconnectRef.current;
1757
+ prevRuntimeRunningForReconnectRef.current = isRuntimeRunning;
1758
+ if (!wasRuntimeRunning ||
1759
+ isRuntimeRunning ||
1760
+ !threadId ||
1761
+ forceStopped ||
1762
+ isReconnecting ||
1763
+ wasRecentlyStoppedRun()) {
1764
+ return;
1765
+ }
1766
+ let cancelled = false;
1767
+ const timer = window.setTimeout(() => {
1768
+ if (!cancelled) {
1769
+ void reconnectActiveRunForThread();
1770
+ }
1771
+ }, 250);
1772
+ return () => {
1773
+ cancelled = true;
1774
+ window.clearTimeout(timer);
1775
+ };
1776
+ }, [
1777
+ forceStopped,
1778
+ isReconnecting,
1779
+ isRuntimeRunning,
1780
+ reconnectActiveRunForThread,
1781
+ threadId,
1782
+ wasRecentlyStoppedRun,
1783
+ ]);
1761
1784
  // Generate a title when the first user message is sent
1762
1785
  useEffect(() => {
1763
1786
  if (!hasRestoredRef.current)
@@ -2030,7 +2053,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2030
2053
  return [
2031
2054
  ...prev,
2032
2055
  {
2033
- id: `${Date.now()}-${prev.length}`,
2056
+ id: `${Date.now()}-${++activityStepIdCounter.current}`,
2034
2057
  label,
2035
2058
  ...(tool ? { tool } : {}),
2036
2059
  },