@ai-matrx/content-ir-react 0.17.0 → 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.
Files changed (43) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +1 -1
  3. package/dist/{component-resolution.types-DLO2w-0n.d.cts → component-resolution.types-DY4pGymy.d.cts} +1 -1
  4. package/dist/{component-resolution.types-DLO2w-0n.d.ts → component-resolution.types-DY4pGymy.d.ts} +1 -1
  5. package/dist/{component-resolution.types-C1MzCf2s.d.cts → component-resolution.types-DwKSF64x.d.cts} +1 -1
  6. package/dist/{component-resolution.types-C1MzCf2s.d.ts → component-resolution.types-DwKSF64x.d.ts} +1 -1
  7. package/dist/db-component.d.cts +1 -1
  8. package/dist/db-component.d.ts +1 -1
  9. package/dist/{directive-host-BOML4BQ7.d.cts → directive-host-Bw2_PrgL.d.cts} +14 -0
  10. package/dist/{directive-host-BOML4BQ7.d.ts → directive-host-Bw2_PrgL.d.ts} +14 -0
  11. package/dist/directives.cjs +56 -17
  12. package/dist/directives.cjs.map +1 -1
  13. package/dist/directives.d.cts +33 -5
  14. package/dist/directives.d.ts +33 -5
  15. package/dist/directives.js +56 -17
  16. package/dist/directives.js.map +1 -1
  17. package/dist/{host-types-Dxzs0C3f.d.cts → host-types-YP_l_Q_t.d.cts} +2 -2
  18. package/dist/{host-types-uKO1zZo3.d.ts → host-types-eSjFmXyF.d.ts} +2 -2
  19. package/dist/host.cjs.map +1 -1
  20. package/dist/host.d.cts +4 -4
  21. package/dist/host.d.ts +4 -4
  22. package/dist/host.js.map +1 -1
  23. package/dist/index.cjs +56 -17
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +36 -8
  26. package/dist/index.d.ts +36 -8
  27. package/dist/index.js +56 -17
  28. package/dist/index.js.map +1 -1
  29. package/dist/react.cjs +55 -17
  30. package/dist/react.cjs.map +1 -1
  31. package/dist/react.d.cts +4 -4
  32. package/dist/react.d.ts +4 -4
  33. package/dist/react.js +55 -17
  34. package/dist/react.js.map +1 -1
  35. package/dist/resolver.d.cts +2 -2
  36. package/dist/resolver.d.ts +2 -2
  37. package/dist/{route-CfKxIEdj.d.ts → route-CtYw2cTN.d.ts} +16 -2
  38. package/dist/{route-DjGv9skd.d.cts → route-KoRMp3Qn.d.cts} +16 -2
  39. package/dist/route.cjs.map +1 -1
  40. package/dist/route.d.cts +2 -2
  41. package/dist/route.d.ts +2 -2
  42. package/dist/route.js.map +1 -1
  43. package/package.json +3 -3
@@ -1,5 +1,5 @@
1
- import { D as DirectiveFailure, a as DirectiveFailureWords, b as DirectiveRecordRef } from './directive-host-BOML4BQ7.js';
2
- export { c as DirectiveApplyResult, d as DirectiveApplyState, e as DirectiveAskRequest, f as DirectiveCopyProps, g as DirectiveHost, h as DirectiveHostProvider, i as DirectiveHostProviderProps, j as DirectiveOpenItemOptions, k as DirectiveRecordProps, l as DirectiveScope, m as DirectiveScopeProvider, n as DirectiveShell, u as useDirectiveHost, o as useDirectiveScope } from './directive-host-BOML4BQ7.js';
1
+ import { D as DirectiveFailure, a as DirectiveFailureWords, b as DirectiveRecordRef } from './directive-host-Bw2_PrgL.js';
2
+ export { c as DirectiveApplyResult, d as DirectiveApplyState, e as DirectiveAskRequest, f as DirectiveCopyProps, g as DirectiveHost, h as DirectiveHostProvider, i as DirectiveHostProviderProps, j as DirectiveOpenItemOptions, k as DirectiveRecordProps, l as DirectiveScope, m as DirectiveScopeProvider, n as DirectiveShell, u as useDirectiveHost, o as useDirectiveScope } from './directive-host-Bw2_PrgL.js';
3
3
  import * as react from 'react';
