@ai-matrx/content-ir-react 0.15.1 → 0.16.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.d.cts CHANGED
@@ -1,9 +1,9 @@
1
- import { c as KindDefinitionSource, C as ContentIrHost } from './host-types-DVIaaGR_.cjs';
1
+ import { c as KindDefinitionSource, C as ContentIrHost } from './host-types-a0XEr5b3.cjs';
2
2
  import { d as ComponentResolutionSource } from './component-resolution.types-C1MzCf2s.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-DJfF35FD.cjs';
6
+ import './directive-host-C4TcPpxX.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-9NfBeqrG.js';
1
+ import { c as KindDefinitionSource, C as ContentIrHost } from './host-types-CEc86R-v.js';
2
2
  import { d as ComponentResolutionSource } from './component-resolution.types-C1MzCf2s.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-DJfF35FD.js';
6
+ import './directive-host-C4TcPpxX.js';
7
7
 
8
8
  /**
9
9
  * The two registries this hook watches. Passed explicitly by a host whose
package/dist/react.js CHANGED
@@ -80,7 +80,7 @@ function useContentIrKindVersion(kind, sources) {
80
80
  }
81
81
 
82
82
  // react/KindInstanceRender.tsx
83
- import { useEffect as useEffect2, useState as useState3 } from "react";
83
+ import { useEffect as useEffect3, useState as useState4 } from "react";
84
84
  import {
85
85
  envelopeFromCompleteValue as envelopeFromCompleteValue2,
86
86
  IR_ENVELOPE_KEY,
@@ -144,11 +144,26 @@ function safeJson(value) {
144
144
  return String(value);
145
145
  }
146
146
  }
147
- function itemChanges(item) {
147
+ function sameValue(a, b) {
148
+ if (a === b) return true;
149
+ if (a === null || a === void 0 || b === null || b === void 0) {
150
+ const emptyA = a === null || a === void 0 || a === "";
151
+ const emptyB = b === null || b === void 0 || b === "";
152
+ return emptyA && emptyB;
153
+ }
154
+ if (typeof a === "object" || typeof b === "object") return safeJson(a) === safeJson(b);
155
+ return false;
156
+ }
157
+ function itemChanges(item, current) {
148
158
  const out = [];
149
159
  for (const [key, raw] of Object.entries(item)) {
150
160
  if (NOT_A_CHANGE.has(key)) continue;
151
- out.push({ key, label: fieldLabel(key), ...describeValue(raw) });
161
+ const change = { key, label: fieldLabel(key), ...describeValue(raw) };
162
+ if (current && Object.prototype.hasOwnProperty.call(current, key)) {
163
+ change.before = describeValue(current[key]);
164
+ change.same = sameValue(current[key], raw);
165
+ }
166
+ out.push(change);
152
167
  }
153
168
  return out;
154
169
  }
@@ -261,11 +276,23 @@ function ApplyDirectiveButton({
261
276
  const conversationId = scope.conversationId || void 0;
262
277
  const items = directive.items;
263
278
  const readsLedger = !!host.applyState && !!host.confirm && directive.parsed.executes && items.length > 0;
264
- const [state, setState] = useState(
265
- readsLedger ? { status: "checking" } : { status: "idle" }
266
- );
279
+ const initial = readsLedger ? { status: "checking" } : { status: "idle" };
280
+ const [state, setState] = useState(initial);
267
281
  const [elapsed, setElapsed] = useState(0);
282
+ const [askingAgain, setAskingAgain] = useState(false);
268
283
  const startedAt = useRef(null);
284
+ const identity = JSON.stringify([directive.slug, items, conversationId ?? null]);
285
+ const [stateFor, setStateFor] = useState(identity);
286
+ if (stateFor !== identity) {
287
+ setStateFor(identity);
288
+ setState(initial);
289
+ setAskingAgain(false);
290
+ setElapsed(0);
291
+ }
292
+ const currentIdentity = useRef(identity);
293
+ useEffect(() => {
294
+ currentIdentity.current = identity;
295
+ }, [identity]);
269
296
  const nounText = nounLabel(directive.noun, host.nouns);
270
297
  const shellKey = readsLedger ? JSON.stringify([directive.slug, items, conversationId ?? null]) : "";
271
298
  const read = host.applyState;
@@ -288,7 +315,8 @@ function ApplyDirectiveButton({
288
315
  applied: shellItems.length,
289
316
  failed: 0,
290
317
  message: answer.message?.trim() ? answer.message.trim() : null,
291
- records: answer.records ?? []
318
+ records: answer.records ?? [],
319
+ copies: Math.max(1, answer.copies ?? 1)
292
320
  } : prev
293
321
  );
294
322
  } else if (answer.state === "in_flight" && polls < FINISH_POLLS_MAX) {
@@ -328,8 +356,12 @@ function ApplyDirectiveButton({
328
356
  async function handleApply(again = false) {
329
357
  if (!confirm) return;
330
358
  const before = state;
359
+ const askedFor = identity;
360
+ const stillHere = () => currentIdentity.current === askedFor;
361
+ const priorCopies = state.status === "applied" ? state.copies : 0;
331
362
  if (host.ask) {
332
- setState({ status: "asking" });
363
+ if (again) setAskingAgain(true);
364
+ else setState({ status: "asking" });
333
365
  let ok = false;
334
366
  try {
335
367
  ok = await host.ask({
@@ -339,12 +371,16 @@ function ApplyDirectiveButton({
339
371
  ...again ? { again: true } : {}
340
372
  });
341
373
  } catch (error) {
374
+ if (!stillHere()) return;
375
+ setAskingAgain(false);
342
376
  setState({
343
377
  status: "error",
344
378
  message: error instanceof Error && error.message ? error.message : "We could not ask you to confirm this, so nothing was done."
345
379
  });
346
380
  return;
347
381
  }
382
+ if (!stillHere()) return;
383
+ setAskingAgain(false);
348
384
  if (!ok) {
349
385
  setState(again ? before : { status: "idle" });
350
386
  return;
@@ -357,15 +393,19 @@ function ApplyDirectiveButton({
357
393
  if (conversationId) shell.conversationId = conversationId;
358
394
  if (again) shell.force = true;
359
395
  const result = await confirm(shell);
396
+ if (!stillHere()) return;
360
397
  setState({
361
398
  status: "applied",
362
399
  applied: result.applied,
363
400
  failed: result.failed,
364
401
  message: result.message?.trim() ? result.message.trim() : null,
365
- records: result.records ?? []
402
+ records: result.records ?? [],
403
+ // "Run again" made one more; a first apply (or a replay of one) is one.
404
+ copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies)
366
405
  });
367
406
  onApplied?.();
368
407
  } catch (error) {
408
+ if (!stillHere()) return;
369
409
  setState({
370
410
  status: "error",
371
411
  message: error instanceof Error && error.message ? error.message : "That couldn't be applied just now. Please try again."
@@ -390,6 +430,15 @@ function ApplyDirectiveButton({
390
430
  bad && !state.message ? ` \xB7 ${state.failed} failed` : ""
391
431
  ] })
392
432
  ] }),
433
+ state.copies > 1 ? /* @__PURE__ */ jsx4(
434
+ "span",
435
+ {
436
+ className: "shrink-0 text-muted-foreground",
437
+ "data-apply-copies": state.copies,
438
+ title: records.length > 0 ? directive.directiveClass === "create" ? "Links to the latest copy." : "Links to the latest run." : void 0,
439
+ children: directive.directiveClass === "create" ? `${state.copies} copies` : `Ran ${state.copies} times`
440
+ }
441
+ ) : null,
393
442
  records.map((record) => /* @__PURE__ */ jsx4("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
394
443
  ...record,
395
444
  fallback: recordFallbackName(label2, record.id),
@@ -400,8 +449,9 @@ function ApplyDirectiveButton({
400
449
  "button",
401
450
  {
402
451
  type: "button",
452
+ disabled: askingAgain,
403
453
  onClick: () => void handleApply(!bad),
404
- className: "inline-flex items-center gap-1 rounded px-1 py-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground",
454
+ className: "inline-flex items-center gap-1 rounded px-1 py-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-50",
405
455
  "data-apply-action": bad ? "retry" : "run-again",
406
456
  children: [
407
457
  /* @__PURE__ */ jsx4(RotateCcwIcon, { className: "h-3 w-3" }),
@@ -439,6 +489,7 @@ function ApplyDirectiveButton({
439
489
  }
440
490
  );
441
491
  }
492
+ const destructive = directive.directiveClass === "delete";
442
493
  return /* @__PURE__ */ jsxs2("span", { className: "inline-flex items-center gap-2", children: [
443
494
  /* @__PURE__ */ jsxs2(
444
495
  "button",
@@ -446,7 +497,8 @@ function ApplyDirectiveButton({
446
497
  type: "button",
447
498
  onClick: () => void handleApply(),
448
499
  disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
449
- className: "inline-flex items-center gap-1 rounded-md border border-border bg-background px-2 py-1 text-xs font-medium hover:bg-accent disabled:opacity-50",
500
+ "data-apply-tone": destructive ? "destructive" : "default",
501
+ 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"}`,
450
502
  children: [
451
503
  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" }),
452
504
  state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
@@ -541,7 +593,7 @@ function getDirectiveRenderer(directive) {
541
593
  }
542
594
 
543
595
  // directives/SideEffectDirectiveCard.tsx
544
- import { useState as useState2 } from "react";
596
+ import { useState as useState3 } from "react";
545
597
  import {
546
598
  asKindInstance,
547
599
  directiveDisplay as directiveDisplay3,
@@ -551,6 +603,39 @@ import {
551
603
  nounLabel as nounLabel2,
552
604
  nounTitleColumn
553
605
  } from "@ai-matrx/content-ir";
606
+
607
+ // directives/record-values.ts
608
+ import { useEffect as useEffect2, useState as useState2 } from "react";
609
+ function useRecordValues(read, ref) {
610
+ const key = ref && read ? `${ref.noun}:${ref.id}` : "";
611
+ const [result, setResult] = useState2({
612
+ key: "",
613
+ values: null,
614
+ status: "idle"
615
+ });
616
+ useEffect2(() => {
617
+ if (!key || !read) return;
618
+ const split = key.indexOf(":");
619
+ const target = { noun: key.slice(0, split), id: key.slice(split + 1) };
620
+ let live = true;
621
+ setResult({ key, values: null, status: "loading" });
622
+ read(target).then((values) => {
623
+ if (!live) return;
624
+ setResult({ key, values, status: values ? "ready" : "unavailable" });
625
+ }).catch(() => {
626
+ if (!live) return;
627
+ setResult({ key, values: null, status: "unavailable" });
628
+ });
629
+ return () => {
630
+ live = false;
631
+ };
632
+ }, [key, read]);
633
+ if (!key) return { values: null, status: "idle" };
634
+ if (result.key !== key) return { values: null, status: "loading" };
635
+ return { values: result.values, status: result.status };
636
+ }
637
+
638
+ // directives/SideEffectDirectiveCard.tsx
554
639
  import { Fragment, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
555
640
  var ROWS_BEFORE_FOLD = 3;
556
641
  function ItemRow({
@@ -604,44 +689,73 @@ function ItemRow({
604
689
  host.renderCopy?.({ label: title, value: item, kind, size: "xs" })
605
690
  ] });
606
691
  }
607
- function TargetRow({
608
- directive,
609
- item,
610
- recordId
692
+ function DirectiveChangeList({
693
+ changes,
694
+ className
611
695
  }) {
612
- const host = useDirectiveHost();
613
- const fallback = recordFallbackName(nounLabel2(directive.noun, host.nouns), recordId);
614
- const changes = directive.directiveClass === "update" ? itemChanges(item) : [];
615
- 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 });
616
- return /* @__PURE__ */ jsxs5("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
617
- /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 items-center gap-2", children: [
618
- /* @__PURE__ */ jsx7("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
619
- host.renderCopy?.({ label: fallback, value: item, size: "xs" })
620
- ] }),
621
- directive.directiveClass === "update" ? changes.length === 0 ? /* @__PURE__ */ jsx7("p", { className: "px-1 text-[11px] text-muted-foreground", children: "Sets no fields \u2014 nothing would change." }) : /* @__PURE__ */ jsx7("ul", { className: "mt-0.5 space-y-0.5 px-1", children: changes.map((change) => /* @__PURE__ */ jsxs5(
696
+ if (changes.length === 0) {
697
+ return /* @__PURE__ */ jsx7("p", { className: `text-[11px] text-muted-foreground${className ? ` ${className}` : ""}`, children: "Sets no fields \u2014 nothing would change." });
698
+ }
699
+ return /* @__PURE__ */ jsx7("ul", { className: `space-y-0.5${className ? ` ${className}` : ""}`, "data-directive-changes": "", children: changes.map((change) => {
700
+ const before = change.before && !change.same ? change.before : null;
701
+ const was = before ? before.clears ? "empty" : before.value : null;
702
+ return /* @__PURE__ */ jsxs5(
622
703
  "li",
623
704
  {
624
705
  className: "flex min-w-0 items-baseline gap-1.5 text-[11px]",
625
- title: `${change.label} \u2192 ${change.full}`,
706
+ title: before ? `${change.label}: ${before.clears ? "empty" : before.full} \u2192 ${change.full}` : `${change.label} \u2192 ${change.full}`,
707
+ "data-change-key": change.key,
626
708
  children: [
627
709
  /* @__PURE__ */ jsx7("span", { className: "shrink-0 text-muted-foreground", children: change.label }),
710
+ was !== null ? /* @__PURE__ */ jsx7(
711
+ "span",
712
+ {
713
+ className: "min-w-0 max-w-[40%] shrink truncate text-muted-foreground line-through",
714
+ "data-change-before": "",
715
+ children: was
716
+ }
717
+ ) : null,
628
718
  /* @__PURE__ */ jsx7("span", { "aria-hidden": true, className: "shrink-0 text-muted-foreground", children: "\u2192" }),
629
719
  /* @__PURE__ */ jsx7(
630
720
  "span",
631
721
  {
632
722
  className: `min-w-0 truncate ${change.clears ? "italic text-muted-foreground" : "text-foreground"}`,
723
+ "data-change-after": "",
633
724
  children: change.value
634
725
  }
635
- )
726
+ ),
727
+ change.same ? /* @__PURE__ */ jsx7("span", { className: "shrink-0 text-muted-foreground", children: "(no change)" }) : null
636
728
  ]
637
729
  },
638
730
  change.key
639
- )) }) : null
731
+ );
732
+ }) });
733
+ }
734
+ function TargetRow({
735
+ directive,
736
+ item,
737
+ recordId
738
+ }) {
739
+ const host = useDirectiveHost();
740
+ const fallback = recordFallbackName(nounLabel2(directive.noun, host.nouns), recordId);
741
+ const isUpdate = directive.directiveClass === "update";
742
+ const current = useRecordValues(
743
+ isUpdate ? host.readRecord : void 0,
744
+ isUpdate ? { noun: directive.noun, id: recordId } : null
745
+ );
746
+ const changes = isUpdate ? itemChanges(item, current.values) : [];
747
+ 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 });
748
+ return /* @__PURE__ */ jsxs5("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
749
+ /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 items-center gap-2", children: [
750
+ /* @__PURE__ */ jsx7("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
751
+ host.renderCopy?.({ label: fallback, value: item, size: "xs" })
752
+ ] }),
753
+ isUpdate ? /* @__PURE__ */ jsx7(DirectiveChangeList, { changes, className: "mt-0.5 px-1" }) : null
640
754
  ] });
641
755
  }
642
756
  function SideEffectDirectiveCard({ directive }) {
643
757
  const host = useDirectiveHost();
644
- const [expanded, setExpanded] = useState2(false);
758
+ const [expanded, setExpanded] = useState3(false);
645
759
  const items = directive.items;
646
760
  const total = items.length;
647
761
  const display = directiveDisplay3(directive.directiveClass, directive.noun, host.nouns);
@@ -779,10 +893,10 @@ function KindInstanceRenderInner({
779
893
  className
780
894
  }) {
781
895
  const host = useContentIrHost();
782
- const [routingStatus, setRoutingStatus] = useState3(
896
+ const [routingStatus, setRoutingStatus] = useState4(
783
897
  () => kindIsRoutable(kind, host) ? "routable" : "checking"
784
898
  );
785
- useEffect2(() => {
899
+ useEffect3(() => {
786
900
  let cancelled = false;
787
901
  let warmed = false;
788
902
  const syncRoutingStatus = () => {