@ai-matrx/content-ir-react 0.15.2 → 0.16.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-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,28 @@ 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, options = {}) {
148
158
  const out = [];
159
+ const titleColumn = options.titleColumn ?? null;
149
160
  for (const [key, raw] of Object.entries(item)) {
150
161
  if (NOT_A_CHANGE.has(key)) continue;
151
- out.push({ key, label: fieldLabel(key), ...describeValue(raw) });
162
+ const label = key === titleColumn ? "Title" : fieldLabel(key);
163
+ const change = { key, label, ...describeValue(raw) };
164
+ if (current && Object.prototype.hasOwnProperty.call(current, key)) {
165
+ change.before = describeValue(current[key]);
166
+ change.same = sameValue(current[key], raw);
167
+ }
168
+ out.push(change);
152
169
  }
153
170
  return out;
154
171
  }
@@ -261,12 +278,23 @@ function ApplyDirectiveButton({
261
278
  const conversationId = scope.conversationId || void 0;
262
279
  const items = directive.items;
263
280
  const readsLedger = !!host.applyState && !!host.confirm && directive.parsed.executes && items.length > 0;
264
- const [state, setState] = useState(
265
- readsLedger ? { status: "checking" } : { status: "idle" }
266
- );
281
+ const initial = readsLedger ? { status: "checking" } : { status: "idle" };
282
+ const [state, setState] = useState(initial);
267
283
  const [elapsed, setElapsed] = useState(0);
268
284
  const [askingAgain, setAskingAgain] = useState(false);
269
285
  const startedAt = useRef(null);
286
+ const identity = JSON.stringify([directive.slug, items, conversationId ?? null]);
287
+ const [stateFor, setStateFor] = useState(identity);
288
+ if (stateFor !== identity) {
289
+ setStateFor(identity);
290
+ setState(initial);
291
+ setAskingAgain(false);
292
+ setElapsed(0);
293
+ }
294
+ const currentIdentity = useRef(identity);
295
+ useEffect(() => {
296
+ currentIdentity.current = identity;
297
+ }, [identity]);
270
298
  const nounText = nounLabel(directive.noun, host.nouns);
271
299
  const shellKey = readsLedger ? JSON.stringify([directive.slug, items, conversationId ?? null]) : "";
272
300
  const read = host.applyState;
@@ -289,7 +317,8 @@ function ApplyDirectiveButton({
289
317
  applied: shellItems.length,
290
318
  failed: 0,
291
319
  message: answer.message?.trim() ? answer.message.trim() : null,
292
- records: answer.records ?? []
320
+ records: answer.records ?? [],
321
+ copies: Math.max(1, answer.copies ?? 1)
293
322
  } : prev
294
323
  );
295
324
  } else if (answer.state === "in_flight" && polls < FINISH_POLLS_MAX) {
@@ -329,6 +358,9 @@ function ApplyDirectiveButton({
329
358
  async function handleApply(again = false) {
330
359
  if (!confirm) return;
331
360
  const before = state;
361
+ const askedFor = identity;
362
+ const stillHere = () => currentIdentity.current === askedFor;
363
+ const priorCopies = state.status === "applied" ? state.copies : 0;
332
364
  if (host.ask) {
333
365
  if (again) setAskingAgain(true);
334
366
  else setState({ status: "asking" });
@@ -341,6 +373,7 @@ function ApplyDirectiveButton({
341
373
  ...again ? { again: true } : {}
342
374
  });
343
375
  } catch (error) {
376
+ if (!stillHere()) return;
344
377
  setAskingAgain(false);
345
378
  setState({
346
379
  status: "error",
@@ -348,6 +381,7 @@ function ApplyDirectiveButton({
348
381
  });
349
382
  return;
350
383
  }
384
+ if (!stillHere()) return;
351
385
  setAskingAgain(false);
352
386
  if (!ok) {
353
387
  setState(again ? before : { status: "idle" });
@@ -361,15 +395,19 @@ function ApplyDirectiveButton({
361
395
  if (conversationId) shell.conversationId = conversationId;
362
396
  if (again) shell.force = true;
363
397
  const result = await confirm(shell);
398
+ if (!stillHere()) return;
364
399
  setState({
365
400
  status: "applied",
366
401
  applied: result.applied,
367
402
  failed: result.failed,
368
403
  message: result.message?.trim() ? result.message.trim() : null,
369
- records: result.records ?? []
404
+ records: result.records ?? [],
405
+ // "Run again" made one more; a first apply (or a replay of one) is one.
406
+ copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies)
370
407
  });
371
408
  onApplied?.();
372
409
  } catch (error) {
410
+ if (!stillHere()) return;
373
411
  setState({
374
412
  status: "error",
375
413
  message: error instanceof Error && error.message ? error.message : "That couldn't be applied just now. Please try again."
@@ -394,6 +432,15 @@ function ApplyDirectiveButton({
394
432
  bad && !state.message ? ` \xB7 ${state.failed} failed` : ""
395
433
  ] })
396
434
  ] }),
435
+ state.copies > 1 ? /* @__PURE__ */ jsx4(
436
+ "span",
437
+ {
438
+ className: "shrink-0 text-muted-foreground",
439
+ "data-apply-copies": state.copies,
440
+ title: records.length > 0 ? directive.directiveClass === "create" ? "Links to the latest copy." : "Links to the latest run." : void 0,
441
+ children: directive.directiveClass === "create" ? `${state.copies} copies` : `Ran ${state.copies} times`
442
+ }
443
+ ) : null,
397
444
  records.map((record) => /* @__PURE__ */ jsx4("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
398
445
  ...record,
399
446
  fallback: recordFallbackName(label2, record.id),
@@ -444,6 +491,7 @@ function ApplyDirectiveButton({
444
491
  }
445
492
  );
446
493
  }
494
+ const destructive = directive.directiveClass === "delete";
447
495
  return /* @__PURE__ */ jsxs2("span", { className: "inline-flex items-center gap-2", children: [
448
496
  /* @__PURE__ */ jsxs2(
449
497
  "button",
@@ -451,7 +499,8 @@ function ApplyDirectiveButton({
451
499
  type: "button",
452
500
  onClick: () => void handleApply(),
453
501
  disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
454
- 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",
502
+ "data-apply-tone": destructive ? "destructive" : "default",
503
+ 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"}`,
455
504
  children: [
456
505
  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" }),
457
506
  state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
@@ -546,7 +595,7 @@ function getDirectiveRenderer(directive) {
546
595
  }
547
596
 
548
597
  // directives/SideEffectDirectiveCard.tsx
549
- import { useState as useState2 } from "react";
598
+ import { useState as useState3 } from "react";
550
599
  import {
551
600
  asKindInstance,
552
601
  directiveDisplay as directiveDisplay3,
@@ -556,6 +605,39 @@ import {
556
605
  nounLabel as nounLabel2,
557
606
  nounTitleColumn
558
607
  } from "@ai-matrx/content-ir";
608
+
609
+ // directives/record-values.ts
610
+ import { useEffect as useEffect2, useState as useState2 } from "react";
611
+ function useRecordValues(read, ref) {
612
+ const key = ref && read ? `${ref.noun}:${ref.id}` : "";
613
+ const [result, setResult] = useState2({
614
+ key: "",
615
+ values: null,
616
+ status: "idle"
617
+ });
618
+ useEffect2(() => {
619
+ if (!key || !read) return;
620
+ const split = key.indexOf(":");
621
+ const target = { noun: key.slice(0, split), id: key.slice(split + 1) };
622
+ let live = true;
623
+ setResult({ key, values: null, status: "loading" });
624
+ read(target).then((values) => {
625
+ if (!live) return;
626
+ setResult({ key, values, status: values ? "ready" : "unavailable" });
627
+ }).catch(() => {
628
+ if (!live) return;
629
+ setResult({ key, values: null, status: "unavailable" });
630
+ });
631
+ return () => {
632
+ live = false;
633
+ };
634
+ }, [key, read]);
635
+ if (!key) return { values: null, status: "idle" };
636
+ if (result.key !== key) return { values: null, status: "loading" };
637
+ return { values: result.values, status: result.status };
638
+ }
639
+
640
+ // directives/SideEffectDirectiveCard.tsx
559
641
  import { Fragment, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
560
642
  var ROWS_BEFORE_FOLD = 3;
561
643
  function ItemRow({
@@ -609,44 +691,73 @@ function ItemRow({
609
691
  host.renderCopy?.({ label: title, value: item, kind, size: "xs" })
610
692
  ] });
611
693
  }
612
- function TargetRow({
613
- directive,
614
- item,
615
- recordId
694
+ function DirectiveChangeList({
695
+ changes,
696
+ className
616
697
  }) {
617
- const host = useDirectiveHost();
618
- const fallback = recordFallbackName(nounLabel2(directive.noun, host.nouns), recordId);
619
- const changes = directive.directiveClass === "update" ? itemChanges(item) : [];
620
- 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 });
621
- return /* @__PURE__ */ jsxs5("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
622
- /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 items-center gap-2", children: [
623
- /* @__PURE__ */ jsx7("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
624
- host.renderCopy?.({ label: fallback, value: item, size: "xs" })
625
- ] }),
626
- 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(
698
+ if (changes.length === 0) {
699
+ return /* @__PURE__ */ jsx7("p", { className: `text-[11px] text-muted-foreground${className ? ` ${className}` : ""}`, children: "Sets no fields \u2014 nothing would change." });
700
+ }
701
+ return /* @__PURE__ */ jsx7("ul", { className: `space-y-0.5${className ? ` ${className}` : ""}`, "data-directive-changes": "", children: changes.map((change) => {
702
+ const before = change.before && !change.same ? change.before : null;
703
+ const was = before ? before.clears ? "empty" : before.value : null;
704
+ return /* @__PURE__ */ jsxs5(
627
705
  "li",
628
706
  {
629
707
  className: "flex min-w-0 items-baseline gap-1.5 text-[11px]",
630
- title: `${change.label} \u2192 ${change.full}`,
708
+ title: before ? `${change.label}: ${before.clears ? "empty" : before.full} \u2192 ${change.full}` : `${change.label} \u2192 ${change.full}`,
709
+ "data-change-key": change.key,
631
710
  children: [
632
711
  /* @__PURE__ */ jsx7("span", { className: "shrink-0 text-muted-foreground", children: change.label }),
712
+ was !== null ? /* @__PURE__ */ jsx7(
713
+ "span",
714
+ {
715
+ className: "min-w-0 max-w-[40%] shrink truncate text-muted-foreground line-through",
716
+ "data-change-before": "",
717
+ children: was
718
+ }
719
+ ) : null,
633
720
  /* @__PURE__ */ jsx7("span", { "aria-hidden": true, className: "shrink-0 text-muted-foreground", children: "\u2192" }),
634
721
  /* @__PURE__ */ jsx7(
635
722
  "span",
636
723
  {
637
724
  className: `min-w-0 truncate ${change.clears ? "italic text-muted-foreground" : "text-foreground"}`,
725
+ "data-change-after": "",
638
726
  children: change.value
639
727
  }
640
- )
728
+ ),
729
+ change.same ? /* @__PURE__ */ jsx7("span", { className: "shrink-0 text-muted-foreground", children: "(no change)" }) : null
641
730
  ]
642
731
  },
643
732
  change.key
644
- )) }) : null
733
+ );
734
+ }) });
735
+ }
736
+ function TargetRow({
737
+ directive,
738
+ item,
739
+ recordId
740
+ }) {
741
+ const host = useDirectiveHost();
742
+ const fallback = recordFallbackName(nounLabel2(directive.noun, host.nouns), recordId);
743
+ const isUpdate = directive.directiveClass === "update";
744
+ const current = useRecordValues(
745
+ isUpdate ? host.readRecord : void 0,
746
+ isUpdate ? { noun: directive.noun, id: recordId } : null
747
+ );
748
+ const changes = isUpdate ? itemChanges(item, current.values, { titleColumn: nounTitleColumn(directive.noun, host.nouns) }) : [];
749
+ 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 });
750
+ return /* @__PURE__ */ jsxs5("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
751
+ /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 items-center gap-2", children: [
752
+ /* @__PURE__ */ jsx7("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
753
+ host.renderCopy?.({ label: fallback, value: item, size: "xs" })
754
+ ] }),
755
+ isUpdate ? /* @__PURE__ */ jsx7(DirectiveChangeList, { changes, className: "mt-0.5 px-1" }) : null
645
756
  ] });
646
757
  }
647
758
  function SideEffectDirectiveCard({ directive }) {
648
759
  const host = useDirectiveHost();
649
- const [expanded, setExpanded] = useState2(false);
760
+ const [expanded, setExpanded] = useState3(false);
650
761
  const items = directive.items;
651
762
  const total = items.length;
652
763
  const display = directiveDisplay3(directive.directiveClass, directive.noun, host.nouns);
@@ -784,10 +895,10 @@ function KindInstanceRenderInner({
784
895
  className
785
896
  }) {
786
897
  const host = useContentIrHost();
787
- const [routingStatus, setRoutingStatus] = useState3(
898
+ const [routingStatus, setRoutingStatus] = useState4(
788
899
  () => kindIsRoutable(kind, host) ? "routable" : "checking"
789
900
  );
790
- useEffect2(() => {
901
+ useEffect3(() => {
791
902
  let cancelled = false;
792
903
  let warmed = false;
793
904
  const syncRoutingStatus = () => {