5a027a0081
Splitting the god files made a pile of copy-paste helpers visible and, for the first time, fixable — sharing them previously meant importing a god file. Hashing function bodies through the TypeScript AST found twelve groups desktop-wide; production code is now at zero duplicates. Each helper went to the module that already owns its concern: firstStringField to lib/text, the two REST 404 predicates to lib/gateway-rpc beside isMissingRpcMethod, useDebounced and prefersReducedMotion to their hooks, the superseded-bootstrap guard to electron/ssh-connection, the composer keyup handler to the trigger hook that owns the rest of that state machine, and clampDataUrlReadMaxMb to apps/shared, replacing a "keep these in sync" comment between two copies. Only helpers with no existing owner got a new file: lib/mcp-servers, lib/audio-context, lib/keyed-timeouts, lib/pointer-drag, and the command palette's status row. Error-shape predicates are the worst thing to copy — when the backend changes how it reports a missing route, every copy has to be found.
38 lines
1.1 KiB
TypeScript
38 lines
1.1 KiB
TypeScript
// One lazily-created AudioContext for the app's synthesized UI cues (wake
|
|
// chime, turn-completion sound, thinking blips). Browsers cap how many
|
|
// contexts a page may open, and these cues are short oscillator bursts that
|
|
// need no isolation from each other, so they share one instead of each holding
|
|
// its own. Nothing closes it — it lives for the window.
|
|
|
|
let ctx: AudioContext | null = null
|
|
|
|
/** The shared AudioContext, or null where WebAudio is unavailable. */
|
|
export function getAudioContext(): AudioContext | null {
|
|
if (typeof window === 'undefined') {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
if (!ctx) {
|
|
const Ctor =
|
|
window.AudioContext || (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext
|
|
|
|
if (!Ctor) {
|
|
return null
|
|
}
|
|
|
|
ctx = new Ctor()
|
|
}
|
|
|
|
// Autoplay policies can leave the context suspended until a gesture; a
|
|
// resume() here recovers it once the user has interacted with the window.
|
|
if (ctx.state === 'suspended') {
|
|
void ctx.resume().catch(() => undefined)
|
|
}
|
|
|
|
return ctx
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|