@ai-matrx/chat 0.4.16 → 0.4.19

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 (53) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/dist/agent-copy/AlchemySurfaceBridge.js +1 -1
  3. package/dist/agents/components/agent-widgets/chat-assistant/AssistantCardStack.js +1 -1
  4. package/dist/agents/components/ambient-assistant/ScrollAssistantLauncher.js +1 -1
  5. package/dist/agents/components/conversation-actions/rename/ConversationRenameDialogHost.js +1 -1
  6. package/dist/agents/components/messages-display/assistant/AgentEmptyMessageDisplay.js +1 -1
  7. package/dist/agents/components/messages-display/assistant/AssistantMessageFooter.js +3 -1
  8. package/dist/agents/components/messages-display/user/AgentUserMessage.js +95 -85
  9. package/dist/agents/components/messages-display/user/useCollapsibleMessageText.d.ts +1 -1
  10. package/dist/agents/components/messages-display/user/useCollapsibleMessageText.js +5 -4
  11. package/dist/agents/components/shared/AgentConversationColumn.d.ts +7 -1
  12. package/dist/agents/components/shared/AgentConversationColumn.js +14 -7
  13. package/dist/agents/components/shared/transcript-layout.d.ts +31 -0
  14. package/dist/agents/components/shared/transcript-layout.js +38 -0
  15. package/dist/agents/redux/execution-system/message-crud/answer-text-splice.js +1 -1
  16. package/dist/agents/redux/execution-system/messages/messages.selectors.js +1 -1
  17. package/dist/canvas/workspace/ChatPanelTitleMenu.js +8 -2
  18. package/dist/canvas/workspace/useCanvasWorkspaceConversation.d.ts +10 -0
  19. package/dist/canvas/workspace/useCanvasWorkspaceConversation.js +50 -3
  20. package/dist/cx-chat/components/ChatMobileAgentName.js +1 -1
  21. package/dist/cx-chat/components/ChatSidebarClient.js +1 -1
  22. package/dist/cx-chat/components/ChatWelcomeClient.js +1 -1
  23. package/dist/cx-chat/components/core/ChatConversationClient.js +1 -1
  24. package/dist/cx-chat/components/user-input/ConversationInput.js +1 -1
  25. package/dist/cx-conversation/ConversationInput.js +1 -1
  26. package/dist/cx-conversation/MessageList.js +1 -1
  27. package/dist/host/ui-slots.js +17 -2
  28. package/dist/surfaces/components/chrome/SurfaceAgentsHeaderButton.d.ts +1 -1
  29. package/dist/surfaces/components/chrome/SurfaceAgentsHeaderButton.js +1 -1
  30. package/dist/tool-call-visualization/db-renderer/DbToolRenderer.js +1 -1
  31. package/dist/{next → ui}/lazy/AgentAssistantMessage.js +3 -4
  32. package/dist/{next → ui}/lazy/AgentPickerSheet.js +3 -4
  33. package/dist/{next → ui}/lazy/ChatVariableInputs.js +5 -7
  34. package/dist/{next → ui}/lazy/ConversationRenameDialogHostImpl.d.ts +1 -1
  35. package/dist/{next → ui}/lazy/ConversationRenameDialogHostImpl.js +3 -3
  36. package/dist/{next → ui}/lazy/CxAssistantMessage.js +3 -4
  37. package/dist/{next → ui}/lazy/DbToolRendererImpl.js +3 -3
  38. package/dist/{next → ui}/lazy/IconResolver.js +3 -4
  39. package/dist/{next → ui}/lazy/PublicChatVariableInputs.js +5 -7
  40. package/dist/{next → ui}/lazy/ScrollAssistantLaunchers.d.ts +1 -1
  41. package/dist/{next → ui}/lazy/ScrollAssistantLaunchers.js +3 -5
  42. package/dist/{next → ui}/lazy/SurfaceAgentsPanelImpl.js +2 -3
  43. package/dist/utils/client-lazy.d.ts +16 -0
  44. package/dist/utils/client-lazy.js +27 -0
  45. package/package.json +9 -9
  46. /package/dist/{next → ui}/lazy/AgentAssistantMessage.d.ts +0 -0
  47. /package/dist/{next → ui}/lazy/AgentPickerSheet.d.ts +0 -0
  48. /package/dist/{next → ui}/lazy/ChatVariableInputs.d.ts +0 -0
  49. /package/dist/{next → ui}/lazy/CxAssistantMessage.d.ts +0 -0
  50. /package/dist/{next → ui}/lazy/DbToolRendererImpl.d.ts +0 -0
  51. /package/dist/{next → ui}/lazy/IconResolver.d.ts +0 -0
  52. /package/dist/{next → ui}/lazy/PublicChatVariableInputs.d.ts +0 -0
  53. /package/dist/{next → ui}/lazy/SurfaceAgentsPanelImpl.d.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.4.19 — Release verification repair
4
+
5
+ - Restores the resume-stall coverage in the package's Jest module suite: the
6
+ test now mocks the current host-neutral lazy picker door, instead of the
7
+ retired `next/dynamic` implementation. This keeps the real resume banner
8
+ assertion isolated from the picker's store dependency.
9
+
10
+ ## 0.4.18 — Side Chat: an unsent chat's menu, and a taken id heals itself
11
+
12
+ - The chat panel's title menu (`ChatPanelTitleMenu`) shows Rename and Open in full chat only once
13
+ the conversation exists on the server (not cache-only, or it has a conversation-list row). An
14
+ unsent New chat shows New chat and the conversation list only.
15
+ - A reserved new-chat id is released (and the page's memory takes over) the moment the first send
16
+ STARTS, not when the server confirms — a kept id could be sent again from a second tab or after a
17
+ reload, and the server answered 409 `conversation_already_exists` forever.
18
+ - Self-heal: when a send is refused with 409 `conversation_already_exists`, the panel starts the
19
+ same chat (same agent / job) again under a FRESH id and puts what the person sent back into its
20
+ composer. `isConversationIdTaken` names the refusal.
21
+
22
+ Consumer action: none.
23
+
24
+ ## 0.4.17 — a lighter first chat screen
25
+
26
+ - The message selectors and the answer-text splice strip thinking with THE rule
27
+ (`@ai-matrx/content-ir/source` `stripThinking`) instead of importing `@ai-matrx/print/markdown`
28
+ (KaTeX + an HTML parser on the first screen). The chat room marks its surface through
29
+ `@ai-matrx/alchemy/react/surface` (not the whole workspace). With `@ai-matrx/icons` 0.3.4 the
30
+ bare Vite canary's first chat screen drops from 2,429 kB to the number in
31
+ `side-panel-bundle-budget.json` (gzip); nothing a person can do changed.
32
+
3
33
  ## 0.4.16 — the Side Chat's URL contract
4
34
 
5
35
  - ONE param family on every home opens, closes and aims the Side Chat (the shell chat dock):
@@ -1579,4 +1609,3 @@ First release. Phases 0 and 1 of
1579
1609
  declared in the export map, so importing one fails honestly instead of resolving to a
1580
1610
  half-built screen. `./native` is reserved for the first real React Native consumer (C25).
1581
1611
 
1582
-
@@ -4,7 +4,7 @@ import { useEffect, useId, useRef } from "react";
4
4
  import {
5
5
  createSurfaceTransferHandle
6
6
  } from "@ai-matrx/alchemy/operate";
7
- import { ContentTransferSurfaceProvider } from "@ai-matrx/alchemy/react/workspace";
7
+ import { ContentTransferSurfaceProvider } from "@ai-matrx/alchemy/react/surface";
8
8
  import { getSurfaceManifest } from "../host/ui-slots.js";
9
9
  function useAlchemySurfaceHandle(surfaceName, getScope) {
10
10
  const instanceId = useId();
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useMemo } from "react";
4
- import { AgentAssistantMessage } from "../../../../next/lazy/AgentAssistantMessage.js";
4
+ import { AgentAssistantMessage } from "../../../../ui/lazy/AgentAssistantMessage.js";
5
5
  import { useAppSelector, useAppDispatch } from "../../../../store/hooks.js";
6
6
  import { selectConversationMessages } from "../../../redux/execution-system/messages/messages.selectors.js";
7
7
  import {
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
- import { ScrollAssistantLauncherImpl, ScrollVoiceAssistantLauncherImpl } from "../../../next/lazy/ScrollAssistantLaunchers.js";
3
+ import { ScrollAssistantLauncherImpl, ScrollVoiceAssistantLauncherImpl } from "../../../ui/lazy/ScrollAssistantLaunchers.js";
4
4
  import { usePathname } from "../../../host/navigation.js";
5
5
  import { useEffect, useState } from "react";
6
6
  import { useIsMobile } from "@ai-matrx/kit/media-query";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
- import { ConversationRenameDialogHostImpl } from "../../../../next/lazy/ConversationRenameDialogHostImpl.js";
3
+ import { ConversationRenameDialogHostImpl } from "../../../../ui/lazy/ConversationRenameDialogHostImpl.js";
4
4
  function ConversationRenameDialogHost() {
5
5
  return /* @__PURE__ */ jsx(ConversationRenameDialogHostImpl, {});
6
6
  }
@@ -3,7 +3,7 @@ import { useAppSelector } from "../../../../store/hooks.js";
3
3
  import { selectInstanceAgentName } from "../../../redux/execution-system/instance-ui-state/instance-ui-state.selectors.js";
4
4
  import { selectInstanceAgentDescription } from "../../../redux/execution-system/instance-ui-state/instance-ui-state.selectors.js";
5
5
  import { Webhook } from "lucide-react";
6
- import { IconResolver } from "../../../../next/lazy/IconResolver.js";
6
+ import { IconResolver } from "../../../../ui/lazy/IconResolver.js";
7
7
  import { selectVisibleInputDefinitions } from "../../../redux/execution-system/instance-variable-values/bound-variable.selectors.js";
8
8
  import { selectIsVariableFormShown } from "../../../redux/execution-system/selectors/aggregate.selectors.js";
9
9
  import { emptyStateInstruction } from "./empty-state-instruction.js";
@@ -31,6 +31,7 @@ import {
31
31
  import { buildConversationMessageTitle } from "../../../utils/conversation-message-title.js";
32
32
  import { resolveAssistantEditTarget } from "../message-options/resolveAssistantEditTarget.js";
33
33
  import { MessageTimestamp } from "../MessageTimestamp.js";
34
+ import { useTranscriptLayout } from "../../shared/transcript-layout.js";
34
35
  function useAssistantMessageActions({
35
36
  messageId,
36
37
  conversationId,
@@ -162,7 +163,8 @@ function AssistantMessageFooter(props) {
162
163
  showOptions,
163
164
  isLatestAssistant
164
165
  } = useAssistantMessageActions(props);
165
- const isHoverOnly = density === "compact" && !isLatestAssistant;
166
+ const answerActions = useTranscriptLayout().answerActions;
167
+ const isHoverOnly = answerActions === "hover" || density === "compact" && !isLatestAssistant;
166
168
  const rowRef = useRef(null);
167
169
  useEffect(() => {
168
170
  const row = rowRef.current;
@@ -30,6 +30,8 @@ import {
30
30
  } from "../../../redux/execution-system/messages/message-context-receipt.js";
31
31
  import { useMachineFramesVisible } from "../../shared/transcript-audience.js";
32
32
  import { useCollapsibleMessageText } from "./useCollapsibleMessageText.js";
33
+ import { useTranscriptLayout } from "../../shared/transcript-layout.js";
34
+ const COMPACT_COLLAPSE_THRESHOLD_PX = 192;
33
35
  import {
34
36
  selectHostSubmittedFirstTurnValues,
35
37
  selectOwnSubmittedFirstTurnValues
@@ -197,7 +199,8 @@ function AgentUserMessage({
197
199
  bodyBlocks,
198
200
  trimmedText
199
201
  ]);
200
- const { isCollapsed, setIsCollapsed, shouldBeCollapsible, measureRef } = useCollapsibleMessageText(collapseSignature);
202
+ const compactUserMessage = useTranscriptLayout().userMessage === "compact";
203
+ const { isCollapsed, setIsCollapsed, shouldBeCollapsible, measureRef } = useCollapsibleMessageText(collapseSignature, compactUserMessage ? COMPACT_COLLAPSE_THRESHOLD_PX : void 0);
201
204
  const authoredByHost = metadata?.authored_by === "host";
202
205
  const isSuperAdmin = useAppSelector(selectIsSuperAdmin);
203
206
  useEffect(() => {
@@ -281,95 +284,102 @@ function AgentUserMessage({
281
284
  }, title: "Collapse", "aria-label": "Collapse" })
282
285
  }
283
286
  ),
284
- /* @__PURE__ */ jsx("div", { className: "bg-muted border border-border rounded-lg px-2 py-2", children: /* @__PURE__ */ jsxs("div", { className: "relative min-w-0", children: [
285
- /* @__PURE__ */ jsxs(
286
- "div",
287
- {
288
- ref: measureRef,
289
- className: cn(
290
- "space-y-1.5 overflow-hidden transition-all duration-300",
291
- // Clamp leaves room below the fade: the first lines (what was
292
- // sent, the context strip) read at full contrast, and the
293
- // expand chevron never sits on top of text.
294
- shouldBeCollapsible && isCollapsed && !editingText && "max-h-24 pb-7"
295
- ),
296
- children: [
297
- machineFramesVisible && isFirstTurnMessage && /* @__PURE__ */ jsx(FirstTurnVariables, { conversationId }),
298
- showLaunchInputs && /* @__PURE__ */ jsx(
299
- FirstTurnLaunchInputs,
300
- {
301
- conversationId,
302
- onOpenChange: (open) => {
303
- if (open) setIsCollapsed(false);
304
- }
305
- }
306
- ),
307
- !database ? null : machineFramesVisible && contextReceipt && (contextReceipt.rows?.length ?? 0) > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-1.5", children: /* @__PURE__ */ jsx(
308
- MessageContextReceipt,
309
- {
310
- conversationId,
311
- messageId,
312
- receipt: contextReceipt,
313
- mismatches: contextMismatches
314
- }
315
- ) }) : machineFramesVisible && contextSnapshot && contextSnapshot.length > 0 && /* @__PURE__ */ jsx(
316
- ContextPolicyChipStrip,
317
- {
318
- conversationId,
319
- agentId,
320
- entries: contextSnapshot
321
- }
322
- ),
323
- /* @__PURE__ */ jsx(
324
- EditInPlace,
325
- {
326
- value: content,
327
- canEdit: canEditText,
328
- editing: editingText,
329
- onEditingChange: setEditingText,
330
- write: (text) => routeUserEditAction(dispatch, { actionId: "save", conversationId, messageId, newContent: text, surfaceKey }),
331
- actions: USER_EDIT_ACTIONS.filter((a) => a.id !== "save").map((a) => ({
332
- id: a.id,
333
- label: a.label,
334
- run: (text) => routeUserEditAction(dispatch, { actionId: a.id, conversationId, messageId, newContent: text, surfaceKey })
335
- })),
336
- discardDescription: "Your message stays exactly as it was sent; what you typed here is dropped.",
337
- editor: {
338
- imagePolicy: "other",
339
- surfaceName: "matrx-user/chat",
340
- sourceFeature: "chat",
341
- contentSource: { type: "chat-message", conversationId, messageId }
342
- },
343
- children: /* @__PURE__ */ jsx(
344
- AgentUserMessageContent,
287
+ /* @__PURE__ */ jsx(
288
+ "div",
289
+ {
290
+ className: compactUserMessage ? "bg-muted rounded-md px-3 py-1.5" : "bg-muted border border-border rounded-lg px-2 py-2",
291
+ "data-user-message-style": compactUserMessage ? "compact" : "card",
292
+ children: /* @__PURE__ */ jsxs("div", { className: "relative min-w-0", children: [
293
+ /* @__PURE__ */ jsxs(
294
+ "div",
295
+ {
296
+ ref: measureRef,
297
+ className: cn(
298
+ "space-y-1.5 overflow-hidden transition-all duration-300",
299
+ // Clamp leaves room below the fade: the first lines (what was
300
+ // sent, the context strip) read at full contrast, and the
301
+ // expand chevron never sits on top of text.
302
+ shouldBeCollapsible && isCollapsed && !editingText && "max-h-24 pb-7"
303
+ ),
304
+ children: [
305
+ machineFramesVisible && isFirstTurnMessage && /* @__PURE__ */ jsx(FirstTurnVariables, { conversationId }),
306
+ showLaunchInputs && /* @__PURE__ */ jsx(
307
+ FirstTurnLaunchInputs,
308
+ {
309
+ conversationId,
310
+ onOpenChange: (open) => {
311
+ if (open) setIsCollapsed(false);
312
+ }
313
+ }
314
+ ),
315
+ !database ? null : machineFramesVisible && contextReceipt && (contextReceipt.rows?.length ?? 0) > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-1.5", children: /* @__PURE__ */ jsx(
316
+ MessageContextReceipt,
317
+ {
318
+ conversationId,
319
+ messageId,
320
+ receipt: contextReceipt,
321
+ mismatches: contextMismatches
322
+ }
323
+ ) }) : machineFramesVisible && contextSnapshot && contextSnapshot.length > 0 && /* @__PURE__ */ jsx(
324
+ ContextPolicyChipStrip,
345
325
  {
346
326
  conversationId,
347
- text: trimmedText,
348
- attachmentParts,
349
- bodyBlocks
327
+ agentId,
328
+ entries: contextSnapshot
329
+ }
330
+ ),
331
+ /* @__PURE__ */ jsx(
332
+ EditInPlace,
333
+ {
334
+ value: content,
335
+ canEdit: canEditText,
336
+ editing: editingText,
337
+ onEditingChange: setEditingText,
338
+ write: (text) => routeUserEditAction(dispatch, { actionId: "save", conversationId, messageId, newContent: text, surfaceKey }),
339
+ actions: USER_EDIT_ACTIONS.filter((a) => a.id !== "save").map((a) => ({
340
+ id: a.id,
341
+ label: a.label,
342
+ run: (text) => routeUserEditAction(dispatch, { actionId: a.id, conversationId, messageId, newContent: text, surfaceKey })
343
+ })),
344
+ discardDescription: "Your message stays exactly as it was sent; what you typed here is dropped.",
345
+ editor: {
346
+ imagePolicy: "other",
347
+ surfaceName: "matrx-user/chat",
348
+ sourceFeature: "chat",
349
+ contentSource: { type: "chat-message", conversationId, messageId }
350
+ },
351
+ children: /* @__PURE__ */ jsx(
352
+ AgentUserMessageContent,
353
+ {
354
+ conversationId,
355
+ text: trimmedText,
356
+ attachmentParts,
357
+ bodyBlocks
358
+ }
359
+ )
350
360
  }
351
361
  )
362
+ ]
363
+ }
364
+ ),
365
+ shouldBeCollapsible && isCollapsed && /* @__PURE__ */ jsxs(Fragment, { children: [
366
+ /* @__PURE__ */ jsx("div", { className: "absolute bottom-0 left-0 right-0 h-10 bg-gradient-to-t from-muted via-muted/80 to-transparent pointer-events-none" }),
367
+ /* @__PURE__ */ jsx("div", { className: "absolute bottom-0 left-0 right-0 flex justify-center pb-1", children: /* @__PURE__ */ jsx(
368
+ Button,
369
+ {
370
+ variant: "quiet",
371
+ onClick: (e) => {
372
+ e.stopPropagation();
373
+ setIsCollapsed(false);
374
+ },
375
+ title: "Expand message",
376
+ children: /* @__PURE__ */ jsx(ChevronDown, { className: "w-4 h-4" })
352
377
  }
353
- )
354
- ]
355
- }
356
- ),
357
- shouldBeCollapsible && isCollapsed && /* @__PURE__ */ jsxs(Fragment, { children: [
358
- /* @__PURE__ */ jsx("div", { className: "absolute bottom-0 left-0 right-0 h-10 bg-gradient-to-t from-muted via-muted/80 to-transparent pointer-events-none" }),
359
- /* @__PURE__ */ jsx("div", { className: "absolute bottom-0 left-0 right-0 flex justify-center pb-1", children: /* @__PURE__ */ jsx(
360
- Button,
361
- {
362
- variant: "quiet",
363
- onClick: (e) => {
364
- e.stopPropagation();
365
- setIsCollapsed(false);
366
- },
367
- title: "Expand message",
368
- children: /* @__PURE__ */ jsx(ChevronDown, { className: "w-4 h-4" })
369
- }
370
- ) })
371
- ] })
372
- ] }) }),
378
+ ) })
379
+ ] })
380
+ ] })
381
+ }
382
+ ),
373
383
  /* @__PURE__ */ jsx(
374
384
  "div",
375
385
  {
@@ -23,7 +23,7 @@
23
23
  * physically clicks expand. A late remeasure re-collapses unless the user has
24
24
  * explicitly toggled.
25
25
  */
26
- export declare function useCollapsibleMessageText(contentKey: string): {
26
+ export declare function useCollapsibleMessageText(contentKey: string, thresholdPx?: number): {
27
27
  isCollapsed: boolean;
28
28
  setIsCollapsed: (value: boolean | ((prev: boolean) => boolean)) => void;
29
29
  shouldBeCollapsible: boolean;
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useLayoutEffect, useRef, useState } from "react";
2
2
  const COLLAPSE_THRESHOLD_PX = 48;
3
- function useCollapsibleMessageText(contentKey) {
3
+ function useCollapsibleMessageText(contentKey, thresholdPx = COLLAPSE_THRESHOLD_PX) {
4
4
  const [isCollapsed, setIsCollapsedState] = useState(true);
5
5
  const [shouldBeCollapsible, setShouldBeCollapsible] = useState(false);
6
6
  const measureRef = useRef(null);
@@ -10,8 +10,9 @@ function useCollapsibleMessageText(contentKey) {
10
10
  const node = measureRef.current;
11
11
  if (!node) return void 0;
12
12
  const measure = () => {
13
- const contentHeight = node.scrollHeight;
14
- const isLong = contentHeight > COLLAPSE_THRESHOLD_PX;
13
+ const padding = typeof getComputedStyle === "function" ? parseFloat(getComputedStyle(node).paddingBottom) || 0 : 0;
14
+ const contentHeight = node.scrollHeight - padding;
15
+ const isLong = contentHeight > thresholdPx;
15
16
  const contentChanged = previousContentRef.current !== contentKey;
16
17
  setShouldBeCollapsible(isLong);
17
18
  if (contentChanged) {
@@ -31,7 +32,7 @@ function useCollapsibleMessageText(contentKey) {
31
32
  const observer = new ResizeObserver(measure);
32
33
  observer.observe(node);
33
34
  return () => observer.disconnect();
34
- }, [contentKey]);
35
+ }, [contentKey, thresholdPx]);
35
36
  const setIsCollapsed = useCallback(
36
37
  (value) => {
37
38
  userToggledRef.current = true;
@@ -2,6 +2,7 @@ import type { ComposerPresentation } from "../inputs/smart-input/composer/compos
2
2
  import type { AttachedContextRailItem } from "../inputs/smart-input/ConversationContextRail.js";
3
3
  import type { VariablesPanelStyle } from "../inputs/variable-input-variations/variable-input-options.js";
4
4
  import { type TranscriptAudience } from "./transcript-audience.js";
5
+ import { type TranscriptLayout } from "./transcript-layout.js";
5
6
  interface SmartInputForwardProps {
6
7
  /** See `SmartAgentInput`'s `draftAlias`: the per-record unsent-draft key. */
7
8
  draftAlias?: string;
@@ -114,6 +115,11 @@ interface AgentConversationColumnBaseProps {
114
115
  * ./transcript-audience.tsx.
115
116
  */
116
117
  audience?: TranscriptAudience;
118
+ /**
119
+ * The host's layout knobs (width, alignment, the person's message style). Omitted = today's
120
+ * web chat exactly. See ./transcript-layout.tsx.
121
+ */
122
+ layout?: TranscriptLayout;
117
123
  }
118
- export declare function AgentConversationColumn({ conversationId, displayConversationId, surfaceKey, constrainWidth, edgeToEdgeScroll, smartInputProps, landingContent, hideInput, hideCreatorPanel, afterMessages, aboveInput, deferColdMarkdown, audience, }: AgentConversationColumnProps): import("react").JSX.Element;
124
+ export declare function AgentConversationColumn({ conversationId, displayConversationId, surfaceKey, constrainWidth, edgeToEdgeScroll, smartInputProps, landingContent, hideInput, hideCreatorPanel, afterMessages, aboveInput, deferColdMarkdown, audience, layout, }: AgentConversationColumnProps): import("react").JSX.Element;
119
125
  export {};
@@ -32,9 +32,13 @@ import {
32
32
  } from "./transcript-audience.js";
33
33
  import { cn } from "@ai-matrx/design-system";
34
34
  import {
35
- ASSISTANT_MESSAGE_COLUMN_CLASS,
36
35
  ASSISTANT_MESSAGE_COLUMN_INSET_CLASS
37
36
  } from "./assistant-message-layout.js";
37
+ import {
38
+ TranscriptLayoutProvider,
39
+ resolveTranscriptLayout,
40
+ transcriptColumnClass
41
+ } from "./transcript-layout.js";
38
42
  import {
39
43
  isWarRoomThreadAgentSurface,
40
44
  traceWarRoomRenderPath
@@ -58,8 +62,11 @@ function AgentConversationColumn({
58
62
  afterMessages,
59
63
  aboveInput,
60
64
  deferColdMarkdown = false,
61
- audience = "builder"
65
+ audience = "builder",
66
+ layout
62
67
  }) {
68
+ const resolvedLayout = resolveTranscriptLayout(layout);
69
+ const columnClass = transcriptColumnClass(resolvedLayout);
63
70
  const dispatch = useAppDispatch();
64
71
  const displayId = displayConversationId ?? conversationId;
65
72
  const scrollRef = useRef(null);
@@ -202,8 +209,8 @@ function AgentConversationColumn({
202
209
  });
203
210
  }, []);
204
211
  const edgeScroll = constrainWidth && edgeToEdgeScroll;
205
- const centerWrap = edgeScroll ? cn(ASSISTANT_MESSAGE_COLUMN_CLASS, ASSISTANT_MESSAGE_COLUMN_INSET_CLASS) : "contents";
206
- return /* @__PURE__ */ jsx(ConversationAbilitiesProvider, { conversationId: displayId, children: /* @__PURE__ */ jsx(TranscriptAudienceProvider, { audience, children: /* @__PURE__ */ jsxs(
212
+ const centerWrap = edgeScroll ? cn(columnClass, ASSISTANT_MESSAGE_COLUMN_INSET_CLASS) : "contents";
213
+ return /* @__PURE__ */ jsx(ConversationAbilitiesProvider, { conversationId: displayId, children: /* @__PURE__ */ jsx(TranscriptAudienceProvider, { audience, children: /* @__PURE__ */ jsx(TranscriptLayoutProvider, { layout: resolvedLayout, children: /* @__PURE__ */ jsxs(
207
214
  "div",
208
215
  {
209
216
  className: cn(
@@ -211,7 +218,7 @@ function AgentConversationColumn({
211
218
  // Legacy: pad the whole column. Edge mode: padding lives on the
212
219
  // centered inner wrappers instead so the scroll area runs to the edge.
213
220
  !edgeScroll && "px-2",
214
- constrainWidth && !edgeScroll && ASSISTANT_MESSAGE_COLUMN_CLASS,
221
+ constrainWidth && !edgeScroll && columnClass,
215
222
  constrainWidth && !edgeScroll && "pb-2",
216
223
  edgeScroll && "w-full pb-2"
217
224
  ),
@@ -319,7 +326,7 @@ function AgentConversationColumn({
319
326
  animate: { opacity: 1, y: 0 },
320
327
  transition: { duration: 0.28, ease: [0.32, 0.72, 0, 1] },
321
328
  className: edgeScroll ? cn(
322
- ASSISTANT_MESSAGE_COLUMN_CLASS,
329
+ columnClass,
323
330
  ASSISTANT_MESSAGE_COLUMN_INSET_CLASS
324
331
  ) : void 0,
325
332
  children: [
@@ -348,7 +355,7 @@ function AgentConversationColumn({
348
355
  )
349
356
  ]
350
357
  }
351
- ) }) });
358
+ ) }) }) });
352
359
  }
353
360
  export {
354
361
  AgentConversationColumn
@@ -0,0 +1,31 @@
1
+ /**
2
+ * How a host lays the ONE conversation column out (HOST-OWNED-SOURCE-PLAN P28e look bar).
3
+ *
4
+ * Knobs, never forks: every value has today's web default, and a host that says nothing gets
5
+ * exactly the web chat. Matrx 2 (desktop) fills its pane, starts at the left edge and draws the
6
+ * person's turn as a compact block — the look its coding screen had before the column.
7
+ *
8
+ * - `width`: "reading" (the canonical `max-w-3xl` reading column) | "fill" (the host's pane).
9
+ * - `align`: "center" | "start" — where a reading-width column sits in a wider pane.
10
+ * - `userMessage`: "card" (the bordered card that collapses a long message behind a chevron) |
11
+ * "compact" (a tight block; only a genuinely long message collapses).
12
+ * - `answerActions`: "shown" (the answer's action row always visible on the latest answer) |
13
+ * "hover" (revealed on hover / keyboard focus — a finished answer ends on its last word, not on
14
+ * a lone ⋯ that reads as "still writing" in a host with few actions).
15
+ */
16
+ import { type ReactNode } from "react";
17
+ export interface TranscriptLayout {
18
+ width?: "reading" | "fill";
19
+ align?: "center" | "start";
20
+ userMessage?: "card" | "compact";
21
+ answerActions?: "shown" | "hover";
22
+ }
23
+ export declare const DEFAULT_TRANSCRIPT_LAYOUT: Required<TranscriptLayout>;
24
+ export declare function resolveTranscriptLayout(layout?: TranscriptLayout): Required<TranscriptLayout>;
25
+ /** The column's width + placement classes for a resolved layout. Web default = the canonical class. */
26
+ export declare function transcriptColumnClass(layout: Required<TranscriptLayout>): string;
27
+ export declare function TranscriptLayoutProvider(props: {
28
+ layout?: TranscriptLayout;
29
+ children: ReactNode;
30
+ }): import("react").FunctionComponentElement<import("react").ProviderProps<Required<TranscriptLayout>>>;
31
+ export declare function useTranscriptLayout(): Required<TranscriptLayout>;
@@ -0,0 +1,38 @@
1
+ "use client";
2
+ import { createContext, createElement, useContext, useMemo } from "react";
3
+ import {
4
+ ASSISTANT_MESSAGE_COLUMN_CLASS
5
+ } from "./assistant-message-layout.js";
6
+ const DEFAULT_TRANSCRIPT_LAYOUT = {
7
+ width: "reading",
8
+ align: "center",
9
+ userMessage: "card",
10
+ answerActions: "shown"
11
+ };
12
+ const TranscriptLayoutContext = createContext(DEFAULT_TRANSCRIPT_LAYOUT);
13
+ function resolveTranscriptLayout(layout) {
14
+ return { ...DEFAULT_TRANSCRIPT_LAYOUT, ...layout ?? {} };
15
+ }
16
+ function transcriptColumnClass(layout) {
17
+ if (layout.width === "fill") return "w-full";
18
+ if (layout.align === "start") return "w-full max-w-3xl";
19
+ return ASSISTANT_MESSAGE_COLUMN_CLASS;
20
+ }
21
+ function TranscriptLayoutProvider(props) {
22
+ const { width, align, userMessage, answerActions } = resolveTranscriptLayout(props.layout);
23
+ const value = useMemo(
24
+ () => ({ width, align, userMessage, answerActions }),
25
+ [width, align, userMessage, answerActions]
26
+ );
27
+ return createElement(TranscriptLayoutContext.Provider, { value }, props.children);
28
+ }
29
+ function useTranscriptLayout() {
30
+ return useContext(TranscriptLayoutContext);
31
+ }
32
+ export {
33
+ DEFAULT_TRANSCRIPT_LAYOUT,
34
+ TranscriptLayoutProvider,
35
+ resolveTranscriptLayout,
36
+ transcriptColumnClass,
37
+ useTranscriptLayout
38
+ };
@@ -6,7 +6,7 @@ import {
6
6
  spliceSave,
7
7
  tokenizeSource
8
8
  } from "@ai-matrx/content-ir/source";
9
- import { removeThinkingContent } from "@ai-matrx/print/markdown";
9
+ import { stripThinking as removeThinkingContent } from "@ai-matrx/content-ir/source";
10
10
  function isTextPart(part) {
11
11
  return part.type === "text" || part.type === void 0;
12
12
  }
@@ -17,7 +17,7 @@ import {
17
17
  readEnvelope,
18
18
  reconstructRegionValue
19
19
  } from "@ai-matrx/content-ir";
20
- import { removeThinkingContent } from "@ai-matrx/print/markdown";
20
+ import { stripThinking as removeThinkingContent } from "@ai-matrx/content-ir/source";
21
21
  import { NON_ANSWER_BLOCK_TYPES } from "../active-requests/active-requests.selectors.js";
22
22
  import {
23
23
  buildMessageCitationIndex,
@@ -5,7 +5,10 @@ import { ChevronDown, ExternalLink, PencilLine, Plus } from "lucide-react";
5
5
  import { AppLink } from "../../host/ui-slots.js";
6
6
  import { Popover, PopoverContent, PopoverTrigger } from "@ai-matrx/design-system";
7
7
  import { useAppSelector } from "../../store/hooks.js";
8
- import { selectConversationTitle } from "../../agents/redux/execution-system/conversations/conversations.selectors.js";
8
+ import {
9
+ selectConversationTitle,
10
+ selectIsCacheOnly
11
+ } from "../../agents/redux/execution-system/conversations/conversations.selectors.js";
9
12
  import { selectConversationListItemById } from "../../agents/redux/conversation-list/conversation-list.selectors.js";
10
13
  import { conversationRenameOpener } from "../../agents/components/conversation-actions/rename/conversationRenameOpener.js";
11
14
  import { conversationTitleText } from "@ai-matrx/content-ir/surfaces";
@@ -33,6 +36,9 @@ function ChatPanelTitleMenu({
33
36
  const [open, setOpen] = useState(false);
34
37
  const realTitle = useChatRealTitle(conversationId);
35
38
  const title = realTitle ?? "New chat";
39
+ const started = useAppSelector(
40
+ (state) => conversationId ? !selectIsCacheOnly(conversationId)(state) || selectConversationListItemById(conversationId)(state) !== null : false
41
+ );
36
42
  const close = () => setOpen(false);
37
43
  return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
38
44
  /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "quiet", iconEnd: /* @__PURE__ */ jsx(ChevronDown, {}), className: "min-w-0", title, children: title }) }),
@@ -56,7 +62,7 @@ function ChatPanelTitleMenu({
56
62
  }
57
63
  }
58
64
  ),
59
- conversationId ? /* @__PURE__ */ jsxs(Fragment, { children: [
65
+ conversationId && started ? /* @__PURE__ */ jsxs(Fragment, { children: [
60
66
  /* @__PURE__ */ jsx(
61
67
  Tile,
62
68
  {
@@ -85,6 +85,16 @@ export declare function rememberWorkspaceConversation(surfaceKey: string, conver
85
85
  export declare function reservedChatKey(surfaceKey: string): string;
86
86
  export declare function reserveWorkspaceConversationId(surfaceKey: string, fresh: boolean): string | undefined;
87
87
  export declare function releaseWorkspaceConversationId(surfaceKey: string): void;
88
+ /**
89
+ * The server refused a new chat's id because it is already TAKEN (409
90
+ * `conversation_already_exists`, run-ai-stream): a reserved id another tab
91
+ * already sent with, or one kept past a send. Retrying the same id can never
92
+ * succeed.
93
+ */
94
+ export declare function isConversationIdTaken(error: {
95
+ message?: unknown;
96
+ error_type?: unknown;
97
+ } | undefined): boolean;
88
98
  export interface CanvasWorkspaceConversationController {
89
99
  conversation: CanvasWorkspaceConversation;
90
100
  /** The id when ready, else null. */
@@ -7,6 +7,10 @@ import { resumeConversation } from "../../agents/redux/execution-system/thunks/r
7
7
  import { useAppDispatch, useAppSelector } from "../../store/hooks.js";
8
8
  import { selectShouldPromptForOrganization } from "../../host/ui-slots.js";
9
9
  import { selectIsCacheOnly } from "../../agents/redux/execution-system/conversations/conversations.selectors.js";
10
+ import { selectLatestRequestId } from "../../agents/redux/execution-system/selectors/aggregate.selectors.js";
11
+ import { selectRequestError } from "../../agents/redux/execution-system/active-requests/active-requests.selectors.js";
12
+ import { extractFlatText } from "../../agents/redux/execution-system/messages/messages.selectors.js";
13
+ import { setUserInputText } from "../../agents/redux/execution-system/instance-user-input/instance-user-input.slice.js";
10
14
  import { replaceAddressWithoutNavigating } from "@ai-matrx/rich-content/utils/addressWithoutNavigating";
11
15
  import { describeLaunchError } from "./describe-launch-error.js";
12
16
  import { sideChatAddress } from "./side-chat-address.js";
@@ -65,6 +69,11 @@ function releaseWorkspaceConversationId(surfaceKey) {
65
69
  } catch {
66
70
  }
67
71
  }
72
+ function isConversationIdTaken(error) {
73
+ if (!error) return false;
74
+ const text = `${typeof error.error_type === "string" ? error.error_type : ""} ${typeof error.message === "string" ? error.message : ""}`;
75
+ return /conversation_already_exists|Conversation already exists/i.test(text);
76
+ }
68
77
  function isCanvasWorkspaceStart(input) {
69
78
  return "kind" in input;
70
79
  }
@@ -103,6 +112,21 @@ function useCanvasWorkspaceConversation(surfaceKey, input) {
103
112
  const serverHasIt = useAppSelector(
104
113
  (state) => conversationId ? !selectIsCacheOnly(conversationId)(state) : false
105
114
  );
115
+ const latestRequestId = useAppSelector(
116
+ (state) => conversationId ? selectLatestRequestId(conversationId)(state) : void 0
117
+ );
118
+ const sendStarted = latestRequestId !== void 0;
119
+ const idTaken = useAppSelector(
120
+ (state) => latestRequestId ? isConversationIdTaken(selectRequestError(latestRequestId)(state)) : false
121
+ );
122
+ const refusedText = useAppSelector((state) => {
123
+ if (!idTaken || !conversationId) return "";
124
+ const entry = state.messages?.byConversationId?.[conversationId];
125
+ const lastUser = entry?.orderedIds.map((id) => entry.byId[id]).filter((record) => record?.role === "user").at(-1);
126
+ const sent = lastUser ? extractFlatText(lastUser).trim() : "";
127
+ const typed = state.instanceUserInput?.byConversationId?.[conversationId]?.text?.trim() ?? "";
128
+ return [sent, typed === sent ? "" : typed].filter(Boolean).join("\n\n");
129
+ });
106
130
  const [keyedFor, setKeyedFor] = useState({ surfaceKey, hasAddress });
107
131
  if (keyedFor.surfaceKey !== surfaceKey || keyedFor.hasAddress !== hasAddress) {
108
132
  setKeyedFor({ surfaceKey, hasAddress });
@@ -141,14 +165,36 @@ function useCanvasWorkspaceConversation(surfaceKey, input) {
141
165
  if (!conversationId && addressOpen) return;
142
166
  const shown = conversationId && (request.kind === "open" || serverHasIt) ? conversationId : null;
143
167
  if (conversationId) {
144
- rememberWorkspaceConversation(surfaceKey, shown);
145
- if (shown) releaseWorkspaceConversationId(surfaceKey);
168
+ const remembered = shown ?? (sendStarted && !idTaken ? conversationId : null);
169
+ rememberWorkspaceConversation(surfaceKey, remembered);
170
+ if (remembered) releaseWorkspaceConversationId(surfaceKey);
146
171
  }
147
172
  const search = sideChatAddress(window.location.search, { conversationId: shown, open: addressOpen, hostedElsewhere });
148
173
  if (search === window.location.search) return;
149
174
  onAddressWrite.current?.(search);
150
175
  replaceAddressWithoutNavigating(`${window.location.pathname}${search}${window.location.hash}`);
151
- }, [hasAddress, addressRead, addressOpen, hostedElsewhere, conversationId, request.kind, serverHasIt, surfaceKey]);
176
+ }, [hasAddress, addressRead, addressOpen, hostedElsewhere, conversationId, request.kind, serverHasIt, sendStarted, idTaken, surfaceKey]);
177
+ const healed = useRef(null);
178
+ const carry = useRef(null);
179
+ useEffect(() => {
180
+ if (!idTaken || serverHasIt || !conversationId || !latestRequestId) return;
181
+ if (healed.current === latestRequestId) return;
182
+ healed.current = latestRequestId;
183
+ console.warn("[canvas-workspace] the new chat's id was already taken (409); starting it again under a fresh id", conversationId);
184
+ releaseWorkspaceConversationId(surfaceKey);
185
+ if (hasAddress) rememberWorkspaceConversation(surfaceKey, null);
186
+ carry.current = refusedText ? { text: refusedText, from: conversationId } : null;
187
+ setConversationId(null);
188
+ setFailure(null);
189
+ setRequest(
190
+ (current) => current.kind === "agent" ? { kind: "agent", agentId: current.agentId, nonce: current.nonce + 1 } : { kind: "new", nonce: current.nonce + 1, ...current.kind === "new" && current.mandateKey ? { mandateKey: current.mandateKey } : {} }
191
+ );
192
+ }, [idTaken, serverHasIt, conversationId, latestRequestId, surfaceKey, hasAddress, refusedText]);
193
+ useEffect(() => {
194
+ if (!conversationId || !carry.current || carry.current.from === conversationId) return;
195
+ dispatch(setUserInputText({ conversationId, text: carry.current.text }));
196
+ carry.current = null;
197
+ }, [conversationId, dispatch]);
152
198
  useEffect(() => {
153
199
  if (!enabled || waitingForOrganization || !addressRead) return;
154
200
  const key = `${surfaceKey}#${request.kind}#${request.nonce}`;
@@ -239,6 +285,7 @@ function useCanvasWorkspaceConversation(surfaceKey, input) {
239
285
  return { conversation, conversationId, startNew, openExisting, startWith };
240
286
  }
241
287
  export {
288
+ isConversationIdTaken,
242
289
  recallWorkspaceConversation,
243
290
  releaseWorkspaceConversationId,
244
291
  rememberWorkspaceConversation,
@@ -3,7 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState, useCallback } from "react";
4
4
  import { ChevronDown } from "lucide-react";
5
5
  import { usePathname, useSearchParams, useRouter } from "../../host/navigation.js";
6
- import { AgentPickerSheet } from "../../next/lazy/AgentPickerSheet.js";
6
+ import { AgentPickerSheet } from "../../ui/lazy/AgentPickerSheet.js";
7
7
  import { useAgentName } from "../../agents/identity/agent-identity.js";
8
8
  import { pushAppHref } from "../../host/ui-slots.js";
9
9
  import { Button } from "@ai-matrx/design-system/controls";
@@ -2,7 +2,7 @@
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState, useCallback, useRef, useEffect } from "react";
4
4
  import { useRouter, usePathname, useSearchParams } from "../../host/navigation.js";
5
- import { AgentPickerSheet } from "../../next/lazy/AgentPickerSheet.js";
5
+ import { AgentPickerSheet } from "../../ui/lazy/AgentPickerSheet.js";
6
6
  import { ChevronDown } from "lucide-react";
7
7
  import { SidebarActions } from "./sidebar/SidebarActions.js";
8
8
  import { SsrSidebarAgents } from "./SsrSidebarAgents.js";
@@ -2,7 +2,7 @@
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useState, useCallback } from "react";
4
4
  import { useRouter } from "../../host/navigation.js";
5
- import { AgentPickerSheet } from "../../next/lazy/AgentPickerSheet.js";
5
+ import { AgentPickerSheet } from "../../ui/lazy/AgentPickerSheet.js";
6
6
  import { useAppSelector, useAppDispatch } from "../../store/hooks.js";
7
7
  import { selectAgentById } from "../../agents/redux/agent-definition/selectors.js";
8
8
  import { selectLatestConversationId } from "../../agents/redux/execution-system/selectors/aggregate.selectors.js";
@@ -2,7 +2,7 @@
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useCallback, useState } from "react";
4
4
  import { useRouter } from "../../../host/navigation.js";
5
- import { AgentPickerSheet } from "../../../next/lazy/AgentPickerSheet.js";
5
+ import { AgentPickerSheet } from "../../../ui/lazy/AgentPickerSheet.js";
6
6
  import { useAppSelector } from "../../../store/hooks.js";
7
7
  import { selectIsAdmin, selectIsAuthenticated } from "../../../host/identity.js";
8
8
  import { useDebugContext } from "../../../host/prefs-react.js";
@@ -9,7 +9,7 @@ import {
9
9
  useMemo
10
10
  } from "react";
11
11
  import { useDebugContext } from "../../../host/prefs-react.js";
12
- import { GuidedVariableInputs, StackedVariableInputs } from "../../../next/lazy/ChatVariableInputs.js";
12
+ import { GuidedVariableInputs, StackedVariableInputs } from "../../../ui/lazy/ChatVariableInputs.js";
13
13
  import { useRouter, useSearchParams, usePathname } from "../../../host/navigation.js";
14
14
  import {
15
15
  CornerDownLeft,
@@ -8,7 +8,7 @@ import {
8
8
  useCallback,
9
9
  useMemo
10
10
  } from "react";
11
- import { GuidedVariableInputs, PublicVariableInputs } from "../next/lazy/PublicChatVariableInputs.js";
11
+ import { GuidedVariableInputs, PublicVariableInputs } from "../ui/lazy/PublicChatVariableInputs.js";
12
12
  import { useRouter, useSearchParams, usePathname } from "../host/navigation.js";
13
13
  import {
14
14
  CornerDownLeft,
@@ -10,7 +10,7 @@ import {
10
10
  selectUIState
11
11
  } from "./_legacy-stubs.js";
12
12
  import { chatConversationsActions } from "./_legacy-stubs.js";
13
- import { AssistantMessage } from "../next/lazy/CxAssistantMessage.js";
13
+ import { AssistantMessage } from "../ui/lazy/CxAssistantMessage.js";
14
14
  import { MessageErrorBoundary } from "./MessageErrorBoundary.js";
15
15
  import { UserMessage } from "./UserMessage.js";
16
16
  function StreamingAssistantMessage({
@@ -291,7 +291,22 @@ const readListRpc = slotFn("readListRpc");
291
291
  const ReadFailure = slotComponent("ReadFailure", unregisteredWidget("ReadFailure"));
292
292
  const ItemRow = slotComponent("ItemRow", unregisteredWidget("ItemRow"));
293
293
  const UntrustedCount = slotComponent("UntrustedCount", unregisteredWidget("UntrustedCount"));
294
- const StaleDataNotice = slotComponent("StaleDataNotice", unregisteredWidget("StaleDataNotice"));
294
+ function PlainStaleDataNotice(props) {
295
+ const what = props.what ?? "this";
296
+ const sentence = props.hasData ? `Showing what was last read; ${what} could not be refreshed.` : `Could not read ${what}.`;
297
+ return createElement(
298
+ "div",
299
+ { role: "status", className: "text-xs text-muted-foreground", "data-stale-data-notice": "" },
300
+ sentence,
301
+ props.detail ? ` ${props.detail}` : null,
302
+ props.onRetry ? createElement(
303
+ "button",
304
+ { type: "button", className: "ml-2 underline", onClick: () => props.onRetry?.() },
305
+ "Try again"
306
+ ) : null
307
+ );
308
+ }
309
+ const StaleDataNotice = slotComponent("StaleDataNotice", PlainStaleDataNotice);
295
310
  const WorkspaceGate = slotComponent("WorkspaceGate", DefaultWorkspaceGate);
296
311
  const OrganizationContextNotice = slotComponent("OrganizationContextNotice", unregisteredWidget("OrganizationContextNotice"));
297
312
  const JsonInspector = slotComponent("JsonInspector", unregisteredWidget("JsonInspector"));
@@ -315,7 +330,7 @@ const ReferenceCopyMenuItem = slotComponent("ReferenceCopyMenuItem", unregistere
315
330
  const ReferenceCopyButton = slotComponent("ReferenceCopyButton", unregisteredWidget("ReferenceCopyButton"));
316
331
  const MandateNotesPanel = slotComponent("MandateNotesPanel", unregisteredWidget("MandateNotesPanel"));
317
332
  const SurfaceBoundAgentsList = slotComponent("SurfaceBoundAgentsList", unregisteredWidget("SurfaceBoundAgentsList"));
318
- const ProposedDirectivesZone = slotComponent("ProposedDirectivesZone", unregisteredWidget("ProposedDirectivesZone"));
333
+ const ProposedDirectivesZone = slotComponent("ProposedDirectivesZone", optionalWidget("ProposedDirectivesZone"));
319
334
  const EntityCommentPopover = slotComponent("EntityCommentPopover", unregisteredWidget("EntityCommentPopover"));
320
335
  const ProTextarea = slotComponent("ProTextarea", (props) => {
321
336
  const { onChange, value, className, placeholder, rows, id, ref, autoFocus, disabled } = props;
@@ -1,4 +1,4 @@
1
- import { SurfaceAgentsPanelImpl } from "../../../next/lazy/SurfaceAgentsPanelImpl.js";
1
+ import { SurfaceAgentsPanelImpl } from "../../../ui/lazy/SurfaceAgentsPanelImpl.js";
2
2
  /** What a guest is told when they reach for Agents — one copy for every door. */
3
3
  export declare const AGENTS_AUTH_GATE: {
4
4
  featureName: string;
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useRef, useState } from "react";
4
- import { SurfaceAgentsPanelImpl } from "../../../next/lazy/SurfaceAgentsPanelImpl.js";
4
+ import { SurfaceAgentsPanelImpl } from "../../../ui/lazy/SurfaceAgentsPanelImpl.js";
5
5
  import { TapTargetButtonTransparent } from "@ai-matrx/tap-target";
6
6
  import { INTELLIGENCE_ICON } from "@ai-matrx/icons/domain";
7
7
  import {
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
- import { DbToolRendererImplLazy as LazyImpl } from "../../next/lazy/DbToolRendererImpl.js";
3
+ import { DbToolRendererImplLazy as LazyImpl } from "../../ui/lazy/DbToolRendererImpl.js";
4
4
  const DbToolRenderer = (props) => {
5
5
  return /* @__PURE__ */ jsx(LazyImpl, { ...props });
6
6
  };
@@ -1,12 +1,11 @@
1
1
  "use client";
2
- import dynamic from "next/dynamic";
3
- const AgentAssistantMessage = dynamic(
2
+ import { clientLazy } from "../../utils/client-lazy.js";
3
+ const AgentAssistantMessage = clientLazy(
4
4
  () => import("../../agents/components/messages-display/assistant/AgentAssistantMessage.js").then(
5
5
  (m) => ({
6
6
  default: m.AgentAssistantMessage
7
7
  })
8
- ),
9
- { ssr: false }
8
+ )
10
9
  );
11
10
  export {
12
11
  AgentAssistantMessage
@@ -1,10 +1,9 @@
1
1
  "use client";
2
- import dynamic from "next/dynamic";
3
- const AgentPickerSheet = dynamic(
2
+ import { clientLazy } from "../../utils/client-lazy.js";
3
+ const AgentPickerSheet = clientLazy(
4
4
  () => import("../../cx-chat/components/agent/AgentPickerSheet.js").then(
5
5
  (m) => ({ default: m.AgentPickerSheet })
6
- ),
7
- { ssr: false }
6
+ )
8
7
  );
9
8
  export {
10
9
  AgentPickerSheet
@@ -1,16 +1,14 @@
1
1
  "use client";
2
- import dynamic from "next/dynamic";
3
- const GuidedVariableInputs = dynamic(
2
+ import { clientLazy } from "../../utils/client-lazy.js";
3
+ const GuidedVariableInputs = clientLazy(
4
4
  () => import("../../cx-chat/components/user-input/GuidedVariableInputs.js").then(
5
5
  (m) => ({ default: m.GuidedVariableInputs })
6
- ),
7
- { ssr: false }
6
+ )
8
7
  );
9
- const StackedVariableInputs = dynamic(
8
+ const StackedVariableInputs = clientLazy(
10
9
  () => import("../../cx-chat/components/user-input/StackedVariableInputs.js").then(
11
10
  (m) => ({ default: m.StackedVariableInputs })
12
- ),
13
- { ssr: false }
11
+ )
14
12
  );
15
13
  export {
16
14
  GuidedVariableInputs,
@@ -1 +1 @@
1
- export declare const ConversationRenameDialogHostImpl: import("react").ComponentType<{}>;
1
+ export declare const ConversationRenameDialogHostImpl: import("react").ComponentType<object>;
@@ -1,8 +1,8 @@
1
1
  "use client";
2
- import dynamic from "next/dynamic";
3
- const ConversationRenameDialogHostImpl = dynamic(
2
+ import { clientLazy } from "../../utils/client-lazy.js";
3
+ const ConversationRenameDialogHostImpl = clientLazy(
4
4
  () => import("../../agents/components/conversation-actions/rename/ConversationRenameDialogHostImpl.js"),
5
- { ssr: false, loading: () => null }
5
+ { loading: () => null }
6
6
  );
7
7
  export {
8
8
  ConversationRenameDialogHostImpl
@@ -1,10 +1,9 @@
1
1
  "use client";
2
- import dynamic from "next/dynamic";
3
- const AssistantMessage = dynamic(
2
+ import { clientLazy } from "../../utils/client-lazy.js";
3
+ const AssistantMessage = clientLazy(
4
4
  () => import("../../cx-conversation/AssistantMessage.js").then(
5
5
  (m) => m.AssistantMessage
6
- ),
7
- { ssr: false }
6
+ )
8
7
  );
9
8
  export {
10
9
  AssistantMessage
@@ -1,10 +1,10 @@
1
1
  "use client";
2
- import dynamic from "next/dynamic";
3
- const DbToolRendererImplLazy = dynamic(
2
+ import { clientLazy } from "../../utils/client-lazy.js";
3
+ const DbToolRendererImplLazy = clientLazy(
4
4
  () => import("../../tool-call-visualization/db-renderer/DbToolRendererImpl.js").then((m) => ({
5
5
  default: m.DbToolRendererImpl
6
6
  })),
7
- { ssr: false, loading: () => null }
7
+ { loading: () => null }
8
8
  );
9
9
  export {
10
10
  DbToolRendererImplLazy
@@ -1,9 +1,8 @@
1
- import dynamic from "next/dynamic";
2
- const IconResolver = dynamic(
1
+ import { clientLazy } from "../../utils/client-lazy.js";
2
+ const IconResolver = clientLazy(
3
3
  () => import("@ai-matrx/icons").then((m) => ({
4
4
  default: m.IconResolver
5
- })),
6
- { ssr: false }
5
+ }))
7
6
  );
8
7
  export {
9
8
  IconResolver
@@ -1,16 +1,14 @@
1
1
  "use client";
2
- import dynamic from "next/dynamic";
3
- const GuidedVariableInputs = dynamic(
2
+ import { clientLazy } from "../../utils/client-lazy.js";
3
+ const GuidedVariableInputs = clientLazy(
4
4
  () => import("../../public-chat/components/GuidedVariableInputs.js").then(
5
5
  (m) => ({ default: m.GuidedVariableInputs })
6
- ),
7
- { ssr: false }
6
+ )
8
7
  );
9
- const PublicVariableInputs = dynamic(
8
+ const PublicVariableInputs = clientLazy(
10
9
  () => import("../../public-chat/components/PublicVariableInputs.js").then(
11
10
  (m) => ({ default: m.PublicVariableInputs })
12
- ),
13
- { ssr: false }
11
+ )
14
12
  );
15
13
  export {
16
14
  GuidedVariableInputs,
@@ -1,3 +1,3 @@
1
1
  import type { ScrollAssistantLauncherImplProps } from "../../agents/components/ambient-assistant/ScrollAssistantLauncherImpl.js";
2
2
  export declare const ScrollAssistantLauncherImpl: import("react").ComponentType<ScrollAssistantLauncherImplProps>;
3
- export declare const ScrollVoiceAssistantLauncherImpl: import("react").ComponentType<{}>;
3
+ export declare const ScrollVoiceAssistantLauncherImpl: import("react").ComponentType<object>;
@@ -1,18 +1,16 @@
1
1
  "use client";
2
- import dynamic from "next/dynamic";
3
- const ScrollAssistantLauncherImpl = dynamic(
2
+ import { clientLazy } from "../../utils/client-lazy.js";
3
+ const ScrollAssistantLauncherImpl = clientLazy(
4
4
  () => import("../../agents/components/ambient-assistant/ScrollAssistantLauncherImpl.js"),
5
5
  {
6
- ssr: false,
7
6
  // A prop-blind loading shell would jump between the two supported heights.
8
7
  // Reveal the correctly sized implementation once its chunk is ready.
9
8
  loading: () => null
10
9
  }
11
10
  );
12
- const ScrollVoiceAssistantLauncherImpl = dynamic(
11
+ const ScrollVoiceAssistantLauncherImpl = clientLazy(
13
12
  () => import("../../agents/components/ambient-assistant/ScrollVoiceAssistantLauncherImpl.js"),
14
13
  {
15
- ssr: false,
16
14
  loading: () => null
17
15
  }
18
16
  );
@@ -1,11 +1,10 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import dynamic from "next/dynamic";
3
+ import { clientLazy } from "../../utils/client-lazy.js";
4
4
  import { Loader2 } from "lucide-react";
5
- const SurfaceAgentsPanelImpl = dynamic(
5
+ const SurfaceAgentsPanelImpl = clientLazy(
6
6
  () => import("../../surfaces/components/chrome/SurfaceAgentsPanelImpl.js"),
7
7
  {
8
- ssr: false,
9
8
  loading: () => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2 p-8 text-xs text-muted-foreground", children: [
10
9
  /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
11
10
  "Loading agents\u2026"
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `clientLazy` — the package's host-neutral replacement for `next/dynamic(..., { ssr: false })`.
3
+ * A lazily loaded component that renders only on the client: on the server and during hydration
4
+ * it renders `loading` (hydration-safe through `useSyncExternalStore`); on the client it renders
5
+ * the loaded component inside a Suspense boundary with the same `loading` as the fallback.
6
+ *
7
+ * Plain React (`lazy` + `Suspense`), so a Vite / Electron / extension host needs no `next`
8
+ * installed (HOST-OWNED-SOURCE-PLAN P28e). Guard: `host/__tests__/no-next-import-outside-next-binding`.
9
+ */
10
+ import { type ComponentType } from "react";
11
+ export declare function useIsClient(): boolean;
12
+ export declare function clientLazy<P extends object>(load: () => Promise<{
13
+ default: ComponentType<P>;
14
+ } | ComponentType<P>>, options?: {
15
+ loading?: ComponentType;
16
+ }): ComponentType<P>;
@@ -0,0 +1,27 @@
1
+ "use client";
2
+ import { createElement, lazy, Suspense, useSyncExternalStore } from "react";
3
+ const subscribe = () => () => {
4
+ };
5
+ const onClient = () => true;
6
+ const onServer = () => false;
7
+ function useIsClient() {
8
+ return useSyncExternalStore(subscribe, onClient, onServer);
9
+ }
10
+ function clientLazy(load, options = {}) {
11
+ const Lazy = lazy(async () => {
12
+ const m = await load();
13
+ return typeof m === "function" || !("default" in m) ? { default: m } : m;
14
+ });
15
+ const Loading = options.loading;
16
+ function ClientLazy(props) {
17
+ const client = useIsClient();
18
+ const fallback = Loading ? createElement(Loading) : null;
19
+ if (!client) return fallback;
20
+ return createElement(Suspense, { fallback }, createElement(Lazy, props));
21
+ }
22
+ return ClientLazy;
23
+ }
24
+ export {
25
+ clientLazy,
26
+ useIsClient
27
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/chat",
3
- "version": "0.4.16",
3
+ "version": "0.4.19",
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; plus the kernel compatibility entries (., /external-store, /matrx, /react, /tools, styles.css, tokens.css) until P28.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -226,26 +226,26 @@
226
226
  "server-only": "^0.0.1",
227
227
  "uuid": "^14.0.2",
228
228
  "zod": "^4.6.5",
229
- "@ai-matrx/agents": "latest",
230
- "@ai-matrx/alchemy": "latest",
229
+ "@ai-matrx/associations": "latest",
231
230
  "@ai-matrx/browser-audio": "latest",
231
+ "@ai-matrx/alchemy": "latest",
232
232
  "@ai-matrx/canvas": "latest",
233
233
  "@ai-matrx/content-ir": "latest",
234
+ "@ai-matrx/agents": "latest",
234
235
  "@ai-matrx/content-ir-react": "latest",
235
- "@ai-matrx/associations": "latest",
236
- "@ai-matrx/data": "latest",
237
- "@ai-matrx/kit": "latest",
238
236
  "@ai-matrx/design-system": "latest",
239
- "@ai-matrx/media": "latest",
237
+ "@ai-matrx/diff": "latest",
240
238
  "@ai-matrx/icons": "latest",
239
+ "@ai-matrx/media": "latest",
241
240
  "@ai-matrx/print": "latest",
242
241
  "@ai-matrx/realtime": "latest",
243
242
  "@ai-matrx/records": "latest",
244
243
  "@ai-matrx/records-ui": "latest",
245
244
  "@ai-matrx/rich-content": "latest",
246
- "@ai-matrx/diff": "latest",
245
+ "@ai-matrx/rich-editor": "latest",
247
246
  "@ai-matrx/tap-target": "latest",
248
- "@ai-matrx/rich-editor": "latest"
247
+ "@ai-matrx/data": "latest",
248
+ "@ai-matrx/kit": "latest"
249
249
  },
250
250
  "devDependencies": {
251
251
  "@arethetypeswrong/cli": "^0.18.5",
File without changes
File without changes
File without changes
File without changes
File without changes