@ai-matrx/agents 0.59.238 → 0.59.240

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.59.240
4
+
5
+ UNVERIFIED: this release has no Unreleased entry and the source comparison could not be made (the caller supplied no source comparison). Whether source changed is unknown; consumer action is unknown.
6
+ Check the package's gitHead on npm against this version's gitHead: `git diff <previous gitHead> <this gitHead> -- apps/shared/matrx-agents/src`.
7
+
8
+ ## 0.59.239
9
+
10
+ Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff 9df1465977 6acfc6b938 -- apps/shared/matrx-agents/src`
11
+ has no shipped-source changes outside tests, fixtures and docs). No consumer action required.
12
+
3
13
  ## 0.59.238
4
14
 
5
15
  - **The context views never warn the person (Arman, 2026-10-22).** `ContextRulesChip` loses `mismatchCount` / `mismatches` (no amber face, no warning glyph, no "N · M" count); `ContextRulesTable` and `ContextRulesPanelBody` lose `mismatches` (no row marker, no "expected X, got Y" banner); `describeMismatches` is deleted. `compareReceipt` is unchanged — the host still files the silent `context_truth_mismatch`. A self-check row keeps its tooltip from `row.blockedBy`. Guard: `context/react/a-context-view-never-warns-the-person.test.tsx`. Consumer action: stop passing `mismatchCount` / `mismatches`.
@@ -1591,11 +1591,16 @@ function normalizeAdmin(row, offerings) {
1591
1591
  };
1592
1592
  }
1593
1593
  var rows = (data) => Array.isArray(data) ? data : [];
1594
+ function aiRpc(client, fn) {
1595
+ const scope = client.schema("ai");
1596
+ if (!scope.rpc) throw new Error(`The model catalog client's schema("ai") has no rpc(), so ${fn} cannot be called.`);
1597
+ return scope.rpc(fn);
1598
+ }
1594
1599
  async function readCatalog(client, variant) {
1595
1600
  if (variant === "admin") {
1596
1601
  const [rpcRes, offeringsRes] = await Promise.all([
1597
1602
  client.rpc("admin_model_catalog"),
1598
- client.rpc("admin_model_offerings")
1603
+ aiRpc(client, "admin_model_offerings")
1599
1604
  ]);
1600
1605
  if (rpcRes.error) throw rpcRes.error;
1601
1606
  if (offeringsRes.error) throw offeringsRes.error;
@@ -756,7 +756,7 @@ declare const selectModelIdentityLookupStatus: (s: ModelRecordsState, modelId: s
756
756
  * - "user" → `ai.model_public` (anon + authenticated; masked, points pricing), filtered to
757
757
  * ROUTABLE models via `ai.model_offering`, which also supplies each model's serving
758
758
  * TIERS (`served_via` — the endpoint's public Matrx brand, never the real vendor).
759
- * - "admin" → `public.admin_model_catalog()` + `public.admin_model_offerings()` (SECURITY
759
+ * - "admin" → `public.admin_model_catalog()` + `ai.admin_model_offerings()` (SECURITY
760
760
  * DEFINER, gated by `is_super_admin()`): raw $ pricing, real vendors, every
761
761
  * offering. NEVER read `ai.model_admin` / `ai.offering` directly from a client.
762
762
  *
@@ -787,6 +787,8 @@ interface ModelCatalogClient {
787
787
  rpc(fn: string, args?: Record<string, unknown>): PromiseLike<ModelCatalogPgResult>;
788
788
  schema(name: string): {
789
789
  from(table: string): ModelCatalogQuery;
790
+ /** Present on every supabase-js schema scope; optional only so read-only fixtures stay valid. */
791
+ rpc?(fn: string, args?: Record<string, unknown>): PromiseLike<ModelCatalogPgResult>;
790
792
  };
791
793
  }
792
794
  type ModelCatalogErrorSink = (event: {
@@ -756,7 +756,7 @@ declare const selectModelIdentityLookupStatus: (s: ModelRecordsState, modelId: s
756
756
  * - "user" → `ai.model_public` (anon + authenticated; masked, points pricing), filtered to
757
757
  * ROUTABLE models via `ai.model_offering`, which also supplies each model's serving
758
758
  * TIERS (`served_via` — the endpoint's public Matrx brand, never the real vendor).
759
- * - "admin" → `public.admin_model_catalog()` + `public.admin_model_offerings()` (SECURITY
759
+ * - "admin" → `public.admin_model_catalog()` + `ai.admin_model_offerings()` (SECURITY
760
760
  * DEFINER, gated by `is_super_admin()`): raw $ pricing, real vendors, every
761
761
  * offering. NEVER read `ai.model_admin` / `ai.offering` directly from a client.
762
762
  *
@@ -787,6 +787,8 @@ interface ModelCatalogClient {
787
787
  rpc(fn: string, args?: Record<string, unknown>): PromiseLike<ModelCatalogPgResult>;
788
788
  schema(name: string): {
789
789
  from(table: string): ModelCatalogQuery;
790
+ /** Present on every supabase-js schema scope; optional only so read-only fixtures stay valid. */
791
+ rpc?(fn: string, args?: Record<string, unknown>): PromiseLike<ModelCatalogPgResult>;
790
792
  };
791
793
  }
792
794
  type ModelCatalogErrorSink = (event: {
@@ -1432,11 +1432,16 @@ function normalizeAdmin(row, offerings) {
1432
1432
  };
1433
1433
  }
1434
1434
  var rows = (data) => Array.isArray(data) ? data : [];
1435
+ function aiRpc(client, fn) {
1436
+ const scope = client.schema("ai");
1437
+ if (!scope.rpc) throw new Error(`The model catalog client's schema("ai") has no rpc(), so ${fn} cannot be called.`);
1438
+ return scope.rpc(fn);
1439
+ }
1435
1440
  async function readCatalog(client, variant) {
1436
1441
  if (variant === "admin") {
1437
1442
  const [rpcRes, offeringsRes] = await Promise.all([
1438
1443
  client.rpc("admin_model_catalog"),
1439
- client.rpc("admin_model_offerings")
1444
+ aiRpc(client, "admin_model_offerings")
1440
1445
  ]);
1441
1446
  if (rpcRes.error) throw rpcRes.error;
1442
1447
  if (offeringsRes.error) throw offeringsRes.error;
@@ -107,7 +107,14 @@ function useLocalModels(override) {
107
107
  port ? port.getModels : () => NO_LOCAL_MODELS,
108
108
  port ? port.getModels : () => NO_LOCAL_MODELS
109
109
  );
110
- return { models, groupLabel: port?.groupLabel ?? "On this computer", open: port?.open ? (id) => port.open?.(id) : void 0 };
110
+ return {
111
+ models,
112
+ groupLabel: port?.groupLabel ?? "On this computer",
113
+ open: port?.open ? (id) => port.open?.(id) : void 0,
114
+ start: port?.start ? (id) => port.start?.(id) : void 0,
115
+ stop: port?.stop ? (id) => port.stop?.(id) : void 0,
116
+ getMore: port?.getMore ? () => port.getMore?.() : void 0
117
+ };
111
118
  }
112
119
  var NO_ACCESS = {};
113
120
  function useModelAccess(override) {
@@ -1405,7 +1412,13 @@ function ModelRow({
1405
1412
  }
1406
1413
  );
1407
1414
  }
1408
- function LocalModelRow({ model, selected, onSelect }) {
1415
+ function LocalModelRow({
1416
+ model,
1417
+ selected,
1418
+ onSelect,
1419
+ control,
1420
+ failure
1421
+ }) {
1409
1422
  const unusable = model.unusable ?? null;
1410
1423
  const dim = model.needsMoreMemory === true;
1411
1424
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
@@ -1446,11 +1459,23 @@ function LocalModelRow({ model, selected, onSelect }) {
1446
1459
  ),
1447
1460
  dim && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "shrink-0 rounded bg-muted px-1 text-[9px] text-muted-foreground", title: "Needs more memory than this computer has free", children: "needs memory" })
1448
1461
  ] }),
1449
- unusable && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { "data-local-model-reason": true, className: "truncate text-[11px] text-muted-foreground", children: unusable })
1462
+ (unusable || failure) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { "data-local-model-reason": true, className: (0, import_design_system.cn)("truncate text-[11px]", failure ? "text-destructive" : "text-muted-foreground"), title: failure ?? void 0, children: failure ?? unusable })
1450
1463
  ] }),
1451
1464
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "w-11" }),
1452
1465
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "block w-12 whitespace-nowrap text-right font-mono text-[11px] tabular-nums text-foreground/80", children: model.contextWindow == null ? "\u2014" : fmtPointsCompact(model.contextWindow) }),
1453
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "w-[2.75rem]" })
1466
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "flex w-[2.75rem] justify-end", onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: control && !unusable ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1467
+ import_design_system.IconButton,
1468
+ {
1469
+ icon: control.kind === "stop" ? import_icons5.StopIcon : import_icons5.PlayIcon,
1470
+ tooltip: control.pending ? control.kind === "stop" ? "Stopping" : "Starting" : control.kind === "stop" ? "Stop" : "Start",
1471
+ "aria-label": control.kind === "stop" ? `Stop ${model.name}` : `Start ${model.name}`,
1472
+ "data-local-model-control": control.kind,
1473
+ "data-pending": control.pending ? "" : void 0,
1474
+ disabled: control.pending,
1475
+ spinning: control.pending,
1476
+ onClick: control.onPress
1477
+ }
1478
+ ) : null })
1454
1479
  ]
