@ai-matrx/chat 0.2.8 → 0.2.10

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 (39) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/agent-copy/export.d.ts +0 -10
  3. package/dist/agent-copy/export.js +0 -23
  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/conversation-actions/conversation-verbs.js +2 -6
  7. package/dist/agents/components/run-controls/PayloadTab.js +6 -1
  8. package/dist/agents/components/run-controls/panels/BackendTargetPanel.js +6 -1
  9. package/dist/agents/menu/agent-actions.js +3 -5
  10. package/dist/agents/redux/agent-shortcuts/thunks.js +9 -5
  11. package/dist/agents/redux/execution-system/instance-resources/remarks.d.ts +6 -0
  12. package/dist/agents/redux/execution-system/instance-resources/remarks.js +16 -0
  13. package/dist/agents/redux/execution-system/instance-ui-state/components/InstanceUIStateCore.js +6 -1
  14. package/dist/agents/redux/execution-system/instance-ui-state/components/InstanceUIStateList.js +6 -1
  15. package/dist/agents/redux/execution-system/message-crud/commit-inline-edit.thunk.js +2 -1
  16. package/dist/canvas/workspace/useCanvasWorkspaceConversation.d.ts +12 -0
  17. package/dist/canvas/workspace/useCanvasWorkspaceConversation.js +29 -1
  18. package/dist/context-menu/types.d.ts +7 -0
  19. package/dist/cx-chat/components/messages/AssistantMessage.js +8 -10
  20. package/dist/cx-chat/components/messages/UserMessage.js +6 -1
  21. package/dist/cx-conversation/AssistantMessage.js +8 -10
  22. package/dist/cx-conversation/UserMessage.js +6 -1
  23. package/dist/host/clipboard.d.ts +7 -0
  24. package/dist/host/clipboard.js +11 -0
  25. package/dist/host/ui-slots.d.ts +2 -6
  26. package/dist/host/ui-slots.js +4 -14
  27. package/dist/public-chat/components/PublicMessageOptionsMenu.js +2 -9
  28. package/dist/store/create-chat-store.d.ts +10 -1
  29. package/dist/store/create-chat-store.js +4 -2
  30. package/dist/surfaces/runtime/SurfaceRuntimeContext.d.ts +10 -0
  31. package/dist/surfaces/runtime/SurfaceRuntimeContext.js +23 -1
  32. package/dist/tool-call-visualization/components/ToolTabBodies.js +6 -10
  33. package/dist/tool-call-visualization/result-fields/ResultFile.js +2 -8
  34. package/dist/tool-call-visualization/result-fields/ShortId.js +1 -23
  35. package/dist/voice-agent/components/LiveVoiceDoor.js +1 -1
  36. package/dist/voice-agent/hooks/useVoiceAgentInstance.js +1 -1
  37. package/dist/window-panels/windows/agents/ObservationalMemoryWindow.d.ts +0 -17
  38. package/dist/window-panels/windows/agents/ObservationalMemoryWindow.js +6 -1
  39. package/package.json +6 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.2.10 — 2026-10-07
4
+ ### Re-sync before matrx-frontend consumes the published package (P27)
5
+
6
+ - Carries every matrx-frontend `packages/chat` edit made after base `f5f30ae893` (32 files): ALC-18 live
7
+ values reach the surface scope and the clicked row's declared item, ALC-19 kit copy/`host/clipboard`,
8
+ board chat keeps its conversation id across a reload, voices read from `@ai-matrx/media/voices`,
9
+ remarks durability-port announcement.
10
+ - Their `@/lib/toast` imports use the package notify port and their `agent-copy/export` download imports
11
+ use `@ai-matrx/kit/download` (0.2.9 dropped the chat copy). Public "Save as file" passes the text,
12
+ not a pre-built Blob, so the saved file holds the markdown.
13
+ - Consumer action: none.
14
+
15
+ ## 0.2.9 — 2026-10-07
16
+
17
+ ALC-19: the download door moved to the base package.
18
+
19
+ - `agent-copy/export` no longer exports `downloadFile` / `downloadUrl`; they live in `@ai-matrx/kit/download` (kit 0.26.0).
20
+ - Consumer action: import `downloadFile` / `downloadUrl` from `@ai-matrx/kit/download`.
21
+ - 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).
22
+
3
23
  ## 0.2.8 — 2026-10-07
4
24
 
5
25
  ALC-19: the one file-download door.
@@ -21,16 +21,6 @@ export interface ExportItem {
21
21
  */
22
22
  onSelect?: () => void | Promise<void>;
23
23
  }
24
- export declare function downloadFile(filename: string, content: BlobPart, mime: string): void;
25
- /**
26
- * Save a file that already has a URL (a data: or blob: URL, or a remote URL the browser may
27
- * download). Same-origin and data/blob URLs are saved under `filename`; a cross-origin remote
28
- * URL is handed to the browser, which may ignore the name. `newTab` opens it in a new tab
29
- * (noopener) as the cross-origin fallback.
30
- */
31
- export declare function downloadUrl(url: string, filename: string, options?: {
32
- newTab?: boolean;
33
- }): void;
34
24
  /** Sanitized `base-2026-07-27.ext` filename. */
35
25
  export declare function exportFilename(base: string, extension: string): string;
36
26
  export declare function jsonExportItem(data: unknown | (() => unknown), label?: string): ExportItem;
@@ -1,24 +1,3 @@
1
- function downloadFile(filename, content, mime) {
2
- const blob = new Blob([content], { type: mime });
3
- const url = URL.createObjectURL(blob);
4
- clickDownloadAnchor(url, filename);
5
- setTimeout(() => URL.revokeObjectURL(url), 1e3);
6
- }
7
- function downloadUrl(url, filename, options = {}) {
8
- clickDownloadAnchor(url, filename, options.newTab);
9
- }
10
- function clickDownloadAnchor(href, filename, newTab = false) {
11
- const anchor = document.createElement("a");
12
- anchor.href = href;
13
- anchor.download = filename;
14
- if (newTab) {
15
- anchor.target = "_blank";
16
- anchor.rel = "noopener noreferrer";
17
- }
18
- document.body.appendChild(anchor);
19
- anchor.click();
20
- document.body.removeChild(anchor);
21
- }
22
1
  function exportFilename(base, extension) {
23
2
  const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
24
3
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -90,8 +69,6 @@ function csvExportItem(rows, label = "CSV", columns) {
90
69
  }
91
70
  export {
92
71
  csvExportItem,
93
- downloadFile,
94
- downloadUrl,
95
72
  exportFilename,
96
73
  jsonExportItem,
97
74
  rowsToCsv,
@@ -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))
@@ -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(
@@ -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,7 @@ 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(() => {
167
172
  setCopied(true);
168
173
  window.setTimeout(() => setCopied(false), 1500);
169
174
  });
@@ -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,12 @@ 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(() => {
25
30
  setCopied(true);
26
31
  setTimeout(() => setCopied(false), 1200);
27
32
  });
@@ -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" },
@@ -333,6 +333,9 @@ const saveShortcutField = createAsyncThunk(
333
333
  dispatch(markShortcutSaved({ id: shortcutId }));
334
334
  }
335
335
  );
