@agent-native/core 0.132.1 → 0.132.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +37 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/chat-threads/store.ts +42 -0
  5. package/corpus/core/src/client/use-chat-threads.ts +76 -16
  6. package/corpus/core/src/server/agent-chat-plugin.ts +16 -1
  7. package/corpus/templates/clips/changelog/2026-07-30-meeting-microphone-transcription-works-reliably-from-the-fir.md +6 -0
  8. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +13 -0
  9. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +2 -0
  10. package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +25 -132
  11. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +5 -0
  12. package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +5 -3
  13. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +23 -0
  14. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +3 -0
  15. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +11 -7
  16. package/corpus/templates/design/changelog/2026-07-30-fixed-the-keyboard-shortcuts-panel-not-opening-with-ctrl-shi.md +6 -0
  17. package/dist/chat-threads/store.d.ts +13 -0
  18. package/dist/chat-threads/store.d.ts.map +1 -1
  19. package/dist/chat-threads/store.js +35 -0
  20. package/dist/chat-threads/store.js.map +1 -1
  21. package/dist/client/use-chat-threads.d.ts.map +1 -1
  22. package/dist/client/use-chat-threads.js +66 -16
  23. package/dist/client/use-chat-threads.js.map +1 -1
  24. package/dist/collab/struct-routes.d.ts +1 -1
  25. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  26. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  27. package/dist/notifications/routes.d.ts +3 -3
  28. package/dist/observability/routes.d.ts +1 -1
  29. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  30. package/dist/server/agent-chat-plugin.js +13 -2
  31. package/dist/server/agent-chat-plugin.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/chat-threads/store.ts +42 -0
  34. package/src/client/use-chat-threads.ts +76 -16
  35. package/src/server/agent-chat-plugin.ts +16 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.132.1",
3
+ "version": "0.132.2",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -766,6 +766,48 @@ export async function searchThreads(
766
766
  .filter((r): r is ChatThreadSummary => r !== null);
767
767
  }
768
768
 
