@burdenoff/fe-libs 2026.1015.1 → 2026.1015.2

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.
@@ -1,6 +1,7 @@
1
1
  import { VoiceTurnTiming } from '../turnTimeline';
2
2
  import { VoiceGraceMs } from '../voice/handsFree';
3
3
  import { VoiceSessionSnapshot } from '../voice/voiceSession';
4
+ import { VoiceReply } from '../voice/voiceReply';
4
5
  /**
5
6
  * A problem worth TELLING the user about that does not warrant killing the
6
7
  * session.
@@ -21,6 +22,18 @@ export type VoiceSessionNotice = "network" | "not-capturing" | "nothing-heard" |
21
22
  * transcript.
22
23
  */
23
24
  | "reply-not-heard";
25
+ /**
26
+ * ★ BOFF-9207: why the turn just spoken ended WITHOUT an answer - a refusal
27
+ * before it started ("No active subscription covers ..."), or a failure. The
28
+ * text is the server's own reason (already in the person's language), shown as
29
+ * given; it is never spoken, so no new speech is paid for.
30
+ */
31
+ export interface VoiceTurnRefusalNotice {
32
+ readonly kind: "turn-refused";
33
+ readonly text: string;
34
+ }
35
+ /** Everything the voice stage can show in its notice slot. */
36
+ export type VoiceSessionNoticeState = VoiceSessionNotice | VoiceTurnRefusalNotice;
24
37
  /**
25
38
  * A line the PANEL says in the voice conversation, outside any turn (BOFF-7803
26
39
  * FE9: the readback of a change that waits for its button).
@@ -69,11 +82,13 @@ interface Options {
69
82
  * Id + text of the newest assistant reply. A CHANGE of id is what advances
70
83
  * the session out of `thinking` — the text alone is not enough, because two
71
84
  * consecutive replies can be identical.
85
+ *
86
+ * ★ BOFF-9207: `refused` is set when the turn ended without an answer (a
87
+ * refusal or a failure) and `text` is the reason. Such a turn has nothing
88
+ * queued for speech, so it is shown, not spoken, and never enters `speaking`.
89
+ * Absent for a real answer, so its object is exactly what it was.
72
90
  */
73
- latestReply: {
74
- id: string;
75
- text: string;
76
- } | null;
91
+ latestReply: VoiceReply | null;
77
92
  /** Language the user is speaking, so the recogniser is pointed correctly. */
78
93
  language: string;
79
94
  /**
@@ -130,7 +145,7 @@ export interface VoiceSubmitMeta {
130
145
  export declare function useVoiceSession({ onSubmit, latestReply, language, graceMs: graceChoice, onSupersede, handsFreeInterrupt, handsFreeStreamNoticeAccepted, onAcceptHandsFreeStreamNotice, onBargeIn, }: Options): {
131
146
  session: Readonly<VoiceSessionSnapshot>;
132
147
  /** A recoverable problem worth showing, or null when all is well. */
