@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/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,26 @@ 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) {
188
198
  const out = [];
189
199
  for (const [key, raw] of Object.entries(item)) {
190
200
  if (NOT_A_CHANGE.has(key)) continue;
191
- out.push({ key, label: fieldLabel(key), ...describeValue(raw) });
201
+ const change = { key, label: fieldLabel(key), ...describeValue(raw) };
202
+ if (current && Object.prototype.hasOwnProperty.call(current, key)) {
203
+ change.before = describeValue(current[key]);
204
+ change.same = sameValue(current[key], raw);
205
+ }
206
+ out.push(change);
192
207
  }
193
208
  return out;
194
209
  }
@@ -301,11 +316,23 @@ function ApplyDirectiveButton({
301
316
  const conversationId = scope.conversationId || void 0;
302
317
  const items = directive.items;
303
318
  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
- );
319
+ const initial = readsLedger ? { status: "checking" } : { status: "idle" };
320
+ const [state, setState] = (0, import_react4.useState)(initial);
307
321
  const [elapsed, setElapsed] = (0, import_react4.useState)(0);
322
+ const [askingAgain, setAskingAgain] = (0, import_react4.useState)(false);
308
323
  const startedAt = (0, import_react4.useRef)(null);
324
+ const identity = JSON.stringify([directive.slug, items, conversationId ?? null]);
325
+ const [stateFor, setStateFor] = (0, import_react4.useState)(identity);
326
+ if (stateFor !== identity) {
327
+ setStateFor(identity);
328
+ setState(initial);
329
+ setAskingAgain(false);
330
+ setElapsed(0);
331
+ }
332
+ const currentIdentity = (0, import_react4.useRef)(identity);
333
+ (0, import_react4.useEffect)(() => {
334
+ currentIdentity.current = identity;
335
+ }, [identity]);
309
336
  const nounText = (0, import_content_ir.nounLabel)(directive.noun, host.nouns);
310
337
  const shellKey = readsLedger ? JSON.stringify([directive.slug, items, conversationId ?? null]) : "";
311
338
  const read = host.applyState;
@@ -328,7 +355,8 @@ function ApplyDirectiveButton({
328
355
  applied: shellItems.length,
329
356
  failed: 0,
330
357
  message: answer.message?.trim() ? answer.message.trim() : null,
331
- records: answer.records ?? []
358
+ records: answer.records ?? [],
359
+ copies: Math.max(1, answer.copies ?? 1)
332
360
  } : prev
333
361
  );
334
362
  } else if (answer.state === "in_flight" && polls < FINISH_POLLS_MAX) {
@@ -368,8 +396,12 @@ function ApplyDirectiveButton({
368
396
  async function handleApply(again = false) {
369
397
  if (!confirm) return;
370
398
  const before = state;
399
+ const askedFor = identity;
400
+ const stillHere = () => currentIdentity.current === askedFor;
401
+ const priorCopies = state.status === "applied" ? state.copies : 0;
371
402
  if (host.ask) {
372
- setState({ status: "asking" });
403
+ if (again) setAskingAgain(true);
404
+ else setState({ status: "asking" });
373
405
  let ok = false;
374
406
  try {
375
407
  ok = await host.ask({
@@ -379,12 +411,16 @@ function ApplyDirectiveButton({
379
411
  ...again ? { again: true } : {}
380
412
  });
381
413
  } catch (error) {
414
+ if (!stillHere()) return;
415
+ setAskingAgain(false);
382
416
  setState({
383
417
  status: "error",
384
418
  message: error instanceof Error && error.message ? error.message : "We could not ask you to confirm this, so nothing was done."
385
419
  });
386
420
  return;
387
421
  }
422
+ if (!stillHere()) return;
423
+ setAskingAgain(false);
388
424
  if (!ok) {
389
425
  setState(again ? before : { status: "idle" });
390
426
  return;
@@ -397,15 +433,19 @@ function ApplyDirectiveButton({
397
433
  if (conversationId) shell.conversationId = conversationId;
398
434
  if (again) shell.force = true;
399
435
  const result = await confirm(shell);
436
+ if (!stillHere()) return;
400
437
  setState({
401
438
  status: "applied",
402
439
  applied: result.applied,
403
440
  failed: result.failed,
404
441
  message: result.message?.trim() ? result.message.trim() : null,
405
- records: result.records ?? []
442
+ records: result.records ?? [],
443
+ // "Run again" made one more; a first apply (or a replay of one) is one.
444
+ copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies)
406
445
  });
407
446
  onApplied?.();
408
447
  } catch (error) {
448
+ if (!stillHere()) return;
409
449
  setState({
410
450
  status: "error",
411
451
  message: error instanceof Error && error.message ? error.message : "That couldn't be applied just now. Please try again."
@@ -430,6 +470,15 @@ function ApplyDirectiveButton({
430
470
  bad && !state.message ? ` \xB7 ${state.failed} failed` : ""
431
471
  ] })
432
472
  ] }),
473
+ state.copies > 1 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
474
+ "span",
475
+ {
476
+ className: "shrink-0 text-muted-foreground",
477
+ "data-apply-copies": state.copies,
478
+ title: records.length > 0 ? directive.directiveClass === "create" ? "Links to the latest copy." : "Links to the latest run." : void 0,
479
+ children: directive.directiveClass === "create" ? `${state.copies} copies` : `Ran ${state.copies} times`
480
+ }
481
+ ) : null,
433
482
  records.map((record) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
434
483
  ...record,
435
484
  fallback: recordFallbackName(label2, record.id),
@@ -440,8 +489,9 @@ function ApplyDirectiveButton({
440
489
  "button",
441
490
  {
442
491
  type: "button",
492
+ disabled: askingAgain,
443
493
  onClick: () => void handleApply(!bad),
444
- className: "inline-flex items-center gap-1 rounded px-1 py-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground",
494
+ 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",
445
495
  "data-apply-action": bad ? "retry" : "run-again",
446
496
  children: [
447
497
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RotateCcwIcon, { className: "h-3 w-3" }),
@@ -479,6 +529,7 @@ function ApplyDirectiveButton({
479
529
  }
480
530
  );
481
531
  }
532
+ const destructive = directive.directiveClass === "delete";
482
533
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "inline-flex items-center gap-2", children: [
483
534
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
484
535
  "button",
@@ -486,7 +537,8 @@ function ApplyDirectiveButton({
486
537
  type: "button",
487
538
  onClick: () => void handleApply(),
488
539
  disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
489
- 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",
540
+ "data-apply-tone": destructive ? "destructive" : "default",
541
+ 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"}`,
490
542
  children: [
491
543
  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" }),
492
544
  state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
@@ -579,8 +631,41 @@ function getDirectiveRenderer(directive) {
579
631
  }
580
632
 
581
633
  // directives/SideEffectDirectiveCard.tsx
582
- var import_react5 = require("react");
634
+ var import_react6 = require("react");
583
635
  var import_content_ir5 = require("@ai-matrx/content-ir");
636
+
637
+ // directives/record-values.ts
638
+ var import_react5 = require("react");
639
+ function useRecordValues(read, ref) {
640
+ const key = ref && read ? `${ref.noun}:${ref.id}` : "";
641
+ const [result, setResult] = (0, import_react5.useState)({
642
+ key: "",
643
+ values: null,
644
+ status: "idle"
645
+ });
646
+ (0, import_react5.useEffect)(() => {
647
+ if (!key || !read) return;
648
+ const split = key.indexOf(":");
649
+ const target = { noun: key.slice(0, split), id: key.slice(split + 1) };
650
+ let live = true;
651
+ setResult({ key, values: null, status: "loading" });
652
+ read(target).then((values) => {
653
+ if (!live) return;
654
+ setResult({ key, values, status: values ? "ready" : "unavailable" });
655
+ }).catch(() => {
656
+ if (!live) return;
657
+ setResult({ key, values: null, status: "unavailable" });
658
+ });
659
+ return () => {
660
+ live = false;
661
+ };
662
+ }, [key, read]);
663
+ if (!key) return { values: null, status: "idle" };
664
+ if (result.key !== key) return { values: null, status: "loading" };
665
+ return { values: result.values, status: result.status };
666
+ }
667
+
668
+ // directives/SideEffectDirectiveCard.tsx
584
669
  var import_jsx_runtime7 = require("react/jsx-runtime");
585
670
  var ROWS_BEFORE_FOLD = 3;
586
671
  function ItemRow({
@@ -634,44 +719,73 @@ function ItemRow({
634
719
  host.renderCopy?.({ label: title, value: item, kind, size: "xs" })
635
720
  ] });
636
721
  }
637
- function TargetRow({
638
- directive,
639
- item,
640
- recordId
722
+ function DirectiveChangeList({
723
+ changes,
724
+ className
641
725
  }) {
642
- const host = useDirectiveHost();
643
- const fallback = recordFallbackName((0, import_content_ir5.nounLabel)(directive.noun, host.nouns), recordId);
644
- const changes = directive.directiveClass === "update" ? itemChanges(item) : [];
645
- 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 });
646
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
647
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
648
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
649
- host.renderCopy?.({ label: fallback, value: item, size: "xs" })
650
- ] }),
651
- 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)(
726
+ if (changes.length === 0) {
727
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: `text-[11px] text-muted-foreground${className ? ` ${className}` : ""}`, children: "Sets no fields \u2014 nothing would change." });
728
+ }
729
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { className: `space-y-0.5${className ? ` ${className}` : ""}`, "data-directive-changes": "", children: changes.map((change) => {
730
+ const before = change.before && !change.same ? change.before : null;
731
+ const was = before ? before.clears ? "empty" : before.value : null;
732
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
652
733
  "li",
653
734
  {
654
735
  className: "flex min-w-0 items-baseline gap-1.5 text-[11px]",
655
- title: `${change.label} \u2192 ${change.full}`,
736
+ title: before ? `${change.label}: ${before.clears ? "empty" : before.full} \u2192 ${change.full}` : `${change.label} \u2192 ${change.full}`,
737
+ "data-change-key": change.key,
656
738
  children: [
657
739
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "shrink-0 text-muted-foreground", children: change.label }),
740
+ was !== null ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
741
+ "span",
742
+ {
743
+ className: "min-w-0 max-w-[40%] shrink truncate text-muted-foreground line-through",
744
+ "data-change-before": "",
745
+ children: was
746
+ }
747
+ ) : null,
658
748
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { "aria-hidden": true, className: "shrink-0 text-muted-foreground", children: "\u2192" }),
659
749
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
660
750
  "span",
661
751
  {
662
752
  className: `min-w-0 truncate ${change.clears ? "italic text-muted-foreground" : "text-foreground"}`,
753
+ "data-change-after": "",
663
754
  children: change.value
664
755
  }
665
- )
756
+ ),
757
+ change.same ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "shrink-0 text-muted-foreground", children: "(no change)" }) : null
666
758
  ]
667
759
  },
668
760
  change.key
669
- )) }) : null
761
+ );
762
+ }) });
763
+ }
764
+ function TargetRow({
765
+ directive,
766
+ item,
767
+ recordId
768
+ }) {
769
+ const host = useDirectiveHost();
770
+ const fallback = recordFallbackName((0, import_content_ir5.nounLabel)(directive.noun, host.nouns), recordId);
771
+ const isUpdate = directive.directiveClass === "update";
772
+ const current = useRecordValues(
773
+ isUpdate ? host.readRecord : void 0,
774
+ isUpdate ? { noun: directive.noun, id: recordId } : null
775
+ );
776
+ const changes = isUpdate ? itemChanges(item, current.values) : [];
777
+ 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 });
778
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
779
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
780
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
781
+ host.renderCopy?.({ label: fallback, value: item, size: "xs" })
782
+ ] }),
783
+ isUpdate ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DirectiveChangeList, { changes, className: "mt-0.5 px-1" }) : null
670
784
  ] });