769
+ /**
770
+ * Scope a thread should carry after a run inside a resource: adopt when it has
771
+ * none, otherwise keep what it has. An unscoped thread reads as general, and a
772
+ * general chat renders inside every resource — so never retag, never clear.
773
+ */
774
+ export function resolveRunThreadScope(
775
+ existing: ChatThreadScope | null,
776
+ incoming: ChatThreadScope | null | undefined,
777
+ ): ChatThreadScope | null {
778
+ if (existing) return existing;
779
+ return incoming ?? null;
780
+ }
781
+
782
+ /**
783
+ * Claim an unscoped thread for `scope`, returning the scope it actually ends up
784
+ * with. `withThreadDataLock` only serializes one process, so two workers can
785
+ * both read the same unscoped row; the `scope_type IS NULL` guard makes the
786
+ * first writer win and the loser reports the winner instead of retagging.
787
+ */
788
+ export async function adoptThreadScopeIfUnscoped(
789
+ id: string,
790
+ scope: ChatThreadScope,
791
+ ): Promise<ChatThreadScope | null> {
792
+ await ensureTable();
793
+ const client = getDbExec();
794
+ const result = await client.execute({
795
+ sql: `UPDATE chat_threads SET scope_type = ?, scope_id = ?, scope_label = ?, updated_at = ? WHERE id = ? AND scope_type IS NULL`,
796
+ args: [
797
+ scope.type,
798
+ scope.id,
799
+ scope.label ?? null,
800
+ Math.max(Date.now(), 1),
801
+ id,
802
+ ],
803
+ });
804
+ if (result.rowsAffected > 0) {
805
+ emitChatThreadChange(id);
806
+ return scope;
807
+ }
808
+ return (await getThread(id))?.scope ?? null;
809
+ }
810
+
769
811
  /**
770
812
  * Detach or rebind a chat's scope. Used by the UI's "Detach from <resource>"
771
813
  * action and by templates that need to retag a chat after a rename. Pass
@@ -91,6 +91,25 @@ async function fetchThreadListPage(
91
91
  });
92
92
  }
93
93
 
94
+ /**
95
+ * Look up one thread the list page did not carry. Distinguishes the three states
96
+ * the caller must not collapse: the thread (found), `null` (the server denies it
97
+ * exists), and `undefined` (unreachable — nothing was learned).
98
+ */
99
+ async function fetchThreadById(
100
+ apiUrl: string,
101
+ id: string,
102
+ ): Promise<ChatThreadSummary | null | undefined> {
103
+ try {
104
+ const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(id)}`);
105
+ if (res.status === 404) return null;
106
+ if (!res.ok) return undefined;
107
+ return (await res.json()) as ChatThreadSummary;
108
+ } catch {
109
+ return undefined;
110
+ }
111
+ }
112
+
94
113
  function emitThreadsUpdated() {
95
114
  if (typeof window === "undefined") return;
96
115
  window.dispatchEvent(new CustomEvent(THREADS_UPDATED_EVENT));
@@ -411,6 +430,17 @@ export function useChatThreads(
411
430
  } catch {
412
431
  nextActiveThreadId = null;
413
432
  }
433
+ // Only a known mismatch disqualifies the pointer — an unresolved scope
434
+ // must not be read as "belongs here".
435
+ if (nextActiveThreadId) {
436
+ const savedScope = readKnownThreadScope(nextActiveThreadId);
437
+ if (
438
+ savedScope !== undefined &&
439
+ !threadCanStayVisibleInScope(savedScope, scopeRef.current)
440
+ ) {
441
+ nextActiveThreadId = null;
442
+ }
443
+ }
414
444
  if (!nextActiveThreadId && autoCreate) {
415
445
  nextActiveThreadId = createLocalThreadId();
416
446
  newlyCreatedRef.current.add(nextActiveThreadId);
@@ -583,7 +613,7 @@ export function useChatThreads(
583
613
 
584
614
  (async () => {
585
615
  const loadedThreads = await fetchThreads();
586
- const savedId = activeThreadIdRef.current;
616
+ const restoredId = activeThreadIdRef.current;
587
617
  if (loadedThreads === undefined) {
588
618
  // Thread-list fetch failed. Do not reclassify a saved id as a new
589
619
  // optimistic tab; AssistantChat should still get a chance to restore
@@ -591,8 +621,40 @@ export function useChatThreads(
591
621
  setIsLoading(false);
592
622
  return;
593
623
  }
624
+ // Exempts route-owned threads (the URL names what the user asked for) and
625
+ // ids this client generated, which have never reached the server.
626
+ const lookupRestored = Boolean(
627
+ restoredId &&
628
+ !routeControlsActiveThread &&
629
+ !newlyCreatedRef.current.has(restoredId),
630
+ );
631
+ const restoredOnPage = restoredId
632
+ ? loadedThreads.find((t) => t.id === restoredId)
633
+ : undefined;
634
+ // One page, so absence from it is not absence from the server — this is what
635
+ // separates an older real thread from the ghost tab reclassified below.
636
+ const restoredThread =
637
+ lookupRestored && !restoredOnPage
638
+ ? await fetchThreadById(apiUrl, restoredId!)
639
+ : restoredOnPage;
640
+ if (restoredThread === undefined && lookupRestored && !restoredOnPage) {
641
+ // Lookup unreachable. Reclassifying now would stamp this thread with the
642
+ // current scope on a guess; leave it untouched for the next mount.
643
+ setIsLoading(false);
644
+ return;
645
+ }
646
+ const restoredBelongsElsewhere = Boolean(
647
+ restoredThread &&
648
+ !threadCanStayVisibleInScope(
649
+ restoredThread.scope ?? null,
650
+ scopeRef.current,
651
+ ),
652
+ );
653
+ if (restoredBelongsElsewhere) setActiveThreadId(null);
654
+ const savedId = restoredBelongsElsewhere ? null : restoredId;
594
655
  const loadedHasSavedId = Boolean(
595
- savedId && loadedThreads.some((t) => t.id === savedId),
656
+ savedId &&
657
+ (restoredThread || loadedThreads.some((t) => t.id === savedId)),
596
658
  );
597
659
  const savedIdCameFromRoute =
598
660
  Boolean(savedId) &&
@@ -646,6 +708,7 @@ export function useChatThreads(
646
708
  setIsLoading(false);
647
709
  })();
648
710
  }, [
711
+ apiUrl,
649
712
  fetchThreads,
650
713
  addOptimisticThread,
651
714
  autoCreate,
@@ -948,12 +1011,9 @@ export function useChatThreads(
948
1011
  [apiUrl, clearUserRenamedThread, createThread],
949
1012
  );
950
1013
 
951
- // Ref to look up the latest scope of a known thread inside
952
- // saveThreadData without making the callback re-create on every
953
- // setThreads. The thread's scope is owned by createThread /
954
- // detachThread / fetchThreads — saveThreadData just mirrors it on
955
- // every save so the server eventually catches up after
956
- // persistSubmittedUserMessage creates the row sans scope.
1014
+ // Reads scope through refs so this callback survives every setThreads. Scope
1015
+ // rides only on creation: a periodic save must never move an existing thread
1016
+ // between resources, however stale this client's guess is.
957
1017
  const saveThreadData = useCallback(
958
1018
  async (
959
1019
  id: string,
@@ -968,7 +1028,7 @@ export function useChatThreads(
968
1028
  try {
969
1029
  const { titleSource, ...threadDataPayload } = data;
970
1030
  const localThread = threadsRef.current.find((t) => t.id === id);
971
- const localScope = localThread?.scope ?? null;
1031
+ const knownScope = readKnownThreadScope(id) ?? null;
972
1032
  const preserveUserTitle = userRenamedThreadIdsRef.current.has(id);
973
1033
  const title = nextThreadTitle(
974
1034
  localThread?.title,
@@ -977,11 +1037,7 @@ export function useChatThreads(
977
1037
  titleSource,
978
1038
  { preserveUserTitle },
979
1039
  );
980
- const payload = {
981
- ...threadDataPayload,
982
- title,
983
- scope: localScope,
984
- };
1040
+ const payload = { ...threadDataPayload, title };
985
1041
  let response = await fetch(
986
1042
  `${apiUrl}/threads/${encodeURIComponent(id)}`,
987
1043
  {
@@ -997,7 +1053,11 @@ export function useChatThreads(
997
1053
  const created = await fetch(`${apiUrl}/threads`, {
998
1054
  method: "POST",
999
1055
  headers: { "Content-Type": "application/json" },
1000
- body: JSON.stringify({ id, title, scope: localScope }),
1056
+ body: JSON.stringify({
1057
+ id,
1058
+ title,
1059
+ ...(knownScope ? { scope: knownScope } : {}),
1060
+ }),
1001
1061
  });
1002
1062
  if (!created.ok) return;
1003
1063
  response = await fetch(
@@ -1059,7 +1119,7 @@ export function useChatThreads(
1059
1119
  });
1060
1120
  } catch {}
1061
1121
  },
1062
- [apiUrl],
1122
+ [apiUrl, readKnownThreadScope],
1063
1123
  );
1064
1124
 
1065
1125
  const generateTitle = useCallback(
@@ -107,10 +107,12 @@ import type {
107
107
  import { readAppStateForCurrentTab } from "../application-state/script-helpers.js";
108
108
  import { runChatThreadDataMigrations } from "../chat-threads/migrations.js";
109
109
  import {
110
+ adoptThreadScopeIfUnscoped,
110
111
  createThread,
111
112
  forkThread,
112
113
  getThread,
113
114
  registerChatThreadsShareable,
115
+ resolveRunThreadScope,
114
116
  resolveThreadAccess,
115
117
  listThreads,
116
118
  searchThreads,
@@ -2538,11 +2540,16 @@ export function createAgentChatPlugin(
2538
2540
  getRequestRunContext()?.owner ?? getRequestUserEmail();
2539
2541
  if (!ownerEmail) return;
2540
2542
 
2543
+ const runScope = getRequestRunContext()?.chatScope ?? null;
2544
+
2541
2545
  await withThreadDataLock(threadId, async () => {
2542
2546
  let thread = await getThread(threadId);
2543
2547
  if (!thread) {
2544
2548
  try {
2545
- thread = await createThread(ownerEmail, { id: threadId });
2549
+ thread = await createThread(ownerEmail, {
2550
+ id: threadId,
2551
+ scope: runScope,
2552
+ });
2546
2553
  } catch {
2547
2554
  thread = await getThread(threadId);
2548
2555
  }
@@ -2566,6 +2573,14 @@ export function createAgentChatPlugin(
2566
2573
  });
2567
2574
  }
2568
2575
 
2576
+ const nextScope = resolveRunThreadScope(thread.scope, runScope);
2577
+ if (nextScope && nextScope !== thread.scope) {
2578
+ thread = {
2579
+ ...thread,
2580
+ scope: await adoptThreadScopeIfUnscoped(threadId, nextScope),
2581
+ };
2582
+ }
2583
+
2569
2584
  let repo: any;
2570
2585
  try {
2571
2586
  repo = JSON.parse(thread.threadData || "{}");