@ai-matrx/agents 0.59.239 → 0.59.241

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,10 +1,15 @@
1
1
  # Changelog
2
2
 
3
- ## 0.59.239
3
+ ## 0.59.241
4
4
 
5
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
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
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
+
8
13
  ## 0.59.238
9
14
 
10
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`.
@@ -310,6 +310,10 @@ function onRequestReason(row) {
310
310
  if (row.max_inline_chars === 0) return "Never inline \u2014 the agent reads it on request";
311
311
  return "Over the limit \u2014 the agent reads it on request";
312
312
  }
313
+ function offByDefaultReason(row) {
314
+ if (row.include || row.decided_by.include !== "page") return null;
315
+ return row.layers?.surface?.auto_context === false ? "Off by default on this page" : null;
316
+ }
313
317
 
314
318
  // context/react/InlineMaxInput.tsx
315
319
  var import_jsx_runtime2 = require("react/jsx-runtime");
@@ -666,7 +670,8 @@ function ContextRuleRow({
666
670
  trail
667
671
  }) {
668
672
  const selfChecked = row.blockedBy === "self_check";
669
- const decided = selfChecked ? SELF_CHECK_TOOLTIP : row.serverResolved ? SERVER_RESOLVED_TOOLTIP : `Include: ${LAYER_LABEL[row.decided_by.include]} \xB7 Limit: ${LAYER_LABEL[row.decided_by.max_inline_chars]}`;
673
+ const decided = selfChecked ? SELF_CHECK_TOOLTIP : row.serverResolved ? SERVER_RESOLVED_TOOLTIP : offByDefaultReason(row) ?? `Include: ${LAYER_LABEL[row.decided_by.include]} \xB7 Limit: ${LAYER_LABEL[row.decided_by.max_inline_chars]}`;
674
+ const shownTrail = trail ?? offByDefaultReason(row);
670
675
  const labelNode = onOpenRow ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
671
676
  "button",
672
677
  {
@@ -710,7 +715,7 @@ function ContextRuleRow({
710
715
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "font-normal text-muted-foreground", children: decided })
711
716
  ] })
712
717
  ] }),
713
- trail ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "min-w-0 max-w-[45%] shrink truncate text-muted-foreground", children: trail }) : null
718
+ shownTrail ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "min-w-0 max-w-[45%] shrink truncate text-muted-foreground", children: shownTrail }) : null
714
719
  ]
715
720
  }
716
721
  ),
@@ -280,6 +280,10 @@ function onRequestReason(row) {
280
280
  if (row.max_inline_chars === 0) return "Never inline \u2014 the agent reads it on request";
281
281
  return "Over the limit \u2014 the agent reads it on request";
282
282
  }
283
+ function offByDefaultReason(row) {
284
+ if (row.include || row.decided_by.include !== "page") return null;
285
+ return row.layers?.surface?.auto_context === false ? "Off by default on this page" : null;
286
+ }
283
287
 
284
288
  // context/react/InlineMaxInput.tsx
285
289
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -636,7 +640,8 @@ function ContextRuleRow({
636
640
  trail
637
641
  }) {
638
642
  const selfChecked = row.blockedBy === "self_check";
639
- const decided = selfChecked ? SELF_CHECK_TOOLTIP : row.serverResolved ? SERVER_RESOLVED_TOOLTIP : `Include: ${LAYER_LABEL[row.decided_by.include]} \xB7 Limit: ${LAYER_LABEL[row.decided_by.max_inline_chars]}`;
643
+ const decided = selfChecked ? SELF_CHECK_TOOLTIP : row.serverResolved ? SERVER_RESOLVED_TOOLTIP : offByDefaultReason(row) ?? `Include: ${LAYER_LABEL[row.decided_by.include]} \xB7 Limit: ${LAYER_LABEL[row.decided_by.max_inline_chars]}`;
644
+ const shownTrail = trail ?? offByDefaultReason(row);
640
645
  const labelNode = onOpenRow ? /* @__PURE__ */ jsx3(
641
646
  "button",
642
647
  {
@@ -680,7 +685,7 @@ function ContextRuleRow({
680
685
  /* @__PURE__ */ jsx3("div", { className: "font-normal text-muted-foreground", children: decided })
681
686
  ] })
682
687
  ] }),
683
- trail ? /* @__PURE__ */ jsx3("span", { className: "min-w-0 max-w-[45%] shrink truncate text-muted-foreground", children: trail }) : null
688
+ shownTrail ? /* @__PURE__ */ jsx3("span", { className: "min-w-0 max-w-[45%] shrink truncate text-muted-foreground", children: shownTrail }) : null
684
689
  ]
685
690
  }
686
691
  ),
@@ -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)(
@@ -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;
@@ -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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/agents",
3
- "version": "0.59.239",
3
+ "version": "0.59.241",
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",
@@ -509,13 +509,13 @@
509
509
  "sideEffects": false,
510
510
  "dependencies": {
511
511
  "@ai-matrx/alchemy": "latest",
512
- "@ai-matrx/associations": "latest",
513
512
  "@ai-matrx/api": "latest",
514
- "@ai-matrx/content-ir": "latest",
515
- "@ai-matrx/data": "latest",
513
+ "@ai-matrx/associations": "latest",
516
514
  "@ai-matrx/design-system": "latest",
517
- "@ai-matrx/diff": "latest",
515
+ "@ai-matrx/data": "latest",
516
+ "@ai-matrx/content-ir": "latest",
518
517
  "@ai-matrx/icons": "latest",
518
+ "@ai-matrx/diff": "latest",
519
519
  "@ai-matrx/kit": "latest"
520
520
  },
521
521
  "peerDependencies": {
@@ -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": "6acfc6b938bd3174a9d71a2368c8f2f7c5ceb976"
564
+ "gitHead": "a53838052abf9a4a70364e27933ecdf4b05a0b40"
565
565
  }