@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.3 — 2026-09-12
4
+
5
+ Reopening an Alchemy preparation now checks the current source while retaining
6
+ manual drafts. Changed direct sources offer an explicit keep-or-regenerate
7
+ choice, all-matching sources detect identity changes without eagerly fetching
8
+ pages, and failed refreshes remain visible instead of silently reusing stale
9
+ content.
10
+
11
+ ### Consumer action
12
+
13
+ Update to 0.18.3. No host configuration changes are required.
14
+
15
+ ## 0.18.2 — 2026-09-12
16
+
17
+ Alchemy copy sessions now reset only when their explicit identity changes,
18
+ preventing organization or authentication transitions from leaking prepared
19
+ state across sessions. Data-table copy sources explicitly identify the table as
20
+ their primary source while preserving raw JSON and hidden-export preparation.
21
+ Failed-run recovery remains limited to valid recoverable runs.
22
+
23
+ ### Consumer action
24
+
25
+ Update to 0.18.2 and provide a stable `identityKey` wherever authentication or
26
+ organization identity can change during a mounted Alchemy session.
27
+
3
28
  ## 0.18.1 — 2026-09-12
4
29
 
5
30
  Reorderable table columns now drag from the visible header label instead of a
@@ -660,7 +660,17 @@ var sourceFor = (id, label, build) => ({
660
660
  }
661
661
  });
662
662
  function MatrxCopyMenu(props) {
663
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(MatrxCopyMenuSession, { ...props }, props.sourceId ?? props.label);
663
+ const inherited = useContentTransferCapabilities();
664
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
665
+ MatrxCopyMenuSession,
666
+ {
667
+ ...props
668
+ },
669
+ JSON.stringify([
670
+ inherited.identityKey ?? null,
671
+ props.sourceId ?? props.label
672
+ ])
673
+ );
664
674
  }
665
675
  function MatrxCopyMenuSession(props) {
666
676
  const instance = React7.useId();
@@ -688,7 +698,7 @@ function MatrxCopyMenuSession(props) {
688
698
  })) : void 0;
689
699
  const agent = aiVisible && props.agent !== void 0 ? make("agent", () => asPayload(resolve(props.agent))) : void 0;
690
700
  const structuredAgent = aiVisible && props.agent !== void 0 ? make("structured-agent", () => structuredPayload(resolve(props.agent))) : void 0;
691
- const table = exportVisible && props.export?.sheetRows ? make("rows", () => {
701
+ const table = (exportVisible || props.primarySource === "table") && props.export?.sheetRows ? make("rows", () => {
692
702
  const value = (0, import_content_transfer.normalizeTransferJson)(props.export.sheetRows());
693
703
  if (!Array.isArray(value) || value.some(
694
704
  (row) => row === null || typeof row !== "object" || Array.isArray(row)
@@ -835,7 +845,7 @@ function MatrxCopyMenuSession(props) {
835
845
  return props.sendToSheet(draft.payload.rows, props.label);
836
846
  }
837
847
  });
838
- const source = json ?? table ?? structuredAgent ?? human ?? builderVariants[0]?.source;
848
+ const source = (props.primarySource === "table" ? table : void 0) ?? json ?? table ?? structuredAgent ?? human ?? builderVariants[0]?.source;
839
849
  if (!source && !actions.length) return null;
840
850
  const target = source ?? make("actions", () => ({ kind: "text", text: props.label }));
841
851
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
@@ -855,7 +865,7 @@ function MatrxCopyMenuSession(props) {
855
865
  formatSources: {
856
866
  ...human ? { plain: human, markdown: humanMarkdown, html: humanMarkdown } : {},
857
867
  ...json ? { json, "compact-json": json } : {},
858
- ...table ? { csv: table, tsv: table } : {}
868
+ ...table && exportVisible ? { csv: table, tsv: table } : {}
859
869
  },
860
870
  ...props.disabled !== void 0 ? { disabled: props.disabled } : {},
861
871
  className: cn(
@@ -1240,6 +1250,10 @@ function materialIdentity(material) {
1240
1250
  }
1241
1251
  return `payload:${id}`;
1242
1252
  }
1253
+ function snapshotContent(snapshot) {
1254
+ const { sourceId, label, payload, coverage, unsavedChanges, omissions, sections, limits } = snapshot;
1255
+ return JSON.stringify({ sourceId, label, payload, coverage, unsavedChanges, omissions, sections, limits });
1256
+ }
1243
1257
  function useFence() {
1244
1258
  const mounted = React8.useRef(true);
1245
1259
  const nextId = React8.useRef(0);
@@ -1288,7 +1302,12 @@ async function buildArtifact(draft, format, label) {
1288
1302
  const extension = format === "plain" ? "txt" : format === "compact-json" ? "json" : format;
1289
1303
  return (0, import_content_transfer2.serialize)(draft, format, { spreadsheetSafe: format === "csv" || format === "tsv", filename: `${filenameFor(label)}.${extension}` });
1290
1304
  }
1291
- function ContentTransferMenu({ source, surface, label, projection, sections, preferences, table, formatSources, variants = EMPTY_VARIANTS, actions = [], capabilities: local, disabled, enabled = true, className, stopPropagation = true, hide = [], controllerRef }) {
1305
+ function ContentTransferMenu(props) {
1306
+ const inherited = React8.useContext(CapabilityContext);
1307
+ const identityKey = props.capabilities?.identityKey ?? inherited.identityKey;
1308
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ContentTransferMenuSession, { ...props }, identityKey ?? "anonymous");
1309
+ }
1310
+ function ContentTransferMenuSession({ source, surface, label, projection, sections, preferences, table, formatSources, variants = EMPTY_VARIANTS, actions = [], capabilities: local, disabled, enabled = true, className, stopPropagation = true, hide = [], controllerRef }) {
1292
1311
  const triggerRef = React8.useRef(null);
1293
1312
  const inherited = React8.useContext(CapabilityContext);
1294
1313
  const nearestSurface = React8.useContext(SurfaceContext);
@@ -1337,7 +1356,7 @@ function ContentTransferMenu({ source, surface, label, projection, sections, pre
1337
1356
  const captured = await material.capture(signal);
1338
1357
  sourceMaterial = { id: captured.snapshot.sourceId, label: captured.snapshot.label, capture: async () => captured };
1339
1358
  } else if ("kind" in material) {
1340
- sourceMaterial = applyMenuProjection ? { id: "direct", label: sourceLabel, capture: async () => (0, import_content_transfer2.directSource)(material, { label: sourceLabel, ...projection ? { projection } : {}, ...sections ? { sections } : {} }) } : material;
1359
+ sourceMaterial = { id: materialIdentity(material), label: sourceLabel, capture: async () => (0, import_content_transfer2.directSource)(material, { sourceId: materialIdentity(material), label: sourceLabel, ...applyMenuProjection && projection ? { projection } : {}, ...applyMenuProjection && sections ? { sections } : {} }) };
1341
1360
  } else if (applyMenuProjection && (projection || sections)) {
1342
1361
  sourceMaterial = {
1343
1362
  id: material.id,
@@ -1354,7 +1373,7 @@ function ContentTransferMenu({ source, surface, label, projection, sections, pre
1354
1373
  } else {
1355
1374
  sourceMaterial = material;
1356
1375
  }
1357
- const raw = "kind" in sourceMaterial ? (0, import_content_transfer2.directSource)(sourceMaterial) : await sourceMaterial.capture({ scope, signal });
1376
+ const raw = await sourceMaterial.capture({ scope, signal });
1358
1377
  const constrained = (0, import_content_transfer2.resolveTransferPreferences)(providerPreferences, preferences, raw.snapshot.limits);
1359
1378
  const result = await (0, import_content_transfer2.capture)({ id: raw.snapshot.sourceId, label: raw.snapshot.label, capture: async () => ({ ...raw, snapshot: { ...raw.snapshot, limits: constrained.limits } }) }, signal, scope);
1360
1379
  if (scope !== "all-matching" || !collectPages) return result;
@@ -1414,31 +1433,117 @@ function ContentTransferMenu({ source, surface, label, projection, sections, pre
1414
1433
  fence.finish(controller);
1415
1434
  }
1416
1435
  }, [captureOne, capturePrimary, fence, formatSources, initialScope, preferences, providerPreferences, report, transport]);
1417
- const openWorkspace = React8.useCallback(async (key = primaryWorkspaceKey, variant) => {
1418
- const variantMaterial = variant && "kind" in variant.source ? { id: `variant:${key}`, label: variant.label, capture: async () => (0, import_content_transfer2.directSource)(variant.source, { label: variant.label }) } : variant?.source;
1419
- const captureWorkspace = variantMaterial ? (scope, signal) => captureOne(variantMaterial, scope, signal, true) : capturePrimary;
1420
- workspaceCaptures.current.set(key, captureWorkspace);
1421
- if (workspaces[key]) {
1422
- setWorkspaceKey(key);
1423
- setWorkspaceOpen(true);
1424
- return;
1425
- }
1426
- const { controller, id } = fence.begin();
1427
- try {
1428
- const result = await captureWorkspace(initialScope, controller.signal);
1429
- if (!fence.valid(id, controller)) return;
1430
- const resolved = (0, import_content_transfer2.resolveTransferPreferences)(providerPreferences, preferences, result.snapshot.limits);
1431
- const original = (0, import_content_transfer2.createDraft)(result.snapshot);
1432
- const base = { ...result, key, original, draft: original, scope: initialScope, preferences: resolved, ...result.collectionError ? { error: result.collectionError } : {} };
1433
- setWorkspaces((current) => ({ ...current, [key]: { ...base, draft: rebuild(base, initialControls(base)) } }));
1434
- setWorkspaceKey(key);
1435
- setWorkspaceOpen(true);
1436
- } catch (error) {
1437
- if (fence.valid(id, controller)) setNotice(errorMessage(error, "Could not capture content"));
1438
- } finally {
1439
- fence.finish(controller);
1440
- }
1441
- }, [captureOne, capturePrimary, fence, initialScope, preferences, primaryWorkspaceKey, providerPreferences, workspaces]);
1436
+ const openWorkspace = React8.useCallback(
1437
+ async (key = primaryWorkspaceKey, variant) => {
1438
+ const variantMaterial = variant && "kind" in variant.source ? {
1439
+ id: `variant:${key}`,
1440
+ label: variant.label,
1441
+ capture: async () => (0, import_content_transfer2.directSource)(variant.source, {
1442
+ sourceId: `variant:${key}`,
1443
+ label: variant.label
1444
+ })
1445
+ } : variant?.source;
1446
+ const captureWorkspace = (scope, signal, collectPages = true) => captureOne(variantMaterial ?? primary, scope, signal, true, collectPages);
1447
+ workspaceCaptures.current.set(key, captureWorkspace);
1448
+ const existing = workspaces[key];
1449
+ const { controller, id } = fence.begin();
1450
+ if (existing) {
1451
+ try {
1452
+ const allMatching = existing.scope === "all-matching";
1453
+ const fresh = await captureWorkspace(
1454
+ existing.scope,
1455
+ controller.signal,
1456
+ !allMatching
1457
+ );
1458
+ if (!fence.valid(id, controller) || !workspaceCaptures.current.has(key))
1459
+ return;
1460
+ if (allMatching && (!existing.allMatching || !fresh.allMatching))
1461
+ throw new Error(
1462
+ "This source can no longer provide an all-matching snapshot."
1463
+ );
1464
+ const collectionIdentity = (value) => value && {
1465
+ query: value.source.query,
1466
+ projection: value.projection,
1467
+ columns: value.columns,
1468
+ limits: value.limits
1469
+ };
1470
+ const changed = allMatching ? JSON.stringify(collectionIdentity(existing.allMatching)) !== JSON.stringify(collectionIdentity(fresh.allMatching)) : snapshotContent(existing.snapshot) !== snapshotContent(fresh.snapshot);
1471
+ setWorkspaces((current) => {
1472
+ const retained = current[key];
1473
+ if (!retained) return current;
1474
+ return {
1475
+ ...current,
1476
+ [key]: {
1477
+ ...retained,
1478
+ error: fresh.collectionError,
1479
+ ...allMatching ? { sourceStale: changed } : changed ? { pending: { ...fresh, scope: retained.scope } } : { pending: void 0 }
1480
+ }
1481
+ };
1482
+ });
1483
+ setWorkspaceKey(key);
1484
+ setWorkspaceOpen(true);
1485
+ } catch (error) {
1486
+ if (fence.valid(id, controller) && workspaceCaptures.current.has(key)) {
1487
+ setWorkspaces(
1488
+ (current) => current[key] ? {
1489
+ ...current,
1490
+ [key]: {
1491
+ ...current[key],
1492
+ error: errorMessage(error, "Could not refresh source")
1493
+ }
1494
+ } : current
1495
+ );
1496
+ setWorkspaceKey(key);
1497
+ setWorkspaceOpen(true);
1498
+ }
1499
+ } finally {
1500
+ fence.finish(controller);
1501
+ }
1502
+ return;
1503
+ }
1504
+ try {
1505
+ const result = await captureWorkspace(initialScope, controller.signal);
1506
+ if (!fence.valid(id, controller)) return;
1507
+ const resolved = (0, import_content_transfer2.resolveTransferPreferences)(
1508
+ providerPreferences,
1509
+ preferences,
1510
+ result.snapshot.limits
1511
+ );
1512
+ const original = (0, import_content_transfer2.createDraft)(result.snapshot);
1513
+ const base = {
1514
+ ...result,
1515
+ key,
1516
+ original,
1517
+ draft: original,
1518
+ scope: initialScope,
1519
+ preferences: resolved,
1520
+ ...result.collectionError ? { error: result.collectionError } : {}
1521
+ };
1522
+ setWorkspaces((current) => ({
1523
+ ...current,
1524
+ [key]: { ...base, draft: rebuild(base, initialControls(base)) }
1525
+ }));
1526
+ setWorkspaceKey(key);
1527
+ setWorkspaceOpen(true);
1528
+ } catch (error) {
1529
+ if (fence.valid(id, controller))
1530
+ setNotice(errorMessage(error, "Could not capture content"));
1531
+ } finally {
1532
+ fence.finish(controller);
1533
+ }
1534
+ },
1535
+ [
1536
+ captureOne,
1537
+ capturePrimary,
1538
+ fence,
1539
+ initialScope,
1540
+ preferences,
1541
+ primaryWorkspaceKey,
1542
+ primary,
1543
+ providerPreferences,
1544
+ workspaces
1545
+ ]
1546
+ );
1442
1547
  React8.useImperativeHandle(controllerRef, () => ({
1443
1548
  focusTrigger: () => triggerRef.current?.focus(),
1444
1549
  prepare: async (requestedSource, options) => {
@@ -1581,6 +1686,9 @@ function PreparationWorkspace({ restoreFocus, open, state, scopes, onClose, onCh
1581
1686
  const [instruction, setInstruction] = React8.useState("");
1582
1687
  const [busy, setBusy] = React8.useState(null);
1583
1688
  const [aiFailed, setAiFailed] = React8.useState(false);
1689
+ React8.useEffect(() => {
1690
+ setAiFailed(false);
1691
+ }, [state.snapshot.id, state.draft.revision]);
1584
1692
  const activeAi = React8.useRef(null);
1585
1693
  const activeCapture = React8.useRef(null);
1586
1694
  React8.useEffect(() => {
@@ -1658,7 +1766,7 @@ function PreparationWorkspace({ restoreFocus, open, state, scopes, onClose, onCh
1658
1766
  setBusy("capture");
1659
1767
  try {
1660
1768
  const result = await captureScope(scope, controller.signal);
1661
- if (fence.valid(id, controller) && latest.current.draft.revision === revision) onChange({ ...latest.current, pending: { ...result, scope }, error: result.collectionError });
1769
+ if (fence.valid(id, controller) && latest.current.draft.revision === revision) onChange({ ...latest.current, pending: { ...result, scope }, sourceStale: false, error: result.collectionError });
1662
1770
  } catch (error) {
1663
1771
  if (fence.valid(id, controller)) onChange({ ...latest.current, error: errorMessage(error, "Refresh failed") });
1664
1772
  } finally {
@@ -1891,7 +1999,7 @@ function PreparationWorkspace({ restoreFocus, open, state, scopes, onClose, onCh
1891
1999
  activeCapture.current = null;
1892
2000
  setBusy(null);
1893
2001
  }, children: "Stop loading rows" }) : null,
1894
- ai && aiSession.current?.recover && busy !== "ai" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Button, { type: "button", variant: "outline", onClick: () => void runAi(true), children: aiFailed ? "Retry or rejoin AI run" : "Rejoin AI run" }) : null,
2002
+ ai && aiFailed && aiSession.current?.recover && busy !== "ai" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Button, { type: "button", variant: "outline", onClick: () => void runAi(true), children: aiFailed ? "Retry or rejoin AI run" : "Rejoin AI run" }) : null,
1895
2003
  ai ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "matrx-alchemy-meta", children: "AI creates a preview. It never copies automatically. Closing detaches this view without cancelling server work." }) : null,
1896
2004
  state.error ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "alert", className: "matrx-alchemy-error", children: state.error }) : null
1897
2005
  ] }) : null
@@ -1901,6 +2009,7 @@ function PreparationWorkspace({ restoreFocus, open, state, scopes, onClose, onCh
1901
2009
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Button, { type: "button", variant: "outline", onClick: () => setPendingControls(null), children: "Keep edited draft" }),
1902
2010
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Button, { type: "button", onClick: applyPendingControls, children: "Rebuild from source" })
1903
2011
  ] }) : null,
2012
+ state.sourceStale ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "alert", className: "matrx-alchemy-decision", children: "The source changed since this draft. Use Refresh source to review the latest data." }) : null,
1904
2013
  state.pending ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "matrx-alchemy-decision", role: "alert", children: [
1905
2014
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: "A fresh source snapshot is ready." }),
1906
2015
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Button, { type: "button", variant: "outline", onClick: () => resolveCapture("keep"), children: "Keep current snapshot and draft" }),