@ai-matrx/content-ir-react 0.11.11 → 0.12.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,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0 — 2026-09-30
4
+
5
+ - **An update/delete card names the record it changes.** Rows for
6
+ `directive_v1_update_*` / `directive_v1_delete_*` items carrying an `id` render
7
+ the EXISTING record through the new host seam `renderRecord` (its live name, a
8
+ way to open it). With no seam the row reads "Task 1a2b3c4d" — never "Item",
9
+ and never an update's NEW title passed off as the record's name.
10
+ - **An update card lists what it changes** — every field the item sets,
11
+ "Due date → 2026-10-15", "Assignee id → cleared" (`itemChanges`, exported with
12
+ `itemRecordId`, `fieldLabel`, `recordFallbackName`, `TARGETING_CLASSES`).
13
+ - **Apply tells the truth about where it is.** New host seam `ask(request)` runs
14
+ BEFORE anything is applied; the control stays idle (disabled) while it is open
15
+ and only a yes moves it to "Applying…". A no leaves the card exactly as it was.
16
+ - **The tally is a door.** `DirectiveApplyResult` gains optional `message` (the
17
+ server's receipt sentence, shown instead of "Applied 1") and `records`
18
+ (`{noun, id}[]`, drawn through `renderRecord` with `context: "applied"` and
19
+ `trashed` for a delete).
20
+
21
+ ### Consumer action
22
+
23
+ - Wire `ask` (your consequence dialog) and stop asking inside `confirm`, or your
24
+ card will still read "Applying…" while your dialog is up.
25
+ - Wire `renderRecord` to your live-name + open primitive, and return `message`
26
+ and `records` from `confirm` (the server's receipt `message` and
27
+ `resource_kind`/`resource_ids`).
28
+
3
29
  ## 0.11.11 — 2026-09-25
4
30
 
5
31
  - **THE PROVISIONAL TIER in `DirectiveRender`.** A directive whose JSON has not
@@ -0,0 +1,116 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { DecodedDirective, DirectiveNounCatalog, DirectiveItemKindLookup } from '@ai-matrx/content-ir';
4
+
5
+ interface DirectiveApplyResult {
6
+ applied: number;
7
+ failed: number;
8
+ /**
9
+ * The server's own receipt sentence ("Deleted task “Ship it”."), shown in
10
+ * place of a bare count. Never composed client-side.
11
+ */
12
+ message?: string | null;
13
+ /**
14
+ * The records the apply created or changed, so the tally is a way IN to them
15
+ * (no dead ends). Rendered through `renderRecord`; absent → the sentence alone.
16
+ */
17
+ records?: DirectiveRecordRef[];
18
+ }
19
+ /** One record a directive names: the noun it is, and its id. */
20
+ interface DirectiveRecordRef {
21
+ noun: string;
22
+ id: string;
23
+ }
24
+ interface DirectiveRecordProps extends DirectiveRecordRef {
25
+ /**
26
+ * What to show until (or unless) the host resolves the live name —
27
+ * "Task 1a2b3c4d". Never blank, never "Item".
28
+ */
29
+ fallback: string;
30
+ /** Where it is drawn: an item row, the confirm dialog, or the applied tally. */
31
+ context: "row" | "dialog" | "applied";
32
+ /**
33
+ * True when the record is (about to be) in the trash — the host may still
34
+ * name it but should not promise it opens as a live record.
35
+ */
36
+ trashed?: boolean;
37
+ }
38
+ /** What the person is being asked to approve — everything the card knows. */
39
+ interface DirectiveAskRequest {
40
+ directive: DecodedDirective;
41
+ /** The items, exactly as they will be sent. */
42
+ items: Record<string, unknown>[];
43
+ /** The noun's human label ("Task"), from the host's catalog when it has one. */
44
+ nounLabel: string;
45
+ }
46
+ interface DirectiveOpenItemOptions {
47
+ /** The item, `__kind`-stamped when the shape has an item kind. */
48
+ item: Record<string, unknown>;
49
+ /** The content-IR kind this item IS, or null when the shape has none. */
50
+ itemKind: string | null;
51
+ /** What the item is called — the same title the card shows. */
52
+ title: string;
53
+ /** "Run Create agent definition · item 1 of 1". */
54
+ subtitle: string | null;
55
+ directive: DecodedDirective;
56
+ index: number;
57
+ total: number;
58
+ }
59
+ interface DirectiveCopyProps {
60
+ label: string;
61
+ /** The value to copy — the whole shell or one item. */
62
+ value: unknown;
63
+ /** The item's kind, when copying an item. */
64
+ kind?: string | null;
65
+ size: "icon" | "xs";
66
+ }
67
+ interface DirectiveHost {
68
+ /**
69
+ * Apply a side-effect directive as the user (the server re-validates every
70
+ * item against the registered model and runs the ONE handler; idempotent).
71
+ * Reject with an Error whose message is safe to show.
72
+ */
73
+ confirm?: (shell: {
74
+ __kind: string;
75
+ items: Record<string, unknown>[];
76
+ }) => Promise<DirectiveApplyResult>;
77
+ /**
78
+ * Ask the person BEFORE anything runs, naming the consequence (the records,
79
+ * what changes). Resolve `true` to go ahead, `false` to leave the card as it
80
+ * was. The Apply control stays idle while this is open — nothing reads as
81
+ * "applying" until the person has said yes. Absent → `confirm` is called
82
+ * directly (a host that asks inside `confirm` keeps working, but its card
83
+ * will read "Applying…" while its dialog is up).
84
+ */
85
+ ask?: (request: DirectiveAskRequest) => Promise<boolean>;
86
+ /**
87
+ * Draw ONE existing record by its live name, as a way to open it. Used on
88
+ * update/delete rows (the record that will change), in the host's own ask
89
+ * dialog if it likes, and on the applied tally (what was written). Absent →
90
+ * the fallback text, unlinked.
91
+ */
92
+ renderRecord?: (props: DirectiveRecordProps) => ReactNode;
93
+ /** Open ONE item to read it properly before approving the write. */
94
+ openItem?: (options: DirectiveOpenItemOptions) => void;
95
+ /** Host copy controls. */
96
+ renderCopy?: (props: DirectiveCopyProps) => ReactNode;
97
+ /** The mirrored noun catalog (label / family / title column). */
98
+ nouns?: DirectiveNounCatalog;
99
+ /** slug → the kind ONE item is (THE DIRECTIVE⇄KIND SEAM), or null. */
100
+ itemKind?: DirectiveItemKindLookup;
101
+ /** Where a decode failure or an unhonourable shell is reported. Default: console.error. */
102
+ reportError?: (message: string) => void;
103
+ }
104
+ interface DirectiveHostProviderProps {
105
+ host: DirectiveHost;
106
+ children: ReactNode;
107
+ }
108
+ declare function DirectiveHostProvider({ host, children }: DirectiveHostProviderProps): react.JSX.Element;
109
+ /**
110
+ * The wired directive host. Unlike the render host this does NOT throw when
111
+ * unwired: a directive still renders (named, itemised, copy-less, apply-less)
112
+ * — degrading the chrome is honest, hiding the content is not.
113
+ */
114
+ declare function useDirectiveHost(): DirectiveHost;
115
+
116
+ export { type DirectiveApplyResult as D, type DirectiveAskRequest as a, type DirectiveCopyProps as b, type DirectiveHost as c, DirectiveHostProvider as d, type DirectiveHostProviderProps as e, type DirectiveOpenItemOptions as f, type DirectiveRecordProps as g, type DirectiveRecordRef as h, useDirectiveHost as u };
@@ -0,0 +1,116 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { DecodedDirective, DirectiveNounCatalog, DirectiveItemKindLookup } from '@ai-matrx/content-ir';
4
+
5
+ interface DirectiveApplyResult {
6
+ applied: number;
7
+ failed: number;
8
+ /**
9
+ * The server's own receipt sentence ("Deleted task “Ship it”."), shown in
10
+ * place of a bare count. Never composed client-side.
11
+ */
12
+ message?: string | null;
13
+ /**
14
+ * The records the apply created or changed, so the tally is a way IN to them
15
+ * (no dead ends). Rendered through `renderRecord`; absent → the sentence alone.
16
+ */
17
+ records?: DirectiveRecordRef[];
18
+ }
19
+ /** One record a directive names: the noun it is, and its id. */
20
+ interface DirectiveRecordRef {
21
+ noun: string;
22
+ id: string;
23
+ }
24
+ interface DirectiveRecordProps extends DirectiveRecordRef {
25
+ /**
26
+ * What to show until (or unless) the host resolves the live name —
27
+ * "Task 1a2b3c4d". Never blank, never "Item".
28
+ */
29
+ fallback: string;
30
+ /** Where it is drawn: an item row, the confirm dialog, or the applied tally. */
31
+ context: "row" | "dialog" | "applied";
32
+ /**
33
+ * True when the record is (about to be) in the trash — the host may still
34
+ * name it but should not promise it opens as a live record.
35
+ */
36
+ trashed?: boolean;
37
+ }
38
+ /** What the person is being asked to approve — everything the card knows. */
39
+ interface DirectiveAskRequest {
40
+ directive: DecodedDirective;
41
+ /** The items, exactly as they will be sent. */
42
+ items: Record<string, unknown>[];
43
+ /** The noun's human label ("Task"), from the host's catalog when it has one. */
44
+ nounLabel: string;
45
+ }
46
+ interface DirectiveOpenItemOptions {
47
+ /** The item, `__kind`-stamped when the shape has an item kind. */
48
+ item: Record<string, unknown>;
49
+ /** The content-IR kind this item IS, or null when the shape has none. */
50
+ itemKind: string | null;
51
+ /** What the item is called — the same title the card shows. */
52
+ title: string;
53
+ /** "Run Create agent definition · item 1 of 1". */
54
+ subtitle: string | null;
55
+ directive: DecodedDirective;
56
+ index: number;
57
+ total: number;
58
+ }
59
+ interface DirectiveCopyProps {
60
+ label: string;
61
+ /** The value to copy — the whole shell or one item. */
62
+ value: unknown;
63
+ /** The item's kind, when copying an item. */
64
+ kind?: string | null;
65
+ size: "icon" | "xs";
66
+ }
67
+ interface DirectiveHost {
68
+ /**
69
+ * Apply a side-effect directive as the user (the server re-validates every
70
+ * item against the registered model and runs the ONE handler; idempotent).
71
+ * Reject with an Error whose message is safe to show.
72
+ */
73
+ confirm?: (shell: {
74
+ __kind: string;
75
+ items: Record<string, unknown>[];
76
+ }) => Promise<DirectiveApplyResult>;
77
+ /**
78
+ * Ask the person BEFORE anything runs, naming the consequence (the records,
79
+ * what changes). Resolve `true` to go ahead, `false` to leave the card as it
80
+ * was. The Apply control stays idle while this is open — nothing reads as
81
+ * "applying" until the person has said yes. Absent → `confirm` is called
82
+ * directly (a host that asks inside `confirm` keeps working, but its card
83
+ * will read "Applying…" while its dialog is up).
84
+ */
85
+ ask?: (request: DirectiveAskRequest) => Promise<boolean>;
86
+ /**
87
+ * Draw ONE existing record by its live name, as a way to open it. Used on
88
+ * update/delete rows (the record that will change), in the host's own ask
89
+ * dialog if it likes, and on the applied tally (what was written). Absent →
90
+ * the fallback text, unlinked.
91
+ */
92
+ renderRecord?: (props: DirectiveRecordProps) => ReactNode;
93
+ /** Open ONE item to read it properly before approving the write. */
94
+ openItem?: (options: DirectiveOpenItemOptions) => void;
95
+ /** Host copy controls. */
96
+ renderCopy?: (props: DirectiveCopyProps) => ReactNode;
97
+ /** The mirrored noun catalog (label / family / title column). */
98
+ nouns?: DirectiveNounCatalog;
99
+ /** slug → the kind ONE item is (THE DIRECTIVE⇄KIND SEAM), or null. */
100
+ itemKind?: DirectiveItemKindLookup;
101
+ /** Where a decode failure or an unhonourable shell is reported. Default: console.error. */
102
+ reportError?: (message: string) => void;
103
+ }
104
+ interface DirectiveHostProviderProps {
105
+ host: DirectiveHost;
106
+ children: ReactNode;
107
+ }
108
+ declare function DirectiveHostProvider({ host, children }: DirectiveHostProviderProps): react.JSX.Element;
109
+ /**
110
+ * The wired directive host. Unlike the render host this does NOT throw when
111
+ * unwired: a directive still renders (named, itemised, copy-less, apply-less)
112
+ * — degrading the chrome is honest, hiding the content is not.
113
+ */
114
+ declare function useDirectiveHost(): DirectiveHost;
115
+
116
+ export { type DirectiveApplyResult as D, type DirectiveAskRequest as a, type DirectiveCopyProps as b, type DirectiveHost as c, DirectiveHostProvider as d, type DirectiveHostProviderProps as e, type DirectiveOpenItemOptions as f, type DirectiveRecordProps as g, type DirectiveRecordRef as h, useDirectiveHost as u };
@@ -22,13 +22,19 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  var directives_exports = {};
23
23
  __export(directives_exports, {
24
24
  ApplyDirectiveButton: () => ApplyDirectiveButton,
25
+ CHANGE_VALUE_MAX: () => CHANGE_VALUE_MAX,
25
26
  DirectiveFallbackCard: () => DirectiveFallbackCard,
26
27
  DirectiveHostProvider: () => DirectiveHostProvider,
27
28
  DirectiveProvisionalCard: () => DirectiveProvisionalCard,
28
29
  DirectiveRender: () => DirectiveRender,
29
30
  SideEffectDirectiveCard: () => SideEffectDirectiveCard,
31
+ TARGETING_CLASSES: () => TARGETING_CLASSES,
32
+ fieldLabel: () => fieldLabel,
30
33
  getDirectiveRenderer: () => getDirectiveRenderer,
34
+ itemChanges: () => itemChanges,
35
+ itemRecordId: () => itemRecordId,
31
36
  provisionalDirectiveSlug: () => provisionalDirectiveSlug,
37
+ recordFallbackName: () => recordFallbackName,
32
38
  registerDirectiveRenderer: () => registerDirectiveRenderer,
33
39
  resetDirectiveRenderers: () => resetDirectiveRenderers,
34
40
  useDirectiveHost: () => useDirectiveHost
@@ -68,6 +74,63 @@ function resetDirectiveRenderers() {
68
74
 
69
75
  // directives/ApplyDirectiveButton.tsx
70
76
  var import_react2 = require("react");
77
+ var import_content_ir2 = require("@ai-matrx/content-ir");
78
+
79
+ // directives/item-changes.ts
80
+ var TARGETING_CLASSES = /* @__PURE__ */ new Set(["update", "delete"]);
81
+ var NOT_A_CHANGE = /* @__PURE__ */ new Set(["__kind", "id"]);
82
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
83
+ var CHANGE_VALUE_MAX = 60;
84
+ function itemRecordId(item) {
85
+ const id = item.id;
86
+ if (typeof id === "string" && id.trim()) return id.trim();
87
+ if (typeof id === "number") return String(id);
88
+ return null;
89
+ }
90
+ function fieldLabel(key) {
91
+ const spaced = key.replace(/_/g, " ").trim();
92
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
93
+ }
94
+ function recordFallbackName(nounLabel3, id) {
95
+ const short = UUID_RE.test(id) ? id.slice(0, 8) : id.length > 12 ? `${id.slice(0, 12)}\u2026` : id;
96
+ return `${nounLabel3} ${short}`;
97
+ }
98
+ function describeValue(value) {
99
+ if (value === null || value === void 0) return { value: "cleared", full: "cleared", clears: true };
100
+ if (typeof value === "string") {
101
+ const trimmed = value.trim();
102
+ if (!trimmed) return { value: "cleared", full: "cleared", clears: true };
103
+ const oneLine = trimmed.replace(/\s+/g, " ");
104
+ const shown = oneLine.length > CHANGE_VALUE_MAX ? `${oneLine.slice(0, CHANGE_VALUE_MAX - 1)}\u2026` : oneLine;
105
+ return { value: shown, full: trimmed, clears: false };
106
+ }
107
+ if (typeof value === "number" || typeof value === "boolean") {
108
+ return { value: String(value), full: String(value), clears: false };
109
+ }
110
+ if (Array.isArray(value)) {
111
+ const n2 = value.length;
112
+ const text2 = n2 === 0 ? "cleared" : `${n2} ${n2 === 1 ? "entry" : "entries"}`;
113
+ return { value: text2, full: safeJson(value), clears: n2 === 0 };
114
+ }
115
+ const n = Object.keys(value).length;
116
+ const text = n === 0 ? "cleared" : `${n} ${n === 1 ? "field" : "fields"}`;
117
+ return { value: text, full: safeJson(value), clears: n === 0 };
118
+ }
119
+ function safeJson(value) {
120
+ try {
121
+ return JSON.stringify(value, null, 2);
122
+ } catch {
123
+ return String(value);
124
+ }
125
+ }
126
+ function itemChanges(item) {
127
+ const out = [];
128
+ for (const [key, raw] of Object.entries(item)) {
129
+ if (NOT_A_CHANGE.has(key)) continue;
130
+ out.push({ key, label: fieldLabel(key), ...describeValue(raw) });
131
+ }
132
+ return out;
133
+ }
71
134
 
72
135
  // directives/icons.tsx
73
136
  var import_jsx_runtime2 = require("react/jsx-runtime");
@@ -185,10 +248,38 @@ function ApplyDirectiveButton({
185
248
  const items = directive.items;
186
249
  async function handleApply() {
187
250
  if (!confirm) return;
251
+ if (host.ask) {
252
+ setState({ status: "asking" });
253
+ let ok = false;
254
+ try {
255
+ ok = await host.ask({
256
+ directive,
257
+ items,
258
+ nounLabel: (0, import_content_ir2.nounLabel)(directive.noun, host.nouns)
259
+ });
260
+ } catch (error) {
261
+ setState({
262
+ status: "error",
263
+ message: error instanceof Error && error.message ? error.message : "We could not ask you to confirm this, so nothing was done."
264
+ });
265
+ return;
266
+ }
267
+ if (!ok) {
268
+ setState({ status: "idle" });
269
+ return;
270
+ }
271
+ }
272
+ setElapsed(0);
188
273
  setState({ status: "applying" });
189
274
  try {
190
275
  const result = await confirm({ __kind: directive.slug, items });
191
- setState({ status: "applied", applied: result.applied, failed: result.failed });
276
+ setState({
277
+ status: "applied",
278
+ applied: result.applied,
279
+ failed: result.failed,
280
+ message: result.message?.trim() ? result.message.trim() : null,
281
+ records: result.records ?? []
282
+ });
192
283
  onApplied?.();
193
284
  } catch (error) {
194
285
  setState({
@@ -199,15 +290,26 @@ function ApplyDirectiveButton({
199
290
  }
200
291
  if (state.status === "applied") {
201
292
  const bad = state.failed > 0;
293
+ const trashed = directive.directiveClass === "delete";
294
+ const label2 = (0, import_content_ir2.nounLabel)(directive.noun, host.nouns);
295
+ const records = host.renderRecord ? state.records : [];
202
296
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
203
297
  "span",
204
298
  {
205
- className: `inline-flex items-center gap-1 text-xs ${bad ? "text-destructive" : "text-primary"}`,
299
+ className: `inline-flex min-w-0 flex-wrap items-center gap-1 text-xs ${bad ? "text-destructive" : "text-primary"}`,
300
+ "data-apply-state": "applied",
206
301
  children: [
207
- bad ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AlertIcon, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CheckCircleIcon, { className: "h-3.5 w-3.5" }),
208
- "Applied ",
209
- state.applied,
210
- bad ? ` \xB7 ${state.failed} failed` : ""
302
+ 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" }),
303
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
304
+ state.message ?? `Applied ${state.applied}`,
305
+ bad && !state.message ? ` \xB7 ${state.failed} failed` : ""
306
+ ] }),
307
+ records.map((record) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
308
+ ...record,
309
+ fallback: recordFallbackName(label2, record.id),
310
+ context: "applied",
311
+ trashed
312
+ }) }, `${record.noun}:${record.id}`))
211
313
  ]
212
314
  }
213
315
  );
@@ -218,7 +320,7 @@ function ApplyDirectiveButton({
218
320
  {
219
321
  type: "button",
220
322
  onClick: () => void handleApply(),
221
- disabled: state.status === "applying" || items.length === 0,
323
+ disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
222
324
  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",
223
325
  children: [
224
326
  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" }),
@@ -235,7 +337,7 @@ function ApplyDirectiveButton({
235
337
 
236
338
  // directives/SideEffectDirectiveCard.tsx
237
339
  var import_react3 = require("react");
238
- var import_content_ir2 = require("@ai-matrx/content-ir");
340
+ var import_content_ir3 = require("@ai-matrx/content-ir");
239
341
  var import_jsx_runtime4 = require("react/jsx-runtime");
240
342
  var ROWS_BEFORE_FOLD = 3;
241
343
  function ItemRow({
@@ -245,12 +347,16 @@ function ItemRow({
245
347
  total
246
348
  }) {
247
349
  const host = useDirectiveHost();
248
- const titleColumn = (0, import_content_ir2.nounTitleColumn)(directive.noun, host.nouns);
249
- const title = (0, import_content_ir2.itemTitle)(item, titleColumn, index, total);
250
- const subtitle = (0, import_content_ir2.itemSubtitle)(item);
251
- const facts = (0, import_content_ir2.itemFacts)(item);
350
+ const recordId = TARGETING_CLASSES.has(directive.directiveClass) ? itemRecordId(item) : null;
351
+ if (recordId) {
352
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TargetRow, { directive, item, recordId });
353
+ }
354
+ const titleColumn = (0, import_content_ir3.nounTitleColumn)(directive.noun, host.nouns);
355
+ const title = (0, import_content_ir3.itemTitle)(item, titleColumn, index, total);
356
+ const subtitle = (0, import_content_ir3.itemSubtitle)(item);
357
+ const facts = (0, import_content_ir3.itemFacts)(item);
252
358
  const kind = host.itemKind?.(directive.slug) ?? null;
253
- const display = (0, import_content_ir2.directiveDisplay)(directive.directiveClass, directive.noun, host.nouns);
359
+ const display = (0, import_content_ir3.directiveDisplay)(directive.directiveClass, directive.noun, host.nouns);
254
360
  const openItem = host.openItem;
255
361
  const body = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
256
362
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "min-w-0 shrink-0 truncate font-medium text-foreground", children: title }),
@@ -269,7 +375,7 @@ function ItemRow({
269
375
  type: "button",
270
376
  onClick: () => openItem({
271
377
  // Stamped so the panel's kind pipeline types it from its own first key.
272
- item: (0, import_content_ir2.asKindInstance)(item, kind) ?? item,
378
+ item: (0, import_content_ir3.asKindInstance)(item, kind) ?? item,
273
379
  itemKind: kind,
274
380
  title,
275
381
  subtitle: `${display.title} \xB7 item ${index + 1} of ${total}`,
@@ -285,12 +391,47 @@ function ItemRow({
285
391
  host.renderCopy?.({ label: title, value: item, kind, size: "xs" })
286
392
  ] });
287
393
  }
394
+ function TargetRow({
395
+ directive,
396
+ item,
397
+ recordId
398
+ }) {
399
+ const host = useDirectiveHost();
400
+ const fallback = recordFallbackName((0, import_content_ir3.nounLabel)(directive.noun, host.nouns), recordId);
401
+ const changes = directive.directiveClass === "update" ? itemChanges(item) : [];
402
+ 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 });
403
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
404
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
405
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
406
+ host.renderCopy?.({ label: fallback, value: item, size: "xs" })
407
+ ] }),
408
+ 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)(
409
+ "li",
410
+ {
411
+ className: "flex min-w-0 items-baseline gap-1.5 text-[11px]",
412
+ title: `${change.label} \u2192 ${change.full}`,
413
+ children: [
414
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "shrink-0 text-muted-foreground", children: change.label }),
415
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { "aria-hidden": true, className: "shrink-0 text-muted-foreground", children: "\u2192" }),
416
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
417
+ "span",
418
+ {
419
+ className: `min-w-0 truncate ${change.clears ? "italic text-muted-foreground" : "text-foreground"}`,
420
+ children: change.value
421
+ }
422
+ )
423
+ ]
424
+ },
425
+ change.key
426
+ )) }) : null
427
+ ] });
428
+ }
288
429
  function SideEffectDirectiveCard({ directive }) {
289
430
  const host = useDirectiveHost();
290
431
  const [expanded, setExpanded] = (0, import_react3.useState)(false);
291
432
  const items = directive.items;
292
433
  const total = items.length;
293
- const display = (0, import_content_ir2.directiveDisplay)(directive.directiveClass, directive.noun, host.nouns);
434
+ const display = (0, import_content_ir3.directiveDisplay)(directive.directiveClass, directive.noun, host.nouns);
294
435
  const Icon = classIcon(directive.directiveClass, directive.noun);
295
436
  const shown = expanded ? items : items.slice(0, ROWS_BEFORE_FOLD);
296
437
  const hidden = total - shown.length;
@@ -329,12 +470,12 @@ function SideEffectDirectiveCard({ directive }) {
329
470
  }
330
471
 
331
472
  // directives/DirectiveFallbackCard.tsx
332
- var import_content_ir3 = require("@ai-matrx/content-ir");
473
+ var import_content_ir4 = require("@ai-matrx/content-ir");
333
474
  var import_jsx_runtime5 = require("react/jsx-runtime");
334
475
  function DirectiveFallbackCard({ directive, reason }) {
335
476
  const host = useDirectiveHost();
336
477
  const count = directive.items.length;
337
- const display = (0, import_content_ir3.directiveDisplay)(directive.directiveClass, directive.noun, host.nouns);
478
+ const display = (0, import_content_ir4.directiveDisplay)(directive.directiveClass, directive.noun, host.nouns);
338
479
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "my-3 inline-flex flex-wrap items-center gap-2 rounded-md border border-border bg-muted px-2.5 py-1 text-sm", children: [
339
480
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(BoxesIcon, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
340
481
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-foreground", children: display.title }),
@@ -357,7 +498,7 @@ function DirectiveFallbackCard({ directive, reason }) {
357
498
  }
358
499
 
359
500
  // directives/DirectiveProvisionalCard.tsx
360
- var import_content_ir4 = require("@ai-matrx/content-ir");
501
+ var import_content_ir5 = require("@ai-matrx/content-ir");
361
502
  var import_jsx_runtime6 = require("react/jsx-runtime");
362
503
  var COMPLETE_SLUG_RE = /^\{\s*"__kind"\s*:\s*"([^"]+)"/;
363
504
  function provisionalDirectiveSlug(content) {
@@ -367,8 +508,8 @@ function provisionalDirectiveSlug(content) {
367
508
  function DirectiveProvisionalCard({ content, streaming, className }) {
368
509
  const host = useDirectiveHost();
369
510
  const slug = provisionalDirectiveSlug(content);
370
- const known = slug ? (0, import_content_ir4.parseDirectiveSlug)(slug) : null;
371
- const display = known ? (0, import_content_ir4.directiveDisplay)(known.directiveClass, known.noun, host.nouns) : null;
511
+ const known = slug ? (0, import_content_ir5.parseDirectiveSlug)(slug) : null;
512
+ const display = known ? (0, import_content_ir5.directiveDisplay)(known.directiveClass, known.noun, host.nouns) : null;
372
513
  const Icon = known ? classIcon(known.directiveClass, known.noun) : BoxesIcon;
373
514
  const title = display?.title ?? "Structured action";
374
515
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -397,7 +538,7 @@ function DirectiveProvisionalCard({ content, streaming, className }) {
397
538
  }
398
539
 
399
540
  // directives/DirectiveRender.tsx
400
- var import_content_ir5 = require("@ai-matrx/content-ir");
541
+ var import_content_ir6 = require("@ai-matrx/content-ir");
401
542
  var import_jsx_runtime7 = require("react/jsx-runtime");
402
543
  function isWholeJson(content) {
403
544
  try {
@@ -409,13 +550,13 @@ function isWholeJson(content) {
409
550
  }
410
551
  function DirectiveRender({ content, className, streaming }) {
411
552
  const host = useDirectiveHost();
412
- const directive = (0, import_content_ir5.tryDecodeDirectiveContent)(content, (message) => {
553
+ const directive = (0, import_content_ir6.tryDecodeDirectiveContent)(content, (message) => {
413
554
  const line = `[kind-directives] ${message}`;
414
555
  if (host.reportError) host.reportError(line);
415
556
  else console.error(line);
416
557
  });
417
558
  if (!directive) {
418
- if (typeof content === "string" && (0, import_content_ir5.looksLikeDirectiveHead)(content) && !isWholeJson(content)) {
559
+ if (typeof content === "string" && (0, import_content_ir6.looksLikeDirectiveHead)(content) && !isWholeJson(content)) {
419
560
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DirectiveProvisionalCard, { content, streaming: streaming !== false, className });
420
561
  }
421
562
  const raw = typeof content === "string" ? content : JSON.stringify(content, null, 2);