@ai-matrx/chat 0.5.6 → 0.5.11

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,46 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.5.11
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/chat/v0.5.8..npm/chat/v0.5.11 -- apps/shared/chat`).
7
+ No source changes intended and no consumer action required.
8
+
9
+ ## 0.5.10 — message Print is the one message print
10
+
11
+ - `conversation/utils/markdown-print` delegates to `@ai-matrx/rich-content/copy/print-message`
12
+ (`printMarkdownContent` now always returns a Promise). Needs rich-content ≥ 0.2.58.
13
+
14
+ Consumer action: none.
15
+
16
+ ## Unreleased — lane L4b rendered-output
17
+ ### an artifact edit shows its new version
18
+
19
+ `getToolArtifact` maps a completed `edit_artifact` call to a new `canvas_item` kind (`id` = the new
20
+ version's `canvas_items.id`, `version`); the tool card draws a "Page · Edited" bar and, under it, the
21
+ host slot **`CanvasItemCard`** (`{ canvasItemId, version? }`) — the version's own card rendered through
22
+ the host's block registry. Unregistered, the slot draws nothing. **Consumer action:** register
23
+ `CanvasItemCard` (matrx-frontend renders the item through its artifact renderer registry).
24
+
25
+ ## 0.5.9
26
+
27
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
28
+ `git diff npm/chat/v0.5.5..npm/chat/v0.5.9 -- apps/shared/chat`).
29
+ No source changes intended and no consumer action required.
30
+
31
+ ## 0.5.8 — attach from outside React
32
+
33
+ - `attachResourceToConversation(store, conversationId, resource)` — THE attach path without a hook
34
+ (`useAttachResource` is it bound to the app store); `attachRenderedArtifact(store, conversationId,
35
+ args)` — the same for a rendered page, for a canvas "Attach to chat ▸" entry or any host port.
36
+ - Consumer action: none required; a canvas host may wire `attachRenderedArtifact` into
37
+ `CanvasOutputPorts.attachOptions`.
38
+
39
+ ## 0.5.7 — a registry/list disagreement no longer crashes the page
40
+
41
+ - The context-items load-time drift check logs a loud console error instead of throwing; the
42
+ unregistered type renders through the generic fallback body. A jest test fails on any disagreement.
43
+
3
44
  ## 0.5.6 — 0.5.5 threw at load
4
45
 
5
46
  - `CONTEXT_ITEM_BLOCK_TYPES` lists `rendered_artifact`: 0.5.5 registered the def without it, so the
@@ -12,8 +12,9 @@ export type RichCopySplitProps = MatrxCopyMenuProps & {
12
12
  exportTitle?: string;
13
13
  };
14
14
  /**
15
- * THE content action set of a markdown host (ContentActions, Arman 2026-10-08: copy, Plain,
16
- * Export, Print and Transform each one click away; Transform opens the Alchemy palette below).
15
+ * THE content action set of a markdown host (ContentActions, Arman 2026-10-08 revised): ONE Copy icon
16
+ * (raw, then Copy raw / formatted / for AI — the host's `agent` envelope), Raw/Formatted, and "…" with
17
+ * Alchemy… (the palette below), the downloads and Print.
17
18
  * Formerly the split Copy of a markdown host (Arman, 2026-10-04: "one click to get either the markdown
18
19
  * version or the no-markup version"). One click copies the person's flavor (`copy.default_flavor`);
19
20
  * the chevron offers exactly Copy markdown and Copy plain text (2026-10-07). The Alchemy palette
@@ -5,6 +5,7 @@ import { ContentActions } from "@ai-matrx/rich-content/copy/ContentActions";
5
5
  import { copyRichContent } from "@ai-matrx/rich-content/copy/copy-commands";
6
6
  import { ExportPaletteAnchor, openExportPalette, useExportPaletteKey } from "@ai-matrx/rich-content/copy/ExportPalette";
7
7
  import { cn } from "@ai-matrx/design-system";
8
+ import { buildAgentPayload } from "@ai-matrx/alchemy/operate";
8
9
  import { CopyButtons } from "../host/ui-slots.js";
9
10
  function RichCopySplit(props) {
10
11
  const { human, className, size } = props;
@@ -25,7 +26,11 @@ function RichCopySplit(props) {
25
26
  },
26
27
  copy,
27
28
  size: splitSize,
28
- label: `Copy ${props.label ?? ""}`.trim(),
29
+ label: props.label ?? "content",
30
+ copyForAi: props.agent ? async () => {
31
+ const built = typeof props.agent === "function" ? await props.agent() : props.agent;
32
+ return typeof built === "string" ? built : buildAgentPayload(built, { url: window.location.href, capturedAt: (/* @__PURE__ */ new Date()).toISOString() });
33
+ } : void 0,
29
34
  title: props.exportTitle ?? props.label,
30
35
  viewKey: props.viewKey,
31
36
  onTransform: () => void openExportPalette(exportKey)
@@ -17,6 +17,7 @@ import type { ComponentType } from "react";
17
17
  import { hasContextItemDef } from "./context-item-block-types.js";
18
18
  import type { ContextItemBodyProps, ContextItemTitleProps, ContextItemTypeDef } from "./types.js";
19
19
  export declare const CONTEXT_ITEM_TYPE_DEFS: ContextItemTypeDef[];
20
+ export declare function findContextItemRegistryDrift(): string[];
20
21
  export { hasContextItemDef };
21
22
  /** Resolve the registered def for a block type, or a graceful fallback. */
22
23
  export declare function resolveContextItemDef(blockType: string): ContextItemTypeDef;
@@ -339,14 +339,17 @@ const FALLBACK_DEF = {
339
339
  editable: false,
340
340
  Body: GenericBody
341
341
  };
342
- {
342
+ function findContextItemRegistryDrift() {
343
343
  const listed = new Set(CONTEXT_ITEM_BLOCK_TYPES);
344
- const drift = [
344
+ return [
345
345
  ...[...BY_BLOCK_TYPE.keys()].filter((t) => !listed.has(t)),
346
346
  ...CONTEXT_ITEM_BLOCK_TYPES.filter((t) => !BY_BLOCK_TYPE.has(t))
347
347
  ];
348
+ }
349
+ {
350
+ const drift = findContextItemRegistryDrift();
348
351
  if (drift.length > 0) {
349
- throw new Error(
352
+ console.error(
350
353
  `context-items registry and CONTEXT_ITEM_BLOCK_TYPES disagree: ${drift.join(", ")}`
351
354
  );
352
355
  }
@@ -371,6 +374,7 @@ function resolveContextItemTitle(blockType) {
371
374
  }
372
375
  export {
373
376
  CONTEXT_ITEM_TYPE_DEFS,
377
+ findContextItemRegistryDrift,
374
378
  hasContextItemDef,
375
379
  resolveContextItemBody,
376
380
  resolveContextItemDef,
@@ -1,3 +1,23 @@
1
+ /**
2
+ * attach-resource — the single mapping from a picked `Resource` to how it gets
3
+ * attached to a conversation, shared by every surface that attaches resources
4
+ * (SmartAgentResourcePickerButton, the RunControlsMenu Attach tab, …).
5
+ *
6
+ * TWO attach shapes, by kind:
7
+ * - A stored FILE → a DURABLE `platform.associations` edge to the conversation
8
+ * (`file → conversation`). The file ID means its complete existing family,
9
+ * including processed documents and Knowledge. This PERSISTS across turns and reloads; the
10
+ * backend reads the conversation's edges at call time and injects the context.
11
+ * The chip renders from the edge list (see `AttachedDocumentChips`), NOT the
12
+ * ephemeral `instanceResources` slice.
13
+ * - Before the conversation row exists, the same stored FILE identity is held
14
+ * as a `processed_document` instance resource and emitted in request.context.
15
+ * It is never downgraded to a provider-native PDF block.
16
+ * - Everything else (media bytes, notes, tasks, webpages, …) → the per-turn
17
+ * `instanceResources` block. A document with no durable file identity still
18
+ * rides content[] as binary media.
19
+ */
20
+ import type { Dispatch } from "@reduxjs/toolkit";
1
21
  import type { ChatRootState } from "../../../../store/root-state.js";
2
22
  import type { Resource } from "../../../resources/types.js";
3
23
  import type { ResourceBlockType } from "../../../types/instance.types.js";
@@ -24,7 +44,17 @@ export declare function findIdenticalResourceId(state: ChatRootState, conversati
24
44
  * the per-turn binary/instanceResources path unchanged. Closing the hosting
25
45
  * popover is the CALLER's job.
26
46
  */
47
+ /** The store handles a non-React caller passes to the attach functions. */
48
+ export interface AttachStore {
49
+ dispatch: Dispatch;
50
+ getState: () => unknown;
51
+ }
27
52
  export declare function useAttachResource(conversationId: string): (resource: Resource) => Promise<boolean>;
53
+ /**
54
+ * THE attach path, callable outside React (a canvas menu entry, a host port).
55
+ * `useAttachResource` is this bound to the app store.
56
+ */
57
+ export declare function attachResourceToConversation(store: AttachStore, conversationId: string, resource: Resource): Promise<boolean>;
28
58
  /**
29
59
  * Inverse of `useAttachResource` for files toggled in the live picker.
30
60
  * Owned media can have both a durable authorization edge and a per-turn media
@@ -183,7 +183,15 @@ async function attachConversationFileEdge(conversationId, sourceId, fileId, labe
183
183
  function useAttachResource(conversationId) {
184
184
  const dispatch = useAppDispatch();
185
185
  const store = useAppStore();
186
- return async (resource) => {
186
+ return (resource) => attachResourceToConversation(
187
+ { dispatch, getState: store.getState },
188
+ conversationId,
189
+ resource
190
+ );
191
+ }
192
+ async function attachResourceToConversation(store, conversationId, resource) {
193
+ const dispatch = store.dispatch;
194
+ {
187
195
  const getState = () => store.getState();
188
196
  const baseBlockType = resourceTypeToBlockType(resource.type);
189
197
  const blockType = refineBlockType(baseBlockType, resource.data);
@@ -298,7 +306,7 @@ function useAttachResource(conversationId) {
298
306
  resourcePreviewLabel
299
307
  );
300
308
  return true;
301
- };
309
+ }
302
310
  }
303
311
  function useDetachResource(conversationId) {
304
312
  const dispatch = useAppDispatch();
@@ -342,6 +350,7 @@ function useDetachResource(conversationId) {
342
350
  };
343
351
  }
344
352
  export {
353
+ attachResourceToConversation,
345
354
  findIdenticalResourceId,
346
355
  resourceLabel,
347
356
  resourceTypeToBlockType,
@@ -1,5 +1,6 @@
1
1
  import type { RenderedArtifactRepresentation } from "../../../types/instance.types.js";
2
2
  import { type RenderedArtifactSource } from "../../../utils/renderedArtifactContext.js";
3
+ import { type AttachStore } from "./attach-resource.js";
3
4
  /** What a host capture port returns: a stored cloud image, or a local PNG to upload. */
4
5
  export type RenderedCaptureResult = {
5
6
  kind: "stored";
@@ -18,3 +19,8 @@ export interface AttachRenderedArtifactArgs {
18
19
  capture?: () => Promise<RenderedCaptureResult>;
19
20
  }
20
21
  export declare function useAttachRenderedArtifact(conversationId: string | null | undefined): (args: AttachRenderedArtifactArgs) => Promise<boolean>;
22
+ /**
23
+ * The same attach outside React (a canvas "Attach to chat ▸" entry). Without
24
+ * `uploadFiles`, a capture must answer a STORED image (the server engine).
25
+ */
26
+ export declare function attachRenderedArtifact(store: AttachStore, conversationId: string | null | undefined, { source, representation, capture }: AttachRenderedArtifactArgs, uploadFiles?: (files: File[]) => Promise<void>): Promise<boolean>;
@@ -11,7 +11,7 @@ import { generateResourceId } from "../../../redux/execution-system/utils/ids.js
11
11
  import {
12
12
  RENDERED_ARTIFACT_BLOCK_TYPE
13
13
  } from "../../../utils/renderedArtifactContext.js";
14
- import { useAttachResource } from "./attach-resource.js";
14
+ import { attachResourceToConversation } from "./attach-resource.js";
15
15
  import { useUploadAgentResources } from "./usePasteImageResource.js";
16
16
  function screenshotName(title) {
17
17
  const base = title.trim().replace(/[^\w\s.-]+/g, "").trim().slice(0, 60) || "page";
@@ -20,88 +20,98 @@ function screenshotName(title) {
20
20
  function useAttachRenderedArtifact(conversationId) {
21
21
  const dispatch = useAppDispatch();
22
22
  const store = useAppStore();
23
- const attachResource = useAttachResource(conversationId ?? "");
24
23
  const uploadResources = useUploadAgentResources(conversationId ?? "", {
25
24
  uploadRoot: "attachments",
26
25
  uploadPath: "screenshots"
27
26
  });
28
- return async ({ source, representation, capture }) => {
29
- if (!conversationId) {
30
- toast.error("Open a chat to attach this to.");
27
+ return (args) => attachRenderedArtifact(
28
+ { dispatch, getState: store.getState },
29
+ conversationId,
30
+ args,
31
+ uploadResources
32
+ );
33
+ }
34
+ async function attachRenderedArtifact(store, conversationId, { source, representation, capture }, uploadFiles) {
35
+ const { dispatch } = store;
36
+ if (!conversationId) {
37
+ toast.error("Open a chat to attach this to.");
38
+ return false;
39
+ }
40
+ if (representation !== "screenshot") {
41
+ const resourceId = generateResourceId();
42
+ dispatch(
43
+ addResource({
44
+ conversationId,
45
+ blockType: RENDERED_ARTIFACT_BLOCK_TYPE,
46
+ resourceId,
47
+ source,
48
+ options: { representation }
49
+ })
50
+ );
51
+ dispatch(
52
+ setResourcePreview({
53
+ conversationId,
54
+ resourceId,
55
+ preview: `${source.title || "Page"} \xB7 ${representation === "code" ? "Code" : "Text"}`
56
+ })
57
+ );
58
+ return true;
59
+ }
60
+ if (!capture) {
61
+ toast.error("This item can't be captured as an image.");
62
+ return false;
63
+ }
64
+ const name = screenshotName(source.title);
65
+ const placeholderId = generateResourceId();
66
+ dispatch(
67
+ addResource({
68
+ conversationId,
69
+ blockType: "image",
70
+ resourceId: placeholderId,
71
+ source: { filename: name, mime_type: "image/png" }
72
+ })
73
+ );
74
+ try {
75
+ const result = await capture();
76
+ const state = store.getState();
77
+ if (state.instanceResources.byConversationId[conversationId]?.[placeholderId] === void 0) {
31
78
  return false;
32
79
  }
33
- if (representation !== "screenshot") {
34
- const resourceId = generateResourceId();
35
- dispatch(
36
- addResource({
37
- conversationId,
38
- blockType: RENDERED_ARTIFACT_BLOCK_TYPE,
39
- resourceId,
40
- source,
41
- options: { representation }
42
- })
43
- );
44
- dispatch(
45
- setResourcePreview({
46
- conversationId,
47
- resourceId,
48
- preview: `${source.title || "Page"} \xB7 ${representation === "code" ? "Code" : "Text"}`
49
- })
50
- );
80
+ if (result.kind === "file") {
81
+ if (!uploadFiles) throw new Error("This capture needs an upload, which is not available here");
82
+ dispatch(removeResource({ conversationId, resourceId: placeholderId }));
83
+ await uploadFiles([result.file]);
51
84
  return true;
52
85
  }
53
- if (!capture) {
54
- toast.error("This item can't be captured as an image.");
55
- return false;
56
- }
57
- const name = screenshotName(source.title);
58
- const placeholderId = generateResourceId();
86
+ const attached = await attachResourceToConversation(store, conversationId, {
87
+ type: "file",
88
+ data: {
89
+ id: result.fileId,
90
+ filename: name,
91
+ url: result.url ?? void 0,
92
+ type: "image/png",
93
+ mime_type: "image/png",
94
+ details: { filename: name }
95
+ }
96
+ });
97
+ if (!attached) throw new Error("The screenshot could not be attached");
98
+ dispatch(removeResource({ conversationId, resourceId: placeholderId }));
99
+ return true;
100
+ } catch (err) {
101
+ const reason = err instanceof Error ? err.message : "Capture failed";
59
102
  dispatch(
60
- addResource({
103
+ setResourceStatus({
61
104
  conversationId,
62
- blockType: "image",
63
105
  resourceId: placeholderId,
64
- source: { filename: name, mime_type: "image/png" }
106
+ status: "error",
107
+ errorMessage: reason
65
108
  })
66
109
  );
67
- try {
68
- const result = await capture();
69
- const stillAttached = store.getState().instanceResources.byConversationId[conversationId]?.[placeholderId] !== void 0;
70
- if (!stillAttached) return false;
71
- if (result.kind === "file") {
72
- dispatch(removeResource({ conversationId, resourceId: placeholderId }));
73
- await uploadResources([result.file]);
74
- return true;
75
- }
76
- const attached = await attachResource({
77
- type: "file",
78
- data: {
79
- id: result.fileId,
80
- filename: name,
81
- url: result.url ?? void 0,
82
- type: "image/png",
83
- mime_type: "image/png",
84
- details: { filename: name }
85
- }
86
- });
87
- if (!attached) throw new Error("The screenshot could not be attached");
88
- dispatch(removeResource({ conversationId, resourceId: placeholderId }));
89
- return true;
90
- } catch (err) {
91
- const reason = err instanceof Error ? err.message : "Capture failed";
92
- dispatch(
93
- setResourceStatus({
94
- conversationId,
95
- resourceId: placeholderId,
96
- status: "error",
97
- errorMessage: reason
98
- })
99
- );
100
- toast.error(`Couldn't capture the page: ${reason}`);
101
- return false;
102
- }
103
- };
110
+ toast.error(`Couldn't capture the page: ${reason}`);
111
+ return false;
112
+ }
104
113
  }
