@ai-matrx/agents 0.59.264 → 0.59.266

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.
@@ -125,6 +125,16 @@ interface AgentSummary {
125
125
  * host can tell "the default" from a stored agent without parsing the id.
126
126
  */
127
127
  mandateKey?: string;
128
+ /**
129
+ * THE THIN ROW (FW-014). `true` when this row came from the picker's page
130
+ * read (`agent.agx_picker_page`), which carries only what a picker row
131
+ * renders: no description, model, offering, task, source or sharer. A
132
+ * consumer that needs those asks `catalog.ensureAgentRows([id])` (hover,
133
+ * open), which replaces it with the full row. Absent = a full row.
134
+ */
135
+ isThin?: boolean;
136
+ /** When the caller last used this agent (their latest conversation with it). Page reads only. */
137
+ lastUsedAt?: string | null;
128
138
  }
129
139
  type AgentSortOption = "updated-desc" | "created-desc" | "name-asc" | "name-desc" | "category-asc";
130
140
  /**
@@ -212,45 +222,6 @@ interface AgentConsumerState {
212
222
  /** The subset of consumer state a caller may patch (paging is derived). */
213
223
  type AgentConsumerPatch = Partial<Omit<AgentConsumerState, "listPage" | "sharedPage">>;
214
224
 
215
- /**
216
- * catalog/default-row-types.ts — @ai-matrx/agents/catalog
217
- *
218
- * Split out of `default-row.ts` so `global-state.ts` can name the cached
219
- * shape without importing the resolver (and its transport work) — the same
220
- * split-out reflex C8 asks for, at module scale.
221
- */
222
-
223
- /** A resolved default row, cached for first paint and compared for drift. */
224
- interface ResolvedDefaultRow {
225
- mandateKey: string;
226
- /** The `agent.definition` id the mandate resolved to. */
227
- holderId: string;
228
- /** The holder's REAL name, read from the catalog — never hardcoded. */
229
- holderName: string;
230
- holderDescription: string | null;
231
- provenance: string;
232
- /** Epoch ms of this resolution. */
233
- at: number;
234
- }
235
- /** What the picker renders and what it screams, together. */
236
- interface DefaultRowState {
237
- mandateKey: string;
238
- /** The row to render at the top of the list. `null` until resolved. */
239
- row: AgentSummary | null;
240
- /** The last live resolution, or the cached one before the live answer lands. */
241
- resolved: ResolvedDefaultRow | null;
242
- /** True while the resolution is in flight. */
243
- loading: boolean;
244
- /**
245
- * 🚨 THE DRIFT SCREAM. Set when the cached first-paint row and the LIVE
246
- * resolution disagree on holder id or holder name. Rendered as a persistent
247
- * in-picker banner AND sent to the `notifier` port — never console-only.
248
- */
249
- drift: string | null;
250
- /** Set when the mandate could not be resolved at all. Loud, with a remedy. */
251
- error: string | null;
252
- }
253
-
254
225
  /**
255
226
  * `@ai-matrx/agents/catalog` — THE PORTS.
256
227
  *
@@ -326,6 +297,101 @@ type AgentCatalogNavigate = (href: string) => void;
326
297
  */
327
298
  type AgentCatalogOpenPeek = (agent: AgentSummary) => void;
328
299
 
