@ai-matrx/content-ir-react 0.14.0 → 0.15.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,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.1 — 2026-10-02
4
+
5
+ - The applied tally keeps its glyph beside its sentence: a long server
6
+ sentence ("Already applied — nothing was changed again. Originally: …") wraps
7
+ next to the icon instead of leaving it alone on a line at phone width. No
8
+ consumer action.
9
+
10
+ ## 0.15.0 — 2026-10-02
11
+
12
+ - **A card in a chat message applies under its conversation.** New
13
+ `DirectiveScopeProvider` / `useDirectiveScope` and a `conversationId` prop on
14
+ `DirectiveRender`. The Apply control passes it to BOTH `confirm` and
15
+ `applyState` as `shell.conversationId` — the namespace an agent proposal's
16
+ Approve uses — so one action rendered twice (proposal card + in-message card)
17
+ keys once and can never apply twice. A note mounts no scope (person namespace).
18
+ - **Run again.** A clean tally carries "Run again": it calls `ask` with
19
+ `again: true` (the question must say it runs a SECOND time) and only a yes
20
+ sends `shell.force = true`; a no leaves the tally as it was. A tally with a
21
+ failed item carries "Retry" instead — a plain apply, never `force`.
22
+ - `confirm` / `applyState` now take `DirectiveShell` (`__kind`, `items`,
23
+ optional `conversationId`, optional `force`).
24
+
25
+ ### Consumer action
26
+
27
+ - Pass the conversation a message renders in (`DirectiveRender conversationId`
28
+ or `DirectiveScopeProvider`), and forward `shell.conversationId` as
29
+ `conversation_id` and `shell.force` as `force` to `/directives/confirm`
30
+ (and `conversationId` to `/directives/apply_state`).
31
+ - Word your `ask` dialog for `request.again`.
32
+
3
33
  ## 0.14.0 — 2026-10-02
4
34
 
5
35
  - **A card survives a reload.** New host seam `applyState(shell)` →
@@ -61,6 +61,28 @@ interface DirectiveAskRequest {
61
61
  items: Record<string, unknown>[];
62
62
  /** The noun's human label ("Task"), from the host's catalog when it has one. */
63
63
  nounLabel: string;
64
+ /**
65
+ * True for "Run again": the ledger already holds this block, and a yes runs
66
+ * it a SECOND time (`force`) — another copy for a create, the same fields
67
+ * written again for an update, another delete for a restored record. The
68
+ * question must say so.
69
+ */
70
+ again?: boolean;
71
+ }
72
+ /**
73
+ * The shell a card hands its host — the two keys, plus WHERE it sits.
74
+ *
75
+ * `conversationId` is the conversation the card renders in (a chat message),
76
+ * absent in a person's own content (a note). It is the idempotency NAMESPACE:
77
+ * a card in an agent's message must apply under the SAME key as that
78
+ * proposal's Approve button, or the one action could run twice.
79
+ */
80
+ interface DirectiveShell {
81
+ __kind: string;
82
+ items: Record<string, unknown>[];
83
+ conversationId?: string;
84
+ /** "Run again": skip the ledger's dedup and apply a second time. */
85
+ force?: boolean;
64
86
  }
65
87
  interface DirectiveOpenItemOptions {
66
88
  /** The item, `__kind`-stamped when the shape has an item kind. */
@@ -89,10 +111,7 @@ interface DirectiveHost {
89
111
  * item against the registered model and runs the ONE handler; idempotent).
90
112
  * Reject with an Error whose message is safe to show.
91
113
  */
92
- confirm?: (shell: {
93
- __kind: string;
94
- items: Record<string, unknown>[];
95
- }) => Promise<DirectiveApplyResult>;
114
+ confirm?: (shell: DirectiveShell) => Promise<DirectiveApplyResult>;
96
115
  /**
97
116
  * Ask the person BEFORE anything runs, naming the consequence (the records,
98
117
  * what changes). Resolve `true` to go ahead, `false` to leave the card as it
@@ -112,10 +131,7 @@ interface DirectiveHost {
112
131
  * read: the card reports it and offers Apply, which is safe because `confirm`
113
132
  * is idempotent. Absent → the card starts at Apply, as before.
114
133
  */
115
- applyState?: (shell: {
116
- __kind: string;
117
- items: Record<string, unknown>[];
118
- }) => Promise<DirectiveApplyState>;
134
+ applyState?: (shell: DirectiveShell) => Promise<DirectiveApplyState>;
119
135
  /**
120
136
  * Draw ONE existing record by its live name, as a way to open it. Used on
121
137
  * update/delete rows (the record that will change), in the host's own ask
@@ -134,6 +150,20 @@ interface DirectiveHost {
134
150
  /** Where a decode failure or an unhonourable shell is reported. Default: console.error. */
135
151
  reportError?: (message: string) => void;
136
152
  }
