@ai-matrx/content-ir-react 0.17.1 → 0.18.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.d.cts CHANGED
@@ -1,9 +1,9 @@
1
- import { c as KindDefinitionSource, C as ContentIrHost } from './host-types-CKu4ap5K.cjs';
1
+ import { c as KindDefinitionSource, C as ContentIrHost } from './host-types-YP_l_Q_t.cjs';
2
2
  import { d as ComponentResolutionSource } from './component-resolution.types-DwKSF64x.cjs';
3
3
  import * as react from 'react';
4
4
  import react__default, { ReactNode } from 'react';
5
5
  import '@ai-matrx/content-ir';
6
- import './directive-host-BOML4BQ7.cjs';
6
+ import './directive-host-Bw2_PrgL.cjs';
7
7
 
8
8
  /**
9
9
  * The two registries this hook watches. Passed explicitly by a host whose
package/dist/react.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { c as KindDefinitionSource, C as ContentIrHost } from './host-types-CQ2oIzUy.js';
1
+ import { c as KindDefinitionSource, C as ContentIrHost } from './host-types-eSjFmXyF.js';
2
2
  import { d as ComponentResolutionSource } from './component-resolution.types-DwKSF64x.js';
3
3
  import * as react from 'react';
4
4
  import react__default, { ReactNode } from 'react';
5
5
  import '@ai-matrx/content-ir';
6
- import './directive-host-BOML4BQ7.js';
6
+ import './directive-host-Bw2_PrgL.js';
7
7
 
8
8
  /**
9
9
  * The two registries this hook watches. Passed explicitly by a host whose
package/dist/react.js CHANGED
@@ -94,7 +94,7 @@ import { looksLikeDirectiveHead, tryDecodeDirectiveContent } from "@ai-matrx/con
94
94
  import { directiveDisplay } from "@ai-matrx/content-ir";
95
95
 
96
96
  // directives/ApplyDirectiveButton.tsx
97
- import { useEffect, useRef, useState } from "react";
97
+ import { useEffect, useRef, useState, useSyncExternalStore as useSyncExternalStore2 } from "react";
98
98
  import { nounLabel } from "@ai-matrx/content-ir";
99
99
 
100
100
  // directives/item-changes.ts
@@ -289,19 +289,52 @@ function classIcon(directiveClass, noun) {
289
289
 
290
290
  // directives/ApplyDirectiveButton.tsx
291
291
  import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
292
+ var applyOutcomes = /* @__PURE__ */ new WeakMap();
293
+ var EMPTY_AUTHORITY = {};
294
+ var outcomeListeners = /* @__PURE__ */ new Set();
295
+ function outcomesOf(host) {
296
+ let held = applyOutcomes.get(host);
297
+ if (!held) {
298
+ held = /* @__PURE__ */ new Map();
299
+ applyOutcomes.set(host, held);
300
+ }
301
+ return held;
302
+ }
303
+ function publishOutcome(host, identity, outcome) {
304
+ outcomesOf(host).set(identity, outcome);
305
+ for (const listener of outcomeListeners) listener();
306
+ }
307
+ function subscribeOutcomes(listener) {
308
+ outcomeListeners.add(listener);
309
+ return () => {
310
+ outcomeListeners.delete(listener);
311
+ };
312
+ }
313
+ function useApplyOutcome(host, identity) {
314
+ return useSyncExternalStore2(
315
+ subscribeOutcomes,
316
+ () => applyOutcomes.get(host)?.get(identity) ?? null,
317
+ () => null
318
+ );
319
+ }
292
320
  var FINISH_POLL_MS = 2e3;
293
321
  var FINISH_POLLS_MAX = 30;
322
+ function readsApplyLedger(host, directive) {
323
+ return !!host.applyState && !!host.confirm && directive.parsed.executes && directive.items.length > 0;
324
+ }
294
325
  function ApplyDirectiveButton({
295
326
  directive,
296
327
  label = "Apply",
297
328
  itemCount,
298
- onApplied
329
+ onApplied,
330
+ onPhase,
331
+ nothingToChange = false
299
332
  }) {
300
333
  const host = useDirectiveHost();
301
334
  const scope = useDirectiveScope();
302
335
  const conversationId = scope.conversationId || void 0;
303
336
  const items = directive.items;
304
- const readsLedger = !!host.applyState && !!host.confirm && directive.parsed.executes && items.length > 0;
337
+ const readsLedger = readsApplyLedger(host, directive);
305
338
  const initial = readsLedger ? { status: "checking" } : { status: "idle" };
306
339
  const [state, setState] = useState(initial);
307
340
  const [elapsed, setElapsed] = useState(0);
@@ -315,6 +348,8 @@ function ApplyDirectiveButton({
315
348
  setAskingAgain(false);
316
349
  setElapsed(0);
317
350
  }
351
+ const shared = useApplyOutcome(host.confirm ?? EMPTY_AUTHORITY, identity);
352
+ const view = state.status === "asking" || !shared ? state : shared;
318
353
  const currentIdentity = useRef(identity);
319
354
  useEffect(() => {
320
355
  currentIdentity.current = identity;
@@ -342,7 +377,8 @@ function ApplyDirectiveButton({
342
377
  failed: 0,
343
378
  message: answer.message?.trim() ? answer.message.trim() : null,
344
379
  records: answer.records ?? [],
345
- copies: Math.max(1, answer.copies ?? 1)
380
+ copies: Math.max(1, answer.copies ?? 1),
381
+ before: answer.before ?? null
346
382
  } : prev
347
383
  );
348
384
  } else if (answer.state === "in_flight" && polls < FINISH_POLLS_MAX) {
@@ -367,24 +403,34 @@ function ApplyDirectiveButton({
367
403
  if (timer) clearTimeout(timer);
368
404
  };
369
405
  }, [shellKey, read, report]);
406
+ const appliedClean = view.status === "applied" && view.failed === 0 ? view : null;
407
+ const appliedBefore = appliedClean ? appliedClean.before : null;
408
+ const phaseKey = view.status === "checking" || view.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
409
+ useEffect(() => {
410
+ if (!onPhase) return;
411
+ if (phaseKey === "pending") onPhase({ phase: "pending" });
412
+ else if (phaseKey === "open") onPhase({ phase: "open" });
413
+ else onPhase({ phase: "applied", before: appliedBefore });
414
+ }, [phaseKey, onPhase, appliedBefore]);
415
+ const applyingSince = view.status === "applying" ? view.startedAt : null;
370
416
  useEffect(() => {
371
- if (state.status !== "applying") return;
372
- startedAt.current = Date.now();
417
+ if (applyingSince === null) return;
418
+ startedAt.current = applyingSince;
373
419
  const id = setInterval(
374
420
  () => setElapsed(Math.round((Date.now() - (startedAt.current ?? 0)) / 1e3)),
375
421
  1e3
376
422
  );
377
423
  return () => clearInterval(id);
378
- }, [state.status]);
424
+ }, [applyingSince]);
379
425
  if (!directive.parsed.executes) return null;
380
426
  const confirm = host.confirm;
381
427
  if (!confirm) return null;
382
428
  async function handleApply(again = false) {
383
429
  if (!confirm) return;
384
- const before = state;
430
+ const prior = view;
385
431
  const askedFor = identity;
386
432
  const stillHere = () => currentIdentity.current === askedFor;
387
- const priorCopies = state.status === "applied" ? state.copies : 0;
433
+ const priorCopies = view.status === "applied" ? view.copies : 0;
388
434
  if (host.ask) {
389
435
  if (again) setAskingAgain(true);
390
436
  else setState({ status: "asking" });
@@ -408,41 +454,43 @@ function ApplyDirectiveButton({
408
454
  if (!stillHere()) return;
409
455
  setAskingAgain(false);
410
456
  if (!ok) {
411
- setState(again ? before : { status: "idle" });
457
+ setState(again ? prior : { status: "idle" });
412
458
  return;
413
459
  }
414
460
  }
415
461
  setElapsed(0);
416
- setState({ status: "applying" });
462
+ setState((prev) => prev.status === "asking" ? { status: "idle" } : prev);
463
+ publishOutcome(confirm, askedFor, { status: "applying", startedAt: Date.now() });
417
464
  try {
418
465
  const shell = { __kind: directive.slug, items };
419
466
  if (conversationId) shell.conversationId = conversationId;
420
467
  if (again) shell.force = true;
421
468
  const result = await confirm(shell);
422
- if (!stillHere()) return;
423
- setState({
469
+ publishOutcome(confirm, askedFor, {
424
470
  status: "applied",
425
471
  applied: result.applied,
426
472
  failed: result.failed,
427
473
  message: result.message?.trim() ? result.message.trim() : null,
428
474
  records: result.records ?? [],
429
475
  // "Run again" made one more; a first apply (or a replay of one) is one.
430
- copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies)
476
+ copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies),
477
+ // "Run again" wrote the same values over themselves; what the FIRST
478
+ // apply replaced is still the change this block made.
479
+ before: again && prior.status === "applied" && prior.before ? prior.before : result.before ?? null
431
480
  });
432
- onApplied?.();
481
+ if (stillHere()) onApplied?.();
433
482
  } catch (error) {
434
- if (!stillHere()) return;
435
- setState({
483
+ publishOutcome(confirm, askedFor, {
436
484
  status: "error",
437
485
  message: error instanceof Error && error.message ? error.message : "That couldn't be applied just now. Please try again."
438
486
  });
439
487
  }
440
488
  }
441
- if (state.status === "applied") {
442
- const bad = state.failed > 0;
489
+ if (view.status === "applied") {
490
+ const bad = view.failed > 0;
443
491
  const trashed = directive.directiveClass === "delete";
444
492
  const label2 = nounText;
445
- const records = host.renderRecord ? state.records : [];
493
+ const records = host.renderRecord ? view.records : [];
446
494
  return /* @__PURE__ */ jsxs2(
447
495
  "span",
448
496
  {
@@ -456,26 +504,26 @@ function ApplyDirectiveButton({
456
504
  {
457
505
  words: failureWords(
458
506
  {
459
- raw: state.message ?? `${state.failed} failed`,
507
+ raw: view.message ?? `${view.failed} failed`,
460
508
  directiveClass: directive.directiveClass,
461
509
  noun: directive.noun,
462
510
  nounLabel: nounText,
463
- failed: state.failed,
464
- total: state.applied + state.failed
511
+ failed: view.failed,
512
+ total: view.applied + view.failed
465
513
  },
466
514
  host.explainFailure
467
515
  ),
468
- raw: state.message
516
+ raw: view.message
469
517
  }
470
- ) : /* @__PURE__ */ jsx4("span", { className: "min-w-0", children: state.message ?? `Applied ${state.applied}` })
518
+ ) : /* @__PURE__ */ jsx4("span", { className: "min-w-0", children: view.message ?? `Applied ${view.applied}` })
471
519
  ] }),
472
- state.copies > 1 ? /* @__PURE__ */ jsx4(
520
+ view.copies > 1 ? /* @__PURE__ */ jsx4(
473
521
  "span",
474
522
  {
475
523
  className: "shrink-0 text-muted-foreground",
476
- "data-apply-copies": state.copies,
524
+ "data-apply-copies": view.copies,
477
525
  title: records.length > 0 ? directive.directiveClass === "create" ? "Links to the latest copy." : "Links to the latest run." : void 0,
478
- children: directive.directiveClass === "create" ? `${state.copies} copies` : `Ran ${state.copies} times`
526
+ children: directive.directiveClass === "create" ? `${view.copies} copies` : `Ran ${view.copies} times`
479
527
  }
480
528
  ) : null,
481
529
  records.map((record) => /* @__PURE__ */ jsx4("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
@@ -502,7 +550,7 @@ function ApplyDirectiveButton({
502
550
  }
503
551
  );
504
552
  }
505
- if (state.status === "checking") {
553
+ if (view.status === "checking") {
506
554
  return /* @__PURE__ */ jsx4(
507
555
  "span",
508
556
  {
@@ -514,7 +562,7 @@ function ApplyDirectiveButton({
514
562
  }
515
563
  );
516
564
  }
517
- if (state.status === "finishing") {
565
+ if (view.status === "finishing") {
518
566
  return /* @__PURE__ */ jsxs2(
519
567
  "span",
520
568
  {
@@ -529,22 +577,25 @@ function ApplyDirectiveButton({
529
577
  );
530
578
  }
531
579
  const destructive = directive.directiveClass === "delete";
580
+ const muted = nothingToChange && view.status === "idle";
532
581
  return /* @__PURE__ */ jsxs2("span", { className: "inline-flex items-center gap-2", children: [
582
+ muted ? /* @__PURE__ */ jsx4("span", { className: "text-xs text-muted-foreground", "data-apply-nothing": "", children: "Nothing to change" }) : null,
533
583
  /* @__PURE__ */ jsxs2(
534
584
  "button",
535
585
  {
536
586
  type: "button",
537
587
  onClick: () => void handleApply(),
538
- disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
539
- "data-apply-tone": destructive ? "destructive" : "default",
540
- 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"}`,
588
+ disabled: view.status === "applying" || view.status === "asking" || items.length === 0,
589
+ "data-apply-tone": muted ? "muted" : destructive ? "destructive" : "default",
590
+ title: muted ? "It sets no fields, so applying it changes nothing." : void 0,
591
+ 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"}`,
541
592
  children: [
542
- 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" }),
543
- state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
593
+ view.status === "applying" ? /* @__PURE__ */ jsx4(LoaderIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx4(PlayIcon, { className: "h-3.5 w-3.5" }),
594
+ view.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
544
595
  ]
545
596
  }
546
597
  ),
547
- state.status === "error" ? /* @__PURE__ */ jsxs2(
598
+ view.status === "error" ? /* @__PURE__ */ jsxs2(
548
599
  "span",
549
600
  {
550
601
  className: "inline-flex max-w-sm items-start gap-1 text-xs text-destructive",
@@ -556,14 +607,14 @@ function ApplyDirectiveButton({
556
607
  {
557
608
  words: failureWords(
558
609
  {
559
- raw: state.message,
610
+ raw: view.message,
560
611
  directiveClass: directive.directiveClass,
561
612
  noun: directive.noun,
562
613
  nounLabel: nounText
563
614
  },
564
615
  host.explainFailure
565
616
  ),
566
- raw: state.message
617
+ raw: view.message
567
618
  }
568
619
  )
569
620
  ]
@@ -735,12 +786,13 @@ function ItemRow({
735
786
  directive,
736
787
  item,
737
788
  index,
738
- total
789
+ total,
790
+ phase
739
791
  }) {
740
792
  const host = useDirectiveHost();
741
793
  const recordId = TARGETING_CLASSES.has(directive.directiveClass) ? itemRecordId(item) : null;
742
794
  if (recordId) {
743
- return /* @__PURE__ */ jsx7(TargetRow, { directive, item, recordId });
795
+ return /* @__PURE__ */ jsx7(TargetRow, { directive, item, recordId, index, phase });
744
796
  }
745
797
  const titleColumn = nounTitleColumn(directive.noun, host.nouns);
746
798
  const title = itemTitle(item, titleColumn, index, total);
@@ -827,16 +879,20 @@ function DirectiveChangeList({
827
879
  function TargetRow({
828
880
  directive,
829
881
  item,
830
- recordId
882
+ recordId,
883
+ index,
884
+ phase
831
885
  }) {
832
886
  const host = useDirectiveHost();
833
887
  const fallback = recordFallbackName(nounLabel2(directive.noun, host.nouns), recordId);
834
888
  const isUpdate = directive.directiveClass === "update";
835
- const current = useRecordValues(
836
- isUpdate ? host.readRecord : void 0,
837
- 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
838
893
  );
839
- const changes = isUpdate ? itemChanges(item, current.values, { titleColumn: 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: nounTitleColumn(directive.noun, host.nouns) }) : [];
840
896
  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 });
841
897
  return /* @__PURE__ */ jsxs5("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
842
898
  /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 items-center gap-2", children: [
@@ -853,6 +909,10 @@ function SideEffectDirectiveCard({ directive }) {
853
909
  const total = items.length;
854
910
  const display = directiveDisplay3(directive.directiveClass, directive.noun, host.nouns);
855
911
  const Icon = classIcon(directive.directiveClass, directive.noun);
912
+ const [phase, setPhase] = useState3(
913
+ () => readsApplyLedger(host, directive) ? { phase: "pending" } : { phase: "open" }
914
+ );
915
+ const nothingToChange = directive.directiveClass === "update" && total > 0 && items.every((item) => itemChanges(item).length === 0);
856
916
  const shown = expanded ? items : items.slice(0, ROWS_BEFORE_FOLD);
857
917
  const hidden = total - shown.length;
858
918
  return /* @__PURE__ */ jsxs5("div", { className: "my-3 rounded-md border border-border bg-card px-2.5 py-1.5", "data-directive": directive.slug, children: [
@@ -867,13 +927,21 @@ function SideEffectDirectiveCard({ directive }) {
867
927
  ] }),
868
928
  /* @__PURE__ */ jsxs5("div", { className: "ml-auto flex items-center gap-1", children: [
869
929
  host.renderCopy?.({ label: display.title, value: directive.shell, size: "icon" }),
870
- /* @__PURE__ */ jsx7(ApplyDirectiveButton, { directive, itemCount: total })
930
+ /* @__PURE__ */ jsx7(
931
+ ApplyDirectiveButton,
932
+ {
933
+ directive,
934
+ itemCount: total,
935
+ onPhase: setPhase,
936
+ nothingToChange
937
+ }
938
+ )
871
939
  ] })
872
940
  ] }),
873
941
  total === 0 ? (
874
942
  // Stated, never silent: an empty batch is a real (and suspicious) state.
875
943
  /* @__PURE__ */ jsx7("p", { className: "mt-1 text-xs text-muted-foreground", children: "No items \u2014 nothing would be written." })
876
- ) : /* @__PURE__ */ jsx7("div", { className: "mt-0.5 divide-y divide-border/60", children: shown.map((item, i) => /* @__PURE__ */ jsx7(ItemRow, { directive, item, index: i, total }, i)) }),
944
+ ) : /* @__PURE__ */ jsx7("div", { className: "mt-0.5 divide-y divide-border/60", children: shown.map((item, i) => /* @__PURE__ */ jsx7(ItemRow, { directive, item, index: i, total, phase }, i)) }),
877
945
  hidden > 0 || expanded ? /* @__PURE__ */ jsxs5(
878
946
  "button",
879
947
  {