@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.
Files changed (2) hide show
  1. package/extensions/voice.ts +64 -26
  2. package/package.json +1 -1
@@ -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
- // Animate the widget every 200ms
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
- frame++;
645
- if (ctx?.hasUI) ctx.ui.setWidget("voice-recording", undefined); // force re-render
646
- showRecordingWidgetFrame(target, frame, waveChars);
647
- }, 200);
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, frame, waveChars);
658
+ showRecordingWidgetFrame(target, 0);
653
659
  }
654
660
 
655
- function showRecordingWidgetFrame(target: "editor" | "btw", frame: number, waveChars: string[]) {
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 SPACE again to stop");
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 SPACE again to stop");
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
- // press SPACE again stop recording. Toggle after activation.
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 && isHolding && voiceState === "recording") {
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 past threshold ──
1088
+ // ── Kitty key-repeat: ALWAYS suppress while holding/recording ──
1071
1089
  if (isKeyRepeat(data)) {
1072
- if (spaceConsumed || isHolding) return { consume: true };
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
- // If already recording (toggle mode for non-Kitty) → stop
1079
- if (voiceState === "recording" && spaceConsumed) {
1080
- isHolding = false;
1081
- spaceConsumed = false;
1082
- spaceDownTime = null;
1083
- clearHoldTimer();
1084
- stopVoiceRecording("editor");
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 = Date.now();
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
- await startVoiceRecording("editor");
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
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codexstar/pi-listen",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "Voice input, first-run onboarding, and side-channel BTW conversations for Pi",
5
5
  "type": "module",
6
6
  "keywords": [