@ai-matrx/chat 0.2.5 → 0.2.8

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 (59) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/agent-copy/export.d.ts +9 -0
  3. package/dist/agent-copy/export.js +13 -2
  4. package/dist/agents/addressing/agentAddress.js +1 -1
  5. package/dist/agents/components/context-items/bodies/ProcessedDocumentTitle.js +3 -2
  6. package/dist/agents/components/context-items/bodies/WebpageBody.js +6 -1
  7. package/dist/agents/components/debug/SandboxFileViewer.js +6 -1
  8. package/dist/agents/components/debug/StreamDebugPanel.js +6 -1
  9. package/dist/agents/components/inputs/input-components/MediaVariableInput.js +1 -1
  10. package/dist/agents/components/inputs/resources/attached-documents.js +3 -2
  11. package/dist/agents/components/inputs/resources/usePasteImageResource.js +2 -2
  12. package/dist/agents/components/messages-display/TranscriptIntegrityCopyButton.js +6 -2
  13. package/dist/agents/components/observational-memory/components/MemoryStateInspector.d.ts +0 -12
  14. package/dist/agents/components/observational-memory/components/MemoryStateInspector.js +10 -2
  15. package/dist/agents/components/run-controls/AgentExecutionTestModal.js +6 -1
  16. package/dist/agents/components/shared/AgentDuplicateOutcomeDialog.d.ts +26 -2
  17. package/dist/agents/components/shared/AgentDuplicateOutcomeDialog.js +50 -4
  18. package/dist/agents/components/shared/AgentOptionsMenu.js +11 -61
  19. package/dist/agents/components/shared/StreamProfilerOverlay.js +6 -1
  20. package/dist/agents/hooks/useAgentDuplicateFlow.d.ts +13 -3
  21. package/dist/agents/hooks/useAgentDuplicateFlow.js +81 -24
  22. package/dist/agents/hooks/useWidgetHandle.js +1 -1
  23. package/dist/agents/prompt-preview/PromptPreviewContent.js +6 -1
  24. package/dist/agents/redux/agent-definition/cache-bust-middleware.js +1 -1
  25. package/dist/agents/redux/agent-definition/parse-messages-variables.js +6 -0
  26. package/dist/agents/redux/agent-shortcuts/thunks.js +1 -1
  27. package/dist/agents/redux/execution-system/instance-resources/resource-source.js +1 -2
  28. package/dist/agents/redux/execution-system/thunks/execute-manual-instance.thunk.js +1 -1
  29. package/dist/agents/redux/execution-system/thunks/launch-conversation.thunk.js +1 -1
  30. package/dist/agents/redux/execution-system/utils/assert-conversation-id.js +1 -1
  31. package/dist/agents/redux/execution-system/utils/content-prefilter.js +2 -2
  32. package/dist/agents/runtime/realtime/launchRealtimeSession.thunk.js +1 -1
  33. package/dist/agents/surface-catalog-profile.d.ts +1 -1
  34. package/dist/agents/types/agent-definition.types.d.ts +8 -6
  35. package/dist/agents/utils/variable-customcomponent.d.ts +2 -2
  36. package/dist/agents/utils/variable-customcomponent.js +4 -4
  37. package/dist/cx-chat/components/messages/AssistantMessage.js +1 -1
  38. package/dist/cx-chat/components/user-input/ConversationInput.js +1 -1
  39. package/dist/cx-conversation/AssistantMessage.js +1 -1
  40. package/dist/cx-conversation/ConversationInput.js +1 -1
  41. package/dist/host/builder-door.d.ts +13 -2
  42. package/dist/host/db-types.d.ts +4 -4
  43. package/dist/host/defaults/diagnostics.js +1 -1
  44. package/dist/host/ui-slots.d.ts +0 -20
  45. package/dist/host/ui-slots.js +0 -26
  46. package/dist/store/create-chat-store.d.ts +2 -0
  47. package/dist/store/create-chat-store.js +15 -3
  48. package/dist/surfaces/runtime/loaded-value-check.js +1 -1
  49. package/dist/surfaces/runtime/surface-writeback.js +1 -1
  50. package/dist/surfaces/utils/route-to-surface.js +1 -1
  51. package/dist/surfaces/utils/surface-display.js +1 -1
  52. package/dist/tool-call-visualization/result-fields/shape.js +1 -1
  53. package/dist/ui/file-source.d.ts +3 -17
  54. package/dist/voice-agent/components/VoiceDebugPanel.js +6 -1
  55. package/dist/voice-agent/hooks/useRealtimeAgentConfig.js +1 -1
  56. package/dist/voice-agent/hooks/useXaiVoiceSession.js +2 -2
  57. package/dist/voice-agent/transport/xaiClient.js +1 -1
  58. package/dist/window-panels/windows/agents/AgentMemoryWindow.js +7 -5
  59. package/package.json +7 -7
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.2.8 — 2026-10-07
4
+
5
+ ALC-19: the one file-download door.
6
+
7
+ - `agent-copy/export`: `downloadFile` revokes its object URL after 1 s (Safari cancels a save revoked in the
8
+ click's tick); new `downloadUrl(url, filename, { newTab? })` saves a file that already has a URL (data:, blob:,
9
+ remote) — the hand-built anchor + `.download =` is gone from every host.
10
+ - Consumer action: replace hand-built `URL.createObjectURL` + anchor downloads with `downloadFile` / `downloadUrl`.
11
+
12
+ ## 0.2.7 — 2026-10-06
13
+ ### Re-sync before matrx-frontend consumes the published package (P27)
14
+
15
+ - Carries every matrx-frontend `packages/chat` edit made after base `9abc4bc369` (41 files, all
16
+ fast-forward): agent duplicate-any-version flow and one version-copy door, files engine read from
17
+ `@ai-matrx/media` directly (P16f, 8 file slots gone), ALC-16/19 kit clipboard copy, pick-list token,
18
+ `host/db-types.ts`, private-store-has-files suite.
19
+ - The ALC-19 copy call sites imported the app's `@/lib/toast`; they use the package notify port
20
+ (`host/notify`) instead, so the host-tie budget stays 0. `ProcessedDocumentTitle` dispatches the media
21
+ rename thunk through its own signature.
22
+ - Consumer action: none.
23
+
24
+ ## 0.2.6 — 2026-10-06
25
+
26
+ - The build no longer bakes `process.env.NODE_ENV` to `"development"` (esbuild's browser-platform default):
27
+ 16 modules read the host's value again, so dev-only checks stop running in production and the
28
+ test-mode throw in `surfaces/runtime/loaded-value-check` fires under Jest (P27).
29
+ - Consumer action: none.
30
+
3
31
  ## 0.2.5 — 2026-10-06
4
32
  ### Re-sync before matrx-frontend consumes the published package (P27)
5
33
 
@@ -22,6 +22,15 @@ export interface ExportItem {
22
22
  onSelect?: () => void | Promise<void>;
23
23
  }
24
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;
25
34
  /** Sanitized `base-2026-07-27.ext` filename. */
26
35
  export declare function exportFilename(base: string, extension: string): string;
27
36
  export declare function jsonExportItem(data: unknown | (() => unknown), label?: string): ExportItem;
@@ -1,13 +1,23 @@
1
1
  function downloadFile(filename, content, mime) {
2
2
  const blob = new Blob([content], { type: mime });
3
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) {
4
11
  const anchor = document.createElement("a");
5
- anchor.href = url;
12
+ anchor.href = href;
6
13
  anchor.download = filename;
14
+ if (newTab) {
15
+ anchor.target = "_blank";
16
+ anchor.rel = "noopener noreferrer";
17
+ }
7
18
  document.body.appendChild(anchor);
8
19
  anchor.click();
9
20
  document.body.removeChild(anchor);
10
- URL.revokeObjectURL(url);
11
21
  }
12
22
  function exportFilename(base, extension) {
13
23
  const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
@@ -81,6 +91,7 @@ function csvExportItem(rows, label = "CSV", columns) {
81
91
  export {
82
92
  csvExportItem,
83
93
  downloadFile,
94
+ downloadUrl,
84
95
  exportFilename,
85
96
  jsonExportItem,
86
97
  rowsToCsv,
@@ -37,7 +37,7 @@ function versionIdMessage(address, suppliedId, context) {
37
37
  function assertAgentIdNotVersion(address, suppliedId, context) {
38
38
  if (!address.isVersion || suppliedId === address.agentId) return;
39
39
  const message = versionIdMessage(address, suppliedId, context);
40
- if (true) throw new TypeError(message);
40
+ if (process.env.NODE_ENV !== "production") throw new TypeError(message);
41
41
  console.error(message);
42
42
  }
43
43
  function reportVersionIdRescued(address, suppliedId, context) {
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
+ import { renameFile } from "@ai-matrx/media/files/engine/redux/thunks";
3
4
  import { toast } from "../../../../host/notify.js";
4
5
  import { useAppDispatch } from "../../../../store/hooks.js";
5
- import { renameFile } from "../../../../host/ui-slots.js";
6
6
  import { useAttachedDocumentDisplayName } from "../../inputs/resources/attached-documents.js";
7
7
  import { PdfNamedSurfaceSwitcher } from "../../../../host/ui-slots.js";
8
8
  function ProcessedDocumentTitle({
@@ -20,7 +20,8 @@ function ProcessedDocumentTitle({
20
20
  const hasSiblings = siblings.length > 1;
21
21
  const handleRename = fileId ? async (next) => {
22
22
  try {
23
- await dispatch(renameFile({ fileId, newName: next })).unwrap();
23
+ const dispatchFiles = dispatch;
24
+ await dispatchFiles(renameFile({ fileId, newName: next })).unwrap();
24
25
  } catch (err) {
25
26
  toast.error(
26
27
  err instanceof Error ? `Rename failed: ${err.message}` : "Rename failed"
@@ -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 as copyToast } from "../../../../host/notify.js";
3
5
  import { useEffect, useState } from "react";
4
6
  import { Check, Copy, ExternalLink } from "lucide-react";
5
7
  import {
@@ -40,13 +42,16 @@ function WebpageBody({ item, setTitle }) {
40
42
  );
41
43
  }
42
44
  function WebpageFooter({ item }) {
45
+ const { copyText } = useClipboard({
46
+ notify: (message, kind) => kind === "error" ? copyToast.error(message) : copyToast.success(message)
47
+ });
43
48
  const webpage = firstWebpage(item);
44
49
  const url = webpage ? webpageUrl(webpage) : null;
45
50
  const [copied, setCopied] = useState(false);
46
51
  if (!url) return null;
47
52
  const copy = async () => {
48
53
  try {
49
- await navigator.clipboard.writeText(url);
54
+ await copyText(url);
50
55
  setCopied(true);
51
56
  setTimeout(() => setCopied(false), 1500);
52
57
  } catch {
@@ -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 as copyToast } from "../../../host/notify.js";
3
5
  import { Button } from "@ai-matrx/design-system/controls";
4
6
  import { useCallback, useEffect, useRef, useState } from "react";
5
7
  import { FileText, Loader2, RefreshCw, Copy, Check } from "lucide-react";
@@ -11,6 +13,9 @@ function SandboxFileViewer({
11
13
  initialPath = "",
12
14
  lockPath = false
13
15
  }) {
16
+ const { copyText } = useClipboard({
17
+ notify: (message, kind) => kind === "error" ? copyToast.error(message) : copyToast.success(message)
18
+ });
14
19
  const [path, setPath] = useState(initialPath);
15
20
  const [content, setContent] = useState(null);
16
21
  const [loading, setLoading] = useState(false);
@@ -45,7 +50,7 @@ function SandboxFileViewer({
45
50
  }, [initialPath, sandboxRowId, load]);
46
51
  const handleCopy = async () => {
47
52
  if (content == null) return;
48
- await navigator.clipboard.writeText(content);
53
+ await copyText(content);
49
54
  setCopied(true);
50
55
  toast.success("File contents copied");
51
56
  setTimeout(() => setCopied(false), 1500);
@@ -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 { Chip, Button, DisclosureHeader } from "@ai-matrx/design-system/controls";
4
6
  import { formatDurationMs } from "@ai-matrx/kit/format";
5
7
  import React, { useState, useRef, useEffect, useMemo } from "react";
@@ -49,9 +51,12 @@ function DebugTabScroll({ children }) {
49
51
  return /* @__PURE__ */ jsx("div", { className: "flex flex-col flex-1 min-h-0 h-full", children: /* @__PURE__ */ jsx(ScrollArea, { className: "flex-1 min-h-0", children }) });
50
52
  }
51
53
  function useCopy() {
54
+ const { copyText } = useClipboard({
55
+ notify: (message, kind) => kind === "error" ? toast.error(message) : toast.success(message)
56
+ });
52
57
  const [copied, setCopied] = useState(null);
53
58
  const copy = (text, id) => {
54
- navigator.clipboard.writeText(text);
59
+ copyText(text);
55
60
  setCopied(id);
56
61
  setTimeout(() => setCopied(null), 1500);
57
62
  };
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useFileUpload } from "@ai-matrx/media/files/engine/handler/hooks/useFileUpload";
3
4
  import { useCallback, useRef, useState } from "react";
4
5
  import {
5
6
  Image as ImageIcon,
@@ -20,7 +21,6 @@ import { FileResourceChip } from "../../../../host/ui-slots.js";
20
21
  import { InlineMediaRef } from "@ai-matrx/media/react";
21
22
  import { useFileDocument } from "../../../../host/ui-slots.js";
22
23
  import { useMediaResolution } from "@ai-matrx/media/core";
23
- import { useFileUpload } from "../../../../host/ui-slots.js";
24
24
  import { FilesResourcePicker } from "../../../../host/ui-slots.js";
25
25
  import { cn } from "@ai-matrx/design-system";
26
26
  import { ResourceFamilyPolicyEditor } from "../resources/ResourceFamilyPolicyEditor.js";
@@ -1,6 +1,7 @@
1
1
  "use client";
2
+ import { fileHandler } from "@ai-matrx/media/files/engine";
3
+ import { useFile } from "@ai-matrx/media/files/engine/handler/hooks/useFile";
2
4
  import { useEffect, useState } from "react";
3
- import { resolveFile, useFile } from "../../../../host/ui-slots.js";
4
5
  import { normalizeResourceFamilyPolicy } from "./resource-family-policy.js";
5
6
  const ATTACHED_DOCUMENT_TOKENS = ["processed_document", "file"];
6
7
  function parseAttachedDocumentMetadata(metadata) {
@@ -78,7 +79,7 @@ function useAttachedDocumentFileNames(fileIds) {
78
79
  if (!idsKey) return void 0;
79
80
  let cancelled = false;
80
81
  for (const fileId of idsKey.split(",")) {
81
- resolveFile({ kind: "file_id", fileId }).then((file) => {
82
+ fileHandler.resolve({ kind: "file_id", fileId }).then((file) => {
82
83
  const name = file.meta.fileName?.trim();
83
84
  if (cancelled || !name) return;
84
85
  setNames((prev) => prev[fileId] === name ? prev : { ...prev, [fileId]: name });
@@ -1,10 +1,10 @@
1
1
  "use client";
2
+ import { useFileUpload } from "@ai-matrx/media/files/engine/handler/hooks/useFileUpload";
3
+ import { normalize } from "@ai-matrx/media/files/engine";
2
4
  import { useEffect, useRef } from "react";
3
5
  import { toast } from "../../../../host/notify.js";
4
6
  import { useAppDispatch, useAppStore } from "../../../../store/hooks.js";
5
- import { useFileUpload } from "../../../../host/ui-slots.js";
6
7
  import { composeUploadFolderPath } from "@ai-matrx/media/files";
7
- import { normalize } from "../../../../host/ui-slots.js";
8
8
  import {
9
9
  addResource,
10
10
  removeResource,
@@ -1,12 +1,13 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
+ import { useClipboard } from "@ai-matrx/kit/clipboard";
4
+ import { toast as copyToast } from "../../../host/notify.js";
3
5
  import { useEffect } from "react";
4
6
  import { usePathname } from "../../../host/navigation.js";
5
7
  import { Stethoscope } from "lucide-react";
6
8
  import { useAppSelector, useAppStore } from "../../../store/hooks.js";
7
9
  import { selectMessageCount } from "../../redux/execution-system/messages/messages.selectors.js";
8
10
  import { selectStreamPhase } from "../../redux/execution-system/selectors/aggregate.selectors.js";
9
- import { writeClipboard } from "../../../agent-copy/clipboard.js";
10
11
  import { toast } from "../../../host/notify.js";
11
12
  import { useDebugContext } from "../../../host/prefs-react.js";
12
13
  import {
@@ -25,6 +26,9 @@ function TranscriptIntegrityCopyButtonInner({
25
26
  surfaceKey,
26
27
  effectiveVisibleGroupLimit
27
28
  }) {
29
+ const { copyText } = useClipboard({
30
+ notify: (message, kind) => kind === "error" ? copyToast.error(message) : copyToast.success(message)
31
+ });
28
32
  const store = useAppStore();
29
33
  const pathname = usePathname() ?? "";
30
34
  const messageCount = useAppSelector(selectMessageCount(conversationId));
@@ -64,7 +68,7 @@ function TranscriptIntegrityCopyButtonInner({
64
68
  effectiveVisibleGroupLimit
65
69
  });
66
70
  try {
67
- await writeClipboard(formatTranscriptIntegrityReport(report));
71
+ await copyText(formatTranscriptIntegrityReport(report));
68
72
  toast.success(
69
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`
70
74
  );
@@ -1,15 +1,3 @@
1
- /**
2
- * MemoryStateInspector
3
- *
4
- * Admin-only view of the raw `cx_observational_memory` row for a conversation.
5
- * Shows the actual `active_observations` text (what's being injected on every
6
- * turn), plus the buffered observation/reflection state, generation counter,
7
- * token counts, and metadata.
8
- *
9
- * We query Supabase directly with the RLS'd client — non-admin users will
10
- * see an empty result even if they somehow reach this component, and admins
11
- * will see the full row.
12
- */
13
1
  import React from "react";
14
2
  interface MemoryStateInspectorProps {
15
3
  conversationId: string;
@@ -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, useEffect, useMemo, useState } from "react";
4
6
  import { extractErrorMessage } from "@ai-matrx/data/net";
5
7
  import {
@@ -262,13 +264,16 @@ function BufferedObservationCard({
262
264
  cycle,
263
265
  defaultOpen
264
266
  }) {
267
+ const { copyText } = useClipboard({
268
+ notify: (message, kind) => kind === "error" ? toast.error(message) : toast.success(message)
269
+ });
265
270
  const [open, setOpen] = useState(defaultOpen);
266
271
  const [copied, setCopied] = useState(false);
267
272
  const text = cycle.observations ?? "";
268
273
  const hasText = text.length > 0;
269
274
  const handleCopy = useCallback(() => {
270
275
  if (!hasText) return;
271
- navigator.clipboard.writeText(text);
276
+ copyText(text);
272
277
  setCopied(true);
273
278
  setTimeout(() => setCopied(false), 1500);
274
279
  }, [text, hasText]);
@@ -498,11 +503,14 @@ function TextPayload({
498
503
  text,
499
504
  maxHeightClass = "max-h-64"
500
505
  }) {
506
+ const { copyText } = useClipboard({
507
+ notify: (message, kind) => kind === "error" ? toast.error(message) : toast.success(message)
508
+ });
501
509
  const [copied, setCopied] = useState(false);
502
510
  const hasText = typeof text === "string" && text.length > 0;
503
511
  const handleCopy = useCallback(() => {
504
512
  if (!text) return;
505
- navigator.clipboard.writeText(text);
513
+ copyText(text);
506
514
  setCopied(true);
507
515
  setTimeout(() => setCopied(false), 1500);
508
516
  }, [text]);
@@ -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 as copyToast } from "../../../host/notify.js";
3
5
  import { useState, useEffect, useCallback } from "react";
4
6
  import { kindTextToMarkdown } from "../../../utils/content-ir/surfaces/kind-text-to-markdown.js";
5
7
  import { useAppSelector } from "../../../store/hooks.js";
@@ -43,6 +45,9 @@ function DirectTestMode({
43
45
  userInput,
44
46
  apiEndpointMode
45
47
  }) {
48
+ const { copyText } = useClipboard({
49
+ notify: (message, kind) => kind === "error" ? copyToast.error(message) : copyToast.success(message)
50
+ });
46
51
  const { launchAgent, close } = useAgentLauncher();
47
52
  const [conversationId, setConversationId] = useState(null);
48
53
  const [copied, setCopied] = useState(false);
@@ -88,7 +93,7 @@ function DirectTestMode({
88
93
  ]);
89
94
  const handleCopy = useCallback(() => {
90
95
  if (responseText) {
91
- navigator.clipboard.writeText(kindTextToMarkdown(responseText));
96
+ copyText(kindTextToMarkdown(responseText));
92
97
  setCopied(true);
93
98
  setTimeout(() => setCopied(false), 2e3);
94
99
  }
@@ -1,4 +1,26 @@
1
- export type DuplicateOutcomeState = "loading" | "success" | "error";
1
+ /** "choose" = the person picks which version to copy before anything runs. */
2
+ export type DuplicateOutcomeState = "choose" | "loading" | "success" | "error";
3
+ /** The value of the "copy the agent as it is now" choice. */
4
+ export declare const CURRENT_VERSION_CHOICE = "current";
5
+ export interface DuplicateVersionOption {
6
+ versionId: string;
7
+ versionNumber: number;
8
+ changedAt: string;
9
+ changeNote: string | null;
10
+ }
11
+ export interface DuplicateVersionChooser {
12
+ /** The source agent's live version number, shown on the default choice. */
13
+ currentVersion: number | null;
14
+ /** Saved past versions, newest first. */
15
+ versions: DuplicateVersionOption[];
16
+ versionsLoading: boolean;
17
+ /** Set when the version list could not be read — the current version still works. */
18
+ versionsError: string | null;
19
+ /** CURRENT_VERSION_CHOICE or a version id. */
20
+ selected: string;
21
+ onSelectedChange: (value: string) => void;
22
+ onConfirm: () => void;
23
+ }
2
24
  export interface AgentDuplicateOutcomeDialogProps {
3
25
  open: boolean;
4
26
  /** Called when the user dismisses the dialog (X button, escape, or "Stay
@@ -15,6 +37,8 @@ export interface AgentDuplicateOutcomeDialogProps {
15
37
  /** Whether the source agent was a builtin/system agent. Tweaks the success
16
38
  * copy so admins know the duplicate is also a system agent. */
17
39
  asSystem?: boolean;
40
+ /** Required for the "choose" state. */
41
+ chooser?: DuplicateVersionChooser;
18
42
  }
19
43
  /**
20
44
  * Post-duplicate "where would you like to go?" choice surface.
@@ -30,4 +54,4 @@ export interface AgentDuplicateOutcomeDialogProps {
30
54
  * Mobile renders as a bottom Drawer (per project mobile rules — never Dialog
31
55
  * on mobile); desktop uses a small Dialog (`sm:max-w-md`).
32
56
  */
33
- export declare function AgentDuplicateOutcomeDialog({ open, onOpenChange, state, newAgentName, newAgentPath, errorMessage, asSystem, }: AgentDuplicateOutcomeDialogProps): import("react").JSX.Element;
57
+ export declare function AgentDuplicateOutcomeDialog({ open, onOpenChange, state, newAgentName, newAgentPath, errorMessage, asSystem, chooser, }: AgentDuplicateOutcomeDialogProps): import("react").JSX.Element;
@@ -26,7 +26,9 @@ import {
26
26
  } from "@ai-matrx/design-system";
27
27
  import { Alert, AlertDescription } from "@ai-matrx/design-system";
28
28
  import { Button } from "@ai-matrx/design-system";
29
+ import { Select } from "@ai-matrx/design-system/controls";
29
30
  import { useIsMobile } from "@ai-matrx/kit/media-query";
31
+ const CURRENT_VERSION_CHOICE = "current";
30
32
  function AgentDuplicateOutcomeDialog({
31
33
  open,
32
34
  onOpenChange,
@@ -34,7 +36,8 @@ function AgentDuplicateOutcomeDialog({
34
36
  newAgentName,
35
37
  newAgentPath,
36
38
  errorMessage,
37
- asSystem = false
39
+ asSystem = false,
40
+ chooser
38
41
  }) {
39
42
  const isMobile = useIsMobile();
40
43
  const router = useRouter();
@@ -52,9 +55,42 @@ function AgentDuplicateOutcomeDialog({
52
55
  const handleStayHere = () => {
53
56
  onOpenChange(false);
54
57
  };
55
- const titleText = state === "loading" ? "Duplicating agent\u2026" : state === "success" ? asSystem ? "System agent duplicated" : "Agent duplicated" : "Duplicate failed";
56
- const descriptionText = state === "loading" ? "Creating a copy with all messages, variables, settings, and tools." : state === "success" ? newAgentName ? `"${newAgentName}" is ready. Where would you like to go?` : "The copy is ready. Where would you like to go?" : errorMessage ?? "Something went wrong while duplicating.";
58
+ const titleText = state === "choose" ? "Duplicate agent" : state === "loading" ? "Duplicating agent\u2026" : state === "success" ? asSystem ? "System agent duplicated" : "Agent duplicated" : "Duplicate failed";
59
+ const descriptionText = state === "choose" ? "Pick the version to copy into a new agent." : state === "loading" ? "Creating a copy with all messages, variables, settings, and tools." : state === "success" ? newAgentName ? `"${newAgentName}" is ready. Where would you like to go?` : "The copy is ready. Where would you like to go?" : errorMessage ?? "Something went wrong while duplicating.";
57
60
  const body = /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
61
+ state === "choose" && chooser && /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
62
+ /* @__PURE__ */ jsx(
63
+ Select,
64
+ {
65
+ "aria-label": "Version to copy",
66
+ className: "w-full",
67
+ value: chooser.selected,
68
+ onValueChange: chooser.onSelectedChange,
69
+ options: [
70
+ {
71
+ value: CURRENT_VERSION_CHOICE,
72
+ label: chooser.currentVersion != null ? `Current version (v${chooser.currentVersion})` : "Current version"
73
+ },
74
+ ...chooser.versions.filter((v) => v.versionNumber !== chooser.currentVersion).map((v) => ({ value: v.versionId, label: versionOptionLabel(v) }))
75
+ ]
76
+ }
77
+ ),
78
+ chooser.versionsLoading && /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
79
+ /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
80
+ "Loading versions\u2026"
81
+ ] }),
82
+ chooser.versionsError && /* @__PURE__ */ jsxs("p", { className: "text-xs text-destructive", children: [
83
+ "Past versions could not be loaded: ",
84
+ chooser.versionsError
85
+ ] }),
86
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
87
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: handleStayHere, children: "Cancel" }),
88
+ /* @__PURE__ */ jsxs(Button, { onClick: chooser.onConfirm, children: [
89
+ /* @__PURE__ */ jsx(Copy, { className: "h-4 w-4" }),
90
+ "Duplicate"
91
+ ] })
92
+ ] })
93
+ ] }),
58
94
  state === "loading" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-3 py-6", children: [
59
95
  /* @__PURE__ */ jsx(Loader2, { className: "h-10 w-10 animate-spin text-primary" }),
60
96
  /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Please wait\u2026" })
@@ -137,6 +173,16 @@ function AgentDuplicateOutcomeDialog({
137
173
  body
138
174
  ] }) });
139
175
  }
176
+ function versionOptionLabel(v) {
177
+ const date = new Date(v.changedAt).toLocaleDateString(void 0, {
178
+ month: "short",
179
+ day: "numeric",
180
+ year: "numeric"
181
+ });
182
+ const note = v.changeNote?.trim();
183
+ return note ? `v${v.versionNumber} \xB7 ${date} \xB7 ${note.length > 40 ? `${note.slice(0, 40)}\u2026` : note}` : `v${v.versionNumber} \xB7 ${date}`;
184
+ }
140
185
  export {
141
- AgentDuplicateOutcomeDialog
186
+ AgentDuplicateOutcomeDialog,
187
+ CURRENT_VERSION_CHOICE
142
188
  };
@@ -2,7 +2,7 @@
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useAppDispatch, useAppSelector } from "../../../store/hooks.js";
4
4
  import { applyOrganizationContextHeader } from "../../../host/server/organization-context.js";
5
- import { getBuilderDoor, requireBuilderDoor } from "../../../host/builder-door.js";
5
+ import { getBuilderDoor } from "../../../host/builder-door.js";
6
6
  import { invalidateAgentCache } from "../../redux/agent-definition/invalidate-agent-cache.thunk.js";
7
7
  import { selectAgentById } from "../../redux/agent-definition/selectors.js";
8
8
  import { useOpenAgentSettingsWindow } from "../../../host/window-openers.js";
@@ -21,7 +21,7 @@ import { useOpenAgentInterfaceVariationsWindow } from "../../../host/window-open
21
21
  import { useOpenSaveTemplateDialog } from "../../../host/window-openers.js";
22
22
  const SAVE_AS_TEMPLATE_LABEL = "Save as template";
23
23
  import { useCallback, useState } from "react";
24
- import { usePathname, Link } from "../../../host/navigation.js";
24
+ import { Link } from "../../../host/navigation.js";
25
25
  import {
26
26
  MoreHorizontal,
27
27
  FileText,
@@ -66,17 +66,11 @@ import {
66
66
  } from "@ai-matrx/design-system";
67
67
  import { Drawer, DrawerContent, DrawerTitle } from "@ai-matrx/design-system";
68
68
  import { MenuTapButton } from "@ai-matrx/tap-target/buttons";
69
- import {
70
- AgentDuplicateOutcomeDialog
71
- } from "./AgentDuplicateOutcomeDialog.js";
69
+ import { useAgentDuplicateFlow } from "../../hooks/useAgentDuplicateFlow.js";
72
70
  import { ReferenceCopyMenuItem } from "../../../host/ui-slots.js";
73
- import {
74
- isAdminSystemAgentsContext
75
- } from "./agent-route-context.js";
76
71
  import { useAgentLifecycleActions } from "../../lifecycle/useAgentLifecycleActions.js";
77
- import { getUserMessage } from "@ai-matrx/agents/matrx";
78
72
  import { selectIsSuperAdmin } from "../../../host/identity.js";
79
- import { selectOrganizationId, isOrganizationSelectionCancelled } from "../../../host/org.js";
73
+ import { selectOrganizationId } from "../../../host/org.js";
80
74
  import { Button, Tile } from "@ai-matrx/design-system/controls";
81
75
  const INTERFACE_VARIATIONS = [
82
76
  "Full Modal",
@@ -182,7 +176,6 @@ function AgentOptionsMenu({
182
176
  const [isConverting, setIsConverting] = useState(false);
183
177
  const [isRefreshingCache, setIsRefreshingCache] = useState(false);
184
178
  const dispatch = useAppDispatch();
185
- const pathname = usePathname();
186
179
  const openSettings = useOpenAgentSettingsWindow();
187
180
  const openRunHistory = useOpenAgentRunHistoryWindow();
188
181
  const openAdvancedEditor = useOpenAgentContentWindow();
@@ -197,45 +190,13 @@ function AgentOptionsMenu({
197
190
  const openAdminFindUsages = useOpenAgentAdminFindUsagesWindow();
198
191
  const openImport = useOpenAgentImportWindow();
199
192
  const openInterfaceVariations = useOpenAgentInterfaceVariationsWindow();
200
- const [duplicateOpen, setDuplicateOpen] = useState(false);
201
- const [duplicateState, setDuplicateState] = useState("loading");
202
- const [duplicatedAgentId, setDuplicatedAgentId] = useState(
203
- null
204
- );
205
- const [duplicatedAgentName, setDuplicatedAgentName] = useState("");
206
- const [duplicateError, setDuplicateError] = useState("");
207
- const [duplicateAsSystem, setDuplicateAsSystem] = useState(false);
193
+ const duplicateFlow = useAgentDuplicateFlow(agentId, {
194
+ basePath,
195
+ fallbackSuffix: ""
196
+ });
208
197
  const agent = useAppSelector((state) => selectAgentById(state, agentId));
209
198
  const isBuiltin = agent?.agentType === "builtin";
210
- const isAdminContext = isAdminSystemAgentsContext(basePath);
211
- const newAgentPath = duplicatedAgentId ? (() => {
212
- const sourceSegment = `${basePath}/${agentId}`;
213
- const suffix = pathname && pathname.startsWith(sourceSegment) ? pathname.slice(sourceSegment.length) : "";
214
- return `${basePath}/${duplicatedAgentId}${suffix}`;
215
- })() : null;
216
- const runDuplicate = useCallback(async () => {
217
- const asSystem = isAdminContext && isBuiltin;
218
- setDuplicateAsSystem(asSystem);
219
- setDuplicatedAgentId(null);
220
- setDuplicatedAgentName(agent?.name ? `Copy of ${agent.name}` : "");
221
- setDuplicateError("");
222
- setDuplicateState("loading");
223
- setDuplicateOpen(true);
224
- try {
225
- const newId = await dispatch(
226
- requireBuilderDoor().duplicateAgent({ agentId, asSystem })
227
- ).unwrap();
228
- setDuplicatedAgentId(newId);
229
- setDuplicateState("success");
230
- } catch (err) {
231
- if (isOrganizationSelectionCancelled(err)) {
232
- setDuplicateOpen(false);
233
- return;
234
- }
235
- setDuplicateError(getUserMessage(err));
236
- setDuplicateState("error");
237
- }
238
- }, [agent?.name, agentId, dispatch, isAdminContext, isBuiltin]);
199
+ const runDuplicate = duplicateFlow.openChooser;
239
200
  const hasBuilderDoor = getBuilderDoor() !== null;
240
201
  const managementItems = AGENT_MANAGEMENT_ITEMS.filter(
241
202
  (item) => (!isBuiltin || item.label !== "Convert to Template") && (hasBuilderDoor || item.label !== "Duplicate")
@@ -395,18 +356,7 @@ function AgentOptionsMenu({
395
356
  setOpen(false);
396
357
  };
397
358
  const trigger = /* @__PURE__ */ jsx(MenuTapButton, { variant: "transparent" });
398
- const duplicateDialog = /* @__PURE__ */ jsx(
399
- AgentDuplicateOutcomeDialog,
400
- {
401
- open: duplicateOpen,
402
- onOpenChange: setDuplicateOpen,
403
- state: duplicateState,
404
- newAgentName: duplicatedAgentName,
405
- newAgentPath,
406
- errorMessage: duplicateError,
407
- asSystem: duplicateAsSystem
408
- }
409
- );
359
+ const duplicateDialog = duplicateFlow.dialog;
410
360
  if (isMobile) {
411
361
  return /* @__PURE__ */ jsxs(Fragment, { children: [
412
362
  /* @__PURE__ */ jsxs(Drawer, { open, onOpenChange: setOpen, children: [
@@ -518,7 +468,7 @@ function AgentOptionsMenu({
518
468
  /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
519
469
  /* @__PURE__ */ jsx(DropdownMenuLabel, { className: "text-[10px] uppercase tracking-wider text-muted-foreground/60 font-semibold", children: "Manage" }),
520
470
  managementItems.map(({ label, icon: Icon, soon }) => {
521
- const duplicateInFlight = duplicateOpen && duplicateState === "loading";
471
+ const duplicateInFlight = duplicateFlow.isDuplicating;
522
472
  const isLoading = label === "Convert to Template" && isConverting || label === "Duplicate" && duplicateInFlight;
523
473
  const displayLabel = label === "Convert to Template" && isConverting ? "Saving..." : label === "Duplicate" && duplicateInFlight ? "Duplicating..." : label;
524
474
  return /* @__PURE__ */ jsxs(
@@ -1,10 +1,15 @@
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 { useEffect, useState } from "react";
4
6
  import { Activity, X, Copy } from "lucide-react";
5
7
  import { cn } from "@ai-matrx/design-system";
6
8
  import { Button } from "@ai-matrx/design-system/controls";
7
9
  function StreamProfilerOverlay() {
10
+ const { copyText } = useClipboard({
11
+ notify: (message, kind) => kind === "error" ? toast.error(message) : toast.success(message)
12
+ });
8
13
  const [isOpen, setIsOpen] = useState(false);
9
14
  const [reports, setReports] = useState([]);
10
15
  useEffect(() => {
@@ -29,7 +34,7 @@ function StreamProfilerOverlay() {
29
34
  /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: /* @__PURE__ */ jsx(X, {}), "aria-label": "Close", onClick: () => setIsOpen(false) })
30
35
  ] }),
31
36
  /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: reports.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-center text-slate-500 italic py-8", children: "No stream data collected yet. Trigger a prompt." }) : reports.map((r, i) => /* @__PURE__ */ jsxs("div", { className: "bg-slate-900 border border-slate-800 rounded-lg p-3 space-y-2 relative group", children: [
32
- /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: /* @__PURE__ */ jsx(Copy, {}), onClick: () => navigator.clipboard.writeText(JSON.stringify(r, null, 2)), title: "Copy JSON", "aria-label": "Copy JSON", className: "absolute top-2 right-2 opacity-0 group-hover:opacity-100" }),
37
+ /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: /* @__PURE__ */ jsx(Copy, {}), onClick: () => copyText(JSON.stringify(r, null, 2)), title: "Copy JSON", "aria-label": "Copy JSON", className: "absolute top-2 right-2 opacity-0 group-hover:opacity-100" }),
33
38
  /* @__PURE__ */ jsx("div", { className: "text-blue-400 font-bold mb-2", children: r.Test }),
34
39
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-x-4 gap-y-1 text-xs", children: [
35
40
  /* @__PURE__ */ jsx("div", { className: "text-slate-500", children: "Client Duration:" }),