@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/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.1 — 2026-10-02
4
+
5
+ - **A record's title column reads "Title" everywhere.** `itemChanges(item, current,
6
+ { titleColumn })` labels the noun's title column "Title" (a note's is stored in
7
+ `label`, and the confirm said "Label" while the form said "Title"); the card's update
8
+ row passes `nounTitleColumn`. Guard: `__tests__/directive-card-g3.test.tsx` ("a
9
+ record's title column…", 2 red on 0.16.0).
10
+
11
+ Consumer action: a host's own confirm passes `{ titleColumn }` to `itemChanges`.
12
+
13
+ ## 0.16.0 — 2026-10-02
14
+
15
+ The G3 review of the action cards (nightly clone). Guard: `__tests__/directive-card-g3.test.tsx`
16
+ (7 cases, every one red on 0.15.2).
17
+
18
+ - **An update says old → new.** New host seam `DirectiveHost.readRecord({noun, id})` →
19
+ the record's current fields (or null). `itemChanges(item, current)` now carries `before`
20
+ and `same`; the new `DirectiveChangeList` draws "Label old → new" (old struck through,
21
+ "empty" for a blank field, "(no change)" when it already holds the value). ONE component
22
+ for the card row and a host's confirm dialog. `useRecordValues(read, ref)` reads it.
23
+ - **A Delete card's Apply looks destructive** (`data-apply-tone="destructive"`), not the
24
+ neutral grey of a create's.
25
+ - **Run again shows the copies.** `DirectiveApplyState.copies` (1 + every Run again) and
26
+ `records` = the LATEST copy's. The tally says "2 copies" (create) / "Ran 2 times" and
27
+ links the newest copy; a yes to Run again counts one more.
28
+ - **The card's state belongs to the block, not its slot.** A host that keys blocks by
29
+ position handed one card instance a different directive when a block was inserted
30
+ above it; an applied Update card flipped back to "Apply" until a reload. The Apply
31
+ control now resets and re-reads the ledger whenever the block it draws changes, and an
32
+ apply that finishes after its block moved away never lands on the new one.
33
+
34
+ Consumer action: pass `readRecord` on the directive host for old → new; a host's own
35
+ confirm dialog draws changes through `DirectiveChangeList`; an `applyState` answer passes
36
+ `copies` when its ledger knows it.
37
+
3
38
  ## 0.15.2 — 2026-10-02
4
39
 
5
40
  - While "Run again" asks, the tally stays on screen (only its own button goes
@@ -32,8 +32,17 @@ interface DirectiveApplyState {
32
32
  state: "not_applied" | "in_flight" | "applied";
33
33
  /** The ledger row's sentence ("Created task."), never composed client-side. */
34
34
  message?: string | null;
35
- /** The records the original apply wrote — the tally's doors. */
35
+ /**
36
+ * The records the LATEST apply wrote — the tally's doors. After "Run again"
37
+ * that is the newest copy, never the first.
38
+ */
36
39
  records?: DirectiveRecordRef[];
40
+ /**
41
+ * How many times the ledger holds this block as applied: 1 for the original,
42
+ * one more for every "Run again". Absent → 1. A create that ran twice made
43
+ * two records, and the card says so (reviewer, 2026-10-02).
44
+ */
45
+ copies?: number;
37
46
  }
38
47
  /** One record a directive names: the noun it is, and its id. */
39
48
  interface DirectiveRecordRef {
@@ -132,6 +141,14 @@ interface DirectiveHost {
132
141
  * is idempotent. Absent → the card starts at Apply, as before.
133
142
  */
134
143
  applyState?: (shell: DirectiveShell) => Promise<DirectiveApplyState>;
144
+ /**
145
+ * Read the CURRENT values of ONE existing record, so an update says what it
146
+ * replaces — "Status open → done", never just "→ done" (reviewer,
147
+ * 2026-10-02). Resolve `null` when the record cannot be read (no access, no
148
+ * such record, a noun the host cannot read); reject only for a real failure.
149
+ * Absent → an update row shows the new values alone, as before.
150
+ */
151
+ readRecord?: (ref: DirectiveRecordRef) => Promise<Record<string, unknown> | null>;
135
152
  /**
136
153
  * Draw ONE existing record by its live name, as a way to open it. Used on
137
154
  * update/delete rows (the record that will change), in the host's own ask
@@ -176,4 +193,4 @@ declare function DirectiveHostProvider({ host, children }: DirectiveHostProvider
176
193
  */
177
194
  declare function useDirectiveHost(): DirectiveHost;
178
195
 
179
- export { type DirectiveApplyResult as D, type DirectiveApplyState as a, type DirectiveAskRequest as b, type DirectiveCopyProps as c, type DirectiveHost as d, DirectiveHostProvider as e, type DirectiveHostProviderProps as f, type DirectiveOpenItemOptions as g, type DirectiveRecordProps as h, type DirectiveRecordRef as i, type DirectiveScope as j, DirectiveScopeProvider as k, type DirectiveShell as l, useDirectiveScope as m, useDirectiveHost as u };
196
+ export { type DirectiveRecordRef as D, type DirectiveApplyResult as a, type DirectiveApplyState as b, type DirectiveAskRequest as c, type DirectiveCopyProps as d, type DirectiveHost as e, DirectiveHostProvider as f, type DirectiveHostProviderProps as g, type DirectiveOpenItemOptions as h, type DirectiveRecordProps as i, type DirectiveScope as j, DirectiveScopeProvider as k, type DirectiveShell as l, useDirectiveScope as m, useDirectiveHost as u };
@@ -32,8 +32,17 @@ interface DirectiveApplyState {
32
32
  state: "not_applied" | "in_flight" | "applied";
33
33
  /** The ledger row's sentence ("Created task."), never composed client-side. */
34
34
  message?: string | null;
35
- /** The records the original apply wrote — the tally's doors. */
35
+ /**
36
+ * The records the LATEST apply wrote — the tally's doors. After "Run again"
37
+ * that is the newest copy, never the first.
38
+ */
36
39
  records?: DirectiveRecordRef[];
40
+ /**
41
+ * How many times the ledger holds this block as applied: 1 for the original,
42
+ * one more for every "Run again". Absent → 1. A create that ran twice made
43
+ * two records, and the card says so (reviewer, 2026-10-02).
44
+ */
45
+ copies?: number;
37
46
  }
38
47
  /** One record a directive names: the noun it is, and its id. */
39
48
  interface DirectiveRecordRef {
@@ -132,6 +141,14 @@ interface DirectiveHost {
132
141
  * is idempotent. Absent → the card starts at Apply, as before.
133
142
  */
134
143
  applyState?: (shell: DirectiveShell) => Promise<DirectiveApplyState>;
144
+ /**
145
+ * Read the CURRENT values of ONE existing record, so an update says what it
146
+ * replaces — "Status open → done", never just "→ done" (reviewer,
147
+ * 2026-10-02). Resolve `null` when the record cannot be read (no access, no
148
+ * such record, a noun the host cannot read); reject only for a real failure.
149
+ * Absent → an update row shows the new values alone, as before.
150
+ */
151
+ readRecord?: (ref: DirectiveRecordRef) => Promise<Record<string, unknown> | null>;
135
152
  /**
136
153
  * Draw ONE existing record by its live name, as a way to open it. Used on
137
154
  * update/delete rows (the record that will change), in the host's own ask
@@ -176,4 +193,4 @@ declare function DirectiveHostProvider({ host, children }: DirectiveHostProvider
176
193
  */
177
194
  declare function useDirectiveHost(): DirectiveHost;
178
195
 
179
- export { type DirectiveApplyResult as D, type DirectiveApplyState as a, type DirectiveAskRequest as b, type DirectiveCopyProps as c, type DirectiveHost as d, DirectiveHostProvider as e, type DirectiveHostProviderProps as f, type DirectiveOpenItemOptions as g, type DirectiveRecordProps as h, type DirectiveRecordRef as i, type DirectiveScope as j, DirectiveScopeProvider as k, type DirectiveShell as l, useDirectiveScope as m, useDirectiveHost as u };
196
+ export { type DirectiveRecordRef as D, type DirectiveApplyResult as a, type DirectiveApplyState as b, type DirectiveAskRequest as c, type DirectiveCopyProps as d, type DirectiveHost as e, DirectiveHostProvider as f, type DirectiveHostProviderProps as g, type DirectiveOpenItemOptions as h, type DirectiveRecordProps as i, type DirectiveScope as j, DirectiveScopeProvider as k, type DirectiveShell as l, useDirectiveScope as m, useDirectiveHost as u };
@@ -23,6 +23,7 @@ var directives_exports = {};
23
23
  __export(directives_exports, {
24
24
  ApplyDirectiveButton: () => ApplyDirectiveButton,
25
25
  CHANGE_VALUE_MAX: () => CHANGE_VALUE_MAX,
26
+ DirectiveChangeList: () => DirectiveChangeList,
26
27
  DirectiveFallbackCard: () => DirectiveFallbackCard,
27
28
  DirectiveHostProvider: () => DirectiveHostProvider,
28
29
  DirectiveProvisionalCard: () => DirectiveProvisionalCard,
@@ -30,6 +31,7 @@ __export(directives_exports, {
30
31
  DirectiveScopeProvider: () => DirectiveScopeProvider,
31
32
  SideEffectDirectiveCard: () => SideEffectDirectiveCard,
32
33
  TARGETING_CLASSES: () => TARGETING_CLASSES,
34
+ describeChangeValue: () => describeChangeValue,
33
35
  fieldLabel: () => fieldLabel,
34
36
  getDirectiveRenderer: () => getDirectiveRenderer,
35
37
  itemChanges: () => itemChanges,
@@ -39,7 +41,8 @@ __export(directives_exports, {
39
41
  registerDirectiveRenderer: () => registerDirectiveRenderer,
40
42
  resetDirectiveRenderers: () => resetDirectiveRenderers,
41
43
  useDirectiveHost: () => useDirectiveHost,
42
- useDirectiveScope: () => useDirectiveScope
44
+ useDirectiveScope: () => useDirectiveScope,
45
+ useRecordValues: () => useRecordValues
43
46
  });
44
47
  module.exports = __toCommonJS(directives_exports);
45
48
 
@@ -107,6 +110,9 @@ function recordFallbackName(nounLabel3, id) {
107
110
  const short = (0, import_uuid.isUuidShape)(id) ? id.slice(0, 8) : id.length > 12 ? `${id.slice(0, 12)}\u2026` : id;
108
111
  return `${nounLabel3} ${short}`;
109
112
  }
113
+ function describeChangeValue(value) {
114
+ return describeValue(value);
115
+ }
110
116
  function describeValue(value) {
111
117
  if (value === null || value === void 0) return { value: "cleared", full: "cleared", clears: true };
112
118
  if (typeof value === "string") {
@@ -135,11 +141,28 @@ function safeJson(value) {
135
141
  return String(value);
136
142
  }
137
143
  }
138
- function itemChanges(item) {
144
+ function sameValue(a, b) {
145
+ if (a === b) return true;
146
+ if (a === null || a === void 0 || b === null || b === void 0) {
147
+ const emptyA = a === null || a === void 0 || a === "";
148
+ const emptyB = b === null || b === void 0 || b === "";
149
+ return emptyA && emptyB;
150
+ }
151
+ if (typeof a === "object" || typeof b === "object") return safeJson(a) === safeJson(b);
152
+ return false;
153
+ }
154
+ function itemChanges(item, current, options = {}) {
139
155
  const out = [];
156
+ const titleColumn = options.titleColumn ?? null;
140
157
  for (const [key, raw] of Object.entries(item)) {
141
158
  if (NOT_A_CHANGE.has(key)) continue;
142
- out.push({ key, label: fieldLabel(key), ...describeValue(raw) });
159
+ const label = key === titleColumn ? "Title" : fieldLabel(key);
160
+ const change = { key, label, ...describeValue(raw) };
161
+ if (current && Object.prototype.hasOwnProperty.call(current, key)) {
162
+ change.before = describeValue(current[key]);
163
+ change.same = sameValue(current[key], raw);
164
+ }
165
+ out.push(change);
143
166
  }
144
167
  return out;
145
168
  }
@@ -252,12 +275,23 @@ function ApplyDirectiveButton({
252
275
  const conversationId = scope.conversationId || void 0;
253
276
  const items = directive.items;
254
277
  const readsLedger = !!host.applyState && !!host.confirm && directive.parsed.executes && items.length > 0;
255
- const [state, setState] = (0, import_react2.useState)(
256
- readsLedger ? { status: "checking" } : { status: "idle" }
257
- );
278
+ const initial = readsLedger ? { status: "checking" } : { status: "idle" };
279
+ const [state, setState] = (0, import_react2.useState)(initial);
258
280
  const [elapsed, setElapsed] = (0, import_react2.useState)(0);
259
281
  const [askingAgain, setAskingAgain] = (0, import_react2.useState)(false);
260
282
  const startedAt = (0, import_react2.useRef)(null);
283
+ const identity = JSON.stringify([directive.slug, items, conversationId ?? null]);
284
+ const [stateFor, setStateFor] = (0, import_react2.useState)(identity);
285
+ if (stateFor !== identity) {
286
+ setStateFor(identity);
287
+ setState(initial);
288
+ setAskingAgain(false);
289
+ setElapsed(0);
290
+ }
291
+ const currentIdentity = (0, import_react2.useRef)(identity);
292
+ (0, import_react2.useEffect)(() => {
293
+ currentIdentity.current = identity;
294
+ }, [identity]);
261
295
  const nounText = (0, import_content_ir2.nounLabel)(directive.noun, host.nouns);
262
296
  const shellKey = readsLedger ? JSON.stringify([directive.slug, items, conversationId ?? null]) : "";
263
297
  const read = host.applyState;
@@ -280,7 +314,8 @@ function ApplyDirectiveButton({
280
314
  applied: shellItems.length,
281
315
  failed: 0,
282
316
  message: answer.message?.trim() ? answer.message.trim() : null,
283
- records: answer.records ?? []
317
+ records: answer.records ?? [],
318
+ copies: Math.max(1, answer.copies ?? 1)
284
319
  } : prev
285
320
  );
286
321
  } else if (answer.state === "in_flight" && polls < FINISH_POLLS_MAX) {
@@ -320,6 +355,9 @@ function ApplyDirectiveButton({
320
355
  async function handleApply(again = false) {
321
356
  if (!confirm) return;
322
357
  const before = state;
358
+ const askedFor = identity;
359
+ const stillHere = () => currentIdentity.current === askedFor;
360
+ const priorCopies = state.status === "applied" ? state.copies : 0;
323
361
  if (host.ask) {
324
362
  if (again) setAskingAgain(true);
325
363
  else setState({ status: "asking" });
@@ -332,6 +370,7 @@ function ApplyDirectiveButton({
332
370
  ...again ? { again: true } : {}
333
371
  });
334
372
  } catch (error) {
373
+ if (!stillHere()) return;
335
374
  setAskingAgain(false);
336
375
  setState({
337
376
  status: "error",
@@ -339,6 +378,7 @@ function ApplyDirectiveButton({
339
378
  });
340
379
  return;
341
380
  }
381
+ if (!stillHere()) return;
342
382
  setAskingAgain(false);
343
383
  if (!ok) {
344
384
  setState(again ? before : { status: "idle" });
@@ -352,15 +392,19 @@ function ApplyDirectiveButton({
352
392
  if (conversationId) shell.conversationId = conversationId;
353
393
  if (again) shell.force = true;
354
394
  const result = await confirm(shell);
395
+ if (!stillHere()) return;
355
396
  setState({
356
397
  status: "applied",
357
398
  applied: result.applied,
358
399
  failed: result.failed,
359
400
  message: result.message?.trim() ? result.message.trim() : null,
360
- records: result.records ?? []
401
+ records: result.records ?? [],
402
+ // "Run again" made one more; a first apply (or a replay of one) is one.
403
+ copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies)
361
404
  });
362
405
  onApplied?.();
363
406
  } catch (error) {
407
+ if (!stillHere()) return;
364
408
  setState({
365
409
  status: "error",
366
410
  message: error instanceof Error && error.message ? error.message : "That couldn't be applied just now. Please try again."
@@ -385,6 +429,15 @@ function ApplyDirectiveButton({
385
429
  bad && !state.message ? ` \xB7 ${state.failed} failed` : ""
386
430
  ] })
387
431
  ] }),
432
+ state.copies > 1 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
433
+ "span",
434
+ {
435
+ className: "shrink-0 text-muted-foreground",
436
+ "data-apply-copies": state.copies,
437
+ title: records.length > 0 ? directive.directiveClass === "create" ? "Links to the latest copy." : "Links to the latest run." : void 0,
438
+ children: directive.directiveClass === "create" ? `${state.copies} copies` : `Ran ${state.copies} times`
439
+ }
440
+ ) : null,
388
441
  records.map((record) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
389
442
  ...record,
390
443
  fallback: recordFallbackName(label2, record.id),
@@ -435,6 +488,7 @@ function ApplyDirectiveButton({
435
488
  }
436
489
  );
437
490
  }
491
+ const destructive = directive.directiveClass === "delete";
438
492
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "inline-flex items-center gap-2", children: [
439
493
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
440
494
  "button",
@@ -442,7 +496,8 @@ function ApplyDirectiveButton({
442
496
  type: "button",
443
497
  onClick: () => void handleApply(),
444
498
  disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
445
- 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",
499
+ "data-apply-tone": destructive ? "destructive" : "default",
500
+ 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"}`,
446
501
  children: [
447
502
  state.status === "applying" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LoaderIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PlayIcon, { className: "h-3.5 w-3.5" }),
