import type { ExtensionAPI, ExtensionContext, } from "@earendil-works/pi-coding-agent"; import type { GippityControlConfig } from "../config.ts"; import { getGippityControlConfigPath, readGippityControlConfig, } from "../config-store.ts"; import { resolveVoiceHelperBinary } from "./binary.ts"; import type { CodexVoiceController } from "./controller.ts"; import type { CodexLanVoiceServerController } from "./lan/controller.ts"; import { buildVoiceSetupInstructions } from "./setup.ts"; import { registerCodexVoiceShortcuts } from "./shortcuts.ts"; import { type CodexVoiceMode, codexVoiceSetupMessage } from "./ui.ts"; export interface CodexVoiceControls { setup(ctx: ExtensionContext): Promise; start(mode: CodexVoiceMode, ctx: ExtensionContext): Promise; stop(ctx: ExtensionContext): Promise; toggleInputMute(ctx: ExtensionContext): void; } export function createCodexVoiceControls(options: { pi: ExtensionAPI; state: { config: GippityControlConfig }; voice: CodexVoiceController; lanVoice: CodexLanVoiceServerController; }): CodexVoiceControls { const { pi, state, voice, lanVoice } = options; const requestSetup = async ( ctx: ExtensionContext, retryCommand: string, force: boolean, mode?: CodexVoiceMode, ): Promise => { const currentConfig = readGippityControlConfig(); state.config = currentConfig; if (!force && currentConfig.voice.audioSetupCompleted) return false; if (mode === "realtime" && voice.prepareRealtimePrompt(ctx) === undefined) return true; if (!ctx.isIdle()) { ctx.ui.notify( "Wait for the current turn before setting up GipPity voice.", "info", ); return true; } pi.sendMessage( codexVoiceSetupMessage( buildVoiceSetupInstructions({ config: currentConfig, configPath: getGippityControlConfigPath(), helperPath: resolveVoiceHelperBinary(), retryCommand, }), ), { triggerTurn: true }, ); return true; }; const setup = async (ctx: ExtensionContext): Promise => { await requestSetup(ctx, "/gippity realtime", true); }; const start = async ( mode: CodexVoiceMode, ctx: ExtensionContext, ): Promise => { if (voice.activeMode === mode) return; if (await requestSetup(ctx, `/gippity ${mode}`, false, mode)) return; await voice.start(ctx, state.config, mode); }; const stop = async (_ctx: ExtensionContext): Promise => { if (voice.activeMode === "dictation") await voice.finishDictation({ announce: true }); else await voice.stop({ announce: true }); }; const toggle = async ( mode: CodexVoiceMode, ctx: ExtensionContext, ): Promise => { if (voice.activeMode === mode) await stop(ctx); else await start(mode, ctx); }; const toggleInputMute = (ctx: ExtensionContext): void => { const muted = !voice.inputMuted; if (!voice.setInputMuted(muted)) { ctx.ui.notify( "Start realtime voice before muting the microphone", "info", ); return; } ctx.ui.notify(`Realtime microphone ${muted ? "muted" : "unmuted"}`, "info"); }; registerCodexVoiceShortcuts(pi, state.config, () => state.config, { startDictation: (ctx) => start("dictation", ctx), finishDictation: (ctx) => stop(ctx), toggleDictation: (ctx) => toggle("dictation", ctx), toggleRealtime: (ctx) => toggle("realtime", ctx), toggleInputMute, toggleServer: async (ctx) => { const enabled = !lanVoice.status().running; await lanVoice.setEnabled(enabled, ctx); if (!enabled) ctx.ui.notify("GipPity control server stopped", "info"); }, }); return { setup, start, stop, toggleInputMute }; }