336
+ function withGlobalShortcutAdminLane(request) {
337
+ return request.setHeader("x-matrx-admin-lane", "1");
338
+ }
336
339
  const createShortcut = createAsyncThunk("agentShortcut/create", async (shortcutData, { dispatch, getState }) => {
337
340
  const userId = selectUserId(getState());
338
341
  const organizationId = await ensureOrgId(shortcutData.organizationId);
@@ -346,7 +349,9 @@ const createShortcut = createAsyncThunk("agentShortcut/create", async (shortcutD
346
349
  createdAt: "",
347
350
  updatedAt: ""
348
351
  };
349
- const { data, error } = await shortcutTable(supabase).insert(agentShortcutToInsert(draft)).select().single();
352
+ const isGlobalWrite = shortcutData.userId === null && shortcutData.organizationId == null && shortcutData.projectId == null && shortcutData.taskId == null;
353
+ const insertRequest = shortcutTable(supabase).insert(agentShortcutToInsert(draft)).select().single();
354
+ const { data, error } = await (isGlobalWrite ? withGlobalShortcutAdminLane(insertRequest) : insertRequest);
350
355
  if (error) throw pgErrorToError(error);
351
356
  const newShortcut = toGlobalOwnershipRecord(
352
357
  dbRowToAgentShortcut(data),
@@ -392,13 +397,12 @@ const duplicateShortcut = createAsyncThunk("agentShortcut/duplicate", async (arg
392
397
  const promoteShortcutToGlobal = createAsyncThunk(
393
398
  "agentShortcut/promoteToGlobal",
394
399
  async ({ shortcutId, targetCategoryId, label }, { dispatch }) => {
395
- const { data, error } = await supabase.rpc(
396
- SHORTCUT_RPCS.promoteToGlobal,
397
- {
400
+ const { data, error } = await withGlobalShortcutAdminLane(
401
+ supabase.rpc(SHORTCUT_RPCS.promoteToGlobal, {
398
402
  p_shortcut_id: shortcutId,
399
403
  p_target_category_id: targetCategoryId,
400
404
  p_label: label && label.trim().length > 0 ? label.trim() : void 0
401
- }
405
+ })
402
406
  );
403
407
  if (error) throw pgErrorToError(error);
404
408
  const newShortcutId = data;
@@ -159,6 +159,12 @@ export interface RemarkDurability {
159
159
  }
160
160
  /** True while some chip of this conversation has not yet been saved server-side. */
161
161
  export declare function hasPendingRemarkWrites(conversationId: string): boolean;
162
+ /**
163
+ * True once the app has registered how unsent chips are kept. Without it every
164
+ * chip is browser-only and lost on reload, so a missing port is announced, never
165
+ * silently stood in for (the door a person touches must say when it is not wired).
166
+ */
167
+ export declare function isRemarkDurabilityRegistered(): boolean;
162
168
  /** Save every pending chip write and wait for its row/ref (called before a send). */
163
169
  export declare function flushRemarkWrites(conversationId: string): Promise<void>;
164
170
  /** Put a conversation's unsent chips back (any device) — the composer calls this once on mount. */
@@ -72,10 +72,23 @@ function buildSource(item, coalesceKey) {
72
72
  function hasPendingRemarkWrites(conversationId) {
73
73
  return durability?.hasPending(conversationId) ?? false;
74
74
  }
75
+ function isRemarkDurabilityRegistered() {
76
+ return durability !== null;
77
+ }
78
+ const announcedMissing = /* @__PURE__ */ new Set();
79
+ function announceMissingDurability(op) {
80
+ if (announcedMissing.has(op)) return;
81
+ announcedMissing.add(op);
82
+ console.error(
83
+ `[remarks] ${op} ran with no durability port registered: unsent chips are NOT kept server-side. The app must call registerRemarkDurability (providers/ChatSurfaceRegistrations).`
84
+ );
85
+ }
75
86
  async function flushRemarkWrites(conversationId) {
87
+ if (!durability) announceMissingDurability("flush");
76
88
  await durability?.flush(conversationId);
77
89
  }
78
90
  function restoreComposerRemarks(conversationId) {
91
+ if (!durability) announceMissingDurability("restore");
79
92
  durability?.restore(conversationId);
80
93
  }
81
94
  let durability = null;
@@ -100,6 +113,7 @@ function stageRemark(conversationId, item, options = {}) {
100
113
  dispatch(setResourceSource({ conversationId, resourceId: live.resourceId, source }));
101
114
  dispatch(setResourcePreview({ conversationId, resourceId: live.resourceId, preview: source.label }));
102
115
  if (!options.fromServer && !item.blockStateRef) {
116
+ if (!durability) announceMissingDurability("save");
103
117
  durability?.save(conversationId, live.resourceId, coalesceKey, item);
104
118
  }
105
119
  return live.resourceId;
@@ -110,6 +124,7 @@ function stageRemark(conversationId, item, options = {}) {
110
124
  dispatch(addResource({ conversationId, blockType: REMARKS_BLOCK_TYPE, source, resourceId }));
111
125
  dispatch(setResourcePreview({ conversationId, resourceId, preview: source.label }));
112
126
  if (!options.fromServer && !item.blockStateRef) {
127
+ if (!durability) announceMissingDurability("save");
113
128
  durability?.save(conversationId, resourceId, coalesceKey, item);
114
129
  }
115
130
  return resourceId;
@@ -205,6 +220,7 @@ export {
205
220
  dismissRemarkChip,
206
221
  flushRemarkWrites,
207
222
  hasPendingRemarkWrites,
223
+ isRemarkDurabilityRegistered,
208
224
  readStoredRemarks,
209
225
  registerRemarkDurability,
210
226
  remarkChipTitle,
@@ -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 { Badge, Button, Tile } from "@ai-matrx/design-system/controls";
4
6
  import { useState } from "react";
5
7
  import { cn } from "@ai-matrx/design-system";
@@ -18,9 +20,12 @@ import {
18
20
  selectInstanceTitle
19
21
  } from "../instance-ui-state.selectors.js";
20
22
  function useCopyText(text) {
23
+ const { copyText } = useClipboard({
24
+ notify: (message, kind) => kind === "error" ? toast.error(message) : toast.success(message)
25
+ });
21
26
  const [copied, setCopied] = useState(false);
22
27
  const copy = () => {
23
- navigator.clipboard.writeText(text).then(() => {
28
+ copyText(text).then(() => {
24
29
  setCopied(true);
25
30
  setTimeout(() => setCopied(false), 1500);
26
31
  });
@@ -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 { useState } from "react";
4
6
  import { cn } from "@ai-matrx/design-system";
5
7
  import {
@@ -19,9 +21,12 @@ import {
19
21
  } from "../instance-ui-state.selectors.js";
20
22
  import { Button } from "@ai-matrx/design-system/controls";
21
23
  function useCopyText(text) {
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 copy = () => {
24
- navigator.clipboard.writeText(text).then(() => {
29
+ copyText(text).then(() => {
25
30
  setCopied(true);
26
31
  setTimeout(() => setCopied(false), 1500);
27
32
  });
@@ -6,6 +6,7 @@ import { updateMessageRecord } from "../messages/messages.slice.js";
6
6
  import { saveAnswerEdit } from "./save-answer-edit.thunk.js";
7
7
  import { refetchSingleMessage } from "./refetch-single-message.thunk.js";
8
8
  import { toast } from "../../../../host/notify.js";
9
+ import { copyToHostClipboard } from "../../../../host/clipboard.js";
9
10
  import { buildContentBlocksForSave } from "../../../../cx-chat/utils/buildContentBlocksForSave.js";
10
11
  import { stripCitationMarkers } from "../messages/message-citations.js";
11
12
  import { extractFlatText } from "../messages/messages.selectors.js";
@@ -97,7 +98,7 @@ async function persistInlineEdit(dispatch, conversationId, messageId, entry) {
97
98
  action: {
98
99
  label: "Copy my edit",
99
100
  onClick: () => {
100
- void navigator.clipboard?.writeText(draft);
101
+ void copyToHostClipboard(draft);
101
102
  }
102
103
  }
103
104
  });
@@ -62,6 +62,18 @@ export declare function conversationInAddress(param: string, search: string): st
62
62
  export declare function workspaceChatMemoryKey(surfaceKey: string): string;
63
63
  export declare function recallWorkspaceConversation(surfaceKey: string): string | null;
64
64
  export declare function rememberWorkspaceConversation(surfaceKey: string, conversationId: string | null): void;
65
+ /**
66
+ * THE RESERVED ID OF AN UNSENT CHAT (2026-10-06). A new chat that has not been
67
+ * sent is unknown to the server, so a reload cannot reopen it and used to mint a
68
+ * different id — which orphaned every chip staged into it (a comment riding
69
+ * "with next message" came back gone). The id is reserved on this device per
70
+ * home and handed to the launch, so the unsent chat comes back under the SAME id
71
+ * and its saved chips are found. Released when the server has the conversation
72
+ * (the memory above takes over) and by New chat (a new chat is a new id).
73
+ */
74
+ export declare function reservedChatKey(surfaceKey: string): string;
75
+ export declare function reserveWorkspaceConversationId(surfaceKey: string, fresh: boolean): string | undefined;
76
+ export declare function releaseWorkspaceConversationId(surfaceKey: string): void;
65
77
  /** `search` with `?<param>=` set to `conversationId`, or removed when null. */
66
78
  export declare function addressWithConversation(param: string, search: string, conversationId: string | null): string;
67
79
  export interface CanvasWorkspaceConversationController {
@@ -39,6 +39,27 @@ function rememberWorkspaceConversation(surfaceKey, conversationId) {
39
39
  } catch {
40
40
  }
41
41
  }
42
+ function reservedChatKey(surfaceKey) {
43
+ return `matrx:workspace-chat-reserved:${surfaceKey}`;
44
+ }
45
+ function reserveWorkspaceConversationId(surfaceKey, fresh) {
46
+ try {
47
+ const key = reservedChatKey(surfaceKey);
48
+ const kept = fresh ? null : window.localStorage.getItem(key);
49
+ if (kept && CONVERSATION_ID.test(kept)) return kept;
50
+ const id = window.crypto.randomUUID();
51
+ window.localStorage.setItem(key, id);
52
+ return id;
53
+ } catch {
54
+ return void 0;
55
+ }
56
+ }
57
+ function releaseWorkspaceConversationId(surfaceKey) {
58
+ try {
59
+ window.localStorage.removeItem(reservedChatKey(surfaceKey));
60
+ } catch {
61
+ }
62
+ }
42
63
  function addressWithConversation(param, search, conversationId) {
43
64
  const params = new URLSearchParams(search);
44
65
  if (conversationId) params.set(param, conversationId);
@@ -99,6 +120,7 @@ function useCanvasWorkspaceConversation(surfaceKey, input) {
99
120
  if (addressParam === null || !addressRead || !conversationId) return;
100
121
  const shown = request.kind === "open" || serverHasIt ? conversationId : null;
101
122
  rememberWorkspaceConversation(surfaceKey, shown);
123
+ if (shown) releaseWorkspaceConversationId(surfaceKey);
102
124
  const search = addressWithConversation(addressParam, window.location.search, shown);
103
125
  if (search === window.location.search) return;
104
126
  replaceAddressWithoutNavigating(`${window.location.pathname}${search}${window.location.hash}`);
@@ -111,8 +133,11 @@ function useCanvasWorkspaceConversation(surfaceKey, input) {
111
133
  const stale = () => handled.current !== key;
112
134
  if (request.kind === "new" || request.kind === "agent") {
113
135
  const runtime = ownSurface ? { runtime: { surfaceName: null } } : {};
114
- const launch = request.kind === "agent" ? launchAgent(request.agentId, { surfaceKey, sourceFeature: "chat", ...runtime }) : launchMandate(request.mandateKey ?? DEFAULT_NEW_CHAT_MANDATE_KEY, {
136
+ const reserved = addressParam !== null ? reserveWorkspaceConversationId(surfaceKey, request.nonce > 0) : void 0;
137
+ const reservation = reserved ? { conversationId: reserved } : {};
138
+ const launch = request.kind === "agent" ? launchAgent(request.agentId, { surfaceKey, sourceFeature: "chat", ...reservation, ...runtime }) : launchMandate(request.mandateKey ?? DEFAULT_NEW_CHAT_MANDATE_KEY, {
115
139
  surfaceKey,
140
+ ...reservation,
116
141
  // A REGISTERED feature, never a new string: this surface IS the chat.
117
142
  sourceFeature: "chat",
118
143
  ...runtime
@@ -193,7 +218,10 @@ export {
193
218
  addressWithConversation,
194
219
  conversationInAddress,
195
220
  recallWorkspaceConversation,
221
+ releaseWorkspaceConversationId,
196
222
  rememberWorkspaceConversation,
223
+ reserveWorkspaceConversationId,
224
+ reservedChatKey,
197
225
  useCanvasWorkspaceConversation,
198
226
  workspaceChatMemoryKey
199
227
  };
@@ -4,6 +4,7 @@ import type { SourceFeature } from "../agents/types/instance.types.js";
4
4
  import type { Scope } from "../agents/redux/shared/scope.js";
5
5
  import type { ApplicationScope } from "../agents/types/scope.types.js";
6
6
  import type { EntityTypeToken } from "@ai-matrx/associations";
7
+ import type { ResolvedItem } from "@ai-matrx/alchemy/declare";
7
8
  /**
8
9
  * The few shapes the host owns (the rich document's content source and actions, the sharing
9
10
  * system's resource types). The package declares them open; the host fills them in by augmenting
@@ -399,6 +400,12 @@ export interface MenuContentProps {
399
400
  variant: MenuVariant;
400
401
  /** The clicked target's own name for the header (`CONTEXT_MENU_HEADING_KEY`). */
401
402
  heading?: ContextMenuHeading | null;
403
+ /**
404
+ * The declared item the menu opened on (a table row: `table_row`, identity `table_id` + `row_id`),
405
+ * raw-free, as `itemSources` resolved it. It becomes the click target's `item`, so Actions and
406
+ * menu rows read the declared item instead of re-deriving it. Null = not on an item.
407
+ */
408
+ item?: ResolvedItem | null;
402
409
  sourceFeature: SourceFeature;
403
410
  surfaceName?: string;
404
411
  menuVersion: number;
@@ -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 { recognizeOurFileUrl } from "@ai-matrx/media/files/engine";
4
6
  import { useState, useEffect, useCallback } from "react";
5
7
  import {
@@ -22,6 +24,7 @@ import { chatConversationsActions } from "../../_legacy-stubs.js";
22
24
  import { RichDocumentActions } from "../../../host/rich-document-slots.js";
23
25
  import { MessageTimestamp } from "../../../agents/components/messages-display/MessageTimestamp.js";
24
26
  import { ErrorAlchemyMenu } from "../../../host/ui-slots.js";
27
+ import { downloadFile } from "@ai-matrx/kit/download";
25
28
  function AssistantMessage({
26
29
  message,
27
30
  sessionId,
@@ -31,6 +34,9 @@ function AssistantMessage({
31
34
  isTtsRequest = false,
32
35
  onContentChange
33
36
  }) {
37
+ const { copyText } = useClipboard({
38
+ notify: (message2, kind) => kind === "error" ? toast.error(message2) : toast.success(message2)
39
+ });
34
40
  const [isAppearing, setIsAppearing] = useState(true);
35
41
  const [isAudioLinkCopied, setIsAudioLinkCopied] = useState(false);
36
42
  const [isDownloading, setIsDownloading] = useState(false);
@@ -97,15 +103,8 @@ function AssistantMessage({
97
103
  try {
98
104
  const resp = await fetch(audioUrl);
99
105
  const blob = await resp.blob();
100
- const objectUrl = URL.createObjectURL(blob);
101
106
  const ext = audioMimeType?.split("/")[1] ?? "wav";
102
- const link = document.createElement("a");
103
- link.href = objectUrl;
104
- link.download = `audio-response.${ext}`;
105
- document.body.appendChild(link);
106
- link.click();
107
- document.body.removeChild(link);
108
- URL.revokeObjectURL(objectUrl);
107
+ downloadFile(`audio-response.${ext}`, blob, blob.type);
109
108
  } catch {
110
109
  } finally {
111
110
  setIsDownloading(false);
@@ -186,8 +185,7 @@ function AssistantMessage({
186
185
  variant: "quiet",
187
186
  onClick: async () => {
188
187
  if (!audioUrl) return;
189
- await navigator.clipboard.writeText(audioUrl).catch(() => {
190
- });
188
+ await copyText(audioUrl);
191
189
  setIsAudioLinkCopied(true);
192
190
  setTimeout(() => setIsAudioLinkCopied(false), 2e3);
193
191
  },
@@ -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 { useState, useEffect, useRef, useMemo } from "react";
4
6
  import {
5
7
  Check,
@@ -84,6 +86,9 @@ function UserMessage({
84
86
  onContentChange,
85
87
  compact = false
86
88
  }) {
89
+ const { copyText } = useClipboard({
90
+ notify: (message2, kind) => kind === "error" ? toast.error(message2) : toast.success(message2)
91
+ });
87
92
  const [isCollapsed, setIsCollapsed] = useState(true);
88
93
  const [isCopied, setIsCopied] = useState(false);
89
94
  const [shouldBeCollapsible, setShouldBeCollapsible] = useState(false);
@@ -130,7 +135,7 @@ function UserMessage({
130
135
  }, [isEditing]);
131
136
  const handleCopy = (e) => {
132
137
  e.stopPropagation();
133
- navigator.clipboard.writeText(message.content).then(() => {
138
+ copyText(message.content).then(() => {
134
139
  setIsCopied(true);
135
140
  setTimeout(() => setIsCopied(false), 2e3);
136
141
  });
@@ -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 { recognizeOurFileUrl } from "@ai-matrx/media/files/engine";
4
6
  import { useState, useEffect, useCallback } from "react";
5
7
  import {
@@ -22,6 +24,7 @@ import { chatConversationsActions } from "./_legacy-stubs.js";
22
24
  import { RichDocumentActions } from "../host/rich-document-slots.js";
23
25
  import { MessageTimestamp } from "../agents/components/messages-display/MessageTimestamp.js";
24
26
  import { ErrorAlchemyMenu } from "../host/ui-slots.js";
27
+ import { downloadFile } from "@ai-matrx/kit/download";
25
28
  function AssistantMessage({
26
29
  message,
27
30
  sessionId,
@@ -31,6 +34,9 @@ function AssistantMessage({
31
34
  isTtsRequest = false,
32
35
  onContentChange
33
36
  }) {
37
+ const { copyText } = useClipboard({
38
+ notify: (message2, kind) => kind === "error" ? toast.error(message2) : toast.success(message2)
39
+ });
34
40
  const [isAppearing, setIsAppearing] = useState(true);
35
41
  const [isAudioLinkCopied, setIsAudioLinkCopied] = useState(false);
36
42
  const [isDownloading, setIsDownloading] = useState(false);
@@ -97,15 +103,8 @@ function AssistantMessage({
97
103
  try {
98
104
  const resp = await fetch(audioUrl);
99
105
  const blob = await resp.blob();
100
- const objectUrl = URL.createObjectURL(blob);
101
106
  const ext = audioMimeType?.split("/")[1] ?? "wav";
102
- const link = document.createElement("a");
103
- link.href = objectUrl;
104
- link.download = `audio-response.${ext}`;
105
- document.body.appendChild(link);
106
- link.click();
107
- document.body.removeChild(link);
108
- URL.revokeObjectURL(objectUrl);
107
+ downloadFile(`audio-response.${ext}`, blob, blob.type);
109
108
  } catch {
110
109
  } finally {
111
110
  setIsDownloading(false);
@@ -186,8 +185,7 @@ function AssistantMessage({
186
185
  variant: "quiet",
187
186
  onClick: async () => {
188
187
  if (!audioUrl) return;
189
- await navigator.clipboard.writeText(audioUrl).catch(() => {
190
- });
188
+ await copyText(audioUrl);
191
189
  setIsAudioLinkCopied(true);
192
190
  setTimeout(() => setIsAudioLinkCopied(false), 2e3);
193
191
  },
@@ -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 { useState, useEffect, useRef, useMemo } from "react";
4
6
  import {
5
7
  Check,
@@ -84,6 +86,9 @@ function UserMessage({
84
86
  onContentChange,
85
87
  compact = false
86
88
  }) {
89
+ const { copyText } = useClipboard({
90
+ notify: (message2, kind) => kind === "error" ? toast.error(message2) : toast.success(message2)
91
+ });
87
92
  const [isCollapsed, setIsCollapsed] = useState(true);
88
93
  const [isCopied, setIsCopied] = useState(false);
89
94
  const [shouldBeCollapsible, setShouldBeCollapsible] = useState(false);
@@ -130,7 +135,7 @@ function UserMessage({
130
135
  }, [isEditing]);
131
136
  const handleCopy = (e) => {
132
137
  e.stopPropagation();
133
- navigator.clipboard.writeText(message.content).then(() => {
138
+ copyText(message.content).then(() => {
134
139
  setIsCopied(true);
135
140
  setTimeout(() => setIsCopied(false), 2e3);
136
141
  });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The clipboard seam — plain-text copy for package code outside a React component.
3
+ *
4
+ * The kit `copyText` with its success and failure notices routed through the notify seam, so a
5
+ * refused copy always announces itself. Resolves to whether the copy landed.
6
+ */
7
+ export declare function copyToHostClipboard(text: string, successMessage?: string): Promise<boolean>;
@@ -0,0 +1,11 @@
1
+ import { copyText } from "@ai-matrx/kit/clipboard";
2
+ import { toast } from "./notify.js";
3
+ function copyToHostClipboard(text, successMessage) {
4
+ return copyText(text, {
5
+ successMessage,
6
+ notify: (message, kind) => kind === "error" ? toast.error(message) : toast.success(message)
7
+ });
8
+ }
9
+ export {
10
+ copyToHostClipboard
11
+ };
@@ -317,12 +317,10 @@ export interface ChatUiSlots {
317
317
  dispatchWarRoomTool: AnyFn;
318
318
  dispatchWarRoomMasterTool: AnyFn;
319
319
  resolveGmailSendConnection: AnyFn;
320
- voiceDisplayName: AnyFn;
321
320
  canvasGetVersionHistory: (canvasId: string) => Promise<CanvasItemRow[]>;
322
321
  canvasGetById: (canvasId: string) => Promise<CanvasItemRow | null>;
323
322
  createSandboxFilesystemAdapter: AnyFn;
324
323
  notesGetById: AnyFn;
325
- isLiveConversationVoice: AnyFn;
326
324
  createHtmlPage: AnyFn;
327
325
  convertMarkdownToHtml: AnyFn;
328
326
  sklActions: Record<string, AnyFn>;
@@ -338,7 +336,6 @@ export interface ChatUiSlots {
338
336
  WorkspaceGate: AnyComponent;
339
337
  OrganizationContextNotice: AnyComponent;
340
338
  JsonInspector: AnyComponent;
341
- InlineCopyButton: AnyComponent;
342
339
  ConfirmDialog: SlotComponent<OpenChangeProps>;
343
340
  ModelListDropdown: SlotComponent<ModelListDropdownProps>;
344
341
  TextWithDoors: AnyComponent;
@@ -357,6 +354,7 @@ export interface ChatUiSlots {
357
354
  AccessGate: AnyComponent;
358
355
  ReferenceCopyMenuItem: AnyComponent;
359
356
  ReferenceCopyButton: AnyComponent;
357
+ InlineCopyButton: AnyComponent;
360
358
  MandateNotesPanel: AnyComponent;
361
359
  SurfaceBoundAgentsList: SlotComponent<AgentsListProps>;
362
360
  ProposedDirectivesZone: AnyComponent;
@@ -604,7 +602,6 @@ export declare const StaleDataNotice: AnyComponent;
604
602
  export declare const WorkspaceGate: AnyComponent;
605
603
  export declare const OrganizationContextNotice: AnyComponent;
606
604
  export declare const JsonInspector: AnyComponent;
607
- export declare const InlineCopyButton: AnyComponent;
608
605
  export declare const ConfirmDialog: SlotComponent<OpenChangeProps>;
609
606
  export declare const ModelListDropdown: SlotComponent<ModelListDropdownProps>;
610
607
  export declare const TextWithDoors: AnyComponent;
@@ -623,6 +620,7 @@ export declare const AnswerTextPreview: AnyComponent;
623
620
  export declare const AccessGate: AnyComponent;
624
621
  export declare const ReferenceCopyMenuItem: AnyComponent;
625
622
  export declare const ReferenceCopyButton: AnyComponent;
623
+ export declare const InlineCopyButton: AnyComponent;
626
624
  export declare const MandateNotesPanel: AnyComponent;
627
625
  export declare const SurfaceBoundAgentsList: SlotComponent<AgentsListProps>;
628
626
  export declare const ProposedDirectivesZone: AnyComponent;
@@ -712,12 +710,10 @@ export declare const adoptCloudBrowserRunFromStream: AnyFn;
712
710
  export declare const dispatchWarRoomTool: AnyFn;
713
711
  export declare const dispatchWarRoomMasterTool: AnyFn;
714
712
  export declare const resolveGmailSendConnection: AnyFn;
715
- export declare const voiceDisplayName: AnyFn;
716
713
  export declare const canvasGetVersionHistory: (canvasId: string) => Promise<CanvasItemRow[]>;
717
714
  export declare const canvasGetById: (canvasId: string) => Promise<CanvasItemRow | null>;
718
715
  export declare const createSandboxFilesystemAdapter: AnyFn;
719
716
  export declare const notesGetById: AnyFn;
720
- export declare const isLiveConversationVoice: AnyFn;
721
717
  export declare const createHtmlPage: AnyFn;
722
718
  export declare const convertMarkdownToHtml: AnyFn;
723
719
  /** The host's skill-library action creators (`sklActions.x(...)`); resolved at call time, a bare host throws naming the slot. */
@@ -4,6 +4,7 @@ import { formatCost, usdToPoints } from "@ai-matrx/kit/format";
4
4
  import { reportUnregisteredHostSlot as reportUnregistered } from "./diagnostics.js";
5
5
  import { DefaultWebpageSnapshotView } from "./defaults/webpage-snapshot-view.js";
6
6
  import { DefaultFullScreenOverlay, DefaultWindowPanel } from "./defaults/window-panel.js";
7
+ import { copyText } from "@ai-matrx/kit/clipboard";
7
8
  const slots = {};
8
9
  function registerChatUi(next) {
9
10
  Object.assign(slots, next);
@@ -68,14 +69,7 @@ const failingThunk = (name) => () => () => {
68
69
  const STAND_INS = {
69
70
  confirm: async () => false,
70
71
  copyRichContent: async () => false,
71
- copyToClipboard: async (text) => {
72
- try {
73
- await navigator.clipboard.writeText(String(text));
74
- return true;
75
- } catch {
76
- return false;
77
- }
78
- },
72
+ copyToClipboard: (text) => copyText(String(text)),
79
73
  useTablesEverywhere: () => NONE,
80
74
  useCenterControlFit: () => true,
81
75
  connectorDefinitionFromMcp: () => null,
@@ -327,7 +321,6 @@ const StaleDataNotice = slotComponent("StaleDataNotice", unregisteredWidget("Sta
327
321
  const WorkspaceGate = slotComponent("WorkspaceGate", unregisteredWidget("WorkspaceGate"));
328
322
  const OrganizationContextNotice = slotComponent("OrganizationContextNotice", unregisteredWidget("OrganizationContextNotice"));
329
323
  const JsonInspector = slotComponent("JsonInspector", unregisteredWidget("JsonInspector"));
330
- const InlineCopyButton = slotComponent("InlineCopyButton", unregisteredWidget("InlineCopyButton"));
331
324
  const ConfirmDialog = slotComponent("ConfirmDialog", unregisteredWidget("ConfirmDialog"));
332
325
  const ModelListDropdown = slotComponent("ModelListDropdown", unregisteredWidget("ModelListDropdown"));
333
326
  const TextWithDoors = slotComponent("TextWithDoors", unregisteredWidget("TextWithDoors"));
@@ -346,6 +339,7 @@ const AnswerTextPreview = slotComponent("AnswerTextPreview", unregisteredWidget(
346
339
  const AccessGate = slotComponent("AccessGate", unregisteredWidget("AccessGate"));
347
340
  const ReferenceCopyMenuItem = slotComponent("ReferenceCopyMenuItem", unregisteredWidget("ReferenceCopyMenuItem"));
348
341
  const ReferenceCopyButton = slotComponent("ReferenceCopyButton", unregisteredWidget("ReferenceCopyButton"));
342
+ const InlineCopyButton = slotComponent("InlineCopyButton", unregisteredWidget("InlineCopyButton"));
349
343
  const MandateNotesPanel = slotComponent("MandateNotesPanel", unregisteredWidget("MandateNotesPanel"));
350
344
  const SurfaceBoundAgentsList = slotComponent("SurfaceBoundAgentsList", unregisteredWidget("SurfaceBoundAgentsList"));
351
345
  const ProposedDirectivesZone = slotComponent("ProposedDirectivesZone", unregisteredWidget("ProposedDirectivesZone"));
@@ -417,12 +411,10 @@ const adoptCloudBrowserRunFromStream = slotFn("adoptCloudBrowserRunFromStream");
417
411
  const dispatchWarRoomTool = slotFn("dispatchWarRoomTool");
418
412
  const dispatchWarRoomMasterTool = slotFn("dispatchWarRoomMasterTool");
419
413
  const resolveGmailSendConnection = slotFn("resolveGmailSendConnection");
420
- const voiceDisplayName = slotFn("voiceDisplayName", (_set, id) => id);
421
414
  const canvasGetVersionHistory = slotFn("canvasGetVersionHistory");
422
415
  const canvasGetById = slotFn("canvasGetById");
423
416
  const createSandboxFilesystemAdapter = slotFn("createSandboxFilesystemAdapter");
424
417
  const notesGetById = slotFn("notesGetById");
425
- const isLiveConversationVoice = slotFn("isLiveConversationVoice", () => false);
426
418
  const createHtmlPage = slotFn("createHtmlPage");
427
419
  const convertMarkdownToHtml = slotFn("convertMarkdownToHtml");
428
420
  const sklActions = new Proxy({}, { get: (_t, prop) => (...args) => {
@@ -668,7 +660,6 @@ export {
668
660
  humanLines,
669
661
  invalidateCanvasItemCache,
670
662
  invalidateMandateCatalogueCache,
671
- isLiveConversationVoice,
672
663
  isSkillStreamEvent,
673
664
  isUuidValue,
674
665
  isWarRoomThreadAgentSurface,
@@ -771,6 +762,5 @@ export {
771
762
  useSkills,
772
763
  useTablesEverywhere,
773
764
  useTextareaFormatting,
774
- useUrlSync,
775
- voiceDisplayName
765
+ useUrlSync
776
766
  };
@@ -29,6 +29,7 @@ import { openOverlay, CHAT_WINDOWS } from "../../host/windows.js";
29
29
  import { kindTextToMarkdown } from "../../utils/content-ir/surfaces/kind-text-to-markdown.js";
30
30
  import { removeCodeSpans, replaceFences } from "@ai-matrx/content-ir/source";
31
31
  import { selectOrganizationId, ensureOrganizationContext, isOrganizationSelectionCancelled } from "../../host/org.js";
32
+ import { downloadFile } from "@ai-matrx/kit/download";
32
33
  const PENDING_ACTION_KEY = "matrx_pending_post_auth_action";
33
34
  const PublicMessageOptionsMenu = ({
34
35
  content: answerContent,
@@ -248,15 +249,7 @@ const PublicMessageOptionsMenu = ({
248
249
  };
249
250
  const handleSaveAsFile = () => {
250
251
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
251
- const blob = new Blob([content], { type: "text/markdown;charset=utf-8" });
252
- const url = URL.createObjectURL(blob);
253
- const link = document.createElement("a");
254
- link.href = url;
255
- link.download = `ai-response-${timestamp}.md`;
256
- document.body.appendChild(link);
257
- link.click();
258
- document.body.removeChild(link);
259
- URL.revokeObjectURL(url);
252
+ downloadFile(`ai-response-${timestamp}.md`, content, "text/markdown;charset=utf-8");
260
253
  onClose();
261
254
  };
262
255
  const handleEmailToMe = async () => {
@@ -1,5 +1,14 @@
1
+ import { type FilesHost } from "@ai-matrx/media/files/engine";
1
2
  import type { ChatState } from "./state.js";
2
- export declare function createChatStore(preloadedState?: Partial<ChatState>): import("@reduxjs/toolkit").EnhancedStore<{
3
+ export interface CreateChatStoreOptions {
4
+ /**
5
+ * The files engine's host (database, server client, optional scope/org/share/notify) for a
6
+ * host that has not called `configureFilesHost` itself. The engine is then wired to THIS
7
+ * store, so file thunks read and write the slice chat renders from.
8
+ */
9
+ files?: Omit<FilesHost, "store">;
10
+ }
11
+ export declare function createChatStore(preloadedState?: Partial<ChatState>, options?: CreateChatStoreOptions): import("@reduxjs/toolkit").EnhancedStore<{
3
12
  voiceAgent: import("../voice-agent/types.js").VoiceAgentState;
4
13
  agentDefinition: import("../agents/types/agent-definition.types.js").AgentDefinitionSliceState;
5
14
  conversationList: import("../agents/redux/conversation-list/conversation-list.types.js").ConversationListState;
@@ -4,12 +4,13 @@ import { fork } from "redux-saga/effects";
4
4
  import {
5
5
  cloudFilesMutationToastMiddleware,
6
6
  cloudFilesRealtimeMiddleware,
7
- cloudFilesReducer
7
+ cloudFilesReducer,
8
+ configureFilesHost
8
9
  } from "@ai-matrx/media/files/engine";
9
10
  import { chatReducers } from "./slices.js";
10
11
  import { chatMiddlewares } from "./middlewares.js";
11
12
  import { chatSagas } from "./sagas.js";
12
- function createChatStore(preloadedState) {
13
+ function createChatStore(preloadedState, options = {}) {
13
14
  const sagaMiddleware = createSagaMiddleware();
14
15
  const store = configureStore({
15
16
  // Files are part of chat (P16f): the files engine's slice and its two middlewares come
@@ -30,6 +31,7 @@ function createChatStore(preloadedState) {
30
31
  ),
31
32
  devTools: process.env.NODE_ENV !== "production"
32
33
  });
34
+ if (options.files) configureFilesHost({ ...options.files, store: () => store });
33
35
  sagaMiddleware.run(function* chatRootSaga() {
34
36
  for (const saga of chatSagas()) yield fork(saga);
35
37
  });
@@ -1,3 +1,4 @@
1
+ import { type LiveValuesStore } from "@ai-matrx/alchemy/surface";
1
2
  import { type ReactNode } from "react";
2
3
  import type { SurfaceScopePayload } from "../types.js";
3
4
  import type { InstanceContextEntry } from "../../agents/types/instance.types.js";
@@ -198,6 +199,15 @@ export interface SurfaceRegistry {
198
199
  /** Called after every registration change. */
199
200
  subscribe: (listener: () => void) => () => void;
200
201
  }
202
+ /**
203
+ * THE PAGE'S LIVE VALUES REACH THE AGENT (ALC-18). A value the surface DECLARES that its runtime
204
+ * and contributions left unset is answered from the page's live capture (`@ai-matrx/alchemy/surface`
205
+ * `liveValues`: identity, selection, sections), only while the publishing page IS this surface —
206
+ * the same rule as `createSurfacePort({ live })`'s `getValue`. A supplied value always wins, and an
207
+ * undeclared live name (the request log, a section nobody declared) never rides along. Every reader
208
+ * of `getScope()` — the send path's context entries, the context window, write-back — gets it.
209
+ */
210
+ export declare function withLiveValues(surfaceName: string, loaded: SurfaceScopePayload, live?: LiveValuesStore): SurfaceScopePayload;
201
211
  /**
202
212
  * The hosts `surfaceName` was mounted inside earlier in this page session
203
213
  * that are STILL mounted, deepest first. Empty when `surfaceName` itself is
@@ -1,6 +1,8 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import { announceUndeclaredLoadedValues } from "./loaded-value-check.js";
4
+ import { liveValueOf, liveValues } from "@ai-matrx/alchemy/surface";
5
+ import { getManifest } from "./registry.js";
4
6
  import {
5
7
  createContext,
6
8
  useCallback,
@@ -15,6 +17,25 @@ import {
15
17
  useAlchemySurfaceHandle
16
18
  } from "../../agent-copy/AlchemySurfaceBridge.js";
17
19
  let nextId = 0;
20
+ function withLiveValues(surfaceName, loaded, live = liveValues) {
21
+ const declared = getManifest(surfaceName)?.values ?? [];
22
+ const missing = declared.filter((v) => !(v.name in loaded));
23
+ if (missing.length === 0) return loaded;
24
+ let capture;
25
+ try {
26
+ capture = live.readFor(surfaceName);
27
+ } catch (err) {
28
+ console.error(`[surfaces] the live values of "${surfaceName}" could not be read`, err);
29
+ return loaded;
30
+ }
31
+ if (!capture) return loaded;
32
+ const filled = { ...loaded };
33
+ for (const v of missing) {
34
+ const value = liveValueOf(capture, v.name);
35
+ if (value !== void 0) filled[v.name] = value;
36
+ }
37
+ return filled;
38
+ }
18
39
  function createRegistry(kind, onRegister) {
19
40
  let stack = [];
20
41
  const listeners = /* @__PURE__ */ new Set();
@@ -70,7 +91,7 @@ function createRegistry(kind, onRegister) {
70
91
  }
71
92
  const loaded = { ...own, ...contributed };
72
93
  announceUndeclaredLoadedValues(surfaceName, loaded);
73
- return loaded;
94
+ return withLiveValues(surfaceName, loaded);
74
95
  };
75
96
  return () => {
76
97
  const own = getScope();
@@ -531,5 +552,6 @@ export {
531
552
  waitForCapturedRuntime,
532
553
  wasPageOwnConversationOf,
533
554
  wasSurfaceMountedThisSession,
555
+ withLiveValues,
534
556
  withScopeContributions
535
557
  };
@@ -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 { resultToHuman } from "../utils/human-copy.js";
4
6
  import React, { useState } from "react";
5
7
  import { useToolConversationId } from "./ToolConversationContext.js";
@@ -33,18 +35,12 @@ const CopyButton = ({
33
35
  text,
34
36
  className
35
37
  }) => {
38
+ const { copyText } = useClipboard({
39
+ notify: (message, kind) => kind === "error" ? toast.error(message) : toast.success(message)
40
+ });
36
41
  const [copied, setCopied] = useState(false);
37
42
  const handleCopy = async () => {
38
- try {
39
- await navigator.clipboard.writeText(text);
40
- } catch {
41
- const textarea = document.createElement("textarea");
42
- textarea.value = text;
43
- document.body.appendChild(textarea);
44
- textarea.select();
45
- document.execCommand("copy");
46
- document.body.removeChild(textarea);
47
- }
43
+ await copyText(text);
48
44
  setCopied(true);
49
45
  setTimeout(() => setCopied(false), 2e3);
50
46
  };
@@ -8,6 +8,7 @@ import { formatFileSize } from "@ai-matrx/kit/format";
8
8
  import { useFileActions } from "../../host/ui-slots.js";
9
9
  import { useMediaResolution } from "@ai-matrx/media/core";
10
10
  import { Button } from "@ai-matrx/design-system/controls";
11
+ import { downloadUrl } from "@ai-matrx/kit/download";
11
12
  function deriveName(file) {
12
13
  if (file.file_name && file.file_name.trim().length > 0) return file.file_name.trim();
13
14
  const src = file.download_url ?? file.url;
@@ -33,14 +34,7 @@ function deriveTypeLabel(file, name) {
33
34
  return null;
34
35
  }
35
36
  function triggerAnchorDownload(href, name) {
36
- const a = document.createElement("a");
37
- a.href = href;
38
- a.download = name;
39
- a.target = "_blank";
40
- a.rel = "noopener noreferrer";
41
- document.body.appendChild(a);
42
- a.click();
43
- a.remove();
37
+ downloadUrl(href, name, { newTab: true });
44
38
  }
45
39
  const ResultFile = ({ file, density = "inline", className }) => {
46
40
  const [busy, setBusy] = useState(false);
@@ -2,31 +2,9 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import React from "react";
4
4
  import { Check, Copy } from "lucide-react";
5
+ import { copyText } from "@ai-matrx/kit/clipboard";
5
6
  import { cn } from "@ai-matrx/design-system";
6
7
  import { Button } from "@ai-matrx/design-system/controls";
7
- async function copyText(text) {
8
- try {
9
- if (navigator.clipboard?.writeText) {
10
- await navigator.clipboard.writeText(text);
11
- return true;
12
- }
13
- } catch {
14
- }
15
- try {
16
- const ta = document.createElement("textarea");
17
- ta.value = text;
18
- ta.style.position = "fixed";
19
- ta.style.opacity = "0";
20
- ta.style.pointerEvents = "none";
21
- document.body.appendChild(ta);
22
- ta.select();
23
- const ok = document.execCommand("copy");
24
- document.body.removeChild(ta);
25
- return ok;
26
- } catch {
27
- return false;
28
- }
29
- }
30
8
  const ShortId = ({
31
9
  value,
32
10
  variant = "compact",
@@ -2,7 +2,7 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { SettingDoor } from "../../host/prefs-react.js";
4
4
  import { useAppSelector } from "../../store/hooks.js";
5
- import { voiceDisplayName } from "../../host/ui-slots.js";
5
+ import { voiceDisplayName } from "@ai-matrx/media/voices";
6
6
  function LiveVoiceDoor({
7
7
  voiceId,
8
8
  agentId
@@ -12,7 +12,7 @@ import {
12
12
  useRealtimeSessionConfig
13
13
  } from "../realtimeSession.js";
14
14
  import { useSessionKnob } from "../../host/prefs-react.js";
15
- import { isLiveConversationVoice } from "../../host/ui-slots.js";
15
+ import { isLiveConversationVoice } from "@ai-matrx/media/voices";
16
16
  const LIVE_CONVERSATION_VOICE_KNOB = "media.conversation.voice";
17
17
  function isVoiceId(v) {
18
18
  return isLiveConversationVoice(v);
@@ -1,20 +1,3 @@
1
- /**
2
- * ObservationalMemoryWindow
3
- *
4
- * Admin-only floating inspector for the Observational Memory feature.
5
- * Wraps `ObservationalMemoryCore` in the standard WindowPanel shell with a
6
- * sidebar listing every conversation the session has seen memory activity
7
- * for — that means either:
8
- * - the conversation has a persisted `observational_memory` metadata block
9
- * (hydrated on conversation load), OR
10
- * - we have received at least one `memory_*` stream event for it this
11
- * session, OR
12
- * - an authoritative cost summary has been fetched for it.
13
- *
14
- * See `/window-panel-authoring` for the registration contract. This window is
15
- * persisted (NOT ephemeral) — admins get their last-selected conversation
16
- * back after a reload, matching the per-conversation nature of the feature.
17
- */
18
1
  import React from "react";
19
2
  interface ObservationalMemoryWindowProps {
20
3
  isOpen: boolean;
@@ -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 { useCallback, useMemo, useState } from "react";
4
6
  import {
5
7
  Brain,
@@ -36,9 +38,12 @@ import {
36
38
  import { CHAT_WINDOWS } from "../../../host/windows.js";
37
39
  import { Button } from "@ai-matrx/design-system/controls";
38
40
  function useCopyText(text) {
41
+ const { copyText } = useClipboard({
42
+ notify: (message, kind) => kind === "error" ? toast.error(message) : toast.success(message)
43
+ });
39
44
  const [copied, setCopied] = useState(false);
40
45
  const copy = useCallback(() => {
41
- navigator.clipboard.writeText(text).then(() => {
46
+ copyText(text).then(() => {
42
47
  setCopied(true);
43
48
  setTimeout(() => setCopied(false), 1500);
44
49
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/chat",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
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",
205
207
  "@ai-matrx/browser-audio": "latest",
206
208
  "@ai-matrx/canvas": "latest",
207
209
  "@ai-matrx/content-ir": "latest",
208
- "@ai-matrx/content-ir-react": "latest",
209
210
  "@ai-matrx/data": "latest",
210
211
  "@ai-matrx/design-system": "latest",
211
212
  "@ai-matrx/diff": "latest",
212
213
  "@ai-matrx/icons": "latest",
213
- "@ai-matrx/kit": "latest",
214
+ "@ai-matrx/content-ir-react": "latest",
215
+ "@ai-matrx/media": "latest",
214
216
  "@ai-matrx/print": "latest",
215
217
  "@ai-matrx/realtime": "latest",
216
218
  "@ai-matrx/records": "latest",
217
219
  "@ai-matrx/records-ui": "latest",
218
220
  "@ai-matrx/tap-target": "latest",
219
- "@ai-matrx/alchemy": "latest",
220
- "@ai-matrx/associations": "latest",
221
- "@ai-matrx/media": "latest"
221
+ "@ai-matrx/kit": "latest"
222
222
  },
223
223
  "devDependencies": {
224
224
  "@arethetypeswrong/cli": "^0.18.5",