@aomi-labs/react 0.3.15 → 0.3.17

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.js CHANGED
@@ -152,6 +152,29 @@ var ThreadStore = class {
152
152
  this.getThreadMetadata = (threadId) => {
153
153
  return this.state.threadMetadata.get(threadId);
154
154
  };
155
+ /** Reset store to a single empty "New Chat" thread (e.g. on wallet disconnect). */
156
+ this.resetToDefault = () => {
157
+ const threadId = generateUUID();
158
+ this.state = {
159
+ currentThreadId: threadId,
160
+ threadViewKey: this.state.threadViewKey + 1,
161
+ threadCnt: 1,
162
+ threads: /* @__PURE__ */ new Map([[threadId, []]]),
163
+ threadMetadata: /* @__PURE__ */ new Map([
164
+ [
165
+ threadId,
166
+ {
167
+ title: "New Chat",
168
+ status: "regular",
169
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
170
+ control: initThreadControl()
171
+ }
172
+ ]
173
+ ])
174
+ };
175
+ this.snapshot = this.buildSnapshot();
176
+ this.emit();
177
+ };
155
178
  this.updateThreadMetadata = (threadId, updates) => {
156
179
  const existing = this.state.threadMetadata.get(threadId);
157
180
  if (!existing) {
@@ -229,7 +252,8 @@ var ThreadStore = class {
229
252
  getThreadMessages: this.getThreadMessages,
230
253
  setThreadMessages: this.setThreadMessages,
231
254
  getThreadMetadata: this.getThreadMetadata,
232
- updateThreadMetadata: this.updateThreadMetadata
255
+ updateThreadMetadata: this.updateThreadMetadata,
256
+ resetToDefault: this.resetToDefault
233
257
  };
234
258
  }
235
259
  };
@@ -460,7 +484,7 @@ function ControlContextProvider({
460
484
  }
461
485
  };
462
486
  void fetchApps();
463
- }, [state.apiKey, publicKey, sessionId]);
487
+ }, [state.apiKey, publicKey]);
464
488
  useEffect(() => {
465
489
  const fetchModels = async () => {
466
490
  try {
@@ -1163,7 +1187,7 @@ function UserContextProvider({ children }) {
1163
1187
  }
1164
1188
 
1165
1189
  // packages/react/src/runtime/core.tsx
1166
- import { useCallback as useCallback7, useEffect as useEffect4, useMemo as useMemo2, useRef as useRef8 } from "react";
1190
+ import { useCallback as useCallback7, useEffect as useEffect4, useMemo as useMemo2, useRef as useRef8, useState as useState6 } from "react";
1167
1191
  import {
1168
1192
  AssistantRuntimeProvider,
1169
1193
  useExternalStoreRuntime
@@ -1175,7 +1199,9 @@ import { useCallback as useCallback5, useEffect as useEffect2, useRef as useRef5
1175
1199
  import { CLIENT_TYPE_WEB_UI } from "@aomi-labs/client";
1176
1200
 
1177
1201
  // packages/react/src/runtime/session-manager.ts
1178
- import { Session as ClientSession } from "@aomi-labs/client";
1202
+ import {
1203
+ Session as ClientSession
1204
+ } from "@aomi-labs/client";
1179
1205
  var SessionManager = class {
1180
1206
  constructor(clientFactory) {
1181
1207
  this.clientFactory = clientFactory;
@@ -1193,6 +1219,9 @@ var SessionManager = class {
1193
1219
  get(threadId) {
1194
1220
  return this.sessions.get(threadId);
1195
1221
  }
1222
+ get size() {
1223
+ return this.sessions.size;
1224
+ }
1196
1225
  forEach(callback) {
1197
1226
  for (const [threadId, session] of this.sessions) {
1198
1227
  callback(session, threadId);
@@ -1205,6 +1234,21 @@ var SessionManager = class {
1205
1234
  this.sessions.delete(threadId);
1206
1235
  }
1207
1236
  }
1237
+ closeIdleExcept(activeThreadId, onBeforeClose) {
1238
+ const closedThreadIds = [];
1239
+ for (const [threadId, session] of this.sessions) {
1240
+ if (threadId === activeThreadId) continue;
1241
+ if (session.getIsProcessing()) continue;
1242
+ if (session.getIsPolling()) continue;
1243
+ if (session.getPendingRequests().length > 0) continue;
1244
+ closedThreadIds.push(threadId);
1245
+ }
1246
+ for (const threadId of closedThreadIds) {
1247
+ onBeforeClose == null ? void 0 : onBeforeClose(threadId);
1248
+ this.close(threadId);
1249
+ }
1250
+ return closedThreadIds;
1251
+ }
1208
1252
  closeAll() {
1209
1253
  for (const [threadId, session] of this.sessions) {
1210
1254
  session.close();
@@ -1316,28 +1360,113 @@ var SUPPORTED_CHAINS = [
1316
1360
  var getChainInfo = (chainId) => chainId === void 0 ? void 0 : SUPPORTED_CHAINS.find((c) => c.id === chainId);
1317
1361
 
1318
1362
  // packages/react/src/runtime/orchestrator.ts
1363
+ var toErrorMessage = (error) => error instanceof Error ? error.message : "Message failed to send";
1364
+ var getOptimisticStatus = (message) => {
1365
+ var _a, _b;
1366
+ const status = (_b = (_a = message.metadata) == null ? void 0 : _a.custom) == null ? void 0 : _b.aomiSendStatus;
1367
+ return status === "sending" || status === "sent" || status === "failed" ? status : void 0;
1368
+ };
1369
+ var hasUnhydratedOptimisticMessage = (messages) => messages.some((message) => {
1370
+ const status = getOptimisticStatus(message);
1371
+ return status === "sending" || status === "sent";
1372
+ });
1373
+ var withOptimisticStatus = (message, status, error) => {
1374
+ var _a, _b;
1375
+ const custom = __spreadProps(__spreadValues({}, (_b = (_a = message.metadata) == null ? void 0 : _a.custom) != null ? _b : {}), {
1376
+ aomiSendStatus: status
1377
+ });
1378
+ if (error) {
1379
+ custom.aomiSendError = toErrorMessage(error);
1380
+ } else {
1381
+ delete custom.aomiSendError;
1382
+ }
1383
+ return __spreadProps(__spreadValues({}, message), {
1384
+ metadata: __spreadProps(__spreadValues({}, message.metadata), {
1385
+ custom
1386
+ })
1387
+ });
1388
+ };
1389
+ var updateOptimisticMessage = (threadContext, threadId, messageId, status, error) => {
1390
+ const messages = threadContext.getThreadMessages(threadId);
1391
+ let changed = false;
1392
+ const nextMessages = messages.map((message) => {
1393
+ if (message.id !== messageId) return message;
1394
+ changed = true;
1395
+ return withOptimisticStatus(message, status, error);
1396
+ });
1397
+ if (changed) {
1398
+ threadContext.setThreadMessages(threadId, nextMessages);
1399
+ }
1400
+ };
1319
1401
  function useRuntimeOrchestrator(aomiClient, options) {
1320
1402
  const threadContext = useThreadContext();
1321
1403
  const threadContextRef = useRef5(threadContext);
1322
1404
  threadContextRef.current = threadContext;
1323
1405
  const aomiClientRef = useRef5(aomiClient);
1324
1406
  aomiClientRef.current = aomiClient;
1407
+ const optionsRef = useRef5(options);
1408
+ optionsRef.current = options;
1325
1409
  const [isRunning, setIsRunning] = useState4(false);
1326
1410
  const sessionManagerRef = useRef5(null);
1327
1411
  if (!sessionManagerRef.current) {
1328
1412
  sessionManagerRef.current = new SessionManager(() => aomiClientRef.current);
1329
1413
  }
1330
1414
  const pendingFetches = useRef5(/* @__PURE__ */ new Set());
1415
+ const initialStatePromises = useRef5(/* @__PURE__ */ new Map());
1416
+ const hydratedThreadIds = useRef5(/* @__PURE__ */ new Set());
1331
1417
  const listenerCleanups = useRef5(/* @__PURE__ */ new Map());
1418
+ const cleanupSessionListeners = useCallback5((threadId) => {
1419
+ var _a;
1420
+ (_a = listenerCleanups.current.get(threadId)) == null ? void 0 : _a();
1421
+ listenerCleanups.current.delete(threadId);
1422
+ }, []);
1423
+ const closeSession = useCallback5(
1424
+ (threadId) => {
1425
+ var _a;
1426
+ cleanupSessionListeners(threadId);
1427
+ pendingFetches.current.delete(threadId);
1428
+ initialStatePromises.current.delete(threadId);
1429
+ hydratedThreadIds.current.delete(threadId);
1430
+ (_a = sessionManagerRef.current) == null ? void 0 : _a.close(threadId);
1431
+ },
1432
+ [cleanupSessionListeners]
1433
+ );
1434
+ const closeIdleSessionsExcept = useCallback5(
1435
+ (activeThreadId) => {
1436
+ var _a, _b;
1437
+ const closedThreadIds = (_b = (_a = sessionManagerRef.current) == null ? void 0 : _a.closeIdleExcept(
1438
+ activeThreadId,
1439
+ cleanupSessionListeners
1440
+ )) != null ? _b : [];
1441
+ for (const threadId of closedThreadIds) {
1442
+ pendingFetches.current.delete(threadId);
1443
+ initialStatePromises.current.delete(threadId);
1444
+ hydratedThreadIds.current.delete(threadId);
1445
+ }
1446
+ return closedThreadIds;
1447
+ },
1448
+ [cleanupSessionListeners]
1449
+ );
1450
+ const closeAllSessions = useCallback5(() => {
1451
+ var _a;
1452
+ pendingFetches.current.clear();
1453
+ initialStatePromises.current.clear();
1454
+ hydratedThreadIds.current.clear();
1455
+ for (const threadId of Array.from(listenerCleanups.current.keys())) {
1456
+ cleanupSessionListeners(threadId);
1457
+ }
1458
+ (_a = sessionManagerRef.current) == null ? void 0 : _a.closeAll();
1459
+ }, [cleanupSessionListeners]);
1332
1460
  const getSession = useCallback5(
1333
1461
  (threadId) => {
1334
1462
  var _a, _b, _c, _d, _e;
1335
1463
  const manager = sessionManagerRef.current;
1336
- const nextApp = options.getApp();
1337
- const nextPublicKey = (_a = options.getPublicKey) == null ? void 0 : _a.call(options);
1338
- const nextApiKey = (_c = (_b = options.getApiKey) == null ? void 0 : _b.call(options)) != null ? _c : void 0;
1339
- const nextClientId = (_d = options.getClientId) == null ? void 0 : _d.call(options);
1340
- const nextUserState = (_e = options.getUserState) == null ? void 0 : _e.call(options);
1464
+ const nextOptions = optionsRef.current;
1465
+ const nextApp = nextOptions.getApp();
1466
+ const nextPublicKey = (_a = nextOptions.getPublicKey) == null ? void 0 : _a.call(nextOptions);
1467
+ const nextApiKey = (_c = (_b = nextOptions.getApiKey) == null ? void 0 : _b.call(nextOptions)) != null ? _c : void 0;
1468
+ const nextClientId = (_d = nextOptions.getClientId) == null ? void 0 : _d.call(nextOptions);
1469
+ const nextUserState = (_e = nextOptions.getUserState) == null ? void 0 : _e.call(nextOptions);
1341
1470
  const existing = manager.get(threadId);
1342
1471
  if (existing) {
1343
1472
  existing.syncRuntimeOptions({
@@ -1366,6 +1495,10 @@ function useRuntimeOrchestrator(aomiClient, options) {
1366
1495
  const converted = toInboundMessage(msg);
1367
1496
  if (converted) threadMessages.push(converted);
1368
1497
  }
1498
+ const existingMessages = threadContextRef.current.getThreadMessages(threadId);
1499
+ if (threadMessages.length === 0 && hasUnhydratedOptimisticMessage(existingMessages)) {
1500
+ return;
1501
+ }
1369
1502
  threadContextRef.current.setThreadMessages(threadId, threadMessages);
1370
1503
  })
1371
1504
  );
@@ -1387,8 +1520,8 @@ function useRuntimeOrchestrator(aomiClient, options) {
1387
1520
  session.on(
1388
1521
  "wallet_requests_changed",
1389
1522
  (requests) => {
1390
- var _a2;
1391
- return (_a2 = options.onPendingRequestsChange) == null ? void 0 : _a2.call(options, requests);
1523
+ var _a2, _b2;
1524
+ return (_b2 = (_a2 = optionsRef.current).onPendingRequestsChange) == null ? void 0 : _b2.call(_a2, requests);
1392
1525
  }
1393
1526
  )
1394
1527
  );
@@ -1397,10 +1530,17 @@ function useRuntimeOrchestrator(aomiClient, options) {
1397
1530
  threadContextRef.current.updateThreadMetadata(threadId, { title });
1398
1531
  })
1399
1532
  );
1400
- const forwardEvent = (type) => session.on(type, (payload) => {
1401
- var _a2;
1402
- (_a2 = options.onEvent) == null ? void 0 : _a2.call(options, { type, payload, sessionId: threadId });
1403
- });
1533
+ const forwardEvent = (type) => session.on(
1534
+ type,
1535
+ (payload) => {
1536
+ var _a2, _b2;
1537
+ (_b2 = (_a2 = optionsRef.current).onEvent) == null ? void 0 : _b2.call(_a2, {
1538
+ type,
1539
+ payload,
1540
+ sessionId: threadId
1541
+ });
1542
+ }
1543
+ );
1404
1544
  cleanups.push(forwardEvent("tool_update"));
1405
1545
  cleanups.push(forwardEvent("tool_complete"));
1406
1546
  cleanups.push(forwardEvent("system_notice"));
@@ -1416,36 +1556,68 @@ function useRuntimeOrchestrator(aomiClient, options) {
1416
1556
  );
1417
1557
  const ensureInitialState = useCallback5(
1418
1558
  async (threadId) => {
1419
- var _a;
1420
- if (pendingFetches.current.has(threadId)) return;
1421
- pendingFetches.current.add(threadId);
1422
- try {
1423
- const session = getSession(threadId);
1424
- await session.fetchCurrentState();
1425
- (_a = options.onPendingRequestsChange) == null ? void 0 : _a.call(options, session.getPendingRequests());
1426
- if (threadContextRef.current.currentThreadId === threadId) {
1427
- setIsRunning(session.getIsProcessing());
1428
- }
1429
- } catch (error) {
1430
- console.error("Failed to fetch initial state:", error);
1559
+ var _a, _b, _c;
1560
+ const existingPromise = initialStatePromises.current.get(threadId);
1561
+ if (existingPromise) {
1562
+ return existingPromise;
1563
+ }
1564
+ const cachedMessages = threadContextRef.current.getThreadMessages(threadId);
1565
+ const existingSession = (_a = sessionManagerRef.current) == null ? void 0 : _a.get(threadId);
1566
+ if (existingSession && (hydratedThreadIds.current.has(threadId) || cachedMessages.length > 0)) {
1567
+ (_c = (_b = optionsRef.current).onPendingRequestsChange) == null ? void 0 : _c.call(
1568
+ _b,
1569
+ existingSession.getPendingRequests()
1570
+ );
1431
1571
  if (threadContextRef.current.currentThreadId === threadId) {
1432
- setIsRunning(false);
1572
+ setIsRunning(existingSession.getIsProcessing());
1433
1573
  }
1434
- } finally {
1435
- pendingFetches.current.delete(threadId);
1574
+ return;
1436
1575
  }
1576
+ const fetchPromise = (async () => {
1577
+ var _a2, _b2;
1578
+ pendingFetches.current.add(threadId);
1579
+ try {
1580
+ const session = getSession(threadId);
1581
+ await session.fetchCurrentState();
1582
+ hydratedThreadIds.current.add(threadId);
1583
+ (_b2 = (_a2 = optionsRef.current).onPendingRequestsChange) == null ? void 0 : _b2.call(
1584
+ _a2,
1585
+ session.getPendingRequests()
1586
+ );
1587
+ if (threadContextRef.current.currentThreadId === threadId) {
1588
+ setIsRunning(session.getIsProcessing());
1589
+ }
1590
+ } catch (error) {
1591
+ console.error("Failed to fetch initial state:", error);
1592
+ if (threadContextRef.current.currentThreadId === threadId) {
1593
+ setIsRunning(false);
1594
+ }
1595
+ } finally {
1596
+ pendingFetches.current.delete(threadId);
1597
+ initialStatePromises.current.delete(threadId);
1598
+ }
1599
+ })();
1600
+ initialStatePromises.current.set(threadId, fetchPromise);
1601
+ return fetchPromise;
1437
1602
  },
1438
1603
  [getSession]
1439
1604
  );
1440
1605
  const sendMessage = useCallback5(
1441
1606
  async (text, threadId) => {
1442
- var _a;
1443
- const session = getSession(threadId);
1607
+ var _a, _b, _c, _d;
1444
1608
  const existingMessages = threadContextRef.current.getThreadMessages(threadId);
1609
+ const optimisticMessageId = String(existingMessages.length);
1445
1610
  const userMessage = {
1611
+ id: optimisticMessageId,
1446
1612
  role: "user",
1447
1613
  content: [{ type: "text", text }],
1448
- createdAt: /* @__PURE__ */ new Date()
1614
+ createdAt: /* @__PURE__ */ new Date(),
1615
+ metadata: {
1616
+ custom: {
1617
+ aomiOriginalText: text,
1618
+ aomiSendStatus: "sending"
1619
+ }
1620
+ }
1449
1621
  };
1450
1622
  threadContextRef.current.setThreadMessages(threadId, [
1451
1623
  ...existingMessages,
@@ -1454,31 +1626,54 @@ function useRuntimeOrchestrator(aomiClient, options) {
1454
1626
  threadContextRef.current.updateThreadMetadata(threadId, {
1455
1627
  lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
1456
1628
  });
1457
- await session.sendAsync(text);
1458
- (_a = options.onPendingRequestsChange) == null ? void 0 : _a.call(options, session.getPendingRequests());
1459
- },
1460
- [getSession]
1461
- );
1462
- const cancelGeneration = useCallback5(
1463
- async (threadId) => {
1464
- var _a;
1465
- const session = (_a = sessionManagerRef.current) == null ? void 0 : _a.get(threadId);
1466
- if (session) {
1467
- await session.interrupt();
1629
+ if (threadContextRef.current.currentThreadId === threadId) {
1630
+ setIsRunning(true);
1631
+ }
1632
+ try {
1633
+ await ((_b = (_a = optionsRef.current).prepareThreadForSend) == null ? void 0 : _b.call(_a, threadId));
1634
+ const session = getSession(threadId);
1635
+ await session.sendAsync(text);
1636
+ if (threadContextRef.current.currentThreadId === threadId) {
1637
+ setIsRunning(session.getIsProcessing());
1638
+ }
1639
+ updateOptimisticMessage(
1640
+ threadContextRef.current,
1641
+ threadId,
1642
+ optimisticMessageId,
1643
+ "sent"
1644
+ );
1645
+ (_d = (_c = optionsRef.current).onPendingRequestsChange) == null ? void 0 : _d.call(
1646
+ _c,
1647
+ session.getPendingRequests()
1648
+ );
1649
+ } catch (error) {
1650
+ if (threadContextRef.current.currentThreadId === threadId) {
1651
+ setIsRunning(false);
1652
+ }
1653
+ updateOptimisticMessage(
1654
+ threadContextRef.current,
1655
+ threadId,
1656
+ optimisticMessageId,
1657
+ "failed",
1658
+ error
1659
+ );
1660
+ throw error;
1468
1661
  }
1469
1662
  },
1470
- []
1663
+ [getSession]
1471
1664
  );
1665
+ const cancelGeneration = useCallback5(async (threadId) => {
1666
+ var _a;
1667
+ const session = (_a = sessionManagerRef.current) == null ? void 0 : _a.get(threadId);
1668
+ if (session) {
1669
+ await session.interrupt();
1670
+ }
1671
+ }, []);
1472
1672
  useEffect2(() => {
1473
1673
  return () => {
1474
- var _a;
1475
- (_a = sessionManagerRef.current) == null ? void 0 : _a.closeAll();
1476
- for (const cleanup of listenerCleanups.current.values()) {
1477
- cleanup();
1478
- }
1479
- listenerCleanups.current.clear();
1674
+ closeAllSessions();
1480
1675
  };
1481
- }, []);
1676
+ }, [closeAllSessions]);
1482
1677
  return {
1483
1678
  sessionManager: sessionManagerRef.current,
1484
1679
  getSession,
@@ -1487,6 +1682,9 @@ function useRuntimeOrchestrator(aomiClient, options) {
1487
1682
  ensureInitialState,
1488
1683
  sendMessage,
1489
1684
  cancelGeneration,
1685
+ closeSession,
1686
+ closeAllSessions,
1687
+ closeIdleSessionsExcept,
1490
1688
  aomiClientRef
1491
1689
  };
1492
1690
  }
@@ -1497,9 +1695,9 @@ var sortByLastActiveDesc = ([, metaA], [, metaB]) => {
1497
1695
  const tsB = parseTimestamp(metaB.lastActiveAt);
1498
1696
  return tsB - tsA;
1499
1697
  };
1500
- function buildThreadLists(threadMetadata) {
1698
+ function buildThreadLists(threadMetadata, shouldShowThread) {
1501
1699
  const entries = Array.from(threadMetadata.entries()).filter(
1502
- ([, meta]) => !isPlaceholderTitle(meta.title)
1700
+ ([threadId, meta]) => !isPlaceholderTitle(meta.title) && shouldShowThread(threadId)
1503
1701
  );
1504
1702
  const regularThreads = entries.filter(([, meta]) => meta.status !== "archived").sort(sortByLastActiveDesc).map(
1505
1703
  ([id, meta]) => ({
@@ -1521,16 +1719,41 @@ function buildThreadListAdapter({
1521
1719
  aomiClientRef,
1522
1720
  threadContext,
1523
1721
  setIsRunning,
1524
- getInitialControl = initThreadControl
1722
+ isLoading = false,
1723
+ getInitialControl = initThreadControl,
1724
+ isRemoteThread = () => true
1525
1725
  }) {
1726
+ const shouldShowThread = (threadId) => {
1727
+ if (isRemoteThread(threadId)) return true;
1728
+ return threadContext.getThreadMessages(threadId).some((message) => message.role === "user");
1729
+ };
1526
1730
  const { regularThreads, archivedThreads } = buildThreadLists(
1527
- threadContext.allThreadsMetadata
1731
+ threadContext.allThreadsMetadata,
1732
+ shouldShowThread
1528
1733
  );
1734
+ const cleanupEmptyLocalThread = () => {
1735
+ const prevId = threadContext.currentThreadId;
1736
+ if (isRemoteThread(prevId)) return;
1737
+ const msgs = threadContext.getThreadMessages(prevId);
1738
+ if (msgs.length > 0) return;
1739
+ threadContext.setThreadMetadata((prev) => {
1740
+ const next = new Map(prev);
1741
+ next.delete(prevId);
1742
+ return next;
1743
+ });
1744
+ threadContext.setThreads((prev) => {
1745
+ const next = new Map(prev);
1746
+ next.delete(prevId);
1747
+ return next;
1748
+ });
1749
+ };
1529
1750
  return {
1530
1751
  threadId: threadContext.currentThreadId,
1752
+ isLoading,
1531
1753
  threads: regularThreads,
1532
1754
  archivedThreads,
1533
1755
  onSwitchToNewThread: () => {
1756
+ cleanupEmptyLocalThread();
1534
1757
  const threadId = generateUUID();
1535
1758
  threadContext.setThreadMetadata(
1536
1759
  (prev) => new Map(prev).set(threadId, {
@@ -1546,6 +1769,7 @@ function buildThreadListAdapter({
1546
1769
  threadContext.bumpThreadViewKey();
1547
1770
  },
1548
1771
  onSwitchToThread: (threadId) => {
1772
+ cleanupEmptyLocalThread();
1549
1773
  threadContext.setCurrentThreadId(threadId);
1550
1774
  threadContext.bumpThreadViewKey();
1551
1775
  },
@@ -1763,6 +1987,22 @@ function RuntimeUserStateProvider({
1763
1987
 
1764
1988
  // packages/react/src/runtime/core.tsx
1765
1989
  import { jsx as jsx7 } from "react/jsx-runtime";
1990
+ var THREAD_PREFETCH_LIMIT = 5;
1991
+ var PREFETCH_IDLE_TIMEOUT_MS = 1500;
1992
+ function scheduleBackgroundTask(task) {
1993
+ const runtimeGlobal = globalThis;
1994
+ if (typeof runtimeGlobal.requestIdleCallback === "function") {
1995
+ const idleId = runtimeGlobal.requestIdleCallback(task, {
1996
+ timeout: PREFETCH_IDLE_TIMEOUT_MS
1997
+ });
1998
+ return () => {
1999
+ var _a;
2000
+ return (_a = runtimeGlobal.cancelIdleCallback) == null ? void 0 : _a.call(runtimeGlobal, idleId);
2001
+ };
2002
+ }
2003
+ const timeoutId = runtimeGlobal.setTimeout(task, 0);
2004
+ return () => runtimeGlobal.clearTimeout(timeoutId);
2005
+ }
1766
2006
  function AomiRuntimeCore({
1767
2007
  children,
1768
2008
  aomiClient
@@ -1792,6 +2032,9 @@ function AomiRuntimeCore({
1792
2032
  ensureInitialState,
1793
2033
  sendMessage: orchestratorSendMessage,
1794
2034
  cancelGeneration: orchestratorCancel,
2035
+ closeSession,
2036
+ closeIdleSessionsExcept,
2037
+ closeAllSessions,
1795
2038
  aomiClientRef
1796
2039
  } = useRuntimeOrchestrator(aomiClient, {
1797
2040
  getPublicKey: () => UserState3.isConnected(getUserState()) ? UserState3.address(getUserState()) : void 0,
@@ -1802,10 +2045,22 @@ function AomiRuntimeCore({
1802
2045
  var _a;
1803
2046
  return (_a = getControlState().clientId) != null ? _a : void 0;
1804
2047
  },
2048
+ prepareThreadForSend: async (threadId) => {
2049
+ await ensureBackendThread(threadId);
2050
+ await syncCurrentThreadControl();
2051
+ },
1805
2052
  onPendingRequestsChange: walletHandler.setRequests,
1806
2053
  onEvent: (event) => eventContext.dispatch(event)
1807
2054
  });
1808
2055
  sessionManagerRef.current = sessionManager;
2056
+ const threadContextRef = useRef8(threadContext);
2057
+ threadContextRef.current = threadContext;
2058
+ const remoteThreadIdsRef = useRef8(/* @__PURE__ */ new Set());
2059
+ const warmedThreadIdsRef = useRef8(/* @__PURE__ */ new Set());
2060
+ const warmPromisesRef = useRef8(/* @__PURE__ */ new Map());
2061
+ const prefetchCancelRef = useRef8(null);
2062
+ const [isThreadLoading, setIsThreadLoading] = useState6(false);
2063
+ const [isThreadListLoading, setIsThreadListLoading] = useState6(true);
1809
2064
  const walletSnapshot = useCallback7(
1810
2065
  (nextUser) => {
1811
2066
  var _a;
@@ -1829,6 +2084,9 @@ function AomiRuntimeCore({
1829
2084
  }
1830
2085
  lastWalletStateRef.current = nextWalletState;
1831
2086
  const sessionId = threadContext.currentThreadId;
2087
+ if (!remoteThreadIdsRef.current.has(sessionId)) {
2088
+ return;
2089
+ }
1832
2090
  const message = JSON.stringify({
1833
2091
  type: "wallet:state_changed",
1834
2092
  payload: nextWalletState
@@ -1843,24 +2101,67 @@ function AomiRuntimeCore({
1843
2101
  getUserState,
1844
2102
  walletSnapshot
1845
2103
  ]);
1846
- const threadContextRef = useRef8(threadContext);
1847
- threadContextRef.current = threadContext;
1848
- const remoteThreadIdsRef = useRef8(/* @__PURE__ */ new Set());
1849
- const warmedThreadIdsRef = useRef8(/* @__PURE__ */ new Set());
1850
2104
  const warmThread = useCallback7(
1851
2105
  async (threadId) => {
1852
2106
  if (!remoteThreadIdsRef.current.has(threadId) || warmedThreadIdsRef.current.has(threadId)) {
1853
2107
  return;
1854
2108
  }
1855
- const userState = getUserState();
1856
- await aomiClientRef.current.createThread(
1857
- threadId,
1858
- UserState3.isConnected(userState) ? UserState3.address(userState) : void 0
1859
- );
1860
- warmedThreadIdsRef.current.add(threadId);
2109
+ const existingPromise = warmPromisesRef.current.get(threadId);
2110
+ if (existingPromise) {
2111
+ return existingPromise;
2112
+ }
2113
+ const warmPromise = (async () => {
2114
+ const userState = getUserState();
2115
+ await aomiClientRef.current.createThread(
2116
+ threadId,
2117
+ UserState3.isConnected(userState) ? UserState3.address(userState) : void 0
2118
+ );
2119
+ warmedThreadIdsRef.current.add(threadId);
2120
+ })();
2121
+ warmPromisesRef.current.set(threadId, warmPromise);
2122
+ try {
2123
+ await warmPromise;
2124
+ } finally {
2125
+ warmPromisesRef.current.delete(threadId);
2126
+ }
1861
2127
  },
1862
2128
  [aomiClientRef, getUserState]
1863
2129
  );
2130
+ const scheduleThreadPrefetch = useCallback7(
2131
+ (threadIds) => {
2132
+ var _a;
2133
+ (_a = prefetchCancelRef.current) == null ? void 0 : _a.call(prefetchCancelRef);
2134
+ const prefetchThreadIds = Array.from(new Set(threadIds)).filter((threadId) => remoteThreadIdsRef.current.has(threadId)).slice(0, THREAD_PREFETCH_LIMIT);
2135
+ if (prefetchThreadIds.length === 0) {
2136
+ prefetchCancelRef.current = null;
2137
+ return;
2138
+ }
2139
+ let cancelled = false;
2140
+ const cancelScheduledTask = scheduleBackgroundTask(() => {
2141
+ void Promise.all(
2142
+ prefetchThreadIds.map(async (threadId) => {
2143
+ if (cancelled || !remoteThreadIdsRef.current.has(threadId)) return;
2144
+ if (threadContextRef.current.getThreadMessages(threadId).length > 0) {
2145
+ return;
2146
+ }
2147
+ try {
2148
+ await warmThread(threadId);
2149
+ if (cancelled || !remoteThreadIdsRef.current.has(threadId))
2150
+ return;
2151
+ await ensureInitialState(threadId);
2152
+ } catch (error) {
2153
+ console.debug("Failed to prefetch thread:", threadId, error);
2154
+ }
2155
+ })
2156
+ );
2157
+ });
2158
+ prefetchCancelRef.current = () => {
2159
+ cancelled = true;
2160
+ cancelScheduledTask();
2161
+ };
2162
+ },
2163
+ [ensureInitialState, warmThread]
2164
+ );
1864
2165
  useEffect4(() => {
1865
2166
  const unsubscribe = eventContext.subscribe("user_state_request", () => {
1866
2167
  var _a, _b, _c;
@@ -1873,19 +2174,49 @@ function AomiRuntimeCore({
1873
2174
  });
1874
2175
  return unsubscribe;
1875
2176
  }, [eventContext, threadContext.currentThreadId, getSession, getUserState]);
2177
+ const ensureBackendThread = useCallback7(
2178
+ async (threadId) => {
2179
+ if (remoteThreadIdsRef.current.has(threadId)) return;
2180
+ const userState = getUserState();
2181
+ await aomiClientRef.current.createThread(
2182
+ threadId,
2183
+ UserState3.isConnected(userState) ? UserState3.address(userState) : void 0
2184
+ );
2185
+ remoteThreadIdsRef.current.add(threadId);
2186
+ warmedThreadIdsRef.current.add(threadId);
2187
+ },
2188
+ [aomiClientRef, getUserState]
2189
+ );
1876
2190
  useEffect4(() => {
1877
2191
  const threadId = threadContext.currentThreadId;
2192
+ closeIdleSessionsExcept(threadId);
2193
+ if (!remoteThreadIdsRef.current.has(threadId)) {
2194
+ setIsThreadLoading(false);
2195
+ return;
2196
+ }
1878
2197
  let cancelled = false;
2198
+ setIsThreadLoading(true);
1879
2199
  void (async () => {
1880
- await warmThread(threadId);
1881
- if (!cancelled) {
1882
- await ensureInitialState(threadId);
2200
+ try {
2201
+ await warmThread(threadId);
2202
+ if (!cancelled) {
2203
+ await ensureInitialState(threadId);
2204
+ }
2205
+ } finally {
2206
+ if (!cancelled) {
2207
+ setIsThreadLoading(false);
2208
+ }
1883
2209
  }
1884
2210
  })();
1885
2211
  return () => {
1886
2212
  cancelled = true;
1887
2213
  };
1888
- }, [ensureInitialState, threadContext.currentThreadId, warmThread]);
2214
+ }, [
2215
+ closeIdleSessionsExcept,
2216
+ ensureInitialState,
2217
+ threadContext.currentThreadId,
2218
+ warmThread
2219
+ ]);
1889
2220
  useEffect4(() => {
1890
2221
  const threadId = threadContext.currentThreadId;
1891
2222
  const currentMeta = threadContext.getThreadMetadata(threadId);
@@ -1901,23 +2232,38 @@ function AomiRuntimeCore({
1901
2232
  threadContext.currentThreadId
1902
2233
  );
1903
2234
  useEffect4(() => {
2235
+ var _a;
1904
2236
  const userAddress = UserState3.isConnected(user) ? UserState3.address(user) : void 0;
1905
2237
  if (!userAddress) {
2238
+ const hadRemoteThreads = remoteThreadIdsRef.current.size > 0;
2239
+ const hadSessions = sessionManager.size > 0;
2240
+ setIsThreadListLoading(false);
2241
+ (_a = prefetchCancelRef.current) == null ? void 0 : _a.call(prefetchCancelRef);
2242
+ prefetchCancelRef.current = null;
1906
2243
  remoteThreadIdsRef.current.clear();
1907
2244
  warmedThreadIdsRef.current.clear();
2245
+ warmPromisesRef.current.clear();
2246
+ closeAllSessions();
2247
+ if (hadRemoteThreads || hadSessions) {
2248
+ threadContextRef.current.resetToDefault();
2249
+ }
1908
2250
  return;
1909
2251
  }
2252
+ let cancelled = false;
2253
+ setIsThreadListLoading(true);
1910
2254
  const fetchThreadList = async () => {
1911
- var _a, _b, _c;
2255
+ var _a2, _b, _c;
1912
2256
  try {
2257
+ const remoteThreadIdsAtFetchStart = new Set(remoteThreadIdsRef.current);
1913
2258
  const threadList = await aomiClientRef.current.listThreads(userAddress);
2259
+ if (cancelled) return;
1914
2260
  const currentContext = threadContextRef.current;
1915
2261
  const remoteThreadIds = /* @__PURE__ */ new Set();
1916
2262
  const newMetadata = new Map(currentContext.allThreadsMetadata);
1917
2263
  let maxChatNum = currentContext.threadCnt;
1918
2264
  for (const thread of threadList) {
1919
2265
  remoteThreadIds.add(thread.session_id);
1920
- const rawTitle = (_a = thread.title) != null ? _a : "";
2266
+ const rawTitle = (_a2 = thread.title) != null ? _a2 : "";
1921
2267
  const title = isPlaceholderTitle(rawTitle) ? "" : rawTitle;
1922
2268
  const lastActive = ((_b = newMetadata.get(thread.session_id)) == null ? void 0 : _b.lastActiveAt) || (/* @__PURE__ */ new Date()).toISOString();
1923
2269
  const existingControl = (_c = newMetadata.get(thread.session_id)) == null ? void 0 : _c.control;
@@ -1935,6 +2281,11 @@ function AomiRuntimeCore({
1935
2281
  }
1936
2282
  }
1937
2283
  }
2284
+ for (const threadId of remoteThreadIdsRef.current) {
2285
+ if (!remoteThreadIdsAtFetchStart.has(threadId)) {
2286
+ remoteThreadIds.add(threadId);
2287
+ }
2288
+ }
1938
2289
  remoteThreadIdsRef.current = remoteThreadIds;
1939
2290
  warmedThreadIdsRef.current = new Set(
1940
2291
  Array.from(warmedThreadIdsRef.current).filter(
@@ -1945,30 +2296,65 @@ function AomiRuntimeCore({
1945
2296
  if (maxChatNum > currentContext.threadCnt) {
1946
2297
  currentContext.setThreadCnt(maxChatNum);
1947
2298
  }
2299
+ scheduleThreadPrefetch(threadList.map((thread) => thread.session_id));
1948
2300
  if (remoteThreadIds.has(currentContext.currentThreadId)) {
1949
- await warmThread(currentContext.currentThreadId);
1950
- await ensureInitialState(currentContext.currentThreadId);
2301
+ setIsThreadLoading(true);
2302
+ try {
2303
+ await warmThread(currentContext.currentThreadId);
2304
+ if (!cancelled) {
2305
+ await ensureInitialState(currentContext.currentThreadId);
2306
+ }
2307
+ } finally {
2308
+ if (!cancelled) {
2309
+ setIsThreadLoading(false);
2310
+ }
2311
+ }
1951
2312
  }
1952
2313
  } catch (error) {
1953
2314
  console.error("Failed to fetch thread list:", error);
2315
+ } finally {
2316
+ if (!cancelled) {
2317
+ setIsThreadListLoading(false);
2318
+ }
1954
2319
  }
1955
2320
  };
1956
2321
  void fetchThreadList();
1957
- }, [user, aomiClientRef, ensureInitialState, warmThread]);
2322
+ return () => {
2323
+ var _a2;
2324
+ cancelled = true;
2325
+ (_a2 = prefetchCancelRef.current) == null ? void 0 : _a2.call(prefetchCancelRef);
2326
+ prefetchCancelRef.current = null;
2327
+ };
2328
+ }, [
2329
+ user,
2330
+ aomiClientRef,
2331
+ ensureInitialState,
2332
+ scheduleThreadPrefetch,
2333
+ warmThread
2334
+ ]);
2335
+ const isRemoteThread = useCallback7(
2336
+ (threadId) => remoteThreadIdsRef.current.has(threadId),
2337
+ []
2338
+ );
1958
2339
  const threadListAdapter = useMemo2(
1959
2340
  () => buildThreadListAdapter({
1960
2341
  aomiClientRef,
1961
2342
  threadContext,
1962
2343
  setIsRunning,
1963
- getInitialControl: getPreferredThreadControl
2344
+ isLoading: isThreadListLoading,
2345
+ getInitialControl: getPreferredThreadControl,
2346
+ isRemoteThread
1964
2347
  }),
1965
2348
  [
1966
2349
  aomiClientRef,
1967
2350
  getPreferredThreadControl,
2351
+ isRemoteThread,
2352
+ isThreadListLoading,
1968
2353
  setIsRunning,
1969
2354
  threadContext,
1970
2355
  threadContext.currentThreadId,
1971
- threadContext.allThreadsMetadata
2356
+ threadContext.allThreadsMetadata,
2357
+ currentMessages
1972
2358
  ]
1973
2359
  );
1974
2360
  useEffect4(() => {
@@ -2003,6 +2389,7 @@ function AomiRuntimeCore({
2003
2389
  }, [eventContext, notificationContext]);
2004
2390
  const runtime = useExternalStoreRuntime({
2005
2391
  messages: currentMessages,
2392
+ isLoading: isThreadLoading,
2006
2393
  setMessages: (msgs) => threadContext.setThreadMessages(threadContext.currentThreadId, [...msgs]),
2007
2394
  isRunning,
2008
2395
  onNew: async (message) => {
@@ -2010,7 +2397,6 @@ function AomiRuntimeCore({
2010
2397
  (part) => part.type === "text"
2011
2398
  ).map((part) => part.text).join("\n");
2012
2399
  if (text) {
2013
- await syncCurrentThreadControl();
2014
2400
  await orchestratorSendMessage(text, threadContext.currentThreadId);
2015
2401
  }
2016
2402
  },
@@ -2022,20 +2408,17 @@ function AomiRuntimeCore({
2022
2408
  });
2023
2409
  useEffect4(() => {
2024
2410
  return () => {
2025
- sessionManager.closeAll();
2411
+ var _a;
2412
+ (_a = prefetchCancelRef.current) == null ? void 0 : _a.call(prefetchCancelRef);
2413
+ closeAllSessions();
2026
2414
  };
2027
- }, [sessionManager]);
2415
+ }, [closeAllSessions]);
2028
2416
  const userContext = useUser();
2029
2417
  const sendMessage = useCallback7(
2030
2418
  async (text) => {
2031
- await syncCurrentThreadControl();
2032
2419
  await orchestratorSendMessage(text, threadContext.currentThreadId);
2033
2420
  },
2034
- [
2035
- orchestratorSendMessage,
2036
- syncCurrentThreadControl,
2037
- threadContext.currentThreadId
2038
- ]
2421
+ [orchestratorSendMessage, threadContext.currentThreadId]
2039
2422
  );
2040
2423
  const cancelGeneration = useCallback7(() => {
2041
2424
  void orchestratorCancel(threadContext.currentThreadId);
@@ -2053,10 +2436,10 @@ function AomiRuntimeCore({
2053
2436
  }, [threadListAdapter]);
2054
2437
  const deleteThread = useCallback7(
2055
2438
  async (threadId) => {
2056
- sessionManager.close(threadId);
2439
+ closeSession(threadId);
2057
2440
  await threadListAdapter.onDelete(threadId);
2058
2441
  },
2059
- [threadListAdapter, sessionManager]
2442
+ [closeSession, threadListAdapter]
2060
2443
  );
2061
2444
  const renameThread = useCallback7(
2062
2445
  async (threadId, title) => {
@@ -2172,9 +2555,13 @@ function AomiRuntimeCore({
2172
2555
  import { jsx as jsx8 } from "react/jsx-runtime";
2173
2556
  function AomiRuntimeProvider({
2174
2557
  children,
2175
- backendUrl = "http://localhost:8080"
2558
+ backendUrl = "http://localhost:8080",
2559
+ clientOptions
2176
2560
  }) {
2177
- const aomiClient = useMemo3(() => new AomiClient({ baseUrl: backendUrl }), [backendUrl]);
2561
+ const aomiClient = useMemo3(
2562
+ () => new AomiClient(__spreadValues({ baseUrl: backendUrl }, clientOptions)),
2563
+ [backendUrl, clientOptions]
2564
+ );
2178
2565
  return /* @__PURE__ */ jsx8(ThreadContextProvider, { children: /* @__PURE__ */ jsx8(NotificationContextProvider, { children: /* @__PURE__ */ jsx8(UserContextProvider, { children: /* @__PURE__ */ jsx8(AomiRuntimeInner, { aomiClient, children }) }) }) });
2179
2566
  }
2180
2567
  function AomiRuntimeInner({
@@ -2205,7 +2592,7 @@ function AomiRuntimeInner({
2205
2592
  }
2206
2593
 
2207
2594
  // packages/react/src/handlers/notification-handler.ts
2208
- import { useCallback as useCallback8, useEffect as useEffect5, useState as useState6 } from "react";
2595
+ import { useCallback as useCallback8, useEffect as useEffect5, useState as useState7 } from "react";
2209
2596
  var notificationIdCounter2 = 0;
2210
2597
  function generateNotificationId() {
2211
2598
  return `notif-${Date.now()}-${++notificationIdCounter2}`;
@@ -2214,7 +2601,7 @@ function useNotificationHandler({
2214
2601
  onNotification
2215
2602
  } = {}) {
2216
2603
  const { subscribe } = useEventContext();
2217
- const [notifications, setNotifications] = useState6([]);
2604
+ const [notifications, setNotifications] = useState7([]);
2218
2605
  useEffect5(() => {
2219
2606
  const unsubscribe = subscribe("notification", (event) => {
2220
2607
  var _a, _b;
@@ -2257,6 +2644,7 @@ export {
2257
2644
  SUPPORTED_CHAINS,
2258
2645
  ThreadContextProvider,
2259
2646
  UserContextProvider,
2647
+ UserState2 as UserState,
2260
2648
  aaModeFromExecutionKind,
2261
2649
  appendFeeCallToPayload,
2262
2650
  buildFeeAAWalletCall,