@aomi-labs/react 0.3.16 → 0.3.18

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/index.cjs CHANGED
@@ -61,6 +61,7 @@ __export(index_exports, {
61
61
  SUPPORTED_CHAINS: () => SUPPORTED_CHAINS,
62
62
  ThreadContextProvider: () => ThreadContextProvider,
63
63
  UserContextProvider: () => UserContextProvider,
64
+ UserState: () => import_client2.UserState,
64
65
  aaModeFromExecutionKind: () => import_client8.aaModeFromExecutionKind,
65
66
  appendFeeCallToPayload: () => import_client8.appendFeeCallToPayload,
66
67
  buildFeeAAWalletCall: () => import_client8.buildFeeAAWalletCall,
@@ -189,6 +190,29 @@ var ThreadStore = class {
189
190
  this.getThreadMetadata = (threadId) => {
190
191
  return this.state.threadMetadata.get(threadId);
191
192
  };
193
+ /** Reset store to a single empty "New Chat" thread (e.g. on wallet disconnect). */
194
+ this.resetToDefault = () => {
195
+ const threadId = generateUUID();
196
+ this.state = {
197
+ currentThreadId: threadId,
198
+ threadViewKey: this.state.threadViewKey + 1,
199
+ threadCnt: 1,
200
+ threads: /* @__PURE__ */ new Map([[threadId, []]]),
201
+ threadMetadata: /* @__PURE__ */ new Map([
202
+ [
203
+ threadId,
204
+ {
205
+ title: "New Chat",
206
+ status: "regular",
207
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
208
+ control: initThreadControl()
209
+ }
210
+ ]
211
+ ])
212
+ };
213
+ this.snapshot = this.buildSnapshot();
214
+ this.emit();
215
+ };
192
216
  this.updateThreadMetadata = (threadId, updates) => {
193
217
  const existing = this.state.threadMetadata.get(threadId);
194
218
  if (!existing) {
@@ -266,7 +290,8 @@ var ThreadStore = class {
266
290
  getThreadMessages: this.getThreadMessages,
267
291
  setThreadMessages: this.setThreadMessages,
268
292
  getThreadMetadata: this.getThreadMetadata,
269
- updateThreadMetadata: this.updateThreadMetadata
293
+ updateThreadMetadata: this.updateThreadMetadata,
294
+ resetToDefault: this.resetToDefault
270
295
  };
271
296
  }
272
297
  };
@@ -497,7 +522,7 @@ function ControlContextProvider({
497
522
  }
498
523
  };
499
524
  void fetchApps();
500
- }, [state.apiKey, publicKey, sessionId]);
525
+ }, [state.apiKey, publicKey]);
501
526
  (0, import_react.useEffect)(() => {
502
527
  const fetchModels = async () => {
503
528
  try {
@@ -1205,6 +1230,9 @@ var SessionManager = class {
1205
1230
  get(threadId) {
1206
1231
  return this.sessions.get(threadId);
1207
1232
  }
1233
+ get size() {
1234
+ return this.sessions.size;
1235
+ }
1208
1236
  forEach(callback) {
1209
1237
  for (const [threadId, session] of this.sessions) {
1210
1238
  callback(session, threadId);
@@ -1217,6 +1245,21 @@ var SessionManager = class {
1217
1245
  this.sessions.delete(threadId);
1218
1246
  }
1219
1247
  }
1248
+ closeIdleExcept(activeThreadId, onBeforeClose) {
1249
+ const closedThreadIds = [];
1250
+ for (const [threadId, session] of this.sessions) {
1251
+ if (threadId === activeThreadId) continue;
1252
+ if (session.getIsProcessing()) continue;
1253
+ if (session.getIsPolling()) continue;
1254
+ if (session.getPendingRequests().length > 0) continue;
1255
+ closedThreadIds.push(threadId);
1256
+ }
1257
+ for (const threadId of closedThreadIds) {
1258
+ onBeforeClose == null ? void 0 : onBeforeClose(threadId);
1259
+ this.close(threadId);
1260
+ }
1261
+ return closedThreadIds;
1262
+ }
1220
1263
  closeAll() {
1221
1264
  for (const [threadId, session] of this.sessions) {
1222
1265
  session.close();
@@ -1328,28 +1371,113 @@ var SUPPORTED_CHAINS = [
1328
1371
  var getChainInfo = (chainId) => chainId === void 0 ? void 0 : SUPPORTED_CHAINS.find((c) => c.id === chainId);
1329
1372
 
1330
1373
  // packages/react/src/runtime/orchestrator.ts
1374
+ var toErrorMessage = (error) => error instanceof Error ? error.message : "Message failed to send";
1375
+ var getOptimisticStatus = (message) => {
1376
+ var _a, _b;
1377
+ const status = (_b = (_a = message.metadata) == null ? void 0 : _a.custom) == null ? void 0 : _b.aomiSendStatus;
1378
+ return status === "sending" || status === "sent" || status === "failed" ? status : void 0;
1379
+ };
1380
+ var hasUnhydratedOptimisticMessage = (messages) => messages.some((message) => {
1381
+ const status = getOptimisticStatus(message);
1382
+ return status === "sending" || status === "sent";
1383
+ });
1384
+ var withOptimisticStatus = (message, status, error) => {
1385
+ var _a, _b;
1386
+ const custom = __spreadProps(__spreadValues({}, (_b = (_a = message.metadata) == null ? void 0 : _a.custom) != null ? _b : {}), {
1387
+ aomiSendStatus: status
1388
+ });
1389
+ if (error) {
1390
+ custom.aomiSendError = toErrorMessage(error);
1391
+ } else {
1392
+ delete custom.aomiSendError;
1393
+ }
1394
+ return __spreadProps(__spreadValues({}, message), {
1395
+ metadata: __spreadProps(__spreadValues({}, message.metadata), {
1396
+ custom
1397
+ })
1398
+ });
1399
+ };
1400
+ var updateOptimisticMessage = (threadContext, threadId, messageId, status, error) => {
1401
+ const messages = threadContext.getThreadMessages(threadId);
1402
+ let changed = false;
1403
+ const nextMessages = messages.map((message) => {
1404
+ if (message.id !== messageId) return message;
1405
+ changed = true;
1406
+ return withOptimisticStatus(message, status, error);
1407
+ });
1408
+ if (changed) {
1409
+ threadContext.setThreadMessages(threadId, nextMessages);
1410
+ }
1411
+ };
1331
1412
  function useRuntimeOrchestrator(aomiClient, options) {
1332
1413
  const threadContext = useThreadContext();
1333
1414
  const threadContextRef = (0, import_react6.useRef)(threadContext);
1334
1415
  threadContextRef.current = threadContext;
1335
1416
  const aomiClientRef = (0, import_react6.useRef)(aomiClient);
1336
1417
  aomiClientRef.current = aomiClient;
1418
+ const optionsRef = (0, import_react6.useRef)(options);
1419
+ optionsRef.current = options;
1337
1420
  const [isRunning, setIsRunning] = (0, import_react6.useState)(false);
1338
1421
  const sessionManagerRef = (0, import_react6.useRef)(null);
1339
1422
  if (!sessionManagerRef.current) {
1340
1423
  sessionManagerRef.current = new SessionManager(() => aomiClientRef.current);
1341
1424
  }
1342
1425
  const pendingFetches = (0, import_react6.useRef)(/* @__PURE__ */ new Set());
1426
+ const initialStatePromises = (0, import_react6.useRef)(/* @__PURE__ */ new Map());
1427
+ const hydratedThreadIds = (0, import_react6.useRef)(/* @__PURE__ */ new Set());
1343
1428
  const listenerCleanups = (0, import_react6.useRef)(/* @__PURE__ */ new Map());
1429
+ const cleanupSessionListeners = (0, import_react6.useCallback)((threadId) => {
1430
+ var _a;
1431
+ (_a = listenerCleanups.current.get(threadId)) == null ? void 0 : _a();
1432
+ listenerCleanups.current.delete(threadId);
1433
+ }, []);
1434
+ const closeSession = (0, import_react6.useCallback)(
1435
+ (threadId) => {
1436
+ var _a;
1437
+ cleanupSessionListeners(threadId);
1438
+ pendingFetches.current.delete(threadId);
1439
+ initialStatePromises.current.delete(threadId);
1440
+ hydratedThreadIds.current.delete(threadId);
1441
+ (_a = sessionManagerRef.current) == null ? void 0 : _a.close(threadId);
1442
+ },
1443
+ [cleanupSessionListeners]
1444
+ );
1445
+ const closeIdleSessionsExcept = (0, import_react6.useCallback)(
1446
+ (activeThreadId) => {
1447
+ var _a, _b;
1448
+ const closedThreadIds = (_b = (_a = sessionManagerRef.current) == null ? void 0 : _a.closeIdleExcept(
1449
+ activeThreadId,
1450
+ cleanupSessionListeners
1451
+ )) != null ? _b : [];
1452
+ for (const threadId of closedThreadIds) {
1453
+ pendingFetches.current.delete(threadId);
1454
+ initialStatePromises.current.delete(threadId);
1455
+ hydratedThreadIds.current.delete(threadId);
1456
+ }
1457
+ return closedThreadIds;
1458
+ },
1459
+ [cleanupSessionListeners]
1460
+ );
1461
+ const closeAllSessions = (0, import_react6.useCallback)(() => {
1462
+ var _a;
1463
+ pendingFetches.current.clear();
1464
+ initialStatePromises.current.clear();
1465
+ hydratedThreadIds.current.clear();
1466
+ for (const threadId of Array.from(listenerCleanups.current.keys())) {
1467
+ cleanupSessionListeners(threadId);
1468
+ }
1469
+ (_a = sessionManagerRef.current) == null ? void 0 : _a.closeAll();
1470
+ }, [cleanupSessionListeners]);
1344
1471
  const getSession = (0, import_react6.useCallback)(
1345
1472
  (threadId) => {
1346
1473
  var _a, _b, _c, _d, _e;
1347
1474
  const manager = sessionManagerRef.current;
1348
- const nextApp = options.getApp();
1349
- const nextPublicKey = (_a = options.getPublicKey) == null ? void 0 : _a.call(options);
1350
- const nextApiKey = (_c = (_b = options.getApiKey) == null ? void 0 : _b.call(options)) != null ? _c : void 0;
1351
- const nextClientId = (_d = options.getClientId) == null ? void 0 : _d.call(options);
1352
- const nextUserState = (_e = options.getUserState) == null ? void 0 : _e.call(options);
1475
+ const nextOptions = optionsRef.current;
1476
+ const nextApp = nextOptions.getApp();
1477
+ const nextPublicKey = (_a = nextOptions.getPublicKey) == null ? void 0 : _a.call(nextOptions);
1478
+ const nextApiKey = (_c = (_b = nextOptions.getApiKey) == null ? void 0 : _b.call(nextOptions)) != null ? _c : void 0;
1479
+ const nextClientId = (_d = nextOptions.getClientId) == null ? void 0 : _d.call(nextOptions);
1480
+ const nextUserState = (_e = nextOptions.getUserState) == null ? void 0 : _e.call(nextOptions);
1353
1481
  const existing = manager.get(threadId);
1354
1482
  if (existing) {
1355
1483
  existing.syncRuntimeOptions({
@@ -1378,6 +1506,10 @@ function useRuntimeOrchestrator(aomiClient, options) {
1378
1506
  const converted = toInboundMessage(msg);
1379
1507
  if (converted) threadMessages.push(converted);
1380
1508
  }
1509
+ const existingMessages = threadContextRef.current.getThreadMessages(threadId);
1510
+ if (threadMessages.length === 0 && hasUnhydratedOptimisticMessage(existingMessages)) {
1511
+ return;
1512
+ }
1381
1513
  threadContextRef.current.setThreadMessages(threadId, threadMessages);
1382
1514
  })
1383
1515
  );
@@ -1399,8 +1531,8 @@ function useRuntimeOrchestrator(aomiClient, options) {
1399
1531
  session.on(
1400
1532
  "wallet_requests_changed",
1401
1533
  (requests) => {
1402
- var _a2;
1403
- return (_a2 = options.onPendingRequestsChange) == null ? void 0 : _a2.call(options, requests);
1534
+ var _a2, _b2;
1535
+ return (_b2 = (_a2 = optionsRef.current).onPendingRequestsChange) == null ? void 0 : _b2.call(_a2, requests);
1404
1536
  }
1405
1537
  )
1406
1538
  );
@@ -1409,10 +1541,17 @@ function useRuntimeOrchestrator(aomiClient, options) {
1409
1541
  threadContextRef.current.updateThreadMetadata(threadId, { title });
1410
1542
  })
1411
1543
  );
1412
- const forwardEvent = (type) => session.on(type, (payload) => {
1413
- var _a2;
1414
- (_a2 = options.onEvent) == null ? void 0 : _a2.call(options, { type, payload, sessionId: threadId });
1415
- });
1544
+ const forwardEvent = (type) => session.on(
1545
+ type,
1546
+ (payload) => {
1547
+ var _a2, _b2;
1548
+ (_b2 = (_a2 = optionsRef.current).onEvent) == null ? void 0 : _b2.call(_a2, {
1549
+ type,
1550
+ payload,
1551
+ sessionId: threadId
1552
+ });
1553
+ }
1554
+ );
1416
1555
  cleanups.push(forwardEvent("tool_update"));
1417
1556
  cleanups.push(forwardEvent("tool_complete"));
1418
1557
  cleanups.push(forwardEvent("system_notice"));
@@ -1428,36 +1567,68 @@ function useRuntimeOrchestrator(aomiClient, options) {
1428
1567
  );
1429
1568
  const ensureInitialState = (0, import_react6.useCallback)(
1430
1569
  async (threadId) => {
1431
- var _a;
1432
- if (pendingFetches.current.has(threadId)) return;
1433
- pendingFetches.current.add(threadId);
1434
- try {
1435
- const session = getSession(threadId);
1436
- await session.fetchCurrentState();
1437
- (_a = options.onPendingRequestsChange) == null ? void 0 : _a.call(options, session.getPendingRequests());
1438
- if (threadContextRef.current.currentThreadId === threadId) {
1439
- setIsRunning(session.getIsProcessing());
1440
- }
1441
- } catch (error) {
1442
- console.error("Failed to fetch initial state:", error);
1570
+ var _a, _b, _c;
1571
+ const existingPromise = initialStatePromises.current.get(threadId);
1572
+ if (existingPromise) {
1573
+ return existingPromise;
1574
+ }
1575
+ const cachedMessages = threadContextRef.current.getThreadMessages(threadId);
1576
+ const existingSession = (_a = sessionManagerRef.current) == null ? void 0 : _a.get(threadId);
1577
+ if (existingSession && (hydratedThreadIds.current.has(threadId) || cachedMessages.length > 0)) {
1578
+ (_c = (_b = optionsRef.current).onPendingRequestsChange) == null ? void 0 : _c.call(
1579
+ _b,
1580
+ existingSession.getPendingRequests()
1581
+ );
1443
1582
  if (threadContextRef.current.currentThreadId === threadId) {
1444
- setIsRunning(false);
1583
+ setIsRunning(existingSession.getIsProcessing());
1445
1584
  }
1446
- } finally {
1447
- pendingFetches.current.delete(threadId);
1585
+ return;
1448
1586
  }
1587
+ const fetchPromise = (async () => {
1588
+ var _a2, _b2;
1589
+ pendingFetches.current.add(threadId);
1590
+ try {
1591
+ const session = getSession(threadId);
1592
+ await session.fetchCurrentState();
1593
+ hydratedThreadIds.current.add(threadId);
1594
+ (_b2 = (_a2 = optionsRef.current).onPendingRequestsChange) == null ? void 0 : _b2.call(
1595
+ _a2,
1596
+ session.getPendingRequests()
1597
+ );
1598
+ if (threadContextRef.current.currentThreadId === threadId) {
1599
+ setIsRunning(session.getIsProcessing());
1600
+ }
1601
+ } catch (error) {
1602
+ console.error("Failed to fetch initial state:", error);
1603
+ if (threadContextRef.current.currentThreadId === threadId) {
1604
+ setIsRunning(false);
1605
+ }
1606
+ } finally {
1607
+ pendingFetches.current.delete(threadId);
1608
+ initialStatePromises.current.delete(threadId);
1609
+ }
1610
+ })();
1611
+ initialStatePromises.current.set(threadId, fetchPromise);
1612
+ return fetchPromise;
1449
1613
  },
1450
1614
  [getSession]
1451
1615
  );
1452
1616
  const sendMessage = (0, import_react6.useCallback)(
1453
1617
  async (text, threadId) => {
1454
- var _a;
1455
- const session = getSession(threadId);
1618
+ var _a, _b, _c, _d;
1456
1619
  const existingMessages = threadContextRef.current.getThreadMessages(threadId);
1620
+ const optimisticMessageId = String(existingMessages.length);
1457
1621
  const userMessage = {
1622
+ id: optimisticMessageId,
1458
1623
  role: "user",
1459
1624
  content: [{ type: "text", text }],
1460
- createdAt: /* @__PURE__ */ new Date()
1625
+ createdAt: /* @__PURE__ */ new Date(),
1626
+ metadata: {
1627
+ custom: {
1628
+ aomiOriginalText: text,
1629
+ aomiSendStatus: "sending"
1630
+ }
1631
+ }
1461
1632
  };
1462
1633
  threadContextRef.current.setThreadMessages(threadId, [
1463
1634
  ...existingMessages,
@@ -1466,31 +1637,54 @@ function useRuntimeOrchestrator(aomiClient, options) {
1466
1637
  threadContextRef.current.updateThreadMetadata(threadId, {
1467
1638
  lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
1468
1639
  });
1469
- await session.sendAsync(text);
1470
- (_a = options.onPendingRequestsChange) == null ? void 0 : _a.call(options, session.getPendingRequests());
1471
- },
1472
- [getSession]
1473
- );
1474
- const cancelGeneration = (0, import_react6.useCallback)(
1475
- async (threadId) => {
1476
- var _a;
1477
- const session = (_a = sessionManagerRef.current) == null ? void 0 : _a.get(threadId);
1478
- if (session) {
1479
- await session.interrupt();
1640
+ if (threadContextRef.current.currentThreadId === threadId) {
1641
+ setIsRunning(true);
1642
+ }
1643
+ try {
1644
+ await ((_b = (_a = optionsRef.current).prepareThreadForSend) == null ? void 0 : _b.call(_a, threadId));
1645
+ const session = getSession(threadId);
1646
+ await session.sendAsync(text);
1647
+ if (threadContextRef.current.currentThreadId === threadId) {
1648
+ setIsRunning(session.getIsProcessing());
1649
+ }
1650
+ updateOptimisticMessage(
1651
+ threadContextRef.current,
1652
+ threadId,
1653
+ optimisticMessageId,
1654
+ "sent"
1655
+ );
1656
+ (_d = (_c = optionsRef.current).onPendingRequestsChange) == null ? void 0 : _d.call(
1657
+ _c,
1658
+ session.getPendingRequests()
1659
+ );
1660
+ } catch (error) {
1661
+ if (threadContextRef.current.currentThreadId === threadId) {
1662
+ setIsRunning(false);
1663
+ }
1664
+ updateOptimisticMessage(
1665
+ threadContextRef.current,
1666
+ threadId,
1667
+ optimisticMessageId,
1668
+ "failed",
1669
+ error
1670
+ );
1671
+ throw error;
1480
1672
  }
1481
1673
  },
1482
- []
1674
+ [getSession]
1483
1675
  );
1676
+ const cancelGeneration = (0, import_react6.useCallback)(async (threadId) => {
1677
+ var _a;
1678
+ const session = (_a = sessionManagerRef.current) == null ? void 0 : _a.get(threadId);
1679
+ if (session) {
1680
+ await session.interrupt();
1681
+ }
1682
+ }, []);
1484
1683
  (0, import_react6.useEffect)(() => {
1485
1684
  return () => {
1486
- var _a;
1487
- (_a = sessionManagerRef.current) == null ? void 0 : _a.closeAll();
1488
- for (const cleanup of listenerCleanups.current.values()) {
1489
- cleanup();
1490
- }
1491
- listenerCleanups.current.clear();
1685
+ closeAllSessions();
1492
1686
  };
1493
- }, []);
1687
+ }, [closeAllSessions]);
1494
1688
  return {
1495
1689
  sessionManager: sessionManagerRef.current,
1496
1690
  getSession,
@@ -1499,6 +1693,9 @@ function useRuntimeOrchestrator(aomiClient, options) {
1499
1693
  ensureInitialState,
1500
1694
  sendMessage,
1501
1695
  cancelGeneration,
1696
+ closeSession,
1697
+ closeAllSessions,
1698
+ closeIdleSessionsExcept,
1502
1699
  aomiClientRef
1503
1700
  };
1504
1701
  }
@@ -1509,9 +1706,9 @@ var sortByLastActiveDesc = ([, metaA], [, metaB]) => {
1509
1706
  const tsB = parseTimestamp(metaB.lastActiveAt);
1510
1707
  return tsB - tsA;
1511
1708
  };
1512
- function buildThreadLists(threadMetadata) {
1709
+ function buildThreadLists(threadMetadata, shouldShowThread) {
1513
1710
  const entries = Array.from(threadMetadata.entries()).filter(
1514
- ([, meta]) => !isPlaceholderTitle(meta.title)
1711
+ ([threadId, meta]) => !isPlaceholderTitle(meta.title) && shouldShowThread(threadId)
1515
1712
  );
1516
1713
  const regularThreads = entries.filter(([, meta]) => meta.status !== "archived").sort(sortByLastActiveDesc).map(
1517
1714
  ([id, meta]) => ({
@@ -1533,16 +1730,41 @@ function buildThreadListAdapter({
1533
1730
  aomiClientRef,
1534
1731
  threadContext,
1535
1732
  setIsRunning,
1536
- getInitialControl = initThreadControl
1733
+ isLoading = false,
1734
+ getInitialControl = initThreadControl,
1735
+ isRemoteThread = () => true
1537
1736
  }) {
1737
+ const shouldShowThread = (threadId) => {
1738
+ if (isRemoteThread(threadId)) return true;
1739
+ return threadContext.getThreadMessages(threadId).some((message) => message.role === "user");
1740
+ };
1538
1741
  const { regularThreads, archivedThreads } = buildThreadLists(
1539
- threadContext.allThreadsMetadata
1742
+ threadContext.allThreadsMetadata,
1743
+ shouldShowThread
1540
1744
  );
1745
+ const cleanupEmptyLocalThread = () => {
1746
+ const prevId = threadContext.currentThreadId;
1747
+ if (isRemoteThread(prevId)) return;
1748
+ const msgs = threadContext.getThreadMessages(prevId);
1749
+ if (msgs.length > 0) return;
1750
+ threadContext.setThreadMetadata((prev) => {
1751
+ const next = new Map(prev);
1752
+ next.delete(prevId);
1753
+ return next;
1754
+ });
1755
+ threadContext.setThreads((prev) => {
1756
+ const next = new Map(prev);
1757
+ next.delete(prevId);
1758
+ return next;
1759
+ });
1760
+ };
1541
1761
  return {
1542
1762
  threadId: threadContext.currentThreadId,
1763
+ isLoading,
1543
1764
  threads: regularThreads,
1544
1765
  archivedThreads,
1545
1766
  onSwitchToNewThread: () => {
1767
+ cleanupEmptyLocalThread();
1546
1768
  const threadId = generateUUID();
1547
1769
  threadContext.setThreadMetadata(
1548
1770
  (prev) => new Map(prev).set(threadId, {
@@ -1558,6 +1780,7 @@ function buildThreadListAdapter({
1558
1780
  threadContext.bumpThreadViewKey();
1559
1781
  },
1560
1782
  onSwitchToThread: (threadId) => {
1783
+ cleanupEmptyLocalThread();
1561
1784
  threadContext.setCurrentThreadId(threadId);
1562
1785
  threadContext.bumpThreadViewKey();
1563
1786
  },
@@ -1775,6 +1998,22 @@ function RuntimeUserStateProvider({
1775
1998
 
1776
1999
  // packages/react/src/runtime/core.tsx
1777
2000
  var import_jsx_runtime7 = require("react/jsx-runtime");
2001
+ var THREAD_PREFETCH_LIMIT = 5;
2002
+ var PREFETCH_IDLE_TIMEOUT_MS = 1500;
2003
+ function scheduleBackgroundTask(task) {
2004
+ const runtimeGlobal = globalThis;
2005
+ if (typeof runtimeGlobal.requestIdleCallback === "function") {
2006
+ const idleId = runtimeGlobal.requestIdleCallback(task, {
2007
+ timeout: PREFETCH_IDLE_TIMEOUT_MS
2008
+ });
2009
+ return () => {
2010
+ var _a;
2011
+ return (_a = runtimeGlobal.cancelIdleCallback) == null ? void 0 : _a.call(runtimeGlobal, idleId);
2012
+ };
2013
+ }
2014
+ const timeoutId = runtimeGlobal.setTimeout(task, 0);
2015
+ return () => runtimeGlobal.clearTimeout(timeoutId);
2016
+ }
1778
2017
  function AomiRuntimeCore({
1779
2018
  children,
1780
2019
  aomiClient
@@ -1804,6 +2043,9 @@ function AomiRuntimeCore({
1804
2043
  ensureInitialState,
1805
2044
  sendMessage: orchestratorSendMessage,
1806
2045
  cancelGeneration: orchestratorCancel,
2046
+ closeSession,
2047
+ closeIdleSessionsExcept,
2048
+ closeAllSessions,
1807
2049
  aomiClientRef
1808
2050
  } = useRuntimeOrchestrator(aomiClient, {
1809
2051
  getPublicKey: () => import_client5.UserState.isConnected(getUserState()) ? import_client5.UserState.address(getUserState()) : void 0,
@@ -1814,10 +2056,22 @@ function AomiRuntimeCore({
1814
2056
  var _a;
1815
2057
  return (_a = getControlState().clientId) != null ? _a : void 0;
1816
2058
  },
2059
+ prepareThreadForSend: async (threadId) => {
2060
+ await ensureBackendThread(threadId);
2061
+ await syncCurrentThreadControl();
2062
+ },
1817
2063
  onPendingRequestsChange: walletHandler.setRequests,
1818
2064
  onEvent: (event) => eventContext.dispatch(event)
1819
2065
  });
1820
2066
  sessionManagerRef.current = sessionManager;
2067
+ const threadContextRef = (0, import_react10.useRef)(threadContext);
2068
+ threadContextRef.current = threadContext;
2069
+ const remoteThreadIdsRef = (0, import_react10.useRef)(/* @__PURE__ */ new Set());
2070
+ const warmedThreadIdsRef = (0, import_react10.useRef)(/* @__PURE__ */ new Set());
2071
+ const warmPromisesRef = (0, import_react10.useRef)(/* @__PURE__ */ new Map());
2072
+ const prefetchCancelRef = (0, import_react10.useRef)(null);
2073
+ const [isThreadLoading, setIsThreadLoading] = (0, import_react10.useState)(false);
2074
+ const [isThreadListLoading, setIsThreadListLoading] = (0, import_react10.useState)(true);
1821
2075
  const walletSnapshot = (0, import_react10.useCallback)(
1822
2076
  (nextUser) => {
1823
2077
  var _a;
@@ -1841,6 +2095,9 @@ function AomiRuntimeCore({
1841
2095
  }
1842
2096
  lastWalletStateRef.current = nextWalletState;
1843
2097
  const sessionId = threadContext.currentThreadId;
2098
+ if (!remoteThreadIdsRef.current.has(sessionId)) {
2099
+ return;
2100
+ }
1844
2101
  const message = JSON.stringify({
1845
2102
  type: "wallet:state_changed",
1846
2103
  payload: nextWalletState
@@ -1855,24 +2112,67 @@ function AomiRuntimeCore({
1855
2112
  getUserState,
1856
2113
  walletSnapshot
1857
2114
  ]);
1858
- const threadContextRef = (0, import_react10.useRef)(threadContext);
1859
- threadContextRef.current = threadContext;
1860
- const remoteThreadIdsRef = (0, import_react10.useRef)(/* @__PURE__ */ new Set());
1861
- const warmedThreadIdsRef = (0, import_react10.useRef)(/* @__PURE__ */ new Set());
1862
2115
  const warmThread = (0, import_react10.useCallback)(
1863
2116
  async (threadId) => {
1864
2117
  if (!remoteThreadIdsRef.current.has(threadId) || warmedThreadIdsRef.current.has(threadId)) {
1865
2118
  return;
1866
2119
  }
1867
- const userState = getUserState();
1868
- await aomiClientRef.current.createThread(
1869
- threadId,
1870
- import_client5.UserState.isConnected(userState) ? import_client5.UserState.address(userState) : void 0
1871
- );
1872
- warmedThreadIdsRef.current.add(threadId);
2120
+ const existingPromise = warmPromisesRef.current.get(threadId);
2121
+ if (existingPromise) {
2122
+ return existingPromise;
2123
+ }
2124
+ const warmPromise = (async () => {
2125
+ const userState = getUserState();
2126
+ await aomiClientRef.current.createThread(
2127
+ threadId,
2128
+ import_client5.UserState.isConnected(userState) ? import_client5.UserState.address(userState) : void 0
2129
+ );
2130
+ warmedThreadIdsRef.current.add(threadId);
2131
+ })();
2132
+ warmPromisesRef.current.set(threadId, warmPromise);
2133
+ try {
2134
+ await warmPromise;
2135
+ } finally {
2136
+ warmPromisesRef.current.delete(threadId);
2137
+ }
1873
2138
  },
1874
2139
  [aomiClientRef, getUserState]
1875
2140
  );
2141
+ const scheduleThreadPrefetch = (0, import_react10.useCallback)(
2142
+ (threadIds) => {
2143
+ var _a;
2144
+ (_a = prefetchCancelRef.current) == null ? void 0 : _a.call(prefetchCancelRef);
2145
+ const prefetchThreadIds = Array.from(new Set(threadIds)).filter((threadId) => remoteThreadIdsRef.current.has(threadId)).slice(0, THREAD_PREFETCH_LIMIT);
2146
+ if (prefetchThreadIds.length === 0) {
2147
+ prefetchCancelRef.current = null;
2148
+ return;
2149
+ }
2150
+ let cancelled = false;
2151
+ const cancelScheduledTask = scheduleBackgroundTask(() => {
2152
+ void Promise.all(
2153
+ prefetchThreadIds.map(async (threadId) => {
2154
+ if (cancelled || !remoteThreadIdsRef.current.has(threadId)) return;
2155
+ if (threadContextRef.current.getThreadMessages(threadId).length > 0) {
2156
+ return;
2157
+ }
2158
+ try {
2159
+ await warmThread(threadId);
2160
+ if (cancelled || !remoteThreadIdsRef.current.has(threadId))
2161
+ return;
2162
+ await ensureInitialState(threadId);
2163
+ } catch (error) {
2164
+ console.debug("Failed to prefetch thread:", threadId, error);
2165
+ }
2166
+ })
2167
+ );
2168
+ });
2169
+ prefetchCancelRef.current = () => {
2170
+ cancelled = true;
2171
+ cancelScheduledTask();
2172
+ };
2173
+ },
2174
+ [ensureInitialState, warmThread]
2175
+ );
1876
2176
  (0, import_react10.useEffect)(() => {
1877
2177
  const unsubscribe = eventContext.subscribe("user_state_request", () => {
1878
2178
  var _a, _b, _c;
@@ -1885,19 +2185,49 @@ function AomiRuntimeCore({
1885
2185
  });
1886
2186
  return unsubscribe;
1887
2187
  }, [eventContext, threadContext.currentThreadId, getSession, getUserState]);
2188
+ const ensureBackendThread = (0, import_react10.useCallback)(
2189
+ async (threadId) => {
2190
+ if (remoteThreadIdsRef.current.has(threadId)) return;
2191
+ const userState = getUserState();
2192
+ await aomiClientRef.current.createThread(
2193
+ threadId,
2194
+ import_client5.UserState.isConnected(userState) ? import_client5.UserState.address(userState) : void 0
2195
+ );
2196
+ remoteThreadIdsRef.current.add(threadId);
2197
+ warmedThreadIdsRef.current.add(threadId);
2198
+ },
2199
+ [aomiClientRef, getUserState]
2200
+ );
1888
2201
  (0, import_react10.useEffect)(() => {
1889
2202
  const threadId = threadContext.currentThreadId;
2203
+ closeIdleSessionsExcept(threadId);
2204
+ if (!remoteThreadIdsRef.current.has(threadId)) {
2205
+ setIsThreadLoading(false);
2206
+ return;
2207
+ }
1890
2208
  let cancelled = false;
2209
+ setIsThreadLoading(true);
1891
2210
  void (async () => {
1892
- await warmThread(threadId);
1893
- if (!cancelled) {
1894
- await ensureInitialState(threadId);
2211
+ try {
2212
+ await warmThread(threadId);
2213
+ if (!cancelled) {
2214
+ await ensureInitialState(threadId);
2215
+ }
2216
+ } finally {
2217
+ if (!cancelled) {
2218
+ setIsThreadLoading(false);
2219
+ }
1895
2220
  }
1896
2221
  })();
1897
2222
  return () => {
1898
2223
  cancelled = true;
1899
2224
  };
1900
- }, [ensureInitialState, threadContext.currentThreadId, warmThread]);
2225
+ }, [
2226
+ closeIdleSessionsExcept,
2227
+ ensureInitialState,
2228
+ threadContext.currentThreadId,
2229
+ warmThread
2230
+ ]);
1901
2231
  (0, import_react10.useEffect)(() => {
1902
2232
  const threadId = threadContext.currentThreadId;
1903
2233
  const currentMeta = threadContext.getThreadMetadata(threadId);
@@ -1913,23 +2243,38 @@ function AomiRuntimeCore({
1913
2243
  threadContext.currentThreadId
1914
2244
  );
1915
2245
  (0, import_react10.useEffect)(() => {
2246
+ var _a;
1916
2247
  const userAddress = import_client5.UserState.isConnected(user) ? import_client5.UserState.address(user) : void 0;
1917
2248
  if (!userAddress) {
2249
+ const hadRemoteThreads = remoteThreadIdsRef.current.size > 0;
2250
+ const hadSessions = sessionManager.size > 0;
2251
+ setIsThreadListLoading(false);
2252
+ (_a = prefetchCancelRef.current) == null ? void 0 : _a.call(prefetchCancelRef);
2253
+ prefetchCancelRef.current = null;
1918
2254
  remoteThreadIdsRef.current.clear();
1919
2255
  warmedThreadIdsRef.current.clear();
2256
+ warmPromisesRef.current.clear();
2257
+ closeAllSessions();
2258
+ if (hadRemoteThreads || hadSessions) {
2259
+ threadContextRef.current.resetToDefault();
2260
+ }
1920
2261
  return;
1921
2262
  }
2263
+ let cancelled = false;
2264
+ setIsThreadListLoading(true);
1922
2265
  const fetchThreadList = async () => {
1923
- var _a, _b, _c;
2266
+ var _a2, _b, _c;
1924
2267
  try {
2268
+ const remoteThreadIdsAtFetchStart = new Set(remoteThreadIdsRef.current);
1925
2269
  const threadList = await aomiClientRef.current.listThreads(userAddress);
2270
+ if (cancelled) return;
1926
2271
  const currentContext = threadContextRef.current;
1927
2272
  const remoteThreadIds = /* @__PURE__ */ new Set();
1928
2273
  const newMetadata = new Map(currentContext.allThreadsMetadata);
1929
2274
  let maxChatNum = currentContext.threadCnt;
1930
2275
  for (const thread of threadList) {
1931
2276
  remoteThreadIds.add(thread.session_id);
1932
- const rawTitle = (_a = thread.title) != null ? _a : "";
2277
+ const rawTitle = (_a2 = thread.title) != null ? _a2 : "";
1933
2278
  const title = isPlaceholderTitle(rawTitle) ? "" : rawTitle;
1934
2279
  const lastActive = ((_b = newMetadata.get(thread.session_id)) == null ? void 0 : _b.lastActiveAt) || (/* @__PURE__ */ new Date()).toISOString();
1935
2280
  const existingControl = (_c = newMetadata.get(thread.session_id)) == null ? void 0 : _c.control;
@@ -1947,6 +2292,11 @@ function AomiRuntimeCore({
1947
2292
  }
1948
2293
  }
1949
2294
  }
2295
+ for (const threadId of remoteThreadIdsRef.current) {
2296
+ if (!remoteThreadIdsAtFetchStart.has(threadId)) {
2297
+ remoteThreadIds.add(threadId);
2298
+ }
2299
+ }
1950
2300
  remoteThreadIdsRef.current = remoteThreadIds;
1951
2301
  warmedThreadIdsRef.current = new Set(
1952
2302
  Array.from(warmedThreadIdsRef.current).filter(
@@ -1957,30 +2307,65 @@ function AomiRuntimeCore({
1957
2307
  if (maxChatNum > currentContext.threadCnt) {
1958
2308
  currentContext.setThreadCnt(maxChatNum);
1959
2309
  }
2310
+ scheduleThreadPrefetch(threadList.map((thread) => thread.session_id));
1960
2311
  if (remoteThreadIds.has(currentContext.currentThreadId)) {
1961
- await warmThread(currentContext.currentThreadId);
1962
- await ensureInitialState(currentContext.currentThreadId);
2312
+ setIsThreadLoading(true);
2313
+ try {
2314
+ await warmThread(currentContext.currentThreadId);
2315
+ if (!cancelled) {
2316
+ await ensureInitialState(currentContext.currentThreadId);
2317
+ }
2318
+ } finally {
2319
+ if (!cancelled) {
2320
+ setIsThreadLoading(false);
2321
+ }
2322
+ }
1963
2323
  }
1964
2324
  } catch (error) {
1965
2325
  console.error("Failed to fetch thread list:", error);
2326
+ } finally {
2327
+ if (!cancelled) {
2328
+ setIsThreadListLoading(false);
2329
+ }
1966
2330
  }
1967
2331
  };
1968
2332
  void fetchThreadList();
1969
- }, [user, aomiClientRef, ensureInitialState, warmThread]);
2333
+ return () => {
2334
+ var _a2;
2335
+ cancelled = true;
2336
+ (_a2 = prefetchCancelRef.current) == null ? void 0 : _a2.call(prefetchCancelRef);
2337
+ prefetchCancelRef.current = null;
2338
+ };
2339
+ }, [
2340
+ user,
2341
+ aomiClientRef,
2342
+ ensureInitialState,
2343
+ scheduleThreadPrefetch,
2344
+ warmThread
2345
+ ]);
2346
+ const isRemoteThread = (0, import_react10.useCallback)(
2347
+ (threadId) => remoteThreadIdsRef.current.has(threadId),
2348
+ []
2349
+ );
1970
2350
  const threadListAdapter = (0, import_react10.useMemo)(
1971
2351
  () => buildThreadListAdapter({
1972
2352
  aomiClientRef,
1973
2353
  threadContext,
1974
2354
  setIsRunning,
1975
- getInitialControl: getPreferredThreadControl
2355
+ isLoading: isThreadListLoading,
2356
+ getInitialControl: getPreferredThreadControl,
2357
+ isRemoteThread
1976
2358
  }),
1977
2359
  [
1978
2360
  aomiClientRef,
1979
2361
  getPreferredThreadControl,
2362
+ isRemoteThread,
2363
+ isThreadListLoading,
1980
2364
  setIsRunning,
1981
2365
  threadContext,
1982
2366
  threadContext.currentThreadId,
1983
- threadContext.allThreadsMetadata
2367
+ threadContext.allThreadsMetadata,
2368
+ currentMessages
1984
2369
  ]
1985
2370
  );
1986
2371
  (0, import_react10.useEffect)(() => {
@@ -2015,6 +2400,7 @@ function AomiRuntimeCore({
2015
2400
  }, [eventContext, notificationContext]);
2016
2401
  const runtime = (0, import_react11.useExternalStoreRuntime)({
2017
2402
  messages: currentMessages,
2403
+ isLoading: isThreadLoading,
2018
2404
  setMessages: (msgs) => threadContext.setThreadMessages(threadContext.currentThreadId, [...msgs]),
2019
2405
  isRunning,
2020
2406
  onNew: async (message) => {
@@ -2022,7 +2408,6 @@ function AomiRuntimeCore({
2022
2408
  (part) => part.type === "text"
2023
2409
  ).map((part) => part.text).join("\n");
2024
2410
  if (text) {
2025
- await syncCurrentThreadControl();
2026
2411
  await orchestratorSendMessage(text, threadContext.currentThreadId);
2027
2412
  }
2028
2413
  },
@@ -2034,20 +2419,17 @@ function AomiRuntimeCore({
2034
2419
  });
2035
2420
  (0, import_react10.useEffect)(() => {
2036
2421
  return () => {
2037
- sessionManager.closeAll();
2422
+ var _a;
2423
+ (_a = prefetchCancelRef.current) == null ? void 0 : _a.call(prefetchCancelRef);
2424
+ closeAllSessions();
2038
2425
  };
2039
- }, [sessionManager]);
2426
+ }, [closeAllSessions]);
2040
2427
  const userContext = useUser();
2041
2428
  const sendMessage = (0, import_react10.useCallback)(
2042
2429
  async (text) => {
2043
- await syncCurrentThreadControl();
2044
2430
  await orchestratorSendMessage(text, threadContext.currentThreadId);
2045
2431
  },
2046
- [
2047
- orchestratorSendMessage,
2048
- syncCurrentThreadControl,
2049
- threadContext.currentThreadId
2050
- ]
2432
+ [orchestratorSendMessage, threadContext.currentThreadId]
2051
2433
  );
2052
2434
  const cancelGeneration = (0, import_react10.useCallback)(() => {
2053
2435
  void orchestratorCancel(threadContext.currentThreadId);
@@ -2065,10 +2447,10 @@ function AomiRuntimeCore({
2065
2447
  }, [threadListAdapter]);
2066
2448
  const deleteThread = (0, import_react10.useCallback)(
2067
2449
  async (threadId) => {
2068
- sessionManager.close(threadId);
2450
+ closeSession(threadId);
2069
2451
  await threadListAdapter.onDelete(threadId);
2070
2452
  },
2071
- [threadListAdapter, sessionManager]
2453
+ [closeSession, threadListAdapter]
2072
2454
  );
2073
2455
  const renameThread = (0, import_react10.useCallback)(
2074
2456
  async (threadId, title) => {
@@ -2184,9 +2566,13 @@ function AomiRuntimeCore({
2184
2566
  var import_jsx_runtime8 = require("react/jsx-runtime");
2185
2567
  function AomiRuntimeProvider({
2186
2568
  children,
2187
- backendUrl = "http://localhost:8080"
2569
+ backendUrl = "http://localhost:8080",
2570
+ clientOptions
2188
2571
  }) {
2189
- const aomiClient = (0, import_react12.useMemo)(() => new import_client6.AomiClient({ baseUrl: backendUrl }), [backendUrl]);
2572
+ const aomiClient = (0, import_react12.useMemo)(
2573
+ () => new import_client6.AomiClient(__spreadValues({ baseUrl: backendUrl }, clientOptions)),
2574
+ [backendUrl, clientOptions]
2575
+ );
2190
2576
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ThreadContextProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(NotificationContextProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(UserContextProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AomiRuntimeInner, { aomiClient, children }) }) }) });
2191
2577
  }
2192
2578
  function AomiRuntimeInner({
@@ -2270,6 +2656,7 @@ function useNotificationHandler({
2270
2656
  SUPPORTED_CHAINS,
2271
2657
  ThreadContextProvider,
2272
2658
  UserContextProvider,
2659
+ UserState,
2273
2660
  aaModeFromExecutionKind,
2274
2661
  appendFeeCallToPayload,
2275
2662
  buildFeeAAWalletCall,