671
785
  }
672
786
  function SideEffectDirectiveCard({ directive }) {
673
787
  const host = useDirectiveHost();
674
- const [expanded, setExpanded] = (0, import_react5.useState)(false);
788
+ const [expanded, setExpanded] = (0, import_react6.useState)(false);
675
789
  const items = directive.items;
676
790
  const total = items.length;
677
791
  const display = (0, import_content_ir5.directiveDisplay)(directive.directiveClass, directive.noun, host.nouns);
@@ -804,10 +918,10 @@ function KindInstanceRenderInner({
804
918
  className
805
919
  }) {
806
920
  const host = useContentIrHost();
807
- const [routingStatus, setRoutingStatus] = (0, import_react6.useState)(
921
+ const [routingStatus, setRoutingStatus] = (0, import_react7.useState)(
808
922
  () => kindIsRoutable(kind, host) ? "routable" : "checking"
809
923
  );
810
- (0, import_react6.useEffect)(() => {
924
+ (0, import_react7.useEffect)(() => {
811
925
  let cancelled = false;
812
926
  let warmed = false;
813
927
  const syncRoutingStatus = () => {
@@ -855,7 +969,7 @@ function KindInstanceRenderInner({
855
969
  }
856
970
 
857
971
  // react/ProvisionalKindBoundary.tsx
858
- var import_react7 = __toESM(require("react"), 1);
972
+ var import_react8 = __toESM(require("react"), 1);
859
973
 
860
974
  // route/partial-kind-route.ts
861
975
  var import_content_ir9 = require("@ai-matrx/content-ir");
@@ -869,7 +983,7 @@ function markKindPartialUnsafe(kind) {
869
983
 
870
984
  // react/ProvisionalKindBoundary.tsx
871
985
  var import_jsx_runtime10 = require("react/jsx-runtime");
872
- var ProvisionalKindBoundaryInner = class extends import_react7.default.Component {
986
+ var ProvisionalKindBoundaryInner = class extends import_react8.default.Component {
873
987
  state = { failed: false };
874
988
  static getDerivedStateFromError() {
875
989
  return { failed: true };
@@ -905,7 +1019,7 @@ function ProvisionalKindFrame({ children }) {
905
1019
  }
906
1020
 
907
1021
  // react/GenericStructuredView.tsx
908
- var import_react8 = require("react");
1022
+ var import_react9 = require("react");
909
1023
  var import_content_ir10 = require("@ai-matrx/content-ir");
910
1024
  var import_jsx_runtime11 = require("react/jsx-runtime");
911
1025
  function readStructuredValue(content, metadata) {