@ai-matrx/agents 0.8.0 → 0.9.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,140 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 — 2026-09-08
4
+
5
+ **THE ONE PICKER NOW RENDERS ON REACT NATIVE — `@ai-matrx/agents/catalog/native`.**
6
+
7
+ Matrx Mobile (Expo 54 / RN 0.81) was the last client still hand-rolling its
8
+ agent list. Its sheet showed four HARDCODED agents plus a Supabase read of a
9
+ `prompts` table that **does not exist in the platform database** — a read whose
10
+ PostgREST error was swallowed into an empty array, so the list silently never
11
+ had anything but the four constants. Three of those four constants named the
12
+ WRONG agent: the ids behind "General Chat", "Deep Research" and "Code Helper"
13
+ resolve in `agent.definition` to *Agent Ledger*, *Knowledge Search Test* and
14
+ *Balanced News Analysis*. A label lying on screen, with nothing anywhere that
15
+ could notice — the same class the mandate default row was built to end.
16
+
17
+ React Native could not adopt the picker before this release for two concrete
18
+ reasons, and both are fixed here rather than worked around in the app.
19
+
20
+ ### Consumer action (C28)
21
+
22
+ **Web hosts: none.** `@ai-matrx/agents/catalog` and `/catalog/react` keep every
23
+ export, every name and every behaviour. The provider, the store subscription
24
+ and the two list hooks moved to `catalog/shared/*` so both platforms run one
25
+ implementation; `catalog/react` re-exports all of them unchanged, and the
26
+ `"use client"` banner is still stamped on the react build and still absent from
27
+ the pure ones (the tarball canary now judges eight artefacts instead of four).
28
+
29
+ **React Native hosts:**
30
+
31
+ ```tsx
32
+ import AsyncStorage from "@react-native-async-storage/async-storage";
33
+ import { createAgentCatalog } from "@ai-matrx/agents/catalog";
34
+ import { AgentCatalogProvider, AgentListSheet } from "@ai-matrx/agents/catalog/native";
35
+
36
+ const catalog = createAgentCatalog({
37
+ client: supabase,
38
+ identity: { requireUserId: () => requireUserId() },
39
+ storage: AsyncStorage, // NEW — the async storage seam
40
+ transport, // only when a picker passes defaultMandateKey
41
+ errorSink: reportToDiagnostics,
42
+ });
43
+
44
+ <AgentCatalogProvider catalog={catalog} navigate={(href) => router.push(href)}>
45
+ <AgentListSheet consumerId="chat" onSelect={setAgentId} activeAgentId={agentId} />
46
+ </AgentCatalogProvider>
47
+ ```
48
+
49
+ - `react-native` and `react-native-svg` are OPTIONAL peers. Install
50
+ `react-native-svg` (`npx expo install react-native-svg`) for the real glyphs;
51
+ without it the icons fall back to text glyphs and **say so once** through the
52
+ `errorSink`, with the remedy. Nothing goes blank and nothing goes quiet.
53
+ - Bind `navigate` if your rows should be able to open an agent. Unbound, the
54
+ default is a no-op that REPORTS through the `errorSink` — never a control
55
+ that silently does nothing.
56
+ - `@ai-matrx/agents/catalog/styles.css` is a WEB artefact; the native entry
57
+ needs no stylesheet.
58
+
59
+ ### What is new
60
+
61
+ - **`@ai-matrx/agents/catalog/native`** — the picker in React Native
62
+ primitives over the SAME headless kernel and the SAME hooks:
63
+ `AgentCatalogProvider`, `AgentListSheet` (the body of a bottom sheet — the
64
+ host keeps its own sheet chrome, so no gesture library is forced on anyone),
65
+ `AgentListInlinePicker`, and every part beneath them (`AgentListContent`,
66
+ `AgentListTabs`, `AgentFilterBar`, `AgentRow`, `AgentDetailCard`,
67
+ `AgentSubView`, `FavoriteAgentButton`, the five primitives, `AgentIcon`).
68
+ The shells take the web shells' prop surface name for name: `onSelect`,
69
+ `activeAgentId`, `consumerId`, `initialTab`, `includeSystemInAll`,
70
+ `visibleTabs`, `systemTabLabel`, `showPinnedAgent`, `excludeAgentIds`,
71
+ `defaultMandateKey`, `compact`.
72
+ - **The async storage seam** — `AgentCatalogAsyncStorage` /
73
+ `AgentCatalogStorageLike`, plus `readCachedDefaultRowAsync` and
74
+ `writeCachedDefaultRowAsync`. `createAgentCatalog({ storage })` now accepts an
75
+ `AsyncStorage`-shaped store verbatim. React Native has no `localStorage`, so
76
+ before this a native host could only hand the catalog a store that answered
77
+ `null` synchronously (a lie) or lose the default row's first paint entirely.
78
+ **The web path is unchanged**: `AgentCatalogStorage` is still synchronous,
79
+ `readCachedDefaultRow`/`writeCachedDefaultRow` still have their synchronous
80
+ signatures, `defaultStorage()` still prefers `localStorage`, and the async
81
+ helpers over a synchronous store produce byte-identical cache payloads
82
+ (proven in `catalog/__tests__/storage-seam.test.ts`).
83
+ - **`AgentCatalogNativeTheme`** (C26 on a platform with no CSS) — the native
84
+ twin of the `--matrx-agent-*` sheet: every token ships a DEFAULT VALUE so the
85
+ picker renders correctly unthemed, and a host re-brands by passing `theme` to
86
+ the provider. `DEFAULT_AGENT_CATALOG_NATIVE_THEME` (dark) and
87
+ `LIGHT_AGENT_CATALOG_NATIVE_THEME` ship; `resolveAgentCatalogNativeTheme`
88
+ merges an override and keeps every value it omits. No native component
89
+ contains a colour literal.
90
+ - **`catalog/shared/icon-paths.ts`** — the glyph geometry, once. The web icons
91
+ now read it too, so a shape can never differ between the two platforms; the
92
+ native entry draws the same paths through `react-native-svg` (C19: still no
93
+ icon-library dependency, on either platform).
94
+ - **`AgentListTabCounts` moved** to `catalog/shared/useAgentListCore` beside the
95
+ code that computes it. `catalog/react/AgentListTabs` re-exports it unchanged.
96
+
97
+ ### Behavioural deltas on native, recorded rather than hidden
98
+
99
+ 1. **No hover preview.** The web picker's side panel opens on hover; a touch
100
+ screen has no hover, so the detail card is reached by the row's chevron —
101
+ the same three-view stack (`list → detail → sort/categories/tags`) the web
102
+ `AgentListInlinePicker` already used on mobile widths.
103
+ 2. **No cmd-click link.** `AgentRow`'s anchor exists on the web so a person can
104
+ open an agent in a new tab. React Native has neither, so the row is an
105
+ ordinary `Pressable`; the `LinkComponent` port still exists and still ships
106
+ a working native default.
107
+ 3. **`autoFocusSearch` defaults to `false`** (the web inline picker defaults to
108
+ `true`): a keyboard that opens with the sheet covers the list it was meant
109
+ to filter.
110
+
111
+ ### Proof
112
+
113
+ - **The 293-case ordering matrix, re-measured through the RENDERED native
114
+ list** (`catalog/native/__tests__/parity.test.tsx`): every case's ids, in
115
+ order, read back out of the mounted native tree — not out of the selectors a
116
+ second time. Sharing a kernel is an architecture claim; this is the
117
+ measurement. It carries a falsifiability leg.
118
+ - **The tarball canary imports AND requires `./catalog/native`**, checks every
119
+ documented export, proves the theme override merges over the PACKED build,
120
+ proves the icon degradation announces exactly once with its remedy, and
121
+ compiles a native usage (including an `AsyncStorage`-shaped `storage`) against
122
+ the packed types under `exactOptionalPropertyTypes` + `noUncheckedIndexedAccess`.
123
+ - **The banner canary now judges eight artefacts**: the `"use client"` prologue
124
+ must be present on both `catalog/react` builds and ABSENT from all four
125
+ `catalog/index.*` and all four `catalog/native/index.*` files.
126
+
127
+ ### Known limits, stated plainly
128
+
129
+ There is still **no React Native canary harness** in this workspace (campaign
130
+ decision C7 schedules it for Phase 2), and the real `react-native` package
131
+ cannot be loaded by vitest — it ships Flow-typed source only Metro parses. The
132
+ native tests therefore render against a minimal RN stand-in
133
+ (`catalog/native/__tests__/react-native.stub.tsx`) that maps RN primitives onto
134
+ DOM nodes. That proves rows, order, labels, counts, handlers and every
135
+ announced degradation. It does **not** prove pixel layout, gestures or Hermes —
136
+ those are proven by the consuming Expo app's own build and device smoke.
137
+
3
138
  ## 0.8.0 — 2026-09-08
