@ai-matrx/content-ir-react 0.17.1 → 0.18.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
@@ -331,17 +331,22 @@ function classIcon(directiveClass, noun) {
331
331
  var import_jsx_runtime4 = require("react/jsx-runtime");
332
332
  var FINISH_POLL_MS = 2e3;
333
333
  var FINISH_POLLS_MAX = 30;
334
+ function readsApplyLedger(host, directive) {
335
+ return !!host.applyState && !!host.confirm && directive.parsed.executes && directive.items.length > 0;
336
+ }
334
337
  function ApplyDirectiveButton({
335
338
  directive,
336
339
  label = "Apply",
337
340
  itemCount,
338
- onApplied
341
+ onApplied,
342
+ onPhase,
343
+ nothingToChange = false
339
344
  }) {
340
345
  const host = useDirectiveHost();
341
346
  const scope = useDirectiveScope();
342
347
  const conversationId = scope.conversationId || void 0;
343
348
  const items = directive.items;
344
- const readsLedger = !!host.applyState && !!host.confirm && directive.parsed.executes && items.length > 0;
349
+ const readsLedger = readsApplyLedger(host, directive);
345
350
  const initial = readsLedger ? { status: "checking" } : { status: "idle" };
346
351
  const [state, setState] = (0, import_react4.useState)(initial);
347
352
  const [elapsed, setElapsed] = (0, import_react4.useState)(0);
@@ -382,7 +387,8 @@ function ApplyDirectiveButton({
382
387
  failed: 0,
383
388
  message: answer.message?.trim() ? answer.message.trim() : null,
384
389
  records: answer.records ?? [],
385
- copies: Math.max(1, answer.copies ?? 1)
390
+ copies: Math.max(1, answer.copies ?? 1),
391
+ before: answer.before ?? null
386
392
  } : prev
387
393
  );
388
394
  } else if (answer.state === "in_flight" && polls < FINISH_POLLS_MAX) {
@@ -407,6 +413,15 @@ function ApplyDirectiveButton({
407
413
  if (timer) clearTimeout(timer);
408
414
  };
409
415
  }, [shellKey, read, report]);
416
+ const appliedClean = state.status === "applied" && state.failed === 0 ? state : null;
417
+ const appliedBefore = appliedClean ? appliedClean.before : null;
418
+ const phaseKey = state.status === "checking" || state.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
419
+ (0, import_react4.useEffect)(() => {
420
+ if (!onPhase) return;
421
+ if (phaseKey === "pending") onPhase({ phase: "pending" });
422
+ else if (phaseKey === "open") onPhase({ phase: "open" });
423
+ else onPhase({ phase: "applied", before: appliedBefore });
424
+ }, [phaseKey, onPhase, appliedBefore]);
410
425
  (0, import_react4.useEffect)(() => {
411
426
  if (state.status !== "applying") return;
412
427
  startedAt.current = Date.now();
@@ -421,7 +436,7 @@ function ApplyDirectiveButton({
421
436
  if (!confirm) return null;
422
437
  async function handleApply(again = false) {
423
438
  if (!confirm) return;
424
- const before = state;
439
+ const prior = state;
425
440
  const askedFor = identity;
426
441
  const stillHere = () => currentIdentity.current === askedFor;
427
442
  const priorCopies = state.status === "applied" ? state.copies : 0;
@@ -448,7 +463,7 @@ function ApplyDirectiveButton({
448
463
  if (!stillHere()) return;
449
464
  setAskingAgain(false);
450
465
  if (!ok) {
451
- setState(again ? before : { status: "idle" });
466
+ setState(again ? prior : { status: "idle" });
452
467
  return;
453
468
  }
454
469
  }
@@ -467,7 +482,10 @@ function ApplyDirectiveButton({
467
482
  message: result.message?.trim() ? result.message.trim() : null,
468
483
  records: result.records ?? [],
469
484
  // "Run again" made one more; a first apply (or a replay of one) is one.
470
- copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies)
485
+ copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies),
486
+ // "Run again" wrote the same values over themselves; what the FIRST
487
+ // apply replaced is still the change this block made.
488
+ before: again && prior.status === "applied" && prior.before ? prior.before : result.before ?? null
471
489
  });
472
490
  onApplied?.();
473
491
  } catch (error) {
@@ -569,15 +587,18 @@ function ApplyDirectiveButton({
569
587
  );
570
588
  }
571
589
  const destructive = directive.directiveClass === "delete";
590
+ const muted = nothingToChange && state.status === "idle";
572
591
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "inline-flex items-center gap-2", children: [
592
+ muted ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-xs text-muted-foreground", "data-apply-nothing": "", children: "Nothing to change" }) : null,
573
593
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
574
594
  "button",
575
595
  {
576
596
  type: "button",
577
597
  onClick: () => void handleApply(),
578
598
  disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
579
- "data-apply-tone": destructive ? "destructive" : "default",
580
- 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"}`,
599
+ "data-apply-tone": muted ? "muted" : destructive ? "destructive" : "default",
600
+ title: muted ? "It sets no fields, so applying it changes nothing." : void 0,
601
+ className: `inline-flex items-center gap-1 rounded-md border px-2 py-1 text-xs font-medium disabled:opacity-50 ${muted ? "border-border/60 bg-background text-muted-foreground hover:bg-accent" : destructive ? "border-destructive/50 bg-background text-destructive hover:bg-destructive/10" : "border-border bg-background hover:bg-accent"}`,
581
602
  children: [
582
603
  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" }),
583
604
  state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
@@ -765,12 +786,13 @@ function ItemRow({
765
786
  directive,
766
787
  item,
767
788
  index,
768
- total
789
+ total,
790
+ phase
769
791
  }) {
770
792
  const host = useDirectiveHost();
771
793
  const recordId = TARGETING_CLASSES.has(directive.directiveClass) ? itemRecordId(item) : null;
772
794
  if (recordId) {
773
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TargetRow, { directive, item, recordId });
795
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TargetRow, { directive, item, recordId, index, phase });
774
796
  }
775
797
  const titleColumn = (0, import_content_ir5.nounTitleColumn)(directive.noun, host.nouns);
776
798
  const title = (0, import_content_ir5.itemTitle)(item, titleColumn, index, total);
@@ -857,16 +879,20 @@ function DirectiveChangeList({
857
879
  function TargetRow({
858
880
  directive,
859
881
  item,
860
- recordId
882
+ recordId,
883
+ index,
884
+ phase
861
885
  }) {
862
886
  const host = useDirectiveHost();
863
887
  const fallback = recordFallbackName((0, import_content_ir5.nounLabel)(directive.noun, host.nouns), recordId);
864
888
  const isUpdate = directive.directiveClass === "update";
865
- const current = useRecordValues(
866
- isUpdate ? host.readRecord : void 0,
867
- isUpdate ? { noun: directive.noun, id: recordId } : null
889
+ const readsLive = isUpdate && phase.phase === "open";
890
+ const live = useRecordValues(
891
+ readsLive ? host.readRecord : void 0,
892
+ readsLive ? { noun: directive.noun, id: recordId } : null
868
893
  );
869
- const changes = isUpdate ? itemChanges(item, current.values, { titleColumn: (0, import_content_ir5.nounTitleColumn)(directive.noun, host.nouns) }) : [];
894
+ const compareTo = phase.phase === "applied" ? phase.before?.[index] ?? null : readsLive ? live.values : null;
895
+ const changes = isUpdate ? itemChanges(item, compareTo, { titleColumn: (0, import_content_ir5.nounTitleColumn)(directive.noun, host.nouns) }) : [];
870
896
  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 });
871
897
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
872
898
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
@@ -883,6 +909,10 @@ function SideEffectDirectiveCard({ directive }) {
883
909
  const total = items.length;
884
910
  const display = (0, import_content_ir5.directiveDisplay)(directive.directiveClass, directive.noun, host.nouns);
885
911
  const Icon = classIcon(directive.directiveClass, directive.noun);
912
+ const [phase, setPhase] = (0, import_react6.useState)(
913
+ () => readsApplyLedger(host, directive) ? { phase: "pending" } : { phase: "open" }
914
+ );
915
+ const nothingToChange = directive.directiveClass === "update" && total > 0 && items.every((item) => itemChanges(item).length === 0);
886
916
  const shown = expanded ? items : items.slice(0, ROWS_BEFORE_FOLD);
887
917
  const hidden = total - shown.length;
888
918
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "my-3 rounded-md border border-border bg-card px-2.5 py-1.5", "data-directive": directive.slug, children: [
@@ -897,13 +927,21 @@ function SideEffectDirectiveCard({ directive }) {
897
927
  ] }),
898
928
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ml-auto flex items-center gap-1", children: [
899
929
  host.renderCopy?.({ label: display.title, value: directive.shell, size: "icon" }),
900
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ApplyDirectiveButton, { directive, itemCount: total })
930
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
931
+ ApplyDirectiveButton,
932
+ {
933
+ directive,
934
+ itemCount: total,
935
+ onPhase: setPhase,
936
+ nothingToChange
937
+ }
938
+ )
901
939
  ] })
902
940
  ] }),
903
941
  total === 0 ? (
904
942
  // Stated, never silent: an empty batch is a real (and suspicious) state.
905
943
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "mt-1 text-xs text-muted-foreground", children: "No items \u2014 nothing would be written." })
906
- ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "mt-0.5 divide-y divide-border/60", children: shown.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ItemRow, { directive, item, index: i, total }, i)) }),
944
+ ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "mt-0.5 divide-y divide-border/60", children: shown.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ItemRow, { directive, item, index: i, total, phase }, i)) }),
907
945
  hidden > 0 || expanded ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
908
946
  "button",
909
947
  {