@ai-matrx/chat 0.2.9 → 0.3.2

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 (60) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/agent-copy/clipboard.d.ts +2 -4
  3. package/dist/agent-copy/clipboard.js +1 -14
  4. package/dist/agent-copy/page-capture/pageCapture.d.ts +11 -62
  5. package/dist/agent-copy/page-capture/pageCapture.js +5 -22
  6. package/dist/agents/components/context-items/bodies/WebpageBody.js +1 -1
  7. package/dist/agents/components/context-preview/AttachedContextSection.js +9 -8
  8. package/dist/agents/components/context-preview/ContextCompareView.js +13 -13
  9. package/dist/agents/components/context-preview/ContextPreviewPanel.js +13 -13
  10. package/dist/agents/components/conversation-actions/conversation-verbs.js +2 -6
  11. package/dist/agents/components/debug/SandboxFileViewer.js +1 -1
  12. package/dist/agents/components/debug/StreamDebugPanel.js +2 -2
  13. package/dist/agents/components/messages-display/TranscriptIntegrityCopyButton.js +1 -1
  14. package/dist/agents/components/observational-memory/components/MemoryStateInspector.js +4 -4
  15. package/dist/agents/components/run-controls/AgentExecutionTestModal.js +2 -2
  16. package/dist/agents/components/run-controls/PayloadTab.js +7 -1
  17. package/dist/agents/components/run-controls/panels/BackendTargetPanel.js +7 -1
  18. package/dist/agents/components/shared/AgentSaveStatus.js +22 -1
  19. package/dist/agents/hooks/useAgentAutoSave.js +14 -2
  20. package/dist/agents/menu/agent-actions.js +3 -5
  21. package/dist/agents/prompt-preview/PromptPreviewContent.js +1 -1
  22. package/dist/agents/redux/agent-definition/slice.js +1 -0
  23. package/dist/agents/redux/agent-settings/agentSettingsSlice.d.ts +3 -0
  24. package/dist/agents/redux/agent-settings/agentSettingsSlice.js +12 -2
  25. package/dist/agents/redux/agent-settings/types.d.ts +7 -0
  26. package/dist/agents/redux/agent-shortcuts/thunks.js +9 -5
  27. package/dist/agents/redux/execution-system/instance-resources/remarks.d.ts +6 -0
  28. package/dist/agents/redux/execution-system/instance-resources/remarks.js +16 -0
  29. package/dist/agents/redux/execution-system/instance-ui-state/components/InstanceUIStateCore.js +7 -1
  30. package/dist/agents/redux/execution-system/instance-ui-state/components/InstanceUIStateList.js +7 -1
  31. package/dist/agents/redux/execution-system/message-crud/commit-inline-edit.thunk.js +2 -1
  32. package/dist/canvas/workspace/useCanvasWorkspaceConversation.d.ts +12 -0
  33. package/dist/canvas/workspace/useCanvasWorkspaceConversation.js +29 -1
  34. package/dist/context-menu/types.d.ts +7 -0
  35. package/dist/cx-chat/components/messages/AssistantMessage.js +8 -10
  36. package/dist/cx-chat/components/messages/UserMessage.js +7 -1
  37. package/dist/cx-conversation/AssistantMessage.js +8 -10
  38. package/dist/cx-conversation/UserMessage.js +7 -1
  39. package/dist/host/clipboard.d.ts +7 -0
  40. package/dist/host/clipboard.js +11 -0
  41. package/dist/host/db-types.d.ts +15 -12
  42. package/dist/host/ui-slots.d.ts +2 -8
  43. package/dist/host/ui-slots.js +11 -16
  44. package/dist/public-chat/components/PublicMessageOptionsMenu.js +9 -16
  45. package/dist/store/create-chat-store.d.ts +10 -1
  46. package/dist/store/create-chat-store.js +4 -2
  47. package/dist/surfaces/runtime/SurfaceRuntimeContext.d.ts +10 -0
  48. package/dist/surfaces/runtime/SurfaceRuntimeContext.js +23 -1
  49. package/dist/surfaces/services/surface-config.service.d.ts +3 -1
  50. package/dist/surfaces/services/surface-config.service.js +33 -17
  51. package/dist/tool-call-visualization/components/ToolTabBodies.d.ts +0 -4
  52. package/dist/tool-call-visualization/components/ToolTabBodies.js +2 -25
  53. package/dist/tool-call-visualization/result-fields/ResultFile.js +2 -8
  54. package/dist/tool-call-visualization/result-fields/ShortId.js +1 -23
  55. package/dist/voice-agent/components/LiveVoiceDoor.js +1 -1
  56. package/dist/voice-agent/components/VoiceDebugPanel.js +1 -1
  57. package/dist/voice-agent/hooks/useVoiceAgentInstance.js +1 -1
  58. package/dist/window-panels/windows/agents/ObservationalMemoryWindow.d.ts +0 -17
  59. package/dist/window-panels/windows/agents/ObservationalMemoryWindow.js +7 -1
  60. package/package.json +7 -7