1455
1480
  }
1456
1481
  );
@@ -1553,7 +1578,41 @@ function ModelListDropdown({
1553
1578
  const priorityModelSet = priorityModelIds ? new Set(priorityModelIds) : null;
1554
1579
  const eligibleModels = allowedModelSet ? models.filter((model) => allowedModelSet.has(model.id)) : models;
1555
1580
  const selected = models.find((m) => m.id === value) ?? null;
1556
- const { models: localAll, groupLabel: localGroupLabel, open: openLocal } = useLocalModels(localModelsProp);
1581
+ const { models: localAll, groupLabel: localGroupLabel, open: openLocal, start: startLocal, stop: stopLocal, getMore: getMoreLocal } = useLocalModels(localModelsProp);
1582
+ const [localPending, setLocalPending] = (0, import_react2.useState)(() => /* @__PURE__ */ new Set());
1583
+ const [localFailure, setLocalFailure] = (0, import_react2.useState)(() => /* @__PURE__ */ new Map());
1584
+ const runLocal = (id2, action, verb) => {
1585
+ setLocalFailure((cur) => {
1586
+ if (!cur.has(id2)) return cur;
1587
+ const next = new Map(cur);
1588
+ next.delete(id2);
1589
+ return next;
1590
+ });
1591
+ setLocalPending((cur) => new Set(cur).add(id2));
1592
+ let result;
1593
+ try {
1594
+ result = action(id2);
1595
+ } catch (error2) {
1596
+ result = Promise.reject(error2);
1597
+ }
1598
+ void Promise.resolve(result).catch((error2) => {
1599
+ console.error(`[model-picker] could not ${verb} ${id2}:`, error2);
1600
+ const why = error2 instanceof Error && error2.message ? error2.message : `Could not ${verb}`;
1601
+ setLocalFailure((cur) => new Map(cur).set(id2, why));
1602
+ }).finally(
1603
+ () => setLocalPending((cur) => {
1604
+ const next = new Set(cur);
1605
+ next.delete(id2);
1606
+ return next;
1607
+ })
1608
+ );
1609
+ };
1610
+ const localControl = (m) => {
1611
+ const pending = localPending.has(m.id) || m.starting === true;
1612
+ if (m.running === true && stopLocal) return { kind: "stop", pending, onPress: () => runLocal(m.id, stopLocal, "stop") };
1613
+ if (m.running !== true && startLocal) return { kind: "start", pending, onPress: () => runLocal(m.id, startLocal, "start") };
1614
+ return null;
1615
+ };
1557
1616
  const selectedLocal = localAll.find((m) => m.id === value) ?? null;
1558
1617
  const localEligible = !modelOnly && variant === "user" && !allowedModelSet && (effectiveSelectionPurpose === "chat" || effectiveSelectionPurpose === "agent");
1559
1618
  const localRows = (0, import_react2.useMemo)(() => {
@@ -1954,11 +2013,36 @@ function ModelListDropdown({
1954
2013
  children: emptyOptionLabel
1955
2014
  }
1956
2015
  ),
1957
- localRows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1958
- localRunnable.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: GROUP_HEADING, children: localGroupLabel }),
1959
- localRunnable.map((l) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LocalModelRow, { model: l, selected: l.id === value, onSelect: () => handleSelectLocal(l) }, l.id)),
2016
+ (localRows.length > 0 || localEligible && tab === "all" && !query.trim() && getMoreLocal) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
2017
+ (localRunnable.length > 0 || localRows.length === 0 && getMoreLocal) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: GROUP_HEADING, children: localGroupLabel }),
2018
+ localRunnable.map((l) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LocalModelRow, { model: l, selected: l.id === value, onSelect: () => handleSelectLocal(l), control: localControl(l), failure: localFailure.get(l.id) ?? null }, l.id)),
2019
+ getMoreLocal && localEligible && tab === "all" && !query.trim() && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2020
+ "div",
2021
+ {
2022
+ role: "button",
2023
+ tabIndex: 0,
2024
+ "data-local-get-more": "",
2025
+ onClick: () => {
2026
+ getMoreLocal();
2027
+ handleOpen(false);
2028
+ },
2029
+ onKeyDown: (e) => {
2030
+ if (e.key === "Enter" || e.key === " ") {
2031
+ e.preventDefault();
2032
+ getMoreLocal();
2033
+ handleOpen(false);
2034
+ }
2035
+ },
2036
+ className: "grid w-full cursor-pointer grid-cols-[16px_20px_minmax(0,1fr)] items-center gap-2 rounded px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground focus:bg-muted/60 focus:outline-none",
2037
+ children: [
2038
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "w-4" }),
2039
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "flex w-5 justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons5.PlusIcon, { className: "h-3.5 w-3.5" }) }),
2040
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "truncate", children: "Get More Models..." })
2041
+ ]
2042
+ }
2043
+ ),
1960
2044
  localUnusable.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: GROUP_HEADING, children: "Cannot run here" }),
