@ai-matrx/chat 0.2.10 → 0.3.3

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/agent-copy/clipboard.d.ts +2 -4
  3. package/dist/agent-copy/clipboard.js +1 -14
  4. package/dist/agents/components/context-items/bodies/WebpageBody.js +1 -1
  5. package/dist/agents/components/context-preview/AttachedContextSection.js +9 -8
  6. package/dist/agents/components/context-preview/ContextCompareView.js +13 -13
  7. package/dist/agents/components/context-preview/ContextPreviewPanel.js +13 -13
  8. package/dist/agents/components/debug/SandboxFileViewer.js +1 -1
  9. package/dist/agents/components/debug/StreamDebugPanel.js +2 -2
  10. package/dist/agents/components/messages-display/TranscriptIntegrityCopyButton.js +1 -1
  11. package/dist/agents/components/observational-memory/components/MemoryStateInspector.js +4 -4
  12. package/dist/agents/components/run-controls/AgentExecutionTestModal.js +2 -2
  13. package/dist/agents/components/run-controls/PayloadTab.js +2 -1
  14. package/dist/agents/components/run-controls/panels/BackendTargetPanel.js +2 -1
  15. package/dist/agents/components/shared/AgentSaveStatus.js +22 -1
  16. package/dist/agents/hooks/useAgentAutoSave.js +22 -5
  17. package/dist/agents/prompt-preview/PromptPreviewContent.js +1 -1
  18. package/dist/agents/redux/agent-definition/slice.js +1 -0
  19. package/dist/agents/redux/agent-settings/agentSettingsSlice.d.ts +3 -0
  20. package/dist/agents/redux/agent-settings/agentSettingsSlice.js +12 -2
  21. package/dist/agents/redux/agent-settings/types.d.ts +7 -0
  22. package/dist/agents/redux/execution-system/instance-ui-state/components/InstanceUIStateCore.js +2 -1
  23. package/dist/agents/redux/execution-system/instance-ui-state/components/InstanceUIStateList.js +2 -1
  24. package/dist/agents/redux/execution-system/messages/messages.selectors.d.ts +10 -0
  25. package/dist/agents/redux/execution-system/messages/messages.selectors.js +31 -1
  26. package/dist/agents/redux/execution-system/thunks/process-stream.js +17 -0
  27. package/dist/agents/redux/execution-system/thunks/terminal-request-failure.d.ts +27 -0
  28. package/dist/agents/redux/execution-system/thunks/terminal-request-failure.js +56 -0
  29. package/dist/cx-chat/components/messages/AssistantMessage.js +1 -1
  30. package/dist/cx-chat/components/messages/UserMessage.js +2 -1
  31. package/dist/cx-conversation/AssistantMessage.js +1 -1
  32. package/dist/cx-conversation/UserMessage.js +2 -1
  33. package/dist/host/db-types.d.ts +15 -12
  34. package/dist/host/ui-slots.d.ts +2 -4
  35. package/dist/host/ui-slots.js +10 -5
  36. package/dist/public-chat/components/PublicMessageOptionsMenu.js +7 -7
  37. package/dist/surfaces/services/surface-config.service.d.ts +3 -1
  38. package/dist/surfaces/services/surface-config.service.js +33 -17
  39. package/dist/tool-call-visualization/components/ToolTabBodies.d.ts +0 -4
  40. package/dist/tool-call-visualization/components/ToolTabBodies.js +2 -21
  41. package/dist/voice-agent/components/VoiceDebugPanel.js +1 -1
  42. package/dist/window-panels/windows/agents/ObservationalMemoryWindow.js +2 -1
  43. package/package.json +7 -7
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.3.3 — 2026-10-07
4
+ ### Re-sync before matrx-frontend consumes the published package (P27)
5
+
6
+ - Carries every
7
+ matrx-frontend `packages/chat` edit after `7f630d97d9`: a server-ended failed request is a failed turn
8
+ (`thunks/terminal-request-failure`, process-stream, messages selectors), agent autosave recovery.
9
+ Its suite needs the app's content-ir host, so it lives app-side with the other host suites.
10
+ - Consumer action: none.
11
+
12
+ ## 0.3.2
13
+
14
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
15
+ `git diff npm/chat/v0.2.10..npm/chat/v0.3.2 -- apps/shared/chat`).
16
+ No source changes intended and no consumer action required.
17
+
18
+ ## 0.3.1 — 2026-10-07
19
+ ### Re-sync before matrx-frontend consumes the published package (P27)
20
+
21
+ - Carries every matrx-frontend `packages/chat` edit after `636e7ef2dd` (context-preview copy, agent-settings
22
+ `offeringId`, surface-config service, db-types) and `surfaces/lib/ui-surfaces-agent-writes` +
23
+ `ui-surfaces-scope` (+ suite), which earlier re-syncs missed; the app imports both.
24
+ - Consumer action: none.
25
+
26
+ ## 0.3.0 — 2026-10-07
27
+
28
+ ALC-19: the chat clipboard slot is content-shaped.
29
+
30
+ - Host UI slot `copyToClipboard` (`host/ui-slots`) is renamed `copyContent(content, options)`; its stand-in copies a string as text (anything else as JSON) through `@ai-matrx/kit/clipboard` and calls `options.onError` on failure. `agent-copy/clipboard` no longer exports the failure-swallowing `writeClipboard` (only `approxTokens`).
31
+ - Consumer action: register `copyContent` (not `copyToClipboard`) in `registerChatUi`; import `copyContent` from `@ai-matrx/chat/host/ui-slots`; replace any `writeClipboard` import with `@ai-matrx/kit/clipboard` `copyText`.
32
+
3
33
  ## 0.2.10 — 2026-10-07
4
34
  ### Re-sync before matrx-frontend consumes the published package (P27)
5
35
 
@@ -1,8 +1,6 @@
1
1
  /**
2
- * Shared clipboard + payload-size helpers for the agent-copy primitives.
3
- * One implementation — CopyButtons, AiCopyMenu, and the Groomer all use these;
4
- * never re-roll a clipboard write or a token estimate at a callsite.
2
+ * Shared payload-size helper for the agent-copy primitives. Clipboard writes go through
3
+ * `@ai-matrx/kit/clipboard`; never re-roll a token estimate at a callsite.
5
4
  */
6
- export declare function writeClipboard(text: string): Promise<void>;
7
5
  /** Rough token estimate (chars / 4) — same convention as the Groomer window. */
8
6
  export declare function approxTokens(text: string): number;
@@ -1,19 +1,6 @@
1
- async function writeClipboard(text) {
2
- try {
3
- await navigator.clipboard.writeText(text);
4
- } catch {
5
- const textarea = document.createElement("textarea");
6
- textarea.value = text;
7
- document.body.appendChild(textarea);
8
- textarea.select();
9
- document.execCommand("copy");
10
- document.body.removeChild(textarea);
11
- }
12
- }
13
1
  function approxTokens(text) {
14
2
  return Math.ceil(text.length / 4);
15
3
  }
16
4
  export {
17
- approxTokens,
18
- writeClipboard
5
+ approxTokens
19
6
  };
@@ -51,7 +51,7 @@ function WebpageFooter({ item }) {
51
51
  if (!url) return null;
52
52
  const copy = async () => {
53
53
  try {
54
- await copyText(url);
54
+ if (!await copyText(url)) return;
55
55
  setCopied(true);
56
56
  setTimeout(() => setCopied(false), 1500);
57
57
  } catch {
@@ -32,7 +32,7 @@ import { setConversationDocumentEnabledThunk } from "../../redux/execution-syste
32
32
  import { setScratchpadGateThunk } from "../../redux/execution-system/instance-working-document/scratchpad.thunks.js";
33
33
  import { useActiveContextLayerItems } from "../context-items/useActiveContextLayerItems.js";
34
34
  import { docKindForContextKey } from "../../utils/workingDocumentContext.js";
35
- import { InlineCopyButton } from "../../../host/ui-slots.js";
35
+ import { CopyButtons } from "../../../host/ui-slots.js";
36
36
  import { cn } from "@ai-matrx/design-system";
37
37
  import { contextEntryLabel } from "../context-policies-display/contextEntryLabel.js";
38
38
  const COPY_REVEAL = "opacity-0 transition-opacity pointer-coarse:opacity-100";
@@ -196,11 +196,12 @@ function AttachedContextSection({
196
196
  k
197
197
  )) }),
198
198
  /* @__PURE__ */ jsx(
199
- InlineCopyButton,
199
+ CopyButtons,
200
200
  {
201
- content: variables,
201
+ label: "Copy",
202
+ json: variables,
202
203
  size: "xs",
203
- className: cn(COPY_REVEAL, "group-hover/vars:opacity-100")
204
+ className: cn("absolute top-1 right-1 z-10", COPY_REVEAL, "group-hover/vars:opacity-100")
204
205
  }
205
206
  )
206
207
  ] }),
