opencode-groq-dictate 1.1.6 → 1.1.7

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.
Files changed (3) hide show
  1. package/README.md +21 -3
  2. package/package.json +1 -1
  3. package/tui.tsx +59 -12
package/README.md CHANGED
@@ -67,16 +67,34 @@ clone out of `~/.config/opencode/plugins/` before installing from npm.
67
67
  | Key | Action |
68
68
  |-----|--------|
69
69
  | `alt+v` | start recording, press again to stop and transcribe |
70
+ | `ctrl+alt+v` | same, but the prompt is submitted as soon as the text lands |
71
+ | click on `🎤 Groq Dictate` or on the `state` row | same as `alt+v` |
72
+ | `ctrl+click` on the same two | same as `ctrl+alt+v` |
70
73
  | `esc` | cancel the take: no upload, no request, no cost |
71
74
  | `/dictate` | same as `alt+v`, from the command palette |
72
75
 
73
76
  A take stops on its own after 60 seconds. Everything else is in the palette under
74
- **Voice**: `Dictation model`, `Dictation language`, `Dictation quota`.
77
+ **Voice**: `Dictate (Groq) and submit`, `Dictation model`, `Dictation language`,
78
+ `Dictation quota`.
79
+
80
+ Both bindings are ordinary commands, so you can rebind or disable them in
81
+ `cli.json` without touching the plugin:
82
+
83
+ ```jsonc
84
+ {
85
+ "keybinds": {
86
+ "opencode-groq-dictate.toggle": "alt+d",
87
+ "opencode-groq-dictate.toggleSubmit": false
88
+ }
89
+ }
90
+ ```
91
+
92
+ The click handlers stay available even when both bindings are off.
75
93
 
76
94
  Super+V cannot be used: VTE (Tilix, xfce4-terminal) only enables xterm
77
95
  `modifyOtherKeys`, which never reports the Super modifier, so the binding could not
78
- fire inside the terminal. `alt+v` is reported by both the legacy and the
79
- `modifyOtherKeys` encodings.
96
+ fire inside the terminal. `alt+v` and `ctrl+alt+v` are reported by both the legacy
97
+ and the `modifyOtherKeys` encodings.
80
98
 
81
99
  ## Screenshots
82
100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-groq-dictate",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "description": "Groq dictation in the OpenCode v2 TUI: alt+v records, transcribes with whisper-large-v3-turbo, and pastes the text into the prompt.",
5
5
  "private": false,
6
6
  "type": "module",
