@ai-matrx/chat 0.3.5 → 0.3.6

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
  # @ai-matrx/chat
2
2
 
3
+ ## 0.3.6 — 2026-10-07
4
+ ### Re-sync before matrx-frontend consumes the published package (P27)
5
+
6
+ - `src/surfaces/lib/ui-surfaces-agent-writes` + `ui-surfaces-scope` ship at last: the repo-root `lib/`
7
+ ignore (Python) kept them out of every commit since 0.3.1; `apps/shared/chat/.gitignore` re-includes
8
+ `src/**/lib/`.
9
+ - Carries every matrx-frontend `packages/chat` edit after `62656d3a69` (begin-fresh chat, agent-definition
10
+ `markFieldSaved`, tool-state effects/registry, ALC-19 copy results).
11
+ - Consumer action: none.
12
+
3
13
  ## 0.3.5 — 2026-10-07
4
14
  ### Chat mode opens the agent itself; Duplicate opens the copy in a new tab
5
15
 
@@ -18,8 +18,13 @@ import type { ComposerMode } from "../inputs/smart-input/composer/composer-types
18
18
  *
19
19
  * The quick actions are the `agents.chat_composer.quick_actions` knob.
20
20
  */
21
- export declare function ChatNewClient({ agentId, composer, }: {
21
+ export declare function ChatNewClient({ agentId, pinned, composer, }: {
22
22
  agentId: string | null;
23
+ /**
24
+ * `/chat/new?agent=<id>`: the person named the agent, so it answers directly
25
+ * and the default-chat mandate neither seeds nor overrides it.
26
+ */
27
+ pinned?: boolean;
23
28
  /** The three-mode composer (server-read "last mode used" cookie). */
24
29
  composer: {
25
30
  initialMode: ComposerMode | null;
@@ -18,9 +18,15 @@ import { asClause } from "@ai-matrx/kit/text";
18
18
  import { WorkspaceGate } from "../../../host/ui-slots.js";
19
19
  function ChatNewClient({
20
20
  agentId,
21
+ pinned = false,
21
22
  composer
22
23
  }) {
23
- return agentId ? /* @__PURE__ */ jsx(ChatNewBody, { agentId, composer }) : /* @__PURE__ */ jsx(ChatNewClientResolved, { composer });
24
+ const { mandate } = useMandate(DEFAULT_NEW_CHAT_MANDATE_KEY, { optional: true });
25
+ if (pinned && agentId) {
26
+ return /* @__PURE__ */ jsx(ChatNewBody, { agentId, composer, pinned: true });
27
+ }
28
+ const effectiveAgentId = mandate?.agentId ?? agentId;
29
+ return effectiveAgentId ? /* @__PURE__ */ jsx(ChatNewBody, { agentId: effectiveAgentId, composer }) : /* @__PURE__ */ jsx(ChatNewClientResolved, { composer });
24
30
  }
25
31
  function ChatNewClientResolved({
26
32
  composer
@@ -62,14 +68,15 @@ function ChatMandateUnavailable({ error }) {
62
68
  }
63
69
  function ChatNewBody({
64
70
  agentId,
65
- composer
71
+ composer,
72
+ pinned = false
66
73
  }) {
67
74
  const surfaceKey = chatRouteSurfaceKey(agentId);
68
75
  return /* @__PURE__ */ jsx(
69
76
  ChatRoomClient,
70
77
  {
71
78
  agentId,
72
- mandateKey: DEFAULT_NEW_CHAT_MANDATE_KEY,
79
+ mandateKey: pinned ? void 0 : DEFAULT_NEW_CHAT_MANDATE_KEY,
73
80
  composer,
74
81
  landingContent: (conversationId, composerPresentation) => composerPresentation ? /* @__PURE__ */ jsx(
75
82
  NewChatGreeting,
@@ -50,6 +50,8 @@ export declare function stageChatAgentSwitch({ dispatch, router, getState, targe
50
50
  }): void;
51
51
  /** Catch package-owned chat links before they bypass the same-tab handoff. */
52
52
  export declare function interceptChatAgentLink(event: ReactMouseEvent<HTMLElement>, args: Omit<Parameters<typeof stageChatAgentSwitch>[0], "targetAgentId" | "href">): void;
53
+ /** How long "New chat" waits for the default-agent lookup before navigating anyway. */
54
+ export declare const DEFAULT_AGENT_LOOKUP_WAIT_MS = 1500;
53
55
  /** Start a brand-new chat: drop stale surface focus, bump the fresh-session
54
56
  * nonce (so `/chat/new` remints even when the path is unchanged), navigate.
55
57
  *
@@ -92,6 +92,22 @@ function interceptChatAgentLink(event, args) {
92
92
  href
93
93
  });
94
94
  }
95
+ const DEFAULT_AGENT_LOOKUP_WAIT_MS = 1500;
96
+ async function resolveDefaultAgentBounded() {
97
+ let timer;
98
+ const lookup = resolveMandateAsking(DEFAULT_NEW_CHAT_MANDATE_KEY).then(
99
+ (resolved) => resolved.agentId
100
+ );
101
+ lookup.catch(() => void 0);
102
+ const wait = new Promise((resolve) => {
103
+ timer = setTimeout(() => resolve(null), DEFAULT_AGENT_LOOKUP_WAIT_MS);
104
+ });
105
+ try {
106
+ return await Promise.race([lookup, wait]);
107
+ } finally {
108
+ clearTimeout(timer);
109
+ }
110
+ }
95
111
  async function beginFreshChat({
96
112
  dispatch,
97
113
  router,
@@ -103,7 +119,7 @@ async function beginFreshChat({
103
119
  const auth = state.userAuth;
104
120
  if (auth && auth.id !== null && !auth.isAnonymous) {
105
121
  try {
106
- defaultAgentId = (await resolveMandateAsking(DEFAULT_NEW_CHAT_MANDATE_KEY)).agentId;
122
+ defaultAgentId = await resolveDefaultAgentBounded();
107
123
  } catch (error) {
108
124
  if (isOrganizationSelectionCancelled(error)) return;
109
125
  console.error(
@@ -125,6 +141,7 @@ async function beginFreshChat({
125
141
  router.push(getFreshChatHref(activeAgentId, defaultAgentId));
126
142
  }
127
143
  export {
144
+ DEFAULT_AGENT_LOOKUP_WAIT_MS,
128
145
  beginFreshChat,
129
146
  chatRouteSurfaceKey,
130
147
  getFreshChatHref,
@@ -41,7 +41,7 @@ function ComposerAgentPeek({ agentId, onDone }) {
41
41
  AgentDetailCard,
42
42
  {
43
43
  agent,
44
- actions: /* @__PURE__ */ jsx(AgentPeekDuplicateButton, { agentId, onDuplicated: onDone })
44
+ actions: /* @__PURE__ */ jsx(AgentPeekDuplicateButton, { agentId, onStart: onDone })
45
45
  }
46
46
  );
47
47
  }
@@ -1,5 +1,8 @@
1
- export declare function AgentPeekDuplicateButton({ agentId, onDuplicated, }: {
1
+ export declare function AgentPeekDuplicateButton({ agentId, onStart, }: {
2
2
  agentId: string;
3
- /** Close the peek once the copy opens. */
4
- onDuplicated?: () => void;
3
+ /**
4
+ * Close the peek as the copy starts: the organization question (when none is
5
+ * chosen) and the new tab must never open behind a menu.
6
+ */
7
+ onStart?: () => void;
5
8
  }): import("react").JSX.Element | null;
@@ -13,7 +13,7 @@ import { selectAgentById, selectAgentReadyForBuilder } from "../../redux/agent-d
13
13
  import { agentGoHref } from "../../addressing/agentAddress.js";
14
14
  function AgentPeekDuplicateButton({
15
15
  agentId,
16
- onDuplicated
16
+ onStart
17
17
  }) {
18
18
  const dispatch = useAppDispatch();
19
19
  const userId = useAppSelector(selectUserId);
@@ -27,20 +27,21 @@ function AgentPeekDuplicateButton({
27
27
  const duplicate = async () => {
28
28
  if (busy) return;
29
29
  setBusy(true);
30
- const tab = window.open("about:blank", "_blank");
30
+ onStart?.();
31
31
  try {
32
32
  const newId = await dispatch(door.duplicateAgent(agentId)).unwrap();
33
33
  const href = agentGoHref(newId, "/build");
34
+ const tab = window.open(href, "_blank");
34
35
  if (tab) {
35
36
  tab.opener = null;
36
- tab.location.href = href;
37
+ toast.success(`Duplicated "${record.name}" \u2014 opened in a new tab`);
37
38
  } else {
38
- window.open(href, "_blank", "noopener");
39
+ toast.success(`Duplicated "${record.name}"`, {
40
+ description: "Your browser kept the new tab closed",
41
+ action: { label: "Open", onClick: () => window.open(href, "_blank", "noopener") }
42
+ });
39
43
  }
40
- toast.success(`Duplicated "${record.name}" \u2014 opened in a new tab`);
41
- onDuplicated?.();
42
44
  } catch (err) {
43
- tab?.close();
44
45
  if (!isOrganizationSelectionCancelled(err)) {
45
46
  toast.error("Could not duplicate agent", { description: getUserMessage(err) });
46
47
  }
@@ -121,6 +121,18 @@ export declare const agentDefinitionSlice: import("@reduxjs/toolkit").Slice<Agen
121
121
  markAgentSaved(state: AgentDefinitionSliceState, action: PayloadAction<{
122
122
  id: string;
123
123
  }>): void;
124
+ /**
125
+ * A SINGLE field was written to the database (inline edits such as the
126
+ * Agent Info Editor's name). Only that field becomes the clean baseline —
127
+ * every other staged edit (e.g. an Output Schema kind binding applied in
128
+ * Model Settings) must stay dirty so the main Save still persists it.
129
+ * `markAgentSaved` here used to wipe every dirty flag, silently dropping
130
+ * unrelated staged edits (feedback 222a2925).
131
+ */
132
+ markAgentFieldSaved(state: AgentDefinitionSliceState, action: PayloadAction<{
133
+ id: string;
134
+ field: keyof AgentDefinition;
135
+ }>): void;
124
136
  /** Save failed — restore from the snapshot taken before the optimistic write. */
125
137
  rollbackAgentOptimisticUpdate(state: AgentDefinitionSliceState, action: PayloadAction<{
126
138
  id: string;
@@ -227,7 +239,10 @@ export declare const upsertAgent: import("@reduxjs/toolkit").ActionCreatorWithPa
227
239
  id: string;
228
240
  }, "agentDefinition/resetAllAgentFields">, markAgentSaved: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
229
241
  id: string;
230
- }, "agentDefinition/markAgentSaved">, rollbackAgentOptimisticUpdate: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
242
+ }, "agentDefinition/markAgentSaved">, markAgentFieldSaved: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
243
+ id: string;
244
+ field: keyof AgentDefinition;
245
+ }, "agentDefinition/markAgentFieldSaved">, rollbackAgentOptimisticUpdate: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
231
246
  id: string;
232
247
  snapshot: FieldSnapshot;
233
248
  }, "agentDefinition/rollbackAgentOptimisticUpdate">, undoAgentEdit: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
@@ -493,6 +493,24 @@ const agentDefinitionSlice = createSlice({
493
493
  if (!record) return;
494
494
  markRecordClean(record);
495
495
  },
496
+ /**
497
+ * A SINGLE field was written to the database (inline edits such as the
498
+ * Agent Info Editor's name). Only that field becomes the clean baseline —
499
+ * every other staged edit (e.g. an Output Schema kind binding applied in
500
+ * Model Settings) must stay dirty so the main Save still persists it.
501
+ * `markAgentSaved` here used to wipe every dirty flag, silently dropping
502
+ * unrelated staged edits (feedback 222a2925).
503
+ */
504
+ markAgentFieldSaved(state, action) {
505
+ const { id, field } = action.payload;
506
+ const record = state.agents[id];
507
+ if (!record) return;
508
+ removeField(record._dirtyFields, field);
509
+ delete record._fieldHistory[field];
510
+ record._undoPast = record._undoPast.filter((e) => e.field !== field);
511
+ record._undoFuture = record._undoFuture.filter((e) => e.field !== field);
512
+ record._dirty = fieldFlagsSize(record._dirtyFields) > 0;
513
+ },
496
514
  /** Save failed — restore from the snapshot taken before the optimistic write. */
497
515
  rollbackAgentOptimisticUpdate(state, action) {
498
516
  const { id, snapshot } = action.payload;
@@ -636,6 +654,7 @@ const {
636
654
  resetAgentField,
637
655
  resetAllAgentFields,
638
656
  markAgentSaved,
657
+ markAgentFieldSaved,
639
658
  rollbackAgentOptimisticUpdate,
640
659
  undoAgentEdit,
641
660
  redoAgentEdit,
@@ -655,6 +674,7 @@ export {
655
674
  agentDefinitionSlice,
656
675
  clearAgentUndoHistory,
657
676
  slice_default as default,
677
+ markAgentFieldSaved,
658
678
  markAgentFieldsLoaded,
659
679
  markAgentSaved,
660
680
  mergePartialAgent,
@@ -564,6 +564,32 @@ export interface LinkedAgentRef {
564
564
  * silently reported as "not linked".
565
565
  */
566
566
  deletedAt: string | null;
567
+ /** Archived agents are not offered as a sync partner. */
568
+ isArchived: boolean;
569
+ /**
570
+ * Who owns it, from the viewer's seat: "me", a person, an organization (an
571
+ * org-visible agent of someone else in an org the viewer can read) or the
572
+ * system. Drives every owner word in Linked Agent Sync.
573
+ */
574
+ ownerKind: "me" | "person" | "organization" | "system";
575
+ /** Person display name or organization name; null when not readable ("Someone else"). */
576
+ ownerName: string | null;
577
+ /** The viewer's level from `agx_get_access_level` (owner/admin/editor/viewer/public/none). */
578
+ accessLevel: string;
579
+ /**
580
+ * The builder's own save rule for a user agent: owner or editor. Always false
581
+ * for a system agent — that write is the registered admin feature, decided by
582
+ * the panel from `agent.system-sync`.
583
+ */
584
+ canEdit: boolean;
585
+ }
586
+ /**
587
+ * The agent this one was made from, when it still exists by id but the viewer
588
+ * can no longer open it (unshared, made private, archived, or deleted).
589
+ */
590
+ export interface LostLinkedSource {
591
+ id: string;
592
+ reason: "unreadable" | "archived" | "deleted";
567
593
  }
568
594
  /**
569
595
  * Resolved linkage around a single agent. `source` is what it was copied from;
@@ -574,8 +600,10 @@ export interface LinkedCounterpartResult {
574
600
  self: LinkedAgentRef;
575
601
  source: LinkedAgentRef | null;
576
602
  derived: LinkedAgentRef[];
603
+ /** Set when `self.sourceAgentId` names an agent the viewer can no longer open. */
604
+ lostSource: LostLinkedSource | null;
577
605
  }
578
- /** Result of `createPersonalCopy` — idempotent personal copy of a system agent. */
606
+ /** Result of `createPersonalCopy` — idempotent personal copy of any agent the viewer can read. */
579
607
  export interface PersonalCopyResult {
580
608
  agentId: string;
581
609
  /** True when a personal copy already existed and was returned instead of created. */
@@ -410,7 +410,7 @@ const loadCodeEditHistoryThunk = slotFn("loadCodeEditHistoryThunk");
410
410
  const applySkillStreamEvent = slotFn("applySkillStreamEvent");
411
411
  const isSkillStreamEvent = slotFn("isSkillStreamEvent", () => false);
412
412
  const materializeMessageArtifacts = slotFn("materializeMessageArtifacts");
413
- const reconcileMessagesArtifacts = slotFn("reconcileMessagesArtifacts", () => void 0);
413
+ const reconcileMessagesArtifacts = slotFn("reconcileMessagesArtifacts", async () => void 0);
414
414
  const noteBrowserActivity = slotFn("noteBrowserActivity");
415
415
  const selectCloudBrowserRunLive = slotFn("selectCloudBrowserRunLive");
416
416
  const adoptCloudBrowserRunFromStream = slotFn("adoptCloudBrowserRunFromStream");
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Pure parsers for the UI Surfaces registry's agent write targets
3
+ * (`create_surfaces` / `update_surfaces` / `delete_surfaces` on
4
+ * `matrx-admin/ui-surfaces`). Each reads the agent's WHOLE list, reports every
5
+ * problem at once, and returns plans the page saves through its own service
6
+ * functions. No I/O here — unit-tested in `__tests__/ui-surfaces-agent-writes.test.ts`.
7
+ */
8
+ export declare const DEFAULT_PARENT = "matrx-default/default";
9
+ export declare const DEFAULT_SORT_ORDER = 150;
10
+ /** What a parser needs to know about one existing surface. */
11
+ export interface ExistingSurface {
12
+ name: string;
13
+ has_manifest: boolean;
14
+ }
15
+ export interface SurfaceWriteContext {
16
+ existing: readonly ExistingSurface[];
17
+ clientNames: readonly string[];
18
+ }
19
+ export interface CreateSurfacePlan {
20
+ name: string;
21
+ client_name: string;
22
+ label: string | null;
23
+ description: string;
24
+ parent_surface_name: string | null;
25
+ sort_order: number;
26
+ is_active: boolean;
27
+ executor_name: string | null;
28
+ url_pattern: string | null;
29
+ }
30
+ export interface UpdateSurfacePatch {
31
+ description?: string | null;
32
+ parent_surface_name?: string | null;
33
+ sort_order?: number;
34
+ is_active?: boolean;
35
+ executor_name?: string | null;
36
+ url_pattern?: string | null;
37
+ }
38
+ export interface UpdateSurfacePlan {
39
+ name: string;
40
+ patch: UpdateSurfacePatch;
41
+ changed: string[];
42
+ }
43
+ export declare function parseCreateSurfacesValue(value: unknown, ctx: SurfaceWriteContext): CreateSurfacePlan[];
44
+ export declare function parseUpdateSurfacesValue(value: unknown, ctx: SurfaceWriteContext): UpdateSurfacePlan[];
45
+ export declare function parseDeleteSurfacesValue(value: unknown, ctx: SurfaceWriteContext): ExistingSurface[];
46
+ /** Fields the New surface dialog holds — the `new_surface_draft` target. */
47
+ export interface NewSurfaceDraftFields {
48
+ client?: string;
49
+ local?: string;
50
+ /** A surface name, or null for a root surface. */
51
+ parent_surface_name?: string | null;
52
+ /** A tier label: Pages | Specialized | Overlays | Editor variants | Debug. */
53
+ tier?: string;
54
+ description?: string;
55
+ }
56
+ export declare const NEW_SURFACE_TIERS: readonly ["Pages", "Specialized", "Overlays", "Editor variants", "Debug"];
57
+ /**
58
+ * Read the `new_surface_draft` value: an OBJECT with any of { name, parent_surface_name,
59
+ * tier, description }. Every problem is reported at once; nothing is saved.
60
+ */
61
+ export declare function parseNewSurfaceDraftValue(value: unknown, ctx: SurfaceWriteContext): NewSurfaceDraftFields;
@@ -0,0 +1,305 @@
1
+ import {
2
+ collectProblems,
3
+ ListLevelProblem,
4
+ ProblemList,
5
+ readCollectionList,
6
+ repeatsProblem
7
+ } from "../runtime/collection-write-targets.js";
8
+ const DEFAULT_PARENT = "matrx-default/default";
9
+ const DEFAULT_SORT_ORDER = 150;
10
+ const LOCAL_RE = /^[a-z0-9][a-z0-9-/]*$/;
11
+ const UPDATE_FIELDS = [
12
+ "description",
13
+ "parent_surface_name",
14
+ "sort_order",
15
+ "is_active",
16
+ "executor_name",
17
+ "url_pattern"
18
+ ];
19
+ const MANIFEST_OWNED = /* @__PURE__ */ new Set([
20
+ "description",
21
+ "parent_surface_name",
22
+ "is_active",
23
+ "url_pattern"
24
+ ]);
25
+ function asObject(raw) {
26
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
27
+ throw new Error("must be an object.");
28
+ return raw;
29
+ }
30
+ function optionalText(obj, key, problems, nullable) {
31
+ if (!(key in obj)) return void 0;
32
+ const v = obj[key];
33
+ if (v === null && nullable) return null;
34
+ if (typeof v !== "string") {
35
+ problems.add(`${key} must be a string${nullable ? " or null" : ""}.`);
36
+ return void 0;
37
+ }
38
+ return v.trim();
39
+ }
40
+ function readSortOrder(obj, problems) {
41
+ if (!("sort_order" in obj)) return void 0;
42
+ const v = obj.sort_order;
43
+ if (typeof v !== "number" || !Number.isInteger(v) || v < 100) {
44
+ problems.add(
45
+ "sort_order must be a whole number of at least 100 (0-99 is reserved)."
46
+ );
47
+ return void 0;
48
+ }
49
+ return v;
50
+ }
51
+ function readParent(obj, names, problems, self) {
52
+ const parent = optionalText(obj, "parent_surface_name", problems, true);
53
+ if (typeof parent === "string") {
54
+ if (!parent) return null;
55
+ if (parent === self) {
56
+ problems.add("a surface cannot be its own parent.");
57
+ return void 0;
58
+ }
59
+ if (!names.has(parent)) {
60
+ problems.add(`parent_surface_name "${parent}" is not a surface in the registry.`);
61
+ return void 0;
62
+ }
63
+ }
64
+ return parent;
65
+ }
66
+ function nameOfRaw(raw) {
67
+ if (typeof raw === "string") return raw;
68
+ if (raw && typeof raw === "object" && typeof raw.name === "string")
69
+ return raw.name;
70
+ return void 0;
71
+ }
72
+ function parseCreateSurfacesValue(value, ctx) {
73
+ const target = "create_surfaces";
74
+ const list = readCollectionList(target, "surfaces", value);
75
+ const names = new Set(ctx.existing.map((s) => s.name));
76
+ const clients = new Set(ctx.clientNames);
77
+ return collectProblems(
78
+ target,
79
+ list,
80
+ (raw) => {
81
+ const obj = asObject(raw);
82
+ const problems = new ProblemList(target);
83
+ const name = typeof obj.name === "string" ? obj.name.trim() : "";
84
+ let client = "";
85
+ if (!name) problems.add('name is required ("<client>/<local>").');
86
+ else {
87
+ const slash = name.indexOf("/");
88
+ client = slash > 0 ? name.slice(0, slash) : "";
89
+ const local = slash > 0 ? name.slice(slash + 1) : "";
90
+ if (!client || !local)
91
+ problems.add(`name "${name}" must be "<client>/<local>".`);
92
+ else {
93
+ if (!clients.has(client))
94
+ problems.add(
95
+ `client "${client}" is not a UI client (one of: ${[...clients].join(", ")}).`
96
+ );
97
+ if (!LOCAL_RE.test(local))
98
+ problems.add(
99
+ `the local part "${local}" may use only lowercase letters, digits, hyphens and slashes.`
100
+ );
101
+ }
102
+ }
103
+ const label = optionalText(obj, "label", problems, true);
104
+ const description = optionalText(obj, "description", problems, true);
105
+ const parent = readParent(obj, names, problems);
106
+ const sort = readSortOrder(obj, problems);
107
+ const executor = optionalText(obj, "executor_name", problems, true);
108
+ const url = optionalText(obj, "url_pattern", problems, true);
109
+ let isActive = true;
110
+ if ("is_active" in obj) {
111
+ if (typeof obj.is_active !== "boolean")
112
+ problems.add("is_active must be true or false.");
113
+ else isActive = obj.is_active;
114
+ }
115
+ problems.throwIfAny();
116
+ return {
117
+ name,
118
+ client_name: client,
119
+ label: label || null,
120
+ description: description ?? "",
121
+ parent_surface_name: parent === void 0 ? DEFAULT_PARENT : parent,
122
+ sort_order: sort ?? DEFAULT_SORT_ORDER,
123
+ is_active: isActive,
124
+ executor_name: executor || null,
125
+ url_pattern: url || null
126
+ };
127
+ },
128
+ {
129
+ nameOf: nameOfRaw,
130
+ listChecks: (items) => [
131
+ repeatsProblem(target, items.map((i) => i.name), "name"),
132
+ ...items.filter((i) => i.name && names.has(i.name)).map(
133
+ (i) => `${target}[${i.index}] "${i.name}" already exists in the registry \u2014 use update_surfaces to change it.`
134
+ )
135
+ ]
136
+ }
137
+ );
138
+ }
139
+ function parseUpdateSurfacesValue(value, ctx) {
140
+ const target = "update_surfaces";
141
+ const list = readCollectionList(target, "surfaces", value);
142
+ const byName = new Map(ctx.existing.map((s) => [s.name, s]));
143
+ const names = new Set(byName.keys());
144
+ return collectProblems(
145
+ target,
146
+ list,
147
+ (raw) => {
148
+ const obj = asObject(raw);
149
+ const name = typeof obj.name === "string" ? obj.name.trim() : "";
150
+ if (!name) throw new Error("name is required (from surfaces).");
151
+ const current = byName.get(name);
152
+ if (!current)
153
+ throw new ListLevelProblem(
154
+ `${target}: "${name}" is not a surface in the registry.`
155
+ );
156
+ const problems = new ProblemList(target);
157
+ const unknown = Object.keys(obj).filter(
158
+ (k) => k !== "name" && !UPDATE_FIELDS.includes(k)
159
+ );
160
+ if (unknown.length)
161
+ problems.add(
162
+ `cannot change ${unknown.join(", ")} here (changeable: ${UPDATE_FIELDS.join(", ")}).`
163
+ );
164
+ if (current.has_manifest) {
165
+ const owned = Object.keys(obj).filter((k) => MANIFEST_OWNED.has(k));
166
+ if (owned.length)
167
+ problems.add(
168
+ `"${name}" has a code manifest, which owns ${owned.join(", ")} \u2014 Sync manifests would overwrite the change. Change the manifest in code; here only sort_order and executor_name may change.`
169
+ );
170
+ }
171
+ const patch = {};
172
+ const description = optionalText(obj, "description", problems, true);
173
+ if (description !== void 0) patch.description = description || null;
174
+ const parent = readParent(obj, names, problems, name);
175
+ if (parent !== void 0) patch.parent_surface_name = parent;
176
+ const sort = readSortOrder(obj, problems);
177
+ if (sort !== void 0) patch.sort_order = sort;
178
+ if ("is_active" in obj) {
179
+ if (typeof obj.is_active !== "boolean")
180
+ problems.add("is_active must be true or false.");
181
+ else patch.is_active = obj.is_active;
182
+ }
183
+ const executor = optionalText(obj, "executor_name", problems, true);
184
+ if (executor !== void 0) patch.executor_name = executor || null;
185
+ const url = optionalText(obj, "url_pattern", problems, true);
186
+ if (url !== void 0) patch.url_pattern = url || null;
187
+ problems.throwIfAny();
188
+ const changed = Object.keys(patch);
189
+ if (changed.length === 0) throw new Error("sends no field to change.");
190
+ return { name, patch, changed };
191
+ },
192
+ {
193
+ nameOf: nameOfRaw,
194
+ listChecks: (items) => [
195
+ repeatsProblem(target, items.map((i) => i.name), "surface")
196
+ ]
197
+ }
198
+ );
199
+ }
200
+ function parseDeleteSurfacesValue(value, ctx) {
201
+ const target = "delete_surfaces";
202
+ const list = readCollectionList(target, "surfaces", value);
203
+ const byName = new Map(ctx.existing.map((s) => [s.name, s]));
204
+ return collectProblems(
205
+ target,
206
+ list,
207
+ (raw) => {
208
+ const name = nameOfRaw(raw)?.trim();
209
+ if (!name) throw new Error('must be a surface name or { "name": "\u2026" }.');
210
+ const current = byName.get(name);
211
+ if (!current)
212
+ throw new ListLevelProblem(
213
+ `${target}: "${name}" is not a surface in the registry.`
214
+ );
215
+ if (current.has_manifest)
216
+ throw new Error(
217
+ `"${name}" has a code manifest, so code owns it: remove the manifest in code, or deactivate it instead.`
218
+ );
219
+ return current;
220
+ },
221
+ {
222
+ nameOf: nameOfRaw,
223
+ listChecks: (items) => [
224
+ repeatsProblem(target, items.map((i) => i.name), "surface")
225
+ ]
226
+ }
227
+ );
228
+ }
229
+ const NEW_SURFACE_TIERS = [
230
+ "Pages",
231
+ "Specialized",
232
+ "Overlays",
233
+ "Editor variants",
234
+ "Debug"
235
+ ];
236
+ function parseNewSurfaceDraftValue(value, ctx) {
237
+ const target = "new_surface_draft";
238
+ const problems = new ProblemList(target);
239
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
240
+ problems.add(
241
+ "new_surface_draft expects a JSON OBJECT like { name, parent_surface_name, tier, description }."
242
+ );
243
+ problems.throwIfAny();
244
+ }
245
+ const obj = value;
246
+ const allowed = ["name", "parent_surface_name", "tier", "description"];
247
+ const unknown = Object.keys(obj).filter((k) => !allowed.includes(k));
248
+ if (unknown.length)
249
+ problems.add(
250
+ `the dialog has no ${unknown.join(", ")} field (fields: ${allowed.join(", ")}).`
251
+ );
252
+ const out = {};
253
+ if ("name" in obj) {
254
+ const name = typeof obj.name === "string" ? obj.name.trim() : "";
255
+ const slash = name.indexOf("/");
256
+ const client = slash > 0 ? name.slice(0, slash) : "";
257
+ const local = slash > 0 ? name.slice(slash + 1) : "";
258
+ if (!client || !local) problems.add(`name must be "<client>/<local>".`);
259
+ else {
260
+ if (!ctx.clientNames.includes(client))
261
+ problems.add(
262
+ `client "${client}" is not a UI client (one of: ${ctx.clientNames.join(", ")}).`
263
+ );
264
+ if (!LOCAL_RE.test(local))
265
+ problems.add(
266
+ `the local part "${local}" may use only lowercase letters, digits, hyphens and slashes.`
267
+ );
268
+ if (ctx.existing.some((s) => s.name === name))
269
+ problems.add(`"${name}" already exists in the registry.`);
270
+ out.client = client;
271
+ out.local = local;
272
+ }
273
+ }
274
+ if ("parent_surface_name" in obj) {
275
+ const parent = readParent(
276
+ obj,
277
+ new Set(ctx.existing.map((s) => s.name)),
278
+ problems
279
+ );
280
+ if (parent !== void 0) out.parent_surface_name = parent;
281
+ }
282
+ if ("tier" in obj) {
283
+ if (typeof obj.tier !== "string" || !NEW_SURFACE_TIERS.includes(obj.tier))
284
+ problems.add(`tier must be one of: ${NEW_SURFACE_TIERS.join(", ")}.`);
285
+ else out.tier = obj.tier;
286
+ }
287
+ if ("description" in obj) {
288
+ if (typeof obj.description !== "string")
289
+ problems.add("description must be a string.");
290
+ else out.description = obj.description;
291
+ }
292
+ problems.throwIfAny();
293
+ if (Object.keys(out).length === 0)
294
+ throw new Error("new_surface_draft sends no field to fill. Nothing was changed.");
295
+ return out;
296
+ }
297
+ export {
298
+ DEFAULT_PARENT,
299
+ DEFAULT_SORT_ORDER,
300
+ NEW_SURFACE_TIERS,
301
+ parseCreateSurfacesValue,
302
+ parseDeleteSurfacesValue,
303
+ parseNewSurfaceDraftValue,
304
+ parseUpdateSurfacesValue
305
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Pure scope builder for the UI Surfaces registry page
3
+ * (`matrx-admin/ui-surfaces`, `/administration/ui/surfaces`).
4
+ *
5
+ * Called by `SurfacesContainer` with state the page already rendered — it
6
+ * never fetches (the runtime polls it every 400 ms). Not loaded yet → the
7
+ * registry keys are omitted; a failed load reports only `load_state`.
8
+ */
9
+ import type { SurfaceScopePayload } from "../types.js";
10
+ import { type UiSurfaceScopeEntry, type UiSurfacesFilterScope, type UiSurfacesRegistryCounts } from "../manifests/admin-ui-surfaces.manifest.js";
11
+ import { type SurfaceWithStats } from "../services/surfaces.service.js";
12
+ import type { SurfacesFilterState } from "../host-surface-types.js";
13
+ import type { NewSurfaceDraftScope } from "../host-surface-types.js";
14
+ /** How many rows `surface_list` carries up front (~4,000 chars). */
15
+ export declare const SURFACE_LIST_MAX_ROWS = 40;
16
+ export type UiSurfacesDialog = "new_surface" | "new_client" | "candidates" | "sync_manifests" | "drift_report";
17
+ export interface UiSurfacesScopeInput {
18
+ loading: boolean;
19
+ error: string | null;
20
+ /** Every row loaded (empty until the first successful load). */
21
+ surfaces: readonly SurfaceWithStats[];
22
+ /** The rows after the page's own filters, in the page's order. */
23
+ visible: readonly SurfaceWithStats[];
24
+ clientNames: readonly string[];
25
+ manifestedNames: ReadonlySet<string>;
26
+ filters: SurfacesFilterState;
27
+ readinessCounts: UiSurfacesRegistryCounts["readiness"];
28
+ candidatesAvailable: number;
29
+ /** The drift report's total (countDriftIssues); null until it loads. */
30
+ driftIssues: number | null;
31
+ peekedName: string | null;
32
+ openDialog: UiSurfacesDialog | null;
33
+ /** The New surface dialog's live values; null while it is closed. */
34
+ newSurfaceDraft: NewSurfaceDraftScope | null;
35
+ }
36
+ export declare function filtersToScope(f: SurfacesFilterState): UiSurfacesFilterScope;
37
+ export declare function toSurfaceScopeEntry(row: SurfaceWithStats, manifestedNames: ReadonlySet<string>): UiSurfaceScopeEntry;
38
+ /** The condensed on-screen list as one XML bundle. */
39
+ export declare function buildSurfaceListBundle(visible: readonly SurfaceWithStats[], manifestedNames: ReadonlySet<string>, filters: UiSurfacesFilterScope): string;
40
+ export declare function buildUiSurfacesScope(input: UiSurfacesScopeInput): SurfaceScopePayload;
@@ -0,0 +1,116 @@
1
+ import { getManifest } from "../runtime/registry.js";
2
+ import {
3
+ createAdminUiSurfacesScope
4
+ } from "../manifests/admin-ui-surfaces.manifest.js";
5
+ import {
6
+ readinessBucketOf,
7
+ tierFor
8
+ } from "../services/surfaces.service.js";
9
+ import { surfaceCheckState } from "../utils/surface-check-ledger.js";
10
+ import { xmlElement, xmlList } from "../runtime/context-bundle.js";
11
+ const SURFACE_LIST_MAX_ROWS = 40;
12
+ function filtersToScope(f) {
13
+ return {
14
+ client: f.client === "__all__" ? "all" : f.client,
15
+ status: f.status,
16
+ check_age: f.checked,
17
+ manifest: f.manifest,
18
+ parent: f.parent === "__all__" ? "all" : f.parent === "__none__" ? "none" : f.parent,
19
+ readiness: f.readiness
20
+ };
21
+ }
22
+ function toSurfaceScopeEntry(row, manifestedNames) {
23
+ return {
24
+ name: row.name,
25
+ label: row.label ?? null,
26
+ client_name: row.client_name,
27
+ executor_name: row.executor_name ?? null,
28
+ parent_surface_name: row.parent_surface_name ?? null,
29
+ sort_order: row.sort_order,
30
+ tier: tierFor(row.sort_order).label,
31
+ is_active: row.is_active !== false,
32
+ readiness: readinessBucketOf(row),
33
+ readiness_note: row.readiness_note ?? null,
34
+ has_manifest: manifestedNames.has(row.name),
35
+ value_count: row.surfaceValueCount,
36
+ declared_value_count: getManifest(row.name)?.values.length ?? null,
37
+ agent_count: row.agentCount,
38
+ tool_count: row.toolCount,
39
+ check_state: surfaceCheckState(row),
40
+ last_checked_at: row.last_checked_at ?? null,
41
+ last_checked_by: row.last_checked_by ?? null,
42
+ url_pattern: row.url_pattern ?? null,
43
+ description: row.description ?? null
44
+ };
45
+ }
46
+ function buildSurfaceListBundle(visible, manifestedNames, filters) {
47
+ const active = Object.fromEntries(
48
+ Object.entries(filters).filter(([, v]) => v !== "all")
49
+ );
50
+ return xmlList(
51
+ "surfaces",
52
+ visible,
53
+ (row) => xmlElement("s", {
54
+ name: row.name,
55
+ label: row.label,
56
+ client: row.client_name,
57
+ parent: row.parent_surface_name,
58
+ tier: `${tierFor(row.sort_order).label} ${row.sort_order}`,
59
+ readiness: readinessBucketOf(row),
60
+ values: row.surfaceValueCount,
61
+ declared: getManifest(row.name)?.values.length,
62
+ agents: row.agentCount,
63
+ tools: row.toolCount,
64
+ checked: surfaceCheckState(row),
65
+ active: row.is_active !== false,
66
+ manifest: manifestedNames.has(row.name)
67
+ }),
68
+ { maxRows: SURFACE_LIST_MAX_ROWS, attrs: active }
69
+ ) || '<surfaces total="0"/>';
70
+ }
71
+ function buildUiSurfacesScope(input) {
72
+ const filters = filtersToScope(input.filters);
73
+ const extras = {
74
+ ...input.peekedName ? { peeked_surface: input.peekedName } : {},
75
+ ...input.openDialog ? { open_dialog: input.openDialog } : {},
76
+ ...input.newSurfaceDraft ? { new_surface_draft: input.newSurfaceDraft } : {}
77
+ };
78
+ if (input.error) {
79
+ return createAdminUiSurfacesScope({
80
+ load_state: `error: ${input.error}`,
81
+ filters,
82
+ ...extras
83
+ });
84
+ }
85
+ if (input.loading && input.surfaces.length === 0) {
86
+ return createAdminUiSurfacesScope({ load_state: "loading", filters, ...extras });
87
+ }
88
+ const surfaces = input.surfaces.map(
89
+ (s) => toSurfaceScopeEntry(s, input.manifestedNames)
90
+ );
91
+ return createAdminUiSurfacesScope({
92
+ load_state: "ready",
93
+ filters,
94
+ surface_list: buildSurfaceListBundle(input.visible, input.manifestedNames, filters),
95
+ surfaces,
96
+ registry_counts: {
97
+ total: surfaces.length,
98
+ active: surfaces.filter((s) => s.is_active).length,
99
+ manifests: input.manifestedNames.size,
100
+ unused: surfaces.filter((s) => s.agent_count === 0 && s.tool_count === 0).length,
101
+ visible: input.visible.length,
102
+ readiness: input.readinessCounts,
103
+ candidates_available: input.candidatesAvailable,
104
+ ...input.driftIssues !== null ? { drift_issues: input.driftIssues } : {}
105
+ },
106
+ client_names: [...input.clientNames],
107
+ ...extras
108
+ });
109
+ }
110
+ export {
111
+ SURFACE_LIST_MAX_ROWS,
112
+ buildSurfaceListBundle,
113
+ buildUiSurfacesScope,
114
+ filtersToScope,
115
+ toSurfaceScopeEntry
116
+ };
@@ -27,7 +27,7 @@
27
27
  * that dispatches the feature's refetch thunk(s). Known not-yet-covered
28
28
  * writers (no canonical Redux refetch today, or their state is hook-local):
29
29
  * picklist (session-cached `usePickListDetail`), dictionary, workbook,
30
- * dataset/usertable, document. The working document is already reconciled by
30
+ * dataset, document. The working document is already reconciled by
31
31
  * its own `context_changed` re-read; the scratchpad is agent-readonly.
32
32
  */
33
33
  import type { ChatRootState } from "../../store/root-state.js";
@@ -1066,7 +1066,6 @@ const APP_FEATURE_TOOL_NAMES = /* @__PURE__ */ new Set([
1066
1066
  "user_todos",
1067
1067
  "document",
1068
1068
  "dataset",
1069
- "usertable_create",
1070
1069
  "knowledge_search",
1071
1070
  "document_search",
1072
1071
  "knowledge_browse",
@@ -1202,7 +1201,6 @@ const TOOL_GLYPHS = {
1202
1201
  update_plan: { icon: ListTodo, accent: "primary" },
1203
1202
  document: { icon: FileText, accent: "slate" },
1204
1203
  dataset: { icon: Table2, accent: "cyan" },
1205
- usertable_create: { icon: Table2, accent: "cyan" },
1206
1204
  workbook: { icon: Sheet, accent: "green" },
1207
1205
  dictionary: { icon: BookText, accent: "amber" },
1208
1206
  note: { icon: NotebookPen, accent: "amber" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/chat",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "THE one AI Matrx person-to-agent chat system: the full chat (running agents, conversations, tool calls, streaming, context, surfaces, voice) as per-module subpaths @ai-matrx/chat/<domain>/<module> over a host contract (/host, /host/react) whose only required value is a db; plus the kernel compatibility entries (., /external-store, /matrx, /react, /tools, styles.css, tokens.css) until P28.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -202,23 +202,23 @@
202
202
  "uuid": "^14.0.2",
203
203
  "zod": "^4.6.5",
204
204
  "@ai-matrx/agents": "latest",
205
- "@ai-matrx/alchemy": "latest",
206
205
  "@ai-matrx/associations": "latest",
206
+ "@ai-matrx/alchemy": "latest",
207
207
  "@ai-matrx/browser-audio": "latest",
208
208
  "@ai-matrx/canvas": "latest",
209
- "@ai-matrx/content-ir": "latest",
210
209
  "@ai-matrx/content-ir-react": "latest",
211
- "@ai-matrx/data": "latest",
212
- "@ai-matrx/design-system": "latest",
213
210
  "@ai-matrx/diff": "latest",
211
+ "@ai-matrx/data": "latest",
214
212
  "@ai-matrx/icons": "latest",
215
- "@ai-matrx/kit": "latest",
213
+ "@ai-matrx/design-system": "latest",
216
214
  "@ai-matrx/media": "latest",
215
+ "@ai-matrx/kit": "latest",
217
216
  "@ai-matrx/print": "latest",
218
217
  "@ai-matrx/realtime": "latest",
219
218
  "@ai-matrx/records": "latest",
220
219
  "@ai-matrx/records-ui": "latest",
221
- "@ai-matrx/tap-target": "latest"
220
+ "@ai-matrx/tap-target": "latest",
221
+ "@ai-matrx/content-ir": "latest"
222
222
  },
223
223
  "devDependencies": {
224
224
  "@arethetypeswrong/cli": "^0.18.5",