@ai-matrx/design-system 0.18.1 → 0.18.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -648,6 +648,10 @@ function materialIdentity(material) {
648
648
  }
649
649
  return `payload:${id}`;
650
650
  }
651
+ function snapshotContent(snapshot) {
652
+ const { sourceId, label, payload, coverage, unsavedChanges, omissions, sections, limits } = snapshot;
653
+ return JSON.stringify({ sourceId, label, payload, coverage, unsavedChanges, omissions, sections, limits });
654
+ }
651
655
  function useFence() {
652
656
  const mounted = React7.useRef(true);
653
657
  const nextId = React7.useRef(0);
@@ -696,7 +700,12 @@ async function buildArtifact(draft, format, label) {
696
700
  const extension = format === "plain" ? "txt" : format === "compact-json" ? "json" : format;
697
701
  return serialize(draft, format, { spreadsheetSafe: format === "csv" || format === "tsv", filename: `${filenameFor(label)}.${extension}` });
698
702
  }
699
- function ContentTransferMenu({ source, surface, label, projection, sections, preferences, table, formatSources, variants = EMPTY_VARIANTS, actions = [], capabilities: local, disabled, enabled = true, className, stopPropagation = true, hide = [], controllerRef }) {
703
+ function ContentTransferMenu(props) {
704
+ const inherited = React7.useContext(CapabilityContext);
705
+ const identityKey = props.capabilities?.identityKey ?? inherited.identityKey;
706
+ return /* @__PURE__ */ jsx7(ContentTransferMenuSession, { ...props }, identityKey ?? "anonymous");
707
+ }
708
+ function ContentTransferMenuSession({ source, surface, label, projection, sections, preferences, table, formatSources, variants = EMPTY_VARIANTS, actions = [], capabilities: local, disabled, enabled = true, className, stopPropagation = true, hide = [], controllerRef }) {
700
709
  const triggerRef = React7.useRef(null);
701
710
  const inherited = React7.useContext(CapabilityContext);
702
711
  const nearestSurface = React7.useContext(SurfaceContext);
@@ -745,7 +754,7 @@ function ContentTransferMenu({ source, surface, label, projection, sections, pre
745
754
  const captured = await material.capture(signal);
746
755
  sourceMaterial = { id: captured.snapshot.sourceId, label: captured.snapshot.label, capture: async () => captured };
747
756
  } else if ("kind" in material) {
748
- sourceMaterial = applyMenuProjection ? { id: "direct", label: sourceLabel, capture: async () => directSource(material, { label: sourceLabel, ...projection ? { projection } : {}, ...sections ? { sections } : {} }) } : material;
757
+ sourceMaterial = { id: materialIdentity(material), label: sourceLabel, capture: async () => directSource(material, { sourceId: materialIdentity(material), label: sourceLabel, ...applyMenuProjection && projection ? { projection } : {}, ...applyMenuProjection && sections ? { sections } : {} }) };
749
758
  } else if (applyMenuProjection && (projection || sections)) {
750
759
  sourceMaterial = {
751
760
  id: material.id,
@@ -762,7 +771,7 @@ function ContentTransferMenu({ source, surface, label, projection, sections, pre
762
771
  } else {
763
772
  sourceMaterial = material;
764
773
  }
765
- const raw = "kind" in sourceMaterial ? directSource(sourceMaterial) : await sourceMaterial.capture({ scope, signal });
774
+ const raw = await sourceMaterial.capture({ scope, signal });
766
775
  const constrained = resolveTransferPreferences(providerPreferences, preferences, raw.snapshot.limits);
767
776
  const result = await capture({ id: raw.snapshot.sourceId, label: raw.snapshot.label, capture: async () => ({ ...raw, snapshot: { ...raw.snapshot, limits: constrained.limits } }) }, signal, scope);
768
777
  if (scope !== "all-matching" || !collectPages) return result;
@@ -822,31 +831,117 @@ function ContentTransferMenu({ source, surface, label, projection, sections, pre
822
831
  fence.finish(controller);
823
832
  }
824
833
  }, [captureOne, capturePrimary, fence, formatSources, initialScope, preferences, providerPreferences, report, transport]);
825
- const openWorkspace = React7.useCallback(async (key = primaryWorkspaceKey, variant) => {
826
- const variantMaterial = variant && "kind" in variant.source ? { id: `variant:${key}`, label: variant.label, capture: async () => directSource(variant.source, { label: variant.label }) } : variant?.source;
827
- const captureWorkspace = variantMaterial ? (scope, signal) => captureOne(variantMaterial, scope, signal, true) : capturePrimary;
828
- workspaceCaptures.current.set(key, captureWorkspace);
829
- if (workspaces[key]) {
830
- setWorkspaceKey(key);
831
- setWorkspaceOpen(true);
832
- return;
833
- }
834
- const { controller, id } = fence.begin();
835
- try {
836
- const result = await captureWorkspace(initialScope, controller.signal);
837
- if (!fence.valid(id, controller)) return;
838
- const resolved = resolveTransferPreferences(providerPreferences, preferences, result.snapshot.limits);
839
- const original = createDraft(result.snapshot);
840
- const base = { ...result, key, original, draft: original, scope: initialScope, preferences: resolved, ...result.collectionError ? { error: result.collectionError } : {} };
841
- setWorkspaces((current) => ({ ...current, [key]: { ...base, draft: rebuild(base, initialControls(base)) } }));
842
- setWorkspaceKey(key);
843
- setWorkspaceOpen(true);
844
- } catch (error) {
845
- if (fence.valid(id, controller)) setNotice(errorMessage(error, "Could not capture content"));
846
- } finally {
847
- fence.finish(controller);
848
- }
849
- }, [captureOne, capturePrimary, fence, initialScope, preferences, primaryWorkspaceKey, providerPreferences, workspaces]);
834
+ const openWorkspace = React7.useCallback(
835
+ async (key = primaryWorkspaceKey, variant) => {
836
+ const variantMaterial = variant && "kind" in variant.source ? {
837
+ id: `variant:${key}`,
838
+ label: variant.label,
839
+ capture: async () => directSource(variant.source, {
840
+ sourceId: `variant:${key}`,
841
+ label: variant.label
842
+ })
843
+ } : variant?.source;
844
+ const captureWorkspace = (scope, signal, collectPages = true) => captureOne(variantMaterial ?? primary, scope, signal, true, collectPages);
845
+ workspaceCaptures.current.set(key, captureWorkspace);
846
+ const existing = workspaces[key];
847
+ const { controller, id } = fence.begin();
848
+ if (existing) {
849
+ try {
850
+ const allMatching = existing.scope === "all-matching";
851
+ const fresh = await captureWorkspace(
852
+ existing.scope,
853
+ controller.signal,
854
+ !allMatching
855
+ );
856
+ if (!fence.valid(id, controller) || !workspaceCaptures.current.has(key))
857
+ return;
858
+ if (allMatching && (!existing.allMatching || !fresh.allMatching))
859
+ throw new Error(
860
+ "This source can no longer provide an all-matching snapshot."
861
+ );
862
+ const collectionIdentity = (value) => value && {
863
+ query: value.source.query,
864
+ projection: value.projection,
865
+ columns: value.columns,
866
+ limits: value.limits
867
+ };
868
+ const changed = allMatching ? JSON.stringify(collectionIdentity(existing.allMatching)) !== JSON.stringify(collectionIdentity(fresh.allMatching)) : snapshotContent(existing.snapshot) !== snapshotContent(fresh.snapshot);
869
+ setWorkspaces((current) => {
870
+ const retained = current[key];
871
+ if (!retained) return current;
872
+ return {
873
+ ...current,
874
+ [key]: {
875
+ ...retained,
876
+ error: fresh.collectionError,
877
+ ...allMatching ? { sourceStale: changed } : changed ? { pending: { ...fresh, scope: retained.scope } } : { pending: void 0 }
878
+ }
879
+ };
880
+ });
881
+ setWorkspaceKey(key);
882
+ setWorkspaceOpen(true);
883
+ } catch (error) {
884
+ if (fence.valid(id, controller) && workspaceCaptures.current.has(key)) {
885
+ setWorkspaces(
886
+ (current) => current[key] ? {
887
+ ...current,
888
+ [key]: {
889
+ ...current[key],
890
+ error: errorMessage(error, "Could not refresh source")
891
+ }
892
+ } : current
893
+ );
894
+ setWorkspaceKey(key);
895
+ setWorkspaceOpen(true);
896
+ }
897
+ } finally {
898
+ fence.finish(controller);
899
+ }
900
+ return;
901
+ }
902
+ try {
903
+ const result = await captureWorkspace(initialScope, controller.signal);
904
+ if (!fence.valid(id, controller)) return;
905
+ const resolved = resolveTransferPreferences(
906
+ providerPreferences,
907
+ preferences,
908
+ result.snapshot.limits
909
+ );
910
+ const original = createDraft(result.snapshot);
911
+ const base = {
912
+ ...result,
913
+ key,
914
+ original,
915
+ draft: original,
916
+ scope: initialScope,
917
+ preferences: resolved,
918
+ ...result.collectionError ? { error: result.collectionError } : {}
919
+ };
920
+ setWorkspaces((current) => ({
921
+ ...current,
922
+ [key]: { ...base, draft: rebuild(base, initialControls(base)) }
923
+ }));
924
+ setWorkspaceKey(key);
925
+ setWorkspaceOpen(true);
926
+ } catch (error) {
927
+ if (fence.valid(id, controller))
928
+ setNotice(errorMessage(error, "Could not capture content"));
929
+ } finally {
930
+ fence.finish(controller);
931
+ }
932
+ },
933
+ [
934
+ captureOne,
935
+ capturePrimary,
936
+ fence,
937
+ initialScope,
938
+ preferences,
939
+ primaryWorkspaceKey,
940
+ primary,
941
+ providerPreferences,
942
+ workspaces
943
+ ]
944
+ );
850
945
  React7.useImperativeHandle(controllerRef, () => ({
851
946
  focusTrigger: () => triggerRef.current?.focus(),
852
947
  prepare: async (requestedSource, options) => {
@@ -989,6 +1084,9 @@ function PreparationWorkspace({ restoreFocus, open, state, scopes, onClose, onCh
989
1084
  const [instruction, setInstruction] = React7.useState("");
990
1085
  const [busy, setBusy] = React7.useState(null);
991
1086
  const [aiFailed, setAiFailed] = React7.useState(false);
1087
+ React7.useEffect(() => {
1088
+ setAiFailed(false);
1089
+ }, [state.snapshot.id, state.draft.revision]);
992
1090
  const activeAi = React7.useRef(null);
993
1091
  const activeCapture = React7.useRef(null);
994
1092
  React7.useEffect(() => {
@@ -1066,7 +1164,7 @@ function PreparationWorkspace({ restoreFocus, open, state, scopes, onClose, onCh
1066
1164
  setBusy("capture");
1067
1165
  try {
1068
1166
  const result = await captureScope(scope, controller.signal);
1069
- if (fence.valid(id, controller) && latest.current.draft.revision === revision) onChange({ ...latest.current, pending: { ...result, scope }, error: result.collectionError });
1167
+ if (fence.valid(id, controller) && latest.current.draft.revision === revision) onChange({ ...latest.current, pending: { ...result, scope }, sourceStale: false, error: result.collectionError });
1070
1168
  } catch (error) {
1071
1169
  if (fence.valid(id, controller)) onChange({ ...latest.current, error: errorMessage(error, "Refresh failed") });
1072
1170
  } finally {
@@ -1299,7 +1397,7 @@ function PreparationWorkspace({ restoreFocus, open, state, scopes, onClose, onCh
1299
1397
  activeCapture.current = null;
1300
1398
  setBusy(null);
1301
1399
  }, children: "Stop loading rows" }) : null,
1302
- ai && aiSession.current?.recover && busy !== "ai" ? /* @__PURE__ */ jsx7(Button, { type: "button", variant: "outline", onClick: () => void runAi(true), children: aiFailed ? "Retry or rejoin AI run" : "Rejoin AI run" }) : null,
1400
+ ai && aiFailed && aiSession.current?.recover && busy !== "ai" ? /* @__PURE__ */ jsx7(Button, { type: "button", variant: "outline", onClick: () => void runAi(true), children: aiFailed ? "Retry or rejoin AI run" : "Rejoin AI run" }) : null,
1303
1401
  ai ? /* @__PURE__ */ jsx7("p", { className: "matrx-alchemy-meta", children: "AI creates a preview. It never copies automatically. Closing detaches this view without cancelling server work." }) : null,
1304
1402
  state.error ? /* @__PURE__ */ jsx7("p", { role: "alert", className: "matrx-alchemy-error", children: state.error }) : null
1305
1403
  ] }) : null
@@ -1309,6 +1407,7 @@ function PreparationWorkspace({ restoreFocus, open, state, scopes, onClose, onCh
1309
1407
  /* @__PURE__ */ jsx7(Button, { type: "button", variant: "outline", onClick: () => setPendingControls(null), children: "Keep edited draft" }),
1310
1408
  /* @__PURE__ */ jsx7(Button, { type: "button", onClick: applyPendingControls, children: "Rebuild from source" })
1311
1409
  ] }) : null,
1410
+ state.sourceStale ? /* @__PURE__ */ jsx7("p", { role: "alert", className: "matrx-alchemy-decision", children: "The source changed since this draft. Use Refresh source to review the latest data." }) : null,
1312
1411
  state.pending ? /* @__PURE__ */ jsxs4("div", { className: "matrx-alchemy-decision", role: "alert", children: [
1313
1412
  /* @__PURE__ */ jsx7("p", { children: "A fresh source snapshot is ready." }),
1314
1413
  /* @__PURE__ */ jsx7(Button, { type: "button", variant: "outline", onClick: () => resolveCapture("keep"), children: "Keep current snapshot and draft" }),
@@ -1426,7 +1525,17 @@ var sourceFor = (id, label, build) => ({
1426
1525
  }
1427
1526
  });
1428
1527
  function MatrxCopyMenu(props) {
1429
- return /* @__PURE__ */ jsx8(MatrxCopyMenuSession, { ...props }, props.sourceId ?? props.label);
1528
+ const inherited = useContentTransferCapabilities();
1529
+ return /* @__PURE__ */ jsx8(
1530
+ MatrxCopyMenuSession,
1531
+ {
1532
+ ...props
1533
+ },
1534
+ JSON.stringify([
1535
+ inherited.identityKey ?? null,
1536
+ props.sourceId ?? props.label
1537
+ ])
1538
+ );
1430
1539
  }
1431
1540
  function MatrxCopyMenuSession(props) {
1432
1541
  const instance = React8.useId();
@@ -1454,7 +1563,7 @@ function MatrxCopyMenuSession(props) {
1454
1563
  })) : void 0;
1455
1564
  const agent = aiVisible && props.agent !== void 0 ? make("agent", () => asPayload(resolve(props.agent))) : void 0;
1456
1565
  const structuredAgent = aiVisible && props.agent !== void 0 ? make("structured-agent", () => structuredPayload(resolve(props.agent))) : void 0;
1457
- const table = exportVisible && props.export?.sheetRows ? make("rows", () => {
1566
+ const table = (exportVisible || props.primarySource === "table") && props.export?.sheetRows ? make("rows", () => {
1458
1567
  const value = normalizeTransferJson(props.export.sheetRows());
1459
1568
  if (!Array.isArray(value) || value.some(
1460
1569
  (row) => row === null || typeof row !== "object" || Array.isArray(row)
@@ -1601,7 +1710,7 @@ function MatrxCopyMenuSession(props) {
1601
1710
  return props.sendToSheet(draft.payload.rows, props.label);
1602
1711
  }
1603
1712
  });
1604
- const source = json ?? table ?? structuredAgent ?? human ?? builderVariants[0]?.source;
1713
+ const source = (props.primarySource === "table" ? table : void 0) ?? json ?? table ?? structuredAgent ?? human ?? builderVariants[0]?.source;
1605
1714
  if (!source && !actions.length) return null;
1606
1715
  const target = source ?? make("actions", () => ({ kind: "text", text: props.label }));
1607
1716
  return /* @__PURE__ */ jsxs5(Fragment2, { children: [
@@ -1621,7 +1730,7 @@ function MatrxCopyMenuSession(props) {
1621
1730
  formatSources: {
1622
1731
  ...human ? { plain: human, markdown: humanMarkdown, html: humanMarkdown } : {},
1623
1732
  ...json ? { json, "compact-json": json } : {},
1624
- ...table ? { csv: table, tsv: table } : {}
1733
+ ...table && exportVisible ? { csv: table, tsv: table } : {}
1625
1734
  },
1626
1735
  ...props.disabled !== void 0 ? { disabled: props.disabled } : {},
1627
1736
  className: cn(
@@ -1904,8 +2013,9 @@ function useMatrxDataTableHost() {
1904
2013
  import { jsx as jsx10 } from "react/jsx-runtime";
1905
2014
  function CopyButtons(props) {
1906
2015
  const { CopyControls } = useMatrxDataTableHost();
1907
- if (CopyControls) return /* @__PURE__ */ jsx10(CopyControls, { ...props });
1908
- return /* @__PURE__ */ jsx10(MatrxCopyMenu, { ...props });
2016
+ const copyProps = props.export?.sheetRows ? { ...props, primarySource: "table" } : props;
2017
+ if (CopyControls) return /* @__PURE__ */ jsx10(CopyControls, { ...copyProps });
2018
+ return /* @__PURE__ */ jsx10(MatrxCopyMenu, { ...copyProps });
1909
2019
  }
1910
2020
 
1911
2021
  // src/data-table/MatrxUuidCell.tsx