@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.
@@ -44,7 +44,7 @@ function resetDirectiveRenderers() {
44
44
  }
45
45
 
46
46
  // directives/ApplyDirectiveButton.tsx
47
- import { useEffect, useRef, useState } from "react";
47
+ import { useEffect as useEffect2, useRef, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
48
48
  import { nounLabel } from "@ai-matrx/content-ir";
49
49
 
50
50
  // directives/item-changes.ts
@@ -123,6 +123,64 @@ function itemChanges(item, current, options = {}) {
123
123
  return out;
124
124
  }
125
125
 
126
+ // directives/record-values.ts
127
+ import { useEffect, useState, useSyncExternalStore } from "react";
128
+ var revisions = /* @__PURE__ */ new Map();
129
+ var listeners = /* @__PURE__ */ new Set();
130
+ function recordRevision(id) {
131
+ return revisions.get(id) ?? 0;
132
+ }
133
+ function noteRecordChanged(id) {
134
+ if (!id) return;
135
+ revisions.set(id, recordRevision(id) + 1);
136
+ for (const listener of listeners) listener();
137
+ }
138
+ function subscribeRevisions(listener) {
139
+ listeners.add(listener);
140
+ return () => {
141
+ listeners.delete(listener);
142
+ };
143
+ }
144
+ function useRecordRevision(id) {
145
+ return useSyncExternalStore(
146
+ subscribeRevisions,
147
+ () => recordRevision(id),
148
+ () => 0
149
+ );
150
+ }
151
+ function useRecordValues(read, ref) {
152
+ const revision = useRecordRevision(ref?.id ?? "");
153
+ const key = ref && read ? `${ref.noun}:${ref.id}#${revision}` : "";
154
+ const [result, setResult] = useState({
155
+ key: "",
156
+ values: null,
157
+ status: "idle"
158
+ });
159
+ useEffect(() => {
160
+ if (!key || !read) return;
161
+ const split = key.indexOf(":");
162
+ const target = {
163
+ noun: key.slice(0, split),
164
+ id: key.slice(split + 1, key.lastIndexOf("#"))
165
+ };
166
+ let live = true;
167
+ setResult({ key, values: null, status: "loading" });
168
+ read(target).then((values) => {
169
+ if (!live) return;
170
+ setResult({ key, values, status: values ? "ready" : "unavailable" });
171
+ }).catch(() => {
172
+ if (!live) return;
173
+ setResult({ key, values: null, status: "unavailable" });
174
+ });
175
+ return () => {
176
+ live = false;
177
+ };
178
+ }, [key, read]);
179
+ if (!key) return { values: null, status: "idle" };
180
+ if (result.key !== key) return { values: null, status: "loading" };
181
+ return { values: result.values, status: result.status };
182
+ }
183
+
126
184
  // directives/failure-words.ts
127
185
  var PAST = {
128
186
  create: "created",
@@ -242,6 +300,41 @@ function classIcon(directiveClass, noun) {
242
300
 
243
301
  // directives/ApplyDirectiveButton.tsx
244
302
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
303
+ function announceChangedRecords(directive, items) {
304
+ if (!TARGETING_CLASSES.has(directive.directiveClass)) return;
305
+ for (const item of items) {
306
+ const id = itemRecordId(item);
307
+ if (id) noteRecordChanged(id);
308
+ }
309
+ }
310
+ var applyOutcomes = /* @__PURE__ */ new WeakMap();
311
+ var EMPTY_AUTHORITY = {};
312
+ var outcomeListeners = /* @__PURE__ */ new Set();
313
+ function outcomesOf(host) {
314
+ let held = applyOutcomes.get(host);
315
+ if (!held) {
316
+ held = /* @__PURE__ */ new Map();
317
+ applyOutcomes.set(host, held);
318
+ }
319
+ return held;
320
+ }
321
+ function publishOutcome(host, identity, outcome) {
322
+ outcomesOf(host).set(identity, outcome);
323
+ for (const listener of outcomeListeners) listener();
324
+ }
325
+ function subscribeOutcomes(listener) {
326
+ outcomeListeners.add(listener);
327
+ return () => {
328
+ outcomeListeners.delete(listener);
329
+ };
330
+ }
331
+ function useApplyOutcome(host, identity) {
332
+ return useSyncExternalStore2(
333
+ subscribeOutcomes,
334
+ () => applyOutcomes.get(host)?.get(identity) ?? null,
335
+ () => null
336
+ );
337
+ }
245
338
  var FINISH_POLL_MS = 2e3;
246
339
  var FINISH_POLLS_MAX = 30;
247
340
  function readsApplyLedger(host, directive) {
@@ -261,27 +354,29 @@ function ApplyDirectiveButton({
261
354
  const items = directive.items;
262
355
  const readsLedger = readsApplyLedger(host, directive);
263
356
  const initial = readsLedger ? { status: "checking" } : { status: "idle" };
264
- const [state, setState] = useState(initial);
265
- const [elapsed, setElapsed] = useState(0);
266
- const [askingAgain, setAskingAgain] = useState(false);
357
+ const [state, setState] = useState2(initial);
358
+ const [elapsed, setElapsed] = useState2(0);
359
+ const [askingAgain, setAskingAgain] = useState2(false);
267
360
  const startedAt = useRef(null);
268
361
  const identity = JSON.stringify([directive.slug, items, conversationId ?? null]);
269
- const [stateFor, setStateFor] = useState(identity);
362
+ const [stateFor, setStateFor] = useState2(identity);
270
363
  if (stateFor !== identity) {
271
364
  setStateFor(identity);
272
365
  setState(initial);
273
366
  setAskingAgain(false);
274
367
  setElapsed(0);
275
368
  }
369
+ const shared = useApplyOutcome(host.confirm ?? EMPTY_AUTHORITY, identity);
370
+ const view = state.status === "asking" || !shared ? state : shared;
276
371
  const currentIdentity = useRef(identity);
277
- useEffect(() => {
372
+ useEffect2(() => {
278
373
  currentIdentity.current = identity;
279
374
  }, [identity]);
280
375
  const nounText = nounLabel(directive.noun, host.nouns);
281
376
  const shellKey = readsLedger ? JSON.stringify([directive.slug, items, conversationId ?? null]) : "";
282
377
  const read = host.applyState;
283
378
  const report = host.reportError;
284
- useEffect(() => {
379
+ useEffect2(() => {
285
380
  if (!shellKey || !read) return;
286
381
  const [slug, shellItems, inConversation] = JSON.parse(shellKey);
287
382
  const shell = { __kind: slug, items: shellItems };
@@ -326,33 +421,34 @@ function ApplyDirectiveButton({
326
421
  if (timer) clearTimeout(timer);
327
422
  };
328
423
  }, [shellKey, read, report]);
329
- const appliedClean = state.status === "applied" && state.failed === 0 ? state : null;
424
+ const appliedClean = view.status === "applied" && view.failed === 0 ? view : null;
330
425
  const appliedBefore = appliedClean ? appliedClean.before : null;
331
- const phaseKey = state.status === "checking" || state.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
332
- useEffect(() => {
426
+ const phaseKey = view.status === "checking" || view.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
427
+ useEffect2(() => {
333
428
  if (!onPhase) return;
334
429
  if (phaseKey === "pending") onPhase({ phase: "pending" });
335
430
  else if (phaseKey === "open") onPhase({ phase: "open" });
336
431
  else onPhase({ phase: "applied", before: appliedBefore });
337
432
  }, [phaseKey, onPhase, appliedBefore]);
338
- useEffect(() => {
339
- if (state.status !== "applying") return;
340
- startedAt.current = Date.now();
433
+ const applyingSince = view.status === "applying" ? view.startedAt : null;
434
+ useEffect2(() => {
435
+ if (applyingSince === null) return;
436
+ startedAt.current = applyingSince;
341
437
  const id = setInterval(
342
438
  () => setElapsed(Math.round((Date.now() - (startedAt.current ?? 0)) / 1e3)),
343
439
  1e3
344
440
  );
345
441
  return () => clearInterval(id);
346
- }, [state.status]);
442
+ }, [applyingSince]);
347
443
  if (!directive.parsed.executes) return null;
348
444
  const confirm = host.confirm;
349
445
  if (!confirm) return null;
350
446
  async function handleApply(again = false) {
351
447
  if (!confirm) return;
352
- const prior = state;
448
+ const prior = view;
353
449
  const askedFor = identity;
354
450
  const stillHere = () => currentIdentity.current === askedFor;
355
- const priorCopies = state.status === "applied" ? state.copies : 0;
451
+ const priorCopies = view.status === "applied" ? view.copies : 0;
356
452
  if (host.ask) {
357
453
  if (again) setAskingAgain(true);
358
454
  else setState({ status: "asking" });
@@ -381,14 +477,20 @@ function ApplyDirectiveButton({
381
477
  }
382
478
  }
383
479
  setElapsed(0);
384
- setState({ status: "applying" });
480
+ setState((prev) => prev.status === "asking" ? { status: "idle" } : prev);
481
+ publishOutcome(confirm, askedFor, { status: "applying", startedAt: Date.now() });
385
482
  try {
386
483
  const shell = { __kind: directive.slug, items };
387
484
  if (conversationId) shell.conversationId = conversationId;
388
485
  if (again) shell.force = true;
389
- const result = await confirm(shell);
390
- if (!stillHere()) return;
391
- setState({
486
+ let result;
487
+ try {
488
+ result = await confirm(shell);
489
+ } finally {
490
+ announceChangedRecords(directive, items);
491
+ }
492
+ for (const record of result.records ?? []) noteRecordChanged(record.id);
493
+ publishOutcome(confirm, askedFor, {
392
494
  status: "applied",
393
495
  applied: result.applied,
394
496
  failed: result.failed,
@@ -400,20 +502,19 @@ function ApplyDirectiveButton({
400
502
  // apply replaced is still the change this block made.
401
503
  before: again && prior.status === "applied" && prior.before ? prior.before : result.before ?? null
402
504
  });
403
- onApplied?.();
505
+ if (stillHere()) onApplied?.();
404
506
  } catch (error) {
405
- if (!stillHere()) return;
406
- setState({
507
+ publishOutcome(confirm, askedFor, {
407
508
  status: "error",
408
509
  message: error instanceof Error && error.message ? error.message : "That couldn't be applied just now. Please try again."
409
510
  });
410
511
  }
411
512
  }
412
- if (state.status === "applied") {
413
- const bad = state.failed > 0;
513
+ if (view.status === "applied") {
514
+ const bad = view.failed > 0;
414
515
  const trashed = directive.directiveClass === "delete";
415
516
  const label2 = nounText;
416
- const records = host.renderRecord ? state.records : [];
517
+ const records = host.renderRecord ? view.records : [];
417
518
  return /* @__PURE__ */ jsxs2(
418
519
  "span",
419
520
  {
@@ -427,26 +528,26 @@ function ApplyDirectiveButton({
427
528
  {
428
529
  words: failureWords(
429
530
  {
430
- raw: state.message ?? `${state.failed} failed`,
531
+ raw: view.message ?? `${view.failed} failed`,
431
532
  directiveClass: directive.directiveClass,
432
533
  noun: directive.noun,
433
534
  nounLabel: nounText,
434
- failed: state.failed,
435
- total: state.applied + state.failed
535
+ failed: view.failed,
536
+ total: view.applied + view.failed
436
537
  },
437
538
  host.explainFailure
438
539
  ),
439
- raw: state.message
540
+ raw: view.message
440
541
  }
441
- ) : /* @__PURE__ */ jsx3("span", { className: "min-w-0", children: state.message ?? `Applied ${state.applied}` })
542
+ ) : /* @__PURE__ */ jsx3("span", { className: "min-w-0", children: view.message ?? `Applied ${view.applied}` })
442
543
  ] }),
443
- state.copies > 1 ? /* @__PURE__ */ jsx3(
544
+ view.copies > 1 ? /* @__PURE__ */ jsx3(
444
545
  "span",
445
546
  {
446
547
  className: "shrink-0 text-muted-foreground",
447
- "data-apply-copies": state.copies,
548
+ "data-apply-copies": view.copies,
448
549
  title: records.length > 0 ? directive.directiveClass === "create" ? "Links to the latest copy." : "Links to the latest run." : void 0,
449
- children: directive.directiveClass === "create" ? `${state.copies} copies` : `Ran ${state.copies} times`
550
+ children: directive.directiveClass === "create" ? `${view.copies} copies` : `Ran ${view.copies} times`
450
551
  }
451
552
  ) : null,
452
553
  records.map((record) => /* @__PURE__ */ jsx3("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
@@ -473,7 +574,7 @@ function ApplyDirectiveButton({
473
574
  }
474
575
  );
475
576
  }
476
- if (state.status === "checking") {
577
+ if (view.status === "checking") {
477
578
  return /* @__PURE__ */ jsx3(
478
579
  "span",
479
580
  {
@@ -485,7 +586,7 @@ function ApplyDirectiveButton({
485
586
  }
486
587
  );
487
588
  }
488
- if (state.status === "finishing") {
589
+ if (view.status === "finishing") {
489
590
  return /* @__PURE__ */ jsxs2(
490
591
  "span",
491
592
  {
@@ -500,7 +601,7 @@ function ApplyDirectiveButton({
500
601
  );
501
602
  }
502
603
  const destructive = directive.directiveClass === "delete";
503
- const muted = nothingToChange && state.status === "idle";
604
+ const muted = nothingToChange && view.status === "idle";
504
605
  return /* @__PURE__ */ jsxs2("span", { className: "inline-flex items-center gap-2", children: [
505
606
  muted ? /* @__PURE__ */ jsx3("span", { className: "text-xs text-muted-foreground", "data-apply-nothing": "", children: "Nothing to change" }) : null,
506
607
  /* @__PURE__ */ jsxs2(
@@ -508,17 +609,17 @@ function ApplyDirectiveButton({
508
609
  {
509
610
  type: "button",
510
611
  onClick: () => void handleApply(),
511
- disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
612
+ disabled: view.status === "applying" || view.status === "asking" || items.length === 0,
512
613
  "data-apply-tone": muted ? "muted" : destructive ? "destructive" : "default",
513
614
  title: muted ? "It sets no fields, so applying it changes nothing." : void 0,
514
615
  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"}`,
515
616
  children: [
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" }),
517
- state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
617
+ view.status === "applying" ? /* @__PURE__ */ jsx3(LoaderIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx3(PlayIcon, { className: "h-3.5 w-3.5" }),
618
+ view.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
518
619
  ]
519
620
  }
520
621
  ),
521
- state.status === "error" ? /* @__PURE__ */ jsxs2(
622
+ view.status === "error" ? /* @__PURE__ */ jsxs2(
522
623
  "span",
523
624
  {
524
625
  className: "inline-flex max-w-sm items-start gap-1 text-xs text-destructive",
@@ -530,14 +631,14 @@ function ApplyDirectiveButton({
530
631
  {
531
632
  words: failureWords(
532
633
  {
533
- raw: state.message,
634
+ raw: view.message,
534
635
  directiveClass: directive.directiveClass,
535
636
  noun: directive.noun,
536
637
  nounLabel: nounText
537
638
  },
538
639
  host.explainFailure
539
640
  ),
540
- raw: state.message
641
+ raw: view.message
541
642
  }
542
643
  )
543
644
  ]
@@ -549,7 +650,7 @@ function FailureNotice({
549
650
  words,
550
651
  raw
551
652
  }) {
552
- const [open, setOpen] = useState(false);
653
+ const [open, setOpen] = useState2(false);
553
654
  const detail = raw?.trim() ?? "";
554
655
  return /* @__PURE__ */ jsxs2("span", { className: "flex min-w-0 flex-col gap-0.5", "data-apply-failure": true, children: [
555
656
  /* @__PURE__ */ jsxs2("span", { className: "min-w-0", children: [
@@ -590,39 +691,6 @@ import {
590
691
  nounLabel as nounLabel2,
591
692
  nounTitleColumn
592
693
  } from "@ai-matrx/content-ir";
593
-
594
- // directives/record-values.ts
595
- import { useEffect as useEffect2, useState as useState2 } from "react";
596
- function useRecordValues(read, ref) {
597
- const key = ref && read ? `${ref.noun}:${ref.id}` : "";
598
- const [result, setResult] = useState2({
599
- key: "",
600
- values: null,
601
- status: "idle"
602
- });
603
- useEffect2(() => {
604
- if (!key || !read) return;
605
- const split = key.indexOf(":");
606
- const target = { noun: key.slice(0, split), id: key.slice(split + 1) };
607
- let live = true;
608
- setResult({ key, values: null, status: "loading" });
609
- read(target).then((values) => {
610
- if (!live) return;
611
- setResult({ key, values, status: values ? "ready" : "unavailable" });
612
- }).catch(() => {
613
- if (!live) return;
614
- setResult({ key, values: null, status: "unavailable" });
615
- });
616
- return () => {
617
- live = false;
618
- };
619
- }, [key, read]);
620
- if (!key) return { values: null, status: "idle" };
621
- if (result.key !== key) return { values: null, status: "loading" };
622
- return { values: result.values, status: result.status };
623
- }
624
-
625
- // directives/SideEffectDirectiveCard.tsx
626
694
  import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
627
695
  var ROWS_BEFORE_FOLD = 3;
628
696
  function ItemRow({
@@ -926,13 +994,16 @@ export {
926
994
  getDirectiveRenderer,
927
995
  itemChanges,
928
996
  itemRecordId,
997
+ noteRecordChanged,
929
998
  provisionalDirectiveSlug,
930
999
  readsApplyLedger,
931
1000
  recordFallbackName,
1001
+ recordRevision,
932
1002
  registerDirectiveRenderer,
933
1003
  resetDirectiveRenderers,
934
1004
  useDirectiveHost,
935
1005
  useDirectiveScope,
1006
+ useRecordRevision,
936
1007
  useRecordValues
937
1008
  };
938
1009
  //# sourceMappingURL=directives.js.map