@burdenoff/fe-libs 2026.903.2 → 2026.903.3

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.
@@ -16,4 +16,7 @@ export { isVoiceTurn, voiceSessionIdOf, voiceMarker, chatTurns, voiceTurnsOnly,
16
16
  export type { VoiceSessionGroup, VoiceTranscriptTurn } from './voice/turnVisibility';
17
17
  export { useVoiceInput } from './voice/useVoiceInput';
18
18
  export type { UseVoiceInputOptions, UseVoiceInputResult, VoiceInputError } from './voice/useVoiceInput';
19
+ export { toCaptionText } from './voice/captionText';
20
+ export { parseConfirmationRefusal, findPendingWrite } from './pendingWrite';
21
+ export type { PendingWrite, PendingWriteValue } from './pendingWrite';
19
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/assistant/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAC9E,OAAO,EAAE,+BAA+B,EAAE,MAAM,mCAAmC,CAAC;AACpF,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,QAAQ,EACR,aAAa,EACb,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAKpB,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,4BAA4B,EAC5B,sBAAsB,EACtB,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,wBAAwB,EACxB,yBAAyB,EACzB,aAAa,EACb,qBAAqB,EACrB,yBAAyB,EACzB,uBAAuB,EACvB,2BAA2B,GAC5B,MAAM,OAAO,CAAC;AAEf,YAAY,EACV,aAAa,EACb,mBAAmB,EACnB,WAAW,EACX,sBAAsB,EACtB,yBAAyB,EACzB,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,iBAAiB,EACjB,2BAA2B,EAC3B,oBAAoB,EACpB,2BAA2B,EAC3B,YAAY,GACb,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,SAAS,EACT,cAAc,EACd,cAAc,EACd,yBAAyB,GAC1B,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/assistant/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAC9E,OAAO,EAAE,+BAA+B,EAAE,MAAM,mCAAmC,CAAC;AACpF,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,QAAQ,EACR,aAAa,EACb,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAKpB,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,4BAA4B,EAC5B,sBAAsB,EACtB,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,wBAAwB,EACxB,yBAAyB,EACzB,aAAa,EACb,qBAAqB,EACrB,yBAAyB,EACzB,uBAAuB,EACvB,2BAA2B,GAC5B,MAAM,OAAO,CAAC;AAEf,YAAY,EACV,aAAa,EACb,mBAAmB,EACnB,WAAW,EACX,sBAAsB,EACtB,yBAAyB,EACzB,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,iBAAiB,EACjB,2BAA2B,EAC3B,oBAAoB,EACpB,2BAA2B,EAC3B,YAAY,GACb,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,SAAS,EACT,cAAc,EACd,cAAc,EACd,yBAAyB,GAC1B,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxG,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC5E,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The write the assistant is asking permission for — read from the request
3
+ * itself, not from the model's description of it.
4
+ *
5
+ * ★ WHY THIS EXISTS. In "Take action" mode the in-sandbox helper refuses every
6
+ * mutation on its first attempt and returns a `CONFIRMATION_REQUIRED` tool
7
+ * result naming the operation and the exact values it was about to send. The
8
+ * model is then instructed to describe that change in prose and ask the user.
9
+ *
10
+ * So today the user approves a PARAGRAPH. The paragraph is generated by the same
11
+ * model that wants the write to go through, and nothing checks it against the
12
+ * payload — a field can be omitted, rounded, or described as something milder
13
+ * than it is, and the approval still counts.
14
+ *
15
+ * The refusal itself is already in the message parts the panel receives, and the
16
+ * panel throws it away (`getToolProgress` reduces a tool part to
17
+ * "Completed: <tool>"). Reading it back means the confirmation card can show the
18
+ * operation and values that will actually be sent.
19
+ *
20
+ * ★ WHAT THIS IS NOT. It does not make the UI the authorization boundary. The
21
+ * approval still travels back through the model, which holds the single-use
22
+ * token and could in principle retry without asking — see the module docstring
23
+ * in `ai-assistant/src/write-confirmation.ts`, which names the remaining gap and
24
+ * the fix ("the UI holding the approval"). This narrows what the user can be
25
+ * misled about; it does not close that.
26
+ */
27
+ export interface PendingWriteValue {
28
+ key: string;
29
+ /** Already stringified for display; objects and arrays are JSON. */
30
+ value: string;
31
+ }
32
+ export interface PendingWrite {
33
+ /** GraphQL operation name, e.g. `createCrop`. */
34
+ operation: string;
35
+ /**
36
+ * The variables, flattened for display. `null` when the refusal reported no
37
+ * variables or the block could not be parsed — the card then falls back to
38
+ * naming the operation alone rather than implying there is nothing to send.
39
+ */
40
+ values: PendingWriteValue[] | null;
41
+ }
42
+ /**
43
+ * A tool part as the transport delivers it. Deliberately loose: `state.output`
44
+ * is typed `unknown` upstream and different agent versions have wrapped the text
45
+ * differently, so this accepts anything and proves the shape at runtime.
46
+ */
47
+ interface ToolPartLike {
48
+ type?: string;
49
+ state?: {
50
+ output?: unknown;
51
+ [key: string]: unknown;
52
+ } | undefined;
53
+ }
54
+ /**
55
+ * Parse one refusal.
56
+ *
57
+ * ★ The single-use token is deliberately NOT extracted. It is a capability: the
58
+ * write goes through if it is echoed back, so putting it on screen would invite
59
+ * a user — or anything reading the page — to replay it, and would turn a
60
+ * confirmation card into a bypass. The card needs the operation and the values;
61
+ * it never needs the token.
62
+ */
63
+ export declare function parseConfirmationRefusal(text: string): PendingWrite | null;
64
+ /**
65
+ * The pending write in a set of message parts, if the agent refused one.
66
+ *
67
+ * Takes the LAST match: within a turn the model may be told to stop after the
68
+ * first refusal, but a turn that somehow produced two is asking about the most
69
+ * recent one.
70
+ */
71
+ export declare function findPendingWrite(parts: readonly ToolPartLike[] | undefined): PendingWrite | null;
72
+ export {};
73
+ //# sourceMappingURL=pendingWrite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pendingWrite.d.ts","sourceRoot":"","sources":["../../../src/shared/assistant/pendingWrite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,MAAM,EAAE,iBAAiB,EAAE,GAAG,IAAI,CAAC;CACpC;AAOD;;;;GAIG;AACH,UAAU,YAAY;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;CAClE;AA+BD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAsD1E;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,SAAS,GAAG,YAAY,GAAG,IAAI,CAWhG"}
@@ -0,0 +1,85 @@
1
+ //#region src/shared/assistant/pendingWrite.ts
2
+ var e = "CONFIRMATION_REQUIRED", t = /CONFIRMATION_REQUIRED: the write "([^"]+)" was NOT sent/, n = "Values you were about to send:";
3
+ function r(e) {
4
+ if (typeof e == "string") return e;
5
+ if (e && typeof e == "object" && "output" in e) {
6
+ let t = e.output;
7
+ if (typeof t == "string") return t;
8
+ }
9
+ return null;
10
+ }
11
+ function i(e) {
12
+ if (!e || typeof e != "object" || Array.isArray(e)) return null;
13
+ let t = Object.entries(e);
14
+ return t.length === 0 ? null : t.map(([e, t]) => ({
15
+ key: e,
16
+ value: typeof t == "string" ? t : t == null ? "—" : JSON.stringify(t)
17
+ }));
18
+ }
19
+ function a(r) {
20
+ if (!r.includes(e)) return null;
21
+ let a = t.exec(r)?.[1]?.trim();
22
+ if (!a) return null;
23
+ let o = r.indexOf(n);
24
+ if (o === -1) return {
25
+ operation: a,
26
+ values: null
27
+ };
28
+ let s = r.slice(o + 30).trim();
29
+ if (!s || s.startsWith("(no variables)")) return {
30
+ operation: a,
31
+ values: null
32
+ };
33
+ let c = s.indexOf("{");
34
+ if (c === -1) return {
35
+ operation: a,
36
+ values: null
37
+ };
38
+ let l = 0, u = -1, d = !1, f = !1;
39
+ for (let e = c; e < s.length; e += 1) {
40
+ let t = s[e];
41
+ if (f) {
42
+ f = !1;
43
+ continue;
44
+ }
45
+ if (t === "\\" && d) {
46
+ f = !0;
47
+ continue;
48
+ }
49
+ if (t === "\"") d = !d;
50
+ else if (!d && t === "{") l += 1;
51
+ else if (!d && t === "}" && (--l, l === 0)) {
52
+ u = e + 1;
53
+ break;
54
+ }
55
+ }
56
+ if (u === -1) return {
57
+ operation: a,
58
+ values: null
59
+ };
60
+ try {
61
+ return {
62
+ operation: a,
63
+ values: i(JSON.parse(s.slice(c, u)))
64
+ };
65
+ } catch {
66
+ return {
67
+ operation: a,
68
+ values: null
69
+ };
70
+ }
71
+ }
72
+ function o(e) {
73
+ if (!e) return null;
74
+ let t = null;
75
+ for (let n of e) {
76
+ if (n?.type !== "tool") continue;
77
+ let e = r(n.state?.output);
78
+ if (!e) continue;
79
+ let i = a(e);
80
+ i && (t = i);
81
+ }
82
+ return t;
83
+ }
84
+ //#endregion
85
+ export { o as findPendingWrite, a as parseConfirmationRefusal };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Markdown → the words, for a caption someone READS.
3
+ *
4
+ * The live voice caption is the one surface where the model's raw Markdown was
5
+ * printed literally: bold stars, hash headings, table pipes, fence backticks.
6
+ * The audio path has been clean for a while (`toSpeakableText`), but that
7
+ * function is the wrong tool here and reusing it would be worse than the bug.
8
+ *
9
+ * ★ `toSpeakableText` is a SCRIPT FOR A VOICE, not a Markdown cleaner. It
10
+ * replaces a table with "Table with 40 rows and 2 columns", a fenced block with
11
+ * "Code block", a link with the word "link", and rewrites `x@y.com` as "x at y
12
+ * dot com" — sensible in an earpiece, destructive on a screen where the reader
13
+ * can simply read the thing. Worse, those replacements come from an English
14
+ * default list only localised inside the speech provider, so reusing it would
15
+ * print English chrome inside a Tamil or Hindi caption: the exact
16
+ * mixed-language defect that code exists to prevent.
17
+ *
18
+ * So this strips MARKERS and keeps CONTENT. Every cell value, all code text,
19
+ * bare URLs, email addresses, emoji and punctuation survive verbatim. Nothing
20
+ * is summarised, truncated or annotated: a caption that quietly drops the
21
+ * user's numbers is a worse failure than one showing a stray asterisk. Where
22
+ * this function is unsure, it leaves the text ALONE.
23
+ *
24
+ * ★ Newlines are PRESERVED, which only helps where the element renders them.
25
+ * `VoiceCaption` carries `whitespace-pre-wrap` for exactly that. The mini
26
+ * controller deliberately does NOT: it is `line-clamp-3`, and real newlines
27
+ * each burn one of the three lines — measured at the strip's true width, that
28
+ * cut a 116-character reply to 41 visible characters. Markers still go; the
29
+ * breaks are allowed to collapse on the clamped surface.
30
+ *
31
+ * ★ NO LOOKBEHIND ANYWHERE IN CODE. Safari < 16.4 throws on that assertion
32
+ * class at PARSE time, not match time — one literal takes down the whole chunk
33
+ * on those devices, and this ships to phones. Lookahead is fine; where a
34
+ * leading boundary is needed it is CAPTURED and put back. Same rule as
35
+ * `speakableText.ts`, and `captionText.test.ts` asserts it on the source.
36
+ */
37
+ /**
38
+ * Strip Markdown markers for display, keeping all of the content.
39
+ *
40
+ * Safe on plain text: a reply with no Markdown comes back unchanged.
41
+ */
42
+ export declare function toCaptionText(markdown: string): string;
43
+ //# sourceMappingURL=captionText.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captionText.d.ts","sourceRoot":"","sources":["../../../../src/shared/assistant/voice/captionText.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAkKH;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA2CtD"}
@@ -0,0 +1,49 @@
1
+ //#region src/shared/assistant/voice/captionText.ts
2
+ var e = "", t = "\\uE000", n = /^[\s:|-]*$/, r = /^\s*([-*_])\s*(?:\1\s*){2,}$/, i = /^\s*=+\s*$/;
3
+ function a(e) {
4
+ return e.includes("|") && e.includes("-") && n.test(e);
5
+ }
6
+ function o(e) {
7
+ let t = /* @__PURE__ */ new Set();
8
+ return e.forEach((n, r) => {
9
+ if (!a(n)) return;
10
+ let i = e[r - 1];
11
+ r > 0 && i !== void 0 && i.includes("|") && t.add(r - 1);
12
+ for (let n = r + 1; n < e.length; n += 1) {
13
+ let r = e[n];
14
+ if (r === void 0 || r.trim() === "" || !r.includes("|")) break;
15
+ t.add(n);
16
+ }
17
+ }), t;
18
+ }
19
+ function s(n, r) {
20
+ let i = (t) => (r.push(t), `${e}${r.length - 1}${e}`);
21
+ return n.replace(/^ {0,3}```[^\n]*\n([\s\S]*?)^ {0,3}```[^\n]*$/gm, (e, t) => i(t.replace(/\n$/, ""))).replace(/^ {0,3}```[^\n]*\n?([\s\S]*)$/m, (e, t) => i(t)).replace(/`([^`\n]+)`/g, (e, t) => i(t)).replace(RegExp(`<((?:https?://|mailto:)[^>\\s${t}]+)>`, "gi"), (e, t) => i(t)).replace(RegExp(`<([^@<>\\s${t}]+@[^@<>\\s${t}]+\\.[^@<>\\s${t}]+)>`, "g"), (e, t) => i(t)).replace(RegExp(`\\b(?:https?://|www\\.)[^\\s<>()\\[\\]${t}]+`, "gi"), (e) => i(e)).replace(/\b[\w.+-]{1,64}@[\w-]{1,63}\.[\w.-]{1,63}\b/g, (e) => i(e));
22
+ }
23
+ function c(t, n) {
24
+ let r = RegExp(`${e}(\\d+)${e}`, "g"), i = t;
25
+ for (let e = 0; e <= n.length; e += 1) {
26
+ let e = i.replace(r, (e, t) => n[Number(t)] ?? "");
27
+ if (e === i) break;
28
+ i = e;
29
+ }
30
+ return i;
31
+ }
32
+ function l(e) {
33
+ return e.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]*)\]\[[^\]]*\]/g, "$1").replace(/(^|[^\w~])~~(\S(?:[^\n]*?\S)?)~~(?=[^\w~]|$)/g, "$1$2").replace(/(^|[^\w*])\*\*(\S(?:[^\n]*?\S)?)\*\*(?=[^\w*]|$)/g, "$1$2").replace(/(^|[^\p{L}\p{N}_])__(\S(?:[^\n]*?\S)?)__(?=[^\p{L}\p{N}_]|$)/gu, "$1$2").replace(/(^|[^\w*])\*(\S(?:[^*\n]*\S)?)\*(?=[^\w*]|$)/g, "$1$2").replace(/(^|[^\p{L}\p{N}_])_(\S(?:[^_\n]*\S)?)_(?=[^\p{L}\p{N}_]|$)/gu, "$1$2");
34
+ }
35
+ function u(e) {
36
+ return e.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((e) => e.trim()).join(" · ");
37
+ }
38
+ function d(t) {
39
+ if (!t) return "";
40
+ let n = [], d = s(t.replace(/\r\n/g, "\n").replace(new RegExp(e, "g"), ""), n).split("\n"), f = o(d), p = [];
41
+ return d.forEach((e, t) => {
42
+ if (a(e) || r.test(e) || i.test(e) && (p[p.length - 1] ?? "").trim() !== "") return;
43
+ let n = e;
44
+ for (; /^\s*>\s?/.test(n);) n = n.replace(/^\s*>\s?/, "");
45
+ n = n.replace(/^\s*#{1,6}\s+/, ""), n = n.replace(/^(\s*)[-*+]\s+/, "$1"), n = n.replace(/^(\s*)\d{1,2}[.)]\s+/, "$1"), f.has(t) && (n = u(n)), p.push(l(n).replace(/\s+$/, ""));
46
+ }), c(p.join("\n").replace(/\n{3,}/g, "\n\n").trim(), n);
47
+ }
48
+ //#endregion
49
+ export { d as toCaptionText };
@@ -0,0 +1,142 @@
1
+ import { Locale } from '../../providers/shell/I18nProvider';
2
+ /**
3
+ * BCP-47 tag we ask the engine for, per app locale.
4
+ *
5
+ * Deliberately identical to `useVoiceInput`'s RECOGNITION_LANG: regional choices
6
+ * follow where HealthyBowl actually operates (India for ta/hi), because a voice's
7
+ * pronunciation model is region-specific. Typed `Record<Locale, …>` so adding a
8
+ * locale to fe-libs fails the build here rather than silently falling back to
9
+ * English.
10
+ */
11
+ export declare const SPEECH_LANG: Record<Locale, string>;
12
+ /**
13
+ * The subset of `SpeechSynthesisVoice` this module needs. Structural on purpose:
14
+ * `selectVoiceForLocale` returns the caller's own array element, so the browser
15
+ * engine gets back the real `SpeechSynthesisVoice` instance it must assign to
16
+ * `utterance.voice`, while tests pass literals.
17
+ */
18
+ export interface SpeechVoiceLike {
19
+ readonly name: string;
20
+ readonly lang: string;
21
+ /** The engine's default voice (usually the OS UI language). */
22
+ readonly default?: boolean;
23
+ /** On-device rather than server-side — no network, no latency, no vendor. */
24
+ readonly localService?: boolean;
25
+ }
26
+ /** `en_GB`, `EN-gb` and `en-GB` are the same tag as far as matching goes. */
27
+ export declare function normalizeLangTag(tag: string): string;
28
+ /** `ta-IN` → `ta`. The part that decides whether a voice is intelligible at all. */
29
+ export declare function primaryLanguageSubtag(tag: string): string;
30
+ export declare function speechLangForLocale(locale: string): string;
31
+ /**
32
+ * Pick the best installed voice for an app locale, or `null` when the device has
33
+ * no voice for that language at all.
34
+ *
35
+ * The ladder, in order:
36
+ * 1. exact tag (`ta-IN` for Tamil);
37
+ * 2. same language, any region (`en-GB` when only `en-US` was asked for) —
38
+ * an accent is a far smaller problem than the wrong language;
39
+ * 3. within whichever tier matched: the engine's default voice, then an
40
+ * on-device voice, then engine order.
41
+ *
42
+ * There is intentionally NO cross-language fallback. Reading a Tamil reply with
43
+ * an English voice produces confident nonsense; the caller disables the control
44
+ * and says why instead (see `AssistantSpeechProvider`).
45
+ */
46
+ /**
47
+ * Every installed voice that can read this language, best first.
48
+ *
49
+ * Exposed so the UI can offer a CHOICE. Quality varies enormously between
50
+ * voices for the same language — Apple's "Enhanced"/"Premium" downloads and
51
+ * Chrome's network "Google …" voices are markedly better than the compact
52
+ * on-device defaults — and none of that is machine-readable, so the honest
53
+ * answer is to rank by the weak signals available and let the listener decide.
54
+ */
55
+ export declare function voicesForLocale<T extends SpeechVoiceLike>(voices: readonly T[], locale: string): T[];
56
+ /**
57
+ * Best-effort quality ordering. Higher is better.
58
+ *
59
+ * Every signal here is a heuristic on a NAME, which is fragile and locale-
60
+ * dependent — hence the voice picker: this decides the DEFAULT, not the answer.
61
+ *
62
+ * Deliberately NOT preferring `localService`. That is an offline-capability
63
+ * flag, not a quality one, and on desktop Chrome it actively selects the
64
+ * compact on-device voice over the better-sounding network one. The previous
65
+ * ladder preferred it and was therefore choosing the worse voice.
66
+ */
67
+ export declare function voiceQualityRank(voice: SpeechVoiceLike): number;
68
+ /**
69
+ * The voice to read this language with: the user's explicit pick if it is still
70
+ * installed, otherwise the best-ranked one.
71
+ *
72
+ * `preferredName` is matched by name because that is the only identifier stable
73
+ * across sessions — voice objects are recreated on every page load, and
74
+ * `voiceURI` is not reliably unique across platforms.
75
+ */
76
+ export declare function selectVoiceForLocale<T extends SpeechVoiceLike>(voices: readonly T[], locale: string, preferredName?: string | null): T | null;
77
+ /**
78
+ * What the UI should do with the speak control, given what the engine reports.
79
+ *
80
+ * `pending` matters: `speechSynthesis.getVoices()` famously returns `[]` on the
81
+ * first call in Chrome and only fills in after `voiceschanged`. Treating an
82
+ * empty list as "no voice" would disable the button for a moment on every load
83
+ * and, on engines that never fire the event, forever — so an empty list stays
84
+ * enabled and we let the engine resolve `utterance.lang` itself.
85
+ */
86
+ export type VoiceAvailability = 'unsupported' | 'pending' | 'ready' | 'missing-voice' | 'no-voices';
87
+ /**
88
+ * `probeSettled` is what separates "the list has not arrived yet" from "this
89
+ * device genuinely has none".
90
+ *
91
+ * Both look like `voices.length === 0`, and the difference is not knowable from
92
+ * the list alone — only the caller, having waited out `VOICE_LIST_TIMEOUT_MS`
93
+ * and looked again, can tell them apart. Defaulting to `false` keeps the
94
+ * optimistic reading (`pending`, control stays enabled) for every caller that
95
+ * has not probed, which is the pre-existing behaviour.
96
+ *
97
+ * Without this distinction the empty list stays `pending` forever, `speechGate`
98
+ * keeps answering `wait-for-voices`, and the button is enabled but does nothing
99
+ * on every press — a dead control with no explanation, which is exactly how
100
+ * this feature looked on any Linux box or bare container.
101
+ */
102
+ export declare function voiceAvailability(isSupported: boolean, voices: readonly SpeechVoiceLike[], locale: string, probeSettled?: boolean): VoiceAvailability;
103
+ /**
104
+ * Human name for the language we would have spoken, in the reader's own locale
105
+ * ("Tamil" for an English UI, "தமிழ்" for a Tamil one).
106
+ *
107
+ * Used to make the disabled reason actionable: "No Tamil voice is installed"
108
+ * tells a user what to go and install; "no voice is installed" does not.
109
+ * `Intl.DisplayNames` is unavailable in some embedded webviews, so the subtag
110
+ * itself is the fallback — degraded copy beats a thrown render.
111
+ */
112
+ export declare function languageDisplayName(languageTag: string, inLocale: string): string;
113
+ /**
114
+ * What a caller that wants to speak RIGHT NOW should do.
115
+ *
116
+ * The one decision every playback path has to make, in one place, because
117
+ * having it in three (the per-message button, the header toggle, the auto-speak
118
+ * subscription) is how "there is no cross-language fallback" stopped being true
119
+ * on two of them: an empty or unmatched voice list means the engine picks its
120
+ * own default, and a Tamil reply comes out in an English voice.
121
+ *
122
+ * - `speak` — a voice for this language exists; go.
123
+ * - `wait-for-voices` — the list has not arrived yet (Chrome's first
124
+ * `getVoices()` returns `[]`). Speaking now would gamble on the engine
125
+ * default, and refusing forever would disable the control on engines that
126
+ * never fire `voiceschanged`; so the caller waits for the list, briefly, and
127
+ * asks again. The control stays enabled throughout.
128
+ * - `block` — unsupported, or no voice for this language. Say nothing.
129
+ */
130
+ export type SpeechGate = 'speak' | 'wait-for-voices' | 'block';
131
+ export declare function speechGate(availability: VoiceAvailability): SpeechGate;
132
+ /**
133
+ * Should the control be visible but inert?
134
+ *
135
+ * `pending` deliberately is NOT disabled — see `speechGate`. `no-voices` is,
136
+ * because by then we have waited and looked again: staying enabled would leave
137
+ * the user pressing a button that can never do anything.
138
+ */
139
+ export declare function isSpeechBlocked(availability: VoiceAvailability): boolean;
140
+ /** How long to wait for a late voice list before giving up on one attempt. */
141
+ export declare const VOICE_LIST_TIMEOUT_MS = 2000;
142
+ //# sourceMappingURL=speechVoices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speechVoices.d.ts","sourceRoot":"","sources":["../../../../src/shared/assistant/voice/speechVoices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oCAAoC,CAAC;AAEjE;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAU9C,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;;;;;;;;;;;GAcG;AACH;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,eAAe,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAYpG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAa/D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,eAAe,EAC5D,MAAM,EAAE,SAAS,CAAC,EAAE,EACpB,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,GAC5B,CAAC,GAAG,IAAI,CAQV;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,SAAS,GAAG,OAAO,GAAG,eAAe,GAAG,WAAW,CAAC;AAEpG;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,OAAO,EACpB,MAAM,EAAE,SAAS,eAAe,EAAE,EAClC,MAAM,EAAE,MAAM,EACd,YAAY,UAAQ,GACnB,iBAAiB,CAInB;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQjF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,iBAAiB,GAAG,OAAO,CAAC;AAE/D,wBAAgB,UAAU,CAAC,YAAY,EAAE,iBAAiB,GAAG,UAAU,CAItE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,iBAAiB,GAAG,OAAO,CAExE;AAED,8EAA8E;AAC9E,eAAO,MAAM,qBAAqB,OAAQ,CAAC"}
@@ -0,0 +1,184 @@
1
+ /**
2
+ * The voice-conversation state machine (BOFF-7120).
3
+ *
4
+ * A live voice session is a loop — listen, submit, wait, speak, listen again —
5
+ * and every interesting decision in it is about TIMING and INTERRUPTION rather
6
+ * than about rendering. Those decisions live here, with no DOM, no audio and no
7
+ * React, for the same reason `browserSpeechEngine` pushed its logic into
8
+ * `speakableText`/`speechVoices`: this repo's vitest runs in the `node`
9
+ * environment, so anything touching `window` cannot be tested at all.
10
+ *
11
+ * ★ **The microphone is CLOSED while the assistant speaks.** It was open, so
12
+ * that talking over the assistant could interrupt it, guarded by matching the
13
+ * transcript against the text being spoken. That guard was too weak to survive
14
+ * contact with reality: speech recognition of synthesised speech mis-hears it
15
+ * ("sow ragi" -> "so ragi"), so exact containment fails, the mis-heard echo is
16
+ * accepted as a new question, submitted, answered, spoken — and round it goes.
17
+ * The operator hit this loop on a real device.
18
+ *
19
+ * Browsers do not give the Web Speech API the acoustic echo cancellation that a
20
+ * native duplex app has, so there is no text-level fix for this. Interruption is
21
+ * now an explicit tap instead, which is reliable, and the echo check survives
22
+ * only as a guard on the moments just after playback stops.
23
+ *
24
+ * ★ **Mute is NOT a phase.** It is a property of the microphone, orthogonal to
25
+ * where the conversation is. Modelling it as a state meant muting while the
26
+ * assistant was thinking discarded the `thinking` phase, so the reply — already
27
+ * in flight — was then ignored, and unmuting landed in `listening` with nothing
28
+ * pending and nothing coming. The session dead-ended and the user's question was
29
+ * silently lost.
30
+ *
31
+ * What this deliberately does NOT model: the assistant is turn-based
32
+ * (speech -> text -> LLM -> text -> speech), so `thinking` is a real, visible
33
+ * phase lasting seconds. A native speech-to-speech API would collapse
34
+ * listening/thinking/speaking into one duplex stream; we are not that, and
35
+ * pretending otherwise would hide latency the UI must show.
36
+ */
37
+ /** Where the CONVERSATION is. Independent of whether the mic is capturing. */
38
+ export type VoiceSessionPhase = 'idle' | 'listening' | 'thinking' | 'speaking' | 'ended';
39
+ export interface VoiceSessionSnapshot {
40
+ phase: VoiceSessionPhase;
41
+ /** Microphone off. Orthogonal to `phase` — see the header. */
42
+ muted: boolean;
43
+ /** FINALISED speech for the turn not yet submitted, accumulated. */
44
+ pending: string;
45
+ /** The recogniser's current guess, replaced wholesale until it finalises. */
46
+ interim: string;
47
+ /** Why the session stopped, when it stopped abnormally. */
48
+ error: VoiceSessionError | null;
49
+ }
50
+ /**
51
+ * Deliberately does NOT include a "playback blocked" variant. Nothing can
52
+ * detect it — the speech provider does not surface a per-message playback
53
+ * refusal — and the watchdog in `useVoiceSession` already recovers by handing
54
+ * the turn back silently, which continues the conversation rather than killing
55
+ * it. An error the code cannot raise is dead UI copy.
56
+ */
57
+ export type VoiceSessionError = 'no-speech-support' | 'no-voice-support' | 'mic-denied';
58
+ export type VoiceSessionEvent =
59
+ /** `final` distinguishes a committed phrase from a revisable guess. */
60
+ {
61
+ type: 'start';
62
+ } | {
63
+ type: 'speech';
64
+ transcript: string;
65
+ final: boolean;
66
+ }
67
+ /** Nothing heard for `SILENCE_SUBMIT_MS`. */
68
+ | {
69
+ type: 'silence';
70
+ } | {
71
+ type: 'reply';
72
+ text: string;
73
+ } | {
74
+ type: 'speech-ended';
75
+ } | {
76
+ type: 'mute';
77
+ muted: boolean;
78
+ }
79
+ /** The user tapped to cut the assistant off. Explicit, not inferred. */
80
+ | {
81
+ type: 'interrupt';
82
+ } | {
83
+ type: 'fail';
84
+ error: VoiceSessionError;
85
+ } | {
86
+ type: 'end';
87
+ };
88
+ /**
89
+ * How long the user must stop talking before the turn is submitted.
90
+ *
91
+ * Long enough to survive a mid-sentence breath — a farmer describing a problem
92
+ * pauses — and short enough that the session does not feel unresponsive. A
93
+ * premature send truncates the question, which costs a whole 2-5s round trip to
94
+ * recover from.
95
+ *
96
+ * ★ Raised from 1200ms because 1.2s does not survive an ordinary pause: the
97
+ * operator reported being cut off mid-sentence, and the cost is worse than a
98
+ * truncated prompt — once the turn is submitted the reducer discards every
99
+ * `speech` event outside `listening`, so the rest of the sentence is not
100
+ * queued, it is thrown away, and the assistant answers half a question.
101
+ *
102
+ * The asymmetry is what settles the number: waiting 800ms longer costs a moment
103
+ * of silence, while cutting someone off costs the whole round trip AND makes
104
+ * them repeat themselves.
105
+ */
106
+ export declare const SILENCE_SUBMIT_MS = 2000;
107
+ /**
108
+ * Quiet needed before the pause is SHOWN as a pause.
109
+ *
110
+ * The submit timer above is armed the instant there is something submittable —
111
+ * which is mid-sentence — so it cannot itself say whether the user has stopped.
112
+ * This is the delay before the stage switches to `endpoint-grace`, leaving the
113
+ * remaining ~1.6s of the window visibly readable as "still listening, finish
114
+ * your thought" rather than as the session having already moved on.
115
+ *
116
+ * Short enough to be seen, long enough that an ordinary breath between words
117
+ * does not flicker the stage. ★800, not 400: this file already records that
118
+ * SILENCE_SUBMIT_MS had to go 1200 -> 2000 because ordinary pauses ran longer
119
+ * than expected, so a 400ms threshold sits inside normal inter-clause silence
120
+ * and would oscillate the stage mid-sentence. Still leaves ~1.2s visible.
121
+ */
122
+ export declare const ENDPOINT_GRACE_MS = 800;
123
+ /**
124
+ * How long the `interrupting` stage may persist without the microphone coming
125
+ * back. A ceiling, not a duration: the state normally ends the moment capture
126
+ * resumes. It exists so that a session whose microphone never reopens — the
127
+ * failure case — cannot latch this on and hide its own error message.
128
+ */
129
+ export declare const INTERRUPT_MAX_MS = 1500;
130
+ /**
131
+ * How long the `interrupting` stage is held even once the recogniser reports
132
+ * back.
133
+ *
134
+ * ★ `isListening` waits for the recogniser's own `onstart` as of #367, so it is
135
+ * now an honest capture signal. The dwell stays: `onstart` can still fire a beat
136
+ * before audio is usable, and a session whose microphone never opens must not
137
+ * latch this on and hide its own error.
138
+ */
139
+ export declare const INTERRUPT_MIN_MS = 500;
140
+ /** A turn is submitted only when there is something worth sending. */
141
+ export declare function isSubmittable(text: string): boolean;
142
+ /** Everything heard this turn: committed phrases plus the current guess. */
143
+ export declare function spokenSoFar(snapshot: VoiceSessionSnapshot): string;
144
+ export declare function isEcho(transcript: string, spokenText: string | null): boolean;
145
+ /**
146
+ * What to do with a reply the app has just observed.
147
+ *
148
+ * Pure, because the rule is subtle and the hook that used to hold it has no
149
+ * test file at all — which is how the original defect shipped.
150
+ *
151
+ * Three outcomes:
152
+ * - already claimed -> do nothing (a re-render, or two identical answers)
153
+ * - arrived while thinking -> claim it AND advance the turn
154
+ * - arrived any other time -> ★ CLAIM IT, but do not advance
155
+ *
156
+ * That last case is the one that matters. Interrupting stops playback but
157
+ * nothing cancels the model, so the abandoned answer still lands seconds later
158
+ * while the phase is `listening`. Leaving it unclaimed meant the NEXT question
159
+ * found it waiting and spoke the answer to the question the user had cut off.
160
+ */
161
+ export declare function planReplyConsumption(replyId: string | null | undefined, phase: VoiceSessionPhase, consumedReplyId: string | null): {
162
+ claim: boolean;
163
+ advance: boolean;
164
+ };
165
+ export declare const INITIAL_SESSION: Readonly<VoiceSessionSnapshot>;
166
+ /**
167
+ * The whole session loop, as one pure reduction.
168
+ *
169
+ * `spokenText` is what the assistant is currently saying, supplied so echo can
170
+ * be told from interruption. It is an argument rather than snapshot state
171
+ * because it belongs to the speech engine, not to the session.
172
+ */
173
+ export declare function voiceSessionReducer(snapshot: VoiceSessionSnapshot, event: VoiceSessionEvent, spokenText?: string | null): VoiceSessionSnapshot;
174
+ /**
175
+ * Is the microphone capturing right now?
176
+ *
177
+ * False while the assistant speaks, which is the whole point: an open
178
+ * microphone during playback is what created the feedback loop. False whenever
179
+ * muted, whatever the phase.
180
+ */
181
+ export declare function isCapturing(snapshot: VoiceSessionSnapshot): boolean;
182
+ /** The orb's visual mode. Kept here so the UI has no state logic of its own. */
183
+ export declare function orbMode(snapshot: VoiceSessionSnapshot): 'idle' | 'listening' | 'thinking' | 'speaking';
184
+ //# sourceMappingURL=voiceSession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voiceSession.d.ts","sourceRoot":"","sources":["../../../../src/shared/assistant/voice/voiceSession.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,8EAA8E;AAC9E,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC;AAEzF,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,iBAAiB,CAAC;IACzB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,CAAC;IACf,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACjC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,mBAAmB,GAAG,kBAAkB,GAAG,YAAY,CAAC;AAExF,MAAM,MAAM,iBAAiB;AAC3B,uEAAuE;AACrE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE;AACxD,6CAA6C;GAC3C;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,GACxB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE;AAClC,wEAAwE;GACtE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,iBAAiB,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAEpB;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,QAAQ,EAAE,oBAAoB,GAAG,MAAM,CAElE;AAqCD,wBAAgB,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAY7E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAClC,KAAK,EAAE,iBAAiB,EACxB,eAAe,EAAE,MAAM,GAAG,IAAI,GAC7B;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAItC;AAED,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,oBAAoB,CAMzD,CAAC;AAEH;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,oBAAoB,EAC9B,KAAK,EAAE,iBAAiB,EACxB,UAAU,GAAE,MAAM,GAAG,IAAW,GAC/B,oBAAoB,CAmFtB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,oBAAoB,GAAG,OAAO,CAKnE;AAED,gFAAgF;AAChF,wBAAgB,OAAO,CAAC,QAAQ,EAAE,oBAAoB,GAAG,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,CAatG"}
@@ -0,0 +1,49 @@
1
+ import { VoiceSessionSnapshot } from './voiceSession';
2
+ export type VoiceStageState = 'connecting' | 'listening' | 'endpoint-grace' | 'thinking' | 'speaking' | 'interrupting' | 'mic-error' | 'ended';
3
+ export interface StageInputs {
4
+ snapshot: VoiceSessionSnapshot;
5
+ /** Is the recogniser genuinely running? False during start-up. */
6
+ capturing: boolean;
7
+ /** True from the moment the user cuts the assistant off until listening resumes. */
8
+ interrupting?: boolean;
9
+ /**
10
+ * The user has stopped producing new words and the submit timer is running.
11
+ *
12
+ * Its own state because the checklist asks for it: the two-second correction
13
+ * window should look like a deliberate pause the user can still speak into,
14
+ * not like the session has already moved on.
15
+ */
16
+ awaitingEndpoint?: boolean;
17
+ }
18
+ /**
19
+ * Precedence is the whole design. A failure outranks a phase — a session that
20
+ * cannot hear must never render as "Listening…", which is precisely the lie the
21
+ * operator screenshotted.
22
+ */
23
+ export declare function voiceStageState({ snapshot, capturing, interrupting, awaitingEndpoint, }: StageInputs): VoiceStageState;
24
+ /** Does this stage show the user's live transcript rather than the reply? */
25
+ export declare function showsUserCaption(state: VoiceStageState): boolean;
26
+ /** Does this stage show the assistant's caption? */
27
+ export declare function showsAssistantCaption(state: VoiceStageState): boolean;
28
+ /** Is the microphone control an INTERRUPT rather than a mute toggle? */
29
+ export declare function micIsInterrupt(state: VoiceStageState): boolean;
30
+ /**
31
+ * Does this stage show a LIVE waveform?
32
+ *
33
+ * ★ Shared rather than repeated, because the two surfaces had already drifted:
34
+ * the full stage and the docked strip each answered this question with their
35
+ * own inline expression, so teaching one about a new state silently left the
36
+ * other frozen. `endpoint-grace` in particular MUST animate — it is the window
37
+ * in which the user may still be talking, and a still waveform beside the words
38
+ * "still listening" says the opposite of the label.
39
+ */
40
+ export declare function waveformIsActive(state: VoiceStageState): boolean;
41
+ /**
42
+ * Is the waveform open but WAITING rather than actively hearing speech?
43
+ *
44
+ * The correction window is not the same as mid-sentence listening, and a status
45
+ * line is a weak way to say so: a calmer, shorter profile makes the pause
46
+ * visible without implying the microphone closed.
47
+ */
48
+ export declare function waveformIsSettled(state: VoiceStageState): boolean;
49
+ //# sourceMappingURL=voiceStageState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voiceStageState.d.ts","sourceRoot":"","sources":["../../../../src/shared/assistant/voice/voiceStageState.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE3D,MAAM,MAAM,eAAe,GACzB,YAAY,GAAG,WAAW,GAAG,gBAAgB,GAAG,UAAU,GAAG,UAAU,GAAG,cAAc,GAAG,WAAW,GAAG,OAAO,CAAC;AAEnH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,kEAAkE;IAClE,SAAS,EAAE,OAAO,CAAC;IACnB,oFAAoF;IACpF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,SAAS,EACT,YAAoB,EACpB,gBAAwB,GACzB,EAAE,WAAW,GAAG,eAAe,CAwB/B;AAED,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAKhE;AAED,oDAAoD;AACpD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAErE;AAED,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAE9D;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAEjE"}
@@ -6,4 +6,6 @@ import { buildProgress as y, getAssistantMedia as b, isRecoverable as x, mapSess
6
6
  import { createSandboxAssistantTransport as T } from "./shared/assistant/createSandboxAssistantTransport.js";
7
7
  import { useSandboxAssistantTransport as E } from "./shared/assistant/useSandboxAssistantTransport.js";
8
8
  import { chatTurns as D, groupBySession as O, groupConsecutiveBySession as k, isVoiceTurn as A, voiceMarker as j, voiceSessionIdOf as M, voiceTurnsOnly as N } from "./shared/assistant/voice/turnVisibility.js";
9
- export { v as AssistantRuntimeManager, y as buildProgress, n as buildPromptRequestBody, D as chatTurns, r as checkAssistantAiCredits, i as createAssistantSandbox, a as createAssistantSession, T as createSandboxAssistantTransport, o as downloadAssistantArtifact, s as extendAssistantSandboxTTL, c as findExistingSandbox, l as getAssistantArtifacts, b as getAssistantMedia, u as getAssistantMessages, d as getAssistantSession, f as getAssistantStreamAccess, O as groupBySession, k as groupConsecutiveBySession, t as isQuotaExhaustedError, x as isRecoverable, A as isVoiceTurn, S as mapSessionToHistory, C as mergeMessagesById, p as reportAssistantAiTokenUsage, w as sanitize, m as sendAssistantPromptAsync, h as sendHeartbeat, E as useSandboxAssistantTransport, e as useVoiceInput, j as voiceMarker, M as voiceSessionIdOf, N as voiceTurnsOnly, g as waitForAssistantServiceReady, _ as waitForSandboxRunning };
9
+ import { toCaptionText as P } from "./shared/assistant/voice/captionText.js";
10
+ import { findPendingWrite as F, parseConfirmationRefusal as I } from "./shared/assistant/pendingWrite.js";
11
+ export { v as AssistantRuntimeManager, y as buildProgress, n as buildPromptRequestBody, D as chatTurns, r as checkAssistantAiCredits, i as createAssistantSandbox, a as createAssistantSession, T as createSandboxAssistantTransport, o as downloadAssistantArtifact, s as extendAssistantSandboxTTL, c as findExistingSandbox, F as findPendingWrite, l as getAssistantArtifacts, b as getAssistantMedia, u as getAssistantMessages, d as getAssistantSession, f as getAssistantStreamAccess, O as groupBySession, k as groupConsecutiveBySession, t as isQuotaExhaustedError, x as isRecoverable, A as isVoiceTurn, S as mapSessionToHistory, C as mergeMessagesById, I as parseConfirmationRefusal, p as reportAssistantAiTokenUsage, w as sanitize, m as sendAssistantPromptAsync, h as sendHeartbeat, P as toCaptionText, E as useSandboxAssistantTransport, e as useVoiceInput, j as voiceMarker, M as voiceSessionIdOf, N as voiceTurnsOnly, g as waitForAssistantServiceReady, _ as waitForSandboxRunning };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/fe-libs",
3
- "version": "2026.903.2",
3
+ "version": "2026.903.3",
4
4
  "description": "Burdenoff frontend primitives and domain libraries",
5
5
  "type": "module",
6
6
  "bin": {