@ai-matrx/chat 0.4.22 → 0.4.26

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/agent-copy/RichCopySplit.d.ts +11 -1
  3. package/dist/agent-copy/RichCopySplit.js +9 -3
  4. package/dist/agents/components/chat/ChatConversationRoom.js +6 -4
  5. package/dist/agents/components/chat/ChatNewClient.js +7 -4
  6. package/dist/agents/components/chat/ChatNewHeader.js +3 -2
  7. package/dist/agents/components/chat/default-chat-mandate.d.ts +7 -0
  8. package/dist/agents/components/chat/default-chat-mandate.js +9 -0
  9. package/dist/agents/components/context-items/bodies/UnregisteredBody.d.ts +6 -3
  10. package/dist/agents/components/context-items/bodies/UnregisteredBody.js +3 -9
  11. package/dist/agents/components/inputs/resources/attach-resource.js +1 -0
  12. package/dist/agents/components/inputs/smart-input/SmartAgentInput.js +16 -1
  13. package/dist/agents/components/inputs/smart-input/composer/ComposerMenu.js +3 -2
  14. package/dist/agents/components/inputs/smart-input/composer/ComposerPlusMenu.js +41 -35
  15. package/dist/agents/components/messages-display/assistant/AgentAssistantMessage.js +29 -20
  16. package/dist/agents/components/messages-display/user/AgentUserMessage.js +3 -1
  17. package/dist/agents/components/shared/AgentConversationColumn.d.ts +10 -1
  18. package/dist/agents/components/shared/AgentConversationColumn.js +26 -2
  19. package/dist/agents/redux/execution-system/messages/messages.slice.js +6 -1
  20. package/dist/agents/redux/execution-system/thunks/await-launching-conversation.d.ts +16 -0
  21. package/dist/agents/redux/execution-system/thunks/await-launching-conversation.js +16 -0
  22. package/dist/agents/redux/execution-system/thunks/execute-instance.thunk.js +34 -1
  23. package/dist/agents/redux/execution-system/thunks/process-stream.js +11 -0
  24. package/dist/agents/redux/execution-system/thunks/smart-execute.thunk.js +10 -0
  25. package/dist/agents/redux/execution-system/utils/required-organization.js +2 -2
  26. package/dist/agents/ui-first-tools/redux/build-ambient-context.js +8 -7
  27. package/dist/host/app-data-slots.d.ts +0 -1
  28. package/dist/host/app-data-slots.js +25 -5
  29. package/dist/host/bare-host-chrome.js +4 -0
  30. package/dist/host/contract.d.ts +8 -2
  31. package/dist/host/contract.js +2 -1
  32. package/dist/host/conversation-hooks-react.d.ts +2 -0
  33. package/dist/host/conversation-hooks-react.js +12 -0
  34. package/dist/host/conversation-hooks.d.ts +79 -0
  35. package/dist/host/conversation-hooks.js +91 -0
  36. package/dist/host/defaults/bare-block-dispatch.d.ts +6 -0
  37. package/dist/host/defaults/bare-block-dispatch.js +60 -0
  38. package/dist/host/defaults/files-host.d.ts +26 -0
  39. package/dist/host/defaults/files-host.js +212 -0
  40. package/dist/host/defaults/memory-navigation.d.ts +12 -0
  41. package/dist/host/defaults/memory-navigation.js +102 -0
  42. package/dist/host/defaults/resource-picker-menu.d.ts +14 -0
  43. package/dist/host/defaults/resource-picker-menu.js +117 -0
  44. package/dist/host/defaults/setting-control-input.d.ts +11 -0
  45. package/dist/host/defaults/setting-control-input.js +91 -0
  46. package/dist/host/index.d.ts +2 -0
  47. package/dist/host/index.js +5 -0
  48. package/dist/host/react.d.ts +1 -0
  49. package/dist/host/react.js +2 -0
  50. package/dist/host/ui-slots.d.ts +25 -1
  51. package/dist/host/ui-slots.js +83 -25
  52. package/dist/host/window-openers.d.ts +2 -0
  53. package/dist/host/window-openers.js +4 -0
  54. package/dist/react.cjs +1 -1
  55. package/dist/react.cjs.map +1 -1
  56. package/dist/react.js +1 -1
  57. package/dist/react.js.map +1 -1
  58. package/dist/tool-call-visualization/registry/registry.js +4 -4
  59. package/dist/ui/aspect-ratio-options.d.ts +4 -26
  60. package/dist/ui/aspect-ratio-options.js +7 -75
  61. package/dist/ui/chat-panel/ChatPanel.d.ts +50 -0
  62. package/dist/ui/chat-panel/ChatPanel.js +175 -0
  63. package/dist/ui/chat-panel/drag.d.ts +87 -0
  64. package/dist/ui/chat-panel/drag.js +194 -0
  65. package/dist/window-panels/windows/agents/AgentRunWindow.js +17 -0
  66. package/dist/window-panels/windows/agents/agentRunWindowAddress.d.ts +19 -0
  67. package/dist/window-panels/windows/agents/agentRunWindowAddress.js +11 -1
  68. package/package.json +5 -5
