@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.
- package/dist/agent/engine/credential-errors.d.ts +1 -1
- package/dist/agent/engine/credential-errors.d.ts.map +1 -1
- package/dist/agent/engine/credential-errors.js +2 -2
- package/dist/agent/engine/credential-errors.js.map +1 -1
- package/dist/cli/workspace-dev.d.ts.map +1 -1
- package/dist/cli/workspace-dev.js +1 -0
- package/dist/cli/workspace-dev.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +5 -5
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +252 -229
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
- package/dist/client/ConnectBuilderCard.js +23 -5
- package/dist/client/ConnectBuilderCard.js.map +1 -1
- package/dist/client/components/CodeRequiredDialog.d.ts.map +1 -1
- package/dist/client/components/CodeRequiredDialog.js +11 -4
- package/dist/client/components/CodeRequiredDialog.js.map +1 -1
- package/dist/client/frame.d.ts.map +1 -1
- package/dist/client/frame.js +25 -9
- package/dist/client/frame.js.map +1 -1
- package/dist/client/integrations/IntegrationsPanel.d.ts.map +1 -1
- package/dist/client/integrations/IntegrationsPanel.js +28 -2
- package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
- package/dist/client/settings/BackgroundAgentSection.d.ts.map +1 -1
- package/dist/client/settings/BackgroundAgentSection.js +2 -1
- package/dist/client/settings/BackgroundAgentSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +13 -1
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +4 -0
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/client/sse-event-processor.js +1 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/db/client.d.ts +6 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +50 -25
- package/dist/db/client.js.map +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/deploy/workspace-deploy.js +7 -0
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/onboarding/default-steps.js +1 -1
- package/dist/onboarding/default-steps.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +11 -5
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/google-oauth.d.ts +8 -7
- package/dist/server/google-oauth.d.ts.map +1 -1
- package/dist/server/google-oauth.js +56 -54
- package/dist/server/google-oauth.js.map +1 -1
- 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;
|
|
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
|
-
|
|
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
|
|
1524
|
-
|
|
1525
|
-
|
|
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
|
-
}, [
|
|
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()}-${
|
|
2056
|
+
id: `${Date.now()}-${++activityStepIdCounter.current}`,
|
|
2034
2057
|
label,
|
|
2035
2058
|
...(tool ? { tool } : {}),
|
|
2036
2059
|
},
|