153
+ /** Where a directive renders — today, the conversation it sits in (if any). */
154
+ interface DirectiveScope {
155
+ conversationId?: string | null;
156
+ }
157
+ /**
158
+ * Tell every card below WHERE it renders. A host draws a chat message's
159
+ * directives inside one of these (or passes `conversationId` to
160
+ * `DirectiveRender`), so the card applies in that conversation's namespace —
161
+ * the same key the agent's proposal approves under. A note mounts none.
162
+ */
163
+ declare function DirectiveScopeProvider({ conversationId, children, }: DirectiveScope & {
164
+ children: ReactNode;
165
+ }): react.JSX.Element;
166
+ declare function useDirectiveScope(): DirectiveScope;
137
167
  interface DirectiveHostProviderProps {
138
168
  host: DirectiveHost;
139
169
  children: ReactNode;
@@ -146,4 +176,4 @@ declare function DirectiveHostProvider({ host, children }: DirectiveHostProvider
146
176
  */
147
177
  declare function useDirectiveHost(): DirectiveHost;
148
178
 
149
- 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, useDirectiveHost as u };
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 };
@@ -61,6 +61,28 @@ interface DirectiveAskRequest {
61
61
  items: Record<string, unknown>[];
62
62
  /** The noun's human label ("Task"), from the host's catalog when it has one. */
63
63
  nounLabel: string;
64
+ /**
65
+ * True for "Run again": the ledger already holds this block, and a yes runs
66
+ * it a SECOND time (`force`) — another copy for a create, the same fields
67
+ * written again for an update, another delete for a restored record. The
68
+ * question must say so.
69
+ */
70
+ again?: boolean;
71
+ }
72
+ /**
73
+ * The shell a card hands its host — the two keys, plus WHERE it sits.
74
+ *
75
+ * `conversationId` is the conversation the card renders in (a chat message),
76
+ * absent in a person's own content (a note). It is the idempotency NAMESPACE:
77
+ * a card in an agent's message must apply under the SAME key as that
78
+ * proposal's Approve button, or the one action could run twice.
79
+ */
80
+ interface DirectiveShell {
81
+ __kind: string;
82
+ items: Record<string, unknown>[];
83
+ conversationId?: string;
84
+ /** "Run again": skip the ledger's dedup and apply a second time. */
85
+ force?: boolean;
64
86
  }
65
87
  interface DirectiveOpenItemOptions {
66
88
  /** The item, `__kind`-stamped when the shape has an item kind. */
@@ -89,10 +111,7 @@ interface DirectiveHost {
89
111
  * item against the registered model and runs the ONE handler; idempotent).
90
112
  * Reject with an Error whose message is safe to show.
91
113
  */
92
- confirm?: (shell: {
93
- __kind: string;
94
- items: Record<string, unknown>[];
95
- }) => Promise<DirectiveApplyResult>;
114
+ confirm?: (shell: DirectiveShell) => Promise<DirectiveApplyResult>;
96
115
  /**
97
116
  * Ask the person BEFORE anything runs, naming the consequence (the records,
98
117
  * what changes). Resolve `true` to go ahead, `false` to leave the card as it
@@ -112,10 +131,7 @@ interface DirectiveHost {
112
131
  * read: the card reports it and offers Apply, which is safe because `confirm`
113
132
  * is idempotent. Absent → the card starts at Apply, as before.
114
133
  */
115
- applyState?: (shell: {
116
- __kind: string;
117
- items: Record<string, unknown>[];
118
- }) => Promise<DirectiveApplyState>;
134
+ applyState?: (shell: DirectiveShell) => Promise<DirectiveApplyState>;
119
135
  /**
120
136
  * Draw ONE existing record by its live name, as a way to open it. Used on
121
137
  * update/delete rows (the record that will change), in the host's own ask
@@ -134,6 +150,20 @@ interface DirectiveHost {
134
150
  /** Where a decode failure or an unhonourable shell is reported. Default: console.error. */
135
151
  reportError?: (message: string) => void;
136
152
  }