448
503
  state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
@@ -457,8 +512,41 @@ function ApplyDirectiveButton({
457
512
  }
458
513
 
459
514
  // directives/SideEffectDirectiveCard.tsx
460
- var import_react3 = require("react");
515
+ var import_react4 = require("react");
461
516
  var import_content_ir3 = require("@ai-matrx/content-ir");
517
+
518
+ // directives/record-values.ts
519
+ var import_react3 = require("react");
520
+ function useRecordValues(read, ref) {
521
+ const key = ref && read ? `${ref.noun}:${ref.id}` : "";
522
+ const [result, setResult] = (0, import_react3.useState)({
523
+ key: "",
524
+ values: null,
525
+ status: "idle"
526
+ });
527
+ (0, import_react3.useEffect)(() => {
528
+ if (!key || !read) return;
529
+ const split = key.indexOf(":");
530
+ const target = { noun: key.slice(0, split), id: key.slice(split + 1) };
531
+ let live = true;
532
+ setResult({ key, values: null, status: "loading" });
533
+ read(target).then((values) => {
534
+ if (!live) return;
535
+ setResult({ key, values, status: values ? "ready" : "unavailable" });
536
+ }).catch(() => {
537
+ if (!live) return;
538
+ setResult({ key, values: null, status: "unavailable" });
539
+ });
540
+ return () => {
541
+ live = false;
542
+ };
543
+ }, [key, read]);
544
+ if (!key) return { values: null, status: "idle" };
545
+ if (result.key !== key) return { values: null, status: "loading" };
546
+ return { values: result.values, status: result.status };
547
+ }
548
+
549
+ // directives/SideEffectDirectiveCard.tsx
462
550
  var import_jsx_runtime4 = require("react/jsx-runtime");
463
551
  var ROWS_BEFORE_FOLD = 3;
464
552
  function ItemRow({
@@ -512,44 +600,73 @@ function ItemRow({
512
600
  host.renderCopy?.({ label: title, value: item, kind, size: "xs" })
513
601
  ] });
514
602
  }
515
- function TargetRow({
516
- directive,
517
- item,
518
- recordId
603
+ function DirectiveChangeList({
604
+ changes,
605
+ className
519
606
  }) {
520
- const host = useDirectiveHost();
521
- const fallback = recordFallbackName((0, import_content_ir3.nounLabel)(directive.noun, host.nouns), recordId);
522
- const changes = directive.directiveClass === "update" ? itemChanges(item) : [];
523
- const name = host.renderRecord ? host.renderRecord({ noun: directive.noun, id: recordId, fallback, context: "row" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "truncate font-medium text-foreground", children: fallback });
524
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
525
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
526
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
527
- host.renderCopy?.({ label: fallback, value: item, size: "xs" })
528
- ] }),
529
- directive.directiveClass === "update" ? changes.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "px-1 text-[11px] text-muted-foreground", children: "Sets no fields \u2014 nothing would change." }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { className: "mt-0.5 space-y-0.5 px-1", children: changes.map((change) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
607
+ if (changes.length === 0) {
608
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: `text-[11px] text-muted-foreground${className ? ` ${className}` : ""}`, children: "Sets no fields \u2014 nothing would change." });
609
+ }
610
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { className: `space-y-0.5${className ? ` ${className}` : ""}`, "data-directive-changes": "", children: changes.map((change) => {
611
+ const before = change.before && !change.same ? change.before : null;
612
+ const was = before ? before.clears ? "empty" : before.value : null;
613
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
530
614
  "li",
531
615
  {
532
616
  className: "flex min-w-0 items-baseline gap-1.5 text-[11px]",
533
- title: `${change.label} \u2192 ${change.full}`,
617
+ title: before ? `${change.label}: ${before.clears ? "empty" : before.full} \u2192 ${change.full}` : `${change.label} \u2192 ${change.full}`,
618
+ "data-change-key": change.key,
534
619
  children: [
535
620
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "shrink-0 text-muted-foreground", children: change.label }),
621
+ was !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
622
+ "span",
623
+ {
624
+ className: "min-w-0 max-w-[40%] shrink truncate text-muted-foreground line-through",
625
+ "data-change-before": "",
626
+ children: was
627
+ }
628
+ ) : null,
536
629
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { "aria-hidden": true, className: "shrink-0 text-muted-foreground", children: "\u2192" }),
537
630
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
538
631
  "span",
539
632
  {
540
633
  className: `min-w-0 truncate ${change.clears ? "italic text-muted-foreground" : "text-foreground"}`,
634
+ "data-change-after": "",
541
635
  children: change.value
542
636
  }
543
- )
637
+ ),
638
+ change.same ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "shrink-0 text-muted-foreground", children: "(no change)" }) : null
544
639
  ]
