opencode-voice-dictation 0.3.0 → 0.3.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,4 +1,4 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const v=require("@opencode/plugin/gui"),l=require("effect"),W=require("node:child_process"),z=require("node:crypto"),A=require("node:fs"),F=require("node:os"),P=require("node:path"),g=require("solid-js/web"),R=require("solid-js"),ie=v.Plugin.define({os:["windows"],stores:{prefs:v.Store.global(l.Schema.Struct({locale:l.Schema.String,minConfidence:l.Schema.Number,trailingSpace:l.Schema.Boolean,autoFocus:l.Schema.Boolean}),{locale:"es-ES",minConfidence:.4,trailingSpace:!0,autoFocus:!0}),speech:v.Store.global(l.Schema.Struct({voiceURI:l.Schema.String,rate:l.Schema.Number,skipCode:l.Schema.Boolean}),{voiceURI:"",rate:1,skipCode:!0}),engine:v.Store.global(l.Schema.Struct({mode:l.Schema.String}),{mode:"windows"})},i18n:{en:{"pill.idle":"Dictate","pill.starting":"Starting…","pill.listening":"Listening…","pill.dictating":"Dictating…","pill.error":"Retry dictation","pill.title":"Voice dictation","pill.winH":"Open Windows voice typing (Win+H)","pill.stop":"Stop dictation","command.toggle":"Toggle voice dictation","command.toggle.description":"Dictate into the prompt with the selected engine","command.configure":"Voice dictation settings","error.title":"Voice dictation","error.language":"The speech language {locale} is not installed. Available: {available}.","error.none":"Windows has no speech recognizer installed. Settings › Time & language › Language › Speech.","error.mic":"No microphone is available for speech recognition.","error.system":"System.Speech is not available in this Windows installation.","error.exit":"The dictation engine stopped unexpectedly.","error.unknown":"Dictation failed ({code}).","toast.discarded":"Dictation discarded: the active session changed.","settings.title":"Voice dictation","settings.description":"Offline dictation with the Windows speech recognizer. The text is inserted into the prompt; it is never sent automatically.","settings.section":"Dictation","settings.engine":"Dictation engine","settings.engine.description":"Windows uses Windows 11 voice typing (Win+H), which is more accurate. System.Speech works offline but is less precise.","engine.windows":"Windows Voice Typing (Win+H)","engine.system":"System.Speech (offline)","settings.winh":"Test Win+H","settings.winh.description":"Sends the Windows voice typing shortcut. If the floating bar appears, it works.","settings.winh.run":"Test Win+H","settings.winh.sent":"Win+H sent. If the Windows dictation bar did not appear, check Settings › Accessibility › Voice typing.","error.winh":"Windows voice typing could not be triggered.","settings.language":"Language","settings.language.description":"A speech recognizer installed in Windows. Accents and punctuation depend on it.","settings.confidence":"Minimum confidence","settings.confidence.description":"Results below this threshold are ignored to avoid noise.","confidence.low":"Low (0.2)","confidence.normal":"Normal (0.4)","confidence.high":"High (0.6)","settings.space":"Add a space before the text","settings.space.description":"Separates the dictation from what is already in the prompt.","settings.focus":"Focus the prompt after dictating","settings.focus.description":"Moves the cursor to the prompt so you can review or send it.","settings.test.section":"Diagnostics","settings.test":"Test microphone","settings.test.description":"Checks installed recognizers and the default microphone.","settings.test.run":"Run test","settings.test.success":"Speech recognition ready: {locale}","settings.test.failed":"Not ready: {reason}","common.reason.no-recognizers":"no speech language installed","common.reason.no-microphone":"no microphone available","read.idle":"Read","read.speaking":"Reading…","read.paused":"Paused","read.pause":"Pause","read.resume":"Resume","read.restart":"Read again","read.stop":"Stop reading","read.title":"Read the last message aloud","command.read":"Read the last message aloud","command.read.description":"Speaks the last reply with a system voice","read.noSession":"Open a conversation to read its last message.","read.empty":"The last message has no text to read.","read.error":"The last message could not be read.","read.noVoices":"Windows has no voices installed for reading.","speech.section":"Read aloud","speech.voice":"Voice","speech.voice.description":"A system voice. Automatic picks one in the app language.","speech.voice.auto":"Automatic","speech.rate":"Speed","speech.rate.description":"How fast the text is spoken.","speech.rate.slow":"0.8×","speech.rate.normal":"1×","speech.rate.fast":"1.2×","speech.rate.faster":"1.5×","speech.skipCode":"Skip code blocks","speech.skipCode.description":"Read only the prose; code, paths and commands are left out.","speech.preview":"Test voice","speech.preview.description":"Speaks a short sample with the current voice and speed.","speech.preview.text":"Hello, this is a reading test."},es:{"pill.idle":"Dictar","pill.starting":"Iniciando…","pill.listening":"Escuchando…","pill.dictating":"Dictando…","pill.error":"Reintentar dictado","pill.title":"Dictado por voz","pill.winH":"Abrir el dictado de Windows (Win+H)","pill.stop":"Detener dictado","command.toggle":"Alternar dictado por voz","command.toggle.description":"Dicta en el prompt con el motor elegido","command.configure":"Ajustes de dictado por voz","error.title":"Dictado por voz","error.language":"El idioma de reconocimiento {locale} no está instalado. Disponibles: {available}.","error.none":"Windows no tiene ningún reconocedor de voz instalado. Configuración › Hora e idioma › Idioma › Voz.","error.mic":"No hay ningún micrófono disponible para el reconocimiento de voz.","error.system":"System.Speech no está disponible en esta instalación de Windows.","error.exit":"El motor de dictado se detuvo inesperadamente.","error.unknown":"El dictado falló ({code}).","toast.discarded":"Dictado descartado: la sesión activa cambió.","settings.title":"Dictado por voz","settings.description":"Dictado offline con el reconocedor de voz de Windows. El texto se inserta en el prompt; nunca se envía solo.","settings.section":"Dictado","settings.engine":"Motor de dictado","settings.engine.description":"Windows usa el dictado de Windows 11 (Win+H), más preciso. System.Speech funciona sin Windows 11 pero es menos exacto.","engine.windows":"Windows Voice Typing (Win+H)","engine.system":"System.Speech (offline)","settings.winh":"Probar Win+H","settings.winh.description":"Envía el atajo del dictado de Windows. Si aparece la barra flotante, funciona.","settings.winh.run":"Probar Win+H","settings.winh.sent":"Win+H enviado. Si no apareció la barra, revisa Configuración › Accesibilidad › Dictado por voz.","settings.language":"Idioma","settings.language.description":"Un reconocedor instalado en Windows. Los acentos y la puntuación dependen de él.","settings.confidence":"Confianza mínima","settings.confidence.description":"Los resultados por debajo de este umbral se ignoran para evitar ruido.","confidence.low":"Baja (0,2)","confidence.normal":"Normal (0,4)","confidence.high":"Alta (0,6)","settings.space":"Añadir un espacio antes del texto","settings.space.description":"Separa el dictado de lo que ya hay en el prompt.","settings.focus":"Enfocar el prompt al terminar","settings.focus.description":"Mueve el cursor al prompt para revisar o enviar el texto.","settings.test.section":"Diagnóstico","settings.test":"Probar micrófono","settings.test.description":"Comprueba los reconocedores instalados y el micrófono predeterminado.","settings.test.run":"Ejecutar prueba","settings.test.success":"Reconocimiento de voz listo: {locale}","settings.test.failed":"No está listo: {reason}","common.reason.no-recognizers":"no hay ningún idioma de voz instalado","common.reason.no-microphone":"no hay micrófono disponible","read.idle":"Leer","read.speaking":"Leyendo…","read.paused":"Pausado","read.pause":"Pausar","read.resume":"Reanudar","read.restart":"Leer de nuevo","read.stop":"Detener lectura","read.title":"Leer en voz alta el último mensaje","command.read":"Leer en voz alta el último mensaje","command.read.description":"Reproduce la última respuesta con una voz del sistema","read.noSession":"Abre una conversación para leer su último mensaje.","read.empty":"El último mensaje no tiene texto para leer.","read.error":"No se pudo leer el último mensaje.","read.noVoices":"Windows no tiene voces instaladas para la lectura.","speech.section":"Lectura en voz alta","speech.voice":"Voz","speech.voice.description":"Una voz del sistema. Automática elige una en el idioma de la app.","speech.voice.auto":"Automática","speech.rate":"Velocidad","speech.rate.description":"A qué ritmo se lee el texto.","speech.rate.slow":"0,8×","speech.rate.normal":"1×","speech.rate.fast":"1,2×","speech.rate.faster":"1,5×","speech.skipCode":"Ignorar bloques de código","speech.skipCode.description":"Lee solo la prosa; omite código, rutas y comandos.","speech.preview":"Probar voz","speech.preview.description":"Lee una frase corta con la voz y velocidad actuales.","speech.preview.text":"Hola, esta es una prueba de lectura."}}}),D=v.Ipc.define({id:"voice-dictation.dictation",state:l.Schema.Struct({status:l.Schema.Literals(["idle","starting","listening","error"]),error:l.Schema.String,locale:l.Schema.String}),methods:{start:{input:l.Schema.Struct({locale:l.Schema.String,minConfidence:l.Schema.Number})},stop:{},diagnose:{output:l.Schema.Struct({locales:l.Schema.Array(l.Schema.String),microphone:l.Schema.Boolean,selected:l.Schema.String,error:l.Schema.String})}},events:{final:l.Schema.Struct({text:l.Schema.String,confidence:l.Schema.Number}),error:l.Schema.Struct({code:l.Schema.String,detail:l.Schema.String,available:l.Schema.Array(l.Schema.String)})}}),q=v.Ipc.define({id:"voice-dictation.speech",methods:{speak:{input:l.Schema.Struct({text:l.Schema.String,rate:l.Schema.Number,voice:l.Schema.String})},stop:{},voices:{output:l.Schema.Array(l.Schema.String)}},events:{done:l.Schema.Struct({ok:l.Schema.Boolean})}}),H=v.Ipc.define({id:"voice-dictation.voice-typing",methods:{toggle:{}}}),j=String.raw`param(
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const S=require("@opencode/plugin/gui"),d=require("effect"),z=require("node:child_process"),V=require("node:crypto"),A=require("node:fs"),D=require("node:os"),L=require("node:path"),g=require("solid-js/web"),T=require("solid-js"),ne=S.Plugin.define({os:["windows"],stores:{prefs:S.Store.global(d.Schema.Struct({locale:d.Schema.String,minConfidence:d.Schema.Number,trailingSpace:d.Schema.Boolean,autoFocus:d.Schema.Boolean}),{locale:"es-ES",minConfidence:.4,trailingSpace:!0,autoFocus:!0}),speech:S.Store.global(d.Schema.Struct({voiceURI:d.Schema.String,rate:d.Schema.Number,skipCode:d.Schema.Boolean}),{voiceURI:"",rate:1,skipCode:!0}),engine:S.Store.global(d.Schema.Struct({mode:d.Schema.String}),{mode:"windows"})},i18n:{en:{"pill.idle":"Dictate","pill.starting":"Starting…","pill.listening":"Listening…","pill.dictating":"Dictating…","pill.error":"Retry dictation","pill.title":"Voice dictation","pill.winH":"Open Windows voice typing (Win+H)","pill.stop":"Stop dictation","command.toggle":"Toggle voice dictation","command.toggle.description":"Dictate into the prompt with the selected engine","command.configure":"Voice dictation settings","error.title":"Voice dictation","error.language":"The speech language {locale} is not installed. Available: {available}.","error.none":"Windows has no speech recognizer installed. Settings › Time & language › Language › Speech.","error.mic":"No microphone is available for speech recognition.","error.system":"System.Speech is not available in this Windows installation.","error.exit":"The dictation engine stopped unexpectedly.","error.unknown":"Dictation failed ({code}).","toast.discarded":"Dictation discarded: the active session changed.","settings.title":"Voice dictation","settings.description":"Offline dictation with the Windows speech recognizer. The text is inserted into the prompt; it is never sent automatically.","settings.section":"Dictation","settings.engine":"Dictation engine","settings.engine.description":"Windows uses Windows 11 voice typing (Win+H), which is more accurate. System.Speech works offline but is less precise.","engine.windows":"Windows Voice Typing (Win+H)","engine.system":"System.Speech (offline)","settings.winh":"Test Win+H","settings.winh.description":"Sends the Windows voice typing shortcut. If the floating bar appears, it works.","settings.winh.run":"Test Win+H","settings.winh.sent":"Win+H sent. If the Windows dictation bar did not appear, check Settings › Accessibility › Voice typing.","error.winh":"Windows voice typing could not be triggered.","settings.language":"Language","settings.language.description":"A speech recognizer installed in Windows. Accents and punctuation depend on it.","settings.confidence":"Minimum confidence","settings.confidence.description":"Results below this threshold are ignored to avoid noise.","confidence.low":"Low (0.2)","confidence.normal":"Normal (0.4)","confidence.high":"High (0.6)","settings.space":"Add a space before the text","settings.space.description":"Separates the dictation from what is already in the prompt.","settings.focus":"Focus the prompt after dictating","settings.focus.description":"Moves the cursor to the prompt so you can review or send it.","settings.test.section":"Diagnostics","settings.test":"Test microphone","settings.test.description":"Checks installed recognizers and the default microphone.","settings.test.run":"Run test","settings.test.success":"Speech recognition ready: {locale}","settings.test.failed":"Not ready: {reason}","common.reason.no-recognizers":"no speech language installed","common.reason.no-microphone":"no microphone available","read.idle":"Read","read.speaking":"Reading…","read.paused":"Paused","read.pause":"Pause","read.resume":"Resume","read.restart":"Read again","read.stop":"Stop reading","read.title":"Read the last message aloud","command.read":"Read the last message aloud","command.read.description":"Speaks the last reply with a system voice","read.noSession":"Open a conversation to read its last message.","read.empty":"The last message has no text to read.","read.error":"The last message could not be read.","read.noVoices":"Windows has no voices installed for reading.","speech.section":"Read aloud","speech.voice":"Voice","speech.voice.description":"A system voice. Automatic picks one in the app language.","speech.voice.auto":"Automatic","speech.rate":"Speed","speech.rate.description":"How fast the text is spoken.","speech.rate.slow":"0.8×","speech.rate.normal":"1×","speech.rate.fast":"1.2×","speech.rate.faster":"1.5×","speech.skipCode":"Skip code blocks","speech.skipCode.description":"Read only the prose; code, paths and commands are left out.","speech.preview":"Test voice","speech.preview.description":"Speaks a short sample with the current voice and speed.","speech.preview.text":"Hello, this is a reading test."},es:{"pill.idle":"Dictar","pill.starting":"Iniciando…","pill.listening":"Escuchando…","pill.dictating":"Dictando…","pill.error":"Reintentar dictado","pill.title":"Dictado por voz","pill.winH":"Abrir el dictado de Windows (Win+H)","pill.stop":"Detener dictado","command.toggle":"Alternar dictado por voz","command.toggle.description":"Dicta en el prompt con el motor elegido","command.configure":"Ajustes de dictado por voz","error.title":"Dictado por voz","error.language":"El idioma de reconocimiento {locale} no está instalado. Disponibles: {available}.","error.none":"Windows no tiene ningún reconocedor de voz instalado. Configuración › Hora e idioma › Idioma › Voz.","error.mic":"No hay ningún micrófono disponible para el reconocimiento de voz.","error.system":"System.Speech no está disponible en esta instalación de Windows.","error.exit":"El motor de dictado se detuvo inesperadamente.","error.unknown":"El dictado falló ({code}).","toast.discarded":"Dictado descartado: la sesión activa cambió.","settings.title":"Dictado por voz","settings.description":"Dictado offline con el reconocedor de voz de Windows. El texto se inserta en el prompt; nunca se envía solo.","settings.section":"Dictado","settings.engine":"Motor de dictado","settings.engine.description":"Windows usa el dictado de Windows 11 (Win+H), más preciso. System.Speech funciona sin Windows 11 pero es menos exacto.","engine.windows":"Windows Voice Typing (Win+H)","engine.system":"System.Speech (offline)","settings.winh":"Probar Win+H","settings.winh.description":"Envía el atajo del dictado de Windows. Si aparece la barra flotante, funciona.","settings.winh.run":"Probar Win+H","settings.winh.sent":"Win+H enviado. Si no apareció la barra, revisa Configuración › Accesibilidad › Dictado por voz.","settings.language":"Idioma","settings.language.description":"Un reconocedor instalado en Windows. Los acentos y la puntuación dependen de él.","settings.confidence":"Confianza mínima","settings.confidence.description":"Los resultados por debajo de este umbral se ignoran para evitar ruido.","confidence.low":"Baja (0,2)","confidence.normal":"Normal (0,4)","confidence.high":"Alta (0,6)","settings.space":"Añadir un espacio antes del texto","settings.space.description":"Separa el dictado de lo que ya hay en el prompt.","settings.focus":"Enfocar el prompt al terminar","settings.focus.description":"Mueve el cursor al prompt para revisar o enviar el texto.","settings.test.section":"Diagnóstico","settings.test":"Probar micrófono","settings.test.description":"Comprueba los reconocedores instalados y el micrófono predeterminado.","settings.test.run":"Ejecutar prueba","settings.test.success":"Reconocimiento de voz listo: {locale}","settings.test.failed":"No está listo: {reason}","common.reason.no-recognizers":"no hay ningún idioma de voz instalado","common.reason.no-microphone":"no hay micrófono disponible","read.idle":"Leer","read.speaking":"Leyendo…","read.paused":"Pausado","read.pause":"Pausar","read.resume":"Reanudar","read.restart":"Leer de nuevo","read.stop":"Detener lectura","read.title":"Leer en voz alta el último mensaje","command.read":"Leer en voz alta el último mensaje","command.read.description":"Reproduce la última respuesta con una voz del sistema","read.noSession":"Abre una conversación para leer su último mensaje.","read.empty":"El último mensaje no tiene texto para leer.","read.error":"No se pudo leer el último mensaje.","read.noVoices":"Windows no tiene voces instaladas para la lectura.","speech.section":"Lectura en voz alta","speech.voice":"Voz","speech.voice.description":"Una voz del sistema. Automática elige una en el idioma de la app.","speech.voice.auto":"Automática","speech.rate":"Velocidad","speech.rate.description":"A qué ritmo se lee el texto.","speech.rate.slow":"0,8×","speech.rate.normal":"1×","speech.rate.fast":"1,2×","speech.rate.faster":"1,5×","speech.skipCode":"Ignorar bloques de código","speech.skipCode.description":"Lee solo la prosa; omite código, rutas y comandos.","speech.preview":"Probar voz","speech.preview.description":"Lee una frase corta con la voz y velocidad actuales.","speech.preview.text":"Hola, esta es una prueba de lectura."}}}),H=S.Ipc.define({id:"voice-dictation.dictation",state:d.Schema.Struct({status:d.Schema.Literals(["idle","starting","listening","error"]),error:d.Schema.String,locale:d.Schema.String}),methods:{start:{input:d.Schema.Struct({locale:d.Schema.String,minConfidence:d.Schema.Number})},stop:{},diagnose:{output:d.Schema.Struct({locales:d.Schema.Array(d.Schema.String),microphone:d.Schema.Boolean,selected:d.Schema.String,error:d.Schema.String})}},events:{final:d.Schema.Struct({text:d.Schema.String,confidence:d.Schema.Number}),error:d.Schema.Struct({code:d.Schema.String,detail:d.Schema.String,available:d.Schema.Array(d.Schema.String)})}}),q=S.Ipc.define({id:"voice-dictation.speech",methods:{speak:{input:d.Schema.Struct({text:d.Schema.String,rate:d.Schema.Number,voice:d.Schema.String})},stop:{},voices:{output:d.Schema.Array(d.Schema.String)}},events:{done:d.Schema.Struct({ok:d.Schema.Boolean})}}),U=S.Ipc.define({id:"voice-dictation.voice-typing",methods:{toggle:{}}}),j=String.raw`param(
2
2
  [string]$Culture = "es-ES",
3
3
  [double]$MinConfidence = 0.4,
4
4
  [switch]$Diagnose
@@ -105,9 +105,9 @@ while ($true) {
105
105
  Start-Sleep -Milliseconds 250
106
106
  }
107
107
  }
