@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/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.0 — 2026-10-02
4
+
5
+ The G3 review of the action cards (nightly clone). Guard: `__tests__/directive-card-g3.test.tsx`
6
+ (7 cases, every one red on 0.15.2).
7
+
8
+ - **An update says old → new.** New host seam `DirectiveHost.readRecord({noun, id})` →
9
+ the record's current fields (or null). `itemChanges(item, current)` now carries `before`
10
+ and `same`; the new `DirectiveChangeList` draws "Label old → new" (old struck through,
11
+ "empty" for a blank field, "(no change)" when it already holds the value). ONE component
12
+ for the card row and a host's confirm dialog. `useRecordValues(read, ref)` reads it.
13
+ - **A Delete card's Apply looks destructive** (`data-apply-tone="destructive"`), not the
14
+ neutral grey of a create's.
15
+ - **Run again shows the copies.** `DirectiveApplyState.copies` (1 + every Run again) and
16
+ `records` = the LATEST copy's. The tally says "2 copies" (create) / "Ran 2 times" and
17
+ links the newest copy; a yes to Run again counts one more.
18
+ - **The card's state belongs to the block, not its slot.** A host that keys blocks by
19
+ position handed one card instance a different directive when a block was inserted
20
+ above it; an applied Update card flipped back to "Apply" until a reload. The Apply
21
+ control now resets and re-reads the ledger whenever the block it draws changes, and an
22
+ apply that finishes after its block moved away never lands on the new one.
23
+
24
+ Consumer action: pass `readRecord` on the directive host for old → new; a host's own
25
+ confirm dialog draws changes through `DirectiveChangeList`; an `applyState` answer passes
26
+ `copies` when its ledger knows it.
27
+
28
+ ## 0.15.2 — 2026-10-02
29
+
30
+ - While "Run again" asks, the tally stays on screen (only its own button goes
31
+ quiet) — it was swapped for a disabled Apply. No consumer action.
32
+
3
33
  ## 0.15.1 — 2026-10-02
4
34
 
5
35
  - The applied tally keeps its glyph beside its sentence: a long server
@@ -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,26 @@ 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) {
139
155
  const out = [];
140
156
  for (const [key, raw] of Object.entries(item)) {
141
157
  if (NOT_A_CHANGE.has(key)) continue;
142
- out.push({ key, label: fieldLabel(key), ...describeValue(raw) });
158
+ const change = { key, label: fieldLabel(key), ...describeValue(raw) };
159
+ if (current && Object.prototype.hasOwnProperty.call(current, key)) {
160
+ change.before = describeValue(current[key]);
161
+ change.same = sameValue(current[key], raw);
162
+ }
163
+ out.push(change);
143
164
  }
144
165
  return out;
145
166
  }
