@ai-matrx/chat 0.5.6 → 0.5.8

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,18 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.5.8 — attach from outside React
4
+
5
+ - `attachResourceToConversation(store, conversationId, resource)` — THE attach path without a hook
6
+ (`useAttachResource` is it bound to the app store); `attachRenderedArtifact(store, conversationId,
7
+ args)` — the same for a rendered page, for a canvas "Attach to chat ▸" entry or any host port.
8
+ - Consumer action: none required; a canvas host may wire `attachRenderedArtifact` into
9
+ `CanvasOutputPorts.attachOptions`.
10
+
11
+ ## 0.5.7 — a registry/list disagreement no longer crashes the page
12
+
13
+ - The context-items load-time drift check logs a loud console error instead of throwing; the
14
+ unregistered type renders through the generic fallback body. A jest test fails on any disagreement.
15
+
3
16
  ## 0.5.6 — 0.5.5 threw at load
4
17
 
5
18
  - `CONTEXT_ITEM_BLOCK_TYPES` lists `rendered_artifact`: 0.5.5 registered the def without it, so the
@@ -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
  };
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.8",
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
- "@ai-matrx/browser-audio": "latest",
172
- "@ai-matrx/agents": "latest",
173
172
  "@ai-matrx/canvas": "latest",
174
173
  "@ai-matrx/content-ir": "latest",
175
174
  "@ai-matrx/content-ir-react": "latest",
176
175
  "@ai-matrx/data": "latest",
176
+ "@ai-matrx/browser-audio": "latest",
177
177
  "@ai-matrx/diff": "latest",
178
178
  "@ai-matrx/icons": "latest",
179
- "@ai-matrx/media": "latest",
179
+ "@ai-matrx/design-system": "latest",
180
+ "@ai-matrx/kit": "latest",
180
181
  "@ai-matrx/print": "latest",
181
182
  "@ai-matrx/realtime": "latest",
182
- "@ai-matrx/records": "latest",
183
+ "@ai-matrx/media": "latest",
183
184
  "@ai-matrx/records-ui": "latest",
184
- "@ai-matrx/rich-content": "latest",
185
+ "@ai-matrx/records": "latest",
185
186
  "@ai-matrx/rich-editor": "latest",
186
- "@ai-matrx/kit": "latest",
187
- "@ai-matrx/design-system": "latest"
187
+ "@ai-matrx/rich-content": "latest"
188
188
  },
189
189
  "devDependencies": {
190
190
  "@arethetypeswrong/cli": "^0.18.5",