@ai4b-team/fsaos-gateway-sdk 3.34.2 → 3.35.1

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/iife/ui.js CHANGED
@@ -1,3 +1,3 @@
1
- /* @fsaos/ui v2.0.0 — FSAOS UI Primitives — 2026-10-03T21:58:22.099Z */
1
+ /* @fsaos/ui v2.0.0 — FSAOS UI Primitives — 2026-10-07T03:44:34.236Z */
2
2
  "use strict";var __FSAOS_UI__=(()=>{var m=Object.create;var r=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var u=Object.getOwnPropertyNames;var f=Object.getPrototypeOf,h=Object.prototype.hasOwnProperty;var y=(e,i)=>()=>(i||e((i={exports:{}}).exports,i),i.exports),N=(e,i)=>{for(var a in i)r(e,a,{get:i[a],enumerable:!0})},s=(e,i,a,l)=>{if(i&&typeof i=="object"||typeof i=="function")for(let t of u(i))!h.call(e,t)&&t!==a&&r(e,t,{get:()=>i[t],enumerable:!(l=g(i,t))||l.enumerable});return e};var x=(e,i,a)=>(a=e!=null?m(f(e)):{},s(i||!e||!e.__esModule?r(a,"default",{value:e,enumerable:!0}):a,e)),b=e=>s(r({},"__esModule",{value:!0}),e);var d=y((V,c)=>{c.exports=React});var P={};N(P,{Badge:()=>z,Breadcrumbs:()=>A,Button:()=>D,Card:()=>w,DataView:()=>R,Detail:()=>_,Empty:()=>I,FilePreview:()=>O,Form:()=>F,Grid:()=>k,Image:()=>G,Link:()=>E,Page:()=>v,Router:()=>T,Sidebar:()=>S,Spinner:()=>B,Stack:()=>C,Table:()=>j,Tabs:()=>W,Video:()=>H});var n=x(d(),1);function o(){return(typeof window<"u"&&window.__FSAOS_CONFIG__||{}).edgeBaseUrl||""}function v(e){return(0,n.createElement)("div",{style:{minHeight:"100vh",display:"flex",flexDirection:"column"},className:e.className||""},e.children)}function C(e){let i=e.direction||"vertical";return(0,n.createElement)("div",{style:{display:"flex",flexDirection:i==="horizontal"?"row":"column",gap:e.gap||"0.5rem",padding:e.padding||"0",alignItems:e.align||"stretch",justifyContent:e.justify||"flex-start",flex:e.flex||"initial"},className:e.className||""},e.children)}function k(e){return(0,n.createElement)("div",{style:{display:"grid",gridTemplateColumns:"repeat("+(e.columns||3)+", 1fr)",gap:e.gap||"1rem",padding:e.padding||"0"},className:e.className||""},e.children)}function S(e){return(0,n.createElement)("div",{style:{display:"flex",flexDirection:"row",minHeight:"100vh"},className:e.className||""},e.children)}function w(e){return(0,n.createElement)("div",{style:{border:"1px solid var(--color-border, #e5e7eb)",borderRadius:"var(--border-radius, 0.5rem)",padding:e.padding||"1rem",background:"var(--color-surface, #fff)"},className:e.className||"",onClick:e.onClick},e.children)}function z(e){return(0,n.createElement)("span",{style:{display:"inline-flex",alignItems:"center",padding:"0.125rem 0.5rem",fontSize:"0.75rem",fontWeight:"500",borderRadius:"9999px",background:e.color||"#e5e7eb",color:e.textColor||"#374151"},className:e.className||""},e.children||e.label)}function B(e){let i=e.size||"1.5rem";return(0,n.createElement)("div",{style:{width:i,height:i,border:"2px solid #e5e7eb",borderTopColor:"var(--color-primary, #3b82f6)",borderRadius:"50%",animation:"fsaos-spin 0.6s linear infinite"}})}function D(e){return(0,n.createElement)("button",{style:{display:"inline-flex",alignItems:"center",justifyContent:"center",padding:"0.5rem 1rem",fontSize:"0.875rem",fontWeight:"500",borderRadius:"var(--border-radius, 0.375rem)",border:e.variant==="outline"?"1px solid var(--color-border, #d1d5db)":"none",background:e.variant==="outline"||e.variant==="ghost"?"transparent":"var(--color-primary, #3b82f6)",color:e.variant==="outline"||e.variant==="ghost"?"var(--color-text, #1a1a1a)":"#fff",cursor:e.disabled?"not-allowed":"pointer",opacity:e.disabled?"0.5":"1",gap:"0.5rem"},className:e.className||"",onClick:e.onClick,disabled:e.disabled},e.children||e.label)}function I(e){return(0,n.createElement)("div",{style:{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",padding:"3rem",color:"var(--color-text-secondary, #6b7280)",textAlign:"center"}},e.icon?(0,n.createElement)("div",{style:{fontSize:"2rem",marginBottom:"0.5rem"}},e.icon):null,(0,n.createElement)("div",{style:{fontWeight:500,marginBottom:"0.25rem"}},e.title||e.message||e.children||"No items"),e.description?(0,n.createElement)("div",{style:{fontSize:"0.875rem",opacity:.7}},e.description):null)}function R(e){return(0,n.createElement)("div",{className:e.className||""},e.children)}function j(e){return(0,n.createElement)("table",{className:e.className||""},e.children)}function F(e){return(0,n.createElement)("form",{className:e.className||"",onSubmit:e.onSubmit},e.children)}function _(e){return(0,n.createElement)("div",{className:e.className||""},e.children)}function T(e){return(0,n.createElement)("div",null,e.children)}function E(e){return(0,n.createElement)("a",{href:e.href||e.to,className:e.className||"",onClick:e.onClick},e.children)}function W(e){return(0,n.createElement)("div",{className:e.className||""},e.children)}function A(e){return(0,n.createElement)("nav",{className:e.className||""},e.children)}function G(e){return(0,n.createElement)("img",{src:e.src||(e.path?o()+e.path:""),alt:e.alt||"",className:e.className||"",style:e.style})}function H(e){return(0,n.createElement)("video",{src:e.src||(e.path?o()+e.path:""),controls:!0,className:e.className||""})}function O(e){return(0,n.createElement)("div",{className:e.className||""},e.children||"File preview")}return b(P);})();
3
3
  //# sourceMappingURL=ui.js.map
package/dist/index.cjs CHANGED
@@ -782,6 +782,10 @@ function listingTypes(filter) {
782
782
  }
783
783
  function affectsListing(query, filter, event) {
784
784
  if (containsChangedItem(query, event)) return true;
785
+ if (filter.spaceIds !== void 0) {
786
+ if (!filter.spaceIds.length) return false;
787
+ if (event.space_item_id !== void 0 && !filter.spaceIds.includes(event.space_item_id) && !filter.spaceIds.includes(event.old_space_item_id)) return false;
788
+ }
785
789
  const scope = filter.scope;
786
790
  const scoped = typeof scope === "string" && scope !== "__unscoped__";
787
791
  const hasPath = typeof event.path === "string" || typeof event.old_path === "string";
@@ -825,6 +829,29 @@ function clearInfiniteItemsRefreshes(client2) {
825
829
  pending2.delete(client2);
826
830
  }
827
831
 
832
+ // src/items-selection.ts
833
+ var SelectedSpaceContractError = class extends Error {
834
+ constructor() {
835
+ super("The server did not confirm the selected-Space listing contract");
836
+ }
837
+ };
838
+ function assertItemsSelection(requested, returned) {
839
+ if (!Array.isArray(returned) || returned.some((id) => typeof id !== "string") || JSON.stringify([...new Set(returned)].sort()) !== JSON.stringify(requested)) {
840
+ throw new SelectedSpaceContractError();
841
+ }
842
+ }
843
+ function normalizeItemsSelection(filter) {
844
+ if (!filter || filter.spaceIds === void 0) return filter;
845
+ return {
846
+ ...filter,
847
+ spaceIds: [...new Set(filter.spaceIds.map((id) => id.toLowerCase()))].sort(),
848
+ ...filter.limit !== void 0 ? { limit: Math.max(1, Math.min(filter.limit, 500)) } : {}
849
+ };
850
+ }
851
+ function unavailableItemsSelection(error) {
852
+ return error instanceof SelectedSpaceContractError || error instanceof EnforcementDeniedError || error instanceof GatewayRequestError && (error.payload?.error === "SPACE_SELECTION_UNAVAILABLE" || error.payload?.error === "QUERY_PARAMS_INVALID" || error.status === 401 || error.status === 403);
853
+ }
854
+
828
855
  // src/vfs-keys.ts
829
856
  function sk() {
830
857
  return getScope() ?? "__unscoped__";
@@ -847,7 +874,7 @@ var vfsKeys = {
847
874
  /** Search results (scope-aware — search is scoped). */
848
875
  search: (query, types) => ["vfs", sk(), "search", query, ...types ?? []],
849
876
  /** Items query (type-based listing with filters, scope-aware). */
850
- items: (type, filters) => ["vfs", sk(), "items", type, JSON.stringify(filters ?? {})],
877
+ items: (type, filters) => ["vfs", sk(), "items", type, JSON.stringify(normalizeItemsSelection(filters ?? {}))],
851
878
  /** All items queries for current scope (for broad invalidation). */
852
879
  allItems: () => ["vfs", sk(), "items"],
853
880
  /** Recursive tree at a path + depth (scope-aware). */
@@ -1737,12 +1764,15 @@ function itemsFilterFromQueryFilter(typeData, extra = {}) {
1737
1764
  return { ...filter, ...extra };
1738
1765
  }
1739
1766
  async function fetchItems(filter, options = {}) {
1767
+ filter = normalizeItemsSelection(filter);
1768
+ if (filter.spaceIds?.length === 0) return { items: [], total_hint: 0 };
1740
1769
  const params = {};
1741
1770
  if (filter.type) params.item_type = filter.type;
1742
1771
  if (filter.scope) {
1743
1772
  params.scope_path = filter.scope;
1744
1773
  }
1745
1774
  const filters = {};
1775
+ if (filter.spaceIds !== void 0) filters.space_ids = filter.spaceIds;
1746
1776
  if (filter.name) filters.name = filter.name;
1747
1777
  if (filter.tag) filters.tag = filter.tag;
1748
1778
  if (filter.parent_id) filters.parent_id = filter.parent_id;
@@ -1769,6 +1799,9 @@ async function fetchItems(filter, options = {}) {
1769
1799
  }
1770
1800
  try {
1771
1801
  const response = await gatewayCall("query", params);
1802
+ if (filter.spaceIds !== void 0) {
1803
+ assertItemsSelection(filter.spaceIds, response.selected_space_ids ?? response.content?.selected_space_ids);
1804
+ }
1772
1805
  const items = extractItems(response);
1773
1806
  for (const item of items) {
1774
1807
  getQueryClient().setQueryData(vfsKeys.item(item.path), item);
@@ -1776,7 +1809,7 @@ async function fetchItems(filter, options = {}) {
1776
1809
  return { items, total_hint: response.total ?? response.content?.total ?? items.length };
1777
1810
  } catch (err) {
1778
1811
  console.error(`[gateway] Failed to fetch items of type ${filter.type ?? "(any_of)"}:`, err);
1779
- if (options.throwOnError) throw err;
1812
+ if (options.throwOnError || filter.spaceIds !== void 0) throw err;
1780
1813
  return { items: [], total_hint: 0 };
1781
1814
  }
1782
1815
  }
@@ -2109,7 +2142,7 @@ function handleCcmChange(data) {
2109
2142
  const sourceId = data.source_item_id || data.source_id;
2110
2143
  const targetId = data.target_item_id || data.target_id;
2111
2144
  const looksEdgeRelated = data.edge_type != null || sourceId != null || targetId != null;
2112
- if (!looksEdgeRelated) return;
2145
+ if (!looksEdgeRelated || !sourceId && !targetId || data.item_type === "message") return;
2113
2146
  const debounceKey = `ccm:${sourceId ?? ""}:${targetId ?? ""}`;
2114
2147
  const existing = debounceTimers.get(debounceKey);
2115
2148
  if (existing) clearTimeout(existing);
@@ -2121,11 +2154,6 @@ function handleCcmChange(data) {
2121
2154
  if (targetId) {
2122
2155
  getQueryClient().invalidateQueries({ queryKey: vfsKeys.edges(targetId) });
2123
2156
  }
2124
- if (!sourceId && !targetId) {
2125
- getQueryClient().invalidateQueries({
2126
- predicate: (query) => query.queryKey[0] === "vfs" && query.queryKey[1] === "edges"
2127
- });
2128
- }
2129
2157
  }, DEBOUNCE_MS);
2130
2158
  debounceTimers.set(debounceKey, timer);
2131
2159
  }
@@ -2307,6 +2335,148 @@ function replaceMounts(refs, anchor) {
2307
2335
  });
2308
2336
  }
2309
2337
 
2338
+ // src/mounted-reader-events.ts
2339
+ function inMountedReadScope(event) {
2340
+ if (typeof event.path !== "string") return false;
2341
+ const roots = [...getMounts().map((mount3) => mount3.path), getAnchorRef()?.path].filter((path) => !!path);
2342
+ if (!roots.length && getScope()) roots.push(getScope());
2343
+ return roots.some((root) => isChannelPath(event.path, root));
2344
+ }
2345
+
2346
+ // src/channel-message-events.ts
2347
+ function channelMessageChange(event, channelPath, parentId, messages) {
2348
+ if (!["message", "card"].includes(event?.item_type) || !isChannelPath(event.path, channelPath)) return false;
2349
+ if (!parentId) return true;
2350
+ if (event.item_id === parentId || event.parent_id === parentId) return true;
2351
+ return messages.some((message) => {
2352
+ if (event.item_id === message.id || event.parent_id === message.id) return true;
2353
+ const root = message.id === parentId ? message.path : message.parent_message_id === parentId ? message.path.slice(0, message.path.lastIndexOf("/")) : void 0;
2354
+ return !!root && isChannelPath(event.path, root);
2355
+ });
2356
+ }
2357
+
2358
+ // src/channel-realtime.ts
2359
+ var watches = /* @__PURE__ */ new WeakMap();
2360
+ function watchChannels(client2, key, normalize) {
2361
+ let entries = watches.get(client2);
2362
+ if (!entries) {
2363
+ entries = /* @__PURE__ */ new Map();
2364
+ watches.set(client2, entries);
2365
+ }
2366
+ const hash = JSON.stringify(key);
2367
+ let watch = entries.get(hash);
2368
+ if (!watch) {
2369
+ let stopped = false;
2370
+ let timer;
2371
+ let running = false;
2372
+ let inventory = false;
2373
+ const channels = /* @__PURE__ */ new Set();
2374
+ const renames = /* @__PURE__ */ new Set();
2375
+ const current = () => client2.getQueryData(key) ?? [];
2376
+ const schedule2 = () => {
2377
+ if (stopped || running || timer) return;
2378
+ timer = setTimeout(() => {
2379
+ timer = void 0;
2380
+ void flush();
2381
+ }, 250);
2382
+ };
2383
+ const flush = async () => {
2384
+ if (stopped) return;
2385
+ running = true;
2386
+ const reload = inventory;
2387
+ inventory = false;
2388
+ const pending3 = [...channels];
2389
+ const checkRename = new Set(renames);
2390
+ renames.clear();
2391
+ channels.clear();
2392
+ try {
2393
+ if (reload) {
2394
+ await refreshQuery(client2, key, { exact: true });
2395
+ } else {
2396
+ const results = await Promise.allSettled(pending3.map(async (id) => {
2397
+ const owner = client2.getQueryCache().find({ queryKey: key, exact: true });
2398
+ if (!owner) return;
2399
+ const result = await gatewayCall("read", { id });
2400
+ if (result.success === false) throw new Error("Channel refresh failed");
2401
+ if (stopped || !owner || client2.getQueryCache().find({ queryKey: key, exact: true }) !== owner) return;
2402
+ const raw = normalizeItem(result.item ?? result);
2403
+ if (raw.id !== id || raw.item_type !== "channel") return;
2404
+ const previous = current().find((item2) => item2.id === id);
2405
+ if (!previous) return;
2406
+ const item = normalize({ ...raw });
2407
+ const next = {
2408
+ ...previous,
2409
+ path: item.path,
2410
+ name: item.name,
2411
+ displayName: item.displayName,
2412
+ messageCount: item.messageCount,
2413
+ aiMode: item.aiMode,
2414
+ isPrivate: item.isPrivate,
2415
+ isDm: item.isDm,
2416
+ autoPilot: item.autoPilot,
2417
+ updatedAt: item.updatedAt
2418
+ };
2419
+ if (checkRename.has(id) && (previous.name !== next.name || previous.displayName !== next.displayName || previous.path !== next.path)) {
2420
+ inventory = true;
2421
+ }
2422
+ client2.setQueryData(key, (list) => list?.map((item2) => item2.id === id ? next : item2));
2423
+ }));
2424
+ const failed = results.find((result) => result.status === "rejected");
2425
+ if (failed?.status === "rejected") throw failed.reason;
2426
+ }
2427
+ } catch (error) {
2428
+ console.warn("[channels] refresh failed", error);
2429
+ } finally {
2430
+ running = false;
2431
+ if (inventory || channels.size) schedule2();
2432
+ }
2433
+ };
2434
+ const inScope = (data) => {
2435
+ if (current().some((c) => c.id === data.item_id || c.path === data.path)) return true;
2436
+ return inMountedReadScope(data);
2437
+ };
2438
+ const onChannel = (data) => {
2439
+ if (!inScope(data)) return;
2440
+ const action = String(data.action ?? "").toLowerCase();
2441
+ if (["insert", "delete", "rename", "move"].includes(action)) inventory = true;
2442
+ else if (action === "update") {
2443
+ const channel = current().find((c) => c.id === data.item_id || c.path === data.path);
2444
+ if (channel) {
2445
+ channels.add(channel.id);
2446
+ renames.add(channel.id);
2447
+ }
2448
+ }
2449
+ if (inventory || channels.size) schedule2();
2450
+ };
2451
+ const onMessage = (data) => {
2452
+ const channel = current().find((c) => c.id === data.parent_id);
2453
+ if (!channel) return;
2454
+ channels.add(channel.id);
2455
+ schedule2();
2456
+ };
2457
+ const stops = ["vfs_change", "ccm_change"].flatMap((type) => [
2458
+ subscribeToEvents(type, onChannel, (data) => data.item_type === "channel"),
2459
+ subscribeToEvents(type, onMessage, (data) => data.item_type === "message")
2460
+ ]);
2461
+ watch = { users: 0, stop: () => {
2462
+ stopped = true;
2463
+ if (timer) clearTimeout(timer);
2464
+ stops.forEach((stop) => stop());
2465
+ } };
2466
+ entries.set(hash, watch);
2467
+ }
2468
+ watch.users++;
2469
+ let released = false;
2470
+ return () => {
2471
+ if (released) return;
2472
+ released = true;
2473
+ if (--watch.users === 0) {
2474
+ watch.stop();
2475
+ entries.delete(hash);
2476
+ }
2477
+ };
2478
+ }
2479
+
2310
2480
  // src/hooks.ts
2311
2481
  function useScopeKey() {
2312
2482
  return React.useSyncExternalStore(subscribeScope, getScopeVersion, getScopeVersion);
@@ -2390,22 +2560,28 @@ function useResolve(params) {
2390
2560
  }
2391
2561
  function useItems(filter) {
2392
2562
  useScopeKey();
2393
- const stableFilter = React.useMemo(() => filter, [JSON.stringify(filter)]);
2394
- return reactQuery.useQuery({
2563
+ const selected = normalizeItemsSelection(filter);
2564
+ const stableFilter = React.useMemo(() => selected, [JSON.stringify(selected)]);
2565
+ const query = reactQuery.useQuery({
2395
2566
  queryKey: vfsKeys.items(
2396
2567
  stableFilter?.type ?? "",
2397
2568
  stableFilter
2398
2569
  ),
2399
2570
  queryFn: () => fetchItems(stableFilter),
2400
2571
  enabled: isListableItemsFilter(stableFilter),
2572
+ ...stableFilter?.spaceIds !== void 0 ? {
2573
+ retry: (attempt, error) => !unavailableItemsSelection(error) && attempt < 1
2574
+ } : {},
2401
2575
  select: (result) => result.items
2402
2576
  });
2577
+ return stableFilter?.spaceIds !== void 0 && unavailableItemsSelection(query.error) ? { ...query, data: void 0 } : query;
2403
2578
  }
2404
2579
  function useInfiniteItems(filter, options = {}) {
2405
2580
  const scopeKey = useScopeKey();
2406
2581
  const { realtime = false } = options;
2407
- const PAGE_SIZE = filter?.limit ?? 20;
2408
- const stableFilter = React.useMemo(() => filter, [JSON.stringify(filter)]);
2582
+ const selected = normalizeItemsSelection(filter);
2583
+ const PAGE_SIZE = selected?.limit ?? 20;
2584
+ const stableFilter = React.useMemo(() => selected, [JSON.stringify(selected)]);
2409
2585
  const stableKey = React.useMemo(() => ["vfs", getScope() ?? "__unscoped__", "infinite-items", stableFilter?.type ?? "", JSON.stringify(stableFilter ?? {})], [scopeKey, stableFilter]);
2410
2586
  const query = reactQuery.useInfiniteQuery({
2411
2587
  queryKey: stableKey,
@@ -2418,16 +2594,21 @@ function useInfiniteItems(filter, options = {}) {
2418
2594
  },
2419
2595
  initialPageParam: 0,
2420
2596
  getNextPageParam: (lastPage, _allPages, lastPageParam) => {
2597
+ if (stableFilter?.spaceIds !== void 0 && lastPage.total_hint !== void 0 && lastPageParam + lastPage.items.length >= lastPage.total_hint) return void 0;
2421
2598
  if (lastPage.items.length < PAGE_SIZE) return void 0;
2422
2599
  return lastPageParam + lastPage.items.length;
2423
2600
  },
2424
2601
  enabled: isListableItemsFilter(stableFilter),
2602
+ ...stableFilter?.spaceIds !== void 0 ? {
2603
+ retry: (attempt, error) => !unavailableItemsSelection(error) && attempt < 1
2604
+ } : {},
2425
2605
  // Realtime marks the cache stale only on relevant changes or reconnect.
2426
2606
  // Expanding a channel or handing it to another observer reuses its pages.
2427
2607
  staleTime: realtime ? Infinity : void 0,
2428
2608
  meta: realtime && stableFilter ? { infiniteItemsRealtime: { filter: stableFilter } } : {}
2429
2609
  });
2430
2610
  const allItems = React.useMemo(() => {
2611
+ if (stableFilter?.spaceIds !== void 0 && unavailableItemsSelection(query.error)) return [];
2431
2612
  if (!query.data?.pages) return [];
2432
2613
  const seen = /* @__PURE__ */ new Set();
2433
2614
  const flat = [];
@@ -2440,17 +2621,18 @@ function useInfiniteItems(filter, options = {}) {
2440
2621
  }
2441
2622
  }
2442
2623
  return flat;
2443
- }, [query.data?.pages]);
2624
+ }, [query.data?.pages, query.error, stableFilter]);
2625
+ const unavailable = stableFilter?.spaceIds !== void 0 && unavailableItemsSelection(query.error);
2444
2626
  return {
2445
2627
  allItems,
2446
2628
  refetch: query.refetch,
2447
2629
  fetchNextPage: query.fetchNextPage,
2448
2630
  isFetchingNextPage: query.isFetchingNextPage,
2449
- isExhausted: !query.hasNextPage,
2631
+ isExhausted: unavailable || !query.hasNextPage,
2450
2632
  isLoading: query.isLoading,
2451
2633
  isFetching: query.isFetching,
2452
2634
  error: query.error,
2453
- totalHint: query.data?.pages?.[0]?.total_hint
2635
+ totalHint: unavailable ? void 0 : query.data?.pages?.[0]?.total_hint
2454
2636
  };
2455
2637
  }
2456
2638
  function useInfiniteChildren(path) {
@@ -2758,10 +2940,15 @@ function useMounts() {
2758
2940
  useScopeKey();
2759
2941
  const version = React.useSyncExternalStore(subscribeMounts, getMountVersion, getMountVersion);
2760
2942
  const mounts = React.useMemo(() => getMounts(), [version]);
2943
+ const anchor = getAnchorRef();
2944
+ const selectedSpaces = React.useMemo(() => mounts.filter((ref) => ref.kind === "space" && (anchor?.kind === "account" || ref.id === anchor?.id)), [mounts, anchor]);
2945
+ const readSpaceIds = React.useMemo(() => selectedSpaces.map((ref) => ref.id).sort(), [selectedSpaces]);
2761
2946
  return {
2762
2947
  mounts,
2948
+ selectedSpaces,
2949
+ readSpaceIds,
2763
2950
  homeAnchor: getHomeAnchor(),
2764
- anchor: getAnchorRef(),
2951
+ anchor,
2765
2952
  setAnchor: selectMount,
2766
2953
  replaceMounts,
2767
2954
  mount,
@@ -3267,15 +3454,18 @@ function useChannelMessages(options) {
3267
3454
  void refreshQuery(qc, stableKey, { exact: true });
3268
3455
  }, 500);
3269
3456
  },
3270
- (data) => {
3271
- return (data.item_type === "message" || data.item_type === "card") && isChannelPath(data.path, channelPath);
3272
- }
3457
+ (data) => channelMessageChange(
3458
+ data,
3459
+ channelPath,
3460
+ parentMessageId,
3461
+ qc.getQueryData(stableKey)?.messages ?? []
3462
+ )
3273
3463
  );
3274
3464
  return () => {
3275
3465
  unsub();
3276
3466
  if (debounceRef.current) clearTimeout(debounceRef.current);
3277
3467
  };
3278
- }, [realtime, isEnabled, channelPath, stableKey, qc]);
3468
+ }, [realtime, isEnabled, channelPath, parentMessageId, stableKey, qc]);
3279
3469
  const response = query.data;
3280
3470
  const messages = response?.messages ?? [];
3281
3471
  const messageCards = response?.messageCards ?? /* @__PURE__ */ new Map();
@@ -3462,9 +3652,12 @@ function useInfiniteChannelMessages(options) {
3462
3652
  void tail();
3463
3653
  }, 500);
3464
3654
  },
3465
- (data) => {
3466
- return (data.item_type === "message" || data.item_type === "card") && isChannelPath(data.path, channelPath);
3467
- }
3655
+ (data) => channelMessageChange(
3656
+ data,
3657
+ channelPath,
3658
+ parentMessageId,
3659
+ qc.getQueryData(stableKey)?.pages.flatMap((page) => page.messages) ?? []
3660
+ )
3468
3661
  );
3469
3662
  return () => {
3470
3663
  disposed = true;
@@ -3532,7 +3725,6 @@ function useAllChannels(options = {}) {
3532
3725
  const scopeKey = useScopeKey();
3533
3726
  const { realtime = true, enabled: enabled2 = true } = options;
3534
3727
  const qc = reactQuery.useQueryClient();
3535
- const debounceRef = React.useRef(null);
3536
3728
  const stableKey = React.useMemo(() => vfsKeys.channels("all"), [scopeKey]);
3537
3729
  const query = reactQuery.useQuery({
3538
3730
  queryKey: stableKey,
@@ -3549,31 +3741,7 @@ function useAllChannels(options = {}) {
3549
3741
  });
3550
3742
  React.useEffect(() => {
3551
3743
  if (!realtime || !enabled2) return;
3552
- const unsub = subscribeToEvents(
3553
- "vfs_change",
3554
- () => {
3555
- if (debounceRef.current) clearTimeout(debounceRef.current);
3556
- debounceRef.current = setTimeout(() => {
3557
- qc.invalidateQueries({ queryKey: stableKey });
3558
- }, 1e3);
3559
- },
3560
- (data) => data.item_type === "channel" || data.item_type === "message"
3561
- );
3562
- const unsub2 = subscribeToEvents(
3563
- "ccm_change",
3564
- () => {
3565
- if (debounceRef.current) clearTimeout(debounceRef.current);
3566
- debounceRef.current = setTimeout(() => {
3567
- qc.invalidateQueries({ queryKey: stableKey });
3568
- }, 1e3);
3569
- },
3570
- (data) => data.item_type === "channel" || data.item_type === "message"
3571
- );
3572
- return () => {
3573
- unsub();
3574
- unsub2();
3575
- if (debounceRef.current) clearTimeout(debounceRef.current);
3576
- };
3744
+ return watchChannels(qc, stableKey, normalizeChannel);
3577
3745
  }, [realtime, enabled2, stableKey, qc]);
3578
3746
  const channels = query.data ?? [];
3579
3747
  return {
@@ -3632,7 +3800,7 @@ function useNotifications(options = {}) {
3632
3800
  qc.invalidateQueries({ queryKey: stableKey });
3633
3801
  }, 2e3);
3634
3802
  },
3635
- (data) => data.item_type === "notification" || data.item_type === "message"
3803
+ (data) => data.item_type === "notification" && inMountedReadScope(data)
3636
3804
  )
3637
3805
  );
3638
3806
  return () => {
@@ -3859,6 +4027,8 @@ function useEventStream(handlerOrEventType, handlerOrOptions, filter) {
3859
4027
  function useRealtimeQuery(options) {
3860
4028
  const { queryKey, queryFn, eventType, filter, enabled: enabled2 = true, debounceMs = 500 } = options;
3861
4029
  const qc = reactQuery.useQueryClient();
4030
+ const filterRef = React.useRef(filter);
4031
+ filterRef.current = filter;
3862
4032
  const debounceRef = React.useRef(null);
3863
4033
  const query = reactQuery.useQuery({
3864
4034
  queryKey,
@@ -3872,10 +4042,10 @@ function useRealtimeQuery(options) {
3872
4042
  () => {
3873
4043
  if (debounceRef.current) clearTimeout(debounceRef.current);
3874
4044
  debounceRef.current = setTimeout(() => {
3875
- qc.invalidateQueries({ queryKey });
4045
+ void refreshQuery(qc, queryKey, { exact: true });
3876
4046
  }, debounceMs);
3877
4047
  },
3878
- filter
4048
+ (data) => filterRef.current?.(data) ?? true
3879
4049
  );
3880
4050
  return () => {
3881
4051
  unsub();
@@ -5487,11 +5657,11 @@ function useTimelineHistory(channelPath, options) {
5487
5657
  qc.invalidateQueries({ queryKey });
5488
5658
  }, debounceMs);
5489
5659
  },
5490
- // Filter: only react to changes on or under the dataset path
5660
+ // Rows are written to the dataset item, not descendant items.
5491
5661
  (data) => {
5492
5662
  const eventPath = data?.path;
5493
5663
  if (!eventPath) return false;
5494
- return eventPath === datasetPath || eventPath.startsWith(datasetPath + "/");
5664
+ return eventPath === datasetPath && data.item_type === "dataset";
5495
5665
  }
5496
5666
  );
5497
5667
  return () => {