@ai-matrx/chat 0.3.6 → 0.3.9

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,25 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.3.9
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/chat/v0.3.5..npm/chat/v0.3.9 -- apps/shared/chat`).
7
+ No source changes intended and no consumer action required.
8
+
9
+ ## 0.3.8
10
+
11
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
12
+ `git diff npm/chat/v0.3.5..npm/chat/v0.3.8 -- apps/shared/chat`).
13
+ No source changes intended and no consumer action required.
14
+
15
+ ## 0.3.7 — 2026-10-07
16
+ ### Re-sync before matrx-frontend consumes the published package (P27)
17
+
18
+ - Carries the matrx-frontend `packages/chat` edits after `3d7ea46a26` not already mirrored here: composer
19
+ output, agent variable cards, agent options menu, edit/fork message thunks, compute targets, context
20
+ scopes, window openers.
21
+ - Consumer action: none.
22
+
3
23
  ## 0.3.6 — 2026-10-07
4
24
  ### Re-sync before matrx-frontend consumes the published package (P27)
5
25
 
@@ -10,6 +10,7 @@
10
10
  * the parent via `onVoiceBusyChange` so Enter-to-send is gated too.
11
11
  */
12
12
  import React from "react";
13
+ import type { SourceFeature } from "@ai-matrx/agents/generated/source-attribution";
13
14
  import type { ComposerMode, ComposerSize } from "./composer/composer-types.js";
14
15
  /**
15
16
  * Shared resting (idle) tint for every toolbar button in this row. Exported so
@@ -67,6 +68,9 @@ interface InputActionButtonsProps {
67
68
  * Absent = Full's button row inside the card.
68
69
  */
69
70
  part?: "send" | "controls" | "launcher";
71
+ /** Attribution for a Live audio session started here. Absent = "chat". */
72
+ sourceFeature?: SourceFeature;
73
+ liveAudioStartsOn?: boolean;
70
74
  };
71
75
  }
72
76
  export declare function InputActionButtons({ conversationId, showSendButton, showVariableIcon, surfaceKey, disableSend, onVoiceBusyChange, extraRightControls, onRequestInputExpand, composer, }: InputActionButtonsProps): React.JSX.Element | null;
@@ -4,7 +4,6 @@ import { useCallback, useState } from "react";
4
4
  import {
5
5
  CornerDownLeft,
6
6
  Braces,
7
- AudioLines,
8
7
  Loader2,
9
8
  Square
10
9
  } from "lucide-react";
@@ -14,7 +13,8 @@ import {
14
13
  TooltipTrigger
15
14
  } from "@ai-matrx/design-system";
16
15
  import { useAppSelector, useAppDispatch } from "../../../../store/hooks.js";
17
- import { announceComingSoon } from "../../../../host/ui-slots.js";
16
+ import { selectAgentIdFromInstance } from "../../../redux/execution-system/conversations/conversations.selectors.js";
17
+ import { VoiceRelayBar } from "../../../../voice-agent/relay/VoiceRelayBar.js";
18
18
  import { AgentMicrophoneButton } from "./AgentMicrophoneButton.js";
19
19
  import { RunControlsMenu } from "./RunControlsMenu.js";
20
20
  import {
@@ -75,6 +75,8 @@ function InputActionButtons({
75
75
  }) {
76
76
  const dispatch = useAppDispatch();
77
77
  const [voiceBusy, setVoiceBusy] = useState(false);
78
+ const [liveAudioOn, setLiveAudioOn] = useState(composer.liveAudioStartsOn ?? false);
79
+ const agentId = useAppSelector(selectAgentIdFromInstance(conversationId)) ?? null;
78
80
  const { isExecuting, executingConversationId } = useSurfaceExecution(
79
81
  conversationId,
80
82
  surfaceKey
@@ -104,12 +106,16 @@ function InputActionButtons({
104
106
  const handleStop = useCallback(() => {
105
107
  dispatch(cancelExecution(executingConversationId ?? conversationId));
106
108
  }, [executingConversationId, conversationId, dispatch]);
107
- const liveAudioButton = showSendButton ? /* @__PURE__ */ jsx(
108
- InputButton,
109
+ const liveAudioButton = showSendButton && agentId ? /* @__PURE__ */ jsx(
110
+ VoiceRelayBar,
109
111
  {
110
- icon: AudioLines,
111
- tooltip: "Live audio",
112
- onClick: () => void announceComingSoon("chat.live-audio")
112
+ variant: "composer",
113
+ enabled: liveAudioOn,
114
+ onEnabledChange: setLiveAudioOn,
115
+ primaryAgentId: agentId,
116
+ conversationId,
117
+ surfaceKey: surfaceKey ?? `composer:${conversationId}`,
118
+ sourceFeature: composer.sourceFeature ?? "chat"
113
119
  }
114
120
  ) : null;
115
121
  const variablesToggle = shouldShowVariables && showVariableIcon ? /* @__PURE__ */ jsx(
@@ -135,7 +141,8 @@ function InputActionButtons({
135
141
  surfaceKey,
136
142
  folded: composer.folded,
137
143
  // Compact's live audio leaves the row when narrow and rides +.
138
- foldLiveAudio: composer.folded && composer.part === "controls"
144
+ foldLiveAudio: composer.folded && composer.part === "controls" && Boolean(liveAudioButton),
145
+ onLiveAudio: () => setLiveAudioOn(true)
139
146
  }
140
147
  }
141
148
  );
@@ -178,7 +185,7 @@ function InputActionButtons({
178
185
  /* @__PURE__ */ jsx(DesktopPresenceIndicator, { conversationId }),
179
186
  variablesToggle,
180
187
  micGroup,
181
- composer.folded ? null : liveAudioButton,
188
+ composer.folded && !liveAudioOn ? null : liveAudioButton,
182
189
  composer.leading
183
190
  ] }),
184
191
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1.5", children: [
@@ -26,6 +26,8 @@ export interface RunControlsMenuProps {
26
26
  folded?: boolean;
27
27
  /** Compact and narrow: live audio lives in the + menu too. */
28
28
  foldLiveAudio?: boolean;
29
+ /** The + menu's Live audio row turns the composer's voice on. */
30
+ onLiveAudio?: () => void;
29
31
  };
30
32
  }
31
33
  export declare function RunControlsMenu({ conversationId, variant, includeAttach, align, side, foldToolbarExtras, onRequestInputExpand, composer, }: RunControlsMenuProps): import("react").JSX.Element;
@@ -113,6 +113,7 @@ function RunControlsMenu({
113
113
  size: composer.size,
114
114
  folded: composer.folded,
115
115
  foldLiveAudio: composer.foldLiveAudio,
116
+ onLiveAudio: composer.onLiveAudio,
116
117
  side,
117
118
  surfaceKey: composer.surfaceKey,
118
119
  onRequestInputExpand
@@ -144,6 +145,7 @@ function RunControlsMenu({
144
145
  size: composer.size,
145
146
  folded: composer.folded,
146
147
  foldLiveAudio: composer.foldLiveAudio,
148
+ onLiveAudio: composer.onLiveAudio,
147
149
  side,
148
150
  surfaceKey: composer.surfaceKey,
149
151
  onRequestInputExpand,
@@ -179,7 +179,7 @@ function SmartAgentInputStacked({
179
179
  surfaceKey,
180
180
  disableSend: sendBlocked
181
181
  };
182
- const composerParts = { size: composer.size, mode: composer.mode, folded };
182
+ const composerParts = { size: composer.size, mode: composer.mode, folded, sourceFeature: composer.sourceFeature, liveAudioStartsOn: composer.liveAudioStartsOn };
183
183
  if (composer.size === "launcher") {
184
184
  return /* @__PURE__ */ jsx(
185
185
  "div",
@@ -53,18 +53,16 @@ function ComposerAgentPill({ conversationId, mode, size, agentControl, menuSide,
53
53
  const agentFilter = useComposerAgentFilter(conversationId);
54
54
  const showOverride = info.modelOverridden && !info.isCustom && composerShows(mode, "agent.presets");
55
55
  const overrideTitle = info.agentModelLabel ? `Agent's own model: ${info.agentModelLabel}` : "Not the agent's own model";