1961
- localUnusable.map((l) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LocalModelRow, { model: l, selected: false, onSelect: () => void 0 }, l.id)),
2045
+ localUnusable.map((l) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LocalModelRow, { model: l, selected: false, onSelect: () => void 0, failure: localFailure.get(l.id) ?? null }, l.id)),
1962
2046
  filtered.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: GROUP_HEADING, children: "Cloud" })
1963
2047
  ] }),
1964
2048
  filtered.length === 0 ? localRows.length > 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "p-4 text-center text-xs text-muted-foreground", children: "No models match your filters." }) : listRows.map(({ model: m, classTier }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -372,7 +372,7 @@ interface ModelRecords {
372
372
  * - "user" → `ai.model_public` (anon + authenticated; masked, points pricing), filtered to
373
373
  * ROUTABLE models via `ai.model_offering`, which also supplies each model's serving
374
374
  * TIERS (`served_via` — the endpoint's public Matrx brand, never the real vendor).
375
- * - "admin" → `public.admin_model_catalog()` + `public.admin_model_offerings()` (SECURITY
375
+ * - "admin" → `public.admin_model_catalog()` + `ai.admin_model_offerings()` (SECURITY
376
376
  * DEFINER, gated by `is_super_admin()`): raw $ pricing, real vendors, every
377
377
  * offering. NEVER read `ai.model_admin` / `ai.offering` directly from a client.
378
378
  *
@@ -436,11 +436,20 @@ interface LocalModelEntry {
436
436
  * picker lists it under "Cannot run here" with this reason, dimmed and never pickable.
437
437
  */
438
438
  unusable?: string;
439
+ /**
440
+ * The model is loaded and answering now. With a port `stop`, the row draws Stop; with `start`
441
+ * and not running, Start. Absent = the host does not say (no control is drawn).
442
+ */
443
+ running?: boolean;
444
+ /** The host is loading it right now: the row's control shows its pending state. */
445
+ starting?: boolean;
439
446
  }
440
447
  /**
441
448
  * The host's local-model source (a store: `getModels` + `subscribe`, like the catalog). The picker
442
- * draws its models as one "On this computer" group marked local. Start/Stop and getting more
443
- * models stay the host's own screens.
449
+ * draws its models as one "On this computer" group marked local. Starting and stopping a model and
450
+ * getting more of them are capabilities by presence: a host that supplies `start` / `stop` / `getMore`
451
+ * gets a compact Start/Stop control per row and one "Get More Models..." row; a host that leaves them
452
+ * out gets neither (its own screens stay the only way).
444
453
  */
445
454
  interface LocalModelsPort {
446
455
  /** Must return the SAME array until the list changes (it is a store snapshot). */
@@ -455,6 +464,15 @@ interface LocalModelsPort {
455
464
  * the id through `onValueChange` (the override path).
456
465
  */
457
466
  open?(id: string): void;
467
+ /**
468
+ * Start the model (load it so it can answer). Present = a row that is not running draws Start. The
469
+ * row shows a pending state until the returned promise settles; a rejection says why on the row.
470
+ */
471
+ start?(id: string): void | Promise<unknown>;
472
+ /** Stop the running model. Present = a running row draws Stop (same pending state and failure line). */
473
+ stop?(id: string): void | Promise<unknown>;
474
+ /** Open the host's place to find and install more models. Present = one "Get More Models..." row ends the group. */
475
+ getMore?(): void;
458
476
  }
459
477
  /**
460
478
  * How the person may reach a cloud model in THIS place (a chat composer passes it to its own picker,
@@ -514,6 +532,9 @@ declare function useLocalModels(override?: LocalModelsPort): {
514
532
  models: readonly LocalModelEntry[];
515
533
  groupLabel: string;
516
534
  open: ((id: string) => void) | undefined;
535
+ start: ((id: string) => void | Promise<unknown>) | undefined;
536
+ stop: ((id: string) => void | Promise<unknown>) | undefined;
537
+ getMore: (() => void) | undefined;
517
538
  };
518
539
  /** The cloud-access rules for this picker; a port passed straight to the picker beats the provider's. */
519
540
  declare function useModelAccess(override?: ModelAccessPort): ModelAccessPort;
@@ -372,7 +372,7 @@ interface ModelRecords {
372
372
  * - "user" → `ai.model_public` (anon + authenticated; masked, points pricing), filtered to
373
373
  * ROUTABLE models via `ai.model_offering`, which also supplies each model's serving
374
374
  * TIERS (`served_via` — the endpoint's public Matrx brand, never the real vendor).
375
- * - "admin" → `public.admin_model_catalog()` + `public.admin_model_offerings()` (SECURITY
375
+ * - "admin" → `public.admin_model_catalog()` + `ai.admin_model_offerings()` (SECURITY
376
376
  * DEFINER, gated by `is_super_admin()`): raw $ pricing, real vendors, every
377
377
  * offering. NEVER read `ai.model_admin` / `ai.offering` directly from a client.
378
378
  *
@@ -436,11 +436,20 @@ interface LocalModelEntry {
436
436
  * picker lists it under "Cannot run here" with this reason, dimmed and never pickable.
437
437
  */
438
438
  unusable?: string;
439
+ /**
440
+ * The model is loaded and answering now. With a port `stop`, the row draws Stop; with `start`
441
+ * and not running, Start. Absent = the host does not say (no control is drawn).
442
+ */
443
+ running?: boolean;
444
+ /** The host is loading it right now: the row's control shows its pending state. */
445
+ starting?: boolean;
439
446
  }
440
447
  /**
441
448
  * The host's local-model source (a store: `getModels` + `subscribe`, like the catalog). The picker
442
- * draws its models as one "On this computer" group marked local. Start/Stop and getting more
443
- * models stay the host's own screens.
449
+ * draws its models as one "On this computer" group marked local. Starting and stopping a model and
450
+ * getting more of them are capabilities by presence: a host that supplies `start` / `stop` / `getMore`
451
+ * gets a compact Start/Stop control per row and one "Get More Models..." row; a host that leaves them
452
+ * out gets neither (its own screens stay the only way).
444
453
  */
445
454
  interface LocalModelsPort {
446
455
  /** Must return the SAME array until the list changes (it is a store snapshot). */
@@ -455,6 +464,15 @@ interface LocalModelsPort {
455
464
  * the id through `onValueChange` (the override path).
456
465
  */
457
466
  open?(id: string): void;
467
+ /**
468
+ * Start the model (load it so it can answer). Present = a row that is not running draws Start. The
469
+ * row shows a pending state until the returned promise settles; a rejection says why on the row.
470
+ */
471
+ start?(id: string): void | Promise<unknown>;
472
+ /** Stop the running model. Present = a running row draws Stop (same pending state and failure line). */
473
+ stop?(id: string): void | Promise<unknown>;
474
+ /** Open the host's place to find and install more models. Present = one "Get More Models..." row ends the group. */
475
+ getMore?(): void;
458
476
  }
459
477
  /**
460
478
  * How the person may reach a cloud model in THIS place (a chat composer passes it to its own picker,
@@ -514,6 +532,9 @@ declare function useLocalModels(override?: LocalModelsPort): {
514
532
  models: readonly LocalModelEntry[];
515
533
  groupLabel: string;
516
534
  open: ((id: string) => void) | undefined;
535
+ start: ((id: string) => void | Promise<unknown>) | undefined;
536
+ stop: ((id: string) => void | Promise<unknown>) | undefined;
537
+ getMore: (() => void) | undefined;
517
538
  };
518
539
  /** The cloud-access rules for this picker; a port passed straight to the picker beats the provider's. */
519
540
  declare function useModelAccess(override?: ModelAccessPort): ModelAccessPort;
@@ -71,7 +71,14 @@ function useLocalModels(override) {
71
71
  port ? port.getModels : () => NO_LOCAL_MODELS,
72
72
  port ? port.getModels : () => NO_LOCAL_MODELS
73
73
  );
74
- return { models, groupLabel: port?.groupLabel ?? "On this computer", open: port?.open ? (id) => port.open?.(id) : void 0 };
74
+ return {
75
+ models,
76
+ groupLabel: port?.groupLabel ?? "On this computer",
77
+ open: port?.open ? (id) => port.open?.(id) : void 0,
78
+ start: port?.start ? (id) => port.start?.(id) : void 0,
79
+ stop: port?.stop ? (id) => port.stop?.(id) : void 0,
80
+ getMore: port?.getMore ? () => port.getMore?.() : void 0
81
+ };
75
82
  }
76
83
  var NO_ACCESS = {};
77
84
  function useModelAccess(override) {
@@ -88,6 +95,7 @@ import {
88
95
  Popover,
89
96
  PopoverTrigger,
90
97
  PopoverContent,
98
+ IconButton,
91
99
  Tooltip,
92
100
  TooltipContent,
93
101
  TooltipProvider,
@@ -302,7 +310,7 @@ var ScaleIcon = sized(GScaleIcon);
302
310
  var LanguagesIcon = sized(GLanguagesIcon);
303
311
 
304
312
  // models/react/ModelListDropdown.tsx
305
- import { ChevronDownIcon, ChevronRightIcon, SearchIcon, StarIcon, VideoIcon } from "@ai-matrx/design-system/icons";
313
+ import { ChevronDownIcon, ChevronRightIcon, PlayIcon, PlusIcon, SearchIcon, StarIcon, StopIcon, VideoIcon } from "@ai-matrx/design-system/icons";
306
314
 
307
315
  // models/succession.ts
308
316
  var MAX_SUCCESSION_HOPS = 8;
@@ -1396,7 +1404,13 @@ function ModelRow({
1396
1404
  }
1397
1405
  );
1398
1406
  }
1399
- function LocalModelRow({ model, selected, onSelect }) {
1407
+ function LocalModelRow({
1408
+ model,
1409
+ selected,
1410
+ onSelect,
1411
+ control,
1412
+ failure
1413
+ }) {
1400
1414
  const unusable = model.unusable ?? null;
1401
1415
  const dim = model.needsMoreMemory === true;
1402
1416
  return /* @__PURE__ */ jsxs2(
@@ -1437,11 +1451,23 @@ function LocalModelRow({ model, selected, onSelect }) {
1437
1451
  ),
1438
1452
  dim && /* @__PURE__ */ jsx3("span", { className: "shrink-0 rounded bg-muted px-1 text-[9px] text-muted-foreground", title: "Needs more memory than this computer has free", children: "needs memory" })
1439
1453
  ] }),
1440
- unusable && /* @__PURE__ */ jsx3("span", { "data-local-model-reason": true, className: "truncate text-[11px] text-muted-foreground", children: unusable })
1454
+ (unusable || failure) && /* @__PURE__ */ jsx3("span", { "data-local-model-reason": true, className: cn("truncate text-[11px]", failure ? "text-destructive" : "text-muted-foreground"), title: failure ?? void 0, children: failure ?? unusable })
1441
1455
  ] }),
1442
1456
  /* @__PURE__ */ jsx3("span", { className: "w-11" }),
1443
1457
  /* @__PURE__ */ jsx3("span", { className: "block w-12 whitespace-nowrap text-right font-mono text-[11px] tabular-nums text-foreground/80", children: model.contextWindow == null ? "\u2014" : fmtPointsCompact(model.contextWindow) }),
1444
- /* @__PURE__ */ jsx3("span", { className: "w-[2.75rem]" })
1458
+ /* @__PURE__ */ jsx3("span", { className: "flex w-[2.75rem] justify-end", onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: control && !unusable ? /* @__PURE__ */ jsx3(
1459
+ IconButton,
1460
+ {
1461
+ icon: control.kind === "stop" ? StopIcon : PlayIcon,
1462
+ tooltip: control.pending ? control.kind === "stop" ? "Stopping" : "Starting" : control.kind === "stop" ? "Stop" : "Start",
1463
+ "aria-label": control.kind === "stop" ? `Stop ${model.name}` : `Start ${model.name}`,
1464
+ "data-local-model-control": control.kind,
1465
+ "data-pending": control.pending ? "" : void 0,
1466
+ disabled: control.pending,
1467
+ spinning: control.pending,
1468
+ onClick: control.onPress
1469
+ }
1470
+ ) : null })
1445
1471
  ]
1446
1472
  }
1447
1473
  );
@@ -1544,7 +1570,41 @@ function ModelListDropdown({
1544
1570
  const priorityModelSet = priorityModelIds ? new Set(priorityModelIds) : null;
1545
1571
  const eligibleModels = allowedModelSet ? models.filter((model) => allowedModelSet.has(model.id)) : models;
1546
1572
  const selected = models.find((m) => m.id === value) ?? null;
1547
- const { models: localAll, groupLabel: localGroupLabel, open: openLocal } = useLocalModels(localModelsProp);
1573
+ const { models: localAll, groupLabel: localGroupLabel, open: openLocal, start: startLocal, stop: stopLocal, getMore: getMoreLocal } = useLocalModels(localModelsProp);
1574
+ const [localPending, setLocalPending] = useState(() => /* @__PURE__ */ new Set());
1575
+ const [localFailure, setLocalFailure] = useState(() => /* @__PURE__ */ new Map());
1576
+ const runLocal = (id2, action, verb) => {
1577
+ setLocalFailure((cur) => {
1578
+ if (!cur.has(id2)) return cur;
1579
+ const next = new Map(cur);
1580
+ next.delete(id2);
1581
+ return next;
1582
+ });
1583
+ setLocalPending((cur) => new Set(cur).add(id2));
1584
+ let result;
1585
+ try {
1586
+ result = action(id2);
1587
+ } catch (error2) {
1588
+ result = Promise.reject(error2);
1589
+ }
1590
+ void Promise.resolve(result).catch((error2) => {
1591
+ console.error(`[model-picker] could not ${verb} ${id2}:`, error2);
1592
+ const why = error2 instanceof Error && error2.message ? error2.message : `Could not ${verb}`;
1593
+ setLocalFailure((cur) => new Map(cur).set(id2, why));
1594
+ }).finally(
1595
+ () => setLocalPending((cur) => {
1596
+ const next = new Set(cur);
1597
+ next.delete(id2);
1598
+ return next;
1599
+ })
1600
+ );
1601
+ };
1602
+ const localControl = (m) => {
1603
+ const pending = localPending.has(m.id) || m.starting === true;
1604
+ if (m.running === true && stopLocal) return { kind: "stop", pending, onPress: () => runLocal(m.id, stopLocal, "stop") };
1605
+ if (m.running !== true && startLocal) return { kind: "start", pending, onPress: () => runLocal(m.id, startLocal, "start") };
1606
+ return null;
1607
+ };
1548
1608
  const selectedLocal = localAll.find((m) => m.id === value) ?? null;
1549
1609
  const localEligible = !modelOnly && variant === "user" && !allowedModelSet && (effectiveSelectionPurpose === "chat" || effectiveSelectionPurpose === "agent");
1550
1610
  const localRows = useMemo2(() => {
@@ -1945,11 +2005,36 @@ function ModelListDropdown({
1945
2005
  children: emptyOptionLabel
1946
2006
  }
1947
2007
  ),
1948
- localRows.length > 0 && /* @__PURE__ */ jsxs2(Fragment2, { children: [
1949
- localRunnable.length > 0 && /* @__PURE__ */ jsx3("div", { className: GROUP_HEADING, children: localGroupLabel }),
1950
- localRunnable.map((l) => /* @__PURE__ */ jsx3(LocalModelRow, { model: l, selected: l.id === value, onSelect: () => handleSelectLocal(l) }, l.id)),
2008
+ (localRows.length > 0 || localEligible && tab === "all" && !query.trim() && getMoreLocal) && /* @__PURE__ */ jsxs2(Fragment2, { children: [
2009
+ (localRunnable.length > 0 || localRows.length === 0 && getMoreLocal) && /* @__PURE__ */ jsx3("div", { className: GROUP_HEADING, children: localGroupLabel }),
2010
+ localRunnable.map((l) => /* @__PURE__ */ jsx3(LocalModelRow, { model: l, selected: l.id === value, onSelect: () => handleSelectLocal(l), control: localControl(l), failure: localFailure.get(l.id) ?? null }, l.id)),
2011
+ getMoreLocal && localEligible && tab === "all" && !query.trim() && /* @__PURE__ */ jsxs2(
2012
+ "div",
2013
+ {
2014
+ role: "button",
2015
+ tabIndex: 0,
2016
+ "data-local-get-more": "",
2017
+ onClick: () => {
2018
+ getMoreLocal();
2019
+ handleOpen(false);
2020
+ },
2021
+ onKeyDown: (e) => {
2022
+ if (e.key === "Enter" || e.key === " ") {
2023
+ e.preventDefault();
2024
+ getMoreLocal();
2025
+ handleOpen(false);
2026
+ }
2027
+ },
2028
+ className: "grid w-full cursor-pointer grid-cols-[16px_20px_minmax(0,1fr)] items-center gap-2 rounded px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground focus:bg-muted/60 focus:outline-none",
2029
+ children: [
2030
+ /* @__PURE__ */ jsx3("span", { className: "w-4" }),
2031
+ /* @__PURE__ */ jsx3("span", { className: "flex w-5 justify-center", children: /* @__PURE__ */ jsx3(PlusIcon, { className: "h-3.5 w-3.5" }) }),
2032
+ /* @__PURE__ */ jsx3("span", { className: "truncate", children: "Get More Models..." })
2033
+ ]
2034
+ }
2035
+ ),
1951
2036
  localUnusable.length > 0 && /* @__PURE__ */ jsx3("div", { className: GROUP_HEADING, children: "Cannot run here" }),
1952
- localUnusable.map((l) => /* @__PURE__ */ jsx3(LocalModelRow, { model: l, selected: false, onSelect: () => void 0 }, l.id)),
2037
+ localUnusable.map((l) => /* @__PURE__ */ jsx3(LocalModelRow, { model: l, selected: false, onSelect: () => void 0, failure: localFailure.get(l.id) ?? null }, l.id)),
1953
2038
  filtered.length > 0 && /* @__PURE__ */ jsx3("div", { className: GROUP_HEADING, children: "Cloud" })
1954
2039
  ] }),
1955
2040
  filtered.length === 0 ? localRows.length > 0 ? null : /* @__PURE__ */ jsx3("div", { className: "p-4 text-center text-xs text-muted-foreground", children: "No models match your filters." }) : listRows.map(({ model: m, classTier }) => /* @__PURE__ */ jsx3(
@@ -568,11 +568,20 @@ interface LocalModelEntry {
568
568
  * picker lists it under "Cannot run here" with this reason, dimmed and never pickable.
569
569
  */
570
570
  unusable?: string;
571
+ /**
572
+ * The model is loaded and answering now. With a port `stop`, the row draws Stop; with `start`
573
+ * and not running, Start. Absent = the host does not say (no control is drawn).
574
+ */
575
+ running?: boolean;
576
+ /** The host is loading it right now: the row's control shows its pending state. */
577
+ starting?: boolean;
571
578
  }
572
579
  /**
573
580
  * The host's local-model source (a store: `getModels` + `subscribe`, like the catalog). The picker
574
- * draws its models as one "On this computer" group marked local. Start/Stop and getting more
575
- * models stay the host's own screens.
581
+ * draws its models as one "On this computer" group marked local. Starting and stopping a model and
582
+ * getting more of them are capabilities by presence: a host that supplies `start` / `stop` / `getMore`
583
+ * gets a compact Start/Stop control per row and one "Get More Models..." row; a host that leaves them
584
+ * out gets neither (its own screens stay the only way).
576
585
  */
577
586
  interface LocalModelsPort {
578
587
  /** Must return the SAME array until the list changes (it is a store snapshot). */
@@ -587,6 +596,15 @@ interface LocalModelsPort {
587
596
  * the id through `onValueChange` (the override path).
588
597
  */
589
598
  open?(id: string): void;
599
+ /**
600
+ * Start the model (load it so it can answer). Present = a row that is not running draws Start. The
601
+ * row shows a pending state until the returned promise settles; a rejection says why on the row.
602
+ */
603
+ start?(id: string): void | Promise<unknown>;
604
+ /** Stop the running model. Present = a running row draws Stop (same pending state and failure line). */
605
+ stop?(id: string): void | Promise<unknown>;
606
+ /** Open the host's place to find and install more models. Present = one "Get More Models..." row ends the group. */
607
+ getMore?(): void;
590
608
  }
591
609
  /**
592
610
  * How the person may reach a cloud model in THIS place (a chat composer passes it to its own picker,
@@ -568,11 +568,20 @@ interface LocalModelEntry {
568
568
  * picker lists it under "Cannot run here" with this reason, dimmed and never pickable.
569
569
  */
570
570
  unusable?: string;
571
+ /**
572
+ * The model is loaded and answering now. With a port `stop`, the row draws Stop; with `start`
573
+ * and not running, Start. Absent = the host does not say (no control is drawn).
574
+ */
575
+ running?: boolean;
576
+ /** The host is loading it right now: the row's control shows its pending state. */
577
+ starting?: boolean;
571
578
  }
572
579
  /**
573
580
  * The host's local-model source (a store: `getModels` + `subscribe`, like the catalog). The picker
574
- * draws its models as one "On this computer" group marked local. Start/Stop and getting more
575
- * models stay the host's own screens.
581
+ * draws its models as one "On this computer" group marked local. Starting and stopping a model and
582
+ * getting more of them are capabilities by presence: a host that supplies `start` / `stop` / `getMore`
583
+ * gets a compact Start/Stop control per row and one "Get More Models..." row; a host that leaves them
584
+ * out gets neither (its own screens stay the only way).
576
585
  */
577
586
  interface LocalModelsPort {
578
587
  /** Must return the SAME array until the list changes (it is a store snapshot). */
@@ -587,6 +596,15 @@ interface LocalModelsPort {
587
596
  * the id through `onValueChange` (the override path).
588
597
  */
589
598
  open?(id: string): void;
599
+ /**
600
+ * Start the model (load it so it can answer). Present = a row that is not running draws Start. The
601
+ * row shows a pending state until the returned promise settles; a rejection says why on the row.
602
+ */
603
+ start?(id: string): void | Promise<unknown>;
604
+ /** Stop the running model. Present = a running row draws Stop (same pending state and failure line). */
605
+ stop?(id: string): void | Promise<unknown>;
606
+ /** Open the host's place to find and install more models. Present = one "Get More Models..." row ends the group. */
607
+ getMore?(): void;
590
608
  }
591
609
  /**
592
610
  * How the person may reach a cloud model in THIS place (a chat composer passes it to its own picker,
@@ -244,7 +244,7 @@ async function readAgentCurrentVersion(client, agentId) {
244
244
  return typeof v === "number" && Number.isInteger(v) ? v : null;
245
245
  }
246
246
  async function declareContractBreak(client, query) {
247
- const { error } = await client.rpc("agx_declare_contract_break", {
247
+ const { error } = await client.schema("agent").rpc("agx_declare_contract_break", {
248
248
  p_agent_id: query.agentId,
249
249
  p_version_number: query.version,
250
250
  p_kind: query.kind ?? void 0
@@ -135,6 +135,7 @@ type Answer = PromiseLike<{
135
135
  interface VersionsClient {
136
136
  rpc(fn: string, args: Record<string, unknown>): Answer;
137
137
  schema(name: "agent"): {
138
+ rpc(fn: string, args: Record<string, unknown>): Answer;
138
139
  from(table: "definition_version" | "definition"): {
139
140
  select(columns: string): {
140
141
  eq(column: string, value: string): {
@@ -135,6 +135,7 @@ type Answer = PromiseLike<{
135
135
  interface VersionsClient {
136
136
  rpc(fn: string, args: Record<string, unknown>): Answer;
137
137
  schema(name: "agent"): {
138
+ rpc(fn: string, args: Record<string, unknown>): Answer;
138
139
  from(table: "definition_version" | "definition"): {
139
140
  select(columns: string): {
140
141
  eq(column: string, value: string): {
@@ -201,7 +201,7 @@ async function readAgentCurrentVersion(client, agentId) {
201
201
  return typeof v === "number" && Number.isInteger(v) ? v : null;
202
202
  }
203
203
  async function declareContractBreak(client, query) {
204
- const { error } = await client.rpc("agx_declare_contract_break", {
204
+ const { error } = await client.schema("agent").rpc("agx_declare_contract_break", {
205
205
  p_agent_id: query.agentId,
206
206
  p_version_number: query.version,
207
207
  p_kind: query.kind ?? void 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/agents",
3
- "version": "0.59.238",
3
+ "version": "0.59.240",
4
4
  "description": "Portable AI Matrx agent stream protocol, event projection, and safe result-presentation primitives.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -561,5 +561,5 @@
561
561
  "verify:tarball": "node ./scripts/verify-tarball.mjs",
562
562
  "typegen:self-test": "node scripts/typegen/drop-guard.self-test.mjs && node scripts/typegen/check-api-types-fresh.mjs --self-test"
563
563
  },
564
- "gitHead": "9df14659778c1a3b93c2c09bddcdc93b145ca736"
564
+ "gitHead": "c3acf23378ea3cfd81a456efb31c6db0b65e89ef"
565
565
  }