105
114
  export {
115
+ attachRenderedArtifact,
106
116
  useAttachRenderedArtifact
107
117
  };
@@ -152,7 +152,7 @@ function ChatAgentMenu({
152
152
  else onSelectAgent(info.customAgentId, { mandateKey: MANDATE_KEYS.chat__cx_default });
153
153
  };
154
154
  return /* @__PURE__ */ jsxs(Fragment, { children: [
155
- info.agentId ? /* @__PURE__ */ jsx(ComposerSubmenu, { row: { icon: Webhook, label: `Agent: ${agentLabel}` }, panelClassName: "w-[340px] h-[420px] p-0", children: /* @__PURE__ */ jsx(ComposerAgentPeek, { agentId: info.agentId, onDone: close }) }) : null,
155
+ info.agentId ? /* @__PURE__ */ jsx(ComposerSubmenu, { row: { icon: Webhook, label: `Agent: ${agentLabel}` }, pageTitle: "Agent", panelClassName: "w-[340px] h-[420px] p-0", children: /* @__PURE__ */ jsx(ComposerAgentPeek, { agentId: info.agentId, onDone: close }) }) : null,
156
156
  isManualMode ? /* @__PURE__ */ jsx(
157
157
  ComposerMenuRow,
158
158
  {
@@ -165,6 +165,7 @@ function ChatAgentMenu({
165
165
  ComposerSubmenu,
166
166
  {
167
167
  row: { icon: Cpu, label: `Model: ${info.effectiveModelLabel ?? "Choose a model"}` },
168
+ pageTitle: "Model",
168
169
  panelClassName: MODEL_PICKER_PANEL,
169
170
  children: (closePanel) => /* @__PURE__ */ jsx(
170
171
  QuickRunModelSelect,
@@ -213,7 +214,7 @@ function AnyAgentRow({
213
214
  agentFilter,
214
215
  choose
215
216
  }) {
216
- return /* @__PURE__ */ jsx(ComposerSubmenu, { row: { icon, label }, panelClassName: AGENT_PICKER_PANEL, children: /* @__PURE__ */ jsx(
217
+ return /* @__PURE__ */ jsx(ComposerSubmenu, { row: { icon, label }, pageTitle: "Agents", panelClassName: AGENT_PICKER_PANEL, children: /* @__PURE__ */ jsx(
217
218
  AgentListPanel,
218
219
  {
219
220
  consumerId: "composer-agent-pill",
@@ -105,7 +105,7 @@ export declare function ComposerMenuLevel({ children }: {
105
105
  * Popover anchored to the row, so Radix treats it as a child layer: clicking
106
106
  * inside it never dismisses the parent menu.
107
107
  */
108
- export declare function ComposerSubmenu({ row, children, panelClassName, open: controlledOpen, onOpenChange, drillInPlace, }: {
108
+ export declare function ComposerSubmenu({ row, children, panelClassName, open: controlledOpen, onOpenChange, drillInPlace, pageTitle, }: {
109
109
  row: Omit<ComposerMenuRowProps, "onClick" | "chevron" | "active">;
110
110
  children: ReactNode | ((close: () => void) => ReactNode);
111
111
  /** Width/height of the cascade panel (e.g. "w-[360px] h-[440px]"). */
@@ -118,6 +118,8 @@ export declare function ComposerSubmenu({ row, children, panelClassName, open: c
118
118
  * the menu. The phone sheet pushes pages either way.
119
119
  */
120
120
  drillInPlace?: boolean;
121
+ /** The phone sheet's page title when the row's label carries a value ("Model: X" → "Model"). */
122
+ pageTitle?: string;
121
123
  }): import("react").JSX.Element;
122
124
  /**
123
125
  * What a narrow composer folded out of its rows (Arman, 2026-10-04): its own
@@ -243,7 +243,8 @@ function ComposerSubmenu({
243
243
  panelClassName,
244
244
  open: controlledOpen,
245
245
  onOpenChange,
246
- drillInPlace
246
+ drillInPlace,
247
+ pageTitle
247
248
  }) {
248
249
  const id = useId();
249
250
  const level = useContext(ComposerMenuLevelContext);
@@ -269,7 +270,7 @@ function ComposerSubmenu({
269
270
  ...row,
270
271
  chevron: true,
271
272
  onClick: () => sheetNav.push({
272
- title: typeof row.label === "string" ? row.label : "",
273
+ title: pageTitle ?? (typeof row.label === "string" ? row.label : ""),
273
274
  // A panel with no fixed height on desktop is content-sized on the
274
275
  // phone too — one sizing decision, both presentations.
275
276
  fit: panelClassName && !/(^|\s)h-\[/.test(panelClassName) ? "content" : "fill",
@@ -12,13 +12,10 @@
12
12
  *
13
13
  * Fix conversion or styling in the package (SAME-SESSION LAW), never here.
14
14
  */
15
- import { type PrintOutcome } from "@ai-matrx/print/core";
15
+ import type { PrintOutcome } from "@ai-matrx/print/core";
16
16
  /**
17
- * Prints a message: its prose through the article skin, and every embedded
18
- * block through the SAME adapter its own print uses (`@ai-matrx/print`'s one
19
- * block-printer registry, keyed by artifact type / `__kind`) — a quiz as its
20
- * questions, a deck as its study sheet, a diagram as its drawing, an HTML page
21
- * as its picture (a print window runs no author scripts; the page's own Print
22
- * is the sharp one, and the person is told so).
17
+ * Prints a message through THE message print (`@ai-matrx/rich-content/copy/print-message`):
18
+ * prose plus every embedded block through its own registry adapter — the same
19
+ * composition the content actions' Print uses.
23
20
  */
24
- export declare function printMarkdownContent(markdown: string, title?: string): PrintOutcome | Promise<PrintOutcome>;
21
+ export declare function printMarkdownContent(markdown: string, title?: string): Promise<PrintOutcome>;
@@ -1,74 +1,17 @@
1
- import { listPrintBlocks, printMarkdown, printMarkdownWhenReady } from "@ai-matrx/print/markdown";
2
- import { getBlockPrinter } from "@ai-matrx/print/core";
1
+ import { printMessage } from "@ai-matrx/rich-content/copy/print-message";
3
2
  import { notifyPrintOutcome } from "../../host/ui-slots.js";
4
- const FRAME_BLOCK_TYPES = /* @__PURE__ */ new Set(["html", "react", "iframe"]);
5
- function blockData(block) {
6
- const text = block.body.trim();
7
- if (text.startsWith("{") || text.startsWith("[")) {
8
- try {
9
- return JSON.parse(text);
10
- } catch {
11
- return block.body;
12
- }
13
- }
14
- return block.body;
15
- }
16
- function adapterFor(block) {
17
- const printer = getBlockPrinter(block.type);
18
- return printer?.toPrintHtml ? printer : null;
19
- }
20
- function printMarkdownContent(markdown, title = "AI Response") {
21
- const blocks = listPrintBlocks(markdown, "article");
22
- const hasMermaid = blocks.some((block) => block.source === "fence" && block.type === "mermaid");
23
- const adapted = blocks.filter((block) => adapterFor(block) !== null);
24
- if (!hasMermaid && adapted.length === 0) {
25
- const outcome = printMarkdown(markdown, { skin: "article", title });
26
- notifyPrintOutcome(outcome);
27
- return outcome;
28
- }
29
- let drawing = null;
30
- const drawAll = () => drawing ??= (async () => {
31
- try {
32
- const { drawMermaidForPrint } = await import("@ai-matrx/rich-content/mermaid/print-render");
33
- const { pictures, failed } = await drawMermaidForPrint(markdown);
34
- if (failed > 0) {
35
- const { toast } = await import("../../host/notify.js");
36
- toast.warning(
37
- `${failed} diagram${failed === 1 ? "" : "s"} could not be drawn and print${failed === 1 ? "s" : ""} as source.`
38
- );
39
- }
40
- return pictures;
41
- } catch (error) {
42
- console.error("[print] diagrams could not be drawn", error);
43
- const { toast } = await import("../../host/notify.js");
44
- toast.warning("Diagrams could not be drawn, so they print as source.");
45
- return null;
46
- }
47
- })();
48
- return printMarkdownWhenReady(markdown, {
3
+ async function printMarkdownContent(markdown, title = "AI Response") {
4
+ const outcome = await printMessage(markdown, {
49
5
  skin: "article",
50
6
  title,
51
- renderBlock: async (block) => {
52
- if (block.source === "fence" && block.type === "mermaid") {
53
- const svg = (await drawAll())?.get(block.body);
54
- return svg ? { svg, alt: "Diagram" } : null;
55
- }
56
- const printer = adapterFor(block);
57
- if (!printer?.toPrintHtml) return null;
58
- return printer.toPrintHtml(blockData(block), {
59
- type: block.type,
60
- raw: block.body,
61
- ...block.title ? { title: block.title } : {}
62
- });
63
- }
64
- }).then(async (outcome) => {
65
- notifyPrintOutcome(outcome);
66
- if (outcome === "opened" && adapted.some((block) => FRAME_BLOCK_TYPES.has(block.type))) {
7
+ onNotice: async (level, message) => {
67
8
  const { toast } = await import("../../host/notify.js");
68
- toast.info("Web pages print as a picture inside a message. For sharp text, print the page from its card.");
9
+ if (level === "warning") toast.warning(message);
10
+ else toast.info(message);
69
11
  }
70
- return outcome;
71
12
  });
13
+ notifyPrintOutcome(outcome);
14
+ return outcome;
72
15
  }
73
16
  export {
74
17
  printMarkdownContent
@@ -80,6 +80,12 @@ interface ValueChangeProps<T> {
80
80
  [extra: string]: any;
81
81
  }
82
82
  type AnyComponent = ComponentType<any>;
83
+ export interface CanvasItemCardProps {
84
+ /** `canvas_items.id` of the version to show. */
85
+ canvasItemId: string;
86
+ /** Its chain version, when the caller knows it. */
87
+ version?: number;
88
+ }
83
89
  /**
84
90
  * A component slot typed by the props the PACKAGE passes. Bivariant on purpose: the host
85
91
  * component has its own (usually richer) prop type, and a slot must accept it as long as the
@@ -105,6 +111,8 @@ export interface ChatUiSlots {
105
111
  FileResourceChip: AnyComponent;
106
112
  ConnectorMark: AnyComponent;
107
113
  MessageFilesStrip: AnyComponent;
114
+ /** A canvas item rendered by id through the host's block registry (a tool's new artifact version). */
115
+ CanvasItemCard: SlotComponent<CanvasItemCardProps>;
108
116
  RulebookNudge: AnyComponent;
109
117
  NegativeVerdictFollowUp: AnyComponent;
110
118
  SpeakerButton: AnyComponent;
@@ -468,6 +476,7 @@ export declare const TableChooser: AnyComponent;
468
476
  export declare const FileResourceChip: AnyComponent;
469
477
  export declare const ConnectorMark: AnyComponent;
470
478
  export declare const MessageFilesStrip: AnyComponent;
479
+ export declare const CanvasItemCard: SlotComponent<CanvasItemCardProps>;
471
480
  export declare const RulebookNudge: AnyComponent;
472
481
  export declare const NegativeVerdictFollowUp: AnyComponent;
473
482
  export declare const SpeakerButton: AnyComponent;
@@ -232,6 +232,7 @@ const TableChooser = slotComponent("TableChooser");
232
232
  const FileResourceChip = slotComponent("FileResourceChip");
233
233
  const ConnectorMark = slotComponent("ConnectorMark");
234
234
  const MessageFilesStrip = slotComponent("MessageFilesStrip", optionalWidget("MessageFilesStrip"));
235
+ const CanvasItemCard = slotComponent("CanvasItemCard", optionalWidget("CanvasItemCard"));
235
236
  const RulebookNudge = slotComponent("RulebookNudge", optionalWidget("RulebookNudge"));
236
237
  const NegativeVerdictFollowUp = slotComponent("NegativeVerdictFollowUp", optionalWidget("NegativeVerdictFollowUp"));
237
238
  const SpeakerButton = slotComponent("SpeakerButton", optionalWidget("SpeakerButton"));
@@ -527,6 +528,7 @@ export {
527
528
  AttachedResourcesSection,
528
529
  AuthGateDialog,
529
530
  BlockHoverPreview,
531
+ CanvasItemCard,
530
532
  ChangeDiff,
531
533
  CitationChip,
532
534
  ClampedNumberInput,
@@ -4,6 +4,7 @@ import {
4
4
  ChevronDown,
5
5
  FileText,
6
6
  Frame,
7
+ Globe,
7
8
  PanelRightOpen,
8
9
  Pencil,
9
10
  StickyNote,
@@ -35,6 +36,11 @@ const KIND_META = {
35
36
  Icon: StickyNote,
36
37
  noun: "Note",
37
38
  iconTint: "text-amber-600 dark:text-amber-400"
39
+ },
40
+ canvas_item: {
41
+ Icon: Globe,
42
+ noun: "Page",
43
+ iconTint: "text-emerald-600 dark:text-emerald-400"
38
44
  }
39
45
  };
40
46
  function ArtifactResultBar({
@@ -134,7 +140,7 @@ function ArtifactResultBar({
134
140
  /* @__PURE__ */ jsx(Frame, { className: "size-4" }),
135
141
  "Canvas"
136
142
  ] }),
137
- /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: openEdit, className: "gap-2", children: [
143
+ artifact.kind !== "canvas_item" && /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: openEdit, className: "gap-2", children: [
138
144
  /* @__PURE__ */ jsx(Pencil, { className: "size-4" }),
139
145
  "Edit"
140
146
  ] }),
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import React, { useEffect, useMemo, useState } from "react";
4
4
  import {
5
5
  ChevronDown,
@@ -41,7 +41,7 @@ import {
41
41
  } from "../result-fields/ToolErrorCard.js";
42
42
  import { guardRoutingOf } from "../result-fields/guard-routing.js";
43
43
  import { resultAsObject } from "../renderers/_shared.js";
44
- import { RecordChangeApprovalCard, useHeldWriteTableName } from "../../host/ui-slots.js";
44
+ import { CanvasItemCard, RecordChangeApprovalCard, useHeldWriteTableName } from "../../host/ui-slots.js";
45
45
  import { SHOW_PARKED_ASK_EVENT } from "../../agents/runtime-reconnect/show-parked-ask.js";
46
46
  import { ParkedOnPersonCard } from "../../action-requests/components/ParkedOnPersonCard.js";
47
47
  import {
@@ -193,6 +193,7 @@ const ToolCallVisualizationInner = ({
193
193
  const querySubtitle = phase === "error" || heldWait || parkedAsk ? null : headerSubtitle;
194
194
  const artifactRaw = phase === "complete" && settled.length === 1 && !heldWait ? getToolArtifact(headerTool) : null;
195
195
  const artifact = artifactRaw && (artifactRaw.kind === "working_document" ? typeof conversationId === "string" && conversationId.length > 0 : Boolean(artifactRaw.id)) ? artifactRaw : null;
196
+ const canvasItemCard = artifact?.kind === "canvas_item" && artifact.id ? /* @__PURE__ */ jsx("div", { className: "mt-1.5", children: /* @__PURE__ */ jsx(CanvasItemCard, { canvasItemId: artifact.id, version: artifact.version }) }) : null;
196
197
  if (entries.length === 0) return null;
197
198
  const handleOpenOverlay = (tabId) => {
198
199
  setInitialOverlayTab(tabId);
@@ -232,17 +233,20 @@ const ToolCallVisualizationInner = ({
232
233
  className
233
234
  ),
234
235
  children: [
235
- artifact ? /* @__PURE__ */ jsx(
236
- ArtifactResultBar,
237
- {
238
- artifact,
239
- conversationId,
240
- peekExpanded: isExpanded,
241
- onTogglePeek: toggleExpand,
242
- onOpenOverlay: handleOpenOverlay,
243
- onOpenWindowPanel: handleOpenWindowPanel
244
- }
245
- ) : cardMode && headerTool && HeaderInline ? (
236
+ artifact ? /* @__PURE__ */ jsxs(Fragment, { children: [
237
+ /* @__PURE__ */ jsx(
238
+ ArtifactResultBar,
239
+ {
240
+ artifact,
241
+ conversationId,
242
+ peekExpanded: isExpanded,
243
+ onTogglePeek: toggleExpand,
244
+ onOpenOverlay: handleOpenOverlay,
245
+ onOpenWindowPanel: handleOpenWindowPanel
246
+ }
247
+ ),
248
+ canvasItemCard
249
+ ] }) : cardMode && headerTool && HeaderInline ? (
246
250
  // Self-headed entity card — no fold line; the card's own header carries
247
251
  // the name + "Open in" menu. createElement (not JSX) since the component
248
252
  // is resolved at runtime from the registry.
@@ -14,18 +14,22 @@
14
14
  * branch here + one entry in the bar's `KIND_META`.
15
15
  */
16
16
  import type { ToolLifecycleEntry } from "../../agents/types/request.types.js";
17
- export type ToolArtifactKind = "working_document" | "note";
17
+ export type ToolArtifactKind = "working_document" | "note" | "canvas_item";
18
18
  export interface ToolArtifact {
19
19
  kind: ToolArtifactKind;
20
20
  /** Generic fallback label; the working-doc bar prefers the live document title. */
21
21
  title: string;
22
22
  /** Past-tense verb for the sub-label: "Edited" / "Rewrote" / "Saved" / "Updated". */
23
23
  verbPast: string;
24
- /** Open handle for id-based kinds (note). The working document uses the shell's conversationId. */
24
+ /** Open handle for id-based kinds (note, canvas_item). The working document uses the shell's conversationId. */
25
25
  id?: string;
26
+ /** canvas_item: the chain version this call produced (the card shows THAT version). */
27
+ version?: number;
26
28
  }
27
29
  /** The `ctx_patch` tool and its live production alias — the canonical set of
28
30
  * tool names that carry a working-document (or any ctx key) patch. Shared so
29
31
  * the drawer's live-diff hook matches patch entries the same way. */
30
32
  export declare const PATCH_TOOLS: Set<string>;
33
+ /** Tools whose result is a new version of a canvas item (`artifact_id` + `version`). */
34
+ export declare const CANVAS_ITEM_TOOLS: Set<string>;
31
35
  export declare function getToolArtifact(entry: ToolLifecycleEntry | null | undefined): ToolArtifact | null;
@@ -1,6 +1,7 @@
1
1
  import { getArg, resultAsObject } from "../renderers/_shared.js";
2
2
  const PATCH_TOOLS = /* @__PURE__ */ new Set(["ctx_patch", "context_patch"]);
3
3
  const NOTE_TOOLS = /* @__PURE__ */ new Set(["note", "war_room_update_note"]);
4
+ const CANVAS_ITEM_TOOLS = /* @__PURE__ */ new Set(["edit_artifact"]);
4
5
  const WORKING_DOC_KEY = "working_document";
5
6
  function pastForPatchCommand(command) {
6
7
  switch (command) {
@@ -45,9 +46,23 @@ function getToolArtifact(entry) {
45
46
  id
46
47
  };
47
48
  }
49
+ if (CANVAS_ITEM_TOOLS.has(tool)) {
50
+ const facts = result && typeof result.artifact_id === "string" ? result : result && typeof result.details === "object" && result.details !== null ? result.details : null;
51
+ const id = facts && typeof facts.artifact_id === "string" ? facts.artifact_id : void 0;
52
+ if (!id) return null;
53
+ const version = facts && typeof facts.version === "number" ? facts.version : void 0;
54
+ return {
55
+ kind: "canvas_item",
56
+ title: version ? `Version ${version}` : "New version",
57
+ verbPast: "Edited",
58
+ id,
59
+ ...version ? { version } : {}
60
+ };
61
+ }
48
62
  return null;
49
63
  }
50
64
  export {
65
+ CANVAS_ITEM_TOOLS,
51
66
  PATCH_TOOLS,
52
67
  getToolArtifact
53
68
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/chat",
3
- "version": "0.5.6",
3
+ "version": "0.5.11",
4
4
  "description": "THE one AI Matrx person-to-agent chat system: the full chat (running agents, conversations, tool calls, streaming, context, surfaces, voice) as per-module subpaths @ai-matrx/chat/<domain>/<module> over a host contract (/host, /host/react) whose only required value is a db. One chat, one renderer: the old kernel entries (., /external-store, /matrx, /react, /tools, styles.css, tokens.css) are retired.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -166,25 +166,25 @@
166
166
  "server-only": "^0.0.1",
167
167
  "uuid": "^14.0.2",
168
168
  "zod": "^4.6.5",
169
+ "@ai-matrx/agents": "latest",
169
170
  "@ai-matrx/alchemy": "latest",
170
171
  "@ai-matrx/associations": "latest",
171
172
  "@ai-matrx/browser-audio": "latest",
172
- "@ai-matrx/agents": "latest",
173
173
  "@ai-matrx/canvas": "latest",
174
174
  "@ai-matrx/content-ir": "latest",
175
175
  "@ai-matrx/content-ir-react": "latest",
176
176
  "@ai-matrx/data": "latest",
177
+ "@ai-matrx/design-system": "latest",
177
178
  "@ai-matrx/diff": "latest",
178
179
  "@ai-matrx/icons": "latest",
179
180
  "@ai-matrx/media": "latest",
181
+ "@ai-matrx/kit": "latest",
180
182
  "@ai-matrx/print": "latest",
181
183
  "@ai-matrx/realtime": "latest",
182
184
  "@ai-matrx/records": "latest",
183
185
  "@ai-matrx/records-ui": "latest",
184
186
  "@ai-matrx/rich-content": "latest",
185
- "@ai-matrx/rich-editor": "latest",
186
- "@ai-matrx/kit": "latest",
187
- "@ai-matrx/design-system": "latest"
187
+ "@ai-matrx/rich-editor": "latest"
188
188
  },
189
189
  "devDependencies": {
190
190
  "@arethetypeswrong/cli": "^0.18.5",