@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/react.cjs CHANGED
@@ -124,7 +124,7 @@ function useContentIrKindVersion(kind, sources) {
124
124
  }
125
125
 
126
126
  // react/KindInstanceRender.tsx
127
- var import_react6 = require("react");
127
+ var import_react7 = require("react");
128
128
  var import_content_ir8 = require("@ai-matrx/content-ir");
129
129
 
130
130
  // directives/DirectiveRender.tsx
@@ -184,11 +184,28 @@ function safeJson(value) {
184
184
  return String(value);
185
185
  }
186
186
  }
187
- function itemChanges(item) {
187
+ function sameValue(a, b) {
188
+ if (a === b) return true;
189
+ if (a === null || a === void 0 || b === null || b === void 0) {
190
+ const emptyA = a === null || a === void 0 || a === "";
191
+ const emptyB = b === null || b === void 0 || b === "";
192
+ return emptyA && emptyB;
193
+ }
194
+ if (typeof a === "object" || typeof b === "object") return safeJson(a) === safeJson(b);
195
+ return false;
196
+ }
197
+ function itemChanges(item, current, options = {}) {
188
198
  const out = [];
199
+ const titleColumn = options.titleColumn ?? null;
189
200
  for (const [key, raw] of Object.entries(item)) {
190
201
  if (NOT_A_CHANGE.has(key)) continue;
191
- out.push({ key, label: fieldLabel(key), ...describeValue(raw) });
202
+ const label = key === titleColumn ? "Title" : fieldLabel(key);
203
+ const change = { key, label, ...describeValue(raw) };
204
+ if (current && Object.prototype.hasOwnProperty.call(current, key)) {
205
+ change.before = describeValue(current[key]);
206
+ change.same = sameValue(current[key], raw);
207
+ }
208
+ out.push(change);
192
209
  }
193
210
  return out;
194
211
  }
@@ -301,12 +318,23 @@ function ApplyDirectiveButton({
301
318
  const conversationId = scope.conversationId || void 0;
302
319
  const items = directive.items;
303
320
  const readsLedger = !!host.applyState && !!host.confirm && directive.parsed.executes && items.length > 0;
304
- const [state, setState] = (0, import_react4.useState)(
305
- readsLedger ? { status: "checking" } : { status: "idle" }
306
- );
321
+ const initial = readsLedger ? { status: "checking" } : { status: "idle" };
322
+ const [state, setState] = (0, import_react4.useState)(initial);
307
323
  const [elapsed, setElapsed] = (0, import_react4.useState)(0);
308
324
  const [askingAgain, setAskingAgain] = (0, import_react4.useState)(false);
309
325
  const startedAt = (0, import_react4.useRef)(null);
326
+ const identity = JSON.stringify([directive.slug, items, conversationId ?? null]);
327
+ const [stateFor, setStateFor] = (0, import_react4.useState)(identity);
328
+ if (stateFor !== identity) {
329
+ setStateFor(identity);
330
+ setState(initial);
331
+ setAskingAgain(false);
332
+ setElapsed(0);
333
+ }
334
+ const currentIdentity = (0, import_react4.useRef)(identity);
335
+ (0, import_react4.useEffect)(() => {
336
+ currentIdentity.current = identity;
337
+ }, [identity]);
310
338
  const nounText = (0, import_content_ir.nounLabel)(directive.noun, host.nouns);
311
339
  const shellKey = readsLedger ? JSON.stringify([directive.slug, items, conversationId ?? null]) : "";
312
340
  const read = host.applyState;
@@ -329,7 +357,8 @@ function ApplyDirectiveButton({
329
357
  applied: shellItems.length,
330
358
  failed: 0,
331
359
  message: answer.message?.trim() ? answer.message.trim() : null,
332
- records: answer.records ?? []
360
+ records: answer.records ?? [],
361
+ copies: Math.max(1, answer.copies ?? 1)
333
362
  } : prev
334
363
  );
335
364
  } else if (answer.state === "in_flight" && polls < FINISH_POLLS_MAX) {
@@ -369,6 +398,9 @@ function ApplyDirectiveButton({
369
398
  async function handleApply(again = false) {
370
399
  if (!confirm) return;
371
400
  const before = state;
401
+ const askedFor = identity;
402
+ const stillHere = () => currentIdentity.current === askedFor;
403
+ const priorCopies = state.status === "applied" ? state.copies : 0;
372
404
  if (host.ask) {
373
405
  if (again) setAskingAgain(true);
374
406
  else setState({ status: "asking" });
@@ -381,6 +413,7 @@ function ApplyDirectiveButton({
381
413
  ...again ? { again: true } : {}
382
414
  });
383
415
  } catch (error) {
416
+ if (!stillHere()) return;
384
417
  setAskingAgain(false);
385
418
  setState({
386
419
  status: "error",
@@ -388,6 +421,7 @@ function ApplyDirectiveButton({
388
421
  });
389
422
  return;
390
423
  }
424
+ if (!stillHere()) return;
391
425
  setAskingAgain(false);
392
426
  if (!ok) {
393
427
  setState(again ? before : { status: "idle" });
@@ -401,15 +435,19 @@ function ApplyDirectiveButton({
401
435
  if (conversationId) shell.conversationId = conversationId;
402
436
  if (again) shell.force = true;
403
437
  const result = await confirm(shell);
438
+ if (!stillHere()) return;
404
439
  setState({
405
440
  status: "applied",
406
441
  applied: result.applied,
407
442
  failed: result.failed,
408
443
  message: result.message?.trim() ? result.message.trim() : null,
409
- records: result.records ?? []
444
+ records: result.records ?? [],
445
+ // "Run again" made one more; a first apply (or a replay of one) is one.
446
+ copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies)
410
447
  });
411
448
  onApplied?.();
412
449
  } catch (error) {
450
+ if (!stillHere()) return;
413
451
  setState({
414
452
  status: "error",
415
453
  message: error instanceof Error && error.message ? error.message : "That couldn't be applied just now. Please try again."
@@ -434,6 +472,15 @@ function ApplyDirectiveButton({
434
472
  bad && !state.message ? ` \xB7 ${state.failed} failed` : ""
435
473
  ] })
436
474
  ] }),
475
+ state.copies > 1 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
476
+ "span",
477
+ {
478
+ className: "shrink-0 text-muted-foreground",
479
+ "data-apply-copies": state.copies,
480
+ title: records.length > 0 ? directive.directiveClass === "create" ? "Links to the latest copy." : "Links to the latest run." : void 0,
481
+ children: directive.directiveClass === "create" ? `${state.copies} copies` : `Ran ${state.copies} times`
482
+ }
483
+ ) : null,
437
484
  records.map((record) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
438
485
  ...record,
439
486
  fallback: recordFallbackName(label2, record.id),
@@ -484,6 +531,7 @@ function ApplyDirectiveButton({
484
531
  }
485
532
  );
486
533
  }
534
+ const destructive = directive.directiveClass === "delete";
487
535
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "inline-flex items-center gap-2", children: [
488
536
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
489
537
  "button",
@@ -491,7 +539,8 @@ function ApplyDirectiveButton({
491
539
  type: "button",
492
540
  onClick: () => void handleApply(),
493
541
  disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
494
- 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",
542
+ "data-apply-tone": destructive ? "destructive" : "default",
543
+ 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"}`,
495
544
  children: [
496
545
  state.status === "applying" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(LoaderIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PlayIcon, { className: "h-3.5 w-3.5" }),
497
546
  state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
@@ -584,8 +633,41 @@ function getDirectiveRenderer(directive) {
584
633
  }
585
634
 
586
635
  // directives/SideEffectDirectiveCard.tsx
587
- var import_react5 = require("react");
636
+ var import_react6 = require("react");
588
637
  var import_content_ir5 = require("@ai-matrx/content-ir");
638
+
639
+ // directives/record-values.ts
640
+ var import_react5 = require("react");
641
+ function useRecordValues(read, ref) {
642
+ const key = ref && read ? `${ref.noun}:${ref.id}` : "";
643
+ const [result, setResult] = (0, import_react5.useState)({
644
+ key: "",
645
+ values: null,
646
+ status: "idle"
647
+ });
648
+ (0, import_react5.useEffect)(() => {
649
+ if (!key || !read) return;
650
+ const split = key.indexOf(":");
651
+ const target = { noun: key.slice(0, split), id: key.slice(split + 1) };
652
+ let live = true;
653
+ setResult({ key, values: null, status: "loading" });
654
+ read(target).then((values) => {
655
+ if (!live) return;
656
+ setResult({ key, values, status: values ? "ready" : "unavailable" });
657
+ }).catch(() => {
658
+ if (!live) return;
659
+ setResult({ key, values: null, status: "unavailable" });
660
+ });
661
+ return () => {
662
+ live = false;
663
+ };
664
+ }, [key, read]);
665
+ if (!key) return { values: null, status: "idle" };
666
+ if (result.key !== key) return { values: null, status: "loading" };
667
+ return { values: result.values, status: result.status };
668
+ }
669
+
670
+ // directives/SideEffectDirectiveCard.tsx
589
671
  var import_jsx_runtime7 = require("react/jsx-runtime");
590
672
  var ROWS_BEFORE_FOLD = 3;
591
673
  function ItemRow({
@@ -639,44 +721,73 @@ function ItemRow({
639
721
  host.renderCopy?.({ label: title, value: item, kind, size: "xs" })
640
722
  ] });
641
723
  }
642
- function TargetRow({
643
- directive,
644
- item,
645
- recordId
724
+ function DirectiveChangeList({
725
+ changes,
726
+ className
646
727
  }) {
647
- const host = useDirectiveHost();
648
- const fallback = recordFallbackName((0, import_content_ir5.nounLabel)(directive.noun, host.nouns), recordId);
649
- const changes = directive.directiveClass === "update" ? itemChanges(item) : [];
650
- const name = host.renderRecord ? host.renderRecord({ noun: directive.noun, id: recordId, fallback, context: "row" }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "truncate font-medium text-foreground", children: fallback });
651
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
652
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
653
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
654
- host.renderCopy?.({ label: fallback, value: item, size: "xs" })
655
- ] }),
656
- directive.directiveClass === "update" ? changes.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "px-1 text-[11px] text-muted-foreground", children: "Sets no fields \u2014 nothing would change." }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { className: "mt-0.5 space-y-0.5 px-1", children: changes.map((change) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
728
+ if (changes.length === 0) {
729
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: `text-[11px] text-muted-foreground${className ? ` ${className}` : ""}`, children: "Sets no fields \u2014 nothing would change." });
730
+ }
731
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { className: `space-y-0.5${className ? ` ${className}` : ""}`, "data-directive-changes": "", children: changes.map((change) => {
732
+ const before = change.before && !change.same ? change.before : null;
733
+ const was = before ? before.clears ? "empty" : before.value : null;
734
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
657
735
  "li",
658
736
  {
659
737
  className: "flex min-w-0 items-baseline gap-1.5 text-[11px]",
660
- title: `${change.label} \u2192 ${change.full}`,
738
+ title: before ? `${change.label}: ${before.clears ? "empty" : before.full} \u2192 ${change.full}` : `${change.label} \u2192 ${change.full}`,
739
+ "data-change-key": change.key,
661
740
  children: [
662
741
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "shrink-0 text-muted-foreground", children: change.label }),
742
+ was !== null ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
743
+ "span",
744
+ {
745
+ className: "min-w-0 max-w-[40%] shrink truncate text-muted-foreground line-through",
746
+ "data-change-before": "",
747
+ children: was
748
+ }
749
+ ) : null,
663
750
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { "aria-hidden": true, className: "shrink-0 text-muted-foreground", children: "\u2192" }),
664
751
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
665
752
  "span",
666
753
  {
667
754
  className: `min-w-0 truncate ${change.clears ? "italic text-muted-foreground" : "text-foreground"}`,
755
+ "data-change-after": "",
668
756
  children: change.value
669
757
  }
670
- )
758
+ ),
759
+ change.same ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "shrink-0 text-muted-foreground", children: "(no change)" }) : null
671
760
  ]
672
761
  },
673
762
  change.key
674
- )) }) : null
763
+ );
764
+ }) });
765
+ }
766
+ function TargetRow({
767
+ directive,
768
+ item,
769
+ recordId
770
+ }) {
771
+ const host = useDirectiveHost();
772
+ const fallback = recordFallbackName((0, import_content_ir5.nounLabel)(directive.noun, host.nouns), recordId);
773
+ const isUpdate = directive.directiveClass === "update";
774
+ const current = useRecordValues(
775
+ isUpdate ? host.readRecord : void 0,
776
+ isUpdate ? { noun: directive.noun, id: recordId } : null
777
+ );
778
+ const changes = isUpdate ? itemChanges(item, current.values, { titleColumn: (0, import_content_ir5.nounTitleColumn)(directive.noun, host.nouns) }) : [];
779
+ const name = host.renderRecord ? host.renderRecord({ noun: directive.noun, id: recordId, fallback, context: "row" }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "truncate font-medium text-foreground", children: fallback });
780
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
781
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
782
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
783
+ host.renderCopy?.({ label: fallback, value: item, size: "xs" })
784
+ ] }),
785
+ isUpdate ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DirectiveChangeList, { changes, className: "mt-0.5 px-1" }) : null
675
786
  ] });