133
- notice: VoiceSessionNotice | null;
148
+ notice: VoiceSessionNoticeState | null;
134
149
  /**
135
150
  * The reply as it is being spoken, chunk by chunk — for the live caption.
136
151
  *
@@ -146,16 +146,23 @@ function Te({ onSubmit: Te, latestReply: f, language: p, graceMs: Ee, onSupersed
146
146
  let Ue = u("");
147
147
  _.phase === "listening" && (Ue.current = o(_)), l(() => {
148
148
  if (_.phase !== "thinking") return;
149
- x(!1), V();
149
+ x(!1), V(), j((e) => typeof e == "object" && e ? null : e);
150
150
  let e = Ue.current.trim();
151
151
  e && Te(e, Q.current ?? `vs-orphan-${Date.now().toString(36)}`, E.current, { correction: L.current.correcting }), E.current = {}, D.current = i;
152
152
  }, [_.phase]), l(() => {
153
153
  let e = ce(f?.id, _.phase, O.current, pe(f?.id));
154
- e.claim && (O.current = f?.id ?? null, e.advance && f && v({
154
+ e.claim && (O.current = f?.id ?? null, e.advance && f && (f.refused ? (V(), j({
155
+ kind: "turn-refused",
156
+ text: f.text
157
+ }), v({ type: "turn-refused" })) : v({
155
158
  type: "reply",
156
159
  text: f.text
157
- }));
158
- }, [f, _.phase]), l(() => {
160
+ })));
161
+ }, [
162
+ f,
163
+ _.phase,
164
+ V
165
+ ]), l(() => {
159
166
  if (S === null || _.phase !== "thinking" || O.current === S) return;
160
167
  if (pe(S)) {
161
168
  C();
@@ -2,7 +2,7 @@ import { FC, ReactNode } from 'react';
2
2
  import { AssistantAttachment } from '../../attachments';
3
3
  import { HandsFreePrivacyNoticeActions } from '../../voice/handsfree/useHandsFreeInterrupt';
4
4
  import { VoiceSessionSnapshot } from '../../voice/voiceSession';
5
- import { VoiceSessionNotice } from '../useVoiceSession';
5
+ import { VoiceSessionNoticeState } from '../useVoiceSession';
6
6
  import { VoiceTranscriptTurn } from '../../voice/turnVisibility';
7
7
  /** How much room the voice session takes: a strip, the panel, or the screen. */
8
8
  export type VoiceView = "mini" | "panel" | "full";
