@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.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 as useEffect2, useRef, useState as useState2, useSyncExternalStore as useSyncExternalStore3 } from "react";
98
98
  import { nounLabel } from "@ai-matrx/content-ir";
99
99
 
100
100
  // directives/item-changes.ts
@@ -170,6 +170,64 @@ function itemChanges(item, current, options = {}) {
170
170
  return out;
171
171
  }
172
172
 
173
+ // directives/record-values.ts
174
+ import { useEffect, useState, useSyncExternalStore as useSyncExternalStore2 } from "react";
175
+ var revisions = /* @__PURE__ */ new Map();
176
+ var listeners = /* @__PURE__ */ new Set();
177
+ function recordRevision(id) {
178
+ return revisions.get(id) ?? 0;
179
+ }
180
+ function noteRecordChanged(id) {
181
+ if (!id) return;
182
+ revisions.set(id, recordRevision(id) + 1);
183
+ for (const listener of listeners) listener();
184
+ }
185
+ function subscribeRevisions(listener) {
186
+ listeners.add(listener);
187
+ return () => {
188
+ listeners.delete(listener);
189
+ };
190
+ }
191
+ function useRecordRevision(id) {
192
+ return useSyncExternalStore2(
193
+ subscribeRevisions,
194
+ () => recordRevision(id),
195
+ () => 0
196
+ );
197
+ }
198
+ function useRecordValues(read, ref) {
199
+ const revision = useRecordRevision(ref?.id ?? "");
200
+ const key = ref && read ? `${ref.noun}:${ref.id}#${revision}` : "";
201
+ const [result, setResult] = useState({
202
+ key: "",
203
+ values: null,
204
+ status: "idle"
205
+ });
206
+ useEffect(() => {
207
+ if (!key || !read) return;
208
+ const split = key.indexOf(":");
209
+ const target = {
210
+ noun: key.slice(0, split),
211
+ id: key.slice(split + 1, key.lastIndexOf("#"))
212
+ };
213
+ let live = true;
214
+ setResult({ key, values: null, status: "loading" });
215
+ read(target).then((values) => {
216
+ if (!live) return;
217
+ setResult({ key, values, status: values ? "ready" : "unavailable" });
218
+ }).catch(() => {
219
+ if (!live) return;
220
+ setResult({ key, values: null, status: "unavailable" });
221
+ });
222
+ return () => {
223
+ live = false;
224
+ };
225
+ }, [key, read]);
226
+ if (!key) return { values: null, status: "idle" };
227
+ if (result.key !== key) return { values: null, status: "loading" };
228
+ return { values: result.values, status: result.status };
229
+ }
230
+
173
231
  // directives/failure-words.ts