@@ -285,12 +286,12 @@ function AttachedContextSection({
285
286
  /* @__PURE__ */ jsx("span", { className: "italic text-muted-foreground", children: "(empty \u2014 sent, but has no content yet)" })
286
287
  ) }),
287
288
  /* @__PURE__ */ jsx(
288
- InlineCopyButton,
289
+ CopyButtons,
289
290
  {
290
- content: item.preview,
291
- formatJson: false,
291
+ label: "Copy",
292
+ human: item.preview,
292
293
  size: "xs",
293
- className: cn(COPY_REVEAL, "group-hover/entry:opacity-100")
294
+ className: cn("absolute top-1 right-1 z-10", COPY_REVEAL, "group-hover/entry:opacity-100")
294
295
  }
295
296
  )
296
297
  ] }, item.key);
@@ -9,7 +9,7 @@ import { Skeleton } from "@ai-matrx/design-system";
9
9
  import { Badge, Button, Chip } from "@ai-matrx/design-system/controls";
10
10
  import { Textarea } from "@ai-matrx/design-system";
11
11
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../../ui/tabs.js";
12
- import { InlineCopyButton } from "../../../host/ui-slots.js";
12
+ import { CopyButtons } from "../../../host/ui-slots.js";
13
13
  import { RichContent } from "../../../host/ui-slots.js";
14
14
  import { SystemItemsLine } from "./SystemItemsLine.js";
15
15
  import { useAppDispatch, useAppSelector } from "../../../store/hooks.js";