4
4
  import { ComponentType } from 'react';
5
5
  import { DecodedDirective, DirectiveClass } from '@ai-matrx/content-ir';
@@ -40,14 +40,42 @@ declare function getDirectiveRenderer(directive: Pick<DecodedDirective, "slug" |
40
40
  /** Tests only. */
41
41
  declare function resetDirectiveRenderers(): void;
42
42
 
43
+ /**
44
+ * What the Apply control knows about its block, for the rest of the card:
45
+ * - `pending` — the ledger has not answered yet;
46
+ * - `open` — nothing applied: rows compare against the record as it is now;
47
+ * - `applied` — the change happened: rows draw it from `before` (the server's
48
+ * record of what it replaced), never from a live re-read, which
49
+ * now holds the NEW value.
50
+ */
51
+ type DirectiveApplyPhase = {
52
+ phase: "pending";
53
+ } | {
54
+ phase: "open";
55
+ } | {
56
+ phase: "applied";
57
+ before: ReadonlyArray<Record<string, unknown> | null> | null;
58
+ };
59
+ /** Does this card read the ledger before offering Apply? (One rule, card and control.) */
60
+ declare function readsApplyLedger(host: {
61
+ applyState?: unknown;
62
+ confirm?: unknown;
63
+ }, directive: DecodedDirective): boolean;
43
64
  interface ApplyDirectiveButtonProps {
44
65
  directive: DecodedDirective;
45
66
  label?: string;
46
67
  /** How many things this will write — shown during the wait so a long apply reads as work, not a frozen spinner. */
47
68
  itemCount?: number;
48
69
  onApplied?: () => void;
70
+ /** Told whenever what the control knows about its block changes. */
71
+ onPhase?: (phase: DirectiveApplyPhase) => void;
72
+ /**
73
+ * The block would change nothing (an update that sets no field). Apply stays
74
+ * — validation offers, never blocks — but muted, with the reason beside it.
75
+ */
76
+ nothingToChange?: boolean;
49
77
  }
50
- declare function ApplyDirectiveButton({ directive, label, itemCount, onApplied, }: ApplyDirectiveButtonProps): react.JSX.Element | null;
78
+ declare function ApplyDirectiveButton({ directive, label, itemCount, onApplied, onPhase, nothingToChange, }: ApplyDirectiveButtonProps): react.JSX.Element | null;
51
79
 
52
80
  /**
53
81
  * WHAT A DIRECTIVE ITEM WILL DO TO AN EXISTING RECORD — for the card, the
@@ -61,7 +89,7 @@ declare function ApplyDirectiveButton({ directive, label, itemCount, onApplied,
61
89
  * not the name of the record being changed. Naming the target is the host's
62
90
  * live lookup (`DirectiveHost.renderRecord`); what changes is right here.
63
91
  *
64
- * Law: common-docs/policies/destructive-and-expensive-actions.md — the sentence
92
+ * Law: common-docs/policies/no-dead-ends.md — the sentence
65
93
  * names what changes.
66
94
  */
67
95
  /** Classes whose items point at an EXISTING record by `id`. */
@@ -195,4 +223,4 @@ interface RecordValues {
195
223
  type RecordReader = (ref: DirectiveRecordRef) => Promise<Record<string, unknown> | null>;
196
224
  declare function useRecordValues(read: RecordReader | undefined, ref: DirectiveRecordRef | null): RecordValues;
197
225
 
198
- export { ApplyDirectiveButton, type ApplyDirectiveButtonProps, CHANGE_VALUE_MAX, type ChangeValue, DirectiveChangeList, DirectiveFailure, DirectiveFailureWords, DirectiveFallbackCard, type DirectiveFallbackCardProps, DirectiveProvisionalCard, type DirectiveProvisionalCardProps, DirectiveRecordRef, DirectiveRender, type DirectiveRenderProps, type DirectiveRenderer, type DirectiveRendererProps, type ItemChange, type ItemChangesOptions, type RecordReader, type RecordValues, type RecordValuesStatus, SideEffectDirectiveCard, type SideEffectDirectiveCardProps, TARGETING_CLASSES, defaultFailureWords, describeChangeValue, failureWords, fieldLabel, getDirectiveRenderer, itemChanges, itemRecordId, provisionalDirectiveSlug, recordFallbackName, registerDirectiveRenderer, resetDirectiveRenderers, useRecordValues };
226
+ export { ApplyDirectiveButton, type ApplyDirectiveButtonProps, CHANGE_VALUE_MAX, type ChangeValue, type DirectiveApplyPhase, DirectiveChangeList, DirectiveFailure, DirectiveFailureWords, DirectiveFallbackCard, type DirectiveFallbackCardProps, DirectiveProvisionalCard, type DirectiveProvisionalCardProps, DirectiveRecordRef, DirectiveRender, type DirectiveRenderProps, type DirectiveRenderer, type DirectiveRendererProps, type ItemChange, type ItemChangesOptions, type RecordReader, type RecordValues, type RecordValuesStatus, SideEffectDirectiveCard, type SideEffectDirectiveCardProps, TARGETING_CLASSES, defaultFailureWords, describeChangeValue, failureWords, fieldLabel, getDirectiveRenderer, itemChanges, itemRecordId, provisionalDirectiveSlug, readsApplyLedger, recordFallbackName, registerDirectiveRenderer, resetDirectiveRenderers, useRecordValues };
@@ -244,17 +244,22 @@ function classIcon(directiveClass, noun) {
244
244
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
245
245
  var FINISH_POLL_MS = 2e3;
246
246
  var FINISH_POLLS_MAX = 30;
247
+ function readsApplyLedger(host, directive) {
248
+ return !!host.applyState && !!host.confirm && directive.parsed.executes && directive.items.length > 0;
249
+ }
247
250
  function ApplyDirectiveButton({
248
251
  directive,
249
252
  label = "Apply",
250
253
  itemCount,
251
- onApplied
254
+ onApplied,
255
+ onPhase,
256
+ nothingToChange = false
252
257
  }) {
253
258
  const host = useDirectiveHost();
254
259
  const scope = useDirectiveScope();
255
260
  const conversationId = scope.conversationId || void 0;
256
261
  const items = directive.items;
257
- const readsLedger = !!host.applyState && !!host.confirm && directive.parsed.executes && items.length > 0;
262
+ const readsLedger = readsApplyLedger(host, directive);
258
263
  const initial = readsLedger ? { status: "checking" } : { status: "idle" };
259
264
  const [state, setState] = useState(initial);
260
265
  const [elapsed, setElapsed] = useState(0);
@@ -295,7 +300,8 @@ function ApplyDirectiveButton({
295
300
  failed: 0,
296
301
  message: answer.message?.trim() ? answer.message.trim() : null,
297
302
  records: answer.records ?? [],
298
- copies: Math.max(1, answer.copies ?? 1)
303
+ copies: Math.max(1, answer.copies ?? 1),
304
+ before: answer.before ?? null
299
305
  } : prev
300
306
  );
301
307
  } else if (answer.state === "in_flight" && polls < FINISH_POLLS_MAX) {
@@ -320,6 +326,15 @@ function ApplyDirectiveButton({
320
326
  if (timer) clearTimeout(timer);
321
327
  };
322
328
  }, [shellKey, read, report]);
329
+ const appliedClean = state.status === "applied" && state.failed === 0 ? state : null;
330
+ const appliedBefore = appliedClean ? appliedClean.before : null;
331
+ const phaseKey = state.status === "checking" || state.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
332
+ useEffect(() => {
333
+ if (!onPhase) return;
334
+ if (phaseKey === "pending") onPhase({ phase: "pending" });
335
+ else if (phaseKey === "open") onPhase({ phase: "open" });
336
+ else onPhase({ phase: "applied", before: appliedBefore });
337
+ }, [phaseKey, onPhase, appliedBefore]);
323
338
  useEffect(() => {
324
339
  if (state.status !== "applying") return;
325
340
  startedAt.current = Date.now();
@@ -334,7 +349,7 @@ function ApplyDirectiveButton({
334
349
  if (!confirm) return null;
335
350
  async function handleApply(again = false) {
336
351
  if (!confirm) return;
337
- const before = state;
352
+ const prior = state;
338
353
  const askedFor = identity;
339
354
  const stillHere = () => currentIdentity.current === askedFor;
340
355
  const priorCopies = state.status === "applied" ? state.copies : 0;
@@ -361,7 +376,7 @@ function ApplyDirectiveButton({
361
376
  if (!stillHere()) return;
362
377
  setAskingAgain(false);
363
378
  if (!ok) {
364
- setState(again ? before : { status: "idle" });
379
+ setState(again ? prior : { status: "idle" });
365
380
  return;
366
381
  }
367
382
  }
@@ -380,7 +395,10 @@ function ApplyDirectiveButton({
380
395
  message: result.message?.trim() ? result.message.trim() : null,
381
396
  records: result.records ?? [],
382
397
  // "Run again" made one more; a first apply (or a replay of one) is one.
383
- copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies)
398
+ copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies),
399
+ // "Run again" wrote the same values over themselves; what the FIRST
400
+ // apply replaced is still the change this block made.
401
+ before: again && prior.status === "applied" && prior.before ? prior.before : result.before ?? null
384
402
  });
385
403
  onApplied?.();
386
404
  } catch (error) {
@@ -482,15 +500,18 @@ function ApplyDirectiveButton({
482
500
  );
483
501
  }
484
502
  const destructive = directive.directiveClass === "delete";
503
+ const muted = nothingToChange && state.status === "idle";
485
504
  return /* @__PURE__ */ jsxs2("span", { className: "inline-flex items-center gap-2", children: [
505
+ muted ? /* @__PURE__ */ jsx3("span", { className: "text-xs text-muted-foreground", "data-apply-nothing": "", children: "Nothing to change" }) : null,
486
506
  /* @__PURE__ */ jsxs2(
487
507
  "button",
488
508
  {
489
509
  type: "button",
490
510
  onClick: () => void handleApply(),
491
511
  disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
492
- "data-apply-tone": destructive ? "destructive" : "default",
493
- 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"}`,
512
+ "data-apply-tone": muted ? "muted" : destructive ? "destructive" : "default",
513
+ title: muted ? "It sets no fields, so applying it changes nothing." : void 0,
514
+ 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"}`,
494
515
  children: [
495
516
  state.status === "applying" ? /* @__PURE__ */ jsx3(LoaderIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx3(PlayIcon, { className: "h-3.5 w-3.5" }),
496
517
  state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
@@ -608,12 +629,13 @@ function ItemRow({
608
629
  directive,
609
630
  item,
610
631
  index,
611
- total
632
+ total,
633
+ phase
612
634
  }) {
613
635
  const host = useDirectiveHost();
614
636
  const recordId = TARGETING_CLASSES.has(directive.directiveClass) ? itemRecordId(item) : null;
615
637
  if (recordId) {
616
- return /* @__PURE__ */ jsx4(TargetRow, { directive, item, recordId });
638
+ return /* @__PURE__ */ jsx4(TargetRow, { directive, item, recordId, index, phase });
617
639
  }
618
640
  const titleColumn = nounTitleColumn(directive.noun, host.nouns);
619
641
  const title = itemTitle(item, titleColumn, index, total);
@@ -700,16 +722,20 @@ function DirectiveChangeList({
700
722
  function TargetRow({
701
723
  directive,
702
724
  item,
703
- recordId
725
+ recordId,
726
+ index,
727
+ phase
704
728
  }) {
705
729
  const host = useDirectiveHost();
706
730
  const fallback = recordFallbackName(nounLabel2(directive.noun, host.nouns), recordId);
707
731
  const isUpdate = directive.directiveClass === "update";
708
- const current = useRecordValues(
709
- isUpdate ? host.readRecord : void 0,
710
- isUpdate ? { noun: directive.noun, id: recordId } : null
732
+ const readsLive = isUpdate && phase.phase === "open";
733
+ const live = useRecordValues(
734
+ readsLive ? host.readRecord : void 0,
735
+ readsLive ? { noun: directive.noun, id: recordId } : null
711
736
  );
712
- const changes = isUpdate ? itemChanges(item, current.values, { titleColumn: nounTitleColumn(directive.noun, host.nouns) }) : [];
737
+ const compareTo = phase.phase === "applied" ? phase.before?.[index] ?? null : readsLive ? live.values : null;
738
+ const changes = isUpdate ? itemChanges(item, compareTo, { titleColumn: nounTitleColumn(directive.noun, host.nouns) }) : [];
713
739
  const name = host.renderRecord ? host.renderRecord({ noun: directive.noun, id: recordId, fallback, context: "row" }) : /* @__PURE__ */ jsx4("span", { className: "truncate font-medium text-foreground", children: fallback });
714
740
  return /* @__PURE__ */ jsxs3("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
715
741
  /* @__PURE__ */ jsxs3("div", { className: "flex min-w-0 items-center gap-2", children: [
@@ -726,6 +752,10 @@ function SideEffectDirectiveCard({ directive }) {
726
752
  const total = items.length;
727
753
  const display = directiveDisplay(directive.directiveClass, directive.noun, host.nouns);
728
754
  const Icon = classIcon(directive.directiveClass, directive.noun);
755
+ const [phase, setPhase] = useState3(
756
+ () => readsApplyLedger(host, directive) ? { phase: "pending" } : { phase: "open" }
757
+ );
758
+ const nothingToChange = directive.directiveClass === "update" && total > 0 && items.every((item) => itemChanges(item).length === 0);
729
759
  const shown = expanded ? items : items.slice(0, ROWS_BEFORE_FOLD);
730
760
  const hidden = total - shown.length;
731
761
  return /* @__PURE__ */ jsxs3("div", { className: "my-3 rounded-md border border-border bg-card px-2.5 py-1.5", "data-directive": directive.slug, children: [
@@ -740,13 +770,21 @@ function SideEffectDirectiveCard({ directive }) {
740
770
  ] }),
741
771
  /* @__PURE__ */ jsxs3("div", { className: "ml-auto flex items-center gap-1", children: [
742
772
  host.renderCopy?.({ label: display.title, value: directive.shell, size: "icon" }),
743
- /* @__PURE__ */ jsx4(ApplyDirectiveButton, { directive, itemCount: total })
773
+ /* @__PURE__ */ jsx4(
774
+ ApplyDirectiveButton,
775
+ {
776
+ directive,
777
+ itemCount: total,
778
+ onPhase: setPhase,
779
+ nothingToChange
780
+ }
781
+ )
744
782
  ] })
745
783
  ] }),
746
784
  total === 0 ? (
747
785
  // Stated, never silent: an empty batch is a real (and suspicious) state.
748
786
  /* @__PURE__ */ jsx4("p", { className: "mt-1 text-xs text-muted-foreground", children: "No items \u2014 nothing would be written." })
749
- ) : /* @__PURE__ */ jsx4("div", { className: "mt-0.5 divide-y divide-border/60", children: shown.map((item, i) => /* @__PURE__ */ jsx4(ItemRow, { directive, item, index: i, total }, i)) }),
787
+ ) : /* @__PURE__ */ jsx4("div", { className: "mt-0.5 divide-y divide-border/60", children: shown.map((item, i) => /* @__PURE__ */ jsx4(ItemRow, { directive, item, index: i, total, phase }, i)) }),
750
788
  hidden > 0 || expanded ? /* @__PURE__ */ jsxs3(
751
789
  "button",
752
790
  {
@@ -889,6 +927,7 @@ export {
889
927
  itemChanges,
890
928
  itemRecordId,
891
929
  provisionalDirectiveSlug,
930
+ readsApplyLedger,
892
931
  recordFallbackName,
893
932
  registerDirectiveRenderer,
894
933
  resetDirectiveRenderers,