174
232
  var PAST = {
175
233
  create: "created",
@@ -289,6 +347,41 @@ function classIcon(directiveClass, noun) {
289
347
 
290
348
  // directives/ApplyDirectiveButton.tsx
291
349
  import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
350
+ function announceChangedRecords(directive, items) {
351
+ if (!TARGETING_CLASSES.has(directive.directiveClass)) return;
352
+ for (const item of items) {
353
+ const id = itemRecordId(item);
354
+ if (id) noteRecordChanged(id);
355
+ }
356
+ }
357
+ var applyOutcomes = /* @__PURE__ */ new WeakMap();
358
+ var EMPTY_AUTHORITY = {};
359
+ var outcomeListeners = /* @__PURE__ */ new Set();
360
+ function outcomesOf(host) {
361
+ let held = applyOutcomes.get(host);
362
+ if (!held) {
363
+ held = /* @__PURE__ */ new Map();
364
+ applyOutcomes.set(host, held);
365
+ }
366
+ return held;
367
+ }
368
+ function publishOutcome(host, identity, outcome) {
369
+ outcomesOf(host).set(identity, outcome);
370
+ for (const listener of outcomeListeners) listener();
371
+ }
372
+ function subscribeOutcomes(listener) {
373
+ outcomeListeners.add(listener);
374
+ return () => {
375
+ outcomeListeners.delete(listener);
376
+ };
377
+ }
378
+ function useApplyOutcome(host, identity) {
379
+ return useSyncExternalStore3(
380
+ subscribeOutcomes,
381
+ () => applyOutcomes.get(host)?.get(identity) ?? null,
382
+ () => null
383
+ );
384
+ }
292
385
  var FINISH_POLL_MS = 2e3;
293
386
  var FINISH_POLLS_MAX = 30;
294
387
  function readsApplyLedger(host, directive) {
@@ -308,27 +401,29 @@ function ApplyDirectiveButton({
308
401
  const items = directive.items;
309
402
  const readsLedger = readsApplyLedger(host, directive);
310
403
  const initial = readsLedger ? { status: "checking" } : { status: "idle" };
311
- const [state, setState] = useState(initial);
312
- const [elapsed, setElapsed] = useState(0);
313
- const [askingAgain, setAskingAgain] = useState(false);
404
+ const [state, setState] = useState2(initial);
405
+ const [elapsed, setElapsed] = useState2(0);
406
+ const [askingAgain, setAskingAgain] = useState2(false);
314
407
  const startedAt = useRef(null);
315
408
  const identity = JSON.stringify([directive.slug, items, conversationId ?? null]);
316
- const [stateFor, setStateFor] = useState(identity);
409
+ const [stateFor, setStateFor] = useState2(identity);
317
410
  if (stateFor !== identity) {
318
411
  setStateFor(identity);
319
412
  setState(initial);
320
413
  setAskingAgain(false);
321
414
  setElapsed(0);
322
415
  }
416
+ const shared = useApplyOutcome(host.confirm ?? EMPTY_AUTHORITY, identity);
417
+ const view = state.status === "asking" || !shared ? state : shared;
323
418
  const currentIdentity = useRef(identity);
324
- useEffect(() => {
419
+ useEffect2(() => {
325
420
  currentIdentity.current = identity;
326
421
  }, [identity]);
327
422
  const nounText = nounLabel(directive.noun, host.nouns);
328
423
  const shellKey = readsLedger ? JSON.stringify([directive.slug, items, conversationId ?? null]) : "";
329
424
  const read = host.applyState;
330
425
  const report = host.reportError;
331
- useEffect(() => {
426
+ useEffect2(() => {
332
427
  if (!shellKey || !read) return;
333
428
  const [slug, shellItems, inConversation] = JSON.parse(shellKey);
334
429
  const shell = { __kind: slug, items: shellItems };
@@ -373,33 +468,34 @@ function ApplyDirectiveButton({
373
468
  if (timer) clearTimeout(timer);
374
469
  };
375
470
  }, [shellKey, read, report]);
376
- const appliedClean = state.status === "applied" && state.failed === 0 ? state : null;
471
+ const appliedClean = view.status === "applied" && view.failed === 0 ? view : null;
377
472
  const appliedBefore = appliedClean ? appliedClean.before : null;
378
- const phaseKey = state.status === "checking" || state.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
379
- useEffect(() => {
473
+ const phaseKey = view.status === "checking" || view.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
474
+ useEffect2(() => {
380
475
  if (!onPhase) return;
381
476
  if (phaseKey === "pending") onPhase({ phase: "pending" });
382
477
  else if (phaseKey === "open") onPhase({ phase: "open" });
383
478
  else onPhase({ phase: "applied", before: appliedBefore });
384
479
  }, [phaseKey, onPhase, appliedBefore]);
385
- useEffect(() => {
386
- if (state.status !== "applying") return;
387
- startedAt.current = Date.now();
480
+ const applyingSince = view.status === "applying" ? view.startedAt : null;
481
+ useEffect2(() => {
482
+ if (applyingSince === null) return;
483
+ startedAt.current = applyingSince;
388
484
  const id = setInterval(
389
485
  () => setElapsed(Math.round((Date.now() - (startedAt.current ?? 0)) / 1e3)),
390
486
  1e3
391
487
  );
392
488
  return () => clearInterval(id);
393
- }, [state.status]);
489
+ }, [applyingSince]);
394
490
  if (!directive.parsed.executes) return null;
395
491
  const confirm = host.confirm;
396
492
  if (!confirm) return null;
397
493
  async function handleApply(again = false) {
398
494
  if (!confirm) return;
399
- const prior = state;
495
+ const prior = view;
400
496
  const askedFor = identity;
401
497
  const stillHere = () => currentIdentity.current === askedFor;
402
- const priorCopies = state.status === "applied" ? state.copies : 0;
498
+ const priorCopies = view.status === "applied" ? view.copies : 0;
403
499
  if (host.ask) {
404
500
  if (again) setAskingAgain(true);
405
501
  else setState({ status: "asking" });
@@ -428,14 +524,20 @@ function ApplyDirectiveButton({
428
524
  }
429
525
  }
430
526
  setElapsed(0);
431
- setState({ status: "applying" });
527
+ setState((prev) => prev.status === "asking" ? { status: "idle" } : prev);
528
+ publishOutcome(confirm, askedFor, { status: "applying", startedAt: Date.now() });
432
529
  try {
433
530
  const shell = { __kind: directive.slug, items };
434
531
  if (conversationId) shell.conversationId = conversationId;
435
532
  if (again) shell.force = true;
436
- const result = await confirm(shell);
437
- if (!stillHere()) return;
438
- setState({
533
+ let result;
534
+ try {
535
+ result = await confirm(shell);
536
+ } finally {
537
+ announceChangedRecords(directive, items);
538
+ }
539
+ for (const record of result.records ?? []) noteRecordChanged(record.id);
540
+ publishOutcome(confirm, askedFor, {
439
541
  status: "applied",
440
542
  applied: result.applied,
441
543
  failed: result.failed,
@@ -447,20 +549,19 @@ function ApplyDirectiveButton({
447
549
  // apply replaced is still the change this block made.
448
550
  before: again && prior.status === "applied" && prior.before ? prior.before : result.before ?? null
449
551
  });
450
- onApplied?.();
552
+ if (stillHere()) onApplied?.();
451
553
  } catch (error) {
452
- if (!stillHere()) return;
453
- setState({
554
+ publishOutcome(confirm, askedFor, {
454
555
  status: "error",
455
556
  message: error instanceof Error && error.message ? error.message : "That couldn't be applied just now. Please try again."
456
557
  });
457
558
  }
458
559
  }
459
- if (state.status === "applied") {
460
- const bad = state.failed > 0;
560
+ if (view.status === "applied") {
561
+ const bad = view.failed > 0;
461
562
  const trashed = directive.directiveClass === "delete";
462
563
  const label2 = nounText;
463
- const records = host.renderRecord ? state.records : [];
564
+ const records = host.renderRecord ? view.records : [];
464
565
  return /* @__PURE__ */ jsxs2(
465
566
  "span",
466
567
  {
@@ -474,26 +575,26 @@ function ApplyDirectiveButton({
474
575
  {
475
576
  words: failureWords(
476
577
  {
477
- raw: state.message ?? `${state.failed} failed`,
578
+ raw: view.message ?? `${view.failed} failed`,
478
579
  directiveClass: directive.directiveClass,
479
580
  noun: directive.noun,
480
581
  nounLabel: nounText,
481
- failed: state.failed,
482
- total: state.applied + state.failed
582
+ failed: view.failed,
583
+ total: view.applied + view.failed
483
584
  },
484
585
  host.explainFailure
485
586
  ),
486
- raw: state.message
587
+ raw: view.message
487
588
  }
488
- ) : /* @__PURE__ */ jsx4("span", { className: "min-w-0", children: state.message ?? `Applied ${state.applied}` })
589
+ ) : /* @__PURE__ */ jsx4("span", { className: "min-w-0", children: view.message ?? `Applied ${view.applied}` })
489
590
  ] }),
490
- state.copies > 1 ? /* @__PURE__ */ jsx4(
591
+ view.copies > 1 ? /* @__PURE__ */ jsx4(
491
592
  "span",
492
593
  {
493
594
  className: "shrink-0 text-muted-foreground",
494
- "data-apply-copies": state.copies,
595
+ "data-apply-copies": view.copies,
495
596
  title: records.length > 0 ? directive.directiveClass === "create" ? "Links to the latest copy." : "Links to the latest run." : void 0,
496
- children: directive.directiveClass === "create" ? `${state.copies} copies` : `Ran ${state.copies} times`
597
+ children: directive.directiveClass === "create" ? `${view.copies} copies` : `Ran ${view.copies} times`
497
598
  }
498
599
  ) : null,
499
600
  records.map((record) => /* @__PURE__ */ jsx4("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
@@ -520,7 +621,7 @@ function ApplyDirectiveButton({
520
621
  }
521
622
  );
522
623
  }
523
- if (state.status === "checking") {
624
+ if (view.status === "checking") {
524
625
  return /* @__PURE__ */ jsx4(
525
626
  "span",
526
627
  {
@@ -532,7 +633,7 @@ function ApplyDirectiveButton({
532
633
  }
533
634
  );
534
635
  }
535
- if (state.status === "finishing") {
636
+ if (view.status === "finishing") {
536
637
  return /* @__PURE__ */ jsxs2(
537
638
  "span",
538
639
  {
@@ -547,7 +648,7 @@ function ApplyDirectiveButton({
547
648
  );
548
649
  }
549
650
  const destructive = directive.directiveClass === "delete";
550
- const muted = nothingToChange && state.status === "idle";
651
+ const muted = nothingToChange && view.status === "idle";
551
652
  return /* @__PURE__ */ jsxs2("span", { className: "inline-flex items-center gap-2", children: [
552
653
  muted ? /* @__PURE__ */ jsx4("span", { className: "text-xs text-muted-foreground", "data-apply-nothing": "", children: "Nothing to change" }) : null,
553
654
  /* @__PURE__ */ jsxs2(
@@ -555,17 +656,17 @@ function ApplyDirectiveButton({
555
656
  {
556
657
  type: "button",
557
658
  onClick: () => void handleApply(),
558
- disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
659
+ disabled: view.status === "applying" || view.status === "asking" || items.length === 0,
559
660
  "data-apply-tone": muted ? "muted" : destructive ? "destructive" : "default",
560
661
  title: muted ? "It sets no fields, so applying it changes nothing." : void 0,
561
662
  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"}`,
562
663
  children: [
563
- 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" }),
564
- state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
664
+ 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" }),
665
+ view.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
565
666
  ]
566
667
  }
567
668
  ),
568
- state.status === "error" ? /* @__PURE__ */ jsxs2(
669
+ view.status === "error" ? /* @__PURE__ */ jsxs2(
569
670
  "span",
570
671
  {
571
672
  className: "inline-flex max-w-sm items-start gap-1 text-xs text-destructive",
@@ -577,14 +678,14 @@ function ApplyDirectiveButton({
577
678
  {
578
679
  words: failureWords(
579
680
  {
580
- raw: state.message,
681
+ raw: view.message,
581
682
  directiveClass: directive.directiveClass,
582
683
  noun: directive.noun,
583
684
  nounLabel: nounText
584
685
  },
585
686
  host.explainFailure
586
687
  ),
587
- raw: state.message
688
+ raw: view.message
588
689
  }
589
690
  )
590
691
  ]
@@ -596,7 +697,7 @@ function FailureNotice({
596
697
  words,
597
698
  raw
598
699
  }) {
599
- const [open, setOpen] = useState(false);
700
+ const [open, setOpen] = useState2(false);
600
701
  const detail = raw?.trim() ?? "";
601
702
  return /* @__PURE__ */ jsxs2("span", { className: "flex min-w-0 flex-col gap-0.5", "data-apply-failure": true, children: [
602
703
  /* @__PURE__ */ jsxs2("span", { className: "min-w-0", children: [
@@ -717,39 +818,6 @@ import {
717
818
  nounLabel as nounLabel2,
718
819
  nounTitleColumn
719
820
  } from "@ai-matrx/content-ir";
720
-
721
- // directives/record-values.ts
722
- import { useEffect as useEffect2, useState as useState2 } from "react";
723
- function useRecordValues(read, ref) {
724
- const key = ref && read ? `${ref.noun}:${ref.id}` : "";
725
- const [result, setResult] = useState2({
726
- key: "",
727
- values: null,
728
- status: "idle"
729
- });
730
- useEffect2(() => {
731
- if (!key || !read) return;
732
- const split = key.indexOf(":");
733
- const target = { noun: key.slice(0, split), id: key.slice(split + 1) };
734
- let live = true;
735
- setResult({ key, values: null, status: "loading" });
736
- read(target).then((values) => {
737
- if (!live) return;
738
- setResult({ key, values, status: values ? "ready" : "unavailable" });
739
- }).catch(() => {
740
- if (!live) return;
741
- setResult({ key, values: null, status: "unavailable" });
742
- });
743
- return () => {
744
- live = false;
745
- };
746
- }, [key, read]);
747
- if (!key) return { values: null, status: "idle" };
748
- if (result.key !== key) return { values: null, status: "loading" };
749
- return { values: result.values, status: result.status };
750
- }
751
-
752
- // directives/SideEffectDirectiveCard.tsx
753
821
  import { Fragment, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
754
822
  var ROWS_BEFORE_FOLD = 3;
755
823
  function ItemRow({