153
+ /** Where a directive renders — today, the conversation it sits in (if any). */
154
+ interface DirectiveScope {
155
+ conversationId?: string | null;
156
+ }
157
+ /**
158
+ * Tell every card below WHERE it renders. A host draws a chat message's
159
+ * directives inside one of these (or passes `conversationId` to
160
+ * `DirectiveRender`), so the card applies in that conversation's namespace —
161
+ * the same key the agent's proposal approves under. A note mounts none.
162
+ */
163
+ declare function DirectiveScopeProvider({ conversationId, children, }: DirectiveScope & {
164
+ children: ReactNode;
165
+ }): react.JSX.Element;
166
+ declare function useDirectiveScope(): DirectiveScope;
137
167
  interface DirectiveHostProviderProps {
138
168
  host: DirectiveHost;
139
169
  children: ReactNode;
@@ -146,4 +176,4 @@ declare function DirectiveHostProvider({ host, children }: DirectiveHostProvider
146
176
  */
147
177
  declare function useDirectiveHost(): DirectiveHost;
148
178
 
149
- 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, useDirectiveHost as u };
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 };
@@ -27,6 +27,7 @@ __export(directives_exports, {
27
27
  DirectiveHostProvider: () => DirectiveHostProvider,
28
28
  DirectiveProvisionalCard: () => DirectiveProvisionalCard,
29
29
  DirectiveRender: () => DirectiveRender,
30
+ DirectiveScopeProvider: () => DirectiveScopeProvider,
30
31
  SideEffectDirectiveCard: () => SideEffectDirectiveCard,
31
32
  TARGETING_CLASSES: () => TARGETING_CLASSES,
32
33
  fieldLabel: () => fieldLabel,
@@ -37,7 +38,8 @@ __export(directives_exports, {
37
38
  recordFallbackName: () => recordFallbackName,
38
39
  registerDirectiveRenderer: () => registerDirectiveRenderer,
39
40
  resetDirectiveRenderers: () => resetDirectiveRenderers,
40
- useDirectiveHost: () => useDirectiveHost
41
+ useDirectiveHost: () => useDirectiveHost,
42
+ useDirectiveScope: () => useDirectiveScope
41
43
  });
42
44
  module.exports = __toCommonJS(directives_exports);
43
45
 
@@ -45,6 +47,16 @@ module.exports = __toCommonJS(directives_exports);
45
47
  var import_react = require("react");
46
48
  var import_jsx_runtime = require("react/jsx-runtime");
47
49
  var DirectiveHostContext = (0, import_react.createContext)(null);
50
+ var DirectiveScopeContext = (0, import_react.createContext)({});
51
+ function DirectiveScopeProvider({
52
+ conversationId,
53
+ children
54
+ }) {
55
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DirectiveScopeContext.Provider, { value: { conversationId: conversationId ?? null }, children });
56
+ }
57
+ function useDirectiveScope() {
58
+ return (0, import_react.useContext)(DirectiveScopeContext);
59
+ }
48
60
  function DirectiveHostProvider({ host, children }) {
49
61
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DirectiveHostContext.Provider, { value: host, children });
50
62
  }
@@ -197,6 +209,10 @@ var MaximizeIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Svg, { .
197
209
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "3", x2: "10", y1: "21", y2: "14" })
198
210
  ] });
199
211
  var LoaderIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Svg, { ...p, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) });
