@codexstar/pi-listen 1.0.14 → 1.0.15
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/extensions/voice.ts +64 -26
- package/package.json +1 -1
package/extensions/voice.ts
CHANGED
|
@@ -636,23 +636,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
636
636
|
/** Animated recording indicator with live waveform */
|
|
637
637
|
function showRecordingWidget(target: "editor" | "btw") {
|
|
638
638
|
if (!ctx?.hasUI) return;
|
|
639
|
-
let frame = 0;
|
|
640
|
-
const waveChars = ["▁", "▂", "▃", "▅", "▆", "▇", "▆", "▅", "▃", "▂"];
|
|
641
639
|
|
|
642
|
-
//
|
|
640
|
+
// Store initial state — once live transcription arrives,
|
|
641
|
+
// updateLiveTranscriptWidget takes over and we stop the animation.
|
|
642
|
+
(showRecordingWidget as any)._target = target;
|
|
643
|
+
(showRecordingWidget as any)._frame = 0;
|
|
644
|
+
(showRecordingWidget as any)._hasTranscript = false;
|
|
645
|
+
|
|
646
|
+
// Animate the widget every 300ms (only while no transcript is showing)
|
|
643
647
|
const animTimer = setInterval(() => {
|
|
644
|
-
|
|
645
|
-
if (
|
|
646
|
-
|
|
647
|
-
|
|
648
|
+
// Stop animating once live transcript takes over
|
|
649
|
+
if ((showRecordingWidget as any)?._hasTranscript) return;
|
|
650
|
+
|
|
651
|
+
(showRecordingWidget as any)._frame = ((showRecordingWidget as any)._frame || 0) + 1;
|
|
652
|
+
showRecordingWidgetFrame(target, (showRecordingWidget as any)._frame);
|
|
653
|
+
}, 300);
|
|
648
654
|
|
|
649
655
|
// Store the timer so we can clean it up
|
|
650
656
|
(showRecordingWidget as any)._animTimer = animTimer;
|
|
651
657
|
|
|
652
|
-
showRecordingWidgetFrame(target,
|
|
658
|
+
showRecordingWidgetFrame(target, 0);
|
|
653
659
|
}
|
|
654
660
|
|
|
655
|
-
|
|
661
|
+
const waveChars = ["▁", "▂", "▃", "▅", "▆", "▇", "▆", "▅", "▃", "▂"];
|
|
662
|
+
|
|
663
|
+
function showRecordingWidgetFrame(target: "editor" | "btw", frame: number) {
|
|
656
664
|
if (!ctx?.hasUI) return;
|
|
657
665
|
ctx.ui.setWidget("voice-recording", (tui, theme) => {
|
|
658
666
|
return {
|
|
@@ -691,7 +699,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
691
699
|
? theme.fg("dim", " Press Ctrl+Shift+B to stop")
|
|
692
700
|
: kittyReleaseDetected
|
|
693
701
|
? theme.fg("dim", " Release SPACE to finalize")
|
|
694
|
-
: theme.fg("dim", " Press
|
|
702
|
+
: theme.fg("dim", " Press Ctrl+Shift+V to stop");
|
|
695
703
|
|
|
696
704
|
const lines = [
|
|
697
705
|
topBorder,
|
|
@@ -717,6 +725,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
717
725
|
function updateLiveTranscriptWidget(interim: string, finals: string[]) {
|
|
718
726
|
if (!ctx?.hasUI) return;
|
|
719
727
|
|
|
728
|
+
// Stop the recording animation — live transcript takes over
|
|
729
|
+
(showRecordingWidget as any)._hasTranscript = true;
|
|
730
|
+
stopRecordingWidgetAnimation();
|
|
731
|
+
|
|
720
732
|
const finalized = finals.join(" ");
|
|
721
733
|
const displayText = finalized + (interim ? (finalized ? " " : "") + interim : "");
|
|
722
734
|
|
|
@@ -745,7 +757,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
745
757
|
const titleLine = ` ${dot} ${label} ${timeStyled}`;
|
|
746
758
|
const hint = kittyReleaseDetected
|
|
747
759
|
? theme.fg("dim", " Release SPACE to finalize")
|
|
748
|
-
: theme.fg("dim", " Press
|
|
760
|
+
: theme.fg("dim", " Press Ctrl+Shift+V to stop");
|
|
749
761
|
|
|
750
762
|
const lines = [topBorder, side(titleLine)];
|
|
751
763
|
|
|
@@ -1005,13 +1017,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
1005
1017
|
// For Kitty protocol terminals: hold → wait threshold → activate →
|
|
1006
1018
|
// release → stop recording. True hold-to-talk.
|
|
1007
1019
|
// For non-Kitty terminals: hold → wait threshold → activate →
|
|
1008
|
-
//
|
|
1020
|
+
// Ctrl+Shift+V or /voice stop to end recording.
|
|
1021
|
+
//
|
|
1022
|
+
// KEY INSIGHT: In non-Kitty terminals, holding a key generates
|
|
1023
|
+
// rapid press events (key-repeat). We CANNOT use "second space press
|
|
1024
|
+
// = stop" because repeats arrive while holding. Instead, non-Kitty
|
|
1025
|
+
// users must use Ctrl+Shift+V to stop.
|
|
1009
1026
|
|
|
1010
1027
|
const HOLD_THRESHOLD_MS = 500; // minimum hold time before voice activates
|
|
1011
1028
|
let kittyReleaseDetected = false;
|
|
1012
1029
|
let spaceDownTime: number | null = null; // timestamp when SPACE was first pressed
|
|
1013
1030
|
let holdActivationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1014
1031
|
let spaceConsumed = false; // whether we've committed to voice (past threshold)
|
|
1032
|
+
let lastSpacePressTime = 0; // debounce rapid space presses from key-repeat
|
|
1015
1033
|
|
|
1016
1034
|
function clearHoldTimer() {
|
|
1017
1035
|
if (holdActivationTimer) {
|
|
@@ -1054,7 +1072,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1054
1072
|
}
|
|
1055
1073
|
|
|
1056
1074
|
// Released after threshold → stop recording (true hold-to-talk)
|
|
1057
|
-
if (spaceConsumed &&
|
|
1075
|
+
if (spaceConsumed && voiceState === "recording") {
|
|
1058
1076
|
isHolding = false;
|
|
1059
1077
|
spaceConsumed = false;
|
|
1060
1078
|
spaceDownTime = null;
|
|
@@ -1067,32 +1085,42 @@ export default function (pi: ExtensionAPI) {
|
|
|
1067
1085
|
return undefined;
|
|
1068
1086
|
}
|
|
1069
1087
|
|
|
1070
|
-
// ── Kitty key-repeat: suppress while holding
|
|
1088
|
+
// ── Kitty key-repeat: ALWAYS suppress while holding/recording ──
|
|
1071
1089
|
if (isKeyRepeat(data)) {
|
|
1072
|
-
if (spaceConsumed || isHolding
|
|
1090
|
+
if (spaceDownTime || spaceConsumed || isHolding || voiceState === "recording") {
|
|
1091
|
+
return { consume: true };
|
|
1092
|
+
}
|
|
1073
1093
|
return undefined;
|
|
1074
1094
|
}
|
|
1075
1095
|
|
|
1076
|
-
// === Key PRESS ===
|
|
1096
|
+
// === Key PRESS (initial press only) ===
|
|
1077
1097
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
return { consume: true };
|
|
1098
|
+
const now = Date.now();
|
|
1099
|
+
|
|
1100
|
+
// Debounce: ignore rapid presses within 100ms (terminal key-repeat
|
|
1101
|
+
// generates press events in non-Kitty terminals since there's no
|
|
1102
|
+
// key-repeat flag — they all look like fresh presses)
|
|
1103
|
+
if (now - lastSpacePressTime < 100) {
|
|
1104
|
+
lastSpacePressTime = now;
|
|
1105
|
+
return { consume: true }; // suppress repeat
|
|
1086
1106
|
}
|
|
1107
|
+
lastSpacePressTime = now;
|
|
1087
1108
|
|
|
1088
1109
|
// If transcribing → ignore
|
|
1089
1110
|
if (voiceState === "transcribing") {
|
|
1090
1111
|
return { consume: true };
|
|
1091
1112
|
}
|
|
1092
1113
|
|
|
1114
|
+
// If already recording: In Kitty mode, release handles stop.
|
|
1115
|
+
// In non-Kitty, we can't safely detect "real second press" vs
|
|
1116
|
+
// key-repeat. Use Ctrl+Shift+V instead. Just consume.
|
|
1117
|
+
if (voiceState === "recording") {
|
|
1118
|
+
return { consume: true };
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1093
1121
|
// Idle → start the hold timer
|
|
1094
1122
|
if (voiceState === "idle" && !spaceDownTime) {
|
|
1095
|
-
spaceDownTime =
|
|
1123
|
+
spaceDownTime = now;
|
|
1096
1124
|
spaceConsumed = false;
|
|
1097
1125
|
|
|
1098
1126
|
// Show a subtle "preparing" indicator
|
|
@@ -1326,9 +1354,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
1326
1354
|
return;
|
|
1327
1355
|
}
|
|
1328
1356
|
if (voiceState === "idle") {
|
|
1329
|
-
|
|
1357
|
+
// Direct start — bypass hold threshold
|
|
1358
|
+
spaceConsumed = true;
|
|
1359
|
+
isHolding = true;
|
|
1360
|
+
const ok = await startVoiceRecording("editor");
|
|
1361
|
+
if (!ok) {
|
|
1362
|
+
isHolding = false;
|
|
1363
|
+
spaceConsumed = false;
|
|
1364
|
+
}
|
|
1330
1365
|
} else if (voiceState === "recording") {
|
|
1331
1366
|
isHolding = false;
|
|
1367
|
+
spaceConsumed = false;
|
|
1368
|
+
spaceDownTime = null;
|
|
1369
|
+
clearHoldTimer();
|
|
1332
1370
|
await stopVoiceRecording("editor");
|
|
1333
1371
|
}
|
|
1334
1372
|
},
|