@@ -252,11 +273,23 @@ function ApplyDirectiveButton({
252
273
  const conversationId = scope.conversationId || void 0;
253
274
  const items = directive.items;
254
275
  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
- );
276
+ const initial = readsLedger ? { status: "checking" } : { status: "idle" };
277
+ const [state, setState] = (0, import_react2.useState)(initial);
258
278
  const [elapsed, setElapsed] = (0, import_react2.useState)(0);
279
+ const [askingAgain, setAskingAgain] = (0, import_react2.useState)(false);
259
280
  const startedAt = (0, import_react2.useRef)(null);
281
+ const identity = JSON.stringify([directive.slug, items, conversationId ?? null]);
282
+ const [stateFor, setStateFor] = (0, import_react2.useState)(identity);
283
+ if (stateFor !== identity) {
284
+ setStateFor(identity);
285
+ setState(initial);
286
+ setAskingAgain(false);
287
+ setElapsed(0);
288
+ }
289
+ const currentIdentity = (0, import_react2.useRef)(identity);
290
+ (0, import_react2.useEffect)(() => {
291
+ currentIdentity.current = identity;
292
+ }, [identity]);
260
293
  const nounText = (0, import_content_ir2.nounLabel)(directive.noun, host.nouns);
261
294
  const shellKey = readsLedger ? JSON.stringify([directive.slug, items, conversationId ?? null]) : "";
262
295
  const read = host.applyState;
@@ -279,7 +312,8 @@ function ApplyDirectiveButton({
279
312
  applied: shellItems.length,
280
313
  failed: 0,
281
314
  message: answer.message?.trim() ? answer.message.trim() : null,
282
- records: answer.records ?? []
315
+ records: answer.records ?? [],
316
+ copies: Math.max(1, answer.copies ?? 1)
283
317
  } : prev
284
318
  );
285
319
  } else if (answer.state === "in_flight" && polls < FINISH_POLLS_MAX) {
@@ -319,8 +353,12 @@ function ApplyDirectiveButton({
319
353
  async function handleApply(again = false) {
320
354
  if (!confirm) return;
321
355
  const before = state;
356
+ const askedFor = identity;
357
+ const stillHere = () => currentIdentity.current === askedFor;
358
+ const priorCopies = state.status === "applied" ? state.copies : 0;
322
359
  if (host.ask) {
323
- setState({ status: "asking" });
360
+ if (again) setAskingAgain(true);
361
+ else setState({ status: "asking" });
324
362
  let ok = false;
325
363
  try {
326
364
  ok = await host.ask({
@@ -330,12 +368,16 @@ function ApplyDirectiveButton({
330
368
  ...again ? { again: true } : {}
331
369
  });
332
370
  } catch (error) {
371
+ if (!stillHere()) return;
372
+ setAskingAgain(false);
333
373
  setState({
334
374
  status: "error",
335
375
  message: error instanceof Error && error.message ? error.message : "We could not ask you to confirm this, so nothing was done."
336
376
  });
337
377
  return;
338
378
  }
379
+ if (!stillHere()) return;
380
+ setAskingAgain(false);
339
381
  if (!ok) {
340
382
  setState(again ? before : { status: "idle" });
341
383
  return;
@@ -348,15 +390,19 @@ function ApplyDirectiveButton({
348
390
  if (conversationId) shell.conversationId = conversationId;
349
391
  if (again) shell.force = true;
350
392
  const result = await confirm(shell);
393
+ if (!stillHere()) return;
351
394
  setState({
352
395
  status: "applied",
353
396
  applied: result.applied,
354
397
  failed: result.failed,
355
398
  message: result.message?.trim() ? result.message.trim() : null,
356
- records: result.records ?? []
399
+ records: result.records ?? [],
400
+ // "Run again" made one more; a first apply (or a replay of one) is one.
401
+ copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies)
357
402
  });
358
403
  onApplied?.();
359
404
  } catch (error) {
405
+ if (!stillHere()) return;
360
406
  setState({
361
407
  status: "error",
362
408
  message: error instanceof Error && error.message ? error.message : "That couldn't be applied just now. Please try again."
@@ -381,6 +427,15 @@ function ApplyDirectiveButton({
381
427
  bad && !state.message ? ` \xB7 ${state.failed} failed` : ""
382
428
  ] })
383
429
  ] }),
430
+ state.copies > 1 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
431
+ "span",
432
+ {
433
+ className: "shrink-0 text-muted-foreground",
434
+ "data-apply-copies": state.copies,
435
+ title: records.length > 0 ? directive.directiveClass === "create" ? "Links to the latest copy." : "Links to the latest run." : void 0,
436
+ children: directive.directiveClass === "create" ? `${state.copies} copies` : `Ran ${state.copies} times`
437
+ }
438
+ ) : null,
384
439
  records.map((record) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
385
440
  ...record,
386
441
  fallback: recordFallbackName(label2, record.id),
@@ -391,8 +446,9 @@ function ApplyDirectiveButton({
391
446
  "button",
392
447
  {
393
448
  type: "button",
449
+ disabled: askingAgain,
394
450
  onClick: () => void handleApply(!bad),
395
- className: "inline-flex items-center gap-1 rounded px-1 py-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground",
451
+ 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",
396
452
  "data-apply-action": bad ? "retry" : "run-again",
397
453
  children: [
398
454
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RotateCcwIcon, { className: "h-3 w-3" }),
@@ -430,6 +486,7 @@ function ApplyDirectiveButton({
430
486
  }
431
487
  );
432
488
  }
489
+ const destructive = directive.directiveClass === "delete";
433
490
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "inline-flex items-center gap-2", children: [
434
491
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
435
492
  "button",
@@ -437,7 +494,8 @@ function ApplyDirectiveButton({
437
494
  type: "button",
438
495
  onClick: () => void handleApply(),
439
496
  disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
440
- 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",
497
+ "data-apply-tone": destructive ? "destructive" : "default",
498
+ 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"}`,
441
499
  children: [
442
500
  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" }),
443
501
  state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
@@ -452,8 +510,41 @@ function ApplyDirectiveButton({
452
510
  }
453
511
 
454
512
  // directives/SideEffectDirectiveCard.tsx
455
- var import_react3 = require("react");
513
+ var import_react4 = require("react");
456
514
  var import_content_ir3 = require("@ai-matrx/content-ir");
515
+
516
+ // directives/record-values.ts
517
+ var import_react3 = require("react");
518
+ function useRecordValues(read, ref) {
519
+ const key = ref && read ? `${ref.noun}:${ref.id}` : "";
520
+ const [result, setResult] = (0, import_react3.useState)({
521
+ key: "",
522
+ values: null,
523
+ status: "idle"
524
+ });
525
+ (0, import_react3.useEffect)(() => {
526
+ if (!key || !read) return;
527
+ const split = key.indexOf(":");
528
+ const target = { noun: key.slice(0, split), id: key.slice(split + 1) };
529
+ let live = true;
530
+ setResult({ key, values: null, status: "loading" });
531
+ read(target).then((values) => {
532
+ if (!live) return;
533
+ setResult({ key, values, status: values ? "ready" : "unavailable" });
534
+ }).catch(() => {
535
+ if (!live) return;
536
+ setResult({ key, values: null, status: "unavailable" });
537
+ });
538
+ return () => {
539
+ live = false;
540
+ };
541
+ }, [key, read]);
542
+ if (!key) return { values: null, status: "idle" };
543
+ if (result.key !== key) return { values: null, status: "loading" };
544
+ return { values: result.values, status: result.status };
545
+ }
546
+
547
+ // directives/SideEffectDirectiveCard.tsx
457
548
  var import_jsx_runtime4 = require("react/jsx-runtime");
458
549
  var ROWS_BEFORE_FOLD = 3;
459
550
  function ItemRow({
@@ -507,44 +598,73 @@ function ItemRow({
507
598
  host.renderCopy?.({ label: title, value: item, kind, size: "xs" })
508
599
  ] });
509
600
  }
510
- function TargetRow({
511
- directive,
512
- item,
513
- recordId
601
+ function DirectiveChangeList({
602
+ changes,
603
+ className
514
604
  }) {
515
- const host = useDirectiveHost();
516
- const fallback = recordFallbackName((0, import_content_ir3.nounLabel)(directive.noun, host.nouns), recordId);
517
- const changes = directive.directiveClass === "update" ? itemChanges(item) : [];
518
- 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 });
519
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
520
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
521
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
522
- host.renderCopy?.({ label: fallback, value: item, size: "xs" })
523
- ] }),
524
- 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)(
605
+ if (changes.length === 0) {
606
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: `text-[11px] text-muted-foreground${className ? ` ${className}` : ""}`, children: "Sets no fields \u2014 nothing would change." });
607
+ }
608
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { className: `space-y-0.5${className ? ` ${className}` : ""}`, "data-directive-changes": "", children: changes.map((change) => {
609
+ const before = change.before && !change.same ? change.before : null;
610
+ const was = before ? before.clears ? "empty" : before.value : null;
611
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
525
612
  "li",
526
613
  {
527
614
  className: "flex min-w-0 items-baseline gap-1.5 text-[11px]",
528
- title: `${change.label} \u2192 ${change.full}`,
615
+ title: before ? `${change.label}: ${before.clears ? "empty" : before.full} \u2192 ${change.full}` : `${change.label} \u2192 ${change.full}`,
616
+ "data-change-key": change.key,
529
617
  children: [
530
618
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "shrink-0 text-muted-foreground", children: change.label }),
619
+ was !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
620
+ "span",
621
+ {
622
+ className: "min-w-0 max-w-[40%] shrink truncate text-muted-foreground line-through",
623
+ "data-change-before": "",
624
+ children: was
625
+ }
626
+ ) : null,
531
627
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { "aria-hidden": true, className: "shrink-0 text-muted-foreground", children: "\u2192" }),
532
628
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
533
629
  "span",
534
630
  {
535
631
  className: `min-w-0 truncate ${change.clears ? "italic text-muted-foreground" : "text-foreground"}`,
632
+ "data-change-after": "",
536
633
  children: change.value
537
634
  }
538
- )
635
+ ),
636
+ change.same ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "shrink-0 text-muted-foreground", children: "(no change)" }) : null
539
637
  ]
540
638
  },
541
639
  change.key
542
- )) }) : null
640
+ );
641
+ }) });
642
+ }
643
+ function TargetRow({
644
+ directive,
645
+ item,
646
+ recordId
647
+ }) {
648
+ const host = useDirectiveHost();
649
+ const fallback = recordFallbackName((0, import_content_ir3.nounLabel)(directive.noun, host.nouns), recordId);
650
+ const isUpdate = directive.directiveClass === "update";
651
+ const current = useRecordValues(
652
+ isUpdate ? host.readRecord : void 0,
653
+ isUpdate ? { noun: directive.noun, id: recordId } : null
654
+ );
655
+ const changes = isUpdate ? itemChanges(item, current.values) : [];
656
+ 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 });
657
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
658
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
659
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
660
+ host.renderCopy?.({ label: fallback, value: item, size: "xs" })
661
+ ] }),
662
+ isUpdate ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DirectiveChangeList, { changes, className: "mt-0.5 px-1" }) : null
543
663
  ] });
544
664
  }
545
665
  function SideEffectDirectiveCard({ directive }) {
546
666
  const host = useDirectiveHost();
547
- const [expanded, setExpanded] = (0, import_react3.useState)(false);
667
+ const [expanded, setExpanded] = (0, import_react4.useState)(false);
548
668
  const items = directive.items;
549
669
  const total = items.length;
550
670
  const display = (0, import_content_ir3.directiveDisplay)(directive.directiveClass, directive.noun, host.nouns);