212
+ var RotateCcwIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Svg, { ...p, children: [
213
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
214
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M3 3v5h5" })
215
+ ] });
200
216
  var CheckCircleIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Svg, { ...p, children: [
201
217
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
202
218
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m9 12 2 2 4-4" })
@@ -232,6 +248,8 @@ function ApplyDirectiveButton({
232
248
  onApplied
233
249
  }) {
234
250
  const host = useDirectiveHost();
251
+ const scope = useDirectiveScope();
252
+ const conversationId = scope.conversationId || void 0;
235
253
  const items = directive.items;
236
254
  const readsLedger = !!host.applyState && !!host.confirm && directive.parsed.executes && items.length > 0;
237
255
  const [state, setState] = (0, import_react2.useState)(
@@ -240,17 +258,19 @@ function ApplyDirectiveButton({
240
258
  const [elapsed, setElapsed] = (0, import_react2.useState)(0);
241
259
  const startedAt = (0, import_react2.useRef)(null);
242
260
  const nounText = (0, import_content_ir2.nounLabel)(directive.noun, host.nouns);
243
- const shellKey = readsLedger ? JSON.stringify([directive.slug, items]) : "";
261
+ const shellKey = readsLedger ? JSON.stringify([directive.slug, items, conversationId ?? null]) : "";
244
262
  const read = host.applyState;
245
263
  const report = host.reportError;
246
264
  (0, import_react2.useEffect)(() => {
247
265
  if (!shellKey || !read) return;
248
- const [slug, shellItems] = JSON.parse(shellKey);
266
+ const [slug, shellItems, inConversation] = JSON.parse(shellKey);
267
+ const shell = { __kind: slug, items: shellItems };
268
+ if (inConversation) shell.conversationId = inConversation;
249
269
  let live = true;
250
270
  let polls = 0;
251
271
  let timer = null;
252
272
  const ask = () => {
253
- read({ __kind: slug, items: shellItems }).then((answer) => {
273
+ read(shell).then((answer) => {
254
274
  if (!live) return;
255
275
  if (answer.state === "applied") {
256
276
  setState(
@@ -296,8 +316,9 @@ function ApplyDirectiveButton({
296
316
  if (!directive.parsed.executes) return null;
297
317
  const confirm = host.confirm;
298
318
  if (!confirm) return null;
299
- async function handleApply() {
319
+ async function handleApply(again = false) {
300
320
  if (!confirm) return;
321
+ const before = state;
301
322
  if (host.ask) {
302
323
  setState({ status: "asking" });
303
324
  let ok = false;
@@ -305,7 +326,8 @@ function ApplyDirectiveButton({
305
326
  ok = await host.ask({
306
327
  directive,
307
328
  items,
308
- nounLabel: nounText
329
+ nounLabel: nounText,
330
+ ...again ? { again: true } : {}
309
331
  });
310
332
  } catch (error) {
311
333
  setState({
@@ -315,14 +337,17 @@ function ApplyDirectiveButton({
315
337
  return;
316
338
  }
317
339
  if (!ok) {
318
- setState({ status: "idle" });
340
+ setState(again ? before : { status: "idle" });
319
341
  return;
320
342
  }
321
343
  }
322
344
  setElapsed(0);
323
345
  setState({ status: "applying" });
324
346
  try {
325
- const result = await confirm({ __kind: directive.slug, items });
347
+ const shell = { __kind: directive.slug, items };
348
+ if (conversationId) shell.conversationId = conversationId;
349
+ if (again) shell.force = true;
350
+ const result = await confirm(shell);
326
351
  setState({
327
352
  status: "applied",
328
353
  applied: result.applied,
@@ -346,20 +371,35 @@ function ApplyDirectiveButton({
346
371
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
347
372
  "span",
348
373
  {
349
- className: `inline-flex min-w-0 flex-wrap items-center gap-1 text-xs ${bad ? "text-destructive" : "text-primary"}`,
374
+ className: `flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5 text-xs ${bad ? "text-destructive" : "text-primary"}`,
350
375
  "data-apply-state": "applied",
351
376
  children: [
352
- bad ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AlertIcon, { className: "h-3.5 w-3.5 shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CheckCircleIcon, { className: "h-3.5 w-3.5 shrink-0" }),
353
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
354
- state.message ?? `Applied ${state.applied}`,
355
- bad && !state.message ? ` \xB7 ${state.failed} failed` : ""
377
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "inline-flex min-w-0 items-start gap-1", children: [
378
+ bad ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AlertIcon, { className: "mt-px h-3.5 w-3.5 shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CheckCircleIcon, { className: "mt-px h-3.5 w-3.5 shrink-0" }),
379
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "min-w-0", children: [
380
+ state.message ?? `Applied ${state.applied}`,
381
+ bad && !state.message ? ` \xB7 ${state.failed} failed` : ""
382
+ ] })
356
383
  ] }),
357
384
  records.map((record) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
358
385
  ...record,
359
386
  fallback: recordFallbackName(label2, record.id),
360
387
  context: "applied",
361
388
  trashed
362
- }) }, `${record.noun}:${record.id}`))
389
+ }) }, `${record.noun}:${record.id}`)),
390
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
391
+ "button",
392
+ {
393
+ type: "button",
394
+ 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",
396
+ "data-apply-action": bad ? "retry" : "run-again",
397
+ children: [
398
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RotateCcwIcon, { className: "h-3 w-3" }),
399
+ bad ? "Retry" : "Run again"
400
+ ]
401
+ }
402
+ )
363
403
  ]
364
404
  }
365
405
  );
@@ -624,7 +664,11 @@ function isWholeJson(content) {
624
664
  return false;
625
665
  }
626
666
  }
627
- function DirectiveRender({ content, className, streaming }) {
667
+ function DirectiveRender({ conversationId, ...props }) {
668
+ if (conversationId === void 0) return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DirectiveRenderInner, { ...props });
669
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DirectiveScopeProvider, { conversationId, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DirectiveRenderInner, { ...props }) });
670
+ }
671
+ function DirectiveRenderInner({ content, className, streaming }) {
628
672
  const host = useDirectiveHost();
629
673
  const directive = (0, import_content_ir6.tryDecodeDirectiveContent)(content, (message) => {
630
674
  const line = `[kind-directives] ${message}`;