300
+ /**
301
+ * catalog/data.ts — @ai-matrx/agents/catalog
302
+ *
303
+ * Every database call the catalog makes, and nothing else. Five of them:
304
+ *
305
+ * 1. `agx_get_list_full()` — the complete catalog (owned + directly-shared
306
+ * + org-shared user agents, unioned with active builtins).
307
+ * 2. `agx_search(p_query, p_deep, p_limit, p_offset)` — tier-2 server search.
308
+ * 3. `agx_resolve_agent_address(p_ids)` — the single-agent name read, used
309
+ * when a mandate's Holder is not in the caller's own catalog.
310
+ * 4. The caller's stars arrive ON the rows: every read above computes
311
+ * `is_favorite` server-side from `platform.user_entity_state`
312
+ * (`platform.my_favorite`). There is no second star read (FW-014; the
313
+ * `ues_get_bulk` overlay over every id was retired 2026-10-23).
314
+ * 5. ONE write: the caller's star, `ues_set` through the same port. The
315
+ * retired `agent.definition.is_favorite` column is never read or written.
316
+ * 6. THE PINNED READ (2026-10-09; by-ids 2026-10-23): the caller's starred
317
+ * ids (`ues_list` through the favorites port), then
318
+ * `agent.agx_get_by_ids(p_ids)` for exactly those ids. A sidebar's Pinned
319
+ * section shows a handful of rows; it never pays for the whole catalog,
320
+ * in the browser or on the server, to find them.
321
+ * 7. THE PICKER PAGE (FW-014, 2026-10-23): `agent.agx_picker_page` — the
322
+ * menu opens with ~50 thin rows (favorites, recently used, name) and the
323
+ * tab counts; scrolling reads the next keyset page. `agent.agx_get_by_ids`
324
+ * fills in a full row on hover/open. The whole-catalog read above remains
325
+ * for surfaces that genuinely list everything, and for a picker whose
326
+ * person narrows by category or tag (the facets need every row).
327
+ *
328
+ * 🚨 THE LIST READ IS COMPLETE OR IT THROWS. PostgREST caps every response at
329
+ * `db-max-rows` (1000 on Matrx Main) and reports success — matrx-frontend's
330
+ * `fetchAgentsListFull` issued a bare `.rpc()` and would silently have
331
+ * returned 1000 rows of a larger catalog (the ~400-builtin truncation
332
+ * incident is the near-miss). Here the read goes through `readAllRows` with
333
+ * `{ count: "exact" }` and a stable `.order("id")`, so a truncation is a
334
+ * loud `IncompleteReadError` instead of a confidently short picker.
335
+ * The DB's own favourites-first/`updated_at` order is irrelevant: every row
336
+ * is re-sorted client-side by `sortFilteredAgents`.
337
+ */
338
+
339
+ /** Which slice of the caller's catalog a picker page reads. */
340
+ interface AgentPickerPageQuery {
341
+ /** `all_system` = the admin "All" tab that blends system agents in. */
342
+ tab: "mine" | "shared" | "all" | "all_system" | "system";
343
+ archive: "active" | "archived" | "both";
344
+ includeQuick: boolean;
345
+ }
346
+ /** The tab badges, counted server-side over the same reach as the rows. */
347
+ interface AgentPickerCounts {
348
+ mine: number;
349
+ shared: number;
350
+ all: number;
351
+ system: number;
352
+ /** Archived rows the active filter hides behind THIS tab (0 unless archive = active). */
353
+ archivedHidden: number;
354
+ }
355
+
356
+ /**
357
+ * catalog/default-row-types.ts — @ai-matrx/agents/catalog
358
+ *
359
+ * Split out of `default-row.ts` so `global-state.ts` can name the cached
360
+ * shape without importing the resolver (and its transport work) — the same
361
+ * split-out reflex C8 asks for, at module scale.
362
+ */
363
+
364
+ /** A resolved default row, cached for first paint and compared for drift. */
365
+ interface ResolvedDefaultRow {
366
+ mandateKey: string;
367
+ /** The `agent.definition` id the mandate resolved to. */
368
+ holderId: string;
369
+ /** The holder's REAL name, read from the catalog — never hardcoded. */
370
+ holderName: string;
371
+ holderDescription: string | null;
372
+ provenance: string;
373
+ /** Epoch ms of this resolution. */
374
+ at: number;
375
+ }
376
+ /** What the picker renders and what it screams, together. */
377
+ interface DefaultRowState {
378
+ mandateKey: string;
379
+ /** The row to render at the top of the list. `null` until resolved. */
380
+ row: AgentSummary | null;
381
+ /** The last live resolution, or the cached one before the live answer lands. */
382
+ resolved: ResolvedDefaultRow | null;
383
+ /** True while the resolution is in flight. */
384
+ loading: boolean;
385
+ /**
386
+ * 🚨 THE DRIFT SCREAM. Set when the cached first-paint row and the LIVE
387
+ * resolution disagree on holder id or holder name. Rendered as a persistent
388
+ * in-picker banner AND sent to the `notifier` port — never console-only.
389
+ */
390
+ drift: string | null;
391
+ /** Set when the mandate could not be resolved at all. Loud, with a remedy. */
392
+ error: string | null;
393
+ }
394
+
329
395
  /**
330
396
  * catalog/schema.ts — @ai-matrx/agents/catalog
331
397
  *
@@ -394,6 +460,20 @@ interface AssertAgentCatalogSchemaOptions {
394
460
  */
395
461
 
396
462
  type AgentCatalogStatus = "idle" | "loading" | "succeeded" | "failed";
