@ai-matrx/content-ir-react 0.15.1 → 0.16.0

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,26 @@ 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) {
807
820
  const out = [];
808
821
  for (const [key, raw] of Object.entries(item)) {
809
822
  if (NOT_A_CHANGE.has(key)) continue;
810
- out.push({ key, label: fieldLabel(key), ...describeValue(raw) });
823
+ const change = { key, label: fieldLabel(key), ...describeValue(raw) };
824
+ if (current && Object.prototype.hasOwnProperty.call(current, key)) {
825
+ change.before = describeValue(current[key]);
826
+ change.same = sameValue(current[key], raw);
827
+ }
828
+ out.push(change);
811
829
  }
812
830
  return out;
813
831
  }
@@ -920,11 +938,23 @@ function ApplyDirectiveButton({
920
938
  const conversationId = scope.conversationId || void 0;
921
939
  const items = directive.items;
922
940
  const readsLedger = !!host.applyState && !!host.confirm && directive.parsed.executes && items.length > 0;
923
- const [state, setState] = useState(
924
- readsLedger ? { status: "checking" } : { status: "idle" }
925
- );
941
+ const initial = readsLedger ? { status: "checking" } : { status: "idle" };
942
+ const [state, setState] = useState(initial);
926
943
  const [elapsed, setElapsed] = useState(0);
944
+ const [askingAgain, setAskingAgain] = useState(false);
927
945
  const startedAt = useRef(null);
946
+ const identity = JSON.stringify([directive.slug, items, conversationId ?? null]);
947
+ const [stateFor, setStateFor] = useState(identity);
948
+ if (stateFor !== identity) {
949
+ setStateFor(identity);
950
+ setState(initial);
951
+ setAskingAgain(false);
952
+ setElapsed(0);
953
+ }
954
+ const currentIdentity = useRef(identity);
955
+ useEffect(() => {
956
+ currentIdentity.current = identity;
957
+ }, [identity]);
928
958
  const nounText = nounLabel(directive.noun, host.nouns);
929
959
  const shellKey = readsLedger ? JSON.stringify([directive.slug, items, conversationId ?? null]) : "";
930
960
  const read = host.applyState;
@@ -947,7 +977,8 @@ function ApplyDirectiveButton({
947
977
  applied: shellItems.length,
948
978
  failed: 0,
949
979
  message: answer.message?.trim() ? answer.message.trim() : null,
950
- records: answer.records ?? []
980
+ records: answer.records ?? [],
981
+ copies: Math.max(1, answer.copies ?? 1)
951
982
  } : prev
952
983
  );
953
984
  } else if (answer.state === "in_flight" && polls < FINISH_POLLS_MAX) {
@@ -987,8 +1018,12 @@ function ApplyDirectiveButton({
987
1018
  async function handleApply(again = false) {
988
1019
  if (!confirm) return;
989
1020
  const before = state;
1021
+ const askedFor = identity;
1022
+ const stillHere = () => currentIdentity.current === askedFor;
1023
+ const priorCopies = state.status === "applied" ? state.copies : 0;
990
1024
  if (host.ask) {
991
- setState({ status: "asking" });
1025
+ if (again) setAskingAgain(true);
1026
+ else setState({ status: "asking" });
992
1027
  let ok = false;
993
1028
  try {
994
1029
  ok = await host.ask({
@@ -998,12 +1033,16 @@ function ApplyDirectiveButton({
998
1033
  ...again ? { again: true } : {}
999
1034
  });
1000
1035
  } catch (error) {
1036
+ if (!stillHere()) return;
1037
+ setAskingAgain(false);
1001
1038
  setState({
1002
1039
  status: "error",
1003
1040
  message: error instanceof Error && error.message ? error.message : "We could not ask you to confirm this, so nothing was done."
1004
1041
  });
1005
1042
  return;
1006
1043
  }
1044
+ if (!stillHere()) return;
1045
+ setAskingAgain(false);
1007
1046
  if (!ok) {
1008
1047
  setState(again ? before : { status: "idle" });
1009
1048
  return;
@@ -1016,15 +1055,19 @@ function ApplyDirectiveButton({
1016
1055
  if (conversationId) shell.conversationId = conversationId;
1017
1056
  if (again) shell.force = true;
1018
1057
  const result = await confirm(shell);
1058
+ if (!stillHere()) return;
1019
1059
  setState({
1020
1060
  status: "applied",
1021
1061
  applied: result.applied,
1022
1062
  failed: result.failed,
1023
1063
  message: result.message?.trim() ? result.message.trim() : null,
1024
- records: result.records ?? []
1064
+ records: result.records ?? [],
1065
+ // "Run again" made one more; a first apply (or a replay of one) is one.
1066
+ copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies)
1025
1067
  });
1026
1068
  onApplied?.();
1027
1069
  } catch (error) {
1070
+ if (!stillHere()) return;
1028
1071
  setState({
1029
1072
  status: "error",
1030
1073
  message: error instanceof Error && error.message ? error.message : "That couldn't be applied just now. Please try again."
@@ -1049,6 +1092,15 @@ function ApplyDirectiveButton({
1049
1092
  bad && !state.message ? ` \xB7 ${state.failed} failed` : ""
1050
1093
  ] })
1051
1094
  ] }),
1095
+ state.copies > 1 ? /* @__PURE__ */ jsx4(
1096
+ "span",
1097
+ {
1098
+ className: "shrink-0 text-muted-foreground",
1099
+ "data-apply-copies": state.copies,
1100
+ title: records.length > 0 ? directive.directiveClass === "create" ? "Links to the latest copy." : "Links to the latest run." : void 0,
1101
+ children: directive.directiveClass === "create" ? `${state.copies} copies` : `Ran ${state.copies} times`
1102
+ }
1103
+ ) : null,
1052
1104
  records.map((record) => /* @__PURE__ */ jsx4("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
1053
1105
  ...record,
1054
1106
  fallback: recordFallbackName(label2, record.id),
@@ -1059,8 +1111,9 @@ function ApplyDirectiveButton({
1059
1111
  "button",
1060
1112
  {
1061
1113
  type: "button",
1114
+ disabled: askingAgain,
1062
1115
  onClick: () => void handleApply(!bad),
1063
- className: "inline-flex items-center gap-1 rounded px-1 py-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground",
1116
+ className: "inline-flex items-center gap-1 rounded px-1 py-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-50",
1064
1117
  "data-apply-action": bad ? "retry" : "run-again",
1065
1118
  children: [
1066
1119
  /* @__PURE__ */ jsx4(RotateCcwIcon, { className: "h-3 w-3" }),
@@ -1098,6 +1151,7 @@ function ApplyDirectiveButton({
1098
1151
  }
1099
1152
  );
1100
1153
  }
1154
+ const destructive = directive.directiveClass === "delete";
1101
1155
  return /* @__PURE__ */ jsxs2("span", { className: "inline-flex items-center gap-2", children: [
1102
1156
  /* @__PURE__ */ jsxs2(
1103
1157
  "button",
@@ -1105,7 +1159,8 @@ function ApplyDirectiveButton({
1105
1159
  type: "button",
1106
1160
  onClick: () => void handleApply(),
1107
1161
  disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
1108
- 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",
1162
+ "data-apply-tone": destructive ? "destructive" : "default",
1163
+ 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"}`,
1109
1164
  children: [
1110
1165
  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" }),
1111
1166
  state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
@@ -1208,7 +1263,7 @@ function resetDirectiveRenderers() {
1208
1263
  }
1209
1264
 
1210
1265
  // directives/SideEffectDirectiveCard.tsx
1211
- import { useState as useState2 } from "react";
1266
+ import { useState as useState3 } from "react";
1212
1267
  import {
1213
1268
  asKindInstance,
1214
1269
  directiveDisplay as directiveDisplay3,
@@ -1218,6 +1273,39 @@ import {
1218
1273
  nounLabel as nounLabel2,
1219
1274
  nounTitleColumn
1220
1275
  } from "@ai-matrx/content-ir";
1276
+
1277
+ // directives/record-values.ts
1278
+ import { useEffect as useEffect2, useState as useState2 } from "react";
1279
+ function useRecordValues(read, ref) {
1280
+ const key = ref && read ? `${ref.noun}:${ref.id}` : "";
1281
+ const [result, setResult] = useState2({
1282
+ key: "",
1283
+ values: null,
1284
+ status: "idle"
1285
+ });
1286
+ useEffect2(() => {
1287
+ if (!key || !read) return;
1288
+ const split = key.indexOf(":");
1289
+ const target = { noun: key.slice(0, split), id: key.slice(split + 1) };
1290
+ let live = true;
1291
+ setResult({ key, values: null, status: "loading" });
1292
+ read(target).then((values) => {
1293
+ if (!live) return;
1294
+ setResult({ key, values, status: values ? "ready" : "unavailable" });
1295
+ }).catch(() => {
1296
+ if (!live) return;
1297
+ setResult({ key, values: null, status: "unavailable" });
1298
+ });
1299
+ return () => {
1300
+ live = false;
1301
+ };
1302
+ }, [key, read]);
1303
+ if (!key) return { values: null, status: "idle" };
1304
+ if (result.key !== key) return { values: null, status: "loading" };
1305
+ return { values: result.values, status: result.status };
1306
+ }
1307
+
1308
+ // directives/SideEffectDirectiveCard.tsx
1221
1309
  import { Fragment, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1222
1310
  var ROWS_BEFORE_FOLD = 3;
1223
1311
  function ItemRow({
@@ -1271,44 +1359,73 @@ function ItemRow({
1271
1359
  host.renderCopy?.({ label: title, value: item, kind, size: "xs" })
1272
1360
  ] });
1273
1361
  }
1274
- function TargetRow({
1275
- directive,
1276
- item,
1277
- recordId
1362
+ function DirectiveChangeList({
1363
+ changes,
1364
+ className
1278
1365
  }) {
1279
- const host = useDirectiveHost();
1280
- const fallback = recordFallbackName(nounLabel2(directive.noun, host.nouns), recordId);
1281
- const changes = directive.directiveClass === "update" ? itemChanges(item) : [];
1282
- 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 });
1283
- return /* @__PURE__ */ jsxs5("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
1284
- /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 items-center gap-2", children: [
1285
- /* @__PURE__ */ jsx7("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
1286
- host.renderCopy?.({ label: fallback, value: item, size: "xs" })
1287
- ] }),
1288
- 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(
1366
+ if (changes.length === 0) {
1367
+ return /* @__PURE__ */ jsx7("p", { className: `text-[11px] text-muted-foreground${className ? ` ${className}` : ""}`, children: "Sets no fields \u2014 nothing would change." });
1368
+ }
1369
+ return /* @__PURE__ */ jsx7("ul", { className: `space-y-0.5${className ? ` ${className}` : ""}`, "data-directive-changes": "", children: changes.map((change) => {
1370
+ const before = change.before && !change.same ? change.before : null;
1371
+ const was = before ? before.clears ? "empty" : before.value : null;
1372
+ return /* @__PURE__ */ jsxs5(
1289
1373
  "li",
1290
1374
  {
1291
1375
  className: "flex min-w-0 items-baseline gap-1.5 text-[11px]",
1292
- title: `${change.label} \u2192 ${change.full}`,
1376
+ title: before ? `${change.label}: ${before.clears ? "empty" : before.full} \u2192 ${change.full}` : `${change.label} \u2192 ${change.full}`,
1377
+ "data-change-key": change.key,
1293
1378
  children: [
1294
1379
  /* @__PURE__ */ jsx7("span", { className: "shrink-0 text-muted-foreground", children: change.label }),
1380
+ was !== null ? /* @__PURE__ */ jsx7(
1381
+ "span",
1382
+ {
1383
+ className: "min-w-0 max-w-[40%] shrink truncate text-muted-foreground line-through",
1384
+ "data-change-before": "",
1385
+ children: was
1386
+ }
1387
+ ) : null,
1295
1388
  /* @__PURE__ */ jsx7("span", { "aria-hidden": true, className: "shrink-0 text-muted-foreground", children: "\u2192" }),
1296
1389
  /* @__PURE__ */ jsx7(
1297
1390
  "span",
1298
1391
  {
1299
1392
  className: `min-w-0 truncate ${change.clears ? "italic text-muted-foreground" : "text-foreground"}`,
1393
+ "data-change-after": "",
1300
1394
  children: change.value
1301
1395
  }
1302
- )
1396
+ ),
1397
+ change.same ? /* @__PURE__ */ jsx7("span", { className: "shrink-0 text-muted-foreground", children: "(no change)" }) : null
1303
1398
  ]
1304
1399
  },
1305
1400
  change.key
1306
- )) }) : null
1401
+ );
1402
+ }) });
1403
+ }
1404
+ function TargetRow({
1405
+ directive,
1406
+ item,
1407
+ recordId
1408
+ }) {
1409
+ const host = useDirectiveHost();
1410
+ const fallback = recordFallbackName(nounLabel2(directive.noun, host.nouns), recordId);
1411
+ const isUpdate = directive.directiveClass === "update";
1412
+ const current = useRecordValues(
1413
+ isUpdate ? host.readRecord : void 0,
1414
+ isUpdate ? { noun: directive.noun, id: recordId } : null
1415
+ );
1416
+ const changes = isUpdate ? itemChanges(item, current.values) : [];
1417
+ 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 });
1418
+ return /* @__PURE__ */ jsxs5("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
1419
+ /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 items-center gap-2", children: [
1420
+ /* @__PURE__ */ jsx7("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
1421
+ host.renderCopy?.({ label: fallback, value: item, size: "xs" })
1422
+ ] }),
1423
+ isUpdate ? /* @__PURE__ */ jsx7(DirectiveChangeList, { changes, className: "mt-0.5 px-1" }) : null
1307
1424
  ] });
1308
1425
  }
1309
1426
  function SideEffectDirectiveCard({ directive }) {
1310
1427
  const host = useDirectiveHost();
1311
- const [expanded, setExpanded] = useState2(false);
1428
+ const [expanded, setExpanded] = useState3(false);
1312
1429
  const items = directive.items;
1313
1430
  const total = items.length;
1314
1431
  const display = directiveDisplay3(directive.directiveClass, directive.noun, host.nouns);
@@ -1422,10 +1539,10 @@ function KindInstanceRenderInner({
1422
1539
  className
1423
1540
  }) {
1424
1541
  const host = useContentIrHost();
1425
- const [routingStatus, setRoutingStatus] = useState3(
1542
+ const [routingStatus, setRoutingStatus] = useState4(
1426
1543
  () => kindIsRoutable(kind, host) ? "routable" : "checking"
1427
1544
  );
1428
- useEffect2(() => {
1545
+ useEffect3(() => {
1429
1546
  let cancelled = false;
1430
1547
  let warmed = false;
1431
1548
  const syncRoutingStatus = () => {
@@ -1823,6 +1940,7 @@ export {
1823
1940
  DIRECTIVE_BLOCK_TYPE,
1824
1941
  DbKindComponentErrorBoundary,
1825
1942
  DelegatedOutput,
1943
+ DirectiveChangeList,
1826
1944
  DirectiveFallbackCard,
1827
1945
  DirectiveHostProvider,
1828
1946
  DirectiveProvisionalCard,
@@ -1843,6 +1961,7 @@ export {
1843
1961
  consoleErrorReporter,
1844
1962
  createDbKindComponentCache,
1845
1963
  createKindRouteBinding,
1964
+ describeChangeValue,
1846
1965
  envelopeFromPartialKind,
1847
1966
  fieldLabel,
1848
1967
  getDirectiveRenderer,
@@ -1871,6 +1990,7 @@ export {
1871
1990
  useContentIrKindVersion,
1872
1991
  useDirectiveHost,
1873
1992
  useDirectiveScope,
1874
- useKindRouteEnv
1993
+ useKindRouteEnv,
1994
+ useRecordValues
1875
1995
  };
1876
1996
  //# sourceMappingURL=index.js.map