@ai-matrx/content-ir-react 0.11.10 → 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,53 @@
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
+
29
+ ## 0.11.11 — 2026-09-25
30
+
31
+ - **THE PROVISIONAL TIER in `DirectiveRender`.** A directive whose JSON has not
32
+ finished arriving (its head already names a reserved `directive_v…` slug, the
33
+ document does not parse yet) now renders `DirectiveProvisionalCard` — the
34
+ catalog title ("Run Create agent definition") with a "Writing…" state — instead
35
+ of the raw growing JSON in a `<pre>`. Found live on the agent builder's
36
+ `directive_v1_action_create_agent_definition` in the agent run window.
37
+ - New optional `streaming?: boolean` prop on `DirectiveRender`. Pass `false` once
38
+ the host knows the stream has ended: an undecodable head then reads "Cut off
39
+ before it finished — nothing can be applied." with what arrived one click
40
+ away, never "Writing…" forever. Omitted = may still be arriving.
41
+ - A WHOLE document that parses but does not decode keeps the existing behaviour
42
+ (reported through the host, rendered raw) — it is never dressed as streaming.
43
+ - New exports: `DirectiveProvisionalCard`, `provisionalDirectiveSlug`.
44
+
45
+ ### Consumer action
46
+
47
+ - Pass your stream state to `DirectiveRender` as `streaming` (e.g. a markdown
48
+ block renderer's "stream active" flag) so a truncated directive reads as cut
49
+ off. Hosts that pass nothing still get the provisional card mid-stream.
50
+
3
51
  ## 0.11.10
4
52
 
5
53
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -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,11 +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,
28
+ DirectiveProvisionalCard: () => DirectiveProvisionalCard,
27
29
  DirectiveRender: () => DirectiveRender,
28
30
  SideEffectDirectiveCard: () => SideEffectDirectiveCard,
31
+ TARGETING_CLASSES: () => TARGETING_CLASSES,
32
+ fieldLabel: () => fieldLabel,
29
33
  getDirectiveRenderer: () => getDirectiveRenderer,
34
+ itemChanges: () => itemChanges,
35
+ itemRecordId: () => itemRecordId,
36
+ provisionalDirectiveSlug: () => provisionalDirectiveSlug,
37
+ recordFallbackName: () => recordFallbackName,
30
38
  registerDirectiveRenderer: () => registerDirectiveRenderer,
31
39
  resetDirectiveRenderers: () => resetDirectiveRenderers,
32
40
  useDirectiveHost: () => useDirectiveHost
@@ -66,6 +74,63 @@ function resetDirectiveRenderers() {
66
74
 
67
75
  // directives/ApplyDirectiveButton.tsx
68
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
+ }
69
134
 
70
135
  // directives/icons.tsx
71
136
  var import_jsx_runtime2 = require("react/jsx-runtime");
@@ -183,10 +248,38 @@ function ApplyDirectiveButton({
183
248
  const items = directive.items;
184
249
  async function handleApply() {
185
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);
186
273
  setState({ status: "applying" });
187
274
  try {
188
275
  const result = await confirm({ __kind: directive.slug, items });
189
- 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
+ });
190
283
  onApplied?.();
191
284
  } catch (error) {
192
285
  setState({
@@ -197,15 +290,26 @@ function ApplyDirectiveButton({
197
290
  }
198
291
  if (state.status === "applied") {
199
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 : [];
200
296
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
201
297
  "span",
202
298
  {
203
- 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",
204
301
  children: [
205
- 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" }),
206
- "Applied ",
207
- state.applied,
208
- 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}`))
209
313
  ]
210
314
  }
211
315
  );
@@ -216,7 +320,7 @@ function ApplyDirectiveButton({
216
320
  {
217
321
  type: "button",
218
322
  onClick: () => void handleApply(),
219
- disabled: state.status === "applying" || items.length === 0,
323
+ disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
220
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",
221
325
  children: [
222
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" }),
@@ -233,7 +337,7 @@ function ApplyDirectiveButton({
233
337
 
234
338
  // directives/SideEffectDirectiveCard.tsx
235
339
  var import_react3 = require("react");
236
- var import_content_ir2 = require("@ai-matrx/content-ir");
340
+ var import_content_ir3 = require("@ai-matrx/content-ir");
237
341
  var import_jsx_runtime4 = require("react/jsx-runtime");
238
342
  var ROWS_BEFORE_FOLD = 3;
239
343
  function ItemRow({
@@ -243,12 +347,16 @@ function ItemRow({
243
347
  total
244
348
  }) {
245
349
  const host = useDirectiveHost();
246
- const titleColumn = (0, import_content_ir2.nounTitleColumn)(directive.noun, host.nouns);
247
- const title = (0, import_content_ir2.itemTitle)(item, titleColumn, index, total);
248
- const subtitle = (0, import_content_ir2.itemSubtitle)(item);
249
- 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);
250
358
  const kind = host.itemKind?.(directive.slug) ?? null;
251
- 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);
252
360
  const openItem = host.openItem;
253
361
  const body = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
254
362
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "min-w-0 shrink-0 truncate font-medium text-foreground", children: title }),
@@ -267,7 +375,7 @@ function ItemRow({
267
375
  type: "button",
268
376
  onClick: () => openItem({
269
377
  // Stamped so the panel's kind pipeline types it from its own first key.
270
- item: (0, import_content_ir2.asKindInstance)(item, kind) ?? item,
378
+ item: (0, import_content_ir3.asKindInstance)(item, kind) ?? item,
271
379
  itemKind: kind,
272
380
  title,
273
381
  subtitle: `${display.title} \xB7 item ${index + 1} of ${total}`,
@@ -283,12 +391,47 @@ function ItemRow({
283
391
  host.renderCopy?.({ label: title, value: item, kind, size: "xs" })
284
392
  ] });
285
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
+ }
286
429
  function SideEffectDirectiveCard({ directive }) {
287
430
  const host = useDirectiveHost();
288
431
  const [expanded, setExpanded] = (0, import_react3.useState)(false);
289
432
  const items = directive.items;
290
433
  const total = items.length;
291
- 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);
292
435
  const Icon = classIcon(directive.directiveClass, directive.noun);
293
436
  const shown = expanded ? items : items.slice(0, ROWS_BEFORE_FOLD);
294
437
  const hidden = total - shown.length;
@@ -327,12 +470,12 @@ function SideEffectDirectiveCard({ directive }) {
327
470
  }
328
471
 
329
472
  // directives/DirectiveFallbackCard.tsx
330
- var import_content_ir3 = require("@ai-matrx/content-ir");
473
+ var import_content_ir4 = require("@ai-matrx/content-ir");
331
474
  var import_jsx_runtime5 = require("react/jsx-runtime");
332
475
  function DirectiveFallbackCard({ directive, reason }) {
333
476
  const host = useDirectiveHost();
334
477
  const count = directive.items.length;
335
- 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);
336
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: [
337
480
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(BoxesIcon, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
338
481
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-foreground", children: display.title }),
@@ -354,19 +497,70 @@ function DirectiveFallbackCard({ directive, reason }) {
354
497
  ] });
355
498
  }
356
499
 
357
- // directives/DirectiveRender.tsx
358
- var import_content_ir4 = require("@ai-matrx/content-ir");
500
+ // directives/DirectiveProvisionalCard.tsx
501
+ var import_content_ir5 = require("@ai-matrx/content-ir");
359
502
  var import_jsx_runtime6 = require("react/jsx-runtime");
360
- function DirectiveRender({ content, className }) {
503
+ var COMPLETE_SLUG_RE = /^\{\s*"__kind"\s*:\s*"([^"]+)"/;
504
+ function provisionalDirectiveSlug(content) {
505
+ const match = content.trimStart().match(COMPLETE_SLUG_RE);
506
+ return match?.[1] ?? null;
507
+ }
508
+ function DirectiveProvisionalCard({ content, streaming, className }) {
509
+ const host = useDirectiveHost();
510
+ const slug = provisionalDirectiveSlug(content);
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;
513
+ const Icon = known ? classIcon(known.directiveClass, known.noun) : BoxesIcon;
514
+ const title = display?.title ?? "Structured action";
515
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
516
+ "div",
517
+ {
518
+ className: `my-3 rounded-md border border-border bg-card px-2.5 py-1.5${className ? ` ${className}` : ""}`,
519
+ "data-directive": slug ?? void 0,
520
+ "data-directive-state": streaming ? "streaming" : "incomplete",
521
+ children: [
522
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1", children: [
523
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
524
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-sm font-medium text-foreground", children: title }),
525
+ display?.family ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-xs text-muted-foreground", children: display.family }) : null,
526
+ streaming ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "inline-flex items-center gap-1.5 text-xs text-muted-foreground", role: "status", children: [
527
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "h-1.5 w-1.5 animate-pulse rounded-full bg-primary", "aria-hidden": true }),
528
+ "Writing\u2026"
529
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-xs text-destructive", role: "status", children: "Cut off before it finished \u2014 nothing can be applied." })
530
+ ] }),
531
+ streaming ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("details", { className: "mt-1 text-xs text-muted-foreground", children: [
532
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("summary", { className: "cursor-pointer select-none hover:text-foreground", children: "Show what arrived" }),
533
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("pre", { className: "mt-1 max-h-64 overflow-auto whitespace-pre-wrap break-all rounded bg-muted px-2 py-1 text-[11px]", children: content })
534
+ ] })
535
+ ]
536
+ }
537
+ );
538
+ }
539
+
540
+ // directives/DirectiveRender.tsx
541
+ var import_content_ir6 = require("@ai-matrx/content-ir");
542
+ var import_jsx_runtime7 = require("react/jsx-runtime");
543
+ function isWholeJson(content) {
544
+ try {
545
+ JSON.parse(content);
546
+ return true;
547
+ } catch {
548
+ return false;
549
+ }
550
+ }
551
+ function DirectiveRender({ content, className, streaming }) {
361
552
  const host = useDirectiveHost();
362
- const directive = (0, import_content_ir4.tryDecodeDirectiveContent)(content, (message) => {
553
+ const directive = (0, import_content_ir6.tryDecodeDirectiveContent)(content, (message) => {
363
554
  const line = `[kind-directives] ${message}`;
364
555
  if (host.reportError) host.reportError(line);
365
556
  else console.error(line);
366
557
  });
367
558
  if (!directive) {
559
+ if (typeof content === "string" && (0, import_content_ir6.looksLikeDirectiveHead)(content) && !isWholeJson(content)) {
560
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DirectiveProvisionalCard, { content, streaming: streaming !== false, className });
561
+ }
368
562
  const raw = typeof content === "string" ? content : JSON.stringify(content, null, 2);
369
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
563
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
370
564
  "pre",
371
565
  {
372
566
  className: `my-3 overflow-x-auto rounded-md border border-border bg-muted px-3 py-2 text-xs text-muted-foreground${className ? ` ${className}` : ""}`,
@@ -375,8 +569,8 @@ function DirectiveRender({ content, className }) {
375
569
  );
376
570
  }
377
571
  const Renderer = getDirectiveRenderer(directive);
378
- if (Renderer) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Renderer, { directive });
379
- if (directive.parsed.executes) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SideEffectDirectiveCard, { directive });
380
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DirectiveFallbackCard, { directive });
572
+ if (Renderer) return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Renderer, { directive });
573
+ if (directive.parsed.executes) return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SideEffectDirectiveCard, { directive });
574
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DirectiveFallbackCard, { directive });
381
575
  }
382
576
  //# sourceMappingURL=directives.cjs.map