@@ -349,7 +349,12 @@ const messagesSlice = createSlice({
349
349
  // The live receipt link — the persisted receipt wins once the
350
350
  // hydrated row carries one (selectMessageContextReceipt).
351
351
  ...live?._requestId ? { _requestId: live._requestId } : {},
352
- ...live?._liveContextReceipt ? { _liveContextReceipt: live._liveContextReceipt } : {}
352
+ ...live?._liveContextReceipt ? { _liveContextReceipt: live._liveContextReceipt } : {},
353
+ // An answer open in the in-place editor stays open: the flag is
354
+ // client-only, and a re-read of the conversation (a window loading
355
+ // the conversation the page already loaded) used to drop it, closing
356
+ // the editor ~1.5s after a double-click and discarding the draft.
357
+ ...live?._editingInPlace ? { _editingInPlace: live._editingInPlace } : {}
353
358
  };
354
359
  entry.orderedIds.push(msg.id);
355
360
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * A SEND THAT BEATS ITS LAUNCH WAITS FOR IT.
3
+ *
4
+ * A fresh chat's composer is interactive as soon as it mounts, holding the id the launcher
5
+ * reserved, while the launcher is still creating that conversation (the agent's run controls, the
6
+ * organization, the mandate are being read). A person who types fast pressed Send in that window
7
+ * and the send was dropped as "no longer exists in this tab" (P29a canary: a send ~7 s after load,
8
+ * right after the workspace was chosen). The conversation was not gone; it had not landed yet.
9
+ *
10
+ * So the send waits, briefly, for its conversation to appear. A conversation that really was
11
+ * reaped never appears, and the caller still says so after the wait.
12
+ */
13
+ import type { ChatRootState } from "../../../../store/root-state.js";
14
+ /** How long a send waits for a launching conversation before calling it gone. */
15
+ export declare const LAUNCHING_CONVERSATION_WAIT_MS = 15000;
16
+ export declare function awaitLaunchingConversation(getState: () => ChatRootState, conversationId: string, timeoutMs?: number): Promise<boolean>;
@@ -0,0 +1,16 @@
1
+ const LAUNCHING_CONVERSATION_WAIT_MS = 15e3;
2
+ const POLL_MS = 50;
3
+ async function awaitLaunchingConversation(getState, conversationId, timeoutMs = LAUNCHING_CONVERSATION_WAIT_MS) {
4
+ const exists = () => getState().conversations.byConversationId[conversationId] !== void 0;
5
+ if (exists()) return true;
6
+ const deadline = Date.now() + timeoutMs;
7
+ while (Date.now() < deadline) {
8
+ await new Promise((resolve) => setTimeout(resolve, POLL_MS));
9
+ if (exists()) return true;
10
+ }
11
+ return false;
12
+ }
13
+ export {
14
+ LAUNCHING_CONVERSATION_WAIT_MS,
15
+ awaitLaunchingConversation
16
+ };
@@ -1,6 +1,10 @@
1
1
  import {
2
2
  executionRejectionMeta
3
3
  } from "./execution-rejection-meta.js";
4
+ import {
5
+ continuesThroughMandate,
6
+ takeTurnExtras
7
+ } from "../../../../host/conversation-hooks.js";
4
8
  import { isHostConversation } from "../host-conversations/host-conversations.selectors.js";
5
9
  import { attributionForSend } from "../../../../host/app-identity.js";
6
10
  import { mintClientTempId } from "@ai-matrx/kit/ids";
@@ -483,7 +487,24 @@ const executeInstance = createAsyncThunk(
483
487
  const pendingBypass = dispatch(
484
488
  consumePendingCacheBypass(conversationId)
485
489
  );
486
- if (isContinuation) {
490
+ const hostExtras = takeTurnExtras(conversationId);
491
+ if (isContinuation && instance.mandateKey && !instance.initialAgentVersionId && continuesThroughMandate(conversationId)) {
492
+ const start = resolveStartPath({
493
+ agentId: instance.agentId,
494
+ pinnedVersionId: null,
495
+ mandateKey: instance.mandateKey,
496
+ overrideConfig: overrideConfig ?? void 0
497
+ });
498
+ url = `${baseUrl}${start.path}`;
499
+ routedPayload = {
500
+ ...payload,
501
+ conversation_id: conversationId,
502
+ is_new: false,
503
+ store: true,
504
+ ...retry && { retry: true },
505
+ ...pendingBypass && { cache_bypass: pendingBypass }
506
+ };
507
+ } else if (isContinuation) {
487
508
  const convPath = resolveEndpointPath(
488
509
  "/ai/conversations/{conversation_id}",
489
510
  overrideConfig
@@ -510,6 +531,18 @@ const executeInstance = createAsyncThunk(
510
531
  ...pendingBypass && { cache_bypass: pendingBypass }
511
532
  };
512
533
  }
534
+ if (hostExtras?.variables) {
535
+ routedPayload.variables = {
536
+ ...routedPayload.variables ?? {},
537
+ ...hostExtras.variables
538
+ };
539
+ }
540
+ if (hostExtras?.context) {
541
+ routedPayload.context = {
542
+ ...routedPayload.context ?? {},
543
+ ...hostExtras.context
544
+ };
545
+ }
513
546
  {
514
547
  const { overrides, error } = resolveRequestOverrides(
515
548
  state,
@@ -1,3 +1,4 @@
1
+ import { tapStreamEvent } from "../../../../host/conversation-hooks.js";
1
2
  import { selectHostConversation } from "../host-conversations/host-conversations.selectors.js";
2
3
  import {
3
4
  applyServerUsageState,
@@ -493,8 +494,16 @@ async function processStream({
493
494
  }) : rawEvents;
494
495
  streamFailure = null;
495
496
  let sawTerminalEvent = false;
497
+ let tapHeld = null;
498
+ const flushTap = () => {
499
+ if (!tapHeld) return;
500
+ const held = tapHeld;
501
+ tapHeld = null;
502
+ tapStreamEvent(conversationId, requestId, held.event);
503
+ };
496
504
  try {
497
505
  for await (const event of events) {
506
+ flushTap();
498
507
  const stamp = readStreamCursor(event);
499
508
  if (stamp !== null) {
500
509
  if (!transportCursor.admit(event)) continue;
@@ -506,6 +515,7 @@ async function processStream({
506
515
  scheduleBatchEvent();
507
516
  }
508
517
  totalEvents++;
518
+ tapHeld = { event };
509
519
  const now = performance.now();
510
520
  if (activeOnEvent) {
511
521
  try {
@@ -2080,6 +2090,7 @@ async function processStream({
2080
2090
  } catch (err) {
2081
2091
  streamFailure = err instanceof Error ? err : new Error(String(err));
2082
2092
  }
2093
+ flushTap();
2083
2094
  if (sawTerminalEvent && streamFailure !== null) {
2084
2095
  streamFailure = null;
2085
2096
  }
@@ -6,6 +6,7 @@ import {
6
6
  SUBMISSION_REFUSED_AT_ADMISSION
7
7
  } from "./execute-instance.thunk.js";
8
8
  import { executeManualInstance } from "./execute-manual-instance.thunk.js";
9
+ import { awaitLaunchingConversation } from "./await-launching-conversation.js";
9
10
  import { splitInputIntoNewConversation } from "./create-instance.thunk.js";
10
11
  import { abortConversation, hasAbortController } from "./abort-registry.js";
11
12
  import { setInstanceStatus } from "../conversations/conversations.slice.js";
@@ -65,6 +66,7 @@ import { markResourcesSubmitted } from "../instance-resources/instance-resources
65
66
  import { flushRemarkWrites, hasPendingRemarkWrites } from "../instance-resources/remarks.js";
66
67
  import { withFreshRemarkRefs } from "../instance-resources/remarks-wire.js";
67
68
  import { isOrganizationSelectionCancelled } from "../../../../host/org.js";
69
+ import { runBeforeSend } from "../../../../host/conversation-hooks.js";
68
70
  function hasConversationAtExecutionBoundary(state, conversationId) {
69
71
  return state.conversations.byConversationId[conversationId] !== void 0;
70
72
  }
@@ -152,6 +154,9 @@ function holdBehindInFlightSend(dispatch, getState, args) {
152
154
  const smartExecute = createAsyncThunk(
153
155
  "instances/smartExecute",
154
156
  async ({ conversationId, surfaceKey, whileRunning = "queue", submission: handed }, { getState, dispatch }) => {
157
+ if (!handed && !hasConversationAtExecutionBoundary(getState(), conversationId)) {
158
+ await awaitLaunchingConversation(getState, conversationId);
159
+ }
155
160
  const entryState = getState();
156
161
  if (!handed) {
157
162
  const inputEntry = entryState.instanceUserInput?.byConversationId[conversationId];
@@ -322,6 +327,11 @@ const smartExecute = createAsyncThunk(
322
327
  })
323
328
  );
324
329
  preSendShown = true;
330
+ const hostGate = await runBeforeSend(conversationId, submission.text);
331
+ if (!hostGate.ok) {
332
+ toast.info("Not sent", { description: hostGate.sentence });
333
+ return;
334
+ }
325
335
  try {
326
336
  await dispatch(
327
337
  refreshSurfaceScope({ conversationId, composerText: submission.text })
@@ -1,8 +1,8 @@
1
1
  import { adminLaneOrganizationId } from "../../../../host/server/admin-lane.js";
2
2
  import { selectOrganizationId } from "../../../../host/org.js";
3
+ import { selectAccessToken, selectFingerprintId } from "../../../../host/identity.js";
3
4
  function isFingerprintGuestExecution(state) {
4
- const partial = state;
5
- return !partial.userAuth?.accessToken && Boolean(partial.userProfile?.fingerprintId);
5
+ return !selectAccessToken(state) && Boolean(selectFingerprintId(state));
6
6
  }
7
7
  function executionOrganizationForRequest(state, conversationId) {
8
8
  if (isFingerprintGuestExecution(state)) return void 0;
@@ -1,5 +1,6 @@
1
1
  import { selectProjectId, selectProjectName, selectTaskId, selectTaskName, selectScopeSelectionsContext } from "../../../context/sources/scopes.js";
2
2
  import { selectOrganizationId, selectOrganizationName } from "../../../host/org.js";
3
+ import { selectChatIdentity } from "../../../host/identity.js";
3
4
  function snapshotRoute(engineered) {
4
5
  if (typeof window === "undefined") {
5
6
  return { url: null, title: null, route_kind: "server" };
@@ -18,22 +19,22 @@ function snapshotRoute(engineered) {
18
19
  return { url, title, route_kind };
19
20
  }
20
21
  function buildAmbientContext(state, conversationId) {
21
- const auth = state.userAuth;
22
- if (!auth?.id) return null;
23
- const profile = state.userProfile?.userMetadata;
24
- const name = profile?.fullName || profile?.name || profile?.preferredUsername || auth.email || "user";
22
+ const auth = selectChatIdentity(state);
23
+ if (!auth.userId) return null;
24
+ const name = auth.displayName || auth.name || auth.preferredUsername || auth.email || "user";
25
25
  const now = /* @__PURE__ */ new Date();
26
26
  const timezone = typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone : "UTC";
27
27
  const locale = typeof navigator !== "undefined" && navigator.language ? navigator.language : "en-US";
28
28
  const snapshot = {
29
29
  user: {
30
- id: auth.id,
30
+ id: auth.userId,
31
31
  name,
32
32
  email: auth.email ?? null,
33
33
  // ADMIN POWER, not identity: an agent on a user page must see an admin
34
34
  // exactly as it sees anyone else (utils/supabase/adminLane.ts).
35
- is_admin: auth.adminLaneOpen === true && (auth.isAdmin ?? false),
36
- admin_level: auth.adminLaneOpen === true ? auth.adminLevel ?? null : null
35
+ // The identity port's adminLevel is already what the host grants on this page.
36
+ is_admin: auth.adminLevel !== null,
37
+ admin_level: auth.adminLevel
37
38
  },
38
39
  client: {
39
40
  surface: "nextjs",
@@ -76,7 +76,6 @@ export declare const useAnchorRecords: (anchor: {
76
76
  type: string;
77
77
  id: string;
78
78
  } | null, enabled?: boolean) => AnchorRecordsState;
79
- /** A host with no shape catalog has no shapes to browse: the read says so, the picker shows it. */
80
79
  export declare const fetchShapePage: (query: EntityListQuery, sort: EntityListSort) => Promise<EntityListPage<ShapeBrowseRow>>;
81
80
  export declare const fetchShapeByKind: (kind: string) => Promise<ShapeBrowseRow | null>;
82
81
  /** A host with no Source Inspector opens a web source in a new tab; a file source has no target here. */
@@ -1,4 +1,6 @@
1
1
  import { hostFn, hostSlot } from "./ui-slots.js";
2
+ import { listOrgParam } from "@ai-matrx/records/list";
3
+ import { supabase } from "./db.js";
2
4
  const CONVERSATION_ANCHOR_TYPE = "conversation";
3
5
  const contentIrKindValidator = hostFn("contentIrKindValidator", () => ({
4
6
  validate: async (_value, kind) => ({
@@ -20,11 +22,29 @@ const useAnchorRecords = hostFn("useAnchorRecords", () => ({
20
22
  message: null,
21
23
  reload: () => void 0
22
24
  }));
23
- const fetchShapePage = hostFn("fetchShapePage", async () => {
24
- throw new Error("This host has no shape catalog (registerChatUi fetchShapePage)");
25
- });
26
- const fetchShapeByKind = hostFn("fetchShapeByKind", async () => {
27
- throw new Error("This host has no shape catalog (registerChatUi fetchShapeByKind)");
25
+ async function readShapePage(query, sort) {
26
+ const { data, error } = await supabase.rpc("shx_list_scoped", {
27
+ p_scope: query.scope.kind,
28
+ p_org_id: listOrgParam(query),
29
+ p_search: query.search.trim() || void 0,
30
+ p_deep: query.deep,
31
+ p_sort: sort.sort,
32
+ p_dir: sort.direction,
33
+ p_filters: query.filters,
34
+ p_limit: sort.pageSize,
35
+ p_offset: (query.page - 1) * sort.pageSize
36
+ });
37
+ if (error) throw new Error(`Could not load the shapes: ${error.message ?? "the database returned an error with no message."}`);
38
+ const rows = data ?? [];
39
+ return { rows, total: rows.length > 0 ? Number(rows[0].total_count) : 0 };
40
+ }
41
+ const fetchShapePage = hostFn("fetchShapePage", readShapePage);
42
+ const fetchShapeByKind = hostFn("fetchShapeByKind", async (kind) => {
43
+ const page = await readShapePage(
44
+ { scope: { kind: "all" }, orgId: null, search: "", deep: false, archived: "active", filters: { kind: { kind: "text", value: kind } }, page: 1 },
45
+ { sort: "label", direction: "asc", favoritesFirst: false, pageSize: 50 }
46
+ );
47
+ return page.rows.find((row) => row.kind === kind) ?? null;
28
48
  });
29
49
  const useOpenCitationSource = hostFn("useOpenCitationSource", () => (source) => {
30
50
  if (source.url && typeof window !== "undefined") window.open(source.url, "_blank", "noopener,noreferrer");
@@ -8,6 +8,8 @@ import { AgentCatalogProvider } from "@ai-matrx/agents/catalog/react";
8
8
  import { createModelCatalog, createModelFavorites } from "@ai-matrx/agents/models";
9
9
  import { ModelCatalogProvider } from "@ai-matrx/agents/models/react";
10
10
  import { hasChatScopes, registerChatScopes } from "../context/sources/scopes.js";
11
+ import { configureBareFilesHost } from "./defaults/files-host.js";
12
+ import { registerBareReadableBlocks } from "./defaults/bare-block-dispatch.js";
11
13
  const SAVE_REASON = "save this";
12
14
  function BareHostChrome({ host, children }) {
13
15
  const [associations] = useState(
@@ -33,6 +35,8 @@ function BareHostChrome({ host, children }) {
33
35
  });
34
36
  }
35
37
  });
38
+ useState(() => configureBareFilesHost(host));
39
+ useState(() => registerBareReadableBlocks());
36
40
  const [catalog] = useState(() => host.catalog());
37
41
  const [models] = useState(() => ({
38
42
  catalog: createModelCatalog({ client: host.db }),
@@ -18,6 +18,7 @@
18
18
  import type { SupabaseClient } from "@supabase/supabase-js";
19
19
  import type { AnchorHTMLAttributes, ComponentType, ReactNode, Ref } from "react";
20
20
  import type { AgentCatalog } from "@ai-matrx/agents/catalog";
21
+ import type { AnyMandateKey } from "@ai-matrx/agents/mandates";
21
22
  import type { ChatDatabase } from "./db-types.js";
22
23
  import type { ChatWindowId } from "./windows.js";
23
24
  import type { CanvasJson } from "@ai-matrx/canvas";
@@ -38,7 +39,7 @@ export type ChatDb = SupabaseClient<ChatDatabase>;
38
39
  * (migrations/log_client_error_names_the_feature_too.sql). A value outside it
39
40
  * makes the database refuse the row, so the diagnostics default checks it first.
40
41
  */
41
- export declare const CHAT_SOURCE_APPS: readonly ["matrx-frontend", "matrx-extend", "matrx-local", "matrx-mobile"];
42
+ export declare const CHAT_SOURCE_APPS: readonly ["matrx-frontend", "matrx-extend", "matrx-local", "matrx-mobile", "workflow-studio"];
42
43
  export type ChatSourceApp = (typeof CHAT_SOURCE_APPS)[number];
43
44
  export type ChatAdminLevel = "developer" | "senior_admin" | "super_admin";
44
45
  export interface ChatIdentity {
@@ -730,7 +731,12 @@ export interface ChatAppIdentity {
730
731
  /** `chat.conversation.source_app` — a producer registered with the server. */
731
732
  sourceApp: string;
732
733
  /** `source_feature` for every conversation this app starts; absent keeps each screen's own. */
733
- sourceFeature?: string;
734
+ sourceFeature?: string; /**
735
+ * The mandate that answers this app's new-chat screen and agentless conversations (Matrx Local:
736
+ * `local.cloud_chat`; Matrx Extend: `extend.browser_chat`). Which agent runs is the database's
737
+ * answer for that key, never the host's. Absent: `chat.default_new_chat`, as on the web.
738
+ */
739
+ defaultChatMandateKey?: AnyMandateKey;
734
740
  }
735
741
  /**
736
742
  * Where each conversation's text may go (lanes/agents-chat-proposal.md W0(a)). The package enforces
@@ -2,7 +2,8 @@ const CHAT_SOURCE_APPS = [
2
2
  "matrx-frontend",
3
3
  "matrx-extend",
4
4
  "matrx-local",
5
- "matrx-mobile"
5
+ "matrx-mobile",
6
+ "workflow-studio"
6
7
  ];
7
8
  export {
8
9
  CHAT_SOURCE_APPS
@@ -0,0 +1,2 @@
1
+ /** The sentence holding this conversation's composer (host/conversation-hooks `held`), or null. */
2
+ export declare function useConversationHeld(conversationId: string | null | undefined): string | null;
@@ -0,0 +1,12 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import { readConversationHeld, subscribeConversationHeld } from "./conversation-hooks.js";
3
+ function useConversationHeld(conversationId) {
4
+ return useSyncExternalStore(
5
+ (listener) => subscribeConversationHeld(conversationId, listener),
6
+ () => readConversationHeld(conversationId),
7
+ () => null
8
+ );
9
+ }
10
+ export {
11
+ useConversationHeld
12
+ };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Per-conversation host hooks (HOST-OWNED-SOURCE-PLAN §2.6, slice P28f).
3
+ *
4
+ * A database-backed host (the workflow studio) runs its conversations through
5
+ * the package's own doors — the mandate route, the real list, the real
6
+ * composer — and only adds what is truly its own, per conversation:
7
+ *
8
+ * beforeSend the pre-send gate. Runs before admission; returns the
9
+ * turn's `variables` + `context`, or refuses with the
10
+ * host's sentence (the words go back in the box).
11
+ * held a sentence that holds the composer ("a change is
12
+ * waiting for you on the canvas"); null = not held.
13
+ * onEvent the stream-event tap: every event of every turn on this
14
+ * conversation, once, in order, after the package applied
15
+ * it. Must not throw (a throw is reported and swallowed).
16
+ * startConversation the host names the next conversation's id; the client
17
+ * never mints one.
18
+ * everyTurnThroughMandate
19
+ * every turn (not only turn 1) POSTs the mandate route,
20
+ * with the gate's variables + context — for a mandate
21
+ * whose provision checks guaranteed values on every turn.
22
+ *
23
+ * Kept OUT of Redux on purpose: these are functions and subscriptions owned by
24
+ * a mounted host surface, registered on mount and released on unmount.
25
+ */
26
+ import type { TypedStreamEvent } from "@ai-matrx/agents/generated/stream-events";
27
+ import { HostSendRefused } from "./source.js";
28
+ export interface HostTurnExtras {
29
+ variables?: Record<string, unknown>;
30
+ context?: Record<string, unknown>;
31
+ }
32
+ export interface HostBeforeSendInput {
33
+ conversationId: string;
34
+ text: string;
35
+ }
36
+ export interface HostHeld {
37
+ get(): string | null;
38
+ subscribe(listener: () => void): () => void;
39
+ }
40
+ export interface HostStreamTapContext {
41
+ conversationId: string;
42
+ requestId: string;
43
+ }
44
+ export interface ConversationHostHooks {
45
+ beforeSend?: (input: HostBeforeSendInput) => Promise<HostTurnExtras | HostSendRefused>;
46
+ held?: HostHeld;
47
+ onEvent?: (event: TypedStreamEvent, ctx: HostStreamTapContext) => void;
48
+ startConversation?: () => Promise<string>;
49
+ everyTurnThroughMandate?: boolean;
50
+ /** How a stored user turn reads on screen (display only; the record is unchanged). */
51
+ displayUserText?: (text: string) => string;
52
+ }
53
+ /** Register a conversation's hooks. Returns the release; a newer registration for the same id wins. */
54
+ export declare function registerConversationHooks(conversationId: string, hooks: ConversationHostHooks): () => void;
55
+ export declare function getConversationHooks(conversationId: string): ConversationHostHooks | undefined;
56
+ export type BeforeSendOutcome = {
57
+ ok: true;
58
+ } | {
59
+ ok: false;
60
+ sentence: string;
61
+ };
62
+ /**
63
+ * Run the conversation's pre-send gate. A conversation with no gate passes.
64
+ * A held conversation is refused with the held sentence. The gate's extras are
65
+ * kept for the request this turn assembles (`takeTurnExtras`).
66
+ */
67
+ export declare function runBeforeSend(conversationId: string, text: string): Promise<BeforeSendOutcome>;
68
+ /** The extras the gate returned for this turn; consumed once. */
69
+ export declare function takeTurnExtras(conversationId: string): HostTurnExtras | null;
70
+ /** Hand one stream event to the conversation's tap. Never throws. */
71
+ export declare function tapStreamEvent(conversationId: string, requestId: string, event: unknown): void;
72
+ /** Should this conversation's later turns also POST the mandate route? */
73
+ export declare function continuesThroughMandate(conversationId: string): boolean;
74
+ /** Ask the conversation's host for the next conversation id; null when it has no say. */
75
+ export declare function startHostConversation(conversationId: string): Promise<string | null>;
76
+ /** Subscribe to changes of this conversation's held sentence (registry + the host's own source). */
77
+ export declare function subscribeConversationHeld(conversationId: string | null | undefined, listener: () => void): () => void;
78
+ /** The held sentence right now, or null. */
79
+ export declare function readConversationHeld(conversationId: string | null | undefined): string | null;
@@ -0,0 +1,91 @@
1
+ import { isHostSendRefused } from "./source.js";
2
+ const hooksByConversation = /* @__PURE__ */ new Map();
3
+ const extrasByConversation = /* @__PURE__ */ new Map();
4
+ const registryListeners = /* @__PURE__ */ new Set();
5
+ function notifyRegistry() {
6
+ for (const listener of registryListeners) listener();
7
+ }
8
+ function registerConversationHooks(conversationId, hooks) {
9
+ hooksByConversation.set(conversationId, hooks);
10
+ notifyRegistry();
11
+ return () => {
12
+ if (hooksByConversation.get(conversationId) === hooks) {
13
+ hooksByConversation.delete(conversationId);
14
+ extrasByConversation.delete(conversationId);
15
+ notifyRegistry();
16
+ }
17
+ };
18
+ }
19
+ function getConversationHooks(conversationId) {
20
+ return hooksByConversation.get(conversationId);
21
+ }
22
+ async function runBeforeSend(conversationId, text) {
23
+ const hooks = hooksByConversation.get(conversationId);
24
+ if (!hooks) return { ok: true };
25
+ const heldSentence = hooks.held?.get() ?? null;
26
+ if (heldSentence) return { ok: false, sentence: heldSentence };
27
+ if (!hooks.beforeSend) return { ok: true };
28
+ try {
29
+ const result = await hooks.beforeSend({ conversationId, text });
30
+ if (isHostSendRefused(result)) return { ok: false, sentence: result.message };
31
+ extrasByConversation.set(conversationId, result ?? {});
32
+ return { ok: true };
33
+ } catch (error) {
34
+ if (isHostSendRefused(error)) return { ok: false, sentence: error.message };
35
+ const message = error instanceof Error ? error.message : String(error);
36
+ return { ok: false, sentence: message || "This message could not be prepared. Nothing was sent." };
37
+ }
38
+ }
39
+ function takeTurnExtras(conversationId) {
40
+ const extras = extrasByConversation.get(conversationId) ?? null;
41
+ extrasByConversation.delete(conversationId);
42
+ return extras;
43
+ }
44
+ function tapStreamEvent(conversationId, requestId, event) {
45
+ const tap = hooksByConversation.get(conversationId)?.onEvent;
46
+ if (!tap) return;
47
+ try {
48
+ tap(event, { conversationId, requestId });
49
+ } catch (error) {
50
+ console.error(`[chat] the host's stream tap threw on conversation "${conversationId}"`, error);
51
+ }
52
+ }
53
+ function continuesThroughMandate(conversationId) {
54
+ return hooksByConversation.get(conversationId)?.everyTurnThroughMandate === true;
55
+ }
56
+ async function startHostConversation(conversationId) {
57
+ const start = hooksByConversation.get(conversationId)?.startConversation;
58
+ return start ? start() : null;
59
+ }
60
+ function subscribeConversationHeld(conversationId, listener) {
61
+ let release = subscribeOnce(conversationId, onChange);
62
+ function onChange() {
63
+ release();
64
+ release = subscribeOnce(conversationId, onChange);
65
+ listener();
66
+ }
67
+ return () => release();
68
+ }
69
+ function subscribeOnce(conversationId, listener) {
70
+ registryListeners.add(listener);
71
+ const held = conversationId ? hooksByConversation.get(conversationId)?.held : void 0;
72
+ const releaseHeld = held?.subscribe(listener);
73
+ return () => {
74
+ registryListeners.delete(listener);
75
+ releaseHeld?.();
76
+ };
77
+ }
78
+ function readConversationHeld(conversationId) {
79
+ return conversationId ? hooksByConversation.get(conversationId)?.held?.get() ?? null : null;
80
+ }
81
+ export {
82
+ continuesThroughMandate,
83
+ getConversationHooks,
84
+ readConversationHeld,
85
+ registerConversationHooks,
86
+ runBeforeSend,
87
+ startHostConversation,
88
+ subscribeConversationHeld,
89
+ takeTurnExtras,
90
+ tapStreamEvent
91
+ };
@@ -0,0 +1,6 @@
1
+ /** The block types the bare host gives a readable fallback (the app's cards replace them). */
2
+ export declare const BARE_READABLE_BLOCK_TYPES: readonly ["decision", "artifact", "matrx", "input_remarks", "decision_questions", "decision_answers", "speech_script", "directive_receipt"];
3
+ /** Markup → readable markdown: options become a list, every other tag is dropped, its text kept. */
4
+ export declare function readableMarkup(content: string): string;
5
+ /** Register the readable fallback for each type no host registered (idempotent). */
6
+ export declare function registerBareReadableBlocks(): void;
@@ -0,0 +1,60 @@
1
+ "use client";
2
+ import {
3
+ registerBlockDispatch,
4
+ resolveBlockDispatch
5
+ } from "@ai-matrx/rich-content/display/chat-markdown/block-registry/block-dispatch";
6
+ const BARE_READABLE_BLOCK_TYPES = [
7
+ "decision",
8
+ "artifact",
9
+ "matrx",
10
+ "input_remarks",
11
+ "decision_questions",
12
+ "decision_answers",
13
+ "speech_script",
14
+ "directive_receipt"
15
+ ];
16
+ function attr(tag, name) {
17
+ return tag.match(new RegExp(`${name}\\s*=\\s*"([^"]*)"`, "i"))?.[1] ?? null;
18
+ }
19
+ function readableMarkup(content) {
20
+ return content.replace(/<option\b([^>]*)>([\s\S]*?)<\/option>/gi, (_m, attrs, text) => {
21
+ const label = attr(attrs, "label");
22
+ const body = text.trim();
23
+ return `
24
+ - ${label && body && label !== body ? `**${label}** ${body}` : label || body}`;
25
+ }).replace(/<option\b([^>]*)\/>/gi, (_m, attrs) => `
26
+ - ${attr(attrs, "label") ?? ""}`).replace(/<\/?[a-z_][\w:.-]*(?:\s[^>]*)?\/?>/gi, "").replace(/\n{3,}/g, "\n\n").trim();
27
+ }
28
+ function asJsonFence(value) {
29
+ return "```json\n" + JSON.stringify(value, null, 2) + "\n```";
30
+ }
31
+ function readable(ctx) {
32
+ const { block } = ctx;
33
+ const content = typeof block.content === "string" ? block.content : "";
34
+ const meta = block.metadata ?? {};
35
+ if (block.type === "matrx") {
36
+ try {
37
+ return ctx.renderBasicMarkdown(asJsonFence(JSON.parse(content)));
38
+ } catch {
39
+ return ctx.renderBasicMarkdown("```\n" + content + "\n```");
40
+ }
41
+ }
42
+ const title = typeof meta.artifactTitle === "string" ? `**${meta.artifactTitle}**
43
+
44
+ ` : "";
45
+ const text = readableMarkup(content);
46
+ if (text) return ctx.renderBasicMarkdown(title + text);
47
+ if (block.serverData !== void 0 && block.serverData !== null) return ctx.renderBasicMarkdown(title + asJsonFence(block.serverData));
48
+ return ctx.renderBasicMarkdown(title);
49
+ }
50
+ const READABLE = (ctx) => readable(ctx);
51
+ function registerBareReadableBlocks() {
52
+ const missing = {};
53
+ for (const type of BARE_READABLE_BLOCK_TYPES) if (!resolveBlockDispatch(type)) missing[type] = READABLE;
54
+ if (Object.keys(missing).length) registerBlockDispatch(missing);
55
+ }
56
+ export {
57
+ BARE_READABLE_BLOCK_TYPES,
58
+ readableMarkup,
59
+ registerBareReadableBlocks
60
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The files engine's host for a bare chat host (P29a finding 3): a host that gave chat only a db
3
+ * still uploads attachments. `BareHostChrome` configures `@ai-matrx/media`'s files engine ONCE with
4
+ * the chat host's db, its server port (bearer + organization headers), the chat store (which mounts
5
+ * the `cloudFiles` slice) and its organization/identity/notify ports. A host that configured the
6
+ * engine itself (matrx-frontend: `features/files/files-host.ts`) is never overridden.
7
+ */
8
+ import { type FilesServerClient, type FilesShareLinks } from "@ai-matrx/media/files/engine/host/configure";
9
+ import type { ResolvedChatHost } from "../contract.js";
10
+ /** The standalone files service (`matrx-files`): the routes it owns go there, everything else to the chat server. */
11
+ export declare const DEFAULT_FILES_SERVICE_URL = "https://files.matrxserver.com";
12
+ export declare function isFilesServiceRoute(path: string, method?: string): boolean;
13
+ /** A failed call, carrying the status and the server's own words. */
14
+ export declare class FilesRequestError extends Error {
15
+ readonly status: number | null;
16
+ readonly code: string;
17
+ readonly userMessage: string;
18
+ constructor(status: number | null, message: string, code?: string);
19
+ }
20
+ export declare function createDefaultFilesServer(host: () => ResolvedChatHost): FilesServerClient;
21
+ /** Where a share link opens (the platform's own `/s/<token>` page). */
22
+ export declare const DEFAULT_SHARE_LINK_ORIGIN = "https://aimatrx.com";
23
+ /** Share links through the platform's own RPCs over the host's db (an attachment upload mints one). */
24
+ export declare function createDefaultShareLinks(host: () => ResolvedChatHost): FilesShareLinks;
25
+ /** Wire the files engine to a bare chat host, unless the host (or a test) already did. */
26
+ export declare function configureBareFilesHost(host: ResolvedChatHost): void;