545
640
  },
546
641
  change.key
547
- )) }) : null
642
+ );
643
+ }) });
644
+ }
645
+ function TargetRow({
646
+ directive,
647
+ item,
648
+ recordId
649
+ }) {
650
+ const host = useDirectiveHost();
651
+ const fallback = recordFallbackName((0, import_content_ir3.nounLabel)(directive.noun, host.nouns), recordId);
652
+ const isUpdate = directive.directiveClass === "update";
653
+ const current = useRecordValues(
654
+ isUpdate ? host.readRecord : void 0,
655
+ isUpdate ? { noun: directive.noun, id: recordId } : null
656
+ );
657
+ const changes = isUpdate ? itemChanges(item, current.values, { titleColumn: (0, import_content_ir3.nounTitleColumn)(directive.noun, host.nouns) }) : [];
658
+ const name = host.renderRecord ? host.renderRecord({ noun: directive.noun, id: recordId, fallback, context: "row" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "truncate font-medium text-foreground", children: fallback });
659
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
660
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
661
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
662
+ host.renderCopy?.({ label: fallback, value: item, size: "xs" })
663
+ ] }),
664
+ isUpdate ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DirectiveChangeList, { changes, className: "mt-0.5 px-1" }) : null
548
665
  ] });
549
666
  }
550
667
  function SideEffectDirectiveCard({ directive }) {
551
668
  const host = useDirectiveHost();
552
- const [expanded, setExpanded] = (0, import_react3.useState)(false);
669
+ const [expanded, setExpanded] = (0, import_react4.useState)(false);
553
670
  const items = directive.items;
554
671
  const total = items.length;
555
672
  const display = (0, import_content_ir3.directiveDisplay)(directive.directiveClass, directive.noun, host.nouns);