Skip to content

Commit fc20b1f

Browse files
committed
Enhance WebSocket error handling and STT error management
- Update OpenAIWhisperClient to ensure WebSocket event listeners are removed only if initialized. - Add error handling in ChatTextArea to set and clear STT error state during recording. - Integrate error display in STTSetupPopover for improved user feedback on STT setup issues. - Sync optimistic UI state in useSTT hook for immediate feedback on recording actions.
1 parent 95efba2 commit fc20b1f

File tree

4 files changed

+41
-12
lines changed

4 files changed

+41
-12
lines changed

src/services/stt/OpenAIWhisperClient.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -193,31 +193,39 @@ export class OpenAIWhisperClient extends EventEmitter {
193193
await new Promise<void>((resolve, reject) => {
194194
const timeout = setTimeout(() => {
195195
reject(new Error("WebSocket connection timeout"))
196-
}, 10000)
196+
}, 5000)
197197

198198
const onOpen = () => {
199199
clearTimeout(timeout)
200-
this.ws!.off("open", onOpen)
201-
this.ws!.off("error", onError)
200+
this.ws?.off("open", onOpen)
201+
this.ws?.off("error", onError)
202202
resolve()
203203
}
204204

205205
const onError = (error: Error) => {
206206
clearTimeout(timeout)
207-
this.ws!.off("open", onOpen)
208-
this.ws!.off("error", onError)
207+
this.ws?.off("open", onOpen)
208+
this.ws?.off("error", onError)
209209
reject(new Error(`WebSocket connection failed: ${error.message}`))
210210
}
211211

212-
this.ws!.once("open", onOpen)
213-
this.ws!.once("error", onError)
212+
if (this.ws) {
213+
this.ws.once("open", onOpen)
214+
this.ws.once("error", onError)
215+
} else {
216+
reject(new Error("WebSocket not initialized"))
217+
}
214218
})
215219

216220
this.isConnecting = false
217221
this.reconnectAttempts = 0
218222
this.emit("connected")
219223
} catch (error) {
220224
this.isConnecting = false
225+
try {
226+
this.ws?.removeAllListeners()
227+
this.ws?.close()
228+
} catch (_cleanupError) {}
221229
this.ws = null
222230
throw error
223231
}
@@ -494,10 +502,8 @@ export class OpenAIWhisperClient extends EventEmitter {
494502
}
495503

496504
// Close WebSocket
497-
if (this.ws) {
498-
this.ws.close(1000, "Client disconnect")
499-
this.ws = null
500-
}
505+
this.ws?.close(1000, "Client disconnect")
506+
this.ws = null
501507

502508
this.sessionConfigured = false
503509
this.pendingAudioChunks = []

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
343343
},
344344
onError: (error) => {
345345
console.error("STT error:", error)
346+
setSttError(error)
346347
recordingStartStateRef.current = null
347348
},
348349
})
@@ -382,6 +383,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
382383
afterCursor: inputValue.slice(pos),
383384
position: pos,
384385
}
386+
setSttError(null)
385387
}
386388
}, [isRecording, inputValue])
387389

@@ -469,6 +471,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
469471

470472
// kilocode_change start: Popover state for STT setup help
471473
const [sttSetupPopoverOpen, setSttSetupPopoverOpen] = useState(false)
474+
const [sttError, setSttError] = useState<string | null>(null)
472475
const handleMicrophoneClick = useCallback(() => {
473476
// If STT is unavailable, open setup popover instead of starting recording
474477
if (!speechToTextStatus?.available) {
@@ -479,6 +482,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
479482
if (isRecording) {
480483
stopSTT()
481484
} else {
485+
setSttError(null) // Clear any previous error when starting new recording
482486
startSTT(language || "en") // Pass user's language from extension state
483487
}
484488
}, [isRecording, startSTT, stopSTT, language, speechToTextStatus?.available])
@@ -1756,7 +1760,8 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
17561760
speechToTextStatus={speechToTextStatus}
17571761
open={sttSetupPopoverOpen}
17581762
onOpenChange={setSttSetupPopoverOpen}
1759-
onFfmpegHelpClick={handleFfmpegHelpClick}>
1763+
onFfmpegHelpClick={handleFfmpegHelpClick}
1764+
error={sttError}>
17601765
<MicrophoneButton
17611766
isRecording={isRecording}
17621767
onClick={handleMicrophoneClick}

webview-ui/src/components/chat/STTSetupPopover.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,21 @@ interface STTSetupPopoverProps {
1616
open: boolean
1717
onOpenChange: (open: boolean) => void
1818
onFfmpegHelpClick: () => void
19+
error?: string | null
1920
}
2021

2122
interface STTSetupPopoverContentProps {
2223
reason?: "openaiKeyMissing" | "ffmpegNotInstalled"
2324
onFfmpegHelpClick: () => void
2425
onOpenChange?: (open: boolean) => void
26+
error?: string | null
2527
}
2628

2729
export const STTSetupPopoverContent: React.FC<STTSetupPopoverContentProps> = ({
2830
reason,
2931
onFfmpegHelpClick,
3032
onOpenChange,
33+
error,
3134
}) => {
3235
const { t } = useTranslation()
3336
const docsUrl = buildDocLink("features/experimental/voice-transcription", "stt_setup")
@@ -59,6 +62,18 @@ export const STTSetupPopoverContent: React.FC<STTSetupPopoverContentProps> = ({
5962
/>
6063
</p>
6164

65+
{error && (
66+
<div
67+
className="my-0 mb-3 p-2 rounded text-sm border"
68+
style={{
69+
backgroundColor: "var(--vscode-inputValidation-errorBackground, rgba(255, 0, 0, 0.1))",
70+
borderColor: "var(--vscode-inputValidation-errorBorder, #ff0000)",
71+
color: "var(--vscode-errorForeground, #ff0000)",
72+
}}>
73+
<strong>Error:</strong> {error}
74+
</div>
75+
)}
76+
6277
<ul className="my-0 mb-0 list-disc list-inside text-sm text-vscode-descriptionForeground space-y-1">
6378
{reason === "openaiKeyMissing" || !reason ? (
6479
<li>{t("kilocode:speechToText.setupPopover.openAiReason")}</li>
@@ -94,6 +109,7 @@ export const STTSetupPopover: React.FC<STTSetupPopoverProps> = ({
94109
open,
95110
onOpenChange,
96111
onFfmpegHelpClick,
112+
error,
97113
}) => {
98114
const portalContainer = useRooPortal("roo-portal")
99115

@@ -120,6 +136,7 @@ export const STTSetupPopover: React.FC<STTSetupPopoverProps> = ({
120136
reason={reason}
121137
onFfmpegHelpClick={onFfmpegHelpClick}
122138
onOpenChange={onOpenChange}
139+
error={error}
123140
/>
124141
</PopoverContent>
125142
</Popover>

webview-ui/src/hooks/useSTT.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export function useSTT(options: UseSTTOptions = {}): UseSTTReturn {
9292
if (msg.sessionId !== sessionIdRef.current) return
9393

9494
setRealIsRecording(false)
95+
setOptimisticIsRecording(false) // Immediately sync optimistic state on stop
9596
setVolume(0)
9697

9798
if (msg.reason === "completed") {

0 commit comments

Comments
 (0)