@ai-matrx/chat 0.3.4 → 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 +23 -0
- package/dist/agents/components/chat/ChatNewClient.d.ts +6 -1
- package/dist/agents/components/chat/ChatNewClient.js +10 -3
- package/dist/agents/components/chat/begin-fresh-chat.d.ts +2 -0
- package/dist/agents/components/chat/begin-fresh-chat.js +18 -1
- package/dist/agents/components/inputs/smart-input/RunControlsMenu.js +1 -1
- package/dist/agents/components/inputs/smart-input/composer/ComposerAgentPeek.d.ts +4 -0
- package/dist/agents/components/inputs/smart-input/composer/ComposerAgentPeek.js +50 -0
- package/dist/agents/components/inputs/smart-input/composer/ComposerAgentPill.js +16 -1
- package/dist/agents/components/shared/AgentPeekDuplicateButton.d.ts +8 -0
- package/dist/agents/components/shared/AgentPeekDuplicateButton.js +66 -0
- package/dist/agents/redux/agent-definition/slice.d.ts +16 -1
- package/dist/agents/redux/agent-definition/slice.js +20 -0
- package/dist/agents/types/agent-definition.types.d.ts +29 -1
- package/dist/host/ui-slots.js +1 -1
- package/dist/surfaces/lib/ui-surfaces-agent-writes.d.ts +61 -0
- package/dist/surfaces/lib/ui-surfaces-agent-writes.js +305 -0
- package/dist/surfaces/lib/ui-surfaces-scope.d.ts +40 -0
- package/dist/surfaces/lib/ui-surfaces-scope.js +116 -0
- package/dist/tool-call-visualization/effects/toolStateEffects.d.ts +1 -1
- package/dist/tool-call-visualization/registry/registry.js +0 -2
- package/dist/window-panels/windows/agents/RunControlsWindow.js +19 -6
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
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
|
+
|
|
13
|
+
## 0.3.5 — 2026-10-07
|
|
14
|
+
### Chat mode opens the agent itself; Duplicate opens the copy in a new tab
|
|
15
|
+
|
|
16
|
+
- The Chat-mode agent pill's menu starts with "This chat's agent": its row opens the agent menu's own
|
|
17
|
+
peek card (`AgentDetailCard`) — view, open, Sneak Peek, and Duplicate when the agent is not yours.
|
|
18
|
+
- `agents/components/shared/AgentPeekDuplicateButton` moved here from matrx-frontend (one Duplicate for
|
|
19
|
+
every peek). It now opens the copy's page in a NEW TAB instead of navigating away from the chat.
|
|
20
|
+
- Needs `@ai-matrx/agents` 0.49.0.
|
|
21
|
+
- The composer's + (Chat Options) is tinted only when something was added — no longer blue on every fresh
|
|
22
|
+
chat. The Chat Options window lists its sections as start-aligned rows (`Tile`), not centred buttons.
|
|
23
|
+
- Consumer action: import `AgentPeekDuplicateButton` from
|
|
24
|
+
`@ai-matrx/chat/agents/components/shared/AgentPeekDuplicateButton` and delete the app's copy.
|
|
25
|
+
|
|
3
26
|
## 0.3.4 — 2026-10-07
|
|
4
27
|
### Re-sync before matrx-frontend consumes the published package (P27)
|
|
5
28
|
|
|
@@ -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
|
-
|
|
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 =
|
|
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,
|
|
@@ -89,7 +89,7 @@ function RunControlsMenu({
|
|
|
89
89
|
Button,
|
|
90
90
|
{
|
|
91
91
|
variant: "quiet",
|
|
92
|
-
tone: rc.addedCount > 0
|
|
92
|
+
tone: rc.addedCount > 0 ? "primary" : void 0,
|
|
93
93
|
icon: /* @__PURE__ */ jsx(TriggerIcon, {}),
|
|
94
94
|
tabIndex: variant === "plus" ? -1 : void 0,
|
|
95
95
|
title: "Chat Options",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { AgentDetailCard, useAgentCatalogState } from "@ai-matrx/agents/catalog/react";
|
|
5
|
+
import { useAppDispatch, useAppSelector } from "../../../../../store/hooks.js";
|
|
6
|
+
import { selectAgentById, selectAgentReadyForBuilder } from "../../../../redux/agent-definition/selectors.js";
|
|
7
|
+
import { fetchFullAgent } from "../../../../redux/agent-definition/thunks.js";
|
|
8
|
+
import { AgentPeekDuplicateButton } from "../../../shared/AgentPeekDuplicateButton.js";
|
|
9
|
+
function ComposerAgentPeek({ agentId, onDone }) {
|
|
10
|
+
const dispatch = useAppDispatch();
|
|
11
|
+
const row = useAgentCatalogState((state) => state.byId[agentId]);
|
|
12
|
+
const record = useAppSelector((state) => selectAgentById(state, agentId));
|
|
13
|
+
const isReady = useAppSelector((state) => selectAgentReadyForBuilder(state, agentId));
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!isReady) void dispatch(fetchFullAgent(agentId)).unwrap().catch(() => void 0);
|
|
16
|
+
}, [dispatch, agentId, isReady]);
|
|
17
|
+
const agent = row ?? (record ? {
|
|
18
|
+
id: record.id,
|
|
19
|
+
name: record.name,
|
|
20
|
+
description: record.description,
|
|
21
|
+
category: record.category,
|
|
22
|
+
tags: record.tags,
|
|
23
|
+
agentType: record.agentType,
|
|
24
|
+
modelId: record.modelId,
|
|
25
|
+
isActive: record.isActive,
|
|
26
|
+
isArchived: record.isArchived,
|
|
27
|
+
isFavorite: record.isFavorite,
|
|
28
|
+
createdBy: record.createdBy,
|
|
29
|
+
organizationId: record.organizationId,
|
|
30
|
+
taskId: record.taskId,
|
|
31
|
+
sourceAgentId: record.sourceAgentId,
|
|
32
|
+
createdAt: record.createdAt,
|
|
33
|
+
updatedAt: record.updatedAt,
|
|
34
|
+
isOwner: record.isOwner,
|
|
35
|
+
accessLevel: record.accessLevel,
|
|
36
|
+
sharedByEmail: record.sharedByEmail,
|
|
37
|
+
orchestra: null
|
|
38
|
+
} : null);
|
|
39
|
+
if (!agent) return null;
|
|
40
|
+
return /* @__PURE__ */ jsx(
|
|
41
|
+
AgentDetailCard,
|
|
42
|
+
{
|
|
43
|
+
agent,
|
|
44
|
+
actions: /* @__PURE__ */ jsx(AgentPeekDuplicateButton, { agentId, onStart: onDone })
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
ComposerAgentPeek
|
|
50
|
+
};
|
|
@@ -36,6 +36,7 @@ import { composerShows } from "./composer-mode-visibility.js";
|
|
|
36
36
|
import { composerPillClass } from "./composer-chip.js";
|
|
37
37
|
import { useComposerAgent, useEffectiveModelId } from "./useComposerAgent.js";
|
|
38
38
|
import { useComposerAgentFilter } from "./useComposerAgentFilter.js";
|
|
39
|
+
import { ComposerAgentPeek } from "./ComposerAgentPeek.js";
|
|
39
40
|
import { presentOrganizationRefusal } from "../../../../../host/org.js";
|
|
40
41
|
import { ensureOrgId, isOrganizationSelectionCancelled } from "../../../../../host/org.js";
|
|
41
42
|
function pillLabel(info, mode) {
|
|
@@ -150,6 +151,17 @@ function ComposerAgentPill({ conversationId, mode, size, agentControl, menuSide,
|
|
|
150
151
|
)
|
|
151
152
|
] });
|
|
152
153
|
}
|
|
154
|
+
function ThisAgentRow({ info, close }) {
|
|
155
|
+
if (!info.agentId) return null;
|
|
156
|
+
return /* @__PURE__ */ jsx(
|
|
157
|
+
ComposerSubmenu,
|
|
158
|
+
{
|
|
159
|
+
row: { icon: Webhook, label: info.agentName ?? "Agent", description: "View, open or duplicate" },
|
|
160
|
+
panelClassName: "w-[340px] h-[420px] p-0",
|
|
161
|
+
children: /* @__PURE__ */ jsx(ComposerAgentPeek, { agentId: info.agentId, onDone: close })
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
}
|
|
153
165
|
function ChatPresetsPanel({
|
|
154
166
|
conversationId,
|
|
155
167
|
info,
|
|
@@ -169,7 +181,7 @@ function ChatPresetsPanel({
|
|
|
169
181
|
if (!onSelectAgent) {
|
|
170
182
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
171
183
|
/* @__PURE__ */ jsx(ComposerMenuLabel, { children: "Agent" }),
|
|
172
|
-
/* @__PURE__ */ jsx(
|
|
184
|
+
/* @__PURE__ */ jsx(ThisAgentRow, { info, close }),
|
|
173
185
|
/* @__PURE__ */ jsx(ComposerMenuHelp, { children: "This page always answers with this agent." }),
|
|
174
186
|
/* @__PURE__ */ jsx(ComposerMenuDivider, {}),
|
|
175
187
|
/* @__PURE__ */ jsx(ComposerMenuLabel, { children: "Model for this chat" }),
|
|
@@ -229,6 +241,9 @@ function ChatPresetsPanel({
|
|
|
229
241
|
void savePersonalDefault("chat_default_offering", offeringId ?? "");
|
|
230
242
|
};
|
|
231
243
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
244
|
+
/* @__PURE__ */ jsx(ComposerMenuLabel, { children: "This chat's agent" }),
|
|
245
|
+
/* @__PURE__ */ jsx(ThisAgentRow, { info, close }),
|
|
246
|
+
/* @__PURE__ */ jsx(ComposerMenuDivider, {}),
|
|
232
247
|
/* @__PURE__ */ jsx(ComposerMenuLabel, { children: "Chat agents" }),
|
|
233
248
|
info.presets.map((preset) => /* @__PURE__ */ jsx(
|
|
234
249
|
ComposerMenuRow,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function AgentPeekDuplicateButton({ agentId, onStart, }: {
|
|
2
|
+
agentId: string;
|
|
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;
|
|
8
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { CopyPlus, Loader2 } from "lucide-react";
|
|
5
|
+
import { Button } from "@ai-matrx/design-system/controls";
|
|
6
|
+
import { getUserMessage } from "@ai-matrx/agents/matrx";
|
|
7
|
+
import { useAppDispatch, useAppSelector } from "../../../store/hooks.js";
|
|
8
|
+
import { selectUserId } from "../../../host/identity.js";
|
|
9
|
+
import { getBuilderDoor } from "../../../host/builder-door.js";
|
|
10
|
+
import { toast } from "../../../host/notify.js";
|
|
11
|
+
import { isOrganizationSelectionCancelled } from "../../../host/org.js";
|
|
12
|
+
import { selectAgentById, selectAgentReadyForBuilder } from "../../redux/agent-definition/selectors.js";
|
|
13
|
+
import { agentGoHref } from "../../addressing/agentAddress.js";
|
|
14
|
+
function AgentPeekDuplicateButton({
|
|
15
|
+
agentId,
|
|
16
|
+
onStart
|
|
17
|
+
}) {
|
|
18
|
+
const dispatch = useAppDispatch();
|
|
19
|
+
const userId = useAppSelector(selectUserId);
|
|
20
|
+
const record = useAppSelector((state) => selectAgentById(state, agentId));
|
|
21
|
+
const isReady = useAppSelector((state) => selectAgentReadyForBuilder(state, agentId));
|
|
22
|
+
const [busy, setBusy] = useState(false);
|
|
23
|
+
const door = getBuilderDoor();
|
|
24
|
+
if (!door || !isReady || !record) return null;
|
|
25
|
+
const ownsIt = record.isOwner === true || record.createdBy != null && record.createdBy === userId;
|
|
26
|
+
if (ownsIt) return null;
|
|
27
|
+
const duplicate = async () => {
|
|
28
|
+
if (busy) return;
|
|
29
|
+
setBusy(true);
|
|
30
|
+
onStart?.();
|
|
31
|
+
try {
|
|
32
|
+
const newId = await dispatch(door.duplicateAgent(agentId)).unwrap();
|
|
33
|
+
const href = agentGoHref(newId, "/build");
|
|
34
|
+
const tab = window.open(href, "_blank");
|
|
35
|
+
if (tab) {
|
|
36
|
+
tab.opener = null;
|
|
37
|
+
toast.success(`Duplicated "${record.name}" \u2014 opened in a new tab`);
|
|
38
|
+
} else {
|
|
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
|
+
});
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (!isOrganizationSelectionCancelled(err)) {
|
|
46
|
+
toast.error("Could not duplicate agent", { description: getUserMessage(err) });
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
setBusy(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return /* @__PURE__ */ jsx(
|
|
53
|
+
Button,
|
|
54
|
+
{
|
|
55
|
+
icon: busy ? /* @__PURE__ */ jsx(Loader2, { className: "animate-spin" }) : /* @__PURE__ */ jsx(CopyPlus, {}),
|
|
56
|
+
variant: "outline",
|
|
57
|
+
onClick: duplicate,
|
|
58
|
+
disabled: busy,
|
|
59
|
+
title: "Make your own copy of this agent, in a new tab",
|
|
60
|
+
children: "Duplicate"
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
AgentPeekDuplicateButton
|
|
66
|
+
};
|
|
@@ -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">,
|
|
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
|
|
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. */
|
package/dist/host/ui-slots.js
CHANGED
|
@@ -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
|
|
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" },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { useAppDispatch } from "../../../store/hooks.js";
|
|
5
5
|
import { closeOverlay, CHAT_WINDOWS } from "../../../host/windows.js";
|
|
@@ -20,7 +20,7 @@ import { selectInstanceAgentId } from "../../../agents/redux/execution-system/in
|
|
|
20
20
|
import { selectAgentName } from "../../../agents/redux/agent-definition/selectors.js";
|
|
21
21
|
import { NonEditableContextMenu } from "../../../host/ui-slots.js";
|
|
22
22
|
import { buildAgentMenuSection, agentEntityRef } from "../../../agents/menu/agent-actions.js";
|
|
23
|
-
import {
|
|
23
|
+
import { Tile } from "@ai-matrx/design-system/controls";
|
|
24
24
|
const OVERLAY_ID = CHAT_WINDOWS.runControlsWindow;
|
|
25
25
|
function RunControlsWindowInner({
|
|
26
26
|
conversationId,
|
|
@@ -104,10 +104,23 @@ function RunControlsWindowInner({
|
|
|
104
104
|
children: rc.tabs.map((t) => {
|
|
105
105
|
const Icon = t.icon;
|
|
106
106
|
const on = activeTab === t.id;
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
return (
|
|
108
|
+
// A list of sections is rows, start-aligned — never centred buttons.
|
|
109
|
+
/* @__PURE__ */ jsx(
|
|
110
|
+
Tile,
|
|
111
|
+
{
|
|
112
|
+
variant: "quiet",
|
|
113
|
+
selected: on,
|
|
114
|
+
icon: /* @__PURE__ */ jsx(Icon, {}),
|
|
115
|
+
title: t.label,
|
|
116
|
+
end: rc.tabTrailing(t.id),
|
|
117
|
+
role: "tab",
|
|
118
|
+
"aria-selected": on,
|
|
119
|
+
onClick: () => setTab(t.id)
|
|
120
|
+
},
|
|
121
|
+
t.id
|
|
122
|
+
)
|
|
123
|
+
);
|
|
111
124
|
})
|
|
112
125
|
}
|
|
113
126
|
),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-matrx/chat",
|
|
3
|
-
"version": "0.3.
|
|
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",
|
|
@@ -201,24 +201,24 @@
|
|
|
201
201
|
"server-only": "^0.0.1",
|
|
202
202
|
"uuid": "^14.0.2",
|
|
203
203
|
"zod": "^4.6.5",
|
|
204
|
-
"@ai-matrx/alchemy": "latest",
|
|
205
204
|
"@ai-matrx/agents": "latest",
|
|
205
|
+
"@ai-matrx/associations": "latest",
|
|
206
|
+
"@ai-matrx/alchemy": "latest",
|
|
206
207
|
"@ai-matrx/browser-audio": "latest",
|
|
207
208
|
"@ai-matrx/canvas": "latest",
|
|
208
|
-
"@ai-matrx/content-ir": "latest",
|
|
209
209
|
"@ai-matrx/content-ir-react": "latest",
|
|
210
|
+
"@ai-matrx/diff": "latest",
|
|
210
211
|
"@ai-matrx/data": "latest",
|
|
212
|
+
"@ai-matrx/icons": "latest",
|
|
211
213
|
"@ai-matrx/design-system": "latest",
|
|
212
|
-
"@ai-matrx/associations": "latest",
|
|
213
|
-
"@ai-matrx/diff": "latest",
|
|
214
|
-
"@ai-matrx/kit": "latest",
|
|
215
214
|
"@ai-matrx/media": "latest",
|
|
216
|
-
"@ai-matrx/
|
|
215
|
+
"@ai-matrx/kit": "latest",
|
|
216
|
+
"@ai-matrx/print": "latest",
|
|
217
217
|
"@ai-matrx/realtime": "latest",
|
|
218
218
|
"@ai-matrx/records": "latest",
|
|
219
|
-
"@ai-matrx/print": "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",
|