@@ -109,12 +109,12 @@ function Block({
109
109
  ) : block ? /* @__PURE__ */ jsxs("div", { className: "group/block relative mt-1.5", children: [
110
110
  /* @__PURE__ */ jsx("pre", { "data-kind-source": "explicit", className: "whitespace-pre-wrap break-words rounded-md border border-border bg-muted/40 px-2.5 py-2 font-mono text-[11px] leading-relaxed text-foreground", children: block }),
111
111
  /* @__PURE__ */ jsx(
112
- InlineCopyButton,
112
+ CopyButtons,
113
113
  {
114
- content: block,
115
- formatJson: false,
114
+ label: "Copy",
115
+ human: block,
116
116
  size: "sm",
117
- className: "opacity-0 transition-opacity pointer-coarse:opacity-100 group-hover/block:opacity-100"
117
+ className: "absolute top-1 right-1 z-10 opacity-0 transition-opacity pointer-coarse:opacity-100 group-hover/block:opacity-100"
118
118
  }
119
119
  )
120
120
  ] }) : /* @__PURE__ */ jsx("div", { className: "mt-1.5 rounded-md border border-border bg-muted/30 px-2.5 py-2 text-xs italic text-muted-foreground", children: "No block \u2014 this side delivers no context for this selection." }),
@@ -141,12 +141,12 @@ function FedBlock({ label, text, slot }) {
141
141
  text ? /* @__PURE__ */ jsxs("div", { className: "group/block relative mt-1", children: [
142
142
  /* @__PURE__ */ jsx("pre", { "data-kind-source": "explicit", className: "max-h-[28rem] overflow-auto whitespace-pre-wrap break-words rounded-md border border-border bg-muted/40 px-2.5 py-2 font-mono text-[11px] leading-relaxed text-foreground", children: text }),
143
143
  /* @__PURE__ */ jsx(
144
- InlineCopyButton,
144
+ CopyButtons,
145
145
  {
146
- content: text,
147
- formatJson: false,
146
+ label: "Copy",
147
+ human: text,
148
148
  size: "sm",
149
- className: "opacity-0 transition-opacity pointer-coarse:opacity-100 group-hover/block:opacity-100"
149
+ className: "absolute top-1 right-1 z-10 opacity-0 transition-opacity pointer-coarse:opacity-100 group-hover/block:opacity-100"
150
150
  }
151
151
  )
152
152
  ] }) : /* @__PURE__ */ jsxs("p", { className: "mt-1 rounded-md border border-border bg-muted/30 px-2.5 py-2 text-xs italic text-muted-foreground", children: [
@@ -278,12 +278,12 @@ function JsonBlock({ label, value, slot }) {
278
278
  /* @__PURE__ */ jsxs("div", { className: "group/block relative mt-1", children: [
279
279
  /* @__PURE__ */ jsx("pre", { "data-kind-source": "explicit", className: "overflow-auto whitespace-pre-wrap break-all rounded-md border border-border bg-muted/40 px-2.5 py-2 font-mono text-[11px] leading-relaxed text-foreground", children: text }),
280
280
  /* @__PURE__ */ jsx(
281
- InlineCopyButton,
281
+ CopyButtons,
282
282
  {
283
- content: text,
284
- formatJson: false,
283
+ label: "Copy",
284
+ human: text,
285
285
  size: "sm",
286
- className: "opacity-0 transition-opacity pointer-coarse:opacity-100 group-hover/block:opacity-100"
286
+ className: "absolute top-1 right-1 z-10 opacity-0 transition-opacity pointer-coarse:opacity-100 group-hover/block:opacity-100"
287
287
  }
288
288
  )
289
289
  ] })
@@ -5,7 +5,7 @@ import { AlertTriangle, Braces, FileCode2, RefreshCw } from "lucide-react";
5
5
  import { cn } from "@ai-matrx/design-system";
6
6
  import { Skeleton } from "@ai-matrx/design-system";
7
7
  import { Badge, Button, Chip, SegmentedControl } from "@ai-matrx/design-system/controls";
8
- import { InlineCopyButton } from "../../../host/ui-slots.js";
8
+ import { CopyButtons } from "../../../host/ui-slots.js";
9
9
  import { useAppSelector } from "../../../store/hooks.js";
10
10
  import { SurfaceRuntimeProvider } from "../../../surfaces/runtime/SurfaceRuntimeContext.js";
11
11
  import {
@@ -68,12 +68,12 @@ function VariableGroup({
68
68
  ] }),
69
69
  /* @__PURE__ */ jsx("div", { className: "mt-1 whitespace-pre-wrap break-words rounded bg-muted/40 px-2 py-1 font-mono text-[11px] leading-relaxed text-foreground/90", children: f.value || /* @__PURE__ */ jsx("span", { className: "italic text-muted-foreground", children: "(empty)" }) }),
70
70
  /* @__PURE__ */ jsx(
71
- InlineCopyButton,
71
+ CopyButtons,
72
72
  {
73
- content: f.value,
74
- formatJson: false,
73
+ label: "Copy",
74
+ human: f.value,
75
75
  size: "xs",
76
- className: "opacity-0 transition-opacity pointer-coarse:opacity-100 group-hover/var:opacity-100"
76
+ className: "absolute top-1 right-1 z-10 opacity-0 transition-opacity pointer-coarse:opacity-100 group-hover/var:opacity-100"
77
77
  }
78
78
  )
79
79
  ] }, key);
@@ -201,12 +201,12 @@ function ResolvedView({
201
201
  className: "relative h-7 w-7 shrink-0",
202
202
  title: "Copy everything the agent receives",
203
203
  children: /* @__PURE__ */ jsx(
204
- InlineCopyButton,
204
+ CopyButtons,
205
205
  {
206
- content: copyAllText,
207
- formatJson: false,
206
+ label: "Copy all for AI",
207
+ human: copyAllText,
208
208
  size: "sm",
209
- tooltipText: "Copy all for AI"
209
+ className: "absolute top-1 right-1 z-10"
210
210
  }
211
211
  )
212
212
  }
@@ -286,12 +286,12 @@ function ResolvedBody({
286
286
  block ? /* @__PURE__ */ jsxs("div", { className: "group/block relative mt-1.5", children: [
287
287
  /* @__PURE__ */ jsx("pre", { className: "whitespace-pre-wrap break-words rounded-md border border-primary/20 bg-muted/40 px-2.5 py-2 font-mono text-[11px] leading-relaxed text-foreground", children: block }),
288
288
  /* @__PURE__ */ jsx(
289
- InlineCopyButton,
289
+ CopyButtons,
290
290
  {
291
- content: block,
292
- formatJson: false,
291
+ label: "Copy",
292
+ human: block,
293
293
  size: "sm",
294
- className: "opacity-0 transition-opacity pointer-coarse:opacity-100 group-hover/block:opacity-100"
294
+ className: "absolute top-1 right-1 z-10 opacity-0 transition-opacity pointer-coarse:opacity-100 group-hover/block:opacity-100"
295
295
  }
296
296
  )
297
297
  ] }) : /* @__PURE__ */ jsx("div", { className: "mt-1.5 rounded-md border border-border bg-muted/30 px-2.5 py-2 text-xs italic text-muted-foreground", children: "No block would be injected \u2014 no organization or scopes active." }),
@@ -50,7 +50,7 @@ function SandboxFileViewer({
50
50
  }, [initialPath, sandboxRowId, load]);
51
51
  const handleCopy = async () => {
52
52
  if (content == null) return;
53
- await copyText(content);
53
+ if (!await copyText(content)) return;
54
54
  setCopied(true);
55
55
  toast.success("File contents copied");
56
56
  setTimeout(() => setCopied(false), 1500);
@@ -55,8 +55,8 @@ function useCopy() {
55
55
  notify: (message, kind) => kind === "error" ? toast.error(message) : toast.success(message)
56
56
  });
57
57
  const [copied, setCopied] = useState(null);
58
- const copy = (text, id) => {
59
- copyText(text);
58
+ const copy = async (text, id) => {
59
+ if (!await copyText(text)) return;
60
60
  setCopied(id);
61
61
  setTimeout(() => setCopied(null), 1500);
62
62
  };
@@ -68,7 +68,7 @@ function TranscriptIntegrityCopyButtonInner({
68
68
  effectiveVisibleGroupLimit
69
69
  });
70
70
  try {
71
- await copyText(formatTranscriptIntegrityReport(report));
71
+ if (!await copyText(formatTranscriptIntegrityReport(report))) return;
72
72
  toast.success(
73
73
  report.anomalies.length === 0 ? "Transcript report copied \u2014 no anomalies detected" : `Transcript report copied \u2014 ${report.anomalies.length} anomal${report.anomalies.length === 1 ? "y" : "ies"} flagged`
74
74
  );
@@ -271,9 +271,9 @@ function BufferedObservationCard({
271
271
  const [copied, setCopied] = useState(false);
272
272
  const text = cycle.observations ?? "";
273
273
  const hasText = text.length > 0;
274
- const handleCopy = useCallback(() => {
274
+ const handleCopy = useCallback(async () => {
275
275
  if (!hasText) return;
276
- copyText(text);
276
+ if (!await copyText(text)) return;
277
277
  setCopied(true);
278
278
  setTimeout(() => setCopied(false), 1500);
279
279
  }, [text, hasText]);
@@ -508,9 +508,9 @@ function TextPayload({
508
508
  });
509
509
  const [copied, setCopied] = useState(false);
510
510
  const hasText = typeof text === "string" && text.length > 0;
511
- const handleCopy = useCallback(() => {
511
+ const handleCopy = useCallback(async () => {
512
512
  if (!text) return;
513
- copyText(text);
513
+ if (!await copyText(text)) return;
514
514
  setCopied(true);
515
515
  setTimeout(() => setCopied(false), 1500);
516
516
  }, [text]);
@@ -91,9 +91,9 @@ function DirectTestMode({
91
91
  close,
92
92
  conversationId
93
93
  ]);
94
- const handleCopy = useCallback(() => {
94
+ const handleCopy = useCallback(async () => {
95
95
  if (responseText) {
96
- copyText(kindTextToMarkdown(responseText));
96
+ if (!await copyText(kindTextToMarkdown(responseText))) return;
97
97
  setCopied(true);
98
98
  setTimeout(() => setCopied(false), 2e3);
99
99
  }
@@ -168,7 +168,8 @@ function PayloadTab({ conversationId }) {
168
168
  if (!request) return;
169
169
  try {
170
170
  const text = JSON.stringify(request, null, 2);
171
- void copyText(text).then(() => {
171
+ void copyText(text).then((copied2) => {
172
+ if (!copied2) return;
172
173
  setCopied(true);
173
174
  window.setTimeout(() => setCopied(false), 1500);
174
175
  });
@@ -26,7 +26,8 @@ function CopyableValue({
26
26
  });
27
27
  const [copied, setCopied] = useState(false);
28
28
  const handleCopy = () => {
29
- void copyText(value).then(() => {
29
+ void copyText(value).then((copied2) => {
30
+ if (!copied2) return;
30
31
  setCopied(true);
31
32
  setTimeout(() => setCopied(false), 1200);
32
33
  });
@@ -2,7 +2,9 @@
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { Badge, Button } from "@ai-matrx/design-system/controls";
4
4
  import { useState } from "react";
5
- import { Save, Loader2, AlertTriangle, Eye } from "lucide-react";
5
+ import { Save, Loader2, AlertTriangle, Eye, Undo2 } from "lucide-react";
6
+ import { useAppDispatch } from "../../../store/hooks.js";
7
+ import { resetAllAgentFields } from "../../redux/agent-definition/slice.js";
6
8
  import { useChatCanvasTab } from "../../../host/canvas.js";
7
9
  import { AGENT_UNSAVED_CHANGES_KIND } from "../../../host/canvas-tabs.js";
8
10
  import { cn } from "@ai-matrx/design-system";
@@ -41,7 +43,9 @@ function AgentSaveStatus({
41
43
  reachBadge,
42
44
  available
43
45
  } = useAgentSaveAction(agentId, { editModeOverride });
46
+ const dispatch = useAppDispatch();
44
47
  const [settingsOpen, setSettingsOpen] = useState(false);
48
+ const [confirmDiscardOpen, setConfirmDiscardOpen] = useState(false);
45
49
  const diffTab = useChatCanvasTab({ kind: AGENT_UNSAVED_CHANGES_KIND, key: agentId });
46
50
  const handleSelectModel = () => {
47
51
  setShowModelWarning(false);
@@ -67,6 +71,7 @@ function AgentSaveStatus({
67
71
  isEditMode && isDirty && /* @__PURE__ */ jsxs(Fragment, { children: [
68
72
  /* @__PURE__ */ jsx(Badge, { tone: "warning", children: isNewRoute ? "Not saved" : "Unsaved" }),
69
73
  /* @__PURE__ */ jsx(ErrorAlchemyMenu, {}),
74
+ !isNewRoute && /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: /* @__PURE__ */ jsx(Undo2, {}), onClick: () => setConfirmDiscardOpen(true), disabled: isLoading, title: "Discard unsaved changes", "aria-label": "Discard unsaved changes" }),
70
75
  !isNewRoute && /* @__PURE__ */ jsx(Button, { variant: "quiet", pressed: diffTab.isVisible, icon: /* @__PURE__ */ jsx(Eye, {}), onClick: () => diffTab.toggle({ title: "Unsaved changes", data: { agentId } }), title: "View unsaved changes", "aria-label": "View unsaved changes" })
71
76
  ] }),
72
77
  isEditMode && /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: isLoading ? /* @__PURE__ */ jsx(Loader2, { className: "animate-spin" }) : /* @__PURE__ */ jsx(Save, {}), onClick: handleSave, disabled: !canSave, title: isReadOnlySave ? "View only \u2014 create your copy to save changes" : isNewRoute ? "Save new agent" : isDirty ? "Save changes" : "No unsaved changes", "aria-label": isReadOnlySave ? "View only \u2014 create your copy to save changes" : isNewRoute ? "Save new agent" : isDirty ? "Save changes" : "No unsaved changes" }),
@@ -90,6 +95,22 @@ function AgentSaveStatus({
90
95
  /* @__PURE__ */ jsx(AlertDialogAction, { onClick: handleSelectModel, children: "Select a Model" })
91
96
  ] })
92
97
  ] }) }),
98
+ /* @__PURE__ */ jsx(AlertDialog, { open: confirmDiscardOpen, onOpenChange: setConfirmDiscardOpen, children: /* @__PURE__ */ jsxs(AlertDialogContent, { children: [
99
+ /* @__PURE__ */ jsxs(AlertDialogHeader, { children: [
100
+ /* @__PURE__ */ jsx(AlertDialogTitle, { children: "Discard unsaved changes?" }),
101
+ /* @__PURE__ */ jsx(AlertDialogDescription, { children: "The agent goes back to its last saved version." })
102
+ ] }),
103
+ /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [
104
+ /* @__PURE__ */ jsx(AlertDialogCancel, { children: "Keep editing" }),
105
+ /* @__PURE__ */ jsx(
106
+ AlertDialogAction,
107
+ {
108
+ onClick: () => dispatch(resetAllAgentFields({ id: agentId })),
109
+ children: "Discard"
110
+ }
111
+ )
112
+ ] })
113
+ ] }) }),
93
114
  /* @__PURE__ */ jsx(
94
115
  AgentSettingsModal,
95
116
  {
@@ -8,7 +8,10 @@ import {
8
8
  selectAgentIsReadOnly,
9
9
  selectAgentAccessResolved
10
10
  } from "../redux/agent-definition/selectors.js";
11
- import { setAgentField } from "../redux/agent-definition/slice.js";
11
+ import {
12
+ resetAllAgentFields,
13
+ setAgentField
14
+ } from "../redux/agent-definition/slice.js";
12
15
  import { readField } from "@ai-matrx/agents/field-flags";
13
16
  import { toast } from "../../host/notify.js";
14
17
  const STORAGE_PREFIX = "agent-autosave:";
@@ -65,7 +68,16 @@ function useAgentAutoSave(agentId) {
65
68
  if (restored.length > 0) {
66
69
  restoredRef.current = true;
67
70
  toast.info(
68
- `Restored unsaved changes to ${record.name || "this agent"} from this browser (${restored.join(", ")}). Save to keep them, or undo to discard.`
71
+ `Restored unsaved changes to ${record.name || "this agent"} from this browser (${restored.join(", ")}). Save to keep them, or discard them.`,
72
+ {
73
+ id: `agent-autosave-restored:${agentId}`,
74
+ duration: 2e4,
75
+ action: {
76
+ label: "Discard",
77
+ // Back to the saved agent; the clean-state effect drops the backup.
78
+ onClick: () => dispatch(resetAllAgentFields({ id: agentId }))
79
+ }
80
+ }
69
81
  );
70
82
  }
71
83
  }, [agentId, record, fetchStatus, dispatch]);
@@ -78,9 +90,14 @@ function useAgentAutoSave(agentId) {
78
90
  try {
79
91
  const snapshot = { _dirty: true };
80
92
  if (record._dirtyFields) {
81
- for (const field of Object.keys(
82
- record._dirtyFields
83
- )) {
93
+ const fields = new Set(
94
+ Object.keys(record._dirtyFields)
95
+ );
96
+ if (fields.has("modelId") || fields.has("settings")) {
97
+ fields.add("modelId");
98
+ fields.add("settings");
99
+ }
100
+ for (const field of fields) {
84
101
  snapshot[field] = readField(record, field);
85
102
  }
86
103
  }
@@ -44,7 +44,7 @@ function PromptPreviewContent({
44
44
  }, [conversationId, store, nonce]);
45
45
  const copy = async (label, text) => {
46
46
  try {
47
- await copyText(text);
47
+ if (!await copyText(text)) return;
48
48
  toast.success(`${label} copied`);
49
49
  } catch {
50
50
  toast.error("Couldn't copy");
@@ -182,6 +182,7 @@ function pushUndoEntry(record, field, previousValue) {
182
182
  function applyFieldEdit(record, field, value) {
183
183
  const previousValue = record[field];
184
184
  const wasDirty = hasField(record._dirtyFields, field);
185
+ if (!wasDirty && isEqual(value, previousValue)) return;
185
186
  if (!wasDirty) {
186
187
  record._fieldHistory[field] = previousValue;
187
188
  }
@@ -579,6 +579,7 @@ export declare const requestModelSwitch: import("@reduxjs/toolkit").AsyncThunk<{
579
579
  newModelControls: import("./types.js").NormalizedControls;
580
580
  mode: ResolutionMode;
581
581
  customActions: {};
582
+ offeringId?: string | undefined;
582
583
  };
583
584
  newDefaults: Partial<{
584
585
  model?: string | undefined;
@@ -831,6 +832,8 @@ export declare const requestModelSwitch: import("@reduxjs/toolkit").AsyncThunk<{
831
832
  }, {
832
833
  agentId: string;
833
834
  newModelId: string;
835
+ /** The new model's class picked in the same gesture (applied on confirm). */
836
+ offeringId?: string;
834
837
  }, import("@reduxjs/toolkit").AsyncThunkConfig>;
835
838
  /**
836
839
  * Save agent settings to the database.
@@ -154,7 +154,11 @@ const loadAgentSettingsDirect = createAsyncThunk(
154
154
  );
155
155
  const requestModelSwitch = createAsyncThunk(
156
156
  "agentSettings/requestModelSwitch",
157
- async ({ agentId, newModelId }, { getState, rejectWithValue }) => {
157
+ async ({
158
+ agentId,
159
+ newModelId,
160
+ offeringId
161
+ }, { getState, rejectWithValue }) => {
158
162
  const state = getState();
159
163
  const newModel = selectModelById(state, newModelId);
160
164
  if (!newModel) {
@@ -200,7 +204,8 @@ const requestModelSwitch = createAsyncThunk(
200
204
  // new model does not carry natively (settings-translation K7). Reset
201
205
  // or remove is the person's explicit choice in the dialog.
202
206
  mode: "keep_all",
203
- customActions: {}
207
+ customActions: {},
208
+ ...offeringId ? { offeringId } : {}
204
209
  },
205
210
  newDefaults
206
211
  };
@@ -433,6 +438,11 @@ const agentSettingsSlice = createSlice({
433
438
  ...resolved,
434
439
  model: pending.newModelId
435
440
  };
441
+ if (pending.offeringId) {
442
+ finalSettings.offering_id = pending.offeringId;
443
+ } else {
444
+ delete finalSettings.offering_id;
445
+ }
436
446
  if (entry.context === "builder") {
437
447
  entry.defaults = finalSettings;
438
448
  } else {
@@ -67,6 +67,13 @@ export interface PendingModelSwitch {
67
67
  mode: ResolutionMode;
68
68
  /** Per-conflict override when mode === 'custom' */
69
69
  customActions: Partial<Record<keyof AgentSettings, ConflictAction>>;
70
+ /**
71
+ * The CLASS (offering) picked with the new model, applied on confirm.
72
+ * Absent = no class picked → the old model's pin is dropped on confirm (a
73
+ * class belongs to exactly one model). Never written before confirm, so the
74
+ * entry never pairs the old model with the new model's class.
75
+ */
76
+ offeringId?: string;
70
77
  }
71
78
  export interface AgentVariable {
72
79
  name: string;
@@ -25,7 +25,8 @@ function useCopyText(text) {
25
25
  });
26
26
  const [copied, setCopied] = useState(false);
27
27
  const copy = () => {
28
- copyText(text).then(() => {
28
+ copyText(text).then((copied2) => {
29
+ if (!copied2) return;
29
30
  setCopied(true);
30
31
  setTimeout(() => setCopied(false), 1500);
31
32
  });
@@ -26,7 +26,8 @@ function useCopyText(text) {
26
26
  });
27
27
  const [copied, setCopied] = useState(false);
28
28
  const copy = () => {
29
- copyText(text).then(() => {
29
+ copyText(text).then((copied2) => {
30
+ if (!copied2) return;
30
31
  setCopied(true);
31
32
  setTimeout(() => setCopied(false), 1500);
32
33
  });
@@ -169,6 +169,16 @@ export declare function extractInspectableText(record: MessageRecord | undefined
169
169
  * CONVERSATION_FAILURE_AND_RETRY_FE_GUIDE.md.
170
170
  */
171
171
  export declare function isFailedRecord(record: MessageRecord | undefined): boolean;
172
+ /**
173
+ * The provider stop reason a reloaded turn was saved with, when it is NOT a
174
+ * normal finish — or null. A turn the provider stopped early can be saved as
175
+ * an ordinary `active` message with its partial text and only
176
+ * `metadata.finish_reason` telling the truth (a recitation stop, 2026-10-06);
177
+ * that row reloaded with no error at all.
178
+ */
179
+ export declare function abnormalFinishReason(record: MessageRecord | undefined): string | null;
180
+ /** The sentence a reloaded early-stopped turn shows. */
181
+ export declare function earlyStopMessage(reason: string): string;
172
182
  /**
173
183
  * The persisted error text for a failed turn. Prefers the structured top-level
174
184
  * `error.message`; falls back to the legacy `metadata.error` string, then to
@@ -282,7 +282,33 @@ function isFailedRecord(record) {
282
282
  if (record.status === "failed") return true;
283
283
  if (record.error) return true;
284
284
  const md = record.metadata;
285
- return !!md && typeof md === "object" && !Array.isArray(md) && md.failed === true;
285
+ if (!!md && typeof md === "object" && !Array.isArray(md) && md.failed === true) {
286
+ return true;
287
+ }
288
+ return abnormalFinishReason(record) !== null;
289
+ }
290
+ const NORMAL_FINISH_REASONS = /* @__PURE__ */ new Set([
291
+ "stop",
292
+ "end_turn",
293
+ "stop_sequence",
294
+ "tool_use",
295
+ "tool_calls",
296
+ "function_call",
297
+ "pause_turn",
298
+ "completed",
299
+ "success"
300
+ ]);
301
+ function abnormalFinishReason(record) {
302
+ if (!record || record.role !== "assistant") return null;
303
+ const md = record.metadata;
304
+ if (!md || typeof md !== "object" || Array.isArray(md)) return null;
305
+ const reason = md.finish_reason;
306
+ if (typeof reason !== "string" || reason.length === 0) return null;
307
+ const normalized = reason.toLowerCase().replace(/^finishreason\./, "");
308
+ return NORMAL_FINISH_REASONS.has(normalized) ? null : normalized;
309
+ }
310
+ function earlyStopMessage(reason) {
311
+ return reason === "max_tokens" || reason === "length" ? "The response hit its length limit before it finished." : "The response was stopped early by the model provider, so it may be incomplete.";
286
312
  }
287
313
  function extractRecordError(record) {
288
314
  if (!record) return void 0;
@@ -294,6 +320,8 @@ function extractRecordError(record) {
294
320
  const err = md.error;
295
321
  if (typeof err === "string" && err.length > 0) return err;
296
322
  }
323
+ const stopped = abnormalFinishReason(record);
324
+ if (stopped) return earlyStopMessage(stopped);
297
325
  const flat = extractFlatText(record);
298
326
  return flat.length > 0 ? flat : void 0;
299
327
  }
@@ -545,6 +573,8 @@ const selectMessagesInterleavedRuns = (conversationId, messageIds) => {
545
573
  };
546
574
  export {
547
575
  EMPTY_CONVERSATION_MESSAGES,
576
+ abnormalFinishReason,
577
+ earlyStopMessage,
548
578
  extractAnswerDocumentText,
549
579
  extractContentBlocks,
550
580
  extractFlatText,
@@ -69,6 +69,10 @@ import {
69
69
  appendCitation
70
70
  } from "../active-requests/active-requests.slice.js";
71
71
  import { parseNormalizedCitation } from "../messages/message-citations.js";
72
+ import {
73
+ terminalFailureFromCompletion,
74
+ terminalFailureFromRecordUpdate
75
+ } from "./terminal-request-failure.js";
72
76
  import { confirmServerSync } from "../conversations/conversations.slice.js";
73
77
  import { receivedFsChange } from "../../../../host/ui-slots.js";
74
78
  import {
@@ -430,6 +434,17 @@ async function processStream({
430
434
  terminalTransportController.abort("post-terminal-grace");
431
435
  }, POST_TERMINAL_GRACE_MS);
432
436
  };
437
+ const failRequestFromTerminalStatus = (error) => {
438
+ if (!error) return;
439
+ if (getState().activeRequests.byRequestId[requestId]?.status === "error") {
440
+ return;
441
+ }
442
+ console.warn(
443
+ `[stream:${requestId.slice(0, 8)}] user request ended FAILED without an error event (${error.error_type}) \u2014 surfacing it as a failed turn.`
444
+ );
445
+ dispatch(setRequestStatus({ requestId, status: "error", error }));
446
+ dispatch(setInstanceStatus({ conversationId, status: "error" }));
447
+ };
433
448
  let lastTransportSeq = getState().activeRequests.byRequestId[requestId]?.lastTransportSeq ?? 0;
434
449
  let transportStreamId = getState().activeRequests.byRequestId[requestId]?.transportStreamId ?? null;
435
450
  let retainedEntry;
@@ -635,6 +650,7 @@ async function processStream({
635
650
  }
636
651
  if (d.operation === "user_request") {
637
652
  dispatch(setCompletion({ requestId, data: d }));
653
+ failRequestFromTerminalStatus(terminalFailureFromCompletion(d));
638
654
  completionStats = result;
639
655
  const totals = completionStats.total_usage?.total;
640
656
  if (totals) {
@@ -1664,6 +1680,7 @@ async function processStream({
1664
1680
  refreshUserMessageWithoutBody(d.record_id);
1665
1681
  }
1666
1682
  } else if (d.table === "user_request") {
1683
+ failRequestFromTerminalStatus(terminalFailureFromRecordUpdate(d));
1667
1684
  dispatch(
1668
1685
  patchUserRequest({
1669
1686
  id: d.record_id,
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A request that ENDED FAILED without an `error` event.
3
+ *
4
+ * The server has three ways to say a user request failed, and only one of
5
+ * them is the `error` event: a terminal failure it persisted on purpose (a
6
+ * provider safety / recitation stop, a model refusal, any finish-reason
7
+ * failure) is announced as a `user_request` completion with
8
+ * `status: "failed"` and/or a `record_update` moving the `user_request` row
9
+ * to `failed`, then a normal `end`. Before this module the stream treated
10
+ * both as bookkeeping, the `end` handler marked the request `complete`, and
11
+ * the turn showed NO error — while its half-written content (a flashcard set
12
+ * cut off by a recitation stop, 2026-10-06) sat there looking finished.
13
+ *
14
+ * Pure: reads the event payload, returns the `ErrorPayload` the request
15
+ * should carry, or null when the event is not a terminal failure. It never
16
+ * matches on the message TEXT (the server is free to reword it) — only on
17
+ * the operation/table, the status, and the structured fields.
18
+ */
19
+ import type { CompletionPayload, ErrorPayload, RecordUpdatePayload } from "@ai-matrx/agents/generated/stream-events";
20
+ /** error_type when the server sent no structured one. */
21
+ export declare const TERMINAL_FAILURE_ERROR_TYPE = "request_failed";
22
+ /** Shown when the server said "failed" and nothing more. */
23
+ export declare const TERMINAL_FAILURE_FALLBACK_MESSAGE = "The response stopped before it finished.";
24
+ /** A `user_request` completion whose status is not success. */
25
+ export declare function terminalFailureFromCompletion(d: CompletionPayload): ErrorPayload | null;
26
+ /** A `record_update` moving the `user_request` row to `failed`. */
27
+ export declare function terminalFailureFromRecordUpdate(d: RecordUpdatePayload): ErrorPayload | null;
@@ -0,0 +1,56 @@
1
+ const TERMINAL_FAILURE_ERROR_TYPE = "request_failed";
2
+ const TERMINAL_FAILURE_FALLBACK_MESSAGE = "The response stopped before it finished.";
3
+ function asRecord(value) {
4
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
5
+ }
6
+ function readStructuredError(raw) {
7
+ if (typeof raw === "string") {
8
+ const trimmed = raw.trim();
9
+ if (trimmed.startsWith("{")) {
10
+ try {
11
+ return readStructuredError(JSON.parse(trimmed));
12
+ } catch {
13
+ }
14
+ }
15
+ return { errorType: null, message: trimmed || null };
16
+ }
17
+ const obj = asRecord(raw);
18
+ if (!obj) return { errorType: null, message: null };
19
+ const errorType = typeof obj.error_type === "string" ? obj.error_type : typeof obj.type === "string" ? obj.type : null;
20
+ const message = typeof obj.user_message === "string" && obj.user_message ? obj.user_message : typeof obj.message === "string" && obj.message ? obj.message : null;
21
+ return { errorType, message };
22
+ }
23
+ function buildPayload(source, fields, finishReason) {
24
+ const structured = readStructuredError(fields?.error);
25
+ const errorType = structured.errorType ?? (typeof fields?.error_type === "string" ? fields.error_type : null) ?? TERMINAL_FAILURE_ERROR_TYPE;
26
+ const message = structured.message ?? TERMINAL_FAILURE_FALLBACK_MESSAGE;
27
+ return {
28
+ error_type: errorType,
29
+ message,
30
+ user_message: message,
31
+ details: {
32
+ source,
33
+ ...finishReason ? { finish_reason: finishReason } : {},
34
+ ...typeof fields?.reason === "string" ? { reason: fields.reason } : {}
35
+ }
36
+ };
37
+ }
38
+ function terminalFailureFromCompletion(d) {
39
+ if (d.operation !== "user_request" || d.status !== "failed") return null;
40
+ const result = asRecord(d.result);
41
+ const metadata = asRecord(result?.metadata);
42
+ const finishReason = typeof result?.finish_reason === "string" ? result.finish_reason : null;
43
+ return buildPayload("completion", metadata ?? result, finishReason);
44
+ }
45
+ function terminalFailureFromRecordUpdate(d) {
46
+ if (d.table !== "user_request" || d.status !== "failed") return null;
47
+ const metadata = asRecord(d.metadata);
48
+ const finishReason = typeof metadata?.finish_reason === "string" ? metadata.finish_reason : null;
49
+ return buildPayload("record_update", metadata, finishReason);
50
+ }
51
+ export {
52
+ TERMINAL_FAILURE_ERROR_TYPE,
53
+ TERMINAL_FAILURE_FALLBACK_MESSAGE,
54
+ terminalFailureFromCompletion,
55
+ terminalFailureFromRecordUpdate
56
+ };
@@ -185,7 +185,7 @@ function AssistantMessage({
185
185
  variant: "quiet",
186
186
  onClick: async () => {
187
187
  if (!audioUrl) return;
188
- await copyText(audioUrl);
188
+ if (!await copyText(audioUrl)) return;
189
189
  setIsAudioLinkCopied(true);
190
190
  setTimeout(() => setIsAudioLinkCopied(false), 2e3);
191
191
  },
@@ -135,7 +135,8 @@ function UserMessage({
135
135
  }, [isEditing]);
136
136
  const handleCopy = (e) => {
137
137
  e.stopPropagation();
138
- copyText(message.content).then(() => {
138
+ copyText(message.content).then((copied) => {
139
+ if (!copied) return;
139
140
  setIsCopied(true);
140
141
  setTimeout(() => setIsCopied(false), 2e3);
141
142
  });
@@ -185,7 +185,7 @@ function AssistantMessage({
185
185
  variant: "quiet",
186
186
  onClick: async () => {
187
187
  if (!audioUrl) return;
188
- await copyText(audioUrl);
188
+ if (!await copyText(audioUrl)) return;
189
189
  setIsAudioLinkCopied(true);
190
190
  setTimeout(() => setIsAudioLinkCopied(false), 2e3);
191
191
  },
@@ -135,7 +135,8 @@ function UserMessage({
135
135
  }, [isEditing]);
136
136
  const handleCopy = (e) => {
137
137
  e.stopPropagation();
138
- copyText(message.content).then(() => {
138
+ copyText(message.content).then((copied) => {
139
+ if (!copied) return;
139
140
  setIsCopied(true);
140
141
  setTimeout(() => setIsCopied(false), 2e3);
141
142
  });
@@ -18887,6 +18887,7 @@ export type ChatDatabase = {
18887
18887
  container_side: string;
18888
18888
  conveys_max: Database["public"]["Enums"]["permission_level"];
18889
18889
  created_at: string;
18890
+ id: string;
18890
18891
  is_active: boolean;
18891
18892
  label: string | null;
18892
18893
  notes: string | null;
@@ -18900,6 +18901,7 @@ export type ChatDatabase = {
18900
18901
  container_side?: string;
18901
18902
  conveys_max?: Database["public"]["Enums"]["permission_level"];
18902
18903
  created_at?: string;
18904
+ id?: string;
18903
18905
  is_active?: boolean;
18904
18906
  label?: string | null;
18905
18907
  notes?: string | null;
@@ -18913,6 +18915,7 @@ export type ChatDatabase = {
18913
18915
  container_side?: string;
18914
18916
  conveys_max?: Database["public"]["Enums"]["permission_level"];
18915
18917
  created_at?: string;
18918
+ id?: string;
18916
18919
  is_active?: boolean;
18917
18920
  label?: string | null;
18918
18921
  notes?: string | null;
@@ -38345,6 +38348,18 @@ export type ChatDatabase = {
38345
38348
  isSetofReturn: true;
38346
38349
  };
38347
38350
  };
38351
+ get_pick_list_for_selection: {
38352
+ Args: {
38353
+ p_list_id: string;
38354
+ };
38355
+ Returns: Json;
38356
+ };
38357
+ get_pick_lists_summary: {
38358
+ Args: {
38359
+ p_user_id: string;
38360
+ };
38361
+ Returns: Json;
38362
+ };
38348
38363
  get_project_members_with_users: {
38349
38364
  Args: {
38350
38365
  p_project_id: string;
@@ -38532,12 +38547,6 @@ export type ChatDatabase = {
38532
38547
  };
38533
38548
  Returns: Json;
38534
38549
  };
38535
- get_pick_list_for_selection: {
38536
- Args: {
38537
- p_list_id: string;
38538
- };
38539
- Returns: Json;
38540
- };
38541
38550
  get_tables_and_columns: {
38542
38551
  Args: never;
38543
38552
  Returns: {
@@ -38695,12 +38704,6 @@ export type ChatDatabase = {
38695
38704
  };
38696
38705
  Returns: Json;
38697
38706
  };
38698
- get_pick_lists_summary: {
38699
- Args: {
38700
- p_user_id: string;
38701
- };
38702
- Returns: Json;
38703
- };
38704
38707
  get_user_messages: {
38705
38708
  Args: {
38706
38709
  p_feedback_id: string;
@@ -163,7 +163,7 @@ export interface ChatUiSlots {
163
163
  SystemInstructionModal: AnyComponent;
164
164
  confirm: AnyFn;
165
165
  copyRichContent: AnyFn;
166
- copyToClipboard: AnyFn;
166
+ copyContent: AnyFn;
167
167
  useTablesEverywhere: AnyFn;
168
168
  useTextareaFormatting: AnyFn;
169
169
  useClipboardPaste: AnyFn;
@@ -354,7 +354,6 @@ export interface ChatUiSlots {
354
354
  AccessGate: AnyComponent;
355
355
  ReferenceCopyMenuItem: AnyComponent;
356
356
  ReferenceCopyButton: AnyComponent;
357
- InlineCopyButton: AnyComponent;
358
357
  MandateNotesPanel: AnyComponent;
359
358
  SurfaceBoundAgentsList: SlotComponent<AgentsListProps>;
360
359
  ProposedDirectivesZone: AnyComponent;
@@ -508,7 +507,7 @@ export declare const WebpageSnapshotView: AnyComponent;
508
507
  export declare function hostFn<K extends keyof ChatUiSlots>(name: K, fallback?: AnyFn): ChatUiSlots[K];
509
508
  export declare const confirm: AnyFn;
510
509
  export declare const copyRichContent: AnyFn;
511
- export declare const copyToClipboard: AnyFn;
510
+ export declare const copyContent: AnyFn;
512
511
  export declare const useTablesEverywhere: AnyFn;
513
512
  export declare const useTextareaFormatting: AnyFn;
514
513
  export declare const useClipboardPaste: AnyFn;
@@ -620,7 +619,6 @@ export declare const AnswerTextPreview: AnyComponent;
620
619
  export declare const AccessGate: AnyComponent;
621
620
  export declare const ReferenceCopyMenuItem: AnyComponent;
622
621
  export declare const ReferenceCopyButton: AnyComponent;
623
- export declare const InlineCopyButton: AnyComponent;
624
622
  export declare const MandateNotesPanel: AnyComponent;
625
623
  export declare const SurfaceBoundAgentsList: SlotComponent<AgentsListProps>;
626
624
  export declare const ProposedDirectivesZone: AnyComponent;
@@ -69,7 +69,14 @@ const failingThunk = (name) => () => () => {
69
69
  const STAND_INS = {
70
70
  confirm: async () => false,
71
71
  copyRichContent: async () => false,
72
- copyToClipboard: (text) => copyText(String(text)),
72
+ // Content-shaped like the host's `copyContent(content, options)`: a string copies as text, anything
73
+ // else as JSON. A failed copy is reported through `onError` and returns false, never swallowed.
74
+ copyContent: async (content, options) => {
75
+ const text = typeof content === "string" ? content : JSON.stringify(content, null, 2);
76
+ const landed = await copyText(text ?? "");
77
+ if (!landed) options?.onError?.(new Error("Failed to copy"));
78
+ return landed;
79
+ },
73
80
  useTablesEverywhere: () => NONE,
74
81
  useCenterControlFit: () => true,
75
82
  connectorDefinitionFromMcp: () => null,
@@ -228,7 +235,7 @@ function hostFn(name, fallback) {
228
235
  }
229
236
  const confirm = slotFn("confirm");
230
237
  const copyRichContent = slotFn("copyRichContent");
231
- const copyToClipboard = slotFn("copyToClipboard");
238
+ const copyContent = slotFn("copyContent");
232
239
  const useTablesEverywhere = slotFn("useTablesEverywhere");
233
240
  const useTextareaFormatting = slotFn("useTextareaFormatting", () => void 0);
234
241
  const useClipboardPaste = slotFn("useClipboardPaste", () => void 0);
@@ -339,7 +346,6 @@ const AnswerTextPreview = slotComponent("AnswerTextPreview", unregisteredWidget(
339
346
  const AccessGate = slotComponent("AccessGate", unregisteredWidget("AccessGate"));
340
347
  const ReferenceCopyMenuItem = slotComponent("ReferenceCopyMenuItem", unregisteredWidget("ReferenceCopyMenuItem"));
341
348
  const ReferenceCopyButton = slotComponent("ReferenceCopyButton", unregisteredWidget("ReferenceCopyButton"));
342
- const InlineCopyButton = slotComponent("InlineCopyButton", unregisteredWidget("InlineCopyButton"));
343
349
  const MandateNotesPanel = slotComponent("MandateNotesPanel", unregisteredWidget("MandateNotesPanel"));
344
350
  const SurfaceBoundAgentsList = slotComponent("SurfaceBoundAgentsList", unregisteredWidget("SurfaceBoundAgentsList"));
345
351
  const ProposedDirectivesZone = slotComponent("ProposedDirectivesZone", unregisteredWidget("ProposedDirectivesZone"));
@@ -548,7 +554,6 @@ export {
548
554
  IconButton,
549
555
  InPlaceEditor,
550
556
  InfoHint,
551
- InlineCopyButton,
552
557
  InputCapabilitiesEditor,
553
558
  ItemMenu,
554
559
  ItemRow,
@@ -628,8 +633,8 @@ export {
628
633
  confirm,
629
634
  connectorDefinitionFromMcp,
630
635
  convertMarkdownToHtml,
636
+ copyContent,
631
637
  copyRichContent,
632
- copyToClipboard,
633
638
  createHtmlPage,
634
639
  createSandboxFilesystemAdapter,
635
640
  currentCostUnit,
@@ -17,7 +17,7 @@ import {
17
17
  Share2
18
18
  } from "lucide-react";
19
19
  import { getMarkdownStylesheet } from "@ai-matrx/print/markdown";
20
- import { AdvancedMenu, copyToClipboard } from "../../host/ui-slots.js";
20
+ import { AdvancedMenu, copyContent } from "../../host/ui-slots.js";
21
21
  import { EmailInputDialog } from "../../host/ui-slots.js";
22
22
  import { AuthGateDialog } from "../../host/ui-slots.js";
23
23
  import { notesCreate } from "../../host/ui-slots.js";
@@ -125,7 +125,7 @@ const PublicMessageOptionsMenu = ({
125
125
  onClose();
126
126
  };
127
127
  const handleCopyPlain = async () => {
128
- await copyToClipboard(content, {
128
+ await copyContent(content, {
129
129
  onSuccess: () => {
130
130
  },
131
131
  onError: (error) => {
@@ -134,7 +134,7 @@ const PublicMessageOptionsMenu = ({
134
134
  });
135
135
  };
136
136
  const handleCopyGoogleDocs = async () => {
137
- await copyToClipboard(content, {
137
+ await copyContent(content, {
138
138
  isMarkdown: true,
139
139
  formatForGoogleDocs: true,
140
140
  onSuccess: () => {
@@ -147,7 +147,7 @@ const PublicMessageOptionsMenu = ({
147
147
  });
148
148
  };
149
149
  const handleCopyWithThinking = async () => {
150
- await copyToClipboard(content, {
150
+ await copyContent(content, {
151
151
  isMarkdown: true,
152
152
  includeThinking: true,
153
153
  onSuccess: () => {
@@ -167,7 +167,7 @@ const PublicMessageOptionsMenu = ({
167
167
  toast.info("HTML preview not available here");
168
168
  return;
169
169
  }
170
- await copyToClipboard(content, {
170
+ await copyContent(content, {
171
171
  isMarkdown: true,
172
172
  formatForWordPress: true,
173
173
  showHtmlPreview: true,
@@ -210,7 +210,7 @@ const PublicMessageOptionsMenu = ({
210
210
  }
211
211
  };
212
212
  const handleCopyCompleteHTML = async () => {
213
- await copyToClipboard(content, {
213
+ await copyContent(content, {
214
214
  isMarkdown: true,
215
215
  formatForWordPress: true,
216
216
  showHtmlPreview: true,
@@ -229,7 +229,7 @@ const PublicMessageOptionsMenu = ({
229
229
  ${filteredHtml}
230
230
  </body>
231
231
  </html>`;
232
- await copyToClipboard(completeHTML, {
232
+ await copyContent(completeHTML, {
233
233
  onSuccess: () => {
234
234
  },
235
235
  onError: (error) => {
@@ -115,7 +115,9 @@ export declare function setRoleSelection(args: {
115
115
  scope: PrefScopeInput;
116
116
  }): Promise<void>;
117
117
  /** Move one agent choice / roster item to Trash (restorable; re-selecting revives it). */
118
- export declare function deleteRolePref(prefId: string): Promise<void>;
118
+ export declare function deleteRolePref(prefId: string, opts?: {
119
+ platformTier?: boolean;
120
+ }): Promise<void>;
119
121
  export declare function addRosterItem(args: {
120
122
  surfaceName: string;
121
123
  roleName: string;
@@ -279,6 +279,9 @@ function matchScope(q, scope, tierOrganization) {
279
279
  }
280
280
  return q.is("user_id", null).is("scope_id", null).eq("organization_id", tierOrganization);
281
281
  }
282
+ function withPlatformRoleAdminLane(request, on) {
283
+ return on ? request.setHeader("x-matrx-admin-lane", "1") : request;
284
+ }
282
285
  async function setRoleSelection(args) {
283
286
  const {
284
287
  surfaceName,
@@ -291,33 +294,46 @@ async function setRoleSelection(args) {
291
294
  const client = sb();
292
295
  let q = client.schema("ui").from("ui_surface_agent_pref").select("id").eq("surface_name", surfaceName).eq("role_name", roleName).eq("kind", "selection").eq("position", position);
293
296
  q = matchScope(q, scope, await tierOrganizationFor(scope));
294
- const { data: existing, error: findErr } = await q.maybeSingle();
297
+ const platformLane = scope.global === true;
298
+ const { data: existing, error: findErr } = await withPlatformRoleAdminLane(
299
+ q,
300
+ platformLane
301
+ ).maybeSingle();
295
302
  if (findErr) throw findErr;
296
303
  if (existing) {
297
304
  await writeOne(
298
- client.schema("ui").from("ui_surface_agent_pref").update({
299
- agent_id: agentId,
300
- deleted_at: null,
301
- ...settings ? { settings } : {}
302
- }).eq("id", existing.id).select("id"),
305
+ withPlatformRoleAdminLane(
306
+ client.schema("ui").from("ui_surface_agent_pref").update({
307
+ agent_id: agentId,
308
+ deleted_at: null,
309
+ ...settings ? { settings } : {}
310
+ }).eq("id", existing.id).select("id"),
311
+ platformLane
312
+ ),
303
313
  { action: "save", noun: "agent choice" }
304
314
  );
305
315
  return;
306
316
  }
307
- const { error } = await client.schema("ui").from("ui_surface_agent_pref").insert({
308
- surface_name: surfaceName,
309
- role_name: roleName,
310
- agent_id: agentId,
311
- kind: "selection",
312
- position,
313
- settings: settings ?? {},
314
- ...await scopeInsertColumns(scope)
315
- });
317
+ const { error } = await withPlatformRoleAdminLane(
318
+ client.schema("ui").from("ui_surface_agent_pref").insert({
319
+ surface_name: surfaceName,
320
+ role_name: roleName,
321
+ agent_id: agentId,
322
+ kind: "selection",
323
+ position,
324
+ settings: settings ?? {},
325
+ ...await scopeInsertColumns(scope)
326
+ }),
327
+ platformLane
328
+ );
316
329
  if (error) throw error;
317
330
  }
318
- async function deleteRolePref(prefId) {
331
+ async function deleteRolePref(prefId, opts = {}) {
319
332
  await writeOne(
320
- sb().schema("ui").from("ui_surface_agent_pref").update({ deleted_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", prefId).is("deleted_at", null).select("id"),
333
+ withPlatformRoleAdminLane(
334
+ sb().schema("ui").from("ui_surface_agent_pref").update({ deleted_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", prefId).is("deleted_at", null).select("id"),
335
+ opts.platformTier === true
336
+ ),
321
337
  { action: "remove", noun: "agent choice" }
322
338
  );
323
339
  }
@@ -1,10 +1,6 @@
1
1
  import React from "react";
2
2
  import type { ToolLifecycleEntry } from "../../agents/types/request.types.js";
3
3
  import type { ToolOverlayTabSpec, ToolRendererProps } from "../types.js";
4
- export declare const CopyButton: React.FC<{
5
- text: string;
6
- className?: string;
7
- }>;
8
4
  export declare const InputView: React.FC<{
9
5
  entry: ToolLifecycleEntry;
10
6
  }>;
@@ -1,11 +1,9 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useClipboard } from "@ai-matrx/kit/clipboard";
4
- import { toast } from "../../host/notify.js";
5
3
  import { resultToHuman } from "../utils/human-copy.js";
6
- import React, { useState } from "react";
4
+ import React from "react";
7
5
  import { useToolConversationId } from "./ToolConversationContext.js";
8
- import { Check, CircleAlert, Copy, FileCode2, Route, Settings2 } from "lucide-react";
6
+ import { CircleAlert, FileCode2, Route, Settings2 } from "lucide-react";
9
7
  import { Badge } from "@ai-matrx/design-system";
10
8
  import { CopyButtons } from "../../host/ui-slots.js";
11
9
  import { JsonInspector } from "../../host/ui-slots.js";
@@ -21,7 +19,6 @@ import {
21
19
  toolEntryBundleToHuman
22
20
  } from "../utils/toolEntryBundle.js";
23
21
  import { ErrorAlchemyMenu } from "../../host/ui-slots.js";
24
- import { Button } from "@ai-matrx/design-system/controls";
25
22
  function buildAgentInput(entry, description, data) {
26
23
  return {
27
24
  kind: "tool-result",
@@ -31,21 +28,6 @@ function buildAgentInput(entry, description, data) {
31
28
  attributes: { tool: entry.toolName, status: entry.status }
32
29
  };
33
30
  }
34
- const CopyButton = ({
35
- text,
36
- className
37
- }) => {
38
- const { copyText } = useClipboard({
39
- notify: (message, kind) => kind === "error" ? toast.error(message) : toast.success(message)
40
- });
41
- const [copied, setCopied] = useState(false);
42
- const handleCopy = async () => {
43
- await copyText(text);
44
- setCopied(true);
45
- setTimeout(() => setCopied(false), 2e3);
46
- };
47
- return /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: handleCopy, title: copied ? "Copied!" : "Copy to clipboard", className, children: copied ? "Copied" : "Copy" });
48
- };
49
31
  const InputView = ({
50
32
  entry
51
33
  }) => {
@@ -221,7 +203,6 @@ const CustomOverlayBody = ({ entry, Component, conversationId: conversationIdPro
221
203
  ) }) });
222
204
  };
223
205
  export {
224
- CopyButton,
225
206
  CustomOverlayBody,
226
207
  EntryResultsBody,
227
208
  ErrorView,
@@ -75,7 +75,7 @@ function VoiceDebugPanel({
75
75
  )
76
76
  ];
77
77
  try {
78
- await copyText(lines.join("\n"));
78
+ if (!await copyText(lines.join("\n"))) return;
79
79
  setCopied(true);
80
80
  setTimeout(() => setCopied(false), 1500);
81
81
  toast.success("Voice debug copied");
@@ -43,7 +43,8 @@ function useCopyText(text) {
43
43
  });
44
44
  const [copied, setCopied] = useState(false);
45
45
  const copy = useCallback(() => {
46
- copyText(text).then(() => {
46
+ copyText(text).then((copied2) => {
47
+ if (!copied2) return;
47
48
  setCopied(true);
48
49
  setTimeout(() => setCopied(false), 1500);
49
50
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/chat",
3
- "version": "0.2.10",
3
+ "version": "0.3.3",
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
- "@ai-matrx/associations": "latest",
207
205
  "@ai-matrx/browser-audio": "latest",
206
+ "@ai-matrx/associations": "latest",
207
+ "@ai-matrx/alchemy": "latest",
208
208
  "@ai-matrx/canvas": "latest",
209
209
  "@ai-matrx/content-ir": "latest",
210
+ "@ai-matrx/content-ir-react": "latest",
210
211
  "@ai-matrx/data": "latest",
211
- "@ai-matrx/design-system": "latest",
212
212
  "@ai-matrx/diff": "latest",
213
+ "@ai-matrx/design-system": "latest",
213
214
  "@ai-matrx/icons": "latest",
214
- "@ai-matrx/content-ir-react": "latest",
215
215
  "@ai-matrx/media": "latest",
216
+ "@ai-matrx/kit": "latest",
216
217
  "@ai-matrx/print": "latest",
217
218
  "@ai-matrx/realtime": "latest",
218
- "@ai-matrx/records": "latest",
219
219
  "@ai-matrx/records-ui": "latest",
220
220
  "@ai-matrx/tap-target": "latest",
221
- "@ai-matrx/kit": "latest"
221
+ "@ai-matrx/records": "latest"
222
222
  },
223
223
  "devDependencies": {
224
224
  "@arethetypeswrong/cli": "^0.18.5",