@ai-matrx/content-ir-react 0.18.0 → 0.19.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/dist/index.cjs CHANGED
@@ -74,10 +74,12 @@ __export(index_exports, {
74
74
  kindIsRoutable: () => kindIsRoutable,
75
75
  kindServerDataFromStoredValue: () => kindServerDataFromStoredValue,
76
76
  markKindPartialUnsafe: () => markKindPartialUnsafe,
77
+ noteRecordChanged: () => noteRecordChanged,
77
78
  provisionalDirectiveSlug: () => provisionalDirectiveSlug,
78
79
  readIrRouteMarker: () => readIrRouteMarker,
79
80
  readsApplyLedger: () => readsApplyLedger,
80
81
  recordFallbackName: () => recordFallbackName,
82
+ recordRevision: () => recordRevision,
81
83
  registerDirectiveRenderer: () => registerDirectiveRenderer,
82
84
  resetDirectiveRenderers: () => resetDirectiveRenderers,
83
85
  resetPartialUnsafeKinds: () => resetPartialUnsafeKinds,
@@ -92,6 +94,7 @@ __export(index_exports, {
92
94
  useDirectiveHost: () => useDirectiveHost,
93
95
  useDirectiveScope: () => useDirectiveScope,
94
96
  useKindRouteEnv: () => useKindRouteEnv,
97
+ useRecordRevision: () => useRecordRevision,
95
98
  useRecordValues: () => useRecordValues
96
99
  });
97
100
  module.exports = __toCommonJS(index_exports);
@@ -366,8 +369,8 @@ var ComponentResolver = class {
366
369
  }
367
370
  bumpKind(kind) {
368
371
  this.kindVersions.set(kind, (this.kindVersions.get(kind) ?? 0) + 1);
369
- const listeners = this.kindListeners.get(kind);
370
- if (listeners) for (const listener of listeners) listener();
372
+ const listeners2 = this.kindListeners.get(kind);
373
+ if (listeners2) for (const listener of listeners2) listener();
371
374
  }
372
375
  notifyChanged() {
373
376
  this.version += 1;
@@ -833,7 +836,7 @@ var import_content_ir8 = require("@ai-matrx/content-ir");
833
836
  var import_content_ir4 = require("@ai-matrx/content-ir");
834
837
 
835
838
  // directives/ApplyDirectiveButton.tsx
836
- var import_react4 = require("react");
839
+ var import_react5 = require("react");
837
840
  var import_content_ir3 = require("@ai-matrx/content-ir");
838
841
 
839
842
  // directives/item-changes.ts
@@ -912,6 +915,64 @@ function itemChanges(item, current, options = {}) {
912
915
  return out;
913
916
  }
914
917
 
918
+ // directives/record-values.ts
919
+ var import_react4 = require("react");
920
+ var revisions = /* @__PURE__ */ new Map();
921
+ var listeners = /* @__PURE__ */ new Set();
922
+ function recordRevision(id) {
923
+ return revisions.get(id) ?? 0;
924
+ }
925
+ function noteRecordChanged(id) {
926
+ if (!id) return;
927
+ revisions.set(id, recordRevision(id) + 1);
928
+ for (const listener of listeners) listener();
929
+ }
930
+ function subscribeRevisions(listener) {
931
+ listeners.add(listener);
932
+ return () => {
933
+ listeners.delete(listener);
934
+ };
935
+ }
936
+ function useRecordRevision(id) {
937
+ return (0, import_react4.useSyncExternalStore)(
938
+ subscribeRevisions,
939
+ () => recordRevision(id),
940
+ () => 0
941
+ );
942
+ }
943
+ function useRecordValues(read, ref) {
944
+ const revision = useRecordRevision(ref?.id ?? "");
945
+ const key = ref && read ? `${ref.noun}:${ref.id}#${revision}` : "";
946
+ const [result, setResult] = (0, import_react4.useState)({
947
+ key: "",
948
+ values: null,
949
+ status: "idle"
950
+ });
951
+ (0, import_react4.useEffect)(() => {
952
+ if (!key || !read) return;
953
+ const split = key.indexOf(":");
954
+ const target = {
955
+ noun: key.slice(0, split),
956
+ id: key.slice(split + 1, key.lastIndexOf("#"))
957
+ };
958
+ let live = true;
959
+ setResult({ key, values: null, status: "loading" });
960
+ read(target).then((values) => {
961
+ if (!live) return;
962
+ setResult({ key, values, status: values ? "ready" : "unavailable" });
963
+ }).catch(() => {
964
+ if (!live) return;
965
+ setResult({ key, values: null, status: "unavailable" });
966
+ });
967
+ return () => {
968
+ live = false;
969
+ };
970
+ }, [key, read]);
971
+ if (!key) return { values: null, status: "idle" };
972
+ if (result.key !== key) return { values: null, status: "loading" };
973
+ return { values: result.values, status: result.status };
974
+ }
975
+
915
976
  // directives/failure-words.ts
916
977
  var PAST = {
917
978
  create: "created",
@@ -1031,6 +1092,41 @@ function classIcon(directiveClass, noun) {
1031
1092
 
1032
1093
  // directives/ApplyDirectiveButton.tsx
1033
1094
  var import_jsx_runtime4 = require("react/jsx-runtime");
1095
+ function announceChangedRecords(directive, items) {
1096
+ if (!TARGETING_CLASSES.has(directive.directiveClass)) return;
1097
+ for (const item of items) {
1098
+ const id = itemRecordId(item);
1099
+ if (id) noteRecordChanged(id);
1100
+ }
1101
+ }
1102
+ var applyOutcomes = /* @__PURE__ */ new WeakMap();
1103
+ var EMPTY_AUTHORITY = {};
1104
+ var outcomeListeners = /* @__PURE__ */ new Set();
1105
+ function outcomesOf(host) {
1106
+ let held = applyOutcomes.get(host);
1107
+ if (!held) {
1108
+ held = /* @__PURE__ */ new Map();
1109
+ applyOutcomes.set(host, held);
1110
+ }
1111
+ return held;
1112
+ }
1113
+ function publishOutcome(host, identity, outcome) {
1114
+ outcomesOf(host).set(identity, outcome);
1115
+ for (const listener of outcomeListeners) listener();
1116
+ }
1117
+ function subscribeOutcomes(listener) {
1118
+ outcomeListeners.add(listener);
1119
+ return () => {
1120
+ outcomeListeners.delete(listener);
1121
+ };
1122
+ }
1123
+ function useApplyOutcome(host, identity) {
1124
+ return (0, import_react5.useSyncExternalStore)(
1125
+ subscribeOutcomes,
1126
+ () => applyOutcomes.get(host)?.get(identity) ?? null,
1127
+ () => null
1128
+ );
1129
+ }
1034
1130
  var FINISH_POLL_MS = 2e3;
1035
1131
  var FINISH_POLLS_MAX = 30;
1036
1132
  function readsApplyLedger(host, directive) {
@@ -1050,27 +1146,29 @@ function ApplyDirectiveButton({
1050
1146
  const items = directive.items;
1051
1147
  const readsLedger = readsApplyLedger(host, directive);
1052
1148
  const initial = readsLedger ? { status: "checking" } : { status: "idle" };
1053
- const [state, setState] = (0, import_react4.useState)(initial);
1054
- const [elapsed, setElapsed] = (0, import_react4.useState)(0);
1055
- const [askingAgain, setAskingAgain] = (0, import_react4.useState)(false);
1056
- const startedAt = (0, import_react4.useRef)(null);
1149
+ const [state, setState] = (0, import_react5.useState)(initial);
1150
+ const [elapsed, setElapsed] = (0, import_react5.useState)(0);
1151
+ const [askingAgain, setAskingAgain] = (0, import_react5.useState)(false);
1152
+ const startedAt = (0, import_react5.useRef)(null);
1057
1153
  const identity = JSON.stringify([directive.slug, items, conversationId ?? null]);
1058
- const [stateFor, setStateFor] = (0, import_react4.useState)(identity);
1154
+ const [stateFor, setStateFor] = (0, import_react5.useState)(identity);
1059
1155
  if (stateFor !== identity) {
1060
1156
  setStateFor(identity);
1061
1157
  setState(initial);
1062
1158
  setAskingAgain(false);
1063
1159
  setElapsed(0);
1064
1160
  }
1065
- const currentIdentity = (0, import_react4.useRef)(identity);
1066
- (0, import_react4.useEffect)(() => {
1161
+ const shared = useApplyOutcome(host.confirm ?? EMPTY_AUTHORITY, identity);
1162
+ const view = state.status === "asking" || !shared ? state : shared;
1163
+ const currentIdentity = (0, import_react5.useRef)(identity);
1164
+ (0, import_react5.useEffect)(() => {
1067
1165
  currentIdentity.current = identity;
1068
1166
  }, [identity]);
1069
1167
  const nounText = (0, import_content_ir3.nounLabel)(directive.noun, host.nouns);
1070
1168
  const shellKey = readsLedger ? JSON.stringify([directive.slug, items, conversationId ?? null]) : "";
1071
1169
  const read = host.applyState;
1072
1170
  const report = host.reportError;
1073
- (0, import_react4.useEffect)(() => {
1171
+ (0, import_react5.useEffect)(() => {
1074
1172
  if (!shellKey || !read) return;
1075
1173
  const [slug, shellItems, inConversation] = JSON.parse(shellKey);
1076
1174
  const shell = { __kind: slug, items: shellItems };
@@ -1115,33 +1213,34 @@ function ApplyDirectiveButton({
1115
1213
  if (timer) clearTimeout(timer);
1116
1214
  };
1117
1215
  }, [shellKey, read, report]);
1118
- const appliedClean = state.status === "applied" && state.failed === 0 ? state : null;
1216
+ const appliedClean = view.status === "applied" && view.failed === 0 ? view : null;
1119
1217
  const appliedBefore = appliedClean ? appliedClean.before : null;
1120
- const phaseKey = state.status === "checking" || state.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
1121
- (0, import_react4.useEffect)(() => {
1218
+ const phaseKey = view.status === "checking" || view.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
1219
+ (0, import_react5.useEffect)(() => {
1122
1220
  if (!onPhase) return;
1123
1221
  if (phaseKey === "pending") onPhase({ phase: "pending" });
1124
1222
  else if (phaseKey === "open") onPhase({ phase: "open" });
1125
1223
  else onPhase({ phase: "applied", before: appliedBefore });
1126
1224
  }, [phaseKey, onPhase, appliedBefore]);
1127
- (0, import_react4.useEffect)(() => {
1128
- if (state.status !== "applying") return;
1129
- startedAt.current = Date.now();
1225
+ const applyingSince = view.status === "applying" ? view.startedAt : null;
1226
+ (0, import_react5.useEffect)(() => {
1227
+ if (applyingSince === null) return;
1228
+ startedAt.current = applyingSince;
1130
1229
  const id = setInterval(
1131
1230
  () => setElapsed(Math.round((Date.now() - (startedAt.current ?? 0)) / 1e3)),
1132
1231
  1e3
1133
1232
  );
1134
1233
  return () => clearInterval(id);
1135
- }, [state.status]);
1234
+ }, [applyingSince]);
1136
1235
  if (!directive.parsed.executes) return null;
1137
1236
  const confirm = host.confirm;
1138
1237
  if (!confirm) return null;
1139
1238
  async function handleApply(again = false) {
1140
1239
  if (!confirm) return;
1141
- const prior = state;
1240
+ const prior = view;
1142
1241
  const askedFor = identity;
1143
1242
  const stillHere = () => currentIdentity.current === askedFor;
1144
- const priorCopies = state.status === "applied" ? state.copies : 0;
1243
+ const priorCopies = view.status === "applied" ? view.copies : 0;
1145
1244
  if (host.ask) {
1146
1245
  if (again) setAskingAgain(true);
1147
1246
  else setState({ status: "asking" });
@@ -1170,14 +1269,20 @@ function ApplyDirectiveButton({
1170
1269
  }
1171
1270
  }
1172
1271
  setElapsed(0);
1173
- setState({ status: "applying" });
1272
+ setState((prev) => prev.status === "asking" ? { status: "idle" } : prev);
1273
+ publishOutcome(confirm, askedFor, { status: "applying", startedAt: Date.now() });
1174
1274
  try {
1175
1275
  const shell = { __kind: directive.slug, items };
1176
1276
  if (conversationId) shell.conversationId = conversationId;
1177
1277
  if (again) shell.force = true;
1178
- const result = await confirm(shell);
1179
- if (!stillHere()) return;
1180
- setState({
1278
+ let result;
1279
+ try {
1280
+ result = await confirm(shell);
1281
+ } finally {
1282
+ announceChangedRecords(directive, items);
1283
+ }
1284
+ for (const record of result.records ?? []) noteRecordChanged(record.id);
1285
+ publishOutcome(confirm, askedFor, {
1181
1286
  status: "applied",
1182
1287
  applied: result.applied,
1183
1288
  failed: result.failed,
@@ -1189,20 +1294,19 @@ function ApplyDirectiveButton({
1189
1294
  // apply replaced is still the change this block made.
1190
1295
  before: again && prior.status === "applied" && prior.before ? prior.before : result.before ?? null
1191
1296
  });
1192
- onApplied?.();
1297
+ if (stillHere()) onApplied?.();
1193
1298
  } catch (error) {
1194
- if (!stillHere()) return;
1195
- setState({
1299
+ publishOutcome(confirm, askedFor, {
1196
1300
  status: "error",
1197
1301
  message: error instanceof Error && error.message ? error.message : "That couldn't be applied just now. Please try again."
1198
1302
  });
1199
1303
  }
1200
1304
  }
1201
- if (state.status === "applied") {
1202
- const bad = state.failed > 0;
1305
+ if (view.status === "applied") {
1306
+ const bad = view.failed > 0;
1203
1307
  const trashed = directive.directiveClass === "delete";
1204
1308
  const label2 = nounText;
1205
- const records = host.renderRecord ? state.records : [];
1309
+ const records = host.renderRecord ? view.records : [];
1206
1310
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1207
1311
  "span",
1208
1312
  {
@@ -1216,26 +1320,26 @@ function ApplyDirectiveButton({
1216
1320
  {
1217
1321
  words: failureWords(
1218
1322
  {
1219
- raw: state.message ?? `${state.failed} failed`,
1323
+ raw: view.message ?? `${view.failed} failed`,
1220
1324
  directiveClass: directive.directiveClass,
1221
1325
  noun: directive.noun,
1222
1326
  nounLabel: nounText,
1223
- failed: state.failed,
1224
- total: state.applied + state.failed
1327
+ failed: view.failed,
1328
+ total: view.applied + view.failed
1225
1329
  },
1226
1330
  host.explainFailure
1227
1331
  ),
1228
- raw: state.message
1332
+ raw: view.message
1229
1333
  }
1230
- ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "min-w-0", children: state.message ?? `Applied ${state.applied}` })
1334
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "min-w-0", children: view.message ?? `Applied ${view.applied}` })
1231
1335
  ] }),
1232
- state.copies > 1 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1336
+ view.copies > 1 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1233
1337
  "span",
1234
1338
  {
1235
1339
  className: "shrink-0 text-muted-foreground",
1236
- "data-apply-copies": state.copies,
1340
+ "data-apply-copies": view.copies,
1237
1341
  title: records.length > 0 ? directive.directiveClass === "create" ? "Links to the latest copy." : "Links to the latest run." : void 0,
1238
- children: directive.directiveClass === "create" ? `${state.copies} copies` : `Ran ${state.copies} times`
1342
+ children: directive.directiveClass === "create" ? `${view.copies} copies` : `Ran ${view.copies} times`
1239
1343
  }
1240
1344
  ) : null,
1241
1345
  records.map((record) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
@@ -1262,7 +1366,7 @@ function ApplyDirectiveButton({
1262
1366
  }
1263
1367
  );
1264
1368
  }
1265
- if (state.status === "checking") {
1369
+ if (view.status === "checking") {
1266
1370
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1267
1371
  "span",
1268
1372
  {
@@ -1274,7 +1378,7 @@ function ApplyDirectiveButton({
1274
1378
  }
1275
1379
  );
1276
1380
  }
1277
- if (state.status === "finishing") {
1381
+ if (view.status === "finishing") {
1278
1382
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1279
1383
  "span",
1280
1384
  {
@@ -1289,7 +1393,7 @@ function ApplyDirectiveButton({
1289
1393
  );
1290
1394
  }
1291
1395
  const destructive = directive.directiveClass === "delete";
1292
- const muted = nothingToChange && state.status === "idle";
1396
+ const muted = nothingToChange && view.status === "idle";
1293
1397
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "inline-flex items-center gap-2", children: [
1294
1398
  muted ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-xs text-muted-foreground", "data-apply-nothing": "", children: "Nothing to change" }) : null,
1295
1399
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
@@ -1297,17 +1401,17 @@ function ApplyDirectiveButton({
1297
1401
  {
1298
1402
  type: "button",
1299
1403
  onClick: () => void handleApply(),
1300
- disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
1404
+ disabled: view.status === "applying" || view.status === "asking" || items.length === 0,
1301
1405
  "data-apply-tone": muted ? "muted" : destructive ? "destructive" : "default",
1302
1406
  title: muted ? "It sets no fields, so applying it changes nothing." : void 0,
1303
1407
  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"}`,
1304
1408
  children: [
1305
- state.status === "applying" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(LoaderIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PlayIcon, { className: "h-3.5 w-3.5" }),
1306
- state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
1409
+ view.status === "applying" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(LoaderIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PlayIcon, { className: "h-3.5 w-3.5" }),
1410
+ view.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
1307
1411
  ]
1308
1412
  }
1309
1413
  ),
1310
- state.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1414
+ view.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1311
1415
  "span",
1312
1416
  {
1313
1417
  className: "inline-flex max-w-sm items-start gap-1 text-xs text-destructive",
@@ -1319,14 +1423,14 @@ function ApplyDirectiveButton({
1319
1423
  {
1320
1424
  words: failureWords(
1321
1425
  {
1322
- raw: state.message,
1426
+ raw: view.message,
1323
1427
  directiveClass: directive.directiveClass,
1324
1428
  noun: directive.noun,
1325
1429
  nounLabel: nounText
1326
1430
  },
1327
1431
  host.explainFailure
1328
1432
  ),
1329
- raw: state.message
1433
+ raw: view.message
1330
1434
  }
1331
1435
  )
1332
1436
  ]
@@ -1338,7 +1442,7 @@ function FailureNotice({
1338
1442
  words,
1339
1443
  raw
1340
1444
  }) {
1341
- const [open, setOpen] = (0, import_react4.useState)(false);
1445
+ const [open, setOpen] = (0, import_react5.useState)(false);
1342
1446
  const detail = raw?.trim() ?? "";
1343
1447
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex min-w-0 flex-col gap-0.5", "data-apply-failure": true, children: [
1344
1448
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "min-w-0", children: [
@@ -1457,39 +1561,6 @@ function resetDirectiveRenderers() {
1457
1561
  // directives/SideEffectDirectiveCard.tsx
1458
1562
  var import_react6 = require("react");
1459
1563
  var import_content_ir7 = require("@ai-matrx/content-ir");
1460
-
1461
- // directives/record-values.ts
1462
- var import_react5 = require("react");
1463
- function useRecordValues(read, ref) {
1464
- const key = ref && read ? `${ref.noun}:${ref.id}` : "";
1465
- const [result, setResult] = (0, import_react5.useState)({
1466
- key: "",
1467
- values: null,
1468
- status: "idle"
1469
- });
1470
- (0, import_react5.useEffect)(() => {
1471
- if (!key || !read) return;
1472
- const split = key.indexOf(":");
1473
- const target = { noun: key.slice(0, split), id: key.slice(split + 1) };
1474
- let live = true;
1475
- setResult({ key, values: null, status: "loading" });
1476
- read(target).then((values) => {
1477
- if (!live) return;
1478
- setResult({ key, values, status: values ? "ready" : "unavailable" });
1479
- }).catch(() => {
1480
- if (!live) return;
1481
- setResult({ key, values: null, status: "unavailable" });
1482
- });
1483
- return () => {
1484
- live = false;
1485
- };
1486
- }, [key, read]);
1487
- if (!key) return { values: null, status: "idle" };
1488
- if (result.key !== key) return { values: null, status: "loading" };
1489
- return { values: result.values, status: result.status };
1490
- }
1491
-
1492
- // directives/SideEffectDirectiveCard.tsx
1493
1564
  var import_jsx_runtime7 = require("react/jsx-runtime");
1494
1565
  var ROWS_BEFORE_FOLD = 3;
1495
1566
  function ItemRow({