@ai-matrx/chat 0.3.5 → 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.
Files changed (38) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/agents/components/chat/ChatNewClient.d.ts +6 -1
  3. package/dist/agents/components/chat/ChatNewClient.js +10 -3
  4. package/dist/agents/components/chat/begin-fresh-chat.d.ts +2 -0
  5. package/dist/agents/components/chat/begin-fresh-chat.js +18 -1
  6. package/dist/agents/components/inputs/smart-input/InputActionButtons.d.ts +4 -0
  7. package/dist/agents/components/inputs/smart-input/InputActionButtons.js +16 -9
  8. package/dist/agents/components/inputs/smart-input/RunControlsMenu.d.ts +2 -0
  9. package/dist/agents/components/inputs/smart-input/RunControlsMenu.js +2 -0
  10. package/dist/agents/components/inputs/smart-input/SmartAgentInputStacked.js +1 -1
  11. package/dist/agents/components/inputs/smart-input/composer/ComposerAgentPeek.js +1 -1
  12. package/dist/agents/components/inputs/smart-input/composer/ComposerAgentPill.js +4 -6
  13. package/dist/agents/components/inputs/smart-input/composer/ComposerOutput.js +9 -4
  14. package/dist/agents/components/inputs/smart-input/composer/ComposerPlusMenu.d.ts +3 -1
  15. package/dist/agents/components/inputs/smart-input/composer/ComposerPlusMenu.js +2 -2
  16. package/dist/agents/components/inputs/smart-input/composer/composer-types.d.ts +5 -0
  17. package/dist/agents/components/inputs/variable-input-variations/AgentVariableCards.js +1 -1
  18. package/dist/agents/components/shared/AgentOptionsMenu.js +2 -2
  19. package/dist/agents/components/shared/AgentPeekDuplicateButton.d.ts +6 -3
  20. package/dist/agents/components/shared/AgentPeekDuplicateButton.js +8 -7
  21. package/dist/agents/redux/agent-definition/slice.d.ts +16 -1
  22. package/dist/agents/redux/agent-definition/slice.js +20 -0
  23. package/dist/agents/redux/execution-system/message-crud/edit-message.thunk.js +3 -6
  24. package/dist/agents/redux/execution-system/message-crud/fork-and-resubmit-from-message.thunk.js +1 -1
  25. package/dist/agents/types/agent-definition.types.d.ts +29 -1
  26. package/dist/compute/targets.d.ts +2 -6
  27. package/dist/context/sources/scopes.d.ts +3 -3
  28. package/dist/host/ui-slots.js +1 -1
  29. package/dist/host/window-openers.d.ts +21 -5
  30. package/dist/surfaces/lib/ui-surfaces-agent-writes.d.ts +61 -0
  31. package/dist/surfaces/lib/ui-surfaces-agent-writes.js +305 -0
  32. package/dist/surfaces/lib/ui-surfaces-scope.d.ts +40 -0
  33. package/dist/surfaces/lib/ui-surfaces-scope.js +116 -0
  34. package/dist/tool-call-visualization/effects/toolStateEffects.d.ts +1 -1
  35. package/dist/tool-call-visualization/registry/registry.js +0 -2
  36. package/dist/voice-agent/relay/VoiceRelayBar.d.ts +7 -1
  37. package/dist/voice-agent/relay/VoiceRelayBar.js +90 -24
  38. package/package.json +5 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
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
+
23
+ ## 0.3.6 — 2026-10-07
24
+ ### Re-sync before matrx-frontend consumes the published package (P27)
25
+
26
+ - `src/surfaces/lib/ui-surfaces-agent-writes` + `ui-surfaces-scope` ship at last: the repo-root `lib/`
27
+ ignore (Python) kept them out of every commit since 0.3.1; `apps/shared/chat/.gitignore` re-includes
28
+ `src/**/lib/`.
29
+ - Carries every matrx-frontend `packages/chat` edit after `62656d3a69` (begin-fresh chat, agent-definition
30
+ `markFieldSaved`, tool-state effects/registry, ALC-19 copy results).
31
+ - Consumer action: none.
32
+
3
33
  ## 0.3.5 — 2026-10-07
4
34
  ### Chat mode opens the agent itself; Duplicate opens the copy in a new tab
5
35
 
@@ -18,8 +18,13 @@ import type { ComposerMode } from "../inputs/smart-input/composer/composer-types
18
18
  *
19
19
  * The quick actions are the `agents.chat_composer.quick_actions` knob.
20
20
  */