463
+ /** One picker slice (tab × archive × quick) read a page at a time (FW-014). */
464
+ interface AgentPickerPageState {
465
+ /** Row ids in server order across every page read so far. */
466
+ ids: string[];
467
+ /** Keyset position of the next page; `null` once the last page is in. */
468
+ nextCursor: unknown;
469
+ done: boolean;
470
+ status: AgentCatalogStatus;
471
+ /** True while a next page (not the first) is in flight. */
472
+ loadingMore: boolean;
473
+ error: string | null;
474
+ counts: AgentPickerCounts | null;
475
+ fetchedAt: number;
476
+ }
397
477
  interface AgentCatalogState {
398
478
  /** Every row, one stable array reference per change (memoization depends on it). */
399
479
  rows: AgentSummary[];
@@ -409,6 +489,8 @@ interface AgentCatalogState {
409
489
  * or by a complete catalog load, whichever came first.
410
490
  */
411
491
  favoriteRowsStatus: AgentCatalogStatus;
492
+ /** The picker's paged slices, keyed by `agentPickerPageKey` (FW-014). */
493
+ pickerPages: Record<string, AgentPickerPageState>;
412
494
  }
413
495
  interface AgentCatalog {
414
496
  readonly catalogId: string;
@@ -417,6 +499,8 @@ interface AgentCatalog {
417
499
  readonly notifier: AgentCatalogNotifier | undefined;
418
500
  /** True when this catalog was given a transport and can resolve mandates. */
419
501
  readonly canResolveMandates: boolean;
502
+ /** `AgentCatalogConfig.pickerReads`, resolved (default `"page"`). */
503
+ readonly pickerReads: "page" | "catalog";
420
504
  /**
421
505
  * The consumer state a brand-new consumer starts from, AFTER the host's
422
506
  * `defaults` knob is applied. Read it instead of
@@ -455,6 +539,28 @@ interface AgentCatalog {
455
539
  * makes no request. Rows that do not parse are skipped.
456
540
  */
457
541
  seedFavoriteRows(rawRows: unknown[]): void;
542
+ /**
543
+ * THE PICKER'S OPEN (FW-014): the first page (~50 thin rows) of one slice,
544
+ * plus its tab counts — never the whole catalog. Fresh for the list's TTL;
545
+ * deduped in flight; safe on every open.
546
+ */
547
+ ensurePickerPage(query: AgentPickerPageQuery): Promise<void>;
548
+ /** The next page of a slice (scroll). No-op when the slice is done or already loading. */
549
+ loadMorePickerPage(query: AgentPickerPageQuery): Promise<void>;
550
+ /**
551
+ * FULL rows for these ids (`agent.agx_get_by_ids`), merged into the registry:
552
+ * a hover card's details, the pinned current agent, a name for an id that is
553
+ * not on page 1. Ids already held as full rows are not asked again; deduped
554
+ * in flight.
555
+ */
556
+ ensureAgentRows(agentIds: readonly string[]): Promise<void>;
557
+ /**
558
+ * The caller's live agents carrying any of these tags, merged into the
559
+ * registry (`agent.agx_picker_tagged`) — the composer's quick agents and
560
+ * presets, never the catalog. Fresh for the list's TTL; deduped in flight;
561
+ * answered at once when the whole catalog is already fresh.
562
+ */
563
+ ensureTaggedRows(tags: readonly string[]): Promise<void>;
458
564
  /** True when the last load is inside the 15-minute TTL. */
459
565
  isFresh(): boolean;
460
566
  /** True when the last load is older than 4 hours (tab-restore threshold). */
@@ -563,10 +669,11 @@ declare function AgentCatalogProvider({ catalog, children, ...ports }: AgentCata
563
669
  declare function lookUpAgentOnce(catalog: AgentCatalog, agentId: string): Promise<void>;
564
670
  /**
565
671
  * ONE agent's catalog row — name, description, category, tags, star, access — read straight from
566
- * the catalog store (the ONE holder of agent identity; no host mirror). When the row is missing the
567
- * catalog is loaded (its own TTL and in-flight dedupe make this safe on every mount); an id the
568
- * list does not carry is looked up by server search, which merges it into the catalog — once per
569
- * id per catalog (the Board's remount law: a woken or remounted tile asks nothing again).
672
+ * the catalog store (the ONE holder of agent identity; no host mirror). When the row is missing (or
673
+ * only a thin picker row) that ONE row is read by id (`ensureAgentRows`, deduped) — never the whole
674
+ * catalog (FW-014); an id that read does not return is looked up by server search, which merges it
675
+ * into the catalog — once per id per catalog (the Board's remount law: a woken or remounted tile
676
+ * asks nothing again).
570
677
  *
571
678
  * Takes the catalog explicitly so a host without `AgentCatalogProvider` (chat, workers) binds it.
572
679
  */
@@ -661,6 +768,14 @@ declare function useAgentListCore({ consumerId, onSelect, navigateTo, activeAgen
661
768
  hoveredAgent: AgentSummary | null;
662
769
  setHoveredAgent: react.Dispatch<react.SetStateAction<AgentSummary | null>>;
663
770
  ensureLoaded: () => void;
771
+ /** FW-014: the list reads a page at a time; true while the slice has more. */
772
+ paged: boolean;
773
+ hasMore: boolean;
774
+ loadingMore: boolean;
775
+ loadMore: () => void;
776
+ /** The tab's total (server count when paged) — the footer's number. */
777
+ totalCount: number;
778
+ ensureFullCatalog: () => void;
664
779
  handleSelectAgent: (agent: AgentSummary) => void;
665
780
  handleAgentHover: (agent: AgentSummary, panelOpen: boolean) => void;
666
781
  handleAgentHoverEnd: (agent: AgentSummary, onClear: () => void) => void;
@@ -1052,8 +1167,19 @@ interface AgentListContentProps {
1052
1167
  agentFilterLabel?: string | null | undefined;
1053
1168
  /** "Show all" on the host-filter chip. */
1054
1169
  onClearAgentFilter?: (() => void) | undefined;
1170
+ /**
1171
+ * FW-014 paging: true while the list has more rows on the server. The list
1172
+ * asks `onReachEnd` for the next page when the scroll nears the end (or when
1173
+ * the rows do not fill the viewport yet).
1174
+ */
1175
+ hasMore?: boolean | undefined;
1176
+ onReachEnd?: (() => void) | undefined;
1177
+ /** True while the next page is in flight — a spinner row closes the list. */
1178
+ loadingMore?: boolean | undefined;
1179
+ /** The tab's total for the footer (server count when paged); defaults to the rendered rows. */
1180
+ totalCount?: number | undefined;
1055
1181
  }
1056
- declare function AgentListContent({ agents, isLoading, consumer, activeAgentId, allCategories, allTags, inputRef, onSelectAgent, resolveAgentHref, onReset, activeFilterCount, isMobile, hoveredAgent, onAgentHover, onAgentHoverEnd, onDetailPress, onFilterChipClick, rightPanel, tabCounts, visibleTabs, systemTabLabel, pinnedAgent, defaultRow, listOpen, archivedHiddenCount, agentFilterLabel, onClearAgentFilter, }: AgentListContentProps): react.JSX.Element;
1182
+ declare function AgentListContent({ agents, isLoading, consumer, activeAgentId, allCategories, allTags, inputRef, onSelectAgent, resolveAgentHref, onReset, activeFilterCount, isMobile, hoveredAgent, onAgentHover, onAgentHoverEnd, onDetailPress, onFilterChipClick, rightPanel, tabCounts, visibleTabs, systemTabLabel, pinnedAgent, defaultRow, listOpen, archivedHiddenCount, agentFilterLabel, onClearAgentFilter, hasMore, onReachEnd, loadingMore, totalCount, }: AgentListContentProps): react.JSX.Element;
1057
1183
 
1058
1184
  interface AgentFilterBarProps {
1059
1185
  consumer: UseAgentConsumerReturn;
@@ -139,12 +139,14 @@ function useAgentSummary(catalog, agentId) {
139
139
  () => catalog && agentId ? catalog.getState().byId[agentId] : void 0,
140
140
  () => catalog && agentId ? catalog.getState().byId[agentId] : void 0
141
141
  );
142
- const present = row !== void 0;
142
+ const present = row !== void 0 && !row.isThin;
143
143
  useEffect(() => {
144
144
  if (!catalog || !agentId || present) return;
145
145
  let cancelled = false;
146
- void catalog.ensureLoaded().then(() => {
147
- if (cancelled || catalog.getState().byId[agentId]) return;
146
+ void catalog.ensureAgentRows([agentId]).catch(() => void 0).then(() => {
147
+ const held = catalog.getState().byId[agentId];
148
+ if (cancelled || held && !held.isThin) return;
149
+ if (held) return;
148
150
  return lookUpAgentOnce(catalog, agentId);
149
151
  }).catch(() => void 0);
150
152
  return () => {
@@ -729,6 +731,14 @@ function agentListFooterLabel(args) {
729
731
  return archivedHiddenCount > 0 ? `${base} \xB7 ${archivedHiddenCount} archived hidden` : base;
730
732
  }
731
733
 
734
+ // catalog/store.ts
735
+ import { createAssociationsStore } from "@ai-matrx/associations/core";
736
+ var LIST_TTL_MS = 15 * 60 * 1e3;
737
+ var LIST_STALE_MS = 4 * 60 * 60 * 1e3;
738
+ function agentPickerPageKey(query) {
739
+ return `${query.tab}|${query.archive}|${query.includeQuick ? "q" : "-"}`;
740
+ }
741
+
732
742
  // catalog/shared/useAgentListCore-react.ts
733
743
  function useAgentListCore({
734
744
  consumerId,
@@ -758,19 +768,60 @@ function useAgentListCore({
758
768
  const allRows = useAgentCatalogRows();
759
769
  const rows = useMemo3(() => includeQuickAgents ? allRows : withoutQuickAgents(allRows), [allRows, includeQuickAgents]);
760
770
  const userId = catalog.getUserIdOrNull();
761
- const tabCountsForFilter = selectAgentTabCounts(rows, consumer.state.archFilter);
771
+ const status = useAgentCatalogState(selectStatus);
772
+ const [setAsideLabel, setSetAsideLabel] = useState2(null);
773
+ const activeAgentFilter = agentFilter && setAsideLabel !== agentFilter.label ? agentFilter : null;
774
+ const [facetsWanted, setFacetsWanted] = useState2(false);
775
+ const needsFullCatalog = facetsWanted || activeAgentFilter !== null || consumer.state.includedCats.length > 0 || consumer.state.includedTags.length > 0 || consumer.state.favFilter !== "all" || consumer.state.accessFilter !== "any" || !consumer.state.favoritesFirst || consumer.state.sortBy !== "updated-desc";
776
+ const fullCatalogReady = status === "succeeded" && catalog.isFresh();
777
+ const pageQuery = {
778
+ tab: includeSystemInAll && consumer.state.tab === "all" ? "all_system" : consumer.state.tab,
779
+ archive: consumer.state.archFilter,
780
+ includeQuick: includeQuickAgents
781
+ };
782
+ const pageKey = agentPickerPageKey(pageQuery);
783
+ const selectPage = useCallback3((state) => state.pickerPages[pageKey], [pageKey]);
784
+ const pageState = useAgentCatalogState(selectPage);
785
+ const pageUnsupported = pageState?.error === "unsupported";
786
+ const paged = catalog.pickerReads === "page" && !needsFullCatalog && !fullCatalogReady && !pageUnsupported;
787
+ const selectById = useCallback3((state) => state.byId, []);
788
+ const byId = useAgentCatalogState(selectById);
789
+ const pageRows = useMemo3(() => {
790
+ if (!pageState) return [];
791
+ const out = [];
792
+ for (const id of pageState.ids) {
793
+ const row = byId[id];
794
+ if (row) out.push(row);
795
+ }
796
+ return out;
797
+ }, [pageState, byId]);
798
+ const tabCountsForFilter = paged ? {
799
+ mine: pageState?.counts?.mine ?? 0,
800
+ shared: pageState?.counts?.shared ?? 0,
801
+ all: (pageState?.counts?.all ?? 0) + (includeSystemInAll ? pageState?.counts?.system ?? 0 : 0),
802
+ system: pageState?.counts?.system ?? 0
803
+ } : selectAgentTabCounts(rows, consumer.state.archFilter);
762
804
  const ownedCount = tabCountsForFilter.mine;
763
805
  const selectFiltered = useMemo3(
764
806
  () => makeSelectFilteredAgents(includeSystemInAll),
765
807
  [includeSystemInAll]
766
808
  );
767
- const filteredAgents = selectFiltered(rows, consumer.state);
809
+ const searchPool = useMemo3(() => {
810
+ if (!paged || !consumer.state.searchTerm) return null;
811
+ const pool = /* @__PURE__ */ new Map();
812
+ for (const row of pageRows) pool.set(row.id, row);
813
+ for (const id of consumer.state.serverMatchedIds) {
814
+ const row = byId[id];
815
+ if (row) pool.set(id, row);
816
+ }
817
+ const list = [...pool.values()];
818
+ return includeQuickAgents ? list : withoutQuickAgents(list);
819
+ }, [paged, consumer.state.searchTerm, consumer.state.serverMatchedIds, pageRows, byId, includeQuickAgents]);
820
+ const filteredAgents = paged ? searchPool ? selectFiltered(searchPool, consumer.state) : pageRows : selectFiltered(rows, consumer.state);
768
821
  const excludedAgentIds = useMemo3(
769
822
  () => new Set(excludeAgentIds),
770
823
  [excludeAgentIds]
771
824
  );
772
- const [setAsideLabel, setSetAsideLabel] = useState2(null);
773
- const activeAgentFilter = agentFilter && setAsideLabel !== agentFilter.label ? agentFilter : null;
774
825
  const filterTest = activeAgentFilter?.test;
775
826
  const applyExcludes = useCallback3(
776
827
  (list) => excludedAgentIds.size === 0 && !filterTest ? list : list.filter(
@@ -793,23 +844,44 @@ function useAgentListCore({
793
844
  () => ({ ...consumer.state, archFilter: "archived" }),
794
845
  [consumer.state]
795
846
  );
796
- const archivedHiddenCount = consumer.archFilter === "active" ? applyExcludes(selectArchivedProbe(rows, archiveProbeState)).length : 0;
797
- const status = useAgentCatalogState(selectStatus);
847
+ const archivedHiddenCount = consumer.archFilter !== "active" || paged && consumer.state.searchTerm ? 0 : paged ? pageState?.counts?.archivedHidden ?? 0 : applyExcludes(selectArchivedProbe(rows, archiveProbeState)).length;
798
848
  const activeAgentId = activeAgentIdOverride ?? null;
799
- const pinnedAgent = activeAgentId ? catalog.getAgent(activeAgentId) : void 0;
849
+ const pinnedAgent = activeAgentId ? byId[activeAgentId] : void 0;
800
850
  const onSystemTab = consumer.tab === "system";
801
851
  const allCategories = onSystemTab ? selectAllSystemAgentCategories(rows) : selectAllAgentCategories(rows);
802
852
  const allTags = onSystemTab ? selectAllSystemAgentTags(rows) : selectAllAgentTags(rows);
803
853
  const tabCounts = tabCountsForFilter;
804
- const isLoading = status === "loading";
805
- const agentsLoaded = status === "succeeded" || status === "failed";
854
+ const [engaged, setEngaged] = useState2(false);
855
+ const pageStatus = pageState?.status ?? "idle";
856
+ const isLoading = paged ? (pageStatus === "loading" || pageStatus === "idle") && pageRows.length === 0 && engaged : status === "loading";
857
+ const agentsLoaded = paged ? pageStatus === "succeeded" || pageStatus === "failed" : status === "succeeded" || status === "failed";
806
858
  const ensureLoaded = useCallback3(() => {
807
859
  if (!hasFetchedRef.current) {
808
860
  hasFetchedRef.current = true;
809
- void catalog.ensureLoaded().catch(() => {
810
- });
861
+ setEngaged(true);
811
862
  }
812
- }, [catalog]);
863
+ }, []);
864
+ const pageQueryRef = useRef2(pageQuery);
865
+ pageQueryRef.current = pageQuery;
866
+ useEffect3(() => {
867
+ if (!engaged) return;
868
+ if (paged) void catalog.ensurePickerPage(pageQueryRef.current).catch(() => void 0);
869
+ else void catalog.ensureLoaded().catch(() => void 0);
870
+ }, [engaged, paged, pageKey, catalog]);
871
+ const loadMore = useCallback3(() => {
872
+ if (!paged) return;
873
+ void catalog.loadMorePickerPage(pageQueryRef.current).catch(() => void 0);
874
+ }, [paged, catalog]);
875
+ const ensureFullCatalog = useCallback3(() => {
876
+ setFacetsWanted(true);
877
+ }, []);
878
+ const hasMore = paged && pageStatus === "succeeded" && pageState?.done === false;
879
+ const loadingMore = paged && pageState?.loadingMore === true;
880
+ const pinnedMissing = Boolean(activeAgentId) && pinnedAgent === void 0;
881
+ useEffect3(() => {
882
+ if (!engaged || !pinnedMissing || !activeAgentId) return;
883
+ void catalog.ensureAgentRows([activeAgentId]).catch(() => void 0);
884
+ }, [engaged, pinnedMissing, activeAgentId, catalog]);
813
885
  const selectDefaultRow = useCallback3(
814
886
  (state) => defaultMandateKey ? state.defaultRows[defaultMandateKey] : void 0,
815
887
  [defaultMandateKey]
@@ -886,13 +958,14 @@ function useAgentListCore({
886
958
  const handleAgentHover = useCallback3(
887
959
  (agent, panelOpen) => {
888
960
  if (panelOpen) return;
961
+ if (agent.isThin) void catalog.ensureAgentRows([agent.id]).catch(() => void 0);
889
962
  if (hoverLeaveTimerRef.current) {
890
963
  clearTimeout(hoverLeaveTimerRef.current);
891
964
  hoverLeaveTimerRef.current = null;
892
965
  }
893
966
  setHoveredAgent(agent);
894
967
  },
895
- []
968
+ [catalog]
896
969
  );
897
970
  const handleAgentHoverEnd = useCallback3(
898
971
  (agent, onClear) => {
@@ -923,8 +996,10 @@ function useAgentListCore({
923
996
  },
924
997
  []
925
998
  );
999
+ const tabTotal = consumer.state.tab === "mine" ? tabCountsForFilter.mine : consumer.state.tab === "shared" ? tabCountsForFilter.shared : consumer.state.tab === "system" ? tabCountsForFilter.system : tabCountsForFilter.all;
1000
+ const totalCount = paged && !consumer.state.searchTerm && excludedAgentIds.size === 0 ? Math.max(tabTotal, agents.length) : agents.length;
926
1001
  const footerLabel = agentListFooterLabel({
927
- count: agents.length,
1002
+ count: totalCount,
928
1003
  archFilter: consumer.archFilter,
929
1004
  archivedHiddenCount
930
1005
  });
@@ -948,6 +1023,14 @@ function useAgentListCore({
948
1023
  hoveredAgent,
949
1024
  setHoveredAgent,
950
1025
  ensureLoaded,
1026
+ /** FW-014: the list reads a page at a time; true while the slice has more. */
1027
+ paged,
1028
+ hasMore,
1029
+ loadingMore,
1030
+ loadMore,
1031
+ /** The tab's total (server count when paged) — the footer's number. */
1032
+ totalCount,
1033
+ ensureFullCatalog,
951
1034
  handleSelectAgent,
952
1035
  handleAgentHover,
953
1036
  handleAgentHoverEnd,
@@ -1442,7 +1525,8 @@ function AgentDetailCard({ agent, onSelect, duplicate, onHoldChange }) {
1442
1525
  const select = useCallback4((state) => state.byId[agent.id], [agent.id]);
1443
1526
  const row = useAgentCatalogState(select);
1444
1527
  const on = row?.isFavorite ?? agent.isFavorite ?? false;
1445
- const record = agentPeekRecord(agent, {
1528
+ const live = row && !row.isThin ? row : agent;
1529
+ const record = agentPeekRecord(live, {
1446
1530
  favorite: {
1447
1531
  on,
1448
1532
  onToggle: () => {
@@ -1739,6 +1823,7 @@ var AGENT_ROW_HEIGHT = 36;
1739
1823
  var UNMEASURED_VIEWPORT = 480;
1740
1824
  var ROW_OVERSCAN = 8;
1741
1825
  var WINDOW_ABOVE = 50;
1826
+ var REACH_END_ROWS = 10;
1742
1827
  function AgentListContent({
1743
1828
  agents,
1744
1829
  isLoading,
@@ -1766,7 +1851,11 @@ function AgentListContent({
1766
1851
  listOpen = false,
1767
1852
  archivedHiddenCount = 0,
1768
1853
  agentFilterLabel = null,
1769
- onClearAgentFilter
1854
+ onClearAgentFilter,
1855
+ hasMore = false,
1856
+ onReachEnd,
1857
+ loadingMore = false,
1858
+ totalCount
1770
1859
  }) {
1771
1860
  const catalog = useAgentCatalog();
1772
1861
  const listScrollRef = useRef3(null);
@@ -1815,6 +1904,12 @@ function AgentListContent({
1815
1904
  scroller.scrollTo({ top });
1816
1905
  setScrollTop(top);
1817
1906
  }, [listOpen, pinnedAgent?.id, activeAgentId, isLoading]);
1907
+ useEffect4(() => {
1908
+ if (!hasMore || !onReachEnd || isLoading) return;
1909
+ const view = viewport > 0 ? viewport : UNMEASURED_VIEWPORT;
1910
+ const listBottom = listStart + agents.length * AGENT_ROW_HEIGHT;
1911
+ if (scrollTop + view >= listBottom - REACH_END_ROWS * AGENT_ROW_HEIGHT) onReachEnd();
1912
+ }, [hasMore, onReachEnd, isLoading, scrollTop, viewport, listStart, agents.length]);
1818
1913
  const win = agents.length <= WINDOW_ABOVE ? { start: 0, end: agents.length, padStart: 0, padEnd: 0 } : computeWindow({
1819
1914
  count: agents.length,
1820
1915
  itemSize: AGENT_ROW_HEIGHT,
@@ -2071,6 +2166,14 @@ function AgentListContent({
2071
2166
  ]
2072
2167
  }
2073
2168
  ),
2169
+ loadingMore && /* @__PURE__ */ jsx10(
2170
+ "div",
2171
+ {
2172
+ "data-testid": "agent-list-loading-more",
2173
+ className: "flex items-center justify-center py-2 text-muted-foreground",
2174
+ children: /* @__PURE__ */ jsx10(LoaderIcon, { className: "w-3.5 h-3.5 matrx-agent-spin" })
2175
+ }
2176
+ ),
2074
2177
  agents.length === 0 && (pinnedAgent || defaultAgent) && /* @__PURE__ */ jsx10("div", { className: "flex flex-col items-center py-4 text-muted-foreground", children: /* @__PURE__ */ jsx10("span", { className: "text-xs", children: emptyLabel }) })
2075
2178
  ] })
2076
2179
  }
@@ -2083,7 +2186,7 @@ function AgentListContent({
2083
2186
  "data-testid": "agent-list-footer",
2084
2187
  className: "text-[10px] text-muted-foreground tabular-nums",
2085
2188
  children: agentListFooterLabel({
2086
- count: agents.length,
2189
+ count: totalCount ?? agents.length,
2087
2190
  archFilter: consumer.archFilter,
2088
2191
  archivedHiddenCount
2089
2192
  })
@@ -2317,6 +2420,11 @@ function AgentListPanelBody({
2317
2420
  hoveredAgent,
2318
2421
  setHoveredAgent,
2319
2422
  ensureLoaded,
2423
+ hasMore,
2424
+ loadMore,
2425
+ loadingMore,
2426
+ totalCount,
2427
+ ensureFullCatalog,
2320
2428
  handleSelectAgent: coreSelectAgent,
2321
2429
  handleAgentHover: coreAgentHover,
2322
2430
  handleAgentHoverEnd: coreAgentHoverEnd,
@@ -2344,6 +2452,7 @@ function AgentListPanelBody({
2344
2452
  onAfterSelect?.(agent);
2345
2453
  };
2346
2454
  const handleFilterChipClick = (panel) => {
2455
+ if (panel !== "sort") ensureFullCatalog();
2347
2456
  if (isMobile) {
2348
2457
  setMobileSubView(panel);
2349
2458
  } else {
@@ -2416,6 +2525,10 @@ function AgentListPanelBody({
2416
2525
  defaultRow,
2417
2526
  agentFilterLabel,
2418
2527
  onClearAgentFilter: clearAgentFilter,
2528
+ hasMore,
2529
+ onReachEnd: loadMore,
2530
+ loadingMore,
2531
+ totalCount,
2419
2532
  listOpen: true
2420
2533
  }
2421
2534
  );
@@ -2755,6 +2868,11 @@ function AgentListInlinePicker({
2755
2868
  agentFilterLabel,
2756
2869
  clearAgentFilter,
2757
2870
  ensureLoaded,
2871
+ hasMore,
2872
+ loadMore,
2873
+ loadingMore,
2874
+ totalCount,
2875
+ ensureFullCatalog,
2758
2876
  handleSelectAgent: coreSelectAgent,
2759
2877
  handleAgentHover,
2760
2878
  handleAgentHoverEnd
@@ -2784,6 +2902,7 @@ function AgentListInlinePicker({
2784
2902
  setMobileSubView(null);
2785
2903
  };
2786
2904
  const handleFilterChipClick = (panel) => {
2905
+ if (panel !== "sort") ensureFullCatalog();
2787
2906
  setMobileSubView(panel);
2788
2907
  };
2789
2908
  const isMobile = true;
@@ -2862,6 +2981,10 @@ function AgentListInlinePicker({
2862
2981
  defaultRow,
2863
2982
  agentFilterLabel,
2864
2983
  onClearAgentFilter: clearAgentFilter,
2984
+ hasMore,
2985
+ onReachEnd: loadMore,
2986
+ loadingMore,
2987
+ totalCount,
2865
2988
  listOpen: true
2866
2989
  }
2867
2990
  ) });
@@ -406,7 +406,6 @@ var PROVISION_OFFER_KINDS = {
406
406
  "social.x_search": "social.x_search.offer",
407
407
  "spaces.ask_page": "spaces.ask_page.offer",
408
408
  "spaces.ask_workspace": "spaces.ask_workspace.offer",
409
- "spaces.autofill": "spaces.autofill.offer",
410
409
  "spaces.build": "spaces.build.offer",
411
410
  "spaces.design_database": "spaces.design_database.offer",
412
411
  "spaces.move_in": "spaces.move_in.offer",