676
787
  }
677
788
  function SideEffectDirectiveCard({ directive }) {
678
789
  const host = useDirectiveHost();
679
- const [expanded, setExpanded] = (0, import_react5.useState)(false);
790
+ const [expanded, setExpanded] = (0, import_react6.useState)(false);
680
791
  const items = directive.items;
681
792
  const total = items.length;
682
793
  const display = (0, import_content_ir5.directiveDisplay)(directive.directiveClass, directive.noun, host.nouns);
@@ -809,10 +920,10 @@ function KindInstanceRenderInner({
809
920
  className
810
921
  }) {
811
922
  const host = useContentIrHost();
812
- const [routingStatus, setRoutingStatus] = (0, import_react6.useState)(
923
+ const [routingStatus, setRoutingStatus] = (0, import_react7.useState)(
813
924
  () => kindIsRoutable(kind, host) ? "routable" : "checking"
814
925
  );
815
- (0, import_react6.useEffect)(() => {
926
+ (0, import_react7.useEffect)(() => {
816
927
  let cancelled = false;
817
928
  let warmed = false;
818
929
  const syncRoutingStatus = () => {
@@ -860,7 +971,7 @@ function KindInstanceRenderInner({
860
971
  }
861
972
 
862
973
  // react/ProvisionalKindBoundary.tsx
863
- var import_react7 = __toESM(require("react"), 1);
974
+ var import_react8 = __toESM(require("react"), 1);
864
975
 
865
976
  // route/partial-kind-route.ts
866
977
  var import_content_ir9 = require("@ai-matrx/content-ir");
@@ -874,7 +985,7 @@ function markKindPartialUnsafe(kind) {
874
985
 
875
986
  // react/ProvisionalKindBoundary.tsx
876
987
  var import_jsx_runtime10 = require("react/jsx-runtime");
877
- var ProvisionalKindBoundaryInner = class extends import_react7.default.Component {
988
+ var ProvisionalKindBoundaryInner = class extends import_react8.default.Component {
878
989
  state = { failed: false };
879
990
  static getDerivedStateFromError() {
880
991
  return { failed: true };
@@ -910,7 +1021,7 @@ function ProvisionalKindFrame({ children }) {
910
1021
  }
911
1022
 
912
1023
  // react/GenericStructuredView.tsx
913
- var import_react8 = require("react");
1024
+ var import_react9 = require("react");
914
1025
  var import_content_ir10 = require("@ai-matrx/content-ir");
915
1026
  var import_jsx_runtime11 = require("react/jsx-runtime");
916
1027
  function readStructuredValue(content, metadata) {