4
139
 
5
140
  **THE ORCHESTRA BADGE IS BACK, in every client at once.**
@@ -69,6 +69,7 @@ __export(catalog_exports, {
69
69
  parseMandateResolution: () => parseMandateResolution,
70
70
  readAgentCatalogRows: () => readAgentCatalogRows,
71
71
  readCachedDefaultRow: () => readCachedDefaultRow,
72
+ readCachedDefaultRowAsync: () => readCachedDefaultRowAsync,
72
73
  readSingleAgentRow: () => readSingleAgentRow,
73
74
  resolveDefaultRow: () => resolveDefaultRow,
74
75
  searchAgentsOnServer: () => searchAgentsOnServer,
@@ -87,7 +88,8 @@ __export(catalog_exports, {
87
88
  sortFilteredAgents: () => sortFilteredAgents,
88
89
  toAgentSummary: () => toAgentSummary,
89
90
  writeAgentFavorite: () => writeAgentFavorite,
90
- writeCachedDefaultRow: () => writeCachedDefaultRow
91
+ writeCachedDefaultRow: () => writeCachedDefaultRow,
92
+ writeCachedDefaultRowAsync: () => writeCachedDefaultRowAsync
91
93
  });
92
94
  module.exports = __toCommonJS(catalog_exports);
93
95
 
@@ -430,6 +432,51 @@ function writeCachedDefaultRow(storage, resolved) {
430
432
  } catch {
431
433
  }
432
434
  }
435
+ function parseCachedDefaultRow(mandateKey, raw) {
436
+ try {
437
+ if (!raw) return null;
438
+ const parsed = JSON.parse(raw);
439
+ if (!isRecord2(parsed)) return null;
440
+ const holderId = parsed["holderId"];
441
+ const holderName = parsed["holderName"];
442
+ if (typeof holderId !== "string" || typeof holderName !== "string") {
443
+ return null;
444
+ }
445
+ const description = parsed["holderDescription"];
446
+ const provenance = parsed["provenance"];
447
+ const at = parsed["at"];
448
+ return {
449
+ mandateKey,
450
+ holderId,
451
+ holderName,
452
+ holderDescription: typeof description === "string" ? description : null,
453
+ provenance: typeof provenance === "string" ? provenance : "system",
454
+ at: typeof at === "number" ? at : 0
455
+ };
456
+ } catch {
457
+ return null;
458
+ }
459
+ }
460
+ async function readCachedDefaultRowAsync(storage, mandateKey) {
461
+ let raw;
462
+ try {
463
+ raw = await storage.getItem(
464
+ CACHE_PREFIX + mandateKey
465
+ );
466
+ } catch {
467
+ return null;
468
+ }
469
+ return parseCachedDefaultRow(mandateKey, raw);
470
+ }
471
+ async function writeCachedDefaultRowAsync(storage, resolved) {
472
+ try {
473
+ await storage.setItem(
474
+ CACHE_PREFIX + resolved.mandateKey,
475
+ JSON.stringify(resolved)
476
+ );
477
+ } catch {
478
+ }
479
+ }
433
480
  function defaultRowFromResolution(resolved) {
434
481
  return {
435
482
  id: mandateAgentId(resolved.mandateKey),
@@ -469,7 +516,7 @@ function detectDefaultRowDrift(cached, live) {
469
516
  }
470
517
  async function resolveDefaultRow(args) {
471
518
  const { mandateKey, transport, client, storage, lookupRow } = args;
472
- const cached = readCachedDefaultRow(storage, mandateKey);
519
+ const cached = await readCachedDefaultRowAsync(storage, mandateKey);
473
520
  const verdict = await askTheServer(transport, mandateKey);
474
521
  if (verdict.holderType !== "agent") {
475
522
  throw new MandateDefaultRowError({
@@ -506,7 +553,7 @@ async function resolveDefaultRow(args) {
506
553
  at: Date.now()
507
554
  };
508
555
  const drift = detectDefaultRowDrift(cached, resolved);
509
- writeCachedDefaultRow(storage, resolved);
556
+ await writeCachedDefaultRowAsync(storage, resolved);
510
557
  return { resolved, row: defaultRowFromResolution(resolved), drift };
511
558
  }
512
559