@ai-matrx/content-ir-react 0.17.1 → 0.18.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.18.1 — 2026-10-02
4
+
5
+ - **An apply belongs to the block, not to the card that asked.** A note re-renders
6
+ its blocks while the confirm dialog is open; the card that asked was unmounted,
7
+ the apply still ran, and the fresh card — which had read the ledger before the
8
+ write landed — offered "Apply" for an update that had already run. From the yes
9
+ on, the apply's progress and outcome are held per apply authority (the host's
10
+ `confirm`) and block identity, and every card drawing that block shows them.
11
+ Guard: `__tests__/directive-card-g6a.test.tsx` ("a card re-mounted while its
12
+ confirm was open…", red on 0.18.0).
13
+
14
+ No consumer action.
15
+
16
+ ## 0.18.0 — 2026-10-02
17
+
18
+ The G6A review of the action cards (nightly clone). Guard:
19
+ `__tests__/directive-card-g6a.test.tsx` (3 red on 0.17.1).
20
+
21
+ - **An applied update says what it changed, from the record of the change.** After
22
+ an Update applied, a reload read the record live for the old value — which by
23
+ then IS the new value — and the row said "Title → X (no change)". The rows now
24
+ compare against the live record only while the block is open; once applied they
25
+ draw the old value from `before` — new on `DirectiveApplyResult` and
26
+ `DirectiveApplyState`, each item's replaced values by item index, which aidream
27
+ ledgers at apply time (1122747fb5) — and without it say only "Title → new".
28
+ Until the ledger answers, a row compares against nothing. New exports:
29
+ `DirectiveApplyPhase`, `readsApplyLedger`; `ApplyDirectiveButton` takes `onPhase`.
30
+ - **An update that sets no field says so before the click.** "Nothing to change"
31
+ beside a muted Apply (`data-apply-tone="muted"`, `data-apply-nothing`). Apply
32
+ still works — validation offers, never blocks.
33
+
34
+ Consumer action: a host's `confirm` passes `before` from the server's per-item
35
+ receipts, and its `applyState` passes `before` from each item of
36
+ `/directives/apply_state`.
37
+
3
38
  ## 0.17.1
4
39
 
5
40
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -15,6 +15,14 @@ interface DirectiveApplyResult {
15
15
  * (no dead ends). Rendered through `renderRecord`; absent → the sentence alone.
16
16
  */
17
17
  records?: DirectiveRecordRef[];
18
+ /**
19
+ * For an UPDATE: what each item's fields held just BEFORE the write, by item
20
+ * index (`null` for an item the server did not record). The card draws the
21
+ * applied change from this record — never from a live re-read, which after the
22
+ * apply answers the NEW value (G6A review, 2026-10-02: "(no change)" for a
23
+ * change that happened).
24
+ */
25
+ before?: ReadonlyArray<Record<string, unknown> | null>;
18
26
  }
19
27
  /**
20
28
  * What the server's action ledger says about ONE block, read on mount so a
@@ -43,6 +51,12 @@ interface DirectiveApplyState {
43
51
  * two records, and the card says so (reviewer, 2026-10-02).
44
52
  */
45
53
  copies?: number;
54
+ /**
55
+ * For an applied UPDATE: each item's replaced values, by item index, from the
56
+ * ledger row (`null` where the ledger has none — a row from before 2026-10-02).
57
+ * Absent → the card says only what each field was set to.
58
+ */
59
+ before?: ReadonlyArray<Record<string, unknown> | null>;
46
60
  }
47
61
  /** One record a directive names: the noun it is, and its id. */
48
62
  interface DirectiveRecordRef {
@@ -15,6 +15,14 @@ interface DirectiveApplyResult {
15
15
  * (no dead ends). Rendered through `renderRecord`; absent → the sentence alone.
16
16
  */
17
17
  records?: DirectiveRecordRef[];
18
+ /**
19
+ * For an UPDATE: what each item's fields held just BEFORE the write, by item
20
+ * index (`null` for an item the server did not record). The card draws the
21
+ * applied change from this record — never from a live re-read, which after the
22
+ * apply answers the NEW value (G6A review, 2026-10-02: "(no change)" for a
23
+ * change that happened).
24
+ */
25
+ before?: ReadonlyArray<Record<string, unknown> | null>;
18
26
  }
19
27
  /**
20
28
  * What the server's action ledger says about ONE block, read on mount so a
@@ -43,6 +51,12 @@ interface DirectiveApplyState {
43
51
  * two records, and the card says so (reviewer, 2026-10-02).
44
52
  */
45
53
  copies?: number;
54
+ /**
55
+ * For an applied UPDATE: each item's replaced values, by item index, from the
56
+ * ledger row (`null` where the ledger has none — a row from before 2026-10-02).
57
+ * Absent → the card says only what each field was set to.
58
+ */
59
+ before?: ReadonlyArray<Record<string, unknown> | null>;
46
60
  }
47
61
  /** One record a directive names: the noun it is, and its id. */
48
62
  interface DirectiveRecordRef {
@@ -39,6 +39,7 @@ __export(directives_exports, {
39
39
  itemChanges: () => itemChanges,
40
40
  itemRecordId: () => itemRecordId,
41
41
  provisionalDirectiveSlug: () => provisionalDirectiveSlug,
42
+ readsApplyLedger: () => readsApplyLedger,
42
43
  recordFallbackName: () => recordFallbackName,
43
44
  registerDirectiveRenderer: () => registerDirectiveRenderer,
44
45
  resetDirectiveRenderers: () => resetDirectiveRenderers,
@@ -288,19 +289,52 @@ function classIcon(directiveClass, noun) {
288
289
 
289
290
  // directives/ApplyDirectiveButton.tsx
290
291
  var import_jsx_runtime3 = require("react/jsx-runtime");
292
+ var applyOutcomes = /* @__PURE__ */ new WeakMap();
293
+ var EMPTY_AUTHORITY = {};
294
+ var outcomeListeners = /* @__PURE__ */ new Set();
295
+ function outcomesOf(host) {
296
+ let held = applyOutcomes.get(host);
297
+ if (!held) {
298
+ held = /* @__PURE__ */ new Map();
299
+ applyOutcomes.set(host, held);
300
+ }
301
+ return held;
302
+ }
303
+ function publishOutcome(host, identity, outcome) {
304
+ outcomesOf(host).set(identity, outcome);
305
+ for (const listener of outcomeListeners) listener();
306
+ }
307
+ function subscribeOutcomes(listener) {
308
+ outcomeListeners.add(listener);
309
+ return () => {
310
+ outcomeListeners.delete(listener);
311
+ };
312
+ }
313
+ function useApplyOutcome(host, identity) {
314
+ return (0, import_react2.useSyncExternalStore)(
315
+ subscribeOutcomes,
316
+ () => applyOutcomes.get(host)?.get(identity) ?? null,
317
+ () => null
318
+ );
319
+ }
291
320
  var FINISH_POLL_MS = 2e3;
292
321
  var FINISH_POLLS_MAX = 30;
322
+ function readsApplyLedger(host, directive) {
323
+ return !!host.applyState && !!host.confirm && directive.parsed.executes && directive.items.length > 0;
324
+ }
293
325
  function ApplyDirectiveButton({
294
326
  directive,
295
327
  label = "Apply",
296
328
  itemCount,
297
- onApplied
329
+ onApplied,
330
+ onPhase,
331
+ nothingToChange = false
298
332
  }) {
299
333
  const host = useDirectiveHost();
300
334
  const scope = useDirectiveScope();
301
335
  const conversationId = scope.conversationId || void 0;
302
336
  const items = directive.items;
303
- const readsLedger = !!host.applyState && !!host.confirm && directive.parsed.executes && items.length > 0;
337
+ const readsLedger = readsApplyLedger(host, directive);
304
338
  const initial = readsLedger ? { status: "checking" } : { status: "idle" };
305
339
  const [state, setState] = (0, import_react2.useState)(initial);
306
340
  const [elapsed, setElapsed] = (0, import_react2.useState)(0);
@@ -314,6 +348,8 @@ function ApplyDirectiveButton({
314
348
  setAskingAgain(false);
315
349
  setElapsed(0);
316
350
  }
351
+ const shared = useApplyOutcome(host.confirm ?? EMPTY_AUTHORITY, identity);
352
+ const view = state.status === "asking" || !shared ? state : shared;
317
353
  const currentIdentity = (0, import_react2.useRef)(identity);
318
354
  (0, import_react2.useEffect)(() => {
319
355
  currentIdentity.current = identity;
@@ -341,7 +377,8 @@ function ApplyDirectiveButton({
341
377
  failed: 0,
342
378
  message: answer.message?.trim() ? answer.message.trim() : null,
343
379
  records: answer.records ?? [],
344
- copies: Math.max(1, answer.copies ?? 1)
380
+ copies: Math.max(1, answer.copies ?? 1),
381
+ before: answer.before ?? null
345
382
  } : prev
346
383
  );
347
384
  } else if (answer.state === "in_flight" && polls < FINISH_POLLS_MAX) {
@@ -366,24 +403,34 @@ function ApplyDirectiveButton({
366
403
  if (timer) clearTimeout(timer);
367
404
  };
368
405
  }, [shellKey, read, report]);
406
+ const appliedClean = view.status === "applied" && view.failed === 0 ? view : null;
407
+ const appliedBefore = appliedClean ? appliedClean.before : null;
408
+ const phaseKey = view.status === "checking" || view.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
409
+ (0, import_react2.useEffect)(() => {
410
+ if (!onPhase) return;
411
+ if (phaseKey === "pending") onPhase({ phase: "pending" });
412
+ else if (phaseKey === "open") onPhase({ phase: "open" });
413
+ else onPhase({ phase: "applied", before: appliedBefore });
414
+ }, [phaseKey, onPhase, appliedBefore]);
415
+ const applyingSince = view.status === "applying" ? view.startedAt : null;
369
416
  (0, import_react2.useEffect)(() => {
370
- if (state.status !== "applying") return;
371
- startedAt.current = Date.now();
417
+ if (applyingSince === null) return;
418
+ startedAt.current = applyingSince;
372
419
  const id = setInterval(
373
420
  () => setElapsed(Math.round((Date.now() - (startedAt.current ?? 0)) / 1e3)),
374
421
  1e3
375
422
  );
376
423
  return () => clearInterval(id);
377
- }, [state.status]);
424
+ }, [applyingSince]);
378
425
  if (!directive.parsed.executes) return null;
379
426
  const confirm = host.confirm;
380
427
  if (!confirm) return null;
381
428
  async function handleApply(again = false) {
382
429
  if (!confirm) return;
383
- const before = state;
430
+ const prior = view;
384
431
  const askedFor = identity;
385
432
  const stillHere = () => currentIdentity.current === askedFor;
386
- const priorCopies = state.status === "applied" ? state.copies : 0;
433
+ const priorCopies = view.status === "applied" ? view.copies : 0;
387
434
  if (host.ask) {
388
435
  if (again) setAskingAgain(true);
389
436
  else setState({ status: "asking" });
@@ -407,41 +454,43 @@ function ApplyDirectiveButton({
407
454
  if (!stillHere()) return;
408
455
  setAskingAgain(false);
409
456
  if (!ok) {
410
- setState(again ? before : { status: "idle" });
457
+ setState(again ? prior : { status: "idle" });
411
458
  return;
412
459
  }
413
460
  }
414
461
  setElapsed(0);
415
- setState({ status: "applying" });
462
+ setState((prev) => prev.status === "asking" ? { status: "idle" } : prev);
463
+ publishOutcome(confirm, askedFor, { status: "applying", startedAt: Date.now() });
416
464
  try {
417
465
  const shell = { __kind: directive.slug, items };
418
466
  if (conversationId) shell.conversationId = conversationId;
419
467
  if (again) shell.force = true;
420
468
  const result = await confirm(shell);
421
- if (!stillHere()) return;
422
- setState({
469
+ publishOutcome(confirm, askedFor, {
423
470
  status: "applied",
424
471
  applied: result.applied,
425
472
  failed: result.failed,
426
473
  message: result.message?.trim() ? result.message.trim() : null,
427
474
  records: result.records ?? [],
428
475
  // "Run again" made one more; a first apply (or a replay of one) is one.
429
- copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies)
476
+ copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies),
477
+ // "Run again" wrote the same values over themselves; what the FIRST
478
+ // apply replaced is still the change this block made.
479
+ before: again && prior.status === "applied" && prior.before ? prior.before : result.before ?? null
430
480
  });
431
- onApplied?.();
481
+ if (stillHere()) onApplied?.();
432
482
  } catch (error) {
433
- if (!stillHere()) return;
434
- setState({
483
+ publishOutcome(confirm, askedFor, {
435
484
  status: "error",
436
485
  message: error instanceof Error && error.message ? error.message : "That couldn't be applied just now. Please try again."
437
486
  });
438
487
  }
439
488
  }
440
- if (state.status === "applied") {
441
- const bad = state.failed > 0;
489
+ if (view.status === "applied") {
490
+ const bad = view.failed > 0;
442
491
  const trashed = directive.directiveClass === "delete";
443
492
  const label2 = nounText;
444
- const records = host.renderRecord ? state.records : [];
493
+ const records = host.renderRecord ? view.records : [];
445
494
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
446
495
  "span",
447
496
  {
@@ -455,26 +504,26 @@ function ApplyDirectiveButton({
455
504
  {
456
505
  words: failureWords(
457
506
  {
458
- raw: state.message ?? `${state.failed} failed`,
507
+ raw: view.message ?? `${view.failed} failed`,
459
508
  directiveClass: directive.directiveClass,
460
509
  noun: directive.noun,
461
510
  nounLabel: nounText,
462
- failed: state.failed,
463
- total: state.applied + state.failed
511
+ failed: view.failed,
512
+ total: view.applied + view.failed
464
513
  },
465
514
  host.explainFailure
466
515
  ),
467
- raw: state.message
516
+ raw: view.message
468
517
  }
469
- ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "min-w-0", children: state.message ?? `Applied ${state.applied}` })
518
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "min-w-0", children: view.message ?? `Applied ${view.applied}` })
470
519
  ] }),
471
- state.copies > 1 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
520
+ view.copies > 1 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
472
521
  "span",
473
522
  {
474
523
  className: "shrink-0 text-muted-foreground",
475
- "data-apply-copies": state.copies,
524
+ "data-apply-copies": view.copies,
476
525
  title: records.length > 0 ? directive.directiveClass === "create" ? "Links to the latest copy." : "Links to the latest run." : void 0,
477
- children: directive.directiveClass === "create" ? `${state.copies} copies` : `Ran ${state.copies} times`
526
+ children: directive.directiveClass === "create" ? `${view.copies} copies` : `Ran ${view.copies} times`
478
527
  }
479
528
  ) : null,
480
529
  records.map((record) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
@@ -501,7 +550,7 @@ function ApplyDirectiveButton({
501
550
  }
502
551
  );
503
552
  }
504
- if (state.status === "checking") {
553
+ if (view.status === "checking") {
505
554
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
506
555
  "span",
507
556
  {
@@ -513,7 +562,7 @@ function ApplyDirectiveButton({
513
562
  }
514
563
  );
515
564
  }
516
- if (state.status === "finishing") {
565
+ if (view.status === "finishing") {
517
566
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
518
567
  "span",
519
568
  {
@@ -528,22 +577,25 @@ function ApplyDirectiveButton({
528
577
  );
529
578
  }
530
579
  const destructive = directive.directiveClass === "delete";
580
+ const muted = nothingToChange && view.status === "idle";
531
581
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "inline-flex items-center gap-2", children: [
582
+ muted ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-xs text-muted-foreground", "data-apply-nothing": "", children: "Nothing to change" }) : null,
532
583
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
533
584
  "button",
534
585
  {
535
586
  type: "button",
536
587
  onClick: () => void handleApply(),
537
- disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
538
- "data-apply-tone": destructive ? "destructive" : "default",
539
- 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"}`,
588
+ disabled: view.status === "applying" || view.status === "asking" || items.length === 0,
589
+ "data-apply-tone": muted ? "muted" : destructive ? "destructive" : "default",
590
+ title: muted ? "It sets no fields, so applying it changes nothing." : void 0,
591
+ 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"}`,
540
592
  children: [
541
- 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" }),
542
- state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
593
+ view.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" }),
594
+ view.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
543
595
  ]
544
596
  }
545
597
  ),
546
- state.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
598
+ view.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
547
599
  "span",
548
600
  {
549
601
  className: "inline-flex max-w-sm items-start gap-1 text-xs text-destructive",
@@ -555,14 +607,14 @@ function ApplyDirectiveButton({
555
607
  {
556
608
  words: failureWords(
557
609
  {
558
- raw: state.message,
610
+ raw: view.message,
559
611
  directiveClass: directive.directiveClass,
560
612
  noun: directive.noun,
561
613
  nounLabel: nounText
562
614
  },
563
615
  host.explainFailure
564
616
  ),
565
- raw: state.message
617
+ raw: view.message
566
618
  }
567
619
  )
568
620
  ]
@@ -646,12 +698,13 @@ function ItemRow({
646
698
  directive,
647
699
  item,
648
700
  index,
649
- total
701
+ total,
702
+ phase
650
703
  }) {
651
704
  const host = useDirectiveHost();
652
705
  const recordId = TARGETING_CLASSES.has(directive.directiveClass) ? itemRecordId(item) : null;
653
706
  if (recordId) {
654
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TargetRow, { directive, item, recordId });
707
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TargetRow, { directive, item, recordId, index, phase });
655
708
  }
656
709
  const titleColumn = (0, import_content_ir3.nounTitleColumn)(directive.noun, host.nouns);
657
710
  const title = (0, import_content_ir3.itemTitle)(item, titleColumn, index, total);
@@ -738,16 +791,20 @@ function DirectiveChangeList({
738
791
  function TargetRow({
739
792
  directive,
740
793
  item,
741
- recordId
794
+ recordId,
795
+ index,
796
+ phase
742
797
  }) {
743
798
  const host = useDirectiveHost();
744
799
  const fallback = recordFallbackName((0, import_content_ir3.nounLabel)(directive.noun, host.nouns), recordId);
745
800
  const isUpdate = directive.directiveClass === "update";
746
- const current = useRecordValues(
747
- isUpdate ? host.readRecord : void 0,
748
- isUpdate ? { noun: directive.noun, id: recordId } : null
801
+ const readsLive = isUpdate && phase.phase === "open";
802
+ const live = useRecordValues(
803
+ readsLive ? host.readRecord : void 0,
804
+ readsLive ? { noun: directive.noun, id: recordId } : null
749
805
  );
750
- const changes = isUpdate ? itemChanges(item, current.values, { titleColumn: (0, import_content_ir3.nounTitleColumn)(directive.noun, host.nouns) }) : [];
806
+ const compareTo = phase.phase === "applied" ? phase.before?.[index] ?? null : readsLive ? live.values : null;
807
+ const changes = isUpdate ? itemChanges(item, compareTo, { titleColumn: (0, import_content_ir3.nounTitleColumn)(directive.noun, host.nouns) }) : [];
751
808
  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 });
752
809
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
753
810
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
@@ -764,6 +821,10 @@ function SideEffectDirectiveCard({ directive }) {
764
821
  const total = items.length;
765
822
  const display = (0, import_content_ir3.directiveDisplay)(directive.directiveClass, directive.noun, host.nouns);
766
823
  const Icon = classIcon(directive.directiveClass, directive.noun);
824
+ const [phase, setPhase] = (0, import_react4.useState)(
825
+ () => readsApplyLedger(host, directive) ? { phase: "pending" } : { phase: "open" }
826
+ );
827
+ const nothingToChange = directive.directiveClass === "update" && total > 0 && items.every((item) => itemChanges(item).length === 0);
767
828
  const shown = expanded ? items : items.slice(0, ROWS_BEFORE_FOLD);
768
829
  const hidden = total - shown.length;
769
830
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "my-3 rounded-md border border-border bg-card px-2.5 py-1.5", "data-directive": directive.slug, children: [
@@ -778,13 +839,21 @@ function SideEffectDirectiveCard({ directive }) {
778
839
  ] }),
779
840
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "ml-auto flex items-center gap-1", children: [
780
841
  host.renderCopy?.({ label: display.title, value: directive.shell, size: "icon" }),
781
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ApplyDirectiveButton, { directive, itemCount: total })
842
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
843
+ ApplyDirectiveButton,
844
+ {
845
+ directive,
846
+ itemCount: total,
847
+ onPhase: setPhase,
848
+ nothingToChange
849
+ }
850
+ )
782
851
  ] })
783
852
  ] }),
784
853
  total === 0 ? (
785
854
  // Stated, never silent: an empty batch is a real (and suspicious) state.
786
855
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "mt-1 text-xs text-muted-foreground", children: "No items \u2014 nothing would be written." })
787
- ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "mt-0.5 divide-y divide-border/60", children: shown.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ItemRow, { directive, item, index: i, total }, i)) }),
856
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "mt-0.5 divide-y divide-border/60", children: shown.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ItemRow, { directive, item, index: i, total, phase }, i)) }),
788
857
  hidden > 0 || expanded ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
789
858
  "button",
790
859
  {