package/tui.tsx CHANGED
@@ -136,7 +136,11 @@ export default Plugin.define({
136
136
  return usd
137
137
  }
138
138
 
139
- async function transcribe(seconds: number) {
139
+ // A recording started with ctrl+alt+v (or ctrl+click) also submits the prompt
140
+ // once the text lands, so the same gesture can send without reaching for Enter.
141
+ let submitOnStop = false
142
+
143
+ async function transcribe(seconds: number, submit: boolean) {
140
144
  const { apiKey, language, model } = settings()
141
145
  const key = apiKey || (await askKey())
142
146
  if (!key) return
@@ -152,6 +156,12 @@ export default Plugin.define({
152
156
  }
153
157
 
154
158
  await paste(outcome.text)
159
+ if (submit) {
160
+ // The TUI applies the paste on the next frame; submitting in the same
161
+ // tick would send the prompt without the dictated text.
162
+ await new Promise((done) => setTimeout(done, 80))
163
+ context.keymap.dispatch("prompt.submit")
164
+ }
155
165
  // The recording is only needed until Groq answers; leaving it in /tmp keeps
156
166
  // a copy of everything dictated on disk.
157
167
  await rm(WAV, { force: true }).catch(() => {})
@@ -162,6 +172,8 @@ export default Plugin.define({
162
172
 
163
173
  async function stop() {
164
174
  if (!recorder) return
175
+ const submit = submitOnStop
176
+ submitOnStop = false
165
177
  const child = recorder
166
178
  recorder = null
167
179
  setRecording(false)
@@ -180,7 +192,7 @@ export default Plugin.define({
180
192
  }
181
193
  context.ui.toast.show({ message: "Transcribing…", variant: "info" })
182
194
  try {
183
- await transcribe(seconds)
195
+ await transcribe(seconds, submit)
184
196
  } catch (error) {
185
197
  context.ui.toast.show({ message: String(error), variant: "error" })
186
198
  } finally {
@@ -197,7 +209,7 @@ export default Plugin.define({
197
209
  )
198
210
  })
199
211
 
200
- async function start() {
212
+ async function start(submit = false) {
201
213
  const { apiKey } = settings()
202
214
  if (!apiKey) {
203
215
  context.ui.toast.show({ message: "Set the Groq API key first", variant: "warning" })
@@ -232,6 +244,9 @@ export default Plugin.define({
232
244
  context.ui.toast.show({ message: `parec: ${error.message}`, variant: "error" })
233
245
  })
234
246
  setRecording(true)
247
+ // Only a take that actually started arms the submit, so a failed start does
248
+ // not make the next alt+v send the prompt.
249
+ submitOnStop = submit
235
250
  setElapsed(0)
236
251
  ticker = startTicker(() => setElapsed((value) => value + 1))
237
252
  // Nobody wants a ten-minute upload because the stop key was forgotten.
@@ -239,13 +254,20 @@ export default Plugin.define({
239
254
  context.ui.toast.show({ message: `Stopped at ${MAX_SECONDS}s`, variant: "warning" })
240
255
  void stop()
241
256
  }, MAX_SECONDS * 1000)
242
- context.ui.toast.show({ message: "Recording… alt+v stop · esc cancel", variant: "info", duration: 2000 })
257
+ context.ui.toast.show({
258
+ message: submit
259
+ ? "Recording… ctrl+alt+v stops and sends · esc cancels"
260
+ : "Recording… alt+v stop · esc cancel",
261
+ variant: "info",
262
+ duration: 2000,
263
+ })
243
264
  }
244
265
 
245
266
 
246
267
  // Esc aborts the take: nothing is uploaded, so no quota and no cost.
247
268
  async function cancel() {
248
269
  if (!recorder) return
270
+ submitOnStop = false
249
271
  const child = recorder
250
272
  recorder = null
251
273
  setRecording(false)
@@ -261,8 +283,11 @@ export default Plugin.define({
261
283
  }
262
284
 
263
285
  // One entry point for the keybinding and for the click on the panel.
264
-
265
- const toggle = () => (recording() ? stop() : start())
286
+ // submit=true means "record, transcribe, then press Enter".
287
+ const toggle = (submit = false) => {
288
+ if (recording()) return stop()
289
+ return start(submit)
290
+ }
266
291
 
267
292
  async function pickModel() {
268
293
  const model = await context.ui.dialog.select({
@@ -318,7 +343,17 @@ export default Plugin.define({
318
343
  bind: "alt+v",
319
344
  palette: true,
320
345
  slash: { name: "dictate" },
321
- run: toggle,
346
+ run: () => toggle(),
347
+ },
348
+ {
349
+ id: "opencode-groq-dictate.toggleSubmit",
350
+ title: "Dictate (Groq) and submit",
351
+ group: "Voice",
352
+ // ctrl+alt is what modifyOtherKeys reports, the same path the TUI's own
353
+ // ctrl+alt defaults take. Rebind or disable it in cli.json like any other.
354
+ bind: "ctrl+alt+v",
355
+ palette: true,
356
+ run: () => toggle(true),
322
357
  },
323
358
  {
324
359
  id: "opencode-groq-dictate.cancel",
@@ -354,7 +389,11 @@ export default Plugin.define({
354
389
  run: pickModel,
355
390
  },
356
391
  ],
357
- bindings: ["opencode-groq-dictate.toggle", "opencode-groq-dictate.cancel"],
392
+ bindings: [
393
+ "opencode-groq-dictate.toggle",
394
+ "opencode-groq-dictate.toggleSubmit",
395
+ "opencode-groq-dictate.cancel",
396
+ ],
358
397
  }))
359
398
 
360
399
  const red = () => context.theme?.hue?.red?.["400"] ?? "#ff5555"
@@ -380,7 +419,15 @@ export default Plugin.define({
380
419
 
381
420
  // Label column, the same width trick the go-meter rows use so the figures line
382
421
  // up under each other instead of drifting.
383
- const row = (label: string, value: string, fg: () => string | undefined, onClick?: () => void) => (
422
+ // ctrl+click mirrors ctrl+alt+v: the OpenTUI MouseEvent carries the modifiers.
423
+ type Click = { modifiers?: { ctrl?: boolean } } | undefined
424
+ const wantsSubmit = (event: Click) => !!event?.modifiers?.ctrl
425
+ const row = (
426
+ label: string,
427
+ value: string,
428
+ fg: () => string | undefined,
429
+ onClick?: (event: Click) => void,
430
+ ) => (
384
431
  <box flexDirection="row" gap={0} onMouseUp={onClick}>
385
432
  <text>{label.padEnd(LABEL_WIDTH)}</text>
386
433
  <text fg={fg()}>{value}</text>
@@ -403,14 +450,14 @@ export default Plugin.define({
403
450
  const state = () => (recording() ? red() : busy() ? amber() : green())
404
451
  return (
405
452
  <box flexDirection="column">
406
- <text fg={muted()} onMouseUp={toggle}>
407
- Groq Dictate
453
+ <text fg={muted()} onMouseUp={(event) => toggle(wantsSubmit(event))}>
454
+ {"🎤 Groq Dictate"}
408
455
  </text>
409
456
  {row(
410
457
  "state",
411
458
  recording() ? `● rec ${elapsed()}s` : busy() ? "transcribing…" : "idle",
412
459
  state,
413
- toggle,
460
+ (event) => toggle(wantsSubmit(event)),
414
461
  )}
415
462
  {row("model", settings().model.replace("whisper-", ""), () => undefined, pickModel)}
416
463
  {row("lang", settings().language, () => undefined, pickLanguage)}