@ai-matrx/agents 0.59.264 → 0.59.265

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.
@@ -198,12 +198,14 @@ function useAgentSummary(catalog, agentId) {
198
198
  () => catalog && agentId ? catalog.getState().byId[agentId] : void 0,
199
199
  () => catalog && agentId ? catalog.getState().byId[agentId] : void 0
200
200
  );
201
- const present = row !== void 0;
201
+ const present = row !== void 0 && !row.isThin;
202
202
  (0, import_react3.useEffect)(() => {
203
203
  if (!catalog || !agentId || present) return;
204
204
  let cancelled = false;
205
- void catalog.ensureLoaded().then(() => {
206
- if (cancelled || catalog.getState().byId[agentId]) return;
205
+ void catalog.ensureAgentRows([agentId]).catch(() => catalog.ensureLoaded()).then(() => {
206
+ const held = catalog.getState().byId[agentId];
207
+ if (cancelled || held && !held.isThin) return;
208
+ if (held) return;
207
209
  return lookUpAgentOnce(catalog, agentId);
208
210
  }).catch(() => void 0);
209
211
  return () => {
@@ -788,6 +790,14 @@ function agentListFooterLabel(args) {
788
790
  return archivedHiddenCount > 0 ? `${base} \xB7 ${archivedHiddenCount} archived hidden` : base;
789
791
  }
790
792
 
793
+ // catalog/store.ts
794
+ var import_core = require("@ai-matrx/associations/core");
795
+ var LIST_TTL_MS = 15 * 60 * 1e3;
796
+ var LIST_STALE_MS = 4 * 60 * 60 * 1e3;
797
+ function agentPickerPageKey(query) {
798
+ return `${query.tab}|${query.archive}|${query.includeQuick ? "q" : "-"}`;
799
+ }
800
+
791
801
  // catalog/shared/useAgentListCore-react.ts
792
802
  function useAgentListCore({
793
803
  consumerId,
@@ -817,19 +827,60 @@ function useAgentListCore({
817
827
  const allRows = useAgentCatalogRows();
818
828
  const rows = (0, import_react5.useMemo)(() => includeQuickAgents ? allRows : withoutQuickAgents(allRows), [allRows, includeQuickAgents]);
819
829
  const userId = catalog.getUserIdOrNull();
820
- const tabCountsForFilter = selectAgentTabCounts(rows, consumer.state.archFilter);
830
+ const status = useAgentCatalogState(selectStatus);
831
+ const [setAsideLabel, setSetAsideLabel] = (0, import_react5.useState)(null);
832
+ const activeAgentFilter = agentFilter && setAsideLabel !== agentFilter.label ? agentFilter : null;
833
+ const [facetsWanted, setFacetsWanted] = (0, import_react5.useState)(false);
834
+ 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";
835
+ const fullCatalogReady = status === "succeeded" && catalog.isFresh();
836
+ const pageQuery = {
837
+ tab: includeSystemInAll && consumer.state.tab === "all" ? "all_system" : consumer.state.tab,
838
+ archive: consumer.state.archFilter,
839
+ includeQuick: includeQuickAgents
840
+ };
841
+ const pageKey = agentPickerPageKey(pageQuery);
842
+ const selectPage = (0, import_react5.useCallback)((state) => state.pickerPages[pageKey], [pageKey]);
843
+ const pageState = useAgentCatalogState(selectPage);
844
+ const pageUnsupported = pageState?.error === "unsupported";
845
+ const paged = catalog.pickerReads === "page" && !needsFullCatalog && !fullCatalogReady && !pageUnsupported;
846
+ const selectById = (0, import_react5.useCallback)((state) => state.byId, []);
847
+ const byId = useAgentCatalogState(selectById);
848
+ const pageRows = (0, import_react5.useMemo)(() => {
849
+ if (!pageState) return [];
850
+ const out = [];
851
+ for (const id of pageState.ids) {
852
+ const row = byId[id];
853
+ if (row) out.push(row);
854
+ }
855
+ return out;
856
+ }, [pageState, byId]);
857
+ const tabCountsForFilter = paged ? {
858
+ mine: pageState?.counts?.mine ?? 0,
859
+ shared: pageState?.counts?.shared ?? 0,
860
+ all: (pageState?.counts?.all ?? 0) + (includeSystemInAll ? pageState?.counts?.system ?? 0 : 0),
861
+ system: pageState?.counts?.system ?? 0
862
+ } : selectAgentTabCounts(rows, consumer.state.archFilter);
821
863
  const ownedCount = tabCountsForFilter.mine;
822
864
  const selectFiltered = (0, import_react5.useMemo)(
823
865
  () => makeSelectFilteredAgents(includeSystemInAll),
824
866
  [includeSystemInAll]
825
867
  );
826
- const filteredAgents = selectFiltered(rows, consumer.state);
868
+ const searchPool = (0, import_react5.useMemo)(() => {
869
+ if (!paged || !consumer.state.searchTerm) return null;
870
+ const pool = /* @__PURE__ */ new Map();
871
+ for (const row of pageRows) pool.set(row.id, row);
872
+ for (const id of consumer.state.serverMatchedIds) {
873
+ const row = byId[id];
874
+ if (row) pool.set(id, row);
875
+ }
876
+ const list = [...pool.values()];
877
+ return includeQuickAgents ? list : withoutQuickAgents(list);
878
+ }, [paged, consumer.state.searchTerm, consumer.state.serverMatchedIds, pageRows, byId, includeQuickAgents]);
879
+ const filteredAgents = paged ? searchPool ? selectFiltered(searchPool, consumer.state) : pageRows : selectFiltered(rows, consumer.state);
827
880
  const excludedAgentIds = (0, import_react5.useMemo)(
828
881
  () => new Set(excludeAgentIds),
829
882
  [excludeAgentIds]
830
883
  );
831
- const [setAsideLabel, setSetAsideLabel] = (0, import_react5.useState)(null);
832
- const activeAgentFilter = agentFilter && setAsideLabel !== agentFilter.label ? agentFilter : null;
833
884
  const filterTest = activeAgentFilter?.test;
834
885
  const applyExcludes = (0, import_react5.useCallback)(
835
886
  (list) => excludedAgentIds.size === 0 && !filterTest ? list : list.filter(
@@ -852,23 +903,44 @@ function useAgentListCore({
852
903
  () => ({ ...consumer.state, archFilter: "archived" }),
853
904
  [consumer.state]
854
905
  );
855
- const archivedHiddenCount = consumer.archFilter === "active" ? applyExcludes(selectArchivedProbe(rows, archiveProbeState)).length : 0;
856
- const status = useAgentCatalogState(selectStatus);
906
+ const archivedHiddenCount = consumer.archFilter !== "active" || paged && consumer.state.searchTerm ? 0 : paged ? pageState?.counts?.archivedHidden ?? 0 : applyExcludes(selectArchivedProbe(rows, archiveProbeState)).length;
857
907
  const activeAgentId = activeAgentIdOverride ?? null;
858
- const pinnedAgent = activeAgentId ? catalog.getAgent(activeAgentId) : void 0;
908
+ const pinnedAgent = activeAgentId ? byId[activeAgentId] : void 0;
859
909
  const onSystemTab = consumer.tab === "system";
860
910
  const allCategories = onSystemTab ? selectAllSystemAgentCategories(rows) : selectAllAgentCategories(rows);
861
911
  const allTags = onSystemTab ? selectAllSystemAgentTags(rows) : selectAllAgentTags(rows);
862
912
  const tabCounts = tabCountsForFilter;
863
- const isLoading = status === "loading";
864
- const agentsLoaded = status === "succeeded" || status === "failed";
913
+ const [engaged, setEngaged] = (0, import_react5.useState)(false);
914
+ const pageStatus = pageState?.status ?? "idle";
915
+ const isLoading = paged ? (pageStatus === "loading" || pageStatus === "idle") && pageRows.length === 0 && engaged : status === "loading";
916
+ const agentsLoaded = paged ? pageStatus === "succeeded" || pageStatus === "failed" : status === "succeeded" || status === "failed";
865
917
  const ensureLoaded = (0, import_react5.useCallback)(() => {
866
918
  if (!hasFetchedRef.current) {
867
919
  hasFetchedRef.current = true;
868
- void catalog.ensureLoaded().catch(() => {
869
- });
920
+ setEngaged(true);
870
921
  }
871
- }, [catalog]);
922
+ }, []);
923
+ const pageQueryRef = (0, import_react5.useRef)(pageQuery);
924
+ pageQueryRef.current = pageQuery;
925
+ (0, import_react5.useEffect)(() => {
926
+ if (!engaged) return;
927
+ if (paged) void catalog.ensurePickerPage(pageQueryRef.current).catch(() => void 0);
928
+ else void catalog.ensureLoaded().catch(() => void 0);
929
+ }, [engaged, paged, pageKey, catalog]);
930
+ const loadMore = (0, import_react5.useCallback)(() => {
931
+ if (!paged) return;
932
+ void catalog.loadMorePickerPage(pageQueryRef.current).catch(() => void 0);
933
+ }, [paged, catalog]);
934
+ const ensureFullCatalog = (0, import_react5.useCallback)(() => {
935
+ setFacetsWanted(true);
936
+ }, []);
937
+ const hasMore = paged && pageStatus === "succeeded" && pageState?.done === false;
938
+ const loadingMore = paged && pageState?.loadingMore === true;
939
+ const pinnedMissing = Boolean(activeAgentId) && pinnedAgent === void 0;
940
+ (0, import_react5.useEffect)(() => {
941
+ if (!engaged || !pinnedMissing || !activeAgentId) return;
942
+ void catalog.ensureAgentRows([activeAgentId]).catch(() => void 0);
943
+ }, [engaged, pinnedMissing, activeAgentId, catalog]);
872
944
  const selectDefaultRow = (0, import_react5.useCallback)(
873
945
  (state) => defaultMandateKey ? state.defaultRows[defaultMandateKey] : void 0,
874
946
  [defaultMandateKey]
@@ -945,13 +1017,14 @@ function useAgentListCore({
945
1017
  const handleAgentHover = (0, import_react5.useCallback)(
946
1018
  (agent, panelOpen) => {
947
1019
  if (panelOpen) return;
1020
+ if (agent.isThin) void catalog.ensureAgentRows([agent.id]).catch(() => void 0);
948
1021
  if (hoverLeaveTimerRef.current) {
949
1022
  clearTimeout(hoverLeaveTimerRef.current);
950
1023
  hoverLeaveTimerRef.current = null;
951
1024
  }
952
1025
  setHoveredAgent(agent);
953
1026
  },
954
- []
1027
+ [catalog]
955
1028
  );
956
1029
  const handleAgentHoverEnd = (0, import_react5.useCallback)(
957
1030
  (agent, onClear) => {
@@ -982,8 +1055,10 @@ function useAgentListCore({
982
1055
  },
983
1056
  []
984
1057
  );
1058
+ const tabTotal = consumer.state.tab === "mine" ? tabCountsForFilter.mine : consumer.state.tab === "shared" ? tabCountsForFilter.shared : consumer.state.tab === "system" ? tabCountsForFilter.system : tabCountsForFilter.all;
1059
+ const totalCount = paged && !consumer.state.searchTerm && excludedAgentIds.size === 0 ? Math.max(tabTotal, agents.length) : agents.length;
985
1060
  const footerLabel = agentListFooterLabel({
986
- count: agents.length,
1061
+ count: totalCount,
987
1062
  archFilter: consumer.archFilter,
988
1063
  archivedHiddenCount
989
1064
  });
@@ -1007,6 +1082,14 @@ function useAgentListCore({
1007
1082
  hoveredAgent,
1008
1083
  setHoveredAgent,
1009
1084
  ensureLoaded,
1085
+ /** FW-014: the list reads a page at a time; true while the slice has more. */
1086
+ paged,
1087
+ hasMore,
1088
+ loadingMore,
1089
+ loadMore,
1090
+ /** The tab's total (server count when paged) — the footer's number. */
1091
+ totalCount,
1092
+ ensureFullCatalog,
1010
1093
  handleSelectAgent,
1011
1094
  handleAgentHover,
1012
1095
  handleAgentHoverEnd,
@@ -1488,7 +1571,8 @@ function AgentDetailCard({ agent, onSelect, duplicate, onHoldChange }) {
1488
1571
  const select = (0, import_react6.useCallback)((state) => state.byId[agent.id], [agent.id]);
1489
1572
  const row = useAgentCatalogState(select);
1490
1573
  const on = row?.isFavorite ?? agent.isFavorite ?? false;
1491
- const record = agentPeekRecord(agent, {
1574
+ const live = row && !row.isThin ? row : agent;
1575
+ const record = agentPeekRecord(live, {
1492
1576
  favorite: {
1493
1577
  on,
1494
1578
  onToggle: () => {
@@ -1785,6 +1869,7 @@ var AGENT_ROW_HEIGHT = 36;
1785
1869
  var UNMEASURED_VIEWPORT = 480;
1786
1870
  var ROW_OVERSCAN = 8;
1787
1871
  var WINDOW_ABOVE = 50;
1872
+ var REACH_END_ROWS = 10;
1788
1873
  function AgentListContent({
1789
1874
  agents,
1790
1875
  isLoading,
@@ -1812,7 +1897,11 @@ function AgentListContent({
1812
1897
  listOpen = false,
1813
1898
  archivedHiddenCount = 0,
1814
1899
  agentFilterLabel = null,
1815
- onClearAgentFilter
1900
+ onClearAgentFilter,
1901
+ hasMore = false,
1902
+ onReachEnd,
1903
+ loadingMore = false,
1904
+ totalCount
1816
1905
  }) {
1817
1906
  const catalog = useAgentCatalog();
1818
1907
  const listScrollRef = (0, import_react7.useRef)(null);
@@ -1861,6 +1950,12 @@ function AgentListContent({
1861
1950
  scroller.scrollTo({ top });
1862
1951
  setScrollTop(top);
1863
1952
  }, [listOpen, pinnedAgent?.id, activeAgentId, isLoading]);
1953
+ (0, import_react7.useEffect)(() => {
1954
+ if (!hasMore || !onReachEnd || isLoading) return;
1955
+ const view = viewport > 0 ? viewport : UNMEASURED_VIEWPORT;
1956
+ const listBottom = listStart + agents.length * AGENT_ROW_HEIGHT;
1957
+ if (scrollTop + view >= listBottom - REACH_END_ROWS * AGENT_ROW_HEIGHT) onReachEnd();
1958
+ }, [hasMore, onReachEnd, isLoading, scrollTop, viewport, listStart, agents.length]);
1864
1959
  const win = agents.length <= WINDOW_ABOVE ? { start: 0, end: agents.length, padStart: 0, padEnd: 0 } : (0, import_virtual_window.computeWindow)({
1865
1960
  count: agents.length,
1866
1961
  itemSize: AGENT_ROW_HEIGHT,
@@ -2117,6 +2212,14 @@ function AgentListContent({
2117
2212
  ]
2118
2213
  }
2119
2214
  ),
2215
+ loadingMore && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2216
+ "div",
2217
+ {
2218
+ "data-testid": "agent-list-loading-more",
2219
+ className: "flex items-center justify-center py-2 text-muted-foreground",
2220
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(LoaderIcon, { className: "w-3.5 h-3.5 matrx-agent-spin" })
2221
+ }
2222
+ ),
2120
2223
  agents.length === 0 && (pinnedAgent || defaultAgent) && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex flex-col items-center py-4 text-muted-foreground", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "text-xs", children: emptyLabel }) })
2121
2224
  ] })
2122
2225
  }
@@ -2129,7 +2232,7 @@ function AgentListContent({
2129
2232
  "data-testid": "agent-list-footer",
2130
2233
  className: "text-[10px] text-muted-foreground tabular-nums",
2131
2234
  children: agentListFooterLabel({
2132
- count: agents.length,
2235
+ count: totalCount ?? agents.length,
2133
2236
  archFilter: consumer.archFilter,
2134
2237
  archivedHiddenCount
2135
2238
  })
@@ -2363,6 +2466,11 @@ function AgentListPanelBody({
2363
2466
  hoveredAgent,
2364
2467
  setHoveredAgent,
2365
2468
  ensureLoaded,
2469
+ hasMore,
2470
+ loadMore,
2471
+ loadingMore,
2472
+ totalCount,
2473
+ ensureFullCatalog,
2366
2474
  handleSelectAgent: coreSelectAgent,
2367
2475
  handleAgentHover: coreAgentHover,
2368
2476
  handleAgentHoverEnd: coreAgentHoverEnd,
@@ -2390,6 +2498,7 @@ function AgentListPanelBody({
2390
2498
  onAfterSelect?.(agent);
2391
2499
  };
2392
2500
  const handleFilterChipClick = (panel) => {
2501
+ if (panel !== "sort") ensureFullCatalog();
2393
2502
  if (isMobile) {
2394
2503
  setMobileSubView(panel);
2395
2504
  } else {
@@ -2462,6 +2571,10 @@ function AgentListPanelBody({
2462
2571
  defaultRow,
2463
2572
  agentFilterLabel,
2464
2573
  onClearAgentFilter: clearAgentFilter,
2574
+ hasMore,
2575
+ onReachEnd: loadMore,
2576
+ loadingMore,
2577
+ totalCount,
2465
2578
  listOpen: true
2466
2579
  }
2467
2580
  );
@@ -2801,6 +2914,11 @@ function AgentListInlinePicker({
2801
2914
  agentFilterLabel,
2802
2915
  clearAgentFilter,
2803
2916
  ensureLoaded,
2917
+ hasMore,
2918
+ loadMore,
2919
+ loadingMore,
2920
+ totalCount,
2921
+ ensureFullCatalog,
2804
2922
  handleSelectAgent: coreSelectAgent,
2805
2923
  handleAgentHover,
2806
2924
  handleAgentHoverEnd
@@ -2830,6 +2948,7 @@ function AgentListInlinePicker({
2830
2948
  setMobileSubView(null);
2831
2949
  };
2832
2950
  const handleFilterChipClick = (panel) => {
2951
+ if (panel !== "sort") ensureFullCatalog();
2833
2952
  setMobileSubView(panel);
2834
2953
  };
2835
2954
  const isMobile = true;
@@ -2908,6 +3027,10 @@ function AgentListInlinePicker({
2908
3027
  defaultRow,
2909
3028
  agentFilterLabel,
2910
3029
  onClearAgentFilter: clearAgentFilter,
3030
+ hasMore,
3031
+ onReachEnd: loadMore,
3032
+ loadingMore,
3033
+ totalCount,
2911
3034
  listOpen: true
2912
3035
  }
2913
3036
  ) });
@@ -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;