opencode-groq-dictate 1.1.5 → 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.
- package/README.md +21 -3
- package/package.json +10 -2
- 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**: `
|
|
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`
|
|
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.
|
|
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",
|
|
@@ -28,5 +28,13 @@
|
|
|
28
28
|
"type": "git",
|
|
29
29
|
"url": "git+https://github.com/LeonVeganMan/opencode-groq-dictate.git"
|
|
30
30
|
},
|
|
31
|
-
"homepage": "https://github.com/LeonVeganMan/opencode-groq-dictate#readme"
|
|
31
|
+
"homepage": "https://github.com/LeonVeganMan/opencode-groq-dictate#readme",
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@opentui/core": ">=0.5.8",
|
|
34
|
+
"@opentui/solid": ">=0.5.8",
|
|
35
|
+
"solid-js": ">=1.9.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@opencode/plugin": "latest"
|
|
39
|
+
}
|
|
32
40
|
}
|
package/tui.tsx
CHANGED
|
@@ -136,7 +136,11 @@ export default Plugin.define({
|
|
|
136
136
|
return usd
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
|
|
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({
|
|
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 = (
|
|
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: [
|
|
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
|
-
|
|
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)}
|