package/CHANGELOG.md CHANGED
@@ -1,11 +1,45 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.3.2
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/chat/v0.2.10..npm/chat/v0.3.2 -- apps/shared/chat`).
7
+ No source changes intended and no consumer action required.
8
+
9
+ ## 0.3.1 — 2026-10-07
10
+ ### Re-sync before matrx-frontend consumes the published package (P27)
11
+
12
+ - Carries every matrx-frontend `packages/chat` edit after `636e7ef2dd` (context-preview copy, agent-settings
13
+ `offeringId`, surface-config service, db-types) and `surfaces/lib/ui-surfaces-agent-writes` +
14
+ `ui-surfaces-scope` (+ suite), which earlier re-syncs missed; the app imports both.
15
+ - Consumer action: none.
16
+
17
+ ## 0.3.0 — 2026-10-07
18
+
19
+ ALC-19: the chat clipboard slot is content-shaped.
20
+
21
+ - 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`).
22
+ - 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`.
23
+
24
+ ## 0.2.10 — 2026-10-07
25
+ ### Re-sync before matrx-frontend consumes the published package (P27)
26
+
27
+ - Carries every matrx-frontend `packages/chat` edit made after base `f5f30ae893` (32 files): ALC-18 live
28
+ values reach the surface scope and the clicked row's declared item, ALC-19 kit copy/`host/clipboard`,
29
+ board chat keeps its conversation id across a reload, voices read from `@ai-matrx/media/voices`,
30
+ remarks durability-port announcement.
31
+ - Their `@/lib/toast` imports use the package notify port and their `agent-copy/export` download imports
32
+ use `@ai-matrx/kit/download` (0.2.9 dropped the chat copy). Public "Save as file" passes the text,
33
+ not a pre-built Blob, so the saved file holds the markdown.
34
+ - Consumer action: none.
35
+
3
36
  ## 0.2.9 — 2026-10-07
4
37
 
5
38
  ALC-19: the download door moved to the base package.
6
39
 
7
40
  - `agent-copy/export` no longer exports `downloadFile` / `downloadUrl`; they live in `@ai-matrx/kit/download` (kit 0.26.0).
8
41
  - Consumer action: import `downloadFile` / `downloadUrl` from `@ai-matrx/kit/download`.
42
+ - The audio-response download, the public chat "save as file" and tool-result file links save through kit's door too (no hand-built anchors left in the package).
9
43
 
10
44
  ## 0.2.8 — 2026-10-07
11
45
 
@@ -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
  };
@@ -16,67 +16,15 @@
16
16
  */
17
17
  import type { AgentPayloadInput } from "@ai-matrx/alchemy/operate";
18
18
  import type { AlchemyGroomerConfig } from "@ai-matrx/alchemy/react/workspace";
19
- export type PageCaptureKind = "table-page" | "record" | "dialog" | "admin-page";
19
+ import { mergeLiveCapture, type LiveCapture, type LiveCaptureKind, type LiveCaptureValue, type LiveContribution, type LiveNamed, type LiveRequest, type LiveSection } from "@ai-matrx/alchemy/surface";
20
+ export type PageCaptureKind = LiveCaptureKind;
20
21
  /** A chosen thing, by name and id. Either may be null (not chosen / not loaded). */
21
- export interface PageCaptureNamed {
22
- id: string | null;
23
- name: string | null;
24
- }
25
- export type PageCaptureValue = PageCaptureNamed | string | number | boolean | null;
26
- export interface PageCaptureSection {
27
- /** Stable id — the Groomer's section id. Unique within one capture. */
28
- id: string;
29
- /** What the person calls this part of the page. */
30
- title: string;
31
- description?: string;
32
- /** `data` survives "Only the data"; `request` does not. */
33
- role: "data" | "request";
34
- value: unknown;
35
- /** The one-line brief (counts, a sentence). Defaults to the value's shape. */
36
- brief?: unknown;
37
- /**
38
- * A section too big to hold on the page (a table's records): `value` is the
39
- * honest stub saying it is not included, and `load` reads it at copy time for
40
- * the "with …" variants. Never a second read while the page is only open.
41
- */
42
- load?: () => Promise<unknown>;
43
- }
44
- export interface PageCaptureRequest {
45
- method: string;
46
- path: string;
47
- status: string;
48
- httpStatus?: number;
49
- durationMs?: number;
50
- requestId?: string;
51
- /** Which client door (supabase-rest, supabase-rpc, aidream, next-api …). */
52
- client?: string;
53
- /** The JSON request body under 8 KB, credentials redacted. */
54
- requestBody?: unknown;
55
- requestBodyNote?: string;
56
- /** A failed request's own sentence from the server (the refusal, verbatim). */
57
- errorSentence?: string;
58
- timestamp: number;
59
- }
60
- export interface PageCapture {
61
- kind: PageCaptureKind;
62
- title: string;
63
- route: string;
64
- url?: string;
65
- /** What this page is OF (table, record, dialog subject …), names and ids. */
66
- identity: Record<string, PageCaptureValue>;
67
- /** The exact choices the person made on the page, in order. */
68
- selection: Record<string, PageCaptureValue>;
69
- sections: PageCaptureSection[];
70
- /** The error sentences on screen, verbatim. */
71
- errors: string[];
72
- requests: PageCaptureRequest[];
73
- }
74
- export interface PageCaptureContribution {
75
- owner: string;
76
- sections: PageCaptureSection[];
77
- /** Names a descendant knows and the page does not (a table's name read inside its mount). */
78
- identity?: Record<string, PageCaptureValue>;
79
- }
22
+ export type PageCaptureNamed = LiveNamed;
23
+ export type PageCaptureValue = LiveCaptureValue;
24
+ export type PageCaptureSection = LiveSection;
25
+ export type PageCaptureRequest = LiveRequest;
26
+ export type PageCapture = LiveCapture;
27
+ export type PageCaptureContribution = LiveContribution;
80
28
  /** The marker a cycle leaves where the repeated object would have been. */
81
29
  export declare const CIRCULAR_MARKER_PREFIX = "[circular reference to ";
82
30
  /** A JSON pointer path in words: `/sections/1/load` → `sections › 1 › load`; root is "the capture itself". */
@@ -95,9 +43,10 @@ export declare function checkedTransferJson<T>(value: T, what: string): T;
95
43
  /** `Name (id)`, `Name`, `(id)`, or "not chosen". */
96
44
  export declare function describeValue(v: PageCaptureValue): string;
97
45
  /** Merge descendant sections into a capture. Two owners claiming one id is a loud error. */
98
- export declare function mergePageCapture(base: PageCapture, contributions: readonly PageCaptureContribution[]): PageCapture;
46
+ /** The page's capture plus its descendants' contributions — alchemy's one merge. */
47
+ export declare const mergePageCapture: typeof mergeLiveCapture;
99
48
  /**
100
- * THE CAPTURE MADE PLAIN AT REGISTRATION (`getActivePageCapture` calls this): identity, selection,
49
+ * THE CAPTURE MADE PLAIN AT READ (`getActivePageCapture` calls this): identity, selection,
101
50
  * every section's value and brief, and every request body are plain JSON. A section's `load` stays a
102
51
  * function — it is how the "with …" copies read — and is never handed to a transfer (see
103
52
  * `pageCaptureJson`).
@@ -1,4 +1,7 @@
1
1
  import { formatCount } from "@ai-matrx/kit/format";
2
+ import {
3
+ mergeLiveCapture
4
+ } from "@ai-matrx/alchemy/surface";
2
5
  const COMPACT_CHARS = 6e3;
3
6
  const CIRCULAR_MARKER_PREFIX = "[circular reference to ";
4
7
  function pathInWords(path) {
@@ -118,28 +121,7 @@ function compactValue(value) {
118
121
  preview: text.slice(0, COMPACT_CHARS)
119
122
  };
120
123
  }
121
- function mergePageCapture(base, contributions) {
122
- const owners = new Map(base.sections.map((s) => [s.id, "page"]));
123
- const sections = [...base.sections];
124
- const identity = { ...base.identity };
125
- for (const c of contributions) {
126
- for (const [k, v] of Object.entries(c.identity ?? {})) {
127
- const prior = identity[k];
128
- if (prior === void 0 || prior === null || isNamed(prior) && !prior.name) identity[k] = v;
129
- }
130
- for (const s of c.sections) {
131
- const prior = owners.get(s.id);
132
- if (prior) {
133
- throw new Error(
134
- `[page-capture] section "${s.id}" is claimed by both "${prior}" and "${c.owner}"`
135
- );
136
- }
137
- owners.set(s.id, c.owner);
138
- sections.push(s);
139
- }
140
- }
141
- return { ...base, identity, sections };
142
- }
124
+ const mergePageCapture = mergeLiveCapture;
143
125
  function plainNamedRecord(rec) {
144
126
  return toPlainJson(rec);
145
127
  }
@@ -192,6 +174,7 @@ function envelopeContext(c) {
192
174
  return {
193
175
  page: c.title,
194
176
  "surface-kind": c.kind,
177
+ ...c.surfaceName ? { surface: c.surfaceName } : {},
195
178
  route: c.route,
196
179
  ...keyed(describeRecord(c.identity)),
197
180
  ...keyed(describeRecord(c.selection))
@@ -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." }),
@@ -1,4 +1,5 @@
1
1
  import { toast } from "../../../host/notify.js";
2
+ import { copyToHostClipboard } from "../../../host/clipboard.js";
2
3
  import { toastDoor } from "../../../host/ui-slots.js";
3
4
  import { openOverlay, CHAT_WINDOWS } from "../../../host/windows.js";
4
5
  import {
@@ -23,12 +24,7 @@ function resolveAbsoluteHref(href) {
23
24
  return `${window.location.origin}${href.startsWith("/") ? href : `/${href}`}`;
24
25
  }
25
26
  async function copyConversationLink(href) {
26
- try {
27
- await navigator.clipboard.writeText(resolveAbsoluteHref(href));
28
- toast.success("Link copied");
29
- } catch {
30
- toast.error("Couldn't copy \u2014 your browser blocked clipboard access");
31
- }
27
+ await copyToHostClipboard(resolveAbsoluteHref(href), "Link copied");
32
28
  }
33
29
  function shareConversation(dispatch, conversationId, title) {
34
30
  dispatch(
@@ -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
  }
@@ -1,5 +1,7 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useClipboard } from "@ai-matrx/kit/clipboard";
4
+ import { toast } from "../../../host/notify.js";
3
5
  import { Badge, Button } from "@ai-matrx/design-system/controls";
4
6
  import { useCallback, useMemo, useState } from "react";
5
7
  import { Copy, Check } from "lucide-react";
@@ -119,6 +121,9 @@ function roledImagesOf(messages) {
119
121
  return rows;
120
122
  }
121
123
  function PayloadTab({ conversationId }) {
124
+ const { copyText } = useClipboard({
125
+ notify: (message, kind) => kind === "error" ? toast.error(message) : toast.success(message)
126
+ });
122
127
  const assembledRequestSelector = useMemo(
123
128
  () => makeSelectAssembledRequest(conversationId),
124
129
  [conversationId]
@@ -163,7 +168,8 @@ function PayloadTab({ conversationId }) {
163
168
  if (!request) return;
164
169
  try {
165
170
  const text = JSON.stringify(request, null, 2);
166
- void navigator.clipboard.writeText(text).then(() => {
171
+ void copyText(text).then((copied2) => {
172
+ if (!copied2) return;
167
173
  setCopied(true);
168
174
  window.setTimeout(() => setCopied(false), 1500);
169
175
  });
@@ -1,5 +1,7 @@
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";
3
5
  import { formatDurationSeconds } from "@ai-matrx/kit/format";
4
6
  import { useState } from "react";
5
7
  import { Copy, Check } from "lucide-react";
@@ -19,9 +21,13 @@ function CopyableValue({
19
21
  mono = true,
20
22
  className
21
23
  }) {
24
+ const { copyText } = useClipboard({
25
+ notify: (message, kind) => kind === "error" ? toast.error(message) : toast.success(message)
26
+ });
22
27
  const [copied, setCopied] = useState(false);
23
28
  const handleCopy = () => {
24
- void navigator.clipboard.writeText(value).then(() => {
29
+ void copyText(value).then((copied2) => {
30
+ if (!copied2) return;
25
31
  setCopied(true);
26
32
  setTimeout(() => setCopied(false), 1200);
27
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]);
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { Cpu, Copy, RefreshCw, ExternalLink } from "lucide-react";
3
- import { toast } from "../../host/notify.js";
3
+ import { copyToHostClipboard } from "../../host/clipboard.js";
4
4
  import {
5
5
  withAvailability
6
6
  } from "../../context-menu/utils/availability.js";
@@ -30,8 +30,7 @@ function buildAgentMenuSection({
30
30
  label: "Copy agent ID",
31
31
  icon: Copy,
32
32
  onSelect: () => {
33
- void navigator.clipboard.writeText(agentId);
34
- toast.success("Agent ID copied");
33
+ void copyToHostClipboard(agentId, "Agent ID copied");
35
34
  }
36
35
  },
37
36
  {
@@ -41,8 +40,7 @@ function buildAgentMenuSection({
41
40
  icon: Copy,
42
41
  disabled: !agentName,
43
42
  onSelect: () => {
44
- void navigator.clipboard.writeText(agentName ?? "");
45
- toast.success("Agent name copied");
43
+ void copyToHostClipboard(agentName ?? "", "Agent name copied");
46
44
  }
47
45
  },
48
46
  { kind: "separator", id: "agent-sep-1" },