21
- export declare function ChatNewClient({ agentId, composer, }: {
21
+ export declare function ChatNewClient({ agentId, pinned, composer, }: {
22
22
  agentId: string | null;
23
+ /**
24
+ * `/chat/new?agent=<id>`: the person named the agent, so it answers directly
25
+ * and the default-chat mandate neither seeds nor overrides it.
26
+ */
27
+ pinned?: boolean;
23
28
  /** The three-mode composer (server-read "last mode used" cookie). */
24
29
  composer: {
25
30
  initialMode: ComposerMode | null;
@@ -18,9 +18,15 @@ import { asClause } from "@ai-matrx/kit/text";
18
18
  import { WorkspaceGate } from "../../../host/ui-slots.js";
19
19
  function ChatNewClient({
20
20
  agentId,
21
+ pinned = false,
21
22
  composer
22
23
  }) {
23
- return agentId ? /* @__PURE__ */ jsx(ChatNewBody, { agentId, composer }) : /* @__PURE__ */ jsx(ChatNewClientResolved, { composer });
24
+ const { mandate } = useMandate(DEFAULT_NEW_CHAT_MANDATE_KEY, { optional: true });
25
+ if (pinned && agentId) {
26
+ return /* @__PURE__ */ jsx(ChatNewBody, { agentId, composer, pinned: true });
27
+ }
28
+ const effectiveAgentId = mandate?.agentId ?? agentId;
29
+ return effectiveAgentId ? /* @__PURE__ */ jsx(ChatNewBody, { agentId: effectiveAgentId, composer }) : /* @__PURE__ */ jsx(ChatNewClientResolved, { composer });
24
30
  }
25
31
  function ChatNewClientResolved({
26
32
  composer
@@ -62,14 +68,15 @@ function ChatMandateUnavailable({ error }) {
62
68
  }
63
69
  function ChatNewBody({
64
70
  agentId,
65
- composer
71
+ composer,
72
+ pinned = false
66
73
  }) {
67
74
  const surfaceKey = chatRouteSurfaceKey(agentId);
68
75
  return /* @__PURE__ */ jsx(
69
76
  ChatRoomClient,
70
77
  {
71
78
  agentId,
72
- mandateKey: DEFAULT_NEW_CHAT_MANDATE_KEY,
79
+ mandateKey: pinned ? void 0 : DEFAULT_NEW_CHAT_MANDATE_KEY,
73
80
  composer,
74
81
  landingContent: (conversationId, composerPresentation) => composerPresentation ? /* @__PURE__ */ jsx(
75
82
  NewChatGreeting,
@@ -50,6 +50,8 @@ export declare function stageChatAgentSwitch({ dispatch, router, getState, targe
50
50
  }): void;
51
51
  /** Catch package-owned chat links before they bypass the same-tab handoff. */
52
52
  export declare function interceptChatAgentLink(event: ReactMouseEvent<HTMLElement>, args: Omit<Parameters<typeof stageChatAgentSwitch>[0], "targetAgentId" | "href">): void;
53
+ /** How long "New chat" waits for the default-agent lookup before navigating anyway. */
54
+ export declare const DEFAULT_AGENT_LOOKUP_WAIT_MS = 1500;
53
55
  /** Start a brand-new chat: drop stale surface focus, bump the fresh-session
54
56
  * nonce (so `/chat/new` remints even when the path is unchanged), navigate.
55
57
  *
@@ -92,6 +92,22 @@ function interceptChatAgentLink(event, args) {
92
92
  href
93
93
  });
94
94
  }
95
+ const DEFAULT_AGENT_LOOKUP_WAIT_MS = 1500;
96
+ async function resolveDefaultAgentBounded() {
97
+ let timer;
98
+ const lookup = resolveMandateAsking(DEFAULT_NEW_CHAT_MANDATE_KEY).then(
99
+ (resolved) => resolved.agentId
100
+ );
101
+ lookup.catch(() => void 0);
102
+ const wait = new Promise((resolve) => {
103
+ timer = setTimeout(() => resolve(null), DEFAULT_AGENT_LOOKUP_WAIT_MS);
104
+ });
105
+ try {
106
+ return await Promise.race([lookup, wait]);
107
+ } finally {
108
+ clearTimeout(timer);
109
+ }
110
+ }
95
111
  async function beginFreshChat({
96
112
  dispatch,
97
113
  router,
@@ -103,7 +119,7 @@ async function beginFreshChat({
103
119
  const auth = state.userAuth;
104
120
  if (auth && auth.id !== null && !auth.isAnonymous) {
105
121
  try {
106
- defaultAgentId = (await resolveMandateAsking(DEFAULT_NEW_CHAT_MANDATE_KEY)).agentId;
122
+ defaultAgentId = await resolveDefaultAgentBounded();
107
123
  } catch (error) {
108
124
  if (isOrganizationSelectionCancelled(error)) return;
109
125
  console.error(
@@ -125,6 +141,7 @@ async function beginFreshChat({
125
141
  router.push(getFreshChatHref(activeAgentId, defaultAgentId));
126
142
  }
127
143
  export {
144
+ DEFAULT_AGENT_LOOKUP_WAIT_MS,
128
145
  beginFreshChat,
129
146
  chatRouteSurfaceKey,
130
147
  getFreshChatHref,
@@ -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",
@@ -41,7 +41,7 @@ function ComposerAgentPeek({ agentId, onDone }) {
41
41
  AgentDetailCard,
42
42
  {
43
43
  agent,
44
- actions: /* @__PURE__ */ jsx(AgentPeekDuplicateButton, { agentId, onDuplicated: onDone })
44
+ actions: /* @__PURE__ */ jsx(AgentPeekDuplicateButton, { agentId, onStart: onDone })
45
45
  }
46
46
  );
47
47
  }
@@ -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 });
@@ -1,5 +1,8 @@
1
- export declare function AgentPeekDuplicateButton({ agentId, onDuplicated, }: {
1
+ export declare function AgentPeekDuplicateButton({ agentId, onStart, }: {
2
2
  agentId: string;
3
- /** Close the peek once the copy opens. */
4
- onDuplicated?: () => void;
3
+ /**
4
+ * Close the peek as the copy starts: the organization question (when none is
5
+ * chosen) and the new tab must never open behind a menu.
6
+ */
7
+ onStart?: () => void;
5
8
  }): import("react").JSX.Element | null;
@@ -13,7 +13,7 @@ import { selectAgentById, selectAgentReadyForBuilder } from "../../redux/agent-d
13
13
  import { agentGoHref } from "../../addressing/agentAddress.js";
14
14
  function AgentPeekDuplicateButton({
15
15
  agentId,
16
- onDuplicated
16
+ onStart
17
17
  }) {
18
18
  const dispatch = useAppDispatch();
19
19
  const userId = useAppSelector(selectUserId);
@@ -27,20 +27,21 @@ function AgentPeekDuplicateButton({
27
27
  const duplicate = async () => {
28
28
  if (busy) return;
29
29
  setBusy(true);
30
- const tab = window.open("about:blank", "_blank");
30
+ onStart?.();
31
31
  try {
32
32
  const newId = await dispatch(door.duplicateAgent(agentId)).unwrap();
33
33
  const href = agentGoHref(newId, "/build");
34
+ const tab = window.open(href, "_blank");
34
35
  if (tab) {
35
36
  tab.opener = null;
36
- tab.location.href = href;
37
+ toast.success(`Duplicated "${record.name}" \u2014 opened in a new tab`);
37
38
  } else {
38
- window.open(href, "_blank", "noopener");
39
+ toast.success(`Duplicated "${record.name}"`, {
40
+ description: "Your browser kept the new tab closed",
41
+ action: { label: "Open", onClick: () => window.open(href, "_blank", "noopener") }
42
+ });
39
43
  }
40
- toast.success(`Duplicated "${record.name}" \u2014 opened in a new tab`);
41
- onDuplicated?.();
42
44
  } catch (err) {
43
- tab?.close();
44
45
  if (!isOrganizationSelectionCancelled(err)) {
45
46
  toast.error("Could not duplicate agent", { description: getUserMessage(err) });
46
47
  }
@@ -121,6 +121,18 @@ export declare const agentDefinitionSlice: import("@reduxjs/toolkit").Slice<Agen
121
121
  markAgentSaved(state: AgentDefinitionSliceState, action: PayloadAction<{
122
122
  id: string;
123
123
  }>): void;
124
+ /**
125
+ * A SINGLE field was written to the database (inline edits such as the
126
+ * Agent Info Editor's name). Only that field becomes the clean baseline —
127
+ * every other staged edit (e.g. an Output Schema kind binding applied in
128
+ * Model Settings) must stay dirty so the main Save still persists it.
129
+ * `markAgentSaved` here used to wipe every dirty flag, silently dropping
130
+ * unrelated staged edits (feedback 222a2925).
131
+ */
132
+ markAgentFieldSaved(state: AgentDefinitionSliceState, action: PayloadAction<{
133
+ id: string;
134
+ field: keyof AgentDefinition;
135
+ }>): void;
124
136
  /** Save failed — restore from the snapshot taken before the optimistic write. */
125
137
  rollbackAgentOptimisticUpdate(state: AgentDefinitionSliceState, action: PayloadAction<{
126
138
  id: string;
@@ -227,7 +239,10 @@ export declare const upsertAgent: import("@reduxjs/toolkit").ActionCreatorWithPa
227
239
  id: string;
228
240
  }, "agentDefinition/resetAllAgentFields">, markAgentSaved: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
229
241
  id: string;
230
- }, "agentDefinition/markAgentSaved">, rollbackAgentOptimisticUpdate: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
242
+ }, "agentDefinition/markAgentSaved">, markAgentFieldSaved: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
243
+ id: string;
244
+ field: keyof AgentDefinition;
245
+ }, "agentDefinition/markAgentFieldSaved">, rollbackAgentOptimisticUpdate: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
231
246
  id: string;
232
247
  snapshot: FieldSnapshot;
233
248
  }, "agentDefinition/rollbackAgentOptimisticUpdate">, undoAgentEdit: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
@@ -493,6 +493,24 @@ const agentDefinitionSlice = createSlice({
493
493
  if (!record) return;
494
494
  markRecordClean(record);
495
495
  },
496
+ /**
497
+ * A SINGLE field was written to the database (inline edits such as the
498
+ * Agent Info Editor's name). Only that field becomes the clean baseline —
499
+ * every other staged edit (e.g. an Output Schema kind binding applied in
500
+ * Model Settings) must stay dirty so the main Save still persists it.
501
+ * `markAgentSaved` here used to wipe every dirty flag, silently dropping
502
+ * unrelated staged edits (feedback 222a2925).
503
+ */
504
+ markAgentFieldSaved(state, action) {
505
+ const { id, field } = action.payload;
506
+ const record = state.agents[id];
507
+ if (!record) return;
508
+ removeField(record._dirtyFields, field);
509
+ delete record._fieldHistory[field];
510
+ record._undoPast = record._undoPast.filter((e) => e.field !== field);
511
+ record._undoFuture = record._undoFuture.filter((e) => e.field !== field);
512
+ record._dirty = fieldFlagsSize(record._dirtyFields) > 0;
513
+ },
496
514
  /** Save failed — restore from the snapshot taken before the optimistic write. */
497
515
  rollbackAgentOptimisticUpdate(state, action) {
498
516
  const { id, snapshot } = action.payload;
@@ -636,6 +654,7 @@ const {
636
654
  resetAgentField,
637
655
  resetAllAgentFields,
638
656
  markAgentSaved,
657
+ markAgentFieldSaved,
639
658
  rollbackAgentOptimisticUpdate,
640
659
  undoAgentEdit,
641
660
  redoAgentEdit,
@@ -655,6 +674,7 @@ export {
655
674
  agentDefinitionSlice,
656
675
  clearAgentUndoHistory,
657
676
  slice_default as default,
677
+ markAgentFieldSaved,
658
678
  markAgentFieldsLoaded,
659
679
  markAgentSaved,
660
680
  mergePartialAgent,
@@ -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
  });
@@ -564,6 +564,32 @@ export interface LinkedAgentRef {
564
564
  * silently reported as "not linked".
565
565
  */
566
566
  deletedAt: string | null;
567
+ /** Archived agents are not offered as a sync partner. */
568
+ isArchived: boolean;
569
+ /**
570
+ * Who owns it, from the viewer's seat: "me", a person, an organization (an
571
+ * org-visible agent of someone else in an org the viewer can read) or the
572
+ * system. Drives every owner word in Linked Agent Sync.
573
+ */
574
+ ownerKind: "me" | "person" | "organization" | "system";
575
+ /** Person display name or organization name; null when not readable ("Someone else"). */
576
+ ownerName: string | null;
577
+ /** The viewer's level from `agx_get_access_level` (owner/admin/editor/viewer/public/none). */
578
+ accessLevel: string;
579
+ /**
580
+ * The builder's own save rule for a user agent: owner or editor. Always false
581
+ * for a system agent — that write is the registered admin feature, decided by
582
+ * the panel from `agent.system-sync`.
583
+ */
584
+ canEdit: boolean;
585
+ }
586
+ /**
587
+ * The agent this one was made from, when it still exists by id but the viewer
588
+ * can no longer open it (unshared, made private, archived, or deleted).
589
+ */
590
+ export interface LostLinkedSource {
591
+ id: string;
592
+ reason: "unreadable" | "archived" | "deleted";
567
593
  }
568
594
  /**
569
595
  * Resolved linkage around a single agent. `source` is what it was copied from;
@@ -574,8 +600,10 @@ export interface LinkedCounterpartResult {
574
600
  self: LinkedAgentRef;
575
601
  source: LinkedAgentRef | null;
576
602
  derived: LinkedAgentRef[];
603
+ /** Set when `self.sourceAgentId` names an agent the viewer can no longer open. */
604
+ lostSource: LostLinkedSource | null;
577
605
  }
578
- /** Result of `createPersonalCopy` — idempotent personal copy of a system agent. */
606
+ /** Result of `createPersonalCopy` — idempotent personal copy of any agent the viewer can read. */
579
607
  export interface PersonalCopyResult {
580
608
  agentId: string;
581
609
  /** True when a personal copy already existed and was returned instead of created. */