108
- `,ne="powershell.exe";function K(){const e=P.join(F.tmpdir(),"opencode-voice-dictation");A.mkdirSync(e,{recursive:!0});const t=z.createHash("sha1").update(j).digest("hex").slice(0,12),i=P.join(e,`dictation-${t}.ps1`);return A.writeFileSync(i,j,"utf8"),i}function Z(e){return W.spawn(ne,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function X(e){const t=e.pid;if(t===void 0){e.kill();return}W.spawn("taskkill",["/PID",String(t),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>e.kill())}function oe(e,t){const i=Z(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",K(),"-Culture",e.locale,"-MinConfidence",String(e.minConfidence)]);let c=!1,o="",r="";const p=s=>{switch(s.type){case"ready":t.onReady(s.locale??e.locale);break;case"final":s.text&&t.onFinal(s.text,s.confidence??0);break;case"error":t.onError(s.code??"UNKNOWN",s.detail??"",s.available??[]);break}};return i.stdout?.setEncoding("utf8"),i.stdout?.on("data",s=>{r+=s;let a=r.indexOf(`
109
- `);for(;a>=0;){const d=r.slice(0,a).replace(/\r$/,"").trim();if(r=r.slice(a+1),d)try{p(JSON.parse(d))}catch{}a=r.indexOf(`
110
- `)}}),i.stderr?.setEncoding("utf8"),i.stderr?.on("data",s=>{o=(o+s).slice(-2e3)}),i.on("error",s=>{c||t.onError("ENGINE_START_FAILED",s.message,[])}),i.on("close",s=>{c||t.onExit(s,o.trim().slice(-500))}),{stop(){c=!0,X(i)}}}function re(){return new Promise((e,t)=>{const i=Z(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",K(),"-Diagnose"]);let c="",o="",r=!1;const p=(a,d)=>{r||(r=!0,clearTimeout(s),a?t(a):e(d))},s=setTimeout(()=>{X(i),p(new Error("diagnose timed out"))},2e4);i.stdout?.setEncoding("utf8"),i.stdout?.on("data",a=>{c+=a}),i.stderr?.setEncoding("utf8"),i.stderr?.on("data",a=>{o+=a}),i.on("error",a=>p(a)),i.on("close",()=>{const a=c.split(/\r?\n/).map(d=>d.trim()).filter(d=>d.startsWith("{")).at(-1);if(!a){p(new Error(o.trim()||"no output from PowerShell"));return}try{const d=JSON.parse(a);p(void 0,{locales:d.locales??[],microphone:d.microphone===!0,selected:d.selected??"",error:d.error??""})}catch(d){p(d instanceof Error?d:new Error(String(d)))}})})}const M=String.raw`param(
108
+ `,ie="powershell.exe";function K(){const e=L.join(D.tmpdir(),"opencode-voice-dictation");A.mkdirSync(e,{recursive:!0});const t=V.createHash("sha1").update(j).digest("hex").slice(0,12),n=L.join(e,`dictation-${t}.ps1`);return A.writeFileSync(n,j,"utf8"),n}function Z(e){return z.spawn(ie,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function X(e){const t=e.pid;if(t===void 0){e.kill();return}z.spawn("taskkill",["/PID",String(t),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>e.kill())}function oe(e,t){const n=Z(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",K(),"-Culture",e.locale,"-MinConfidence",String(e.minConfidence)]);let l=!1,o="",r="";const u=s=>{switch(s.type){case"ready":t.onReady(s.locale??e.locale);break;case"final":s.text&&t.onFinal(s.text,s.confidence??0);break;case"error":t.onError(s.code??"UNKNOWN",s.detail??"",s.available??[]);break}};return n.stdout?.setEncoding("utf8"),n.stdout?.on("data",s=>{r+=s;let a=r.indexOf(`
109
+ `);for(;a>=0;){const p=r.slice(0,a).replace(/\r$/,"").trim();if(r=r.slice(a+1),p)try{u(JSON.parse(p))}catch{}a=r.indexOf(`
110
+ `)}}),n.stderr?.setEncoding("utf8"),n.stderr?.on("data",s=>{o=(o+s).slice(-2e3)}),n.on("error",s=>{l||t.onError("ENGINE_START_FAILED",s.message,[])}),n.on("close",s=>{l||t.onExit(s,o.trim().slice(-500))}),{stop(){l=!0,X(n)}}}function re(){return new Promise((e,t)=>{const n=Z(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",K(),"-Diagnose"]);let l="",o="",r=!1;const u=(a,p)=>{r||(r=!0,clearTimeout(s),a?t(a):e(p))},s=setTimeout(()=>{X(n),u(new Error("diagnose timed out"))},2e4);n.stdout?.setEncoding("utf8"),n.stdout?.on("data",a=>{l+=a}),n.stderr?.setEncoding("utf8"),n.stderr?.on("data",a=>{o+=a}),n.on("error",a=>u(a)),n.on("close",()=>{const a=l.split(/\r?\n/).map(p=>p.trim()).filter(p=>p.startsWith("{")).at(-1);if(!a){u(new Error(o.trim()||"no output from PowerShell"));return}try{const p=JSON.parse(a);u(void 0,{locales:p.locales??[],microphone:p.microphone===!0,selected:p.selected??"",error:p.error??""})}catch(p){u(p instanceof Error?p:new Error(String(p)))}})})}const M=String.raw`param(
111
111
  [string]$TextFile = "",
112
112
  [int]$Rate = 0,
113
113
  [string]$Voice = "",
@@ -160,7 +160,7 @@ try {
160
160
  } catch { }
161
161
 
162
162
  $synth.Dispose()
163
- `,se="powershell.exe";function Y(){const e=P.join(F.tmpdir(),"opencode-voice-dictation");return A.mkdirSync(e,{recursive:!0}),e}function J(){const e=z.createHash("sha1").update(M).digest("hex").slice(0,12),t=P.join(Y(),`speak-${e}.ps1`);return A.writeFileSync(t,M,"utf8"),t}function ae(e){const t=P.join(Y(),`speech-${z.randomUUID()}.txt`);return A.writeFileSync(t,e,"utf8"),t}function Q(e){return W.spawn(se,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function x(e){const t=e.pid;if(t===void 0){e.kill();return}W.spawn("taskkill",["/PID",String(t),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>e.kill())}function ce(e){return Math.max(-10,Math.min(10,Math.round((e-1)*5)))}function le(e,t){const i=ae(e.text),c=Q(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",J(),"-TextFile",i,"-Rate",String(ce(e.rate)),"-Voice",e.voice]);let o=!1;const r=()=>{try{A.rmSync(i,{force:!0})}catch{}};return c.on("error",()=>{r(),o||t(!1)}),c.on("close",p=>{r(),o||t(p===0)}),{stop(){o=!0,x(c),r()}}}function de(){return new Promise(e=>{const t=Q(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",J(),"-ListVoices"]);let i="",c=!1;const o=p=>{c||(c=!0,clearTimeout(r),e(p))},r=setTimeout(()=>{x(t),o([])},1e4);t.stdout?.setEncoding("utf8"),t.stdout?.on("data",p=>{i+=p}),t.on("error",()=>o([])),t.on("close",()=>{const p=i.split(/\r?\n/).map(s=>s.trim()).filter(s=>s.length>0);o(p)})})}const G=String.raw`$ErrorActionPreference = "Stop"
163
+ `,se="powershell.exe";function Y(){const e=L.join(D.tmpdir(),"opencode-voice-dictation");return A.mkdirSync(e,{recursive:!0}),e}function J(){const e=V.createHash("sha1").update(M).digest("hex").slice(0,12),t=L.join(Y(),`speak-${e}.ps1`);return A.writeFileSync(t,M,"utf8"),t}function ae(e){const t=L.join(Y(),`speech-${V.randomUUID()}.txt`);return A.writeFileSync(t,e,"utf8"),t}function Q(e){return z.spawn(se,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function x(e){const t=e.pid;if(t===void 0){e.kill();return}z.spawn("taskkill",["/PID",String(t),"/T","/F"],{windowsHide:!0,stdio:"ignore"}).on("error",()=>e.kill())}function ce(e){return Math.max(-10,Math.min(10,Math.round((e-1)*5)))}function le(e,t){const n=ae(e.text),l=Q(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",J(),"-TextFile",n,"-Rate",String(ce(e.rate)),"-Voice",e.voice]);let o=!1;const r=()=>{try{A.rmSync(n,{force:!0})}catch{}};return l.on("error",()=>{r(),o||t(!1)}),l.on("close",u=>{r(),o||t(u===0)}),{stop(){o=!0,x(l),r()}}}function de(){return new Promise(e=>{const t=Q(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",J(),"-ListVoices"]);let n="",l=!1;const o=u=>{l||(l=!0,clearTimeout(r),e(u))},r=setTimeout(()=>{x(t),o([])},1e4);t.stdout?.setEncoding("utf8"),t.stdout?.on("data",u=>{n+=u}),t.on("error",()=>o([])),t.on("close",()=>{const u=n.split(/\r?\n/).map(s=>s.trim()).filter(s=>s.length>0);o(u)})})}const G=String.raw`$ErrorActionPreference = "Stop"
164
164
 
165
165
  Add-Type @"
166
166
  using System;
@@ -187,6 +187,6 @@ public static class WinH {
187
187
  "@
188
188
 
189
189
  [WinH]::Send()
190
- `,pe="powershell.exe";function ue(){const e=P.join(F.tmpdir(),"opencode-voice-dictation");A.mkdirSync(e,{recursive:!0});const t=z.createHash("sha1").update(G).digest("hex").slice(0,12),i=P.join(e,`win-h-${t}.ps1`);return A.writeFileSync(i,G,"utf8"),i}function ge(e){return W.spawn(pe,e,{windowsHide:!0,stdio:"ignore"})}function me(){return new Promise((e,t)=>{const i=ge(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",ue()]);i.on("error",c=>t(c)),i.on("close",c=>{c===0?e():t(new Error(`Win+H helper exited with code ${c??"unknown"}`))})})}const he=e=>{let t="idle",i="",c="",o,r=!1;const p=()=>{o=void 0,r=!1},s=e.provide(D,{state:()=>({status:t,error:i,locale:c}),start(b,k){o&&(r=!0,o.stop(),p()),t="starting",i="",c=b.locale,s.changed();const I=k.window;try{o=oe({locale:b.locale,minConfidence:b.minConfidence},{onReady(h){t="listening",c=h,s.changed()},onFinal(h,m){s.emit("final",{text:h,confidence:m},I)},onError(h,m,T){t="error",i=h,s.changed(),s.emit("error",{code:h,detail:m,available:T},I)},onExit(h,m){if(r){p(),t="idle",i="",c="",s.changed();return}p(),t!=="error"&&(t="error",i=h===null?"ENGINE_EXIT":`ENGINE_EXIT_${h}`,s.changed(),s.emit("error",{code:"ENGINE_EXIT",detail:m||(h===null?"":String(h)),available:[]},I))}})}catch(h){throw t="error",i="ENGINE_START_FAILED",s.changed(),h}},stop(){o&&(r=!0,o.stop(),p()),t="idle",i="",c="",s.changed()},diagnose(){return re()}});let a;const d=e.provide(q,{speak(b){a?.stop(),a=le(b,k=>{a=void 0,d.emit("done",{ok:k})})},stop(){a&&(a.stop(),a=void 0),d.emit("done",{ok:!0})},voices(){return de()}});e.provide(H,{toggle(){return me()}}),e.onCleanup(()=>{o&&(r=!0,o.stop(),p()),a?.stop(),a=void 0})},fe=250;function Se(e,t){let i=e.replace(/\r\n/g,`
191
- `);return t?i=i.replace(/```[\s\S]*?```/g," "):i=i.replace(/```[^\n]*\n([\s\S]*?)```/g,"$1"),i=i.replace(/`([^`]+)`/g,"$1"),i=i.replace(/!\[[^\]]*\]\([^)]*\)/g," "),i=i.replace(/\[([^\]]+)\]\([^)]*\)/g,"$1"),i=i.replace(/^\s*#{1,6}\s+/gm,""),i=i.replace(/^\s*>\s?/gm,""),i=i.replace(/^\s*[-*+]\s+/gm,""),i=i.replace(/^\s*\d+\.\s+/gm,""),i=i.replace(/(\*\*|__)(.*?)\1/g,"$2"),i=i.replace(/(\*|_)(.*?)\1/g,"$2"),i=i.replace(/^\s*\|.*\|\s*$/gm," "),i=i.replace(/\n{2,}/g,". "),i=i.replace(/\n/g," "),i=i.replace(/\s+/g," "),i=i.replace(/\s+([.,;:!?])/g,"$1"),i=i.replace(/([.,;:!?]){2,}/g,"$1"),i.trim()}function ve(e,t=fe){const i=e.trim();if(!i)return[];const c=i.split(new RegExp("(?<=[.!?…])\\s+")),o=[];let r="";const p=s=>{let a="";for(const d of s.split(/\s+/))d&&(a&&a.length+1+d.length>t?(o.push(a),a=d):a=a?`${a} ${d}`:d);return a};for(const s of c){if(s.length>t){r&&(o.push(r),r=""),r=p(s);continue}r&&r.length+1+s.length>t?(o.push(r),r=s):r=r?`${r} ${s}`:s}return r&&o.push(r),o}function we(e,t){const i=e.stores.speech,[c,o]=R.createSignal(!1),[r,p]=R.createSignal(!1),[s,a]=R.createSignal([]),[d,b]=R.createSignal("none");let k=0,I=[],h="";const m=()=>typeof globalThis.speechSynthesis<"u"?globalThis.speechSynthesis:void 0,T=()=>{const w=m()?.getVoices()??[];if(w.length===0)return;const y=i.value.voiceURI;if(y){const C=w.find(O=>O.voiceURI===y);if(C)return C}const E=e.locale.locale().split("-")[0]?.toLowerCase()??"es";return w.find(C=>C.lang.toLowerCase().startsWith(E))??w[0]},N=async()=>{const w=m()?.getVoices()??[];if(w.length>0){a(w.map(y=>({id:y.voiceURI,label:`${y.name} — ${y.lang}`}))),b("web");return}try{const y=await t.voices();if(y.length>0){a(y.map(E=>({id:E,label:E}))),b("sapi");return}}catch{}b("none")},u=()=>{k++,I=[],m()?.cancel(),p(!1),o(!1),t.stop().catch(()=>{})},f=()=>d()==="web",$=()=>{!c()||r()||!f()||(m()?.pause(),p(!0))},n=()=>{!c()||!r()||!f()||(m()?.resume(),p(!1))},S=()=>{r()?n():$()},U=()=>h?L(h):Promise.resolve(!1),L=async w=>{h=w;const y=Se(w,i.value.skipCode);if(!y)return!1;if(u(),await N(),d()==="web"){const E=m();if(!E)return!1;const C=T(),O=i.value.rate,te=k;I=ve(y),o(!0),p(!1);const V=()=>{if(te!==k)return;const B=I.shift();if(!B){p(!1),o(!1);return}const _=new SpeechSynthesisUtterance(B);C&&(_.voice=C),C?.lang&&(_.lang=C.lang),_.rate=O,_.onend=V,_.onerror=V,E.speak(_)};return V(),!0}if(d()==="sapi"){o(!0),p(!1);try{await t.speak({text:y,rate:i.value.rate,voice:i.value.voiceURI})}catch(E){throw o(!1),E}return!0}throw o(!1),new Error(e.t("read.noVoices"))},ee=async()=>{const w=e.sessions.current();if(!w)return"";const E=(await w.server.client.message.list({sessionID:w.id,type:"assistant",order:"desc",limit:1})).data[0];return!E||E.type!=="assistant"?"":E.content.filter(C=>C.type==="text").map(C=>C.text).join(`
192
- `)};return t.on("done",()=>{p(!1),o(!1)}),m()?.addEventListener?.("voiceschanged",()=>{N()}),N(),{speaking:c,paused:r,backend:d,canPause:f,voices:s,refreshVoices:N,lastAssistantText:ee,speak:L,cancel:u,pause:$,resume:n,togglePause:S,restart:U,preview:()=>L(e.t("speech.preview.text"))}}const ye=["es-ES","es-MX","es-AR","es-CO","pt-BR","pt-PT","en-US","en-GB","fr-FR","de-DE","it-IT"];function be(e){const t=v.usePlugin(),i=t.ui,c=t.stores.prefs,o=t.stores.speech,r=t.stores.engine,p=t.main(D),s=t.main(H),[a,d]=R.createSignal([]),[b,k]=R.createSignal(!1);R.onMount(()=>{p.diagnose().then(n=>{n.locales.length>0&&d(n.locales)}).catch(()=>{}),e.speaker.refreshVoices()});const I=()=>{const n=a(),S=c.value.locale;return(n.includes(S)?n:[S,...n]).map(L=>({value:L,label:L}))},h=()=>[{value:"",label:t.t("speech.voice.auto")},...e.speaker.voices().map(n=>({value:n.id,label:n.label}))],m=n=>{c.update(S=>{n.locale!==void 0&&(S.locale=n.locale),n.minConfidence!==void 0&&(S.minConfidence=n.minConfidence),n.trailingSpace!==void 0&&(S.trailingSpace=n.trailingSpace),n.autoFocus!==void 0&&(S.autoFocus=n.autoFocus)})},T=n=>{o.update(S=>{n.voiceURI!==void 0&&(S.voiceURI=n.voiceURI),n.rate!==void 0&&(S.rate=n.rate),n.skipCode!==void 0&&(S.skipCode=n.skipCode)})},N=n=>{r.update(S=>{S.mode=n})},u=async()=>{try{await s.toggle(),t.toast.show({variant:"default",title:t.t("settings.winh"),description:t.t("settings.winh.sent")})}catch(n){t.toast.show({variant:"error",title:t.t("pill.title"),description:n instanceof Error?n.message:t.t("error.winh")})}},f=async()=>{try{await e.speaker.preview()}catch(n){t.toast.show({variant:"error",title:t.t("read.title"),description:n instanceof Error?n.message:t.t("read.error")})}},$=async()=>{k(!0);try{const n=await p.diagnose();if(n.locales.length===0){t.toast.show({variant:"error",title:t.t("error.title"),description:t.t("settings.test.failed",{reason:t.t("common.reason.no-recognizers")}),persistent:!0});return}if(!n.microphone){t.toast.show({variant:"error",title:t.t("error.title"),description:t.t("settings.test.failed",{reason:t.t("common.reason.no-microphone")}),persistent:!0});return}n.locales.length>0&&!a().length&&d(n.locales),t.toast.show({variant:"success",title:t.t("error.title"),description:t.t("settings.test.success",{locale:n.selected||n.locales[0]||c.value.locale})})}catch(n){t.toast.show({variant:"error",title:t.t("error.title"),description:n instanceof Error?n.message:String(n)})}finally{k(!1)}};return g.createComponent(i.SettingsLayout,{get title(){return t.t("settings.title")},get description(){return t.t("settings.description")},get children(){return[g.createComponent(i.SettingsSection,{get title(){return t.t("settings.section")},get children(){return[g.createComponent(i.SettingsRow,{id:"engine",get title(){return t.t("settings.engine")},get description(){return t.t("settings.engine.description")},get children(){return g.createComponent(i.Select,{get label(){return t.t("settings.engine")},get value(){return r.value.mode},get options(){return[{value:"windows",label:t.t("engine.windows")},{value:"system",label:t.t("engine.system")}]},onChange:n=>N(n)})}}),g.createComponent(R.Show,{get when(){return r.value.mode==="windows"},get children(){return g.createComponent(i.SettingsRow,{id:"winh",get title(){return t.t("settings.winh")},get description(){return t.t("settings.winh.description")},get children(){return g.createComponent(i.Button,{onClick:()=>{u()},get children(){return t.t("settings.winh.run")}})}})}}),g.createComponent(R.Show,{get when(){return r.value.mode==="system"},get children(){return[g.createComponent(i.SettingsRow,{id:"language",get title(){return t.t("settings.language")},get description(){return t.t("settings.language.description")},get children(){return g.createComponent(i.Select,{get label(){return t.t("settings.language")},get value(){return c.value.locale},get options(){return g.memo(()=>I().length>0)()?I():ye.map(n=>({value:n,label:n}))},onChange:n=>m({locale:n})})}}),g.createComponent(i.SettingsRow,{id:"confidence",get title(){return t.t("settings.confidence")},get description(){return t.t("settings.confidence.description")},get children(){return g.createComponent(i.Select,{get label(){return t.t("settings.confidence")},get value(){return String(c.value.minConfidence)},get options(){return[{value:"0.2",label:t.t("confidence.low")},{value:"0.4",label:t.t("confidence.normal")},{value:"0.6",label:t.t("confidence.high")}]},onChange:n=>m({minConfidence:Number(n)||.4})})}}),g.createComponent(i.SettingsRow,{id:"space",get title(){return t.t("settings.space")},get description(){return t.t("settings.space.description")},get children(){return g.createComponent(i.Switch,{get label(){return t.t("settings.space")},hideLabel:!0,get checked(){return c.value.trailingSpace},onChange:n=>m({trailingSpace:n})})}}),g.createComponent(i.SettingsRow,{id:"focus",get title(){return t.t("settings.focus")},get description(){return t.t("settings.focus.description")},get children(){return g.createComponent(i.Switch,{get label(){return t.t("settings.focus")},hideLabel:!0,get checked(){return c.value.autoFocus},onChange:n=>m({autoFocus:n})})}})]}})]}}),g.createComponent(i.SettingsSection,{get title(){return t.t("speech.section")},get children(){return[g.createComponent(i.SettingsRow,{id:"voice",get title(){return t.t("speech.voice")},get description(){return t.t("speech.voice.description")},get children(){return g.createComponent(i.Select,{get label(){return t.t("speech.voice")},get value(){return o.value.voiceURI},get options(){return h()},onChange:n=>T({voiceURI:n})})}}),g.createComponent(i.SettingsRow,{id:"rate",get title(){return t.t("speech.rate")},get description(){return t.t("speech.rate.description")},get children(){return g.createComponent(i.Select,{get label(){return t.t("speech.rate")},get value(){return String(o.value.rate)},get options(){return[{value:"0.8",label:t.t("speech.rate.slow")},{value:"1",label:t.t("speech.rate.normal")},{value:"1.2",label:t.t("speech.rate.fast")},{value:"1.5",label:t.t("speech.rate.faster")}]},onChange:n=>T({rate:Number(n)||1})})}}),g.createComponent(i.SettingsRow,{id:"skip-code",get title(){return t.t("speech.skipCode")},get description(){return t.t("speech.skipCode.description")},get children(){return g.createComponent(i.Switch,{get label(){return t.t("speech.skipCode")},hideLabel:!0,get checked(){return o.value.skipCode},onChange:n=>T({skipCode:n})})}}),g.createComponent(i.SettingsRow,{id:"preview",get title(){return t.t("speech.preview")},get description(){return t.t("speech.preview.description")},get children(){return g.createComponent(i.Button,{onClick:()=>{f()},get children(){return t.t("read.idle")}})}})]}}),g.createComponent(i.SettingsSection,{get title(){return t.t("settings.test.section")},get children(){return g.createComponent(i.SettingsRow,{id:"test",get title(){return t.t("settings.test")},get description(){return t.t("settings.test.description")},get children(){return g.createComponent(i.Button,{get busy(){return b()},onClick:()=>{$()},get children(){return t.t("settings.test.run")}})}})}})]}})}const $e=e=>{const t=e.main(D),i=e.main(q),c=e.main(H),o=we(e,i),r=e.stores.prefs,p=e.stores.engine,[s,a]=R.createSignal(!1);let d;const b=(u,f,$)=>{switch(u){case"LANGUAGE_NOT_AVAILABLE":return e.t("error.language",{locale:f||r.value.locale,available:$.length>0?$.join(", "):"—"});case"NO_RECOGNIZERS":return e.t("error.none");case"MICROPHONE_UNAVAILABLE":return e.t("error.mic");case"SYSTEM_SPEECH_UNAVAILABLE":return e.t("error.system");case"ENGINE_EXIT":return e.t("error.exit");default:return e.t("error.unknown",{code:u})}},k=()=>{s()&&(a(!1),c.toggle().catch(()=>{})),o.cancel(),d=e.sessions.current()?.key,t.start({locale:r.value.locale,minConfidence:r.value.minConfidence}).catch(u=>{e.toast.show({variant:"error",title:e.t("error.title"),description:u instanceof Error?u.message:String(u)})})},I=()=>{t.stop().catch(()=>{})},h=()=>{o.cancel(),e.screen.current()?.composer.focus(),c.toggle().then(()=>a(u=>!u)).catch(u=>{a(!1),e.toast.show({variant:"error",title:e.t("pill.title"),description:u instanceof Error?`${e.t("error.winh")} ${u.message}`:e.t("error.winh")})})},m=()=>{if(p.value.mode==="windows"){h();return}const u=t.state()?.status??"idle";u==="starting"||u==="listening"?I():k()},T=()=>{if(o.speaking()){o.cancel();return}if(!e.sessions.current()){e.toast.show({variant:"error",title:e.t("read.title"),description:e.t("read.noSession")});return}const u=t.state()?.status??"idle";(u==="starting"||u==="listening")&&t.stop().catch(()=>{}),(async()=>{try{const f=await o.lastAssistantText();if(!f.trim()){e.toast.show({variant:"default",title:e.t("read.title"),description:e.t("read.empty")});return}await o.speak(f)||e.toast.show({variant:"default",title:e.t("read.title"),description:e.t("read.empty")})}catch(f){e.toast.show({variant:"error",title:e.t("read.title"),description:f instanceof Error?f.message:e.t("read.error")})}})()};t.on("final",u=>{const f=e.sessions.current()?.key;if(d&&f&&f!==d){e.toast.show({variant:"error",title:e.t("error.title"),description:e.t("toast.discarded")});return}const $=e.screen.current()?.composer;if(!$)return;const n=$.text(),S=r.value.trailingSpace&&n.length>0&&!/\s$/.test(n)?" ":"";$.insert(S+u.text),r.value.autoFocus&&$.focus()}),t.on("error",u=>{e.toast.show({variant:"error",title:e.t("error.title"),description:b(u.code,u.detail,u.available),persistent:u.code==="LANGUAGE_NOT_AVAILABLE"||u.code==="NO_RECOGNIZERS",duration:8e3})});const N=()=>{if(e.app.platform!=="desktop")return;if(p.value.mode==="windows")return s()?{label:e.t("pill.dictating"),icon:"outline-wave-audio",tone:"danger",title:e.t("pill.stop"),run:m}:{label:e.t("pill.idle"),icon:"outline-wave-audio",title:e.t("pill.winH"),run:m};const u=t.state()?.status??"idle";return u==="starting"?{label:e.t("pill.starting"),icon:"outline-wave-audio",title:e.t("pill.title"),run:m}:u==="listening"?{label:e.t("pill.listening"),icon:"outline-wave-audio",tone:"danger",title:e.t("pill.stop"),run:m}:u==="error"?{label:e.t("pill.error"),icon:"outline-wave-audio",tone:"warning",title:e.t("pill.title"),run:m}:{label:e.t("pill.idle"),icon:"outline-wave-audio",title:e.t("pill.title"),run:m}};e.add(v.ComposerPill,()=>({id:"dictate",show:()=>N()})),e.add(v.ComposerPill,()=>({id:"read",order:1,show:()=>{if(e.app.platform!=="desktop")return;if(!o.speaking())return{label:e.t("read.idle"),icon:"outline-audio",title:e.t("read.title"),run:T};const u=o.paused(),f=[];return o.canPause()&&f.push({id:"pause",label:u?e.t("read.resume"):e.t("read.pause"),icon:"outline-audio",run:()=>o.togglePause()}),f.push({id:"restart",label:e.t("read.restart"),icon:"outline-reset",run:()=>{o.restart()}}),f.push({id:"stop",label:e.t("read.stop"),icon:"xmark-small",tone:"danger",run:()=>o.cancel()}),{label:u?e.t("read.paused"):e.t("read.speaking"),icon:"outline-audio",tone:"success",title:e.t("read.title"),rows:f}}})),e.add(v.Command,()=>({id:"toggle",title:e.t("command.toggle"),description:e.t("command.toggle.description"),keywords:"voice dictation microphone speech dictado voz microfono",group:e.t("pill.title"),section:"prompt",bind:"mod+shift+m",slash:{name:"dictado"},editable:!0,enabled:e.app.platform==="desktop",run:m})),e.add(v.Command,()=>({id:"configure",title:e.t("command.configure"),keywords:"voice dictation microphone speech dictado voz microfono",group:e.t("pill.title"),enabled:e.app.platform==="desktop",run:()=>e.layout.settings(e.id)})),e.add(v.Command,()=>({id:"read",title:e.t("command.read"),description:e.t("command.read.description"),keywords:"read aloud speech voice leer voz altavoz lectura",group:e.t("pill.title"),section:"prompt",bind:"mod+shift+l",slash:{name:"leer"},editable:!0,enabled:e.app.platform==="desktop",run:T})),e.add(v.SettingsPage,{id:e.id,icon:"outline-audio",get title(){return e.t("settings.title")},get entries(){return[{id:"engine",title:e.t("settings.engine"),keywords:"engine motor dictation dictado winh voz"},{id:"winh",title:e.t("settings.winh"),keywords:"winh windows voice typing dictado"},{id:"language",title:e.t("settings.language"),keywords:"voice language idioma voz"},{id:"confidence",title:e.t("settings.confidence"),keywords:"voice confidence confianza"},{id:"test",title:e.t("settings.test"),keywords:"voice microphone microphone test microfono"},{id:"voice",title:e.t("speech.voice"),keywords:"read aloud voice voz lectura altavoz"},{id:"rate",title:e.t("speech.rate"),keywords:"read aloud speed velocidad lectura"},{id:"skip-code",title:e.t("speech.skipCode"),keywords:"read aloud code codigo lectura"},{id:"preview",title:e.t("speech.preview"),keywords:"read aloud test voice probar voz"}]},render:()=>g.createComponent(be,{speaker:o})})},Ee=v.Plugin.define({...ie,setup:$e,main:he});exports.default=Ee;
190
+ `,pe="powershell.exe";function ue(){const e=L.join(D.tmpdir(),"opencode-voice-dictation");A.mkdirSync(e,{recursive:!0});const t=V.createHash("sha1").update(G).digest("hex").slice(0,12),n=L.join(e,`win-h-${t}.ps1`);return A.writeFileSync(n,G,"utf8"),n}function ge(e){return z.spawn(pe,e,{windowsHide:!0,stdio:"ignore"})}function me(){return new Promise((e,t)=>{const n=ge(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",ue()]);n.on("error",l=>t(l)),n.on("close",l=>{l===0?e():t(new Error(`Win+H helper exited with code ${l??"unknown"}`))})})}const he=e=>{let t="idle",n="",l="",o,r=!1;const u=()=>{o=void 0,r=!1},s=e.provide(H,{state:()=>({status:t,error:n,locale:l}),start(y,E){o&&(r=!0,o.stop(),u()),t="starting",n="",l=y.locale,s.changed();const C=E.window;try{o=oe({locale:y.locale,minConfidence:y.minConfidence},{onReady(f){t="listening",l=f,s.changed()},onFinal(f,h){s.emit("final",{text:f,confidence:h},C)},onError(f,h,k){t="error",n=f,s.changed(),s.emit("error",{code:f,detail:h,available:k},C)},onExit(f,h){if(r){u(),t="idle",n="",l="",s.changed();return}u(),t!=="error"&&(t="error",n=f===null?"ENGINE_EXIT":`ENGINE_EXIT_${f}`,s.changed(),s.emit("error",{code:"ENGINE_EXIT",detail:h||(f===null?"":String(f)),available:[]},C))}})}catch(f){throw t="error",n="ENGINE_START_FAILED",s.changed(),f}},stop(){o&&(r=!0,o.stop(),u()),t="idle",n="",l="",s.changed()},diagnose(){return re()}});let a;const p=e.provide(q,{speak(y){a?.stop(),a=le(y,E=>{a=void 0,p.emit("done",{ok:E})})},stop(){a&&(a.stop(),a=void 0)},voices(){return de()}});e.provide(U,{toggle(){return me()}}),e.onCleanup(()=>{o&&(r=!0,o.stop(),u()),a?.stop(),a=void 0})},fe=250;function Se(e,t){let n=e.replace(/\r\n/g,`
191
+ `);return t?n=n.replace(/```[\s\S]*?```/g," "):n=n.replace(/```[^\n]*\n([\s\S]*?)```/g,"$1"),n=n.replace(/`([^`]+)`/g,"$1"),n=n.replace(/!\[[^\]]*\]\([^)]*\)/g," "),n=n.replace(/\[([^\]]+)\]\([^)]*\)/g,"$1"),n=n.replace(/^\s*#{1,6}\s+/gm,""),n=n.replace(/^\s*>\s?/gm,""),n=n.replace(/^\s*[-*+]\s+/gm,""),n=n.replace(/^\s*\d+\.\s+/gm,""),n=n.replace(/(\*\*|__)(.*?)\1/g,"$2"),n=n.replace(/(\*|_)(.*?)\1/g,"$2"),n=n.replace(/^\s*\|.*\|\s*$/gm," "),n=n.replace(/\n{2,}/g,". "),n=n.replace(/\n/g," "),n=n.replace(/\s+/g," "),n=n.replace(/\s+([.,;:!?])/g,"$1"),n=n.replace(/([.,;:!?]){2,}/g,"$1"),n.trim()}function ve(e,t=fe){const n=e.trim();if(!n)return[];const l=n.split(new RegExp("(?<=[.!?…])\\s+")),o=[];let r="";const u=s=>{let a="";for(const p of s.split(/\s+/))p&&(a&&a.length+1+p.length>t?(o.push(a),a=p):a=a?`${a} ${p}`:p);return a};for(const s of l){if(s.length>t){r&&(o.push(r),r=""),r=u(s);continue}r&&r.length+1+s.length>t?(o.push(r),r=s):r=r?`${r} ${s}`:s}return r&&o.push(r),o}function we(e,t){const n=e.stores.speech,[l,o]=T.createSignal(!1),[r,u]=T.createSignal(!1),[s,a]=T.createSignal([]),[p,y]=T.createSignal("none");let E=0,C=[],f="";const h=()=>typeof globalThis.speechSynthesis<"u"?globalThis.speechSynthesis:void 0,k=()=>{const v=h()?.getVoices()??[];if(v.length===0)return;const w=n.value.voiceURI;if(w){const $=v.find(O=>O.voiceURI===w);if($)return $}const b=e.locale.locale().split("-")[0]?.toLowerCase()??"es";return v.find($=>$.lang.toLowerCase().startsWith(b))??v[0]},R=async()=>{const v=h()?.getVoices()??[];if(v.length>0){a(v.map(w=>({id:w.voiceURI,label:`${w.name} — ${w.lang}`}))),y("web");return}try{const w=await t.voices();if(w.length>0){a(w.map(b=>({id:b,label:b}))),y("sapi");return}}catch{}y("none")},_=()=>{E++,C=[],h()?.cancel(),u(!1),o(!1),p()==="sapi"&&t.stop().catch(()=>{})},P=()=>p()==="web",c=()=>{!l()||r()||!P()||(h()?.pause(),u(!0))},i=()=>{!l()||!r()||!P()||(h()?.resume(),u(!1))},m=()=>{r()?i():c()},I=()=>f?N(f):Promise.resolve(!1),N=async v=>{f=v;const w=Se(v,n.value.skipCode);if(!w)return!1;if(_(),await R(),p()==="web"){const b=h();if(!b)return!1;const $=k(),O=n.value.rate,te=E;C=ve(w),o(!0),u(!1);const F=()=>{if(te!==E)return;const B=C.shift();if(!B){u(!1),o(!1);return}const W=new SpeechSynthesisUtterance(B);$&&(W.voice=$),$?.lang&&(W.lang=$.lang),W.rate=O,W.onend=F,W.onerror=F,b.speak(W)};return F(),!0}if(p()==="sapi"){o(!0),u(!1);try{await t.speak({text:w,rate:n.value.rate,voice:n.value.voiceURI})}catch(b){throw o(!1),b}return!0}throw o(!1),new Error(e.t("read.noVoices"))},ee=async()=>{const v=e.sessions.current();if(!v)return"";const b=(await v.server.client.message.list({sessionID:v.id,type:"assistant",order:"desc",limit:1})).data[0];return!b||b.type!=="assistant"?"":b.content.filter($=>$.type==="text").map($=>$.text).join(`
192
+ `)};return t.on("done",()=>{u(!1),o(!1)}),h()?.addEventListener?.("voiceschanged",()=>{R()}),R(),{speaking:l,paused:r,backend:p,canPause:P,voices:s,refreshVoices:R,lastAssistantText:ee,speak:N,cancel:_,pause:c,resume:i,togglePause:m,restart:I,preview:()=>N(e.t("speech.preview.text"))}}const ye=["es-ES","es-MX","es-AR","es-CO","pt-BR","pt-PT","en-US","en-GB","fr-FR","de-DE","it-IT"];function be(e){const t=S.usePlugin(),n=t.ui,l=t.stores.prefs,o=t.stores.speech,r=t.stores.engine,u=t.main(H),s=t.main(U),[a,p]=T.createSignal([]),[y,E]=T.createSignal(!1);T.onMount(()=>{u.diagnose().then(i=>{i.locales.length>0&&p(i.locales)}).catch(()=>{}),e.speaker.refreshVoices()});const C=()=>{const i=a(),m=l.value.locale;return(i.includes(m)?i:[m,...i]).map(N=>({value:N,label:N}))},f=()=>[{value:"",label:t.t("speech.voice.auto")},...e.speaker.voices().map(i=>({value:i.id,label:i.label}))],h=i=>{l.update(m=>{i.locale!==void 0&&(m.locale=i.locale),i.minConfidence!==void 0&&(m.minConfidence=i.minConfidence),i.trailingSpace!==void 0&&(m.trailingSpace=i.trailingSpace),i.autoFocus!==void 0&&(m.autoFocus=i.autoFocus)})},k=i=>{o.update(m=>{i.voiceURI!==void 0&&(m.voiceURI=i.voiceURI),i.rate!==void 0&&(m.rate=i.rate),i.skipCode!==void 0&&(m.skipCode=i.skipCode)})},R=i=>{r.update(m=>{m.mode=i})},_=async()=>{try{await s.toggle(),t.toast.show({variant:"default",title:t.t("settings.winh"),description:t.t("settings.winh.sent")})}catch(i){t.toast.show({variant:"error",title:t.t("pill.title"),description:i instanceof Error?i.message:t.t("error.winh")})}},P=async()=>{try{await e.speaker.preview()}catch(i){t.toast.show({variant:"error",title:t.t("read.title"),description:i instanceof Error?i.message:t.t("read.error")})}},c=async()=>{E(!0);try{const i=await u.diagnose();if(i.locales.length===0){t.toast.show({variant:"error",title:t.t("error.title"),description:t.t("settings.test.failed",{reason:t.t("common.reason.no-recognizers")}),persistent:!0});return}if(!i.microphone){t.toast.show({variant:"error",title:t.t("error.title"),description:t.t("settings.test.failed",{reason:t.t("common.reason.no-microphone")}),persistent:!0});return}i.locales.length>0&&!a().length&&p(i.locales),t.toast.show({variant:"success",title:t.t("error.title"),description:t.t("settings.test.success",{locale:i.selected||i.locales[0]||l.value.locale})})}catch(i){t.toast.show({variant:"error",title:t.t("error.title"),description:i instanceof Error?i.message:String(i)})}finally{E(!1)}};return g.createComponent(n.SettingsLayout,{get title(){return t.t("settings.title")},get description(){return t.t("settings.description")},get children(){return[g.createComponent(n.SettingsSection,{get title(){return t.t("settings.section")},get children(){return[g.createComponent(n.SettingsRow,{id:"engine",get title(){return t.t("settings.engine")},get description(){return t.t("settings.engine.description")},get children(){return g.createComponent(n.Select,{get label(){return t.t("settings.engine")},get value(){return r.value.mode},get options(){return[{value:"windows",label:t.t("engine.windows")},{value:"system",label:t.t("engine.system")}]},onChange:i=>R(i)})}}),g.createComponent(T.Show,{get when(){return r.value.mode==="windows"},get children(){return g.createComponent(n.SettingsRow,{id:"winh",get title(){return t.t("settings.winh")},get description(){return t.t("settings.winh.description")},get children(){return g.createComponent(n.Button,{onClick:()=>{_()},get children(){return t.t("settings.winh.run")}})}})}}),g.createComponent(T.Show,{get when(){return r.value.mode==="system"},get children(){return[g.createComponent(n.SettingsRow,{id:"language",get title(){return t.t("settings.language")},get description(){return t.t("settings.language.description")},get children(){return g.createComponent(n.Select,{get label(){return t.t("settings.language")},get value(){return l.value.locale},get options(){return g.memo(()=>C().length>0)()?C():ye.map(i=>({value:i,label:i}))},onChange:i=>h({locale:i})})}}),g.createComponent(n.SettingsRow,{id:"confidence",get title(){return t.t("settings.confidence")},get description(){return t.t("settings.confidence.description")},get children(){return g.createComponent(n.Select,{get label(){return t.t("settings.confidence")},get value(){return String(l.value.minConfidence)},get options(){return[{value:"0.2",label:t.t("confidence.low")},{value:"0.4",label:t.t("confidence.normal")},{value:"0.6",label:t.t("confidence.high")}]},onChange:i=>h({minConfidence:Number(i)||.4})})}}),g.createComponent(n.SettingsRow,{id:"space",get title(){return t.t("settings.space")},get description(){return t.t("settings.space.description")},get children(){return g.createComponent(n.Switch,{get label(){return t.t("settings.space")},hideLabel:!0,get checked(){return l.value.trailingSpace},onChange:i=>h({trailingSpace:i})})}}),g.createComponent(n.SettingsRow,{id:"focus",get title(){return t.t("settings.focus")},get description(){return t.t("settings.focus.description")},get children(){return g.createComponent(n.Switch,{get label(){return t.t("settings.focus")},hideLabel:!0,get checked(){return l.value.autoFocus},onChange:i=>h({autoFocus:i})})}})]}})]}}),g.createComponent(n.SettingsSection,{get title(){return t.t("speech.section")},get children(){return[g.createComponent(n.SettingsRow,{id:"voice",get title(){return t.t("speech.voice")},get description(){return t.t("speech.voice.description")},get children(){return g.createComponent(n.Select,{get label(){return t.t("speech.voice")},get value(){return o.value.voiceURI},get options(){return f()},onChange:i=>k({voiceURI:i})})}}),g.createComponent(n.SettingsRow,{id:"rate",get title(){return t.t("speech.rate")},get description(){return t.t("speech.rate.description")},get children(){return g.createComponent(n.Select,{get label(){return t.t("speech.rate")},get value(){return String(o.value.rate)},get options(){return[{value:"0.8",label:t.t("speech.rate.slow")},{value:"1",label:t.t("speech.rate.normal")},{value:"1.2",label:t.t("speech.rate.fast")},{value:"1.5",label:t.t("speech.rate.faster")}]},onChange:i=>k({rate:Number(i)||1})})}}),g.createComponent(n.SettingsRow,{id:"skip-code",get title(){return t.t("speech.skipCode")},get description(){return t.t("speech.skipCode.description")},get children(){return g.createComponent(n.Switch,{get label(){return t.t("speech.skipCode")},hideLabel:!0,get checked(){return o.value.skipCode},onChange:i=>k({skipCode:i})})}}),g.createComponent(n.SettingsRow,{id:"preview",get title(){return t.t("speech.preview")},get description(){return t.t("speech.preview.description")},get children(){return g.createComponent(n.Button,{onClick:()=>{P()},get children(){return t.t("read.idle")}})}})]}}),g.createComponent(n.SettingsSection,{get title(){return t.t("settings.test.section")},get children(){return g.createComponent(n.SettingsRow,{id:"test",get title(){return t.t("settings.test")},get description(){return t.t("settings.test.description")},get children(){return g.createComponent(n.Button,{get busy(){return y()},onClick:()=>{c()},get children(){return t.t("settings.test.run")}})}})}})]}})}function $e(e){const t=S.usePlugin();return g.memo(()=>e.actions().map(n=>g.createComponent(t.ui.Button,{get variant(){return n.id==="stop"?"danger":"neutral"},get icon(){return n.icon},onClick:()=>{n.run(),e.close()},get children(){return n.label}})))}const Ee=e=>{const t=e.main(H),n=e.main(q),l=e.main(U),o=we(e,n),r=e.stores.prefs,u=e.stores.engine,[s,a]=T.createSignal(!1);let p;const y=(c,i,m)=>{switch(c){case"LANGUAGE_NOT_AVAILABLE":return e.t("error.language",{locale:i||r.value.locale,available:m.length>0?m.join(", "):"—"});case"NO_RECOGNIZERS":return e.t("error.none");case"MICROPHONE_UNAVAILABLE":return e.t("error.mic");case"SYSTEM_SPEECH_UNAVAILABLE":return e.t("error.system");case"ENGINE_EXIT":return e.t("error.exit");default:return e.t("error.unknown",{code:c})}},E=()=>{s()&&(a(!1),l.toggle().catch(()=>{})),o.cancel(),p=e.sessions.current()?.key,t.start({locale:r.value.locale,minConfidence:r.value.minConfidence}).catch(c=>{e.toast.show({variant:"error",title:e.t("error.title"),description:c instanceof Error?c.message:String(c)})})},C=()=>{t.stop().catch(()=>{})},f=()=>{o.cancel(),e.screen.current()?.composer.focus(),l.toggle().then(()=>a(c=>!c)).catch(c=>{a(!1),e.toast.show({variant:"error",title:e.t("pill.title"),description:c instanceof Error?`${e.t("error.winh")} ${c.message}`:e.t("error.winh")})})},h=()=>{if(u.value.mode==="windows"){f();return}const c=t.state()?.status??"idle";c==="starting"||c==="listening"?C():E()},k=()=>{if(o.speaking()){o.cancel();return}if(!e.sessions.current()){e.toast.show({variant:"error",title:e.t("read.title"),description:e.t("read.noSession")});return}const c=t.state()?.status??"idle";(c==="starting"||c==="listening")&&t.stop().catch(()=>{}),(async()=>{try{const i=await o.lastAssistantText();if(!i.trim()){e.toast.show({variant:"default",title:e.t("read.title"),description:e.t("read.empty")});return}await o.speak(i)||e.toast.show({variant:"default",title:e.t("read.title"),description:e.t("read.empty")})}catch(i){e.toast.show({variant:"error",title:e.t("read.title"),description:i instanceof Error?i.message:e.t("read.error")})}})()},R=()=>{if(!o.speaking())return[{id:"start",label:e.t("read.idle"),icon:"outline-audio",run:k}];const c=o.paused(),i=[{id:"restart",label:e.t("read.restart"),icon:"outline-reset",run:()=>{o.restart()}}];return o.canPause()&&i.push({id:"pause",label:c?e.t("read.resume"):e.t("read.pause"),icon:"outline-audio",run:()=>o.togglePause()}),i.push({id:"stop",label:e.t("read.stop"),icon:"xmark-small",run:()=>o.cancel()}),i};t.on("final",c=>{const i=e.sessions.current()?.key;if(p&&i&&i!==p){e.toast.show({variant:"error",title:e.t("error.title"),description:e.t("toast.discarded")});return}const m=e.screen.current()?.composer;if(!m)return;const I=m.text(),N=r.value.trailingSpace&&I.length>0&&!/\s$/.test(I)?" ":"";m.insert(N+c.text),r.value.autoFocus&&m.focus()}),t.on("error",c=>{e.toast.show({variant:"error",title:e.t("error.title"),description:y(c.code,c.detail,c.available),persistent:c.code==="LANGUAGE_NOT_AVAILABLE"||c.code==="NO_RECOGNIZERS",duration:8e3})});const _=()=>{if(e.app.platform!=="desktop")return;if(u.value.mode==="windows")return s()?{label:e.t("pill.dictating"),icon:"outline-wave-audio",tone:"danger",title:e.t("pill.stop"),run:h}:{label:e.t("pill.idle"),icon:"outline-wave-audio",title:e.t("pill.winH"),run:h};const c=t.state()?.status??"idle";return c==="starting"?{label:e.t("pill.starting"),icon:"outline-wave-audio",title:e.t("pill.title"),run:h}:c==="listening"?{label:e.t("pill.listening"),icon:"outline-wave-audio",tone:"danger",title:e.t("pill.stop"),run:h}:c==="error"?{label:e.t("pill.error"),icon:"outline-wave-audio",tone:"warning",title:e.t("pill.title"),run:h}:{label:e.t("pill.idle"),icon:"outline-wave-audio",title:e.t("pill.title"),run:h}},P=()=>{if(e.app.platform!=="desktop")return;const c=o.speaking(),i=o.paused(),m=c?R().map(I=>({id:I.id,label:I.label,icon:I.icon,tone:I.id==="stop"?"danger":void 0,run:I.run})):[];return{label:c?i?e.t("read.paused"):e.t("read.speaking"):e.t("read.idle"),icon:"outline-audio",tone:c?"success":void 0,title:e.t("read.title"),rows:m.length>0?m:void 0,run:c?void 0:k}};e.add(S.ComposerPill,()=>({id:"dictate",show:()=>_()})),e.add(S.ComposerPill,()=>({id:"read",order:1,show:()=>P(),menu:c=>g.createComponent($e,{actions:R,get close(){return c.close}})})),e.add(S.Command,()=>({id:"toggle",title:e.t("command.toggle"),description:e.t("command.toggle.description"),keywords:"voice dictation microphone speech dictado voz microfono",group:e.t("pill.title"),section:"prompt",bind:"mod+shift+m",slash:{name:"dictado"},editable:!0,enabled:e.app.platform==="desktop",run:h})),e.add(S.Command,()=>({id:"configure",title:e.t("command.configure"),keywords:"voice dictation microphone speech dictado voz microfono",group:e.t("pill.title"),enabled:e.app.platform==="desktop",run:()=>e.layout.settings(e.id)})),e.add(S.Command,()=>({id:"read",title:e.t("command.read"),description:e.t("command.read.description"),keywords:"read aloud speech voice leer voz altavoz lectura",group:e.t("pill.title"),section:"prompt",bind:"mod+shift+l",slash:{name:"leer"},editable:!0,enabled:e.app.platform==="desktop",run:k})),e.add(S.SettingsPage,{id:e.id,icon:"outline-audio",get title(){return e.t("settings.title")},get entries(){return[{id:"engine",title:e.t("settings.engine"),keywords:"engine motor dictation dictado winh voz"},{id:"winh",title:e.t("settings.winh"),keywords:"winh windows voice typing dictado"},{id:"language",title:e.t("settings.language"),keywords:"voice language idioma voz"},{id:"confidence",title:e.t("settings.confidence"),keywords:"voice confidence confianza"},{id:"test",title:e.t("settings.test"),keywords:"voice microphone microphone test microfono"},{id:"voice",title:e.t("speech.voice"),keywords:"read aloud voice voz lectura altavoz"},{id:"rate",title:e.t("speech.rate"),keywords:"read aloud speed velocidad lectura"},{id:"skip-code",title:e.t("speech.skipCode"),keywords:"read aloud code codigo lectura"},{id:"preview",title:e.t("speech.preview"),keywords:"read aloud test voice probar voz"}]},render:()=>g.createComponent(be,{speaker:o})})},Ce=S.Plugin.define({...ne,setup:Ee,main:he});exports.default=Ce;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-voice-dictation",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Offline voice dictation and read-aloud for OpenCode Desktop on Windows",
5
5
  "license": "MIT",
6
6
  "author": "Fran Carlos <francarlos803@gmail.com>",
package/src/gui/main.ts CHANGED
@@ -124,7 +124,6 @@ export const main: Plugin.MainSetup = (ctx) => {
124
124
  talking.stop()
125
125
  talking = undefined
126
126
  }
127
- speech.emit("done", { ok: true })
128
127
  },
129
128
 
130
129
  voices() {
package/src/gui/speak.ts CHANGED
@@ -167,7 +167,7 @@ export function createSpeaker(
167
167
  synth()?.cancel()
168
168
  setPaused(false)
169
169
  setSpeaking(false)
170
- void speech.stop().catch(() => {})
170
+ if (backend() === "sapi") void speech.stop().catch(() => {})
171
171
  }
172
172
 
173
173
  const canPause = () => backend() === "web"
@@ -3,15 +3,45 @@ import {
3
3
  Command,
4
4
  ComposerPill,
5
5
  SettingsPage,
6
+ usePlugin,
6
7
  type ComposerPillRow,
7
8
  type ComposerPillView,
9
+ type IconName,
8
10
  type Plugin,
9
11
  } from "@opencode/plugin/gui"
10
12
  import { Dictation, Speech, VoiceTyping } from "./contract"
11
13
  import type { definition } from "./definition"
12
- import { createSpeaker } from "./speak"
14
+ import { createSpeaker, type Speaker } from "./speak"
13
15
  import { VoiceSettings } from "./settings"
14
16
 
17
+ interface ReadAction {
18
+ id: string
19
+ label: string
20
+ icon: IconName
21
+ run: () => void
22
+ }
23
+
24
+ function ReadMenu(props: { actions: () => ReadAction[]; close: () => void }) {
25
+ const ctx = usePlugin<typeof definition>()
26
+
27
+ return (
28
+ <>
29
+ {props.actions().map((action) => (
30
+ <ctx.ui.Button
31
+ variant={action.id === "stop" ? "danger" : "neutral"}
32
+ icon={action.icon}
33
+ onClick={() => {
34
+ action.run()
35
+ props.close()
36
+ }}
37
+ >
38
+ {action.label}
39
+ </ctx.ui.Button>
40
+ ))}
41
+ </>
42
+ )
43
+ }
44
+
15
45
  /**
16
46
  * Window setup: the microphone pill above the composer, the keybind and the
17
47
  * `/dictado` slash command.
@@ -133,6 +163,30 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
133
163
  })()
134
164
  }
135
165
 
166
+ const readActions = (): ReadAction[] => {
167
+ if (!speaker.speaking()) {
168
+ return [{ id: "start", label: ctx.t("read.idle"), icon: "outline-audio", run: read }]
169
+ }
170
+
171
+ const paused = speaker.paused()
172
+ const actions: ReadAction[] = [
173
+ { id: "restart", label: ctx.t("read.restart"), icon: "outline-reset", run: () => void speaker.restart() },
174
+ ]
175
+
176
+ if (speaker.canPause()) {
177
+ actions.push({
178
+ id: "pause",
179
+ label: paused ? ctx.t("read.resume") : ctx.t("read.pause"),
180
+ icon: "outline-audio",
181
+ run: () => speaker.togglePause(),
182
+ })
183
+ }
184
+
185
+ actions.push({ id: "stop", label: ctx.t("read.stop"), icon: "xmark-small", run: () => speaker.cancel() })
186
+
187
+ return actions
188
+ }
189
+
136
190
  dictation.on("final", (data) => {
137
191
  const current = ctx.sessions.current()?.key
138
192
 
@@ -210,53 +264,38 @@ export const setup: Plugin.Setup<typeof definition> = (ctx) => {
210
264
  return { label: ctx.t("pill.idle"), icon: "outline-wave-audio", title: ctx.t("pill.title"), run: toggle }
211
265
  }
212
266
 
267
+ const readView = (): ComposerPillView | undefined => {
268
+ if (ctx.app.platform !== "desktop") return undefined
269
+
270
+ const speaking = speaker.speaking()
271
+ const paused = speaker.paused()
272
+ const rows: ComposerPillRow[] = speaking
273
+ ? readActions().map((action): ComposerPillRow => ({
274
+ id: action.id,
275
+ label: action.label,
276
+ icon: action.icon,
277
+ tone: action.id === "stop" ? "danger" : undefined,
278
+ run: action.run,
279
+ }))
280
+ : []
281
+
282
+ return {
283
+ label: speaking ? (paused ? ctx.t("read.paused") : ctx.t("read.speaking")) : ctx.t("read.idle"),
284
+ icon: "outline-audio",
285
+ tone: speaking ? "success" : undefined,
286
+ title: ctx.t("read.title"),
287
+ rows: rows.length > 0 ? rows : undefined,
288
+ run: speaking ? undefined : read,
289
+ }
290
+ }
291
+
213
292
  ctx.add(ComposerPill, () => ({ id: "dictate", show: () => view() }))
214
293
 
215
294
  ctx.add(ComposerPill, () => ({
216
295
  id: "read",
217
296
  order: 1,
218
- show: (): ComposerPillView | undefined => {
219
- if (ctx.app.platform !== "desktop") return undefined
220
-
221
- if (!speaker.speaking()) {
222
- return { label: ctx.t("read.idle"), icon: "outline-audio", title: ctx.t("read.title"), run: read }
223
- }
224
-
225
- const paused = speaker.paused()
226
- const rows: ComposerPillRow[] = []
227
-
228
- if (speaker.canPause()) {
229
- rows.push({
230
- id: "pause",
231
- label: paused ? ctx.t("read.resume") : ctx.t("read.pause"),
232
- icon: "outline-audio",
233
- run: () => speaker.togglePause(),
234
- })
235
- }
236
-
237
- rows.push({
238
- id: "restart",
239
- label: ctx.t("read.restart"),
240
- icon: "outline-reset",
241
- run: () => void speaker.restart(),
242
- })
243
-
244
- rows.push({
245
- id: "stop",
246
- label: ctx.t("read.stop"),
247
- icon: "xmark-small",
248
- tone: "danger",
249
- run: () => speaker.cancel(),
250
- })
251
-
252
- return {
253
- label: paused ? ctx.t("read.paused") : ctx.t("read.speaking"),
254
- icon: "outline-audio",
255
- tone: "success",
256
- title: ctx.t("read.title"),
257
- rows,
258
- }
259
- },
297
+ show: () => readView(),
298
+ menu: (input) => <ReadMenu actions={readActions} close={input.close} />,
260
299
  }))
261
300
 
262
301
  ctx.add(Command, (): Command => ({