@@ -17,7 +17,7 @@ interface Props {
17
17
  /** Cut the assistant off mid-reply. */
18
18
  onInterrupt?: () => void;
19
19
  /** A recoverable problem to surface. Null when the microphone is healthy. */
20
- notice?: VoiceSessionNotice | null;
20
+ notice?: VoiceSessionNoticeState | null;
21
21
  /**
22
22
  * ★ BOFF-8613 H3: the backend speech stream's privacy notice, while it must
23
23
  * be answered before the microphone opens. Null otherwise (always, unless an
@@ -1,40 +1,40 @@
1
1
  import { useReturnFocus as e } from "../../../hooks/useReturnFocus.js";
2
2
  import { useAssistantControl as t } from "../../appControls/useAssistantControl.js";
3
3
  import { useAssistantTr as n } from "../../i18n.js";
4
- import { micIsInterrupt as r, showsAssistantCaption as ee, voiceStageState as te } from "../../voice/voiceStageState.js";
5
- import { VOICE_THINKING_LABEL as i, composeVoiceStatus as ne, voiceStatusSuffix as re } from "../../voice/voiceStatus.js";
6
- import { spokenSoFar as ie } from "../../voice/voiceSession.js";
7
- import { useAvatarCopy as ae } from "../../avatar/useAvatarCopy.js";
8
- import { ASSISTANT_PANEL_ATTR as oe, ASSISTANT_VOICE_SESSION_ATTR as se } from "../panelOverlays.js";
9
- import { useLiveAvatarHandsFreeGuard as ce, useLiveAvatarTab as le, useLiveAvatarVoiceView as ue } from "../assistantLiveAvatarSeam.js";
10
- import { AssistantAttachmentButton as de } from "../composer/AssistantAttachmentButton.js";
11
- import { AssistantAttachmentChips as fe } from "../composer/AssistantAttachmentChips.js";
12
- import { AssistantVoiceOrb as pe } from "./AssistantVoiceOrb.js";
13
- import { VoiceMiniController as me } from "./VoiceMiniController.js";
14
- import { VoiceStage as he } from "./VoiceStage.js";
15
- import { VoiceTranscriptDrawer as ge } from "./VoiceTranscriptDrawer.js";
16
- import { escapeBelongsToLayerAbove as _e, voiceSessionEscapeAction as ve } from "./voiceSessionEscape.js";
17
- import { HandsFreePrivacyNotice as ye } from "./HandsFreePrivacyNotice.js";
18
- import { useCallback as a, useEffect as o, useMemo as be, useRef as s, useState as c } from "react";
4
+ import { micIsInterrupt as r, showsAssistantCaption as ee, voiceStageState as i } from "../../voice/voiceStageState.js";
5
+ import { VOICE_THINKING_LABEL as a, composeVoiceStatus as te, voiceStatusSuffix as ne } from "../../voice/voiceStatus.js";
6
+ import { spokenSoFar as re } from "../../voice/voiceSession.js";
7
+ import { useAvatarCopy as ie } from "../../avatar/useAvatarCopy.js";
8
+ import { ASSISTANT_PANEL_ATTR as ae, ASSISTANT_VOICE_SESSION_ATTR as oe } from "../panelOverlays.js";
9
+ import { useLiveAvatarHandsFreeGuard as se, useLiveAvatarTab as ce, useLiveAvatarVoiceView as le } from "../assistantLiveAvatarSeam.js";
10
+ import { AssistantAttachmentButton as ue } from "../composer/AssistantAttachmentButton.js";
11
+ import { AssistantAttachmentChips as de } from "../composer/AssistantAttachmentChips.js";
12
+ import { AssistantVoiceOrb as fe } from "./AssistantVoiceOrb.js";
13
+ import { VoiceMiniController as pe } from "./VoiceMiniController.js";
14
+ import { VoiceStage as me } from "./VoiceStage.js";
15
+ import { VoiceTranscriptDrawer as he } from "./VoiceTranscriptDrawer.js";
16
+ import { escapeBelongsToLayerAbove as ge, voiceSessionEscapeAction as _e } from "./voiceSessionEscape.js";
17
+ import { HandsFreePrivacyNotice as ve } from "./HandsFreePrivacyNotice.js";
18
+ import { useCallback as ye, useEffect as o, useMemo as be, useRef as s, useState as c } from "react";
19
19
  import { Maximize2 as xe, Minimize2 as Se, ScrollText as Ce, X as l } from "lucide-react";
20
20
  import { jsx as u, jsxs as d } from "react/jsx-runtime";
21
21
  //#region src/shared/assistant/ui/voice/AssistantVoiceSession.tsx
22
- var f = ({ session: f, turns: Te = [], onSaveTranscript: Ee, onContinueInChat: De, notice: m = null, privacyNotice: h = null, spoken: g, onInterrupt: Oe, onToggleMute: _, attachments: v = [], onAttachFiles: y, onRemoveAttachment: ke, onRetryAttachment: Ae, canAttachMore: je = !0, preparingAttachments: Me = 0, attachmentsLocked: b = !1, attachmentNotice: x = null, spokenChunks: S = null, onEnd: C, capturing: Ne = !0, interrupting: Pe = !1, awaitingEndpoint: Fe = !1, holding: w = !1, correcting: Ie = !1, onLetMeFinish: T, onImDone: E, proposalChip: D = null, proposalTray: O = null, renderAfterTurn: Le, onViewChange: k }) => {
23
- let A = n(), { muted: j, error: M } = f, N = s(null), P = s(null), [F, I] = c("mini"), L = le().open, [Re, ze] = c(L);
24
- Re !== L && (ze(L), L && F !== "mini" && I("mini"));
25
- let R = ue(F, f.phase === "ended"), Be = ce();
22
+ var f = ({ session: f, turns: m = [], onSaveTranscript: Ee, onContinueInChat: De, notice: h = null, privacyNotice: g = null, spoken: _, onInterrupt: Oe, onToggleMute: v, attachments: y = [], onAttachFiles: ke, onRemoveAttachment: Ae, onRetryAttachment: je, canAttachMore: Me = !0, preparingAttachments: b = 0, attachmentsLocked: Ne = !1, attachmentNotice: x = null, spokenChunks: S = null, onEnd: C, capturing: Pe = !0, interrupting: Fe = !1, awaitingEndpoint: Ie = !1, holding: w = !1, correcting: Le = !1, onLetMeFinish: T, onImDone: E, proposalChip: D = null, proposalTray: O = null, renderAfterTurn: Re, onViewChange: k }) => {
23
+ let A = n(), { muted: j, error: M } = f, N = s(null), P = s(null), [ze, F] = c("mini"), I = ce().open, [Be, Ve] = c(I);
24
+ Be !== I && (Ve(I), I && ze !== "mini" && F("mini"));
25
+ let L = le(ze, f.phase === "ended"), He = se();
26
26
  o(() => {
27
- k?.(R);
28
- }, [k, R]);
29
- let Ve = ae(), z = R === "full", B = R === "mini", V = R === "mini" ? "panel" : R === "panel" ? "full" : "mini", [H, U] = c(!1);
27
+ k?.(L);
28
+ }, [k, L]);
29
+ let R = ie(), z = L === "full", B = L === "mini", V = L === "mini" ? "panel" : L === "panel" ? "full" : "mini", [H, U] = c(!1);
30
30
  t("assistant.transcript", be(() => ({ run: () => H ? { status: "already" } : (U(!0), { status: "done" }) }), [H]));
31
- let W = O !== null && !B, G = s(!1), K = s(_);
32
- K.current = _;
33
- let q = a(() => {
34
- f.phase !== "listening" && f.phase !== "thinking" || j || G.current || ie(f) || (G.current = !0, K.current());
35
- }, [j, f]), J = a(() => {
31
+ let W = O !== null && !B, G = s(!1), K = s(v);
32
+ K.current = v;
33
+ let q = ye(() => {
34
+ f.phase !== "listening" && f.phase !== "thinking" || j || G.current || re(f) || (G.current = !0, K.current());
35
+ }, [j, f]), J = ye(() => {
36
36
  G.current && (G.current = !1, K.current());
37
- }, []), He = q;
37
+ }, []), Ue = q;
38
38
  o(() => {
39
39
  let e = () => {
40
40
  document.visibilityState === "hidden" ? q() : J();
@@ -47,12 +47,12 @@ var f = ({ session: f, turns: Te = [], onSaveTranscript: Ee, onContinueInChat: D
47
47
  };
48
48
  }, []), o(() => {
49
49
  document.activeElement?.closest("[data-hands-free-notice]") || N.current?.focus();
50
- }, []), e(P, { fallbackSelector: `[${oe}]` }), o(() => {
50
+ }, []), e(P, { fallbackSelector: `[${ae}]` }), o(() => {
51
51
  let e = (e) => {
52
52
  if (e.key === "Escape") {
53
- let t = ve({
53
+ let t = _e({
54
54
  defaultPrevented: e.defaultPrevented,
55
- layerAbove: _e({
55
+ layerAbove: ge({
56
56
  target: e.target,
57
57
  session: P.current,
58
58
  doc: document
@@ -60,7 +60,7 @@ var f = ({ session: f, turns: Te = [], onSaveTranscript: Ee, onContinueInChat: D
60
60
  transcriptOpen: H
61
61
  });
62
62
  if (t === "ignore") return;
63
- e.preventDefault(), t === "close-transcript" ? U(!1) : h ? h.decline() : W ? O?.onClose() : C();
63
+ e.preventDefault(), t === "close-transcript" ? U(!1) : g ? g.decline() : W ? O?.onClose() : C();
64
64
  return;
65
65
  }
66
66
  if (e.key !== "Tab" || !z) return;
@@ -76,48 +76,48 @@ var f = ({ session: f, turns: Te = [], onSaveTranscript: Ee, onContinueInChat: D
76
76
  H,
77
77
  W,
78
78
  O,
79
- h
79
+ g
80
80
  ]);
81
- let Y = m !== null && M === null, X = Be && m === null && M === null, Ue = ie(f), Z = T && E && f.phase === "listening" && !j && M === null ? {
81
+ let Y = h !== null && M === null, X = He && h === null && M === null, We = re(f), Z = T && E && f.phase === "listening" && !j && M === null ? {
82
82
  letMeFinish: T,
83
83
  imDone: E
84
- } : null, We = Z !== null;
84
+ } : null, Ge = Z !== null;
85
85
  o(() => {
86
- if (We) return;
86
+ if (Ge) return;
87
87
  let e = document.activeElement;
88
88
  (!e || e === document.body) && N.current?.focus();
89
- }, [We]);
90
- let Q = te({
89
+ }, [Ge]);
90
+ let Q = i({
91
91
  snapshot: f,
92
- capturing: Ne,
93
- interrupting: Pe,
94
- awaitingEndpoint: Fe
95
- }), Ge = (() => {
92
+ capturing: Pe,
93
+ interrupting: Fe,
94
+ awaitingEndpoint: Ie
95
+ }), Ke = (() => {
96
96
  switch (Q) {
97
97
  case "connecting": return A("voiceSession.connecting", "Starting the microphone…");
98
98
  case "endpoint-grace": return A("voiceSession.endpointGrace", "Still listening — take your time");
99
99
  case "interrupting": return A("voiceSession.interrupting", "Stopped — go ahead");
100
- case "thinking": return A(i.key, i.fallback);
100
+ case "thinking": return A(a.key, a.fallback);
101
101
  case "speaking": return A("voiceSession.speaking", "Speaking — tap to stop");
102
102
  case "mic-error":
103
103
  case "ended": return "";
104
104
  case "listening": return j ? A("voiceSession.muted", "Microphone off") : A("voiceSession.listening", "Listening…");
105
105
  default: return Q;
106
106
  }
107
- })(), Ke = !j && (Q === "listening" || Q === "endpoint-grace"), qe = Ke && w ? A("voiceSession.holding", "Take your time — tap “I’m done” when you finish") : Ke && Ie ? A("voiceSession.correcting", "Stopped that answer — listening to your correction") : Ge, $ = re({
107
+ })(), qe = !j && (Q === "listening" || Q === "endpoint-grace"), Je = qe && w ? A("voiceSession.holding", "Take your time — tap “I’m done” when you finish") : qe && Le ? A("voiceSession.correcting", "Stopped that answer — listening to your correction") : Ke, $ = ne({
108
108
  tr: A,
109
109
  stage: Q,
110
110
  muted: j,
111
- label: qe
112
- }), Je = ne(qe, $), Ye = Q === "thinking";
111
+ label: Je
112
+ }), Ye = te(Je, $), Xe = Q === "thinking";
113
113
  return /* @__PURE__ */ d("div", {
114
114
  ref: P,
115
- className: `flex flex-col items-center justify-between bg-bg-sunken ${R === "full" ? "fixed inset-x-0 bottom-0 top-14 z-[60] px-6 py-8" : R === "panel" ? "absolute inset-0 z-20 px-4 py-6" : "absolute inset-x-0 bottom-0 z-20 min-h-[11.25rem] border-t border-border-subtle px-3 py-3 shadow-lg"}`,
115
+ className: `flex flex-col items-center justify-between bg-bg-sunken ${L === "full" ? "fixed inset-x-0 bottom-0 top-14 z-[60] px-6 py-8" : L === "panel" ? "absolute inset-0 z-20 px-4 py-6" : "absolute inset-x-0 bottom-0 z-20 min-h-[11.25rem] border-t border-border-subtle px-3 py-3 shadow-lg"}`,
116
116
  role: "dialog",
117
117
  "aria-modal": z || void 0,
118
118
  "aria-label": A("voiceSession.title", "Voice conversation"),
119
119
  "data-testid": "assistant-voice-session",
120
- [se]: "",
120
+ [oe]: "",
121
121
  children: [
122
122
  /* @__PURE__ */ d("button", {
123
123
  ref: N,
@@ -140,12 +140,12 @@ var f = ({ session: f, turns: Te = [], onSaveTranscript: Ee, onContinueInChat: D
140
140
  Y ? /* @__PURE__ */ u("p", {
141
141
  className: "max-w-xs text-center text-xs text-status-warning",
142
142
  "data-testid": "assistant-voice-notice",
143
- children: p(m, A)
143
+ children: p(h, A)
144
144
  }) : null,
145
145
  X ? /* @__PURE__ */ u("p", {
146
146
  className: "max-w-xs text-center text-xs text-text-secondary",
147
147
  "data-testid": "assistant-voice-avatar-hands-free",
148
- children: Ve.handsFreeOff
148
+ children: R.handsFreeOff
149
149
  }) : null,
150
150
  /* @__PURE__ */ u("span", {
151
151
  className: "sr-only start-0 top-0",
@@ -153,7 +153,7 @@ var f = ({ session: f, turns: Te = [], onSaveTranscript: Ee, onContinueInChat: D
153
153
  "aria-live": "polite",
154
154
  "aria-atomic": "true",
155
155
  "data-testid": "assistant-voice-notice-announced",
156
- children: Y && m === "reply-not-heard" ? p(m, A) : X ? Ve.handsFreeOff : null
156
+ children: Y && we(h) ? p(h, A) : X ? R.handsFreeOff : null
157
157
  }),
158
158
  x ? /* @__PURE__ */ u("p", {
159
159
  className: "max-w-xs text-center text-xs text-status-warning",
@@ -165,11 +165,11 @@ var f = ({ session: f, turns: Te = [], onSaveTranscript: Ee, onContinueInChat: D
165
165
  ]
166
166
  }),
167
167
  (() => {
168
- let e = /* @__PURE__ */ u(pe, {
168
+ let e = /* @__PURE__ */ u(fe, {
169
169
  session: f,
170
170
  compact: B,
171
171
  connecting: Q === "connecting",
172
- onPress: r(Q) ? Oe : _,
172
+ onPress: r(Q) ? Oe : v,
173
173
  label: r(Q) ? A("voiceSession.interrupt", "Stop the assistant and speak") : j ? A("voiceSession.unmute", "Turn the microphone on") : A("voiceSession.mute", "Turn the microphone off")
174
174
  }), t = Z ? /* @__PURE__ */ u("button", {
175
175
  type: "button",
@@ -182,26 +182,26 @@ var f = ({ session: f, turns: Te = [], onSaveTranscript: Ee, onContinueInChat: D
182
182
  }) : null, n = t ? /* @__PURE__ */ d("div", {
183
183
  className: "flex flex-col items-center gap-2",
184
184
  children: [e, t]
185
- }) : e, te = ee(Q) ? g : Ue || null, i = M === null ? null : we(M, A);
186
- return B ? /* @__PURE__ */ u(me, {
185
+ }) : e, i = ee(Q) ? _ : We || null, a = M === null ? null : Te(M, A);
186
+ return B ? /* @__PURE__ */ u(pe, {
187
187
  spokenChunks: S,
188
188
  state: Q,
189
- caption: te,
190
- statusText: Je,
189
+ caption: i,
190
+ statusText: Ye,
191
191
  statusSuffix: $,
192
- rotateStatus: Ye,
193
- errorText: i,
192
+ rotateStatus: Xe,
193
+ errorText: a,
194
194
  control: n
195
- }) : /* @__PURE__ */ u(he, {
195
+ }) : /* @__PURE__ */ u(me, {
196
196
  state: Q,
197
- userCaption: Ue || null,
197
+ userCaption: We || null,
198
198
  spokenChunks: S,
199
- assistantCaption: g,
199
+ assistantCaption: _,
200
200
  expanded: z,
201
- statusText: Je,
201
+ statusText: Ye,
202
202
  statusSuffix: $,
203
- rotateStatus: Ye,
204
- errorText: i,
203
+ rotateStatus: Xe,
204
+ errorText: a,
205
205
  control: n
206
206
  });
207
207
  })(),
@@ -210,7 +210,7 @@ var f = ({ session: f, turns: Te = [], onSaveTranscript: Ee, onContinueInChat: D
210
210
  children: /* @__PURE__ */ u("button", {
211
211
  type: "button",
212
212
  onClick: () => {
213
- I("panel"), D.onOpen();
213
+ F("panel"), D.onOpen();
214
214
  },
215
215
  className: `max-w-full truncate rounded-full border border-border-default bg-bg-elevated font-medium text-text-primary transition-colors hover:bg-bg-raised focus:outline-none focus-visible:ring-2 focus-visible:ring-border-strong ${B ? "h-8 px-3 text-xs" : "h-10 px-4 text-sm"}`,
216
216
  "data-testid": "assistant-voice-proposal-chip",
@@ -235,39 +235,39 @@ var f = ({ session: f, turns: Te = [], onSaveTranscript: Ee, onContinueInChat: D
235
235
  })
236
236
  }), O.content]
237
237
  }) : null,
238
- h ? /* @__PURE__ */ u(ye, {
239
- view: R,
240
- onContinue: h.accept,
241
- onNotNow: h.decline
238
+ g ? /* @__PURE__ */ u(ve, {
239
+ view: L,
240
+ onContinue: g.accept,
241
+ onNotNow: g.decline
242
242
  }) : null,
243
- H ? /* @__PURE__ */ u(ge, {
244
- turns: Te,
243
+ H ? /* @__PURE__ */ u(he, {
244
+ turns: m,
245
245
  onClose: () => U(!1),
246
246
  onSave: Ee,
247
247
  onContinueInChat: De,
248
- renderAfterTurn: Le
248
+ renderAfterTurn: Re
249
249
  }) : null,
250
- y ? /* @__PURE__ */ u("div", {
250
+ ke ? /* @__PURE__ */ u("div", {
251
251
  className: `absolute ${z ? "left-6 top-6" : B ? "left-11 top-2" : "left-3 top-3"}`,
252
- children: /* @__PURE__ */ u(de, {
252
+ children: /* @__PURE__ */ u(ue, {
253
253
  onFiles: (e) => {
254
- y(e), I((e) => e === "mini" ? "panel" : e);
254
+ ke(e), F((e) => e === "mini" ? "panel" : e);
255
255
  },
256
- onPickerOpen: He,
256
+ onPickerOpen: Ue,
257
257
  direction: "down",
258
- canAddMore: je,
259
- disabled: b
258
+ canAddMore: Me,
259
+ disabled: Ne
260
260
  })
261
261
  }) : null,
262
- (v.length > 0 || Me > 0) && !B ? /* @__PURE__ */ u("div", {
262
+ (y.length > 0 || b > 0) && !B ? /* @__PURE__ */ u("div", {
263
263
  className: `absolute ${z ? "bottom-24 left-24 right-24" : "bottom-16 left-16 right-16"}`,
264
264
  "data-testid": "assistant-voice-attachments",
265
- children: /* @__PURE__ */ u(fe, {
266
- attachments: v,
267
- onRemove: ke ?? (() => void 0),
268
- onRetry: Ae ?? (() => void 0),
269
- preparingCount: Me,
270
- disabled: b
265
+ children: /* @__PURE__ */ u(de, {
266
+ attachments: y,
267
+ onRemove: Ae ?? (() => void 0),
268
+ onRetry: je ?? (() => void 0),
269
+ preparingCount: b,
270
+ disabled: Ne
271
271
  })
272
272
  }) : null,
273
273
  /* @__PURE__ */ u("button", {
@@ -281,7 +281,7 @@ var f = ({ session: f, turns: Te = [], onSaveTranscript: Ee, onContinueInChat: D
281
281
  }),
282
282
  /* @__PURE__ */ u("button", {
283
283
  type: "button",
284
- onClick: () => I(V),
284
+ onClick: () => F(V),
285
285
  className: `absolute ${z ? "bottom-6 left-6 size-14" : B ? "left-2 top-2 size-8" : "bottom-3 left-3 size-11"} flex items-center justify-center rounded-full bg-bg-elevated text-text-secondary transition-colors hover:bg-bg-raised hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-border-strong`,
286
286
  "aria-label": V === "panel" ? A("voiceSession.expandPanel", "Show the conversation in the panel") : V === "full" ? A("voiceSession.expand", "Expand the conversation to full screen") : A("voiceSession.collapse", "Shrink to the compact controller"),
287
287
  "data-testid": "assistant-voice-expand",
@@ -291,6 +291,12 @@ var f = ({ session: f, turns: Te = [], onSaveTranscript: Ee, onContinueInChat: D
291
291
  });
292
292
  };
293
293
  function p(e, t) {
294
+ return typeof e == "string" ? m(e, t) : e.text;
295
+ }
296
+ function we(e) {
297
+ return e !== null && (typeof e != "string" || e === "reply-not-heard");
298
+ }
299
+ function m(e, t) {
294
300
  switch (e) {
295
301
  case "network": return t("voiceSession.noticeNetwork", "Speech recognition lost its connection. Check your network — it will retry.");
296
302
  case "not-capturing": return t("voiceSession.noticeNotCapturing", "The microphone did not start. Check that this site is allowed to use it, then try again.");
@@ -302,7 +308,7 @@ function p(e, t) {
302
308
  default: return e;
303
309
  }
304
310
  }
305
- function we(e, t) {
311
+ function Te(e, t) {
306
312
  switch (e) {
307
313
  case "mic-denied": return t("voiceSession.micDenied", "Microphone access is blocked. Allow it in your browser settings to talk.");
308
314
  case "no-speech-support": return t("voiceSession.noSpeechSupport", "This browser cannot listen. Try a different browser, or type instead.");
@@ -0,0 +1,23 @@
1
+ import { AssistantConversationProcessing } from '../conversation';
2
+ /**
3
+ * What the voice session is handed as "the newest reply" (`useVoiceSession`'s
4
+ * `latestReply`).
5
+ *
6
+ * `refused` is set only when the turn ENDED IN AN ERROR: a refusal before it
7
+ * started ("No active subscription covers ...") or a failure. Its text is the
8
+ * reason, not an answer, and nothing was published to the speech feed for it, so
9
+ * the session must show it rather than enter `speaking` (BOFF-9207). A real
10
+ * answer is exactly `{ id, text }`, with no extra key, as it always was.
11
+ *
12
+ * `processing` is the entry for the message's OWN conversation; it is matched on
13
+ * the message id because one entry serves a whole conversation and outlives the
14
+ * turn, so an older turn's error must not mark a newer, answered one.
15
+ */
16
+ export interface VoiceReply {
17
+ id: string;
18
+ text: string;
19
+ refused?: boolean;
20
+ }
21
+ export declare function toVoiceReply(message: {
22
+ id: string;
23
+ }, text: string, processing: AssistantConversationProcessing | null | undefined): VoiceReply;
@@ -0,0 +1,13 @@
1
+ //#region src/shared/assistant/voice/voiceReply.ts
2
+ function e(e, t, n) {
3
+ return n?.messageId === e.id && n.status === "error" ? {
4
+ id: e.id,
5
+ text: t,
6
+ refused: !0
7
+ } : {
8
+ id: e.id,
9
+ text: t
10
+ };
11
+ }
12
+ //#endregion
13
+ export { e as toVoiceReply };
@@ -118,6 +118,16 @@ export type VoiceSessionEvent =
118
118
  | {
119
119
  type: 'reply-audio-started';
120
120
  replyId: string;
121
+ }
122
+ /**
123
+ * ★ BOFF-9207: the turn ended WITHOUT an answer - the assistant refused it
124
+ * (a plan that does not cover it, a permission) or it failed before anything
125
+ * was said. There is nothing to speak, so the session must never enter
126
+ * `speaking` for it: only a `thinking` moves, straight back to `listening`.
127
+ * The reason is shown by the caller (`VoiceTurnRefusalNotice`), not spoken.
128
+ */
129
+ | {
130
+ type: 'turn-refused';
121
131
  } | {
122
132
  type: 'speech-ended';
123
133
  } | {
@@ -156,6 +156,13 @@ function v(t, r, i = null) {
156
156
  pending: "",
157
157
  interim: ""
158
158
  } : t;
159
+ case "turn-refused": return t.phase === "thinking" ? {
160
+ ...t,
161
+ ...g,
162
+ phase: "listening",
163
+ pending: "",
164
+ interim: ""
165
+ } : t;
159
166
  case "speech-ended": return t.phase === "speaking" ? {
160
167
  ...t,
161
168
  ...g,