56
- const pill = /* @__PURE__ */ jsxs(
56
+ const pill = /* @__PURE__ */ jsx(
57
57
  Button,
58
58
  {
59
59
  variant: "quiet",
60
60
  iconEnd: /* @__PURE__ */ jsx(ChevronDown, {}),
61
61
  "aria-label": `Agent: ${label}${showOverride ? " (model changed)" : ""}`,
62
62
  title: showOverride ? `${label} \u2014 ${overrideTitle}` : label,
63
- className: "min-w-0",
64
- children: [
65
- label,
66
- showOverride ? /* @__PURE__ */ jsx("span", { "data-testid": "composer-model-override-chip", children: " \xB7 changed" }) : null
67
- ]
63
+ "data-model-override": showOverride ? "" : void 0,
64
+ className: "min-w-0 shrink",
65
+ children: showOverride ? `${label} \xB7 changed` : label
68
66
  }
69
67
  );
70
68
  if (folded) {
@@ -222,8 +222,8 @@ function ShapePicker({
222
222
  if (lock.status === "reading") {
223
223
  return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1 px-2.5 py-2", "aria-label": "Checking the agent's shape", children: /* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-full rounded-md" }) });
224
224
  }
225
- return /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-col", children: [
226
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 px-2.5 pb-1 pt-2", children: [
225
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [
226
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center justify-between gap-2 px-2.5 pb-1 pt-2", children: [
227
227
  /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "Shapes" }),
228
228
  kinds.length > 0 ? /* @__PURE__ */ jsxs("span", { className: "shrink-0 text-[11px] tabular-nums text-muted-foreground", children: [
229
229
  kinds.length,
@@ -315,7 +315,7 @@ function ShapePicker({
315
315
  }
316
316
  function ComposerOutputPanel({ conversationId }) {
317
317
  const output = useComposerOutput(conversationId);
318
- return /* @__PURE__ */ jsxs("div", { className: "@container flex min-h-0 flex-col", children: [
318
+ return /* @__PURE__ */ jsxs("div", { className: "@container flex min-h-0 flex-1 flex-col", children: [
319
319
  /* @__PURE__ */ jsx(ComposerMenuLabel, { children: "Output types" }),
320
320
  /* @__PURE__ */ jsx(TypeGrid, { types: output.types, onToggle: output.toggleType }),
321
321
  /* @__PURE__ */ jsx(ComposerMenuHelp, { children: "Narrows the agent list. Not sent to the model." }),
@@ -333,6 +333,7 @@ function ComposerOutputPill({
333
333
  menuSide
334
334
  }) {
335
335
  const [open, setOpen] = useState(false);
336
+ const [roomierSide, setRoomierSide] = useState(null);
336
337
  const output = useComposerOutput(conversationId);
337
338
  return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, modal: false, children: [
338
339
  /* @__PURE__ */ jsxs(SplitButton, { className: "min-w-0", children: [
@@ -345,6 +346,10 @@ function ComposerOutputPill({
345
346
  "aria-label": `Output: ${output.label}`,
346
347
  title: output.lockWarning ?? void 0,
347
348
  className: "min-w-0",
349
+ onClick: (event) => {
350
+ const rect = event.currentTarget.getBoundingClientRect();
351
+ setRoomierSide(rect.top > window.innerHeight - rect.bottom ? "top" : "bottom");
352
+ },
348
353
  children: output.label
349
354
  }
350
355
  ) }),
@@ -353,7 +358,7 @@ function ComposerOutputPill({
353
358
  /* @__PURE__ */ jsx(
354
359
  PopoverContent,
355
360
  {
356
- side: menuSide,
361
+ side: roomierSide ?? menuSide,
357
362
  align: "start",
358
363
  sideOffset: 8,
359
364
  className: "flex max-h-[var(--radix-popover-content-available-height)] w-[340px] max-w-[calc(100vw-1rem)] flex-col overflow-hidden p-1",
@@ -35,6 +35,8 @@ interface ComposerPlusMenuProps {
35
35
  folded?: boolean;
36
36
  /** Compact and narrow: live audio lives here too. */
37
37
  foldLiveAudio?: boolean;
38
+ /** Turns the composer's Live audio on (the row is folded, so it lives here). */
39
+ onLiveAudio?: () => void;
38
40
  side: "top" | "bottom";
39
41
  /** The host's surface key — auto-clear keeps its display slot in step. */
40
42
  surfaceKey?: string;
@@ -45,7 +47,7 @@ interface ComposerPlusMenuProps {
45
47
  open?: boolean;
46
48
  onOpenChange?: (open: boolean) => void;
47
49
  }
48
- export declare function ComposerPlusMenu({ conversationId, surfaceKey, trigger, mode, folded, foldLiveAudio, side, onRequestInputExpand, presentation, open: openProp, onOpenChange, }: ComposerPlusMenuProps): import("react").JSX.Element;
50
+ export declare function ComposerPlusMenu({ conversationId, surfaceKey, trigger, mode, folded, foldLiveAudio, onLiveAudio, side, onRequestInputExpand, presentation, open: openProp, onOpenChange, }: ComposerPlusMenuProps): import("react").JSX.Element;
49
51
  /**
50
52
  * Environment (brief §6) — where the agent runs, as ONE flat list: Cloud, then
51
53
  * every computer and sandbox this person can use. One click chooses (Arman,
@@ -81,7 +81,6 @@ import {
81
81
  ComposerMenuSwitchRow,
82
82
  ComposerSubmenu
83
83
  } from "./ComposerMenu.js";
84
- import { announceComingSoon } from "../../../../../host/ui-slots.js";
85
84
  import { ComposerMenuSheet } from "./ComposerMenuSheet.js";
86
85
  import { composerShows } from "./composer-mode-visibility.js";
87
86
  import { useTouchOnlyDevice } from "../../../../../ui/composer/useTouchOnlyDevice.js";
@@ -101,6 +100,7 @@ function ComposerPlusMenu({
101
100
  mode,
102
101
  folded = false,
103
102
  foldLiveAudio = false,
103
+ onLiveAudio,
104
104
  side,
105
105
  onRequestInputExpand,
106
106
  presentation = "popover",
@@ -360,7 +360,7 @@ function ComposerPlusMenu({
360
360
  label: "Live audio",
361
361
  onClick: () => {
362
362
  close();
363
- void announceComingSoon("chat.live-audio");
363
+ onLiveAudio?.();
364
364
  }
365
365
  }
366
366
  ) : null
@@ -1,3 +1,4 @@
1
+ import type { SourceFeature } from "@ai-matrx/agents/generated/source-attribution";
1
2
  import type { AnyMandateKey } from "@ai-matrx/agents/mandates";
2
3
  import type { EditableContextMenuProps } from "../../../../../host/ui-slots.js";
3
4
  import type { ApplicationScope } from "../../../../types/scope.types.js";
@@ -83,4 +84,8 @@ export interface ComposerPresentation {
83
84
  maxInputHeightPx?: number;
84
85
  /** The right-click agent menu on the textarea. Absent = no menu. */
85
86
  textMenu?: ComposerTextMenu;
87
+ /** Who started a Live audio session here (attribution). Absent = "chat". */
88
+ sourceFeature?: SourceFeature;
89
+ /** Live audio starts switched on (the person already chose to talk, e.g. a voice interview). */
90
+ liveAudioStartsOn?: boolean;
86
91
  }
@@ -25,7 +25,7 @@ function AgentVariableInputCard({
25
25
  }) {
26
26
  const dispatch = useAppDispatch();
27
27
  const userValues = useAppSelector(selectUserVariableValues(conversationId));
28
- const currentValue = userValues[variable.name];
28
+ const currentValue = userValues[variable.name] ?? variable.defaultValue;
29
29
  const comp = variable.customComponent;
30
30
  const type = comp?.type ?? "textarea";
31
31
  const setValue = (value) => {
@@ -274,7 +274,7 @@ function AgentOptionsMenu({
274
274
  setOpen(false);
275
275
  } else if (label === "Create App") {
276
276
  console.log("[AGENT OPTIONS MENU] Creating app, Agent ID:", agentId);
277
- openCreateApp({ agentId: agentId ?? null });
277
+ openCreateApp();
278
278
  setOpen(false);
279
279
  } else if (label === "Add Data Storage Support") {
280
280
  console.log(
@@ -618,7 +618,7 @@ function MobileMenuContent({
618
618
  openFindUsages({ agentId: agentId ?? null });
619
619
  onClose();
620
620
  } else if (label === "Create App") {
621
- openCreateApp({ agentId: agentId ?? null });
621
+ openCreateApp();
622
622
  onClose();
623
623
  } else if (label === "Add Data Storage Support") {
624
624
  openDataStorage({ agentId: agentId ?? null });
@@ -3,6 +3,7 @@ import { createAsyncThunk } from "@reduxjs/toolkit";
3
3
  import { supabase } from "../../../../host/db.js";
4
4
  import { updateMessageRecord } from "../messages/messages.slice.js";
5
5
  import { extractFlatText } from "../messages/messages.selectors.js";
6
+ import { saveOutputFeedback } from "../../../../host/ui-slots.js";
6
7
  import {
7
8
  clearRequestEditedText,
8
9
  setRequestEditedText
@@ -165,12 +166,8 @@ const editMessage = createAsyncThunk(
165
166
  if (prevRecord.role === "assistant") {
166
167
  void (async () => {
167
168
  try {
168
- const [{ saveOutputFeedback }, { extractFlatText: extractFlatText2 }] = await Promise.all([
169
- import("../../../../host/ui-slots.js"),
170
- import("../messages/messages.selectors.js")
171
- ]);
172
- const originalText = extractFlatText2(prevRecord);
173
- const correctedText = extractFlatText2({
169
+ const originalText = extractFlatText(prevRecord);
170
+ const correctedText = extractFlatText({
174
171
  ...prevRecord,
175
172
  content: newContent
176
173
  });
@@ -1,6 +1,7 @@
1
1
  import { createAsyncThunk } from "@reduxjs/toolkit";
2
2
  import { forkConversation } from "./fork-conversation.thunk.js";
3
3
  import { mergeEditedText } from "./content-blocks.util.js";
4
+ import { toastDoor } from "../../../../host/ui-slots.js";
4
5
  const forkAndResubmitFromMessage = createAsyncThunk(
5
6
  "messages/forkAndResubmitFromMessage",
6
7
  async ({ conversationId, messageId, newContent, surfaceKey }, { dispatch, getState, rejectWithValue }) => {
@@ -48,7 +49,6 @@ const forkAndResubmitFromMessage = createAsyncThunk(
48
49
  );
49
50
  } else {
50
51
  const { toast } = await import("../../../../host/notify.js");
51
- const { toastDoor } = await import("../../../../host/ui-slots.js");
52
52
  toast.success("Branch created", {
53
53
  action: toastDoor("conversation", newConversationId)
54
54
  });
@@ -116,15 +116,13 @@ export interface ChatComputeTargetsSource {
116
116
  error: string | null;
117
117
  fetchInstances: (opts?: {
118
118
  limit?: number;
119
- [option: string]: unknown;
120
119
  }) => Promise<unknown>;
121
120
  createInstance: (request: {
122
121
  organization_id: string;
123
122
  template?: string;
124
- tier?: string;
123
+ tier: "ec2" | "hosted";
125
124
  ttl_seconds?: number;
126
125
  labels?: Record<string, string>;
127
- [field: string]: unknown;
128
126
  }) => Promise<{
129
127
  instance: SandboxInstance | null;
130
128
  error: string | null;
@@ -190,15 +188,13 @@ export declare const useSandboxInstances: () => {
190
188
  error: string | null;
191
189
  fetchInstances: (opts?: {
192
190
  limit?: number;
193
- [option: string]: unknown;
194
191
  }) => Promise<unknown>;
195
192
  createInstance: (request: {
196
193
  organization_id: string;
197
194
  template?: string;
198
- tier?: string;
195
+ tier: "ec2" | "hosted";
199
196
  ttl_seconds?: number;
200
197
  labels?: Record<string, string>;
201
- [field: string]: unknown;
202
198
  }) => Promise<{
203
199
  instance: SandboxInstance | null;
204
200
  error: string | null;
@@ -17,6 +17,7 @@
17
17
  * have no honest default and throw, naming themselves.
18
18
  */
19
19
  import type { ComponentType } from "react";
20
+ import type { AssociationSourceEdge, AssociationTargetEdge } from "@ai-matrx/associations";
20
21
  import type { ChatDatabase, Json } from "../../host/db-types.js";
21
22
  import { type AnyComponent, type AnyFn } from "../../host/registered-source.js";
22
23
  export type ContextItemRow = ChatDatabase["context"]["Tables"]["context_items"]["Row"];
@@ -61,7 +62,6 @@ export interface ScopeTreeOrganization {
61
62
  projects?: Array<{
62
63
  id: string;
63
64
  name: string;
64
- [field: string]: unknown;
65
65
  }>;
66
66
  }
67
67
  export interface ContextItemValue {
@@ -175,10 +175,10 @@ export interface AssociationEdgeRow {
175
175
  }
176
176
  export interface ChatAssociationsService {
177
177
  listForTargets(targetType: string, targetIds: string[], ...rest: any[]): Promise<ScopesRpcResult<{
178
- edges: AssociationEdgeRow[];
178
+ edges: AssociationTargetEdge[];
179
179
  }>>;
180
180
  listForSources(sourceType: string, sourceIds: string[], ...rest: any[]): Promise<ScopesRpcResult<{
181
- edges: AssociationEdgeRow[];
181
+ edges: AssociationSourceEdge[];
182
182
  }>>;
183
183
  add(input: any): Promise<ScopesRpcResult<any>>;
184
184
  remove(input: any): Promise<ScopesRpcResult<any>>;
@@ -27,7 +27,7 @@ export interface OpenAgentIdWindowOptions {
27
27
  agentId?: string | null;
28
28
  }
29
29
  export type OpenAgentAdminFindUsagesWindowOptions = OpenAgentIdWindowOptions;
30
- export type OpenAgentCreateAppWindowOptions = OpenAgentIdWindowOptions;
30
+ export type OpenAgentCreateAppWindowOptions = Record<string, never>;
31
31
  export type OpenAgentDataStorageWindowOptions = OpenAgentIdWindowOptions;
32
32
  export type OpenAgentFindUsagesWindowOptions = OpenAgentIdWindowOptions;
33
33
  export type OpenAgentInterfaceVariationsWindowOptions = OpenAgentIdWindowOptions;
@@ -222,7 +222,7 @@ export interface EditorPrimaryAction {
222
222
  label: string;
223
223
  variant?: "default" | "secondary" | "outline" | "destructive" | "ghost" | "link";
224
224
  }
225
- export interface OpenFullScreenMarkdownEditorOptions {
225
+ interface OpenFullScreenMarkdownEditorOptionsBase {
226
226
  instanceId?: string;
227
227
  content?: string;
228
228
  mode?: unknown;
@@ -236,14 +236,29 @@ export interface OpenFullScreenMarkdownEditorOptions {
236
236
  showSaveButton?: boolean;
237
237
  showCopyButton?: boolean;
238
238
  primaryActions?: EditorPrimaryAction[];
239
- onSave?: (content: string) => void | Promise<void>;
240
- onAction?: (action: string, content: string) => void | Promise<void>;
239
+ }
240
+ type OpenFullScreenMarkdownEditorHandlers = {
241
+ onSave: (content: string) => Promise<void>;
242
+ onAction?: never;
241
243
  onEvent?: (event: {
242
244
  type: "save";
243
245
  content: string;
244
246
  action?: string;
245
247
  }) => void | Promise<void>;
246
- }
248
+ } | {
249
+ onSave?: never;
250
+ onAction: (action: string, content: string) => Promise<void>;
251
+ onEvent?: (event: {
252
+ type: "save";
253
+ content: string;
254
+ action?: string;
255
+ }) => void | Promise<void>;
256
+ } | {
257
+ onSave?: undefined;
258
+ onAction?: undefined;
259
+ onEvent?: undefined;
260
+ };
261
+ export type OpenFullScreenMarkdownEditorOptions = OpenFullScreenMarkdownEditorOptionsBase & OpenFullScreenMarkdownEditorHandlers;
247
262
  export interface ChatFullScreenEditorHandle {
248
263
  instanceId: string;
249
264
  callbackGroupId: string | null;
@@ -361,3 +376,4 @@ export declare function useOpenTopicalMapWindow(): (opts: OpenTopicalMapWindowOp
361
376
  export declare function useOpenScratchpadPanel(): (opts?: OpenScratchpadPanelOptions) => ChatWindowHandle;
362
377
  export declare function useOpenWorkingDocumentPanel(): (opts: OpenWorkingDocumentPanelOptions) => ChatWindowHandle;
363
378
  export declare function useOpenWorkingDocumentWindow(): (opts: OpenWorkingDocumentWindowOptions) => ChatInstanceWindowHandle;
379
+ export {};
@@ -16,7 +16,13 @@ export interface VoiceRelayBarProps {
16
16
  /**
17
17
  * `bar` draws the standalone section chrome. `toolbar` embeds the same
18
18
  * controls in an existing toolbar without adding another layout row.
19
+ * `composer` is the Smart Agent Input's "Live audio": idle it is ONE icon
20
+ * button beside the mic (no word, no extra width); pressed, the same live
21
+ * controls as `toolbar` (Arman, 2026-10-07: one voice control per surface).
19
22
  */
20
- variant?: "bar" | "toolbar";
23
+ variant?: "bar" | "toolbar" | "composer";
24
+ /** Controlled on/off (the composer's + menu turns it on when the row is folded). */
25
+ enabled?: boolean;
26
+ onEnabledChange?: (enabled: boolean) => void;
21
27
  }
22
28
  export declare function VoiceRelayBar(props: VoiceRelayBarProps): import("react").JSX.Element;
@@ -1,6 +1,9 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useState } from "react";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { useAppSelector } from "../../store/hooks.js";
5
+ import { cn } from "@ai-matrx/design-system";
6
+ import { selectVoiceInstructions } from "../state/selectors.js";
4
7
  import { Mic, Square, AudioLines } from "lucide-react";
5
8
  import { Button } from "@ai-matrx/design-system/controls";
6
9
  import { useMandate } from "../../mandates/useMandate.js";
@@ -16,7 +19,10 @@ function ActiveVoiceRelay({
16
19
  conversationId,
17
20
  surfaceKey,
18
21
  sourceFeature,
19
- questionPacing
22
+ questionPacing,
23
+ onEnd,
24
+ autoStart,
25
+ variant
20
26
  }) {
21
27
  const relay = useVoiceRelaySession({
22
28
  communicatorAgentId,
@@ -27,19 +33,26 @@ function ActiveVoiceRelay({
27
33
  questionPacing
28
34
  });
29
35
  const live = relay.status !== "idle" && relay.status !== "error";
36
+ const instructionsReady = useAppSelector(
37
+ (state) => (selectVoiceInstructions(state, relay.instanceId) ?? "").trim().length > 0
38
+ );
39
+ const started = useRef(false);
40
+ useEffect(() => {
41
+ if (!autoStart || !instructionsReady || started.current) return;
42
+ started.current = true;
43
+ relay.toggle();
44
+ }, [autoStart, instructionsReady, relay]);
30
45
  return /* @__PURE__ */ jsxs(Fragment, { children: [
31
46
  /* @__PURE__ */ jsx(
32
47
  Button,
33
48
  {
34
49
  variant: live ? "danger" : "primary",
35
- onClick: relay.toggle,
36
- children: live ? /* @__PURE__ */ jsxs(Fragment, { children: [
37
- /* @__PURE__ */ jsx(Square, { className: "mr-1.5 h-3.5 w-3.5" }),
38
- " End voice"
39
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
40
- /* @__PURE__ */ jsx(Mic, { className: "mr-1.5 h-3.5 w-3.5" }),
41
- " Talk"
42
- ] })
50
+ icon: live ? /* @__PURE__ */ jsx(Square, {}) : /* @__PURE__ */ jsx(Mic, {}),
51
+ onClick: () => {
52
+ relay.toggle();
53
+ if (live) onEnd?.();
54
+ },
55
+ children: live ? "End voice" : "Talk"
43
56
  }
44
57
  ),
45
58
  live ? (
@@ -53,33 +66,86 @@ function ActiveVoiceRelay({
53
66
  }
54
67
  )
55
68
  ) : null,
56
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: relay.error ? relay.error.message : relay.brainBusy ? "thinking\u2026" : relay.status })
69
+ relay.error || relay.brainBusy || relay.status !== "idle" ? (
70
+ // In the composer's row it is ONE line: the full sentence on hover.
71
+ /* @__PURE__ */ jsx(
72
+ "span",
73
+ {
74
+ className: cn(
75
+ "text-xs",
76
+ relay.error ? "text-destructive" : "text-muted-foreground",
77
+ variant === "composer" && "min-w-0 max-w-[14rem] truncate"
78
+ ),
79
+ title: relay.error?.message,
80
+ children: relay.error ? variant === "composer" ? "Voice did not start" : relay.error.message : relay.brainBusy ? "thinking\u2026" : relay.status
81
+ }
82
+ )
83
+ ) : null
57
84
  ] });
58
85
  }
59
86
  function VoiceRelayBar(props) {
60
- const [enabled, setEnabled] = useState(false);
61
- const communicator = useMandate(VOICE_COMMUNICATOR_MANDATE_KEY);
87
+ const [ownEnabled, setOwnEnabled] = useState(false);
88
+ const enabled = props.enabled ?? ownEnabled;
89
+ const setEnabled = (next) => {
90
+ if (props.enabled === void 0) setOwnEnabled(next);
91
+ props.onEnabledChange?.(next);
92
+ };
93
+ if (!enabled && props.variant === "composer") {
94
+ return /* @__PURE__ */ jsx(
95
+ Button,
96
+ {
97
+ variant: "quiet",
98
+ icon: /* @__PURE__ */ jsx(AudioLines, {}),
99
+ "aria-label": "Live audio",
100
+ title: "Live audio \u2014 talk and hear the answer",
101
+ onClick: () => setEnabled(true)
102
+ }
103
+ );
104
+ }
62
105
  return /* @__PURE__ */ jsx(
63
106
  "div",
64
107
  {
65
- className: props.variant === "toolbar" ? "flex items-center gap-1" : "flex items-center gap-2 border-b border-border px-3 py-1.5",
66
- children: !enabled ? /* @__PURE__ */ jsx(Button, { icon: /* @__PURE__ */ jsx(AudioLines, {}), variant: "quiet", onClick: () => setEnabled(true), children: " Voice" }) : communicator.loading ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Connecting the voice layer\u2026" }) : communicator.error || !communicator.mandate ? (
67
- // An unresolvable mandate REFUSES loudly — no fallback persona, ever.
68
- /* @__PURE__ */ jsxs("span", { className: "text-xs text-destructive", children: [
69
- "Voice is unavailable: ",
70
- communicator.error ?? "no Communicator bound",
71
- /* @__PURE__ */ jsx(ErrorAlchemyMenu, { error: communicator.error })
72
- ] })
73
- ) : /* @__PURE__ */ jsx(
74
- ActiveVoiceRelay,
108
+ className: props.variant === "toolbar" || props.variant === "composer" ? "flex items-center gap-1" : "flex items-center gap-2 border-b border-border px-3 py-1.5",
109
+ children: !enabled ? /* @__PURE__ */ jsx(Button, { icon: /* @__PURE__ */ jsx(AudioLines, {}), variant: "quiet", onClick: () => setEnabled(true), children: " Voice" }) : /* @__PURE__ */ jsx(
110
+ EnabledVoiceRelay,
75
111
  {
76
112
  ...props,
77
- communicatorAgentId: communicator.mandate.agentId
113
+ onEnd: props.variant === "composer" ? () => setEnabled(false) : void 0
78
114
  }
79
115
  )
80
116
  }
81
117
  );
82
118
  }
119
+ function EnabledVoiceRelay(props) {
120
+ const communicator = useMandate(VOICE_COMMUNICATOR_MANDATE_KEY);
121
+ if (communicator.loading) {
122
+ return /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-muted-foreground", children: "Connecting the voice layer\u2026" });
123
+ }
124
+ if (communicator.error || !communicator.mandate) {
125
+ return /* @__PURE__ */ jsxs(
126
+ "span",
127
+ {
128
+ className: cn(
129
+ "flex min-w-0 items-center gap-1 text-xs text-destructive",
130
+ props.variant === "composer" && "max-w-[14rem]"
131
+ ),
132
+ title: `Voice is unavailable: ${communicator.error ?? "no Communicator bound"}`,
133
+ children: [
134
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: props.variant === "composer" ? "Voice is unavailable" : `Voice is unavailable: ${communicator.error ?? "no Communicator bound"}` }),
135
+ /* @__PURE__ */ jsx(ErrorAlchemyMenu, { error: communicator.error })
136
+ ]
137
+ }
138
+ );
139
+ }
140
+ return /* @__PURE__ */ jsx(
141
+ ActiveVoiceRelay,
142
+ {
143
+ ...props,
144
+ communicatorAgentId: communicator.mandate.agentId,
145
+ autoStart: props.variant === "composer"
146
+ }
147
+ );
148
+ }
83
149
  export {
84
150
  VoiceRelayBar
85
151
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/chat",
3
- "version": "0.3.6",
3
+ "version": "0.3.9",
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",
@@ -202,23 +202,23 @@
202
202
  "uuid": "^14.0.2",
203
203
  "zod": "^4.6.5",
204
204
  "@ai-matrx/agents": "latest",
205
- "@ai-matrx/associations": "latest",
206
205
  "@ai-matrx/alchemy": "latest",
207
- "@ai-matrx/browser-audio": "latest",
206
+ "@ai-matrx/associations": "latest",
208
207
  "@ai-matrx/canvas": "latest",
208
+ "@ai-matrx/browser-audio": "latest",
209
209
  "@ai-matrx/content-ir-react": "latest",
210
- "@ai-matrx/diff": "latest",
210
+ "@ai-matrx/content-ir": "latest",
211
211
  "@ai-matrx/data": "latest",
212
- "@ai-matrx/icons": "latest",
213
212
  "@ai-matrx/design-system": "latest",
214
- "@ai-matrx/media": "latest",
213
+ "@ai-matrx/diff": "latest",
214
+ "@ai-matrx/icons": "latest",
215
215
  "@ai-matrx/kit": "latest",
216
- "@ai-matrx/print": "latest",
216
+ "@ai-matrx/media": "latest",
217
217
  "@ai-matrx/realtime": "latest",
218
218
  "@ai-matrx/records": "latest",
219
219
  "@ai-matrx/records-ui": "latest",
220
220
  "@ai-matrx/tap-target": "latest",
221
- "@ai-matrx/content-ir": "latest"
221
+ "@ai-matrx/print": "latest"
222
222
  },
223
223
  "devDependencies": {
224
224
  "@arethetypeswrong/cli": "^0.18.5",