@ai-matrx/content-ir-react 0.18.0 → 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,18 @@
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
+
3
16
  ## 0.18.0 — 2026-10-02
4
17
 
5
18
  The G6A review of the action cards (nightly clone). Guard:
@@ -289,6 +289,34 @@ function classIcon(directiveClass, noun) {
289
289
 
290
290
  // directives/ApplyDirectiveButton.tsx
291
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
+ }
292
320
  var FINISH_POLL_MS = 2e3;
293
321
  var FINISH_POLLS_MAX = 30;
294
322
  function readsApplyLedger(host, directive) {
@@ -320,6 +348,8 @@ function ApplyDirectiveButton({
320
348
  setAskingAgain(false);
321
349
  setElapsed(0);
322
350
  }
351
+ const shared = useApplyOutcome(host.confirm ?? EMPTY_AUTHORITY, identity);
352
+ const view = state.status === "asking" || !shared ? state : shared;
323
353
  const currentIdentity = (0, import_react2.useRef)(identity);
324
354
  (0, import_react2.useEffect)(() => {
325
355
  currentIdentity.current = identity;
@@ -373,33 +403,34 @@ function ApplyDirectiveButton({
373
403
  if (timer) clearTimeout(timer);
374
404
  };
375
405
  }, [shellKey, read, report]);
376
- const appliedClean = state.status === "applied" && state.failed === 0 ? state : null;
406
+ const appliedClean = view.status === "applied" && view.failed === 0 ? view : null;
377
407
  const appliedBefore = appliedClean ? appliedClean.before : null;
378
- const phaseKey = state.status === "checking" || state.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
408
+ const phaseKey = view.status === "checking" || view.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
379
409
  (0, import_react2.useEffect)(() => {
380
410
  if (!onPhase) return;
381
411
  if (phaseKey === "pending") onPhase({ phase: "pending" });
382
412
  else if (phaseKey === "open") onPhase({ phase: "open" });
383
413
  else onPhase({ phase: "applied", before: appliedBefore });
384
414
  }, [phaseKey, onPhase, appliedBefore]);
415
+ const applyingSince = view.status === "applying" ? view.startedAt : null;
385
416
  (0, import_react2.useEffect)(() => {
386
- if (state.status !== "applying") return;
387
- startedAt.current = Date.now();
417
+ if (applyingSince === null) return;
418
+ startedAt.current = applyingSince;
388
419
  const id = setInterval(
389
420
  () => setElapsed(Math.round((Date.now() - (startedAt.current ?? 0)) / 1e3)),
390
421
  1e3
391
422
  );
392
423
  return () => clearInterval(id);
393
- }, [state.status]);
424
+ }, [applyingSince]);
394
425
  if (!directive.parsed.executes) return null;
395
426
  const confirm = host.confirm;
396
427
  if (!confirm) return null;
397
428
  async function handleApply(again = false) {
398
429
  if (!confirm) return;
399
- const prior = state;
430
+ const prior = view;
400
431
  const askedFor = identity;
401
432
  const stillHere = () => currentIdentity.current === askedFor;
402
- const priorCopies = state.status === "applied" ? state.copies : 0;
433
+ const priorCopies = view.status === "applied" ? view.copies : 0;
403
434
  if (host.ask) {
404
435
  if (again) setAskingAgain(true);
405
436
  else setState({ status: "asking" });
@@ -428,14 +459,14 @@ function ApplyDirectiveButton({
428
459
  }
429
460
  }
430
461
  setElapsed(0);
431
- setState({ status: "applying" });
462
+ setState((prev) => prev.status === "asking" ? { status: "idle" } : prev);
463
+ publishOutcome(confirm, askedFor, { status: "applying", startedAt: Date.now() });
432
464
  try {
433
465
  const shell = { __kind: directive.slug, items };
434
466
  if (conversationId) shell.conversationId = conversationId;
435
467
  if (again) shell.force = true;
436
468
  const result = await confirm(shell);
437
- if (!stillHere()) return;
438
- setState({
469
+ publishOutcome(confirm, askedFor, {
439
470
  status: "applied",
440
471
  applied: result.applied,
441
472
  failed: result.failed,
@@ -447,20 +478,19 @@ function ApplyDirectiveButton({
447
478
  // apply replaced is still the change this block made.
448
479
  before: again && prior.status === "applied" && prior.before ? prior.before : result.before ?? null
449
480
  });
450
- onApplied?.();
481
+ if (stillHere()) onApplied?.();
451
482
  } catch (error) {
452
- if (!stillHere()) return;
453
- setState({
483
+ publishOutcome(confirm, askedFor, {
454
484
  status: "error",
455
485
  message: error instanceof Error && error.message ? error.message : "That couldn't be applied just now. Please try again."
456
486
  });
457
487
  }
458
488
  }
459
- if (state.status === "applied") {
460
- const bad = state.failed > 0;
489
+ if (view.status === "applied") {
490
+ const bad = view.failed > 0;
461
491
  const trashed = directive.directiveClass === "delete";
462
492
  const label2 = nounText;
463
- const records = host.renderRecord ? state.records : [];
493
+ const records = host.renderRecord ? view.records : [];
464
494
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
465
495
  "span",
466
496
  {
@@ -474,26 +504,26 @@ function ApplyDirectiveButton({
474
504
  {
475
505
  words: failureWords(
476
506
  {
477
- raw: state.message ?? `${state.failed} failed`,
507
+ raw: view.message ?? `${view.failed} failed`,
478
508
  directiveClass: directive.directiveClass,
479
509
  noun: directive.noun,
480
510
  nounLabel: nounText,
481
- failed: state.failed,
482
- total: state.applied + state.failed
511
+ failed: view.failed,
512
+ total: view.applied + view.failed
483
513
  },
484
514
  host.explainFailure
485
515
  ),
486
- raw: state.message
516
+ raw: view.message
487
517
  }
488
- ) : /* @__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}` })
489
519
  ] }),
490
- state.copies > 1 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
520
+ view.copies > 1 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
491
521
  "span",
492
522
  {
493
523
  className: "shrink-0 text-muted-foreground",
494
- "data-apply-copies": state.copies,
524
+ "data-apply-copies": view.copies,
495
525
  title: records.length > 0 ? directive.directiveClass === "create" ? "Links to the latest copy." : "Links to the latest run." : void 0,
496
- children: directive.directiveClass === "create" ? `${state.copies} copies` : `Ran ${state.copies} times`
526
+ children: directive.directiveClass === "create" ? `${view.copies} copies` : `Ran ${view.copies} times`
497
527
  }
498
528
  ) : null,
499
529
  records.map((record) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
@@ -520,7 +550,7 @@ function ApplyDirectiveButton({
520
550
  }
521
551
  );
522
552
  }
523
- if (state.status === "checking") {
553
+ if (view.status === "checking") {
524
554
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
525
555
  "span",
526
556
  {
@@ -532,7 +562,7 @@ function ApplyDirectiveButton({
532
562
  }
533
563
  );
534
564
  }
535
- if (state.status === "finishing") {
565
+ if (view.status === "finishing") {
536
566
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
537
567
  "span",
538
568
  {
@@ -547,7 +577,7 @@ function ApplyDirectiveButton({
547
577
  );
548
578
  }
549
579
  const destructive = directive.directiveClass === "delete";
550
- const muted = nothingToChange && state.status === "idle";
580
+ const muted = nothingToChange && view.status === "idle";
551
581
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "inline-flex items-center gap-2", children: [
552
582
  muted ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-xs text-muted-foreground", "data-apply-nothing": "", children: "Nothing to change" }) : null,
553
583
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
@@ -555,17 +585,17 @@ function ApplyDirectiveButton({
555
585
  {
556
586
  type: "button",
557
587
  onClick: () => void handleApply(),
558
- disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
588
+ disabled: view.status === "applying" || view.status === "asking" || items.length === 0,
559
589
  "data-apply-tone": muted ? "muted" : destructive ? "destructive" : "default",
560
590
  title: muted ? "It sets no fields, so applying it changes nothing." : void 0,
561
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"}`,
562
592
  children: [
563
- 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" }),
564
- 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
565
595
  ]
566
596
  }
567
597
  ),
568
- state.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
598
+ view.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
569
599
  "span",
570
600
  {
571
601
  className: "inline-flex max-w-sm items-start gap-1 text-xs text-destructive",
@@ -577,14 +607,14 @@ function ApplyDirectiveButton({
577
607
  {
578
608
  words: failureWords(
579
609
  {
580
- raw: state.message,
610
+ raw: view.message,
581
611
  directiveClass: directive.directiveClass,
582
612
  noun: directive.noun,
583
613
  nounLabel: nounText
584
614
  },
585
615
  host.explainFailure
586
616
  ),
587
- raw: state.message
617
+ raw: view.message
588
618
  }
589
619
  )
590
620
  ]