@ai-matrx/content-ir-react 0.15.2 → 0.16.1

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/dist/index.js CHANGED
@@ -739,7 +739,7 @@ function useContentIrKindVersion(kind, sources) {
739
739
  }
740
740
 
741
741
  // react/KindInstanceRender.tsx
742
- import { useEffect as useEffect2, useState as useState3 } from "react";
742
+ import { useEffect as useEffect3, useState as useState4 } from "react";
743
743
  import {
744
744
  envelopeFromCompleteValue as envelopeFromCompleteValue3,
745
745
  IR_ENVELOPE_KEY as IR_ENVELOPE_KEY2,
@@ -775,6 +775,9 @@ function recordFallbackName(nounLabel3, id) {
775
775
  const short = isUuidShape(id) ? id.slice(0, 8) : id.length > 12 ? `${id.slice(0, 12)}\u2026` : id;
776
776
  return `${nounLabel3} ${short}`;
777
777
  }
778
+ function describeChangeValue(value) {
779
+ return describeValue(value);
780
+ }
778
781
  function describeValue(value) {
779
782
  if (value === null || value === void 0) return { value: "cleared", full: "cleared", clears: true };
780
783
  if (typeof value === "string") {
@@ -803,11 +806,28 @@ function safeJson(value) {
803
806
  return String(value);
804
807
  }
805
808
  }
806
- function itemChanges(item) {
809
+ function sameValue(a, b) {
810
+ if (a === b) return true;
811
+ if (a === null || a === void 0 || b === null || b === void 0) {
812
+ const emptyA = a === null || a === void 0 || a === "";
813
+ const emptyB = b === null || b === void 0 || b === "";
814
+ return emptyA && emptyB;
815
+ }
816
+ if (typeof a === "object" || typeof b === "object") return safeJson(a) === safeJson(b);
817
+ return false;
818
+ }
819
+ function itemChanges(item, current, options = {}) {
807
820
  const out = [];
821
+ const titleColumn = options.titleColumn ?? null;
808
822
  for (const [key, raw] of Object.entries(item)) {
809
823
  if (NOT_A_CHANGE.has(key)) continue;
810
- out.push({ key, label: fieldLabel(key), ...describeValue(raw) });
824
+ const label = key === titleColumn ? "Title" : fieldLabel(key);
825
+ const change = { key, label, ...describeValue(raw) };
826
+ if (current && Object.prototype.hasOwnProperty.call(current, key)) {
827
+ change.before = describeValue(current[key]);
828
+ change.same = sameValue(current[key], raw);
829
+ }
830
+ out.push(change);
811
831
  }
812
832
  return out;
813
833
  }
@@ -920,12 +940,23 @@ function ApplyDirectiveButton({
920
940
  const conversationId = scope.conversationId || void 0;
921
941
  const items = directive.items;
922
942
  const readsLedger = !!host.applyState && !!host.confirm && directive.parsed.executes && items.length > 0;
923
- const [state, setState] = useState(
924
- readsLedger ? { status: "checking" } : { status: "idle" }
925
- );
943
+ const initial = readsLedger ? { status: "checking" } : { status: "idle" };
944
+ const [state, setState] = useState(initial);
926
945
  const [elapsed, setElapsed] = useState(0);
927
946
  const [askingAgain, setAskingAgain] = useState(false);
928
947
  const startedAt = useRef(null);
948
+ const identity = JSON.stringify([directive.slug, items, conversationId ?? null]);
949
+ const [stateFor, setStateFor] = useState(identity);
950
+ if (stateFor !== identity) {
951
+ setStateFor(identity);
952
+ setState(initial);
953
+ setAskingAgain(false);
954
+ setElapsed(0);
955
+ }
956
+ const currentIdentity = useRef(identity);
957
+ useEffect(() => {
958
+ currentIdentity.current = identity;
959
+ }, [identity]);
929
960
  const nounText = nounLabel(directive.noun, host.nouns);
930
961
  const shellKey = readsLedger ? JSON.stringify([directive.slug, items, conversationId ?? null]) : "";
931
962
  const read = host.applyState;
@@ -948,7 +979,8 @@ function ApplyDirectiveButton({
948
979
  applied: shellItems.length,
949
980
  failed: 0,
950
981
  message: answer.message?.trim() ? answer.message.trim() : null,
951
- records: answer.records ?? []
982
+ records: answer.records ?? [],
983
+ copies: Math.max(1, answer.copies ?? 1)
952
984
  } : prev
953
985
  );
954
986
  } else if (answer.state === "in_flight" && polls < FINISH_POLLS_MAX) {
@@ -988,6 +1020,9 @@ function ApplyDirectiveButton({
988
1020
  async function handleApply(again = false) {
989
1021
  if (!confirm) return;
990
1022
  const before = state;
1023
+ const askedFor = identity;
1024
+ const stillHere = () => currentIdentity.current === askedFor;
1025
+ const priorCopies = state.status === "applied" ? state.copies : 0;
991
1026
  if (host.ask) {
992
1027
  if (again) setAskingAgain(true);
993
1028
  else setState({ status: "asking" });
@@ -1000,6 +1035,7 @@ function ApplyDirectiveButton({
1000
1035
  ...again ? { again: true } : {}
1001
1036
  });
1002
1037
  } catch (error) {
1038
+ if (!stillHere()) return;
1003
1039
  setAskingAgain(false);
1004
1040
  setState({
1005
1041
  status: "error",
@@ -1007,6 +1043,7 @@ function ApplyDirectiveButton({
1007
1043
  });
1008
1044
  return;
1009
1045
  }
1046
+ if (!stillHere()) return;
1010
1047
  setAskingAgain(false);
1011
1048
  if (!ok) {
1012
1049
  setState(again ? before : { status: "idle" });
@@ -1020,15 +1057,19 @@ function ApplyDirectiveButton({
1020
1057
  if (conversationId) shell.conversationId = conversationId;
1021
1058
  if (again) shell.force = true;
1022
1059
  const result = await confirm(shell);
1060
+ if (!stillHere()) return;
1023
1061
  setState({
1024
1062
  status: "applied",
1025
1063
  applied: result.applied,
1026
1064
  failed: result.failed,
1027
1065
  message: result.message?.trim() ? result.message.trim() : null,
1028
- records: result.records ?? []
1066
+ records: result.records ?? [],
1067
+ // "Run again" made one more; a first apply (or a replay of one) is one.
1068
+ copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies)
1029
1069
  });
1030
1070
  onApplied?.();
1031
1071
  } catch (error) {
1072
+ if (!stillHere()) return;
1032
1073
  setState({
1033
1074
  status: "error",
1034
1075
  message: error instanceof Error && error.message ? error.message : "That couldn't be applied just now. Please try again."
@@ -1053,6 +1094,15 @@ function ApplyDirectiveButton({
1053
1094
  bad && !state.message ? ` \xB7 ${state.failed} failed` : ""
1054
1095
  ] })
1055
1096
  ] }),
1097
+ state.copies > 1 ? /* @__PURE__ */ jsx4(
1098
+ "span",
1099
+ {
1100
+ className: "shrink-0 text-muted-foreground",
1101
+ "data-apply-copies": state.copies,
1102
+ title: records.length > 0 ? directive.directiveClass === "create" ? "Links to the latest copy." : "Links to the latest run." : void 0,
1103
+ children: directive.directiveClass === "create" ? `${state.copies} copies` : `Ran ${state.copies} times`
1104
+ }
1105
+ ) : null,
1056
1106
  records.map((record) => /* @__PURE__ */ jsx4("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
1057
1107
  ...record,
1058
1108
  fallback: recordFallbackName(label2, record.id),
@@ -1103,6 +1153,7 @@ function ApplyDirectiveButton({
1103
1153
  }
1104
1154
  );
1105
1155
  }
1156
+ const destructive = directive.directiveClass === "delete";
1106
1157
  return /* @__PURE__ */ jsxs2("span", { className: "inline-flex items-center gap-2", children: [
1107
1158
  /* @__PURE__ */ jsxs2(
1108
1159
  "button",
@@ -1110,7 +1161,8 @@ function ApplyDirectiveButton({
1110
1161
  type: "button",
1111
1162
  onClick: () => void handleApply(),
1112
1163
  disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
1113
- className: "inline-flex items-center gap-1 rounded-md border border-border bg-background px-2 py-1 text-xs font-medium hover:bg-accent disabled:opacity-50",
1164
+ "data-apply-tone": destructive ? "destructive" : "default",
1165
+ className: `inline-flex items-center gap-1 rounded-md border px-2 py-1 text-xs font-medium disabled:opacity-50 ${destructive ? "border-destructive/50 bg-background text-destructive hover:bg-destructive/10" : "border-border bg-background hover:bg-accent"}`,
1114
1166
  children: [
1115
1167
  state.status === "applying" ? /* @__PURE__ */ jsx4(LoaderIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx4(PlayIcon, { className: "h-3.5 w-3.5" }),
1116
1168
  state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
@@ -1213,7 +1265,7 @@ function resetDirectiveRenderers() {
1213
1265
  }
1214
1266
 
1215
1267
  // directives/SideEffectDirectiveCard.tsx
1216
- import { useState as useState2 } from "react";
1268
+ import { useState as useState3 } from "react";
1217
1269
  import {
1218
1270
  asKindInstance,
1219
1271
  directiveDisplay as directiveDisplay3,
@@ -1223,6 +1275,39 @@ import {
1223
1275
  nounLabel as nounLabel2,
1224
1276
  nounTitleColumn
1225
1277
  } from "@ai-matrx/content-ir";
1278
+
1279
+ // directives/record-values.ts
1280
+ import { useEffect as useEffect2, useState as useState2 } from "react";
1281
+ function useRecordValues(read, ref) {
1282
+ const key = ref && read ? `${ref.noun}:${ref.id}` : "";
1283
+ const [result, setResult] = useState2({
1284
+ key: "",
1285
+ values: null,
1286
+ status: "idle"
1287
+ });
1288
+ useEffect2(() => {
1289
+ if (!key || !read) return;
1290
+ const split = key.indexOf(":");
1291
+ const target = { noun: key.slice(0, split), id: key.slice(split + 1) };
1292
+ let live = true;
1293
+ setResult({ key, values: null, status: "loading" });
1294
+ read(target).then((values) => {
1295
+ if (!live) return;
1296
+ setResult({ key, values, status: values ? "ready" : "unavailable" });
1297
+ }).catch(() => {
1298
+ if (!live) return;
1299
+ setResult({ key, values: null, status: "unavailable" });
1300
+ });
1301
+ return () => {
1302
+ live = false;
1303
+ };
1304
+ }, [key, read]);
1305
+ if (!key) return { values: null, status: "idle" };
1306
+ if (result.key !== key) return { values: null, status: "loading" };
1307
+ return { values: result.values, status: result.status };
1308
+ }
1309
+
1310
+ // directives/SideEffectDirectiveCard.tsx
1226
1311
  import { Fragment, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1227
1312
  var ROWS_BEFORE_FOLD = 3;
1228
1313
  function ItemRow({
@@ -1276,44 +1361,73 @@ function ItemRow({
1276
1361
  host.renderCopy?.({ label: title, value: item, kind, size: "xs" })
1277
1362
  ] });
1278
1363
  }
1279
- function TargetRow({
1280
- directive,
1281
- item,
1282
- recordId
1364
+ function DirectiveChangeList({
1365
+ changes,
1366
+ className
1283
1367
  }) {
1284
- const host = useDirectiveHost();
1285
- const fallback = recordFallbackName(nounLabel2(directive.noun, host.nouns), recordId);
1286
- const changes = directive.directiveClass === "update" ? itemChanges(item) : [];
1287
- const name = host.renderRecord ? host.renderRecord({ noun: directive.noun, id: recordId, fallback, context: "row" }) : /* @__PURE__ */ jsx7("span", { className: "truncate font-medium text-foreground", children: fallback });
1288
- return /* @__PURE__ */ jsxs5("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
1289
- /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 items-center gap-2", children: [
1290
- /* @__PURE__ */ jsx7("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
1291
- host.renderCopy?.({ label: fallback, value: item, size: "xs" })
1292
- ] }),
1293
- directive.directiveClass === "update" ? changes.length === 0 ? /* @__PURE__ */ jsx7("p", { className: "px-1 text-[11px] text-muted-foreground", children: "Sets no fields \u2014 nothing would change." }) : /* @__PURE__ */ jsx7("ul", { className: "mt-0.5 space-y-0.5 px-1", children: changes.map((change) => /* @__PURE__ */ jsxs5(
1368
+ if (changes.length === 0) {
1369
+ return /* @__PURE__ */ jsx7("p", { className: `text-[11px] text-muted-foreground${className ? ` ${className}` : ""}`, children: "Sets no fields \u2014 nothing would change." });
1370
+ }
1371
+ return /* @__PURE__ */ jsx7("ul", { className: `space-y-0.5${className ? ` ${className}` : ""}`, "data-directive-changes": "", children: changes.map((change) => {
1372
+ const before = change.before && !change.same ? change.before : null;
1373
+ const was = before ? before.clears ? "empty" : before.value : null;
1374
+ return /* @__PURE__ */ jsxs5(
1294
1375
  "li",
1295
1376
  {
1296
1377
  className: "flex min-w-0 items-baseline gap-1.5 text-[11px]",
1297
- title: `${change.label} \u2192 ${change.full}`,
1378
+ title: before ? `${change.label}: ${before.clears ? "empty" : before.full} \u2192 ${change.full}` : `${change.label} \u2192 ${change.full}`,
1379
+ "data-change-key": change.key,
1298
1380
  children: [
1299
1381
  /* @__PURE__ */ jsx7("span", { className: "shrink-0 text-muted-foreground", children: change.label }),
1382
+ was !== null ? /* @__PURE__ */ jsx7(
1383
+ "span",
1384
+ {
1385
+ className: "min-w-0 max-w-[40%] shrink truncate text-muted-foreground line-through",
1386
+ "data-change-before": "",
1387
+ children: was
1388
+ }
1389
+ ) : null,
1300
1390
  /* @__PURE__ */ jsx7("span", { "aria-hidden": true, className: "shrink-0 text-muted-foreground", children: "\u2192" }),
1301
1391
  /* @__PURE__ */ jsx7(
1302
1392
  "span",
1303
1393
  {
1304
1394
  className: `min-w-0 truncate ${change.clears ? "italic text-muted-foreground" : "text-foreground"}`,
1395
+ "data-change-after": "",
1305
1396
  children: change.value
1306
1397
  }
1307
- )
1398
+ ),
1399
+ change.same ? /* @__PURE__ */ jsx7("span", { className: "shrink-0 text-muted-foreground", children: "(no change)" }) : null
1308
1400
  ]
1309
1401
  },
1310
1402
  change.key
1311
- )) }) : null
1403
+ );
1404
+ }) });
1405
+ }
1406
+ function TargetRow({
1407
+ directive,
1408
+ item,
1409
+ recordId
1410
+ }) {
1411
+ const host = useDirectiveHost();
1412
+ const fallback = recordFallbackName(nounLabel2(directive.noun, host.nouns), recordId);
1413
+ const isUpdate = directive.directiveClass === "update";
1414
+ const current = useRecordValues(
1415
+ isUpdate ? host.readRecord : void 0,
1416
+ isUpdate ? { noun: directive.noun, id: recordId } : null
1417
+ );
1418
+ const changes = isUpdate ? itemChanges(item, current.values, { titleColumn: nounTitleColumn(directive.noun, host.nouns) }) : [];
1419
+ const name = host.renderRecord ? host.renderRecord({ noun: directive.noun, id: recordId, fallback, context: "row" }) : /* @__PURE__ */ jsx7("span", { className: "truncate font-medium text-foreground", children: fallback });
1420
+ return /* @__PURE__ */ jsxs5("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
1421
+ /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 items-center gap-2", children: [
1422
+ /* @__PURE__ */ jsx7("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
1423
+ host.renderCopy?.({ label: fallback, value: item, size: "xs" })
1424
+ ] }),
1425
+ isUpdate ? /* @__PURE__ */ jsx7(DirectiveChangeList, { changes, className: "mt-0.5 px-1" }) : null
1312
1426
  ] });
1313
1427
  }
1314
1428
  function SideEffectDirectiveCard({ directive }) {
1315
1429
  const host = useDirectiveHost();
1316
- const [expanded, setExpanded] = useState2(false);
1430
+ const [expanded, setExpanded] = useState3(false);
1317
1431
  const items = directive.items;
1318
1432
  const total = items.length;
1319
1433
  const display = directiveDisplay3(directive.directiveClass, directive.noun, host.nouns);
@@ -1427,10 +1541,10 @@ function KindInstanceRenderInner({
1427
1541
  className
1428
1542
  }) {
1429
1543
  const host = useContentIrHost();
1430
- const [routingStatus, setRoutingStatus] = useState3(
1544
+ const [routingStatus, setRoutingStatus] = useState4(
1431
1545
  () => kindIsRoutable(kind, host) ? "routable" : "checking"
1432
1546
  );
1433
- useEffect2(() => {
1547
+ useEffect3(() => {
1434
1548
  let cancelled = false;
1435
1549
  let warmed = false;
1436
1550
  const syncRoutingStatus = () => {
@@ -1828,6 +1942,7 @@ export {
1828
1942
  DIRECTIVE_BLOCK_TYPE,
1829
1943
  DbKindComponentErrorBoundary,
1830
1944
  DelegatedOutput,
1945
+ DirectiveChangeList,
1831
1946
  DirectiveFallbackCard,
1832
1947
  DirectiveHostProvider,
1833
1948
  DirectiveProvisionalCard,
@@ -1848,6 +1963,7 @@ export {
1848
1963
  consoleErrorReporter,
1849
1964
  createDbKindComponentCache,
1850
1965
  createKindRouteBinding,
1966
+ describeChangeValue,
1851
1967
  envelopeFromPartialKind,
1852
1968
  fieldLabel,
1853
1969
  getDirectiveRenderer,
@@ -1876,6 +1992,7 @@ export {
1876
1992
  useContentIrKindVersion,
1877
1993
  useDirectiveHost,
1878
1994
  useDirectiveScope,
1879
- useKindRouteEnv
1995
+ useKindRouteEnv,
1996
+ useRecordValues
1880
1997
  };
1881
1998
  //# sourceMappingURL=index.js.map