@ai-matrx/content-ir-react 0.18.0 → 0.19.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
@@ -134,7 +134,7 @@ var import_content_ir6 = require("@ai-matrx/content-ir");
134
134
  var import_content_ir2 = require("@ai-matrx/content-ir");
135
135
 
136
136
  // directives/ApplyDirectiveButton.tsx
137
- var import_react4 = require("react");
137
+ var import_react5 = require("react");
138
138
  var import_content_ir = require("@ai-matrx/content-ir");
139
139
 
140
140
  // directives/item-changes.ts
@@ -210,6 +210,64 @@ function itemChanges(item, current, options = {}) {
210
210
  return out;
211
211
  }
212
212
 
213
+ // directives/record-values.ts
214
+ var import_react4 = require("react");
215
+ var revisions = /* @__PURE__ */ new Map();
216
+ var listeners = /* @__PURE__ */ new Set();
217
+ function recordRevision(id) {
218
+ return revisions.get(id) ?? 0;
219
+ }
220
+ function noteRecordChanged(id) {
221
+ if (!id) return;
222
+ revisions.set(id, recordRevision(id) + 1);
223
+ for (const listener of listeners) listener();
224
+ }
225
+ function subscribeRevisions(listener) {
226
+ listeners.add(listener);
227
+ return () => {
228
+ listeners.delete(listener);
229
+ };
230
+ }
231
+ function useRecordRevision(id) {
232
+ return (0, import_react4.useSyncExternalStore)(
233
+ subscribeRevisions,
234
+ () => recordRevision(id),
235
+ () => 0
236
+ );
237
+ }
238
+ function useRecordValues(read, ref) {
239
+ const revision = useRecordRevision(ref?.id ?? "");
240
+ const key = ref && read ? `${ref.noun}:${ref.id}#${revision}` : "";
241
+ const [result, setResult] = (0, import_react4.useState)({
242
+ key: "",
243
+ values: null,
244
+ status: "idle"
245
+ });
246
+ (0, import_react4.useEffect)(() => {
247
+ if (!key || !read) return;
248
+ const split = key.indexOf(":");
249
+ const target = {
250
+ noun: key.slice(0, split),
251
+ id: key.slice(split + 1, key.lastIndexOf("#"))
252
+ };
253
+ let live = true;
254
+ setResult({ key, values: null, status: "loading" });
255
+ read(target).then((values) => {
256
+ if (!live) return;
257
+ setResult({ key, values, status: values ? "ready" : "unavailable" });
258
+ }).catch(() => {
259
+ if (!live) return;
260
+ setResult({ key, values: null, status: "unavailable" });
261
+ });
262
+ return () => {
263
+ live = false;
264
+ };
265
+ }, [key, read]);
266
+ if (!key) return { values: null, status: "idle" };
267
+ if (result.key !== key) return { values: null, status: "loading" };
268
+ return { values: result.values, status: result.status };
269
+ }
270
+
213
271
  // directives/failure-words.ts
214
272
  var PAST = {
215
273
  create: "created",
@@ -329,6 +387,41 @@ function classIcon(directiveClass, noun) {
329
387
 
330
388
  // directives/ApplyDirectiveButton.tsx
331
389
  var import_jsx_runtime4 = require("react/jsx-runtime");
390
+ function announceChangedRecords(directive, items) {
391
+ if (!TARGETING_CLASSES.has(directive.directiveClass)) return;
392
+ for (const item of items) {
393
+ const id = itemRecordId(item);
394
+ if (id) noteRecordChanged(id);
395
+ }
396
+ }
397
+ var applyOutcomes = /* @__PURE__ */ new WeakMap();
398
+ var EMPTY_AUTHORITY = {};
399
+ var outcomeListeners = /* @__PURE__ */ new Set();
400
+ function outcomesOf(host) {
401
+ let held = applyOutcomes.get(host);
402
+ if (!held) {
403
+ held = /* @__PURE__ */ new Map();
404
+ applyOutcomes.set(host, held);
405
+ }
406
+ return held;
407
+ }
408
+ function publishOutcome(host, identity, outcome) {
409
+ outcomesOf(host).set(identity, outcome);
410
+ for (const listener of outcomeListeners) listener();
411
+ }
412
+ function subscribeOutcomes(listener) {
413
+ outcomeListeners.add(listener);
414
+ return () => {
415
+ outcomeListeners.delete(listener);
416
+ };
417
+ }
418
+ function useApplyOutcome(host, identity) {
419
+ return (0, import_react5.useSyncExternalStore)(
420
+ subscribeOutcomes,
421
+ () => applyOutcomes.get(host)?.get(identity) ?? null,
422
+ () => null
423
+ );
424
+ }
332
425
  var FINISH_POLL_MS = 2e3;
333
426
  var FINISH_POLLS_MAX = 30;
334
427
  function readsApplyLedger(host, directive) {
@@ -348,27 +441,29 @@ function ApplyDirectiveButton({
348
441
  const items = directive.items;
349
442
  const readsLedger = readsApplyLedger(host, directive);
350
443
  const initial = readsLedger ? { status: "checking" } : { status: "idle" };
351
- const [state, setState] = (0, import_react4.useState)(initial);
352
- const [elapsed, setElapsed] = (0, import_react4.useState)(0);
353
- const [askingAgain, setAskingAgain] = (0, import_react4.useState)(false);
354
- const startedAt = (0, import_react4.useRef)(null);
444
+ const [state, setState] = (0, import_react5.useState)(initial);
445
+ const [elapsed, setElapsed] = (0, import_react5.useState)(0);
446
+ const [askingAgain, setAskingAgain] = (0, import_react5.useState)(false);
447
+ const startedAt = (0, import_react5.useRef)(null);
355
448
  const identity = JSON.stringify([directive.slug, items, conversationId ?? null]);
356
- const [stateFor, setStateFor] = (0, import_react4.useState)(identity);
449
+ const [stateFor, setStateFor] = (0, import_react5.useState)(identity);
357
450
  if (stateFor !== identity) {
358
451
  setStateFor(identity);
359
452
  setState(initial);
360
453
  setAskingAgain(false);
361
454
  setElapsed(0);
362
455
  }
363
- const currentIdentity = (0, import_react4.useRef)(identity);
364
- (0, import_react4.useEffect)(() => {
456
+ const shared = useApplyOutcome(host.confirm ?? EMPTY_AUTHORITY, identity);
457
+ const view = state.status === "asking" || !shared ? state : shared;
458
+ const currentIdentity = (0, import_react5.useRef)(identity);
459
+ (0, import_react5.useEffect)(() => {
365
460
  currentIdentity.current = identity;
366
461
  }, [identity]);
367
462
  const nounText = (0, import_content_ir.nounLabel)(directive.noun, host.nouns);
368
463
  const shellKey = readsLedger ? JSON.stringify([directive.slug, items, conversationId ?? null]) : "";
369
464
  const read = host.applyState;
370
465
  const report = host.reportError;
371
- (0, import_react4.useEffect)(() => {
466
+ (0, import_react5.useEffect)(() => {
372
467
  if (!shellKey || !read) return;
373
468
  const [slug, shellItems, inConversation] = JSON.parse(shellKey);
374
469
  const shell = { __kind: slug, items: shellItems };
@@ -413,33 +508,34 @@ function ApplyDirectiveButton({
413
508
  if (timer) clearTimeout(timer);
414
509
  };
415
510
  }, [shellKey, read, report]);
416
- const appliedClean = state.status === "applied" && state.failed === 0 ? state : null;
511
+ const appliedClean = view.status === "applied" && view.failed === 0 ? view : null;
417
512
  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)(() => {
513
+ const phaseKey = view.status === "checking" || view.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
514
+ (0, import_react5.useEffect)(() => {
420
515
  if (!onPhase) return;
421
516
  if (phaseKey === "pending") onPhase({ phase: "pending" });
422
517
  else if (phaseKey === "open") onPhase({ phase: "open" });
423
518
  else onPhase({ phase: "applied", before: appliedBefore });
424
519
  }, [phaseKey, onPhase, appliedBefore]);
425
- (0, import_react4.useEffect)(() => {
426
- if (state.status !== "applying") return;
427
- startedAt.current = Date.now();
520
+ const applyingSince = view.status === "applying" ? view.startedAt : null;
521
+ (0, import_react5.useEffect)(() => {
522
+ if (applyingSince === null) return;
523
+ startedAt.current = applyingSince;
428
524
  const id = setInterval(
429
525
  () => setElapsed(Math.round((Date.now() - (startedAt.current ?? 0)) / 1e3)),
430
526
  1e3
431
527
  );
432
528
  return () => clearInterval(id);
433
- }, [state.status]);
529
+ }, [applyingSince]);
434
530
  if (!directive.parsed.executes) return null;
435
531
  const confirm = host.confirm;
436
532
  if (!confirm) return null;
437
533
  async function handleApply(again = false) {
438
534
  if (!confirm) return;
439
- const prior = state;
535
+ const prior = view;
440
536
  const askedFor = identity;
441
537
  const stillHere = () => currentIdentity.current === askedFor;
442
- const priorCopies = state.status === "applied" ? state.copies : 0;
538
+ const priorCopies = view.status === "applied" ? view.copies : 0;
443
539
  if (host.ask) {
444
540
  if (again) setAskingAgain(true);
445
541
  else setState({ status: "asking" });
@@ -468,14 +564,20 @@ function ApplyDirectiveButton({
468
564
  }
469
565
  }
470
566
  setElapsed(0);
471
- setState({ status: "applying" });
567
+ setState((prev) => prev.status === "asking" ? { status: "idle" } : prev);
568
+ publishOutcome(confirm, askedFor, { status: "applying", startedAt: Date.now() });
472
569
  try {
473
570
  const shell = { __kind: directive.slug, items };
474
571
  if (conversationId) shell.conversationId = conversationId;
475
572
  if (again) shell.force = true;
476
- const result = await confirm(shell);
477
- if (!stillHere()) return;
478
- setState({
573
+ let result;
574
+ try {
575
+ result = await confirm(shell);
576
+ } finally {
577
+ announceChangedRecords(directive, items);
578
+ }
579
+ for (const record of result.records ?? []) noteRecordChanged(record.id);
580
+ publishOutcome(confirm, askedFor, {
479
581
  status: "applied",
480
582
  applied: result.applied,
481
583
  failed: result.failed,
@@ -487,20 +589,19 @@ function ApplyDirectiveButton({
487
589
  // apply replaced is still the change this block made.
488
590
  before: again && prior.status === "applied" && prior.before ? prior.before : result.before ?? null
489
591
  });
490
- onApplied?.();
592
+ if (stillHere()) onApplied?.();
491
593
  } catch (error) {
492
- if (!stillHere()) return;
493
- setState({
594
+ publishOutcome(confirm, askedFor, {
494
595
  status: "error",
495
596
  message: error instanceof Error && error.message ? error.message : "That couldn't be applied just now. Please try again."
496
597
  });
497
598
  }
498
599
  }
499
- if (state.status === "applied") {
500
- const bad = state.failed > 0;
600
+ if (view.status === "applied") {
601
+ const bad = view.failed > 0;
501
602
  const trashed = directive.directiveClass === "delete";
502
603
  const label2 = nounText;
503
- const records = host.renderRecord ? state.records : [];
604
+ const records = host.renderRecord ? view.records : [];
504
605
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
505
606
  "span",
506
607
  {
@@ -514,26 +615,26 @@ function ApplyDirectiveButton({
514
615
  {
515
616
  words: failureWords(
516
617
  {
517
- raw: state.message ?? `${state.failed} failed`,
618
+ raw: view.message ?? `${view.failed} failed`,
518
619
  directiveClass: directive.directiveClass,
519
620
  noun: directive.noun,
520
621
  nounLabel: nounText,
521
- failed: state.failed,
522
- total: state.applied + state.failed
622
+ failed: view.failed,
623
+ total: view.applied + view.failed
523
624
  },
524
625
  host.explainFailure
525
626
  ),
526
- raw: state.message
627
+ raw: view.message
527
628
  }
528
- ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "min-w-0", children: state.message ?? `Applied ${state.applied}` })
629
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "min-w-0", children: view.message ?? `Applied ${view.applied}` })
529
630
  ] }),
530
- state.copies > 1 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
631
+ view.copies > 1 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
531
632
  "span",
532
633
  {
533
634
  className: "shrink-0 text-muted-foreground",
534
- "data-apply-copies": state.copies,
635
+ "data-apply-copies": view.copies,
535
636
  title: records.length > 0 ? directive.directiveClass === "create" ? "Links to the latest copy." : "Links to the latest run." : void 0,
536
- children: directive.directiveClass === "create" ? `${state.copies} copies` : `Ran ${state.copies} times`
637
+ children: directive.directiveClass === "create" ? `${view.copies} copies` : `Ran ${view.copies} times`
537
638
  }
538
639
  ) : null,
539
640
  records.map((record) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
@@ -560,7 +661,7 @@ function ApplyDirectiveButton({
560
661
  }
561
662
  );
562
663
  }
563
- if (state.status === "checking") {
664
+ if (view.status === "checking") {
564
665
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
565
666
  "span",
566
667
  {
@@ -572,7 +673,7 @@ function ApplyDirectiveButton({
572
673
  }
573
674
  );
574
675
  }
575
- if (state.status === "finishing") {
676
+ if (view.status === "finishing") {
576
677
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
577
678
  "span",
578
679
  {
@@ -587,7 +688,7 @@ function ApplyDirectiveButton({
587
688
  );
588
689
  }
589
690
  const destructive = directive.directiveClass === "delete";
590
- const muted = nothingToChange && state.status === "idle";
691
+ const muted = nothingToChange && view.status === "idle";
591
692
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "inline-flex items-center gap-2", children: [
592
693
  muted ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-xs text-muted-foreground", "data-apply-nothing": "", children: "Nothing to change" }) : null,
593
694
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
@@ -595,17 +696,17 @@ function ApplyDirectiveButton({
595
696
  {
596
697
  type: "button",
597
698
  onClick: () => void handleApply(),
598
- disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
699
+ disabled: view.status === "applying" || view.status === "asking" || items.length === 0,
599
700
  "data-apply-tone": muted ? "muted" : destructive ? "destructive" : "default",
600
701
  title: muted ? "It sets no fields, so applying it changes nothing." : void 0,
601
702
  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"}`,
602
703
  children: [
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" }),
604
- state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
704
+ view.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" }),
705
+ view.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
605
706
  ]
606
707
  }
607
708
  ),
608
- state.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
709
+ view.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
609
710
  "span",
610
711
  {
611
712
  className: "inline-flex max-w-sm items-start gap-1 text-xs text-destructive",
@@ -617,14 +718,14 @@ function ApplyDirectiveButton({
617
718
  {
618
719
  words: failureWords(
619
720
  {
620
- raw: state.message,
721
+ raw: view.message,
621
722
  directiveClass: directive.directiveClass,
622
723
  noun: directive.noun,
623
724
  nounLabel: nounText
624
725
  },
625
726
  host.explainFailure
626
727
  ),
627
- raw: state.message
728
+ raw: view.message
628
729
  }
629
730
  )
630
731
  ]
@@ -636,7 +737,7 @@ function FailureNotice({
636
737
  words,
637
738
  raw
638
739
  }) {
639
- const [open, setOpen] = (0, import_react4.useState)(false);
740
+ const [open, setOpen] = (0, import_react5.useState)(false);
640
741
  const detail = raw?.trim() ?? "";
641
742
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex min-w-0 flex-col gap-0.5", "data-apply-failure": true, children: [
642
743
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "min-w-0", children: [
@@ -747,39 +848,6 @@ function getDirectiveRenderer(directive) {
747
848
  // directives/SideEffectDirectiveCard.tsx
748
849
  var import_react6 = require("react");
749
850
  var import_content_ir5 = require("@ai-matrx/content-ir");
750
-
751
- // directives/record-values.ts
752
- var import_react5 = require("react");
753
- function useRecordValues(read, ref) {
754
- const key = ref && read ? `${ref.noun}:${ref.id}` : "";
755
- const [result, setResult] = (0, import_react5.useState)({
756
- key: "",
757
- values: null,
758
- status: "idle"
759
- });
760
- (0, import_react5.useEffect)(() => {
761
- if (!key || !read) return;
762
- const split = key.indexOf(":");
763
- const target = { noun: key.slice(0, split), id: key.slice(split + 1) };
764
- let live = true;
765
- setResult({ key, values: null, status: "loading" });
766
- read(target).then((values) => {
767
- if (!live) return;
768
- setResult({ key, values, status: values ? "ready" : "unavailable" });
769
- }).catch(() => {
770
- if (!live) return;
771
- setResult({ key, values: null, status: "unavailable" });
772
- });
773
- return () => {
774
- live = false;
775
- };
776
- }, [key, read]);
777
- if (!key) return { values: null, status: "idle" };
778
- if (result.key !== key) return { values: null, status: "loading" };
779
- return { values: result.values, status: result.status };
780
- }
781
-
782
- // directives/SideEffectDirectiveCard.tsx
783
851
  var import_jsx_runtime7 = require("react/jsx-runtime");
784
852
  var ROWS_BEFORE_FOLD = 3;
785
853
  function ItemRow({