Răsfoiți Sursa

feat(stt): enhance speech-to-text setup with error handling and status hook (#4587)

- Add STTSetupPopover component for guided setup
- Extract error state management into useSTTStatus hook
- Cache FFmpeg not found result to avoid repeated path checks
- Enhance STT error handling and cleanup processes
- Add MicrophoneButton and STTSetupPopover storybook stories
- Add speechToText.errorState translations for all locales
Chris Hasson 1 lună în urmă
părinte
comite
d1c35c54c2
60 a modificat fișierele cu 827 adăugiri și 175 ștergeri
  1. 5 0
      .changeset/clear-hoops-melt.md
  2. 43 0
      apps/storybook/stories/MicrophoneButton.stories.tsx
  3. 48 0
      apps/storybook/stories/STTSetupPopover.stories.tsx
  4. 0 10
      src/core/webview/ClineProvider.ts
  5. 1 22
      src/core/webview/speechToTextCheck.ts
  6. 14 1
      src/core/webview/sttHandlers.ts
  7. 10 1
      src/core/webview/webviewMessageHandler.ts
  8. 6 0
      src/i18n/locales/ar/kilocode.json
  9. 6 0
      src/i18n/locales/ca/kilocode.json
  10. 6 0
      src/i18n/locales/cs/kilocode.json
  11. 6 0
      src/i18n/locales/de/kilocode.json
  12. 6 0
      src/i18n/locales/en/kilocode.json
  13. 6 0
      src/i18n/locales/es/kilocode.json
  14. 6 0
      src/i18n/locales/fr/kilocode.json
  15. 6 0
      src/i18n/locales/hi/kilocode.json
  16. 6 0
      src/i18n/locales/id/kilocode.json
  17. 6 0
      src/i18n/locales/it/kilocode.json
  18. 6 0
      src/i18n/locales/ja/kilocode.json
  19. 6 0
      src/i18n/locales/ko/kilocode.json
  20. 6 0
      src/i18n/locales/nl/kilocode.json
  21. 6 0
      src/i18n/locales/pl/kilocode.json
  22. 6 0
      src/i18n/locales/pt-BR/kilocode.json
  23. 6 0
      src/i18n/locales/ru/kilocode.json
  24. 6 0
      src/i18n/locales/th/kilocode.json
  25. 6 0
      src/i18n/locales/tr/kilocode.json
  26. 6 0
      src/i18n/locales/uk/kilocode.json
  27. 6 0
      src/i18n/locales/vi/kilocode.json
  28. 6 0
      src/i18n/locales/zh-CN/kilocode.json
  29. 6 0
      src/i18n/locales/zh-TW/kilocode.json
  30. 23 12
      src/services/stt/OpenAIWhisperClient.ts
  31. 32 21
      src/services/stt/STTService.ts
  32. 2 0
      src/shared/ExtensionMessage.ts
  33. 1 0
      src/shared/WebviewMessage.ts
  34. 40 17
      webview-ui/src/components/chat/ChatTextArea.tsx
  35. 46 9
      webview-ui/src/components/chat/MicrophoneButton.tsx
  36. 164 0
      webview-ui/src/components/chat/STTSetupPopover.tsx
  37. 16 6
      webview-ui/src/hooks/useSTT.ts
  38. 40 0
      webview-ui/src/hooks/useSTTStatus.ts
  39. 13 5
      webview-ui/src/i18n/locales/ar/kilocode.json
  40. 9 3
      webview-ui/src/i18n/locales/ca/kilocode.json
  41. 9 3
      webview-ui/src/i18n/locales/cs/kilocode.json
  42. 9 3
      webview-ui/src/i18n/locales/de/kilocode.json
  43. 9 3
      webview-ui/src/i18n/locales/en/kilocode.json
  44. 9 3
      webview-ui/src/i18n/locales/es/kilocode.json
  45. 9 3
      webview-ui/src/i18n/locales/fr/kilocode.json
  46. 11 5
      webview-ui/src/i18n/locales/hi/kilocode.json
  47. 10 4
      webview-ui/src/i18n/locales/id/kilocode.json
  48. 10 4
      webview-ui/src/i18n/locales/it/kilocode.json
  49. 9 3
      webview-ui/src/i18n/locales/ja/kilocode.json
  50. 9 3
      webview-ui/src/i18n/locales/ko/kilocode.json
  51. 10 4
      webview-ui/src/i18n/locales/nl/kilocode.json
  52. 9 3
      webview-ui/src/i18n/locales/pl/kilocode.json
  53. 9 3
      webview-ui/src/i18n/locales/pt-BR/kilocode.json
  54. 9 3
      webview-ui/src/i18n/locales/ru/kilocode.json
  55. 11 5
      webview-ui/src/i18n/locales/th/kilocode.json
  56. 9 3
      webview-ui/src/i18n/locales/tr/kilocode.json
  57. 9 3
      webview-ui/src/i18n/locales/uk/kilocode.json
  58. 10 4
      webview-ui/src/i18n/locales/vi/kilocode.json
  59. 9 3
      webview-ui/src/i18n/locales/zh-CN/kilocode.json
  60. 9 3
      webview-ui/src/i18n/locales/zh-TW/kilocode.json

+ 5 - 0
.changeset/clear-hoops-melt.md

@@ -0,0 +1,5 @@
+---
+"kilo-code": minor
+---
+
+Improve the initial setup experience for the speech-to-text feature by adding an inline setup tooltip

+ 43 - 0
apps/storybook/stories/MicrophoneButton.stories.tsx

@@ -0,0 +1,43 @@
+import type { Meta, StoryObj } from "@storybook/react-vite"
+import { fn } from "storybook/test"
+import { MicrophoneButton } from "@/components/chat/MicrophoneButton"
+import { createTableStory } from "../src/utils/createTableStory"
+
+const meta = {
+	title: "Components/MicrophoneButton",
+	component: MicrophoneButton,
+	parameters: {
+		layout: "centered",
+		backgrounds: {
+			default: "dark",
+			values: [{ name: "dark", value: "#1e1e1e" }],
+		},
+	},
+	args: {
+		isRecording: false,
+		disabled: false,
+		hasError: false,
+		onClick: fn(),
+		onStatusChange: fn(),
+	},
+} satisfies Meta<typeof MicrophoneButton>
+
+export default meta
+type Story = StoryObj<typeof meta>
+
+export const Default: Story = {}
+
+export const States = createTableStory({
+	component: MicrophoneButton,
+	rows: {
+		isRecording: [false, true],
+	},
+	columns: {
+		disabled: [false, true],
+		hasError: [false, true],
+	},
+	defaultProps: {
+		onClick: fn(),
+		onStatusChange: fn(),
+	},
+})

+ 48 - 0
apps/storybook/stories/STTSetupPopover.stories.tsx

@@ -0,0 +1,48 @@
+import type { Meta, StoryObj } from "@storybook/react-vite"
+import { STTSetupPopoverContent } from "@/components/chat/STTSetupPopover"
+
+const meta = {
+	title: "Components/STTSetupPopover",
+	component: STTSetupPopoverContent,
+	parameters: {
+		layout: "centered",
+	},
+	tags: ["autodocs"],
+	render: (args) => (
+		<div className="w-[calc(100vw-32px)] max-w-[400px]">
+			<STTSetupPopoverContent {...args} />
+		</div>
+	),
+	args: {
+		setInputValue: (value: string) => {
+			console.log("setInputValue:", value)
+		},
+		onSend: () => {
+			console.log("onSend clicked")
+		},
+	},
+} satisfies Meta<typeof STTSetupPopoverContent>
+
+export default meta
+type Story = StoryObj<typeof meta>
+
+export const FFmpegNotInstalled: Story = {
+	name: "FFmpeg not installed",
+	args: {
+		reason: "ffmpegNotInstalled",
+		setInputValue: (value: string) => {
+			console.log("setInputValue:", value)
+		},
+		onSend: () => {
+			console.log("onSend clicked")
+		},
+	},
+}
+
+export const OpenAIKeyMissing: Story = {
+	args: {
+		reason: "openaiKeyMissing",
+	},
+}
+
+export const BothMissing: Story = {}

+ 0 - 10
src/core/webview/ClineProvider.ts

@@ -97,7 +97,6 @@ import { Task } from "../task/Task"
 import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt"
 
 import { webviewMessageHandler } from "./webviewMessageHandler"
-import { checkSpeechToTextAvailable } from "./speechToTextCheck" // kilocode_change
 import type { ClineMessage, TodoItem } from "@roo-code/types"
 import { readApiMessages, saveApiMessages, saveTaskMessages } from "../task-persistence"
 import { readTaskMessages } from "../task-persistence/taskMessages"
@@ -2189,14 +2188,6 @@ export class ClineProvider
 				: undefined
 		// kilocode_change end
 
-		// kilocode_change start - checkSpeechToTextAvailable (only when experiment enabled)
-		let speechToTextStatus: { available: boolean; reason?: "openaiKeyMissing" | "ffmpegNotInstalled" } | undefined =
-			undefined
-		if (experiments?.speechToText) {
-			speechToTextStatus = await checkSpeechToTextAvailable(this.providerSettingsManager)
-		}
-		// kilocode_change end - checkSpeechToTextAvailable
-
 		let cloudOrganizations: CloudOrganizationMembership[] = []
 
 		try {
@@ -2421,7 +2412,6 @@ export class ClineProvider
 			featureRoomoteControlEnabled,
 			virtualQuotaActiveModel, // kilocode_change: Include virtual quota active model in state
 			debug: vscode.workspace.getConfiguration(Package.name).get<boolean>("debug", false),
-			speechToTextStatus, // kilocode_change: Speech-to-text availability status with failure reason
 		}
 	}
 

+ 1 - 22
src/core/webview/speechToTextCheck.ts

@@ -11,13 +11,6 @@ export type SpeechToTextAvailabilityResult = {
 	reason?: "openaiKeyMissing" | "ffmpegNotInstalled"
 }
 
-/**
- * Cached availability result with timestamp
- */
-let cachedResult: { available: boolean; reason?: "openaiKeyMissing" | "ffmpegNotInstalled"; timestamp: number } | null =
-	null
-const CACHE_DURATION_MS = 30000 // 30 seconds
-
 /**
  * Check if speech-to-text prerequisites are available
  *
@@ -26,43 +19,29 @@ const CACHE_DURATION_MS = 30000 // 30 seconds
  * 2. FFmpeg is installed and available
  *
  * Note: The experiment flag is checked on the frontend, not here.
- * Results are cached for 30 seconds to prevent redundant FFmpeg checks.
+ * This function always performs a fresh check without caching.
  *
  * @param providerSettingsManager - Provider settings manager for API configuration
- * @param forceRecheck - Force a fresh check, ignoring cache (default: false)
  * @returns Promise<SpeechToTextAvailabilityResult> - Result with availability status and failure reason if unavailable
  */
 export async function checkSpeechToTextAvailable(
 	providerSettingsManager: ProviderSettingsManager,
-	forceRecheck = false,
 ): Promise<SpeechToTextAvailabilityResult> {
-	// Return cached result if valid and not forcing recheck
-	if (cachedResult !== null && !forceRecheck) {
-		const age = Date.now() - cachedResult.timestamp
-		if (age < CACHE_DURATION_MS) {
-			return { available: cachedResult.available, reason: cachedResult.reason }
-		}
-	}
-
 	try {
 		// Check 1: OpenAI API key
 		const apiKey = await getOpenAiApiKey(providerSettingsManager)
 		if (!apiKey) {
-			cachedResult = { available: false, reason: "openaiKeyMissing", timestamp: Date.now() }
 			return { available: false, reason: "openaiKeyMissing" }
 		}
 
 		// Check 2: FFmpeg installed
 		const ffmpegResult = FFmpegCaptureService.findFFmpeg()
 		if (!ffmpegResult.available) {
-			cachedResult = { available: false, reason: "ffmpegNotInstalled", timestamp: Date.now() }
 			return { available: false, reason: "ffmpegNotInstalled" }
 		}
 
-		cachedResult = { available: true, timestamp: Date.now() }
 		return { available: true }
 	} catch (error) {
-		cachedResult = { available: false, timestamp: Date.now() }
 		return { available: false }
 	}
 }

+ 14 - 1
src/core/webview/sttHandlers.ts

@@ -91,7 +91,20 @@ export async function handleSTTStart(clineProvider: ClineProvider, language?: st
 		// Service generates its own prompt from the code glossary
 		await service.start({ apiKey }, language)
 	} catch (error) {
-		console.error("Failed to start STT service:", error)
+		console.error("🎙️ [sttHandlers] ❌ Failed to start STT service:", error)
+
+		// The service.start() catch block should have already called onStopped,
+		// but as a defensive measure, ensure frontend is notified if sessionId is still available
+		const sessionId = service.getSessionId()
+		if (sessionId) {
+			const errorMessage = error instanceof Error ? error.message : "Failed to start STT service"
+			clineProvider.postMessageToWebview({
+				type: "stt:stopped",
+				sessionId,
+				reason: "error",
+				error: errorMessage,
+			})
+		}
 	}
 }
 

+ 10 - 1
src/core/webview/webviewMessageHandler.ts

@@ -3657,6 +3657,7 @@ export const webviewMessageHandler = async (
 			}
 			break
 		}
+		// kilocode_change end: Type-safe global state handler
 		// kilocode_change start: STT (Speech-to-Text) handlers
 		case "stt:start":
 		case "stt:stop":
@@ -3665,7 +3666,15 @@ export const webviewMessageHandler = async (
 			await handleSTTCommand(provider, message as any)
 			break
 		}
-		// kilocode_change end: Type-safe global state handler
+		case "stt:checkAvailability": {
+			const { checkSpeechToTextAvailable } = await import("./speechToTextCheck")
+			provider.postMessageToWebview({
+				type: "stt:statusResponse",
+				speechToTextStatus: await checkSpeechToTextAvailable(provider.providerSettingsManager),
+			})
+			break
+		}
+		// kilocode_change end: STT (Speech-to-Text) handlers
 		case "insertTextToChatArea":
 			provider.postMessageToWebview({ type: "insertTextToChatArea", text: message.text })
 			break

+ 6 - 0
src/i18n/locales/ar/kilocode.json

@@ -201,5 +201,11 @@
 			"installMessage": "تشغيل npm install لتثبيت Kilocode CLI. بمجرد الانتهاء، يمكنك إغلاق هذه الطرفية ومحاولة بدء Agent Manager مرة أخرى.",
 			"authReminder": "If this is your first install, run `kilocode auth` to sign in before starting the Agent Manager."
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "مفتاح API الخاص بـ OpenAI غير صالح أو لا يملك صلاحية الوصول إلى Realtime API. يرجى التحقق من مفتاح API الخاص بك والمحاولة مرة أخرى.",
+			"unknown": "فشل الاتصال. يرجى التحقق من الإعدادات والمحاولة مرة أخرى."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/ca/kilocode.json

@@ -197,5 +197,11 @@
 			"installMessage": "Executant npm install per instal·lar Kilocode CLI. Un cop completat, pots tancar aquest terminal i intentar iniciar el Gestor d'Agents de nou.",
 			"authReminder": "If this is your first install, run `kilocode auth` to sign in before starting the Agent Manager."
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "La vostra clau de l'API d'OpenAI no és vàlida o no té accés a l'API en temps real. Si us plau, comproveu la vostra clau de l'API i torneu-ho a provar.",
+			"unknown": "Ha fallat la connexió. Comproveu la configuració i torneu-ho a provar."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/cs/kilocode.json

@@ -197,5 +197,11 @@
 			"cancelButton": "Zrušit"
 		},
 		"tipMessage": "Kilo: Stiskněte {{shortcut}} pro generování příkazů v terminálu"
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "Váš OpenAI API klíč je neplatný nebo nemá přístup k Realtime API. Zkontrolujte prosím svůj API klíč a zkuste to znovu.",
+			"unknown": "Připojení se nezdařilo. Zkontrolujte prosím svou konfiguraci a zkuste to znovu."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/de/kilocode.json

@@ -197,5 +197,11 @@
 			"installMessage": "Führe npm install aus, um die Kilocode CLI zu installieren. Sobald die Installation abgeschlossen ist, kannst du dieses Terminal schließen und versuchen, den Agent Manager erneut zu starten.",
 			"authReminder": "If this is your first install, run `kilocode auth` to sign in before starting the Agent Manager."
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "Ihr OpenAI API-Schlüssel ist ungültig oder hat keinen Zugriff auf die Realtime API. Bitte überprüfen Sie Ihren API-Schlüssel und versuchen Sie es erneut.",
+			"unknown": "Verbindung fehlgeschlagen. Bitte überprüfen Sie Ihre Konfiguration und versuchen Sie es erneut."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/en/kilocode.json

@@ -197,5 +197,11 @@
 			"okButton": "I Understand",
 			"cancelButton": "Cancel"
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "Your OpenAI API key is invalid or does not have access to the Realtime API. Please check your API key and try again.",
+			"unknown": "Connection failed. Please check your configuration and try again."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/es/kilocode.json

@@ -197,5 +197,11 @@
 			"installMessage": "Ejecutando npm install para instalar Kilocode CLI. Una vez completado, puedes cerrar este terminal e intentar iniciar el Gestor de Agentes nuevamente.",
 			"authReminder": "If this is your first install, run `kilocode auth` to sign in before starting the Agent Manager."
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "Tu clave de API de OpenAI es inválida o no tiene acceso a la API de Realtime. Por favor verifica tu clave de API e inténtalo de nuevo.",
+			"unknown": "Error de conexión. Por favor, verifica tu configuración e inténtalo de nuevo."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/fr/kilocode.json

@@ -197,5 +197,11 @@
 			"installMessage": "Exécution de npm install pour installer Kilocode CLI. Une fois terminé, tu peux fermer ce terminal et essayer de démarrer le Gestionnaire d'Agents à nouveau.",
 			"authReminder": "If this is your first install, run `kilocode auth` to sign in before starting the Agent Manager."
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "Votre clé API OpenAI est invalide ou n'a pas accès à l'API Realtime. Veuillez vérifier votre clé API et réessayer.",
+			"unknown": "Échec de la connexion. Veuillez vérifier votre configuration et réessayer."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/hi/kilocode.json

@@ -197,5 +197,11 @@
 			"cancelButton": "रद्द करें"
 		},
 		"tipMessage": "Kilo: टर्मिनल कमांड उत्पन्न करने के लिए {{shortcut}} दबाएँ"
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "आपकी OpenAI API key अमान्य है या इसमें Realtime API तक पहुंच नहीं है। कृपया अपनी API key जांचें और पुनः प्रयास करें।",
+			"unknown": "कनेक्शन असफल हुआ। कृपया अपनी कॉन्फ़िगरेशन जांचें और पुनः प्रयास करें।"
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/id/kilocode.json

@@ -197,5 +197,11 @@
 			"installMessage": "Menjalankan npm install untuk menginstal Kilocode CLI. Setelah selesai, kamu dapat menutup terminal ini dan mencoba memulai Agent Manager lagi.",
 			"authReminder": "If this is your first install, run `kilocode auth` to sign in before starting the Agent Manager."
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "Kunci API OpenAI Anda tidak valid atau tidak memiliki akses ke Realtime API. Silakan periksa kunci API Anda dan coba lagi.",
+			"unknown": "Koneksi gagal. Silakan periksa konfigurasi Anda dan coba lagi."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/it/kilocode.json

@@ -197,5 +197,11 @@
 			"installMessage": "Esecuzione di npm install per installare Kilocode CLI. Una volta completato, puoi chiudere questo terminale e provare ad avviare nuovamente l'Agent Manager.",
 			"authReminder": "If this is your first install, run `kilocode auth` to sign in before starting the Agent Manager."
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "La tua chiave API OpenAI non è valida o non ha accesso alla Realtime API. Controlla la tua chiave API e riprova.",
+			"unknown": "Connessione fallita. Controlla la tua configurazione e riprova."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/ja/kilocode.json

@@ -197,5 +197,11 @@
 			"installMessage": "Kilocode CLIをインストールするためにnpm installを実行しています。完了したら、このターミナルを閉じてAgent Managerを再度起動してみてください。",
 			"authReminder": "If this is your first install, run `kilocode auth` to sign in before starting the Agent Manager."
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "OpenAI APIキーが無効であるか、Realtime APIへのアクセス権限がありません。APIキーを確認して再度お試しください。",
+			"unknown": "接続に失敗しました。設定を確認してもう一度お試しください。"
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/ko/kilocode.json

@@ -197,5 +197,11 @@
 			"installMessage": "Kilocode CLI를 설치하기 위해 npm install을 실행합니다. 완료되면 이 터미널을 닫고 Agent Manager를 다시 시작해 보세요.",
 			"authReminder": "If this is your first install, run `kilocode auth` to sign in before starting the Agent Manager."
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "OpenAI API 키가 유효하지 않거나 Realtime API에 대한 액세스 권한이 없습니다. API 키를 확인하고 다시 시도해 주세요.",
+			"unknown": "연결에 실패했습니다. 설정을 확인한 후 다시 시도해 주세요."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/nl/kilocode.json

@@ -197,5 +197,11 @@
 			"installMessage": "npm install uitvoeren om Kilocode CLI te installeren. Zodra voltooid, kun je dit terminal sluiten en proberen de Agent Manager opnieuw te starten.",
 			"authReminder": "If this is your first install, run `kilocode auth` to sign in before starting the Agent Manager."
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "Uw OpenAI API-sleutel is ongeldig of heeft geen toegang tot de Realtime API. Controleer uw API-sleutel en probeer het opnieuw.",
+			"unknown": "Verbinding mislukt. Controleer uw configuratie en probeer het opnieuw."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/pl/kilocode.json

@@ -197,5 +197,11 @@
 			"cancelButton": "Anuluj"
 		},
 		"tipMessage": "Kilo: Naciśnij {{shortcut}}, aby wygenerować polecenia terminala"
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "Twój klucz API OpenAI jest nieprawidłowy lub nie ma dostępu do API Realtime. Sprawdź swój klucz API i spróbuj ponownie.",
+			"unknown": "Połączenie nie powiodło się. Sprawdź konfigurację i spróbuj ponownie."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/pt-BR/kilocode.json

@@ -197,5 +197,11 @@
 			"installMessage": "Executando npm install para instalar o Kilocode CLI. Após a conclusão, você pode fechar este terminal e tentar iniciar o Gerenciador de Agentes novamente.",
 			"authReminder": "If this is your first install, run `kilocode auth` to sign in before starting the Agent Manager."
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "Sua chave da API OpenAI é inválida ou não tem acesso à API Realtime. Por favor, verifique sua chave da API e tente novamente.",
+			"unknown": "Falha na conexão. Verifique sua configuração e tente novamente."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/ru/kilocode.json

@@ -197,5 +197,11 @@
 			"installMessage": "Выполнение npm install для установки Kilocode CLI. После завершения можешь закрыть этот терминал и попробовать запустить Agent Manager снова.",
 			"authReminder": "If this is your first install, run `kilocode auth` to sign in before starting the Agent Manager."
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "Ваш API-ключ OpenAI недействителен или не имеет доступа к Realtime API. Пожалуйста, проверьте ваш API-ключ и попробуйте снова.",
+			"unknown": "Подключение не удалось. Пожалуйста, проверьте настройки и повторите попытку."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/th/kilocode.json

@@ -197,5 +197,11 @@
 			"cancelButton": "ยกเลิก"
 		},
 		"tipMessage": "Kilo: กด {{shortcut}} เพื่อสร้างคำสั่งเทอร์มินัล"
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "คีย์ OpenAI API ของคุณไม่ถูกต้องหรือไม่มีสิทธิ์เข้าถึง Realtime API กรุณาตรวจสอบคีย์ API ของคุณและลองใหม่อีกครั้ง",
+			"unknown": "การเชื่อมต่อล้มเหลว กรุณาตรวจสอบการกำหนดค่าของคุณและลองใหม่อีกครั้ง"
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/tr/kilocode.json

@@ -197,5 +197,11 @@
 			"installMessage": "Kilocode CLI'yi yüklemek için npm install çalıştırılıyor. Tamamlandığında, bu terminali kapatıp Agent Manager'ı tekrar başlatmayı deneyebilirsin.",
 			"authReminder": "If this is your first install, run `kilocode auth` to sign in before starting the Agent Manager."
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "OpenAI API anahtarınız geçersiz veya Realtime API'ye erişim izni bulunmuyor. Lütfen API anahtarınızı kontrol edin ve tekrar deneyin.",
+			"unknown": "Bağlantı başarısız oldu. Lütfen yapılandırmanızı kontrol edin ve tekrar deneyin."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/uk/kilocode.json

@@ -197,5 +197,11 @@
 			"installMessage": "Виконання npm install для встановлення Kilocode CLI. Після завершення можеш закрити цей термінал і спробувати запустити Agent Manager знову.",
 			"authReminder": "If this is your first install, run `kilocode auth` to sign in before starting the Agent Manager."
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "Ваш API ключ OpenAI недійсний або не має доступу до Realtime API. Будь ласка, перевірте ваш API ключ і спробуйте знову.",
+			"unknown": "Підключення не вдалося. Будь ласка, перевірте налаштування та спробуйте ще раз."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/vi/kilocode.json

@@ -197,5 +197,11 @@
 			"installMessage": "Đang chạy npm install để cài đặt Kilocode CLI. Sau khi hoàn tất, bạn có thể đóng terminal này và thử khởi động lại Agent Manager.",
 			"authReminder": "If this is your first install, run `kilocode auth` to sign in before starting the Agent Manager."
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "Khóa API OpenAI của bạn không hợp lệ hoặc không có quyền truy cập vào Realtime API. Vui lòng kiểm tra khóa API và thử lại.",
+			"unknown": "Kết nối thất bại. Vui lòng kiểm tra cấu hình của bạn và thử lại."
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/zh-CN/kilocode.json

@@ -197,5 +197,11 @@
 			"cancelButton": "取消"
 		},
 		"tipMessage": "Kilo Code: 按下 {{shortcut}} 生成终端命令"
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "您的 OpenAI API 密钥无效或无权访问实时 API。请检查您的 API 密钥并重试。",
+			"unknown": "连接失败。请检查您的配置并重试。"
+		}
 	}
 }

+ 6 - 0
src/i18n/locales/zh-TW/kilocode.json

@@ -197,5 +197,11 @@
 			"installMessage": "執行 npm install 以安裝 Kilocode CLI。完成後,您可以關閉此終端機並嘗試再次啟動 Agent Manager。",
 			"authReminder": "If this is your first install, run `kilocode auth` to sign in before starting the Agent Manager."
 		}
+	},
+	"speechToText": {
+		"errors": {
+			"invalidApiKey": "您的 OpenAI API 密钥无效或没有访问 Realtime API 的权限。请检查您的 API 密钥并重试。",
+			"unknown": "连接失败。请检查您的配置并重试。"
+		}
 	}
 }

+ 23 - 12
src/services/stt/OpenAIWhisperClient.ts

@@ -2,6 +2,7 @@ import { EventEmitter } from "events"
 import WebSocket from "ws"
 import { ProviderSettingsManager } from "../../core/config/ProviderSettingsManager"
 import { getOpenAiApiKey, getOpenAiBaseUrl } from "./utils/getOpenAiCredentials"
+import { t } from "../../i18n"
 
 /**
  * Configuration for OpenAI Whisper transcription via Realtime API
@@ -193,24 +194,32 @@ export class OpenAIWhisperClient extends EventEmitter {
 			await new Promise<void>((resolve, reject) => {
 				const timeout = setTimeout(() => {
 					reject(new Error("WebSocket connection timeout"))
-				}, 10000)
+				}, 5000)
 
 				const onOpen = () => {
 					clearTimeout(timeout)
-					this.ws!.off("open", onOpen)
-					this.ws!.off("error", onError)
+					this.ws?.off("open", onOpen)
+					this.ws?.off("error", onError)
 					resolve()
 				}
 
 				const onError = (error: Error) => {
 					clearTimeout(timeout)
-					this.ws!.off("open", onOpen)
-					this.ws!.off("error", onError)
-					reject(new Error(`WebSocket connection failed: ${error.message}`))
+					this.ws?.off("open", onOpen)
+					this.ws?.off("error", onError)
+					if (error.message.includes("401")) {
+						reject(new Error(t("kilocode:speechToText.errors.invalidApiKey")))
+					} else {
+						reject(new Error(t("kilocode:speechToText.errors.unknown")))
+					}
 				}
 
-				this.ws!.once("open", onOpen)
-				this.ws!.once("error", onError)
+				if (this.ws) {
+					this.ws.once("open", onOpen)
+					this.ws.once("error", onError)
+				} else {
+					reject(new Error("WebSocket not initialized"))
+				}
 			})
 
 			this.isConnecting = false
@@ -218,6 +227,10 @@ export class OpenAIWhisperClient extends EventEmitter {
 			this.emit("connected")
 		} catch (error) {
 			this.isConnecting = false
+			try {
+				this.ws?.removeAllListeners()
+				this.ws?.close()
+			} catch (_cleanupError) {}
 			this.ws = null
 			throw error
 		}
@@ -494,10 +507,8 @@ export class OpenAIWhisperClient extends EventEmitter {
 		}
 
 		// Close WebSocket
-		if (this.ws) {
-			this.ws.close(1000, "Client disconnect")
-			this.ws = null
-		}
+		this.ws?.close(1000, "Client disconnect")
+		this.ws = null
 
 		this.sessionConfigured = false
 		this.pendingAudioChunks = []

+ 32 - 21
src/services/stt/STTService.ts

@@ -91,6 +91,13 @@ export class STTService {
 
 			this.emitter.onStarted(this.sessionId)
 		} catch (error) {
+			console.log("🎙️ [STTService] ❌ Error during start:", {
+				errorType: error instanceof Error ? error.constructor.name : typeof error,
+				errorMessage: error instanceof Error ? error.message : String(error),
+				stack: error instanceof Error ? error.stack : undefined,
+			})
+
+			this.isActive = false
 			const errorMessage = error instanceof Error ? error.message : "Failed to start"
 			this.emitter.onStopped("error", undefined, errorMessage)
 			await this.cleanupOnError()
@@ -348,14 +355,28 @@ export class STTService {
 	 * Handle recoverable errors by emitting to UI and cleaning up
 	 */
 	private async handleRecoverableError(error: Error): Promise<void> {
+		console.warn("🎙️ [STTService] ⚠️ handleRecoverableError called:", {
+			errorMessage: error.message,
+			isActive: this.isActive,
+			sessionId: this.sessionId,
+		})
+
+		// Immediately stop processing to prevent any new audio/data from being processed
+		this.isActive = false
+
+		// Send error to frontend immediately
+		console.warn("🎙️ [STTService] 📤 Calling emitter.onStopped with error:", {
+			reason: "error",
+			text: undefined,
+			errorMessage: error.message,
+		})
 		this.emitter.onStopped("error", undefined, error.message)
 
-		if (this.isActive) {
-			try {
-				await this.cleanupOnError()
-			} catch (cleanupError) {
-				console.error("Failed to cleanup after error:", cleanupError)
-			}
+		// Cleanup resources asynchronously
+		try {
+			await this.cleanupOnError()
+		} catch (cleanupError) {
+			console.error("🎙️ [STTService] Failed to cleanup after error:", cleanupError)
 		}
 	}
 
@@ -394,21 +415,11 @@ export class STTService {
 	private async cleanupOnError(): Promise<void> {
 		this.isActive = false
 
-		// Force kill FFmpeg and disconnect - use Promise.allSettled to ensure both run
-		const cleanupResults = await Promise.allSettled([
-			this.audioCapture.stop(),
-			this.transcriptionClient?.disconnect() ?? Promise.resolve(),
-		])
-
-		// Log cleanup results for debugging
-		cleanupResults.forEach((result, index) => {
-			const name = index === 0 ? "audioCapture" : "transcriptionClient"
-			if (result.status === "rejected") {
-				console.error(`🎙️ [STTService] Failed to cleanup ${name}:`, result.reason)
-			} else {
-				console.log(`🎙️ [STTService] ${name} cleaned up successfully`)
-			}
-		})
+		await Promise.allSettled(
+			[this.audioCapture?.stop().catch(() => {}), this.transcriptionClient?.disconnect().catch(() => {})].filter(
+				Boolean,
+			),
+		)
 
 		this.resetSession()
 	}

+ 2 - 0
src/shared/ExtensionMessage.ts

@@ -138,6 +138,7 @@ export interface ExtensionMessage {
 		| "stt:transcript" // kilocode_change: STT transcript update
 		| "stt:volume" // kilocode_change: STT volume level
 		| "stt:stopped" // kilocode_change: STT session stopped
+		| "stt:statusResponse" // kilocode_change: Response to stt:checkAvailability request
 		| "setHistoryPreviewCollapsed"
 		| "commandExecutionStatus"
 		| "mcpExecutionStatus"
@@ -275,6 +276,7 @@ export interface ExtensionMessage {
 	isFinal?: boolean // kilocode_change: STT transcript is final
 	level?: number // kilocode_change: STT volume level (0-1)
 	reason?: "completed" | "cancelled" | "error" // kilocode_change: STT stop reason
+	speechToTextStatus?: { available: boolean; reason?: "openaiKeyMissing" | "ffmpegNotInstalled" } // kilocode_change: Speech-to-text availability status response
 	requestId?: string
 	promptText?: string
 	results?: { path: string; type: "file" | "folder"; label?: string }[]

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -145,6 +145,7 @@ export interface WebviewMessage {
 		| "stt:start" // kilocode_change: Start STT recording
 		| "stt:stop" // kilocode_change: Stop STT recording
 		| "stt:cancel" // kilocode_change: Cancel STT recording
+		| "stt:checkAvailability" // kilocode_change: Check STT availability on demand
 		| "includeTaskHistoryInEnhance" // kilocode_change
 		| "snoozeAutocomplete" // kilocode_change
 		| "autoApprovalEnabled"

+ 40 - 17
webview-ui/src/components/chat/ChatTextArea.tsx

@@ -36,9 +36,11 @@ import { IndexingStatusBadge } from "./IndexingStatusBadge"
 import { MicrophoneButton } from "./MicrophoneButton" // kilocode_change: STT microphone button
 import { VolumeVisualizer } from "./VolumeVisualizer" // kilocode_change: STT volume level visual
 import { VoiceRecordingCursor } from "./VoiceRecordingCursor" // kilocode_change: STT recording cursor
+import { STTSetupPopover } from "./STTSetupPopover" // kilocode_change: STT setup help popover
+import { useSTT } from "@/hooks/useSTT" // kilocode_change: STT hook
+import { useSTTStatus } from "@/hooks/useSTTStatus" // kilocode_change: STT status management hook
 import { cn } from "@/lib/utils"
 import { usePromptHistory } from "./hooks/usePromptHistory"
-import { useSTT } from "@/hooks/useSTT" // kilocode_change: STT hook
 
 // kilocode_change start: pull slash commands from Cline
 import SlashCommandMenu from "@/components/chat/SlashCommandMenu"
@@ -162,9 +164,16 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 			ghostServiceSettings, // kilocode_change
 			language, // User's VSCode display language
 			experiments, // kilocode_change: For speechToText experiment flag
-			speechToTextStatus, // kilocode_change: Speech-to-text availability status with failure reason
 		} = useExtensionState()
 
+		// kilocode_change start: Manage STT status and error state with auto-clearing
+		const {
+			status: speechToTextStatus,
+			error: sttError,
+			setError: setSttError,
+			handleStatusChange,
+		} = useSTTStatus()
+		// kilocode_change end: Manage STT status and error state with auto-clearing
 		// kilocode_change start - autocomplete profile type system
 		// Filter out autocomplete profiles - only show chat profiles in the chat interface
 		const listApiConfigMeta = useMemo(() => {
@@ -334,6 +343,8 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 			},
 			onError: (error) => {
 				console.error("STT error:", error)
+				setSttError(error)
+				setSttSetupPopoverOpen(true) // kilocode_change: Auto-show popover on error
 				recordingStartStateRef.current = null
 			},
 		})
@@ -458,13 +469,24 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 			setImageWarning(null)
 		}, [setImageWarning])
 
+		// kilocode_change start: Popover state for STT setup help
+		const [sttSetupPopoverOpen, setSttSetupPopoverOpen] = useState(false)
+
 		const handleMicrophoneClick = useCallback(() => {
+			// If STT is unavailable, open setup popover instead of starting recording
+			if (sttError || !speechToTextStatus?.available) {
+				setSttSetupPopoverOpen(true)
+				return
+			}
+
 			if (isRecording) {
 				stopSTT()
 			} else {
+				setSttError(null) // Clear any previous error when starting new recording
 				startSTT(language || "en") // Pass user's language from extension state
 			}
-		}, [isRecording, startSTT, stopSTT, language])
+		}, [sttError, speechToTextStatus?.available, isRecording, stopSTT, setSttError, startSTT, language])
+		// kilocode_change end: Popover state for STT setup help
 
 		// kilocode_change start: Auto-clear images when model changes to non-image-supporting
 		const prevShouldDisableImages = useRef<boolean>(shouldDisableImages)
@@ -1709,20 +1731,21 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 
 					{/* kilocode_change start: Show microphone button only if experiment enabled */}
 					{experiments?.speechToText && (
-						<MicrophoneButton
-							isRecording={isRecording}
-							onClick={handleMicrophoneClick}
-							disabled={!speechToTextStatus?.available}
-							tooltipContent={
-								!speechToTextStatus?.available && speechToTextStatus
-									? speechToTextStatus.reason === "openaiKeyMissing"
-										? t("kilocode:speechToText.unavailableOpenAiKeyMissing")
-										: speechToTextStatus.reason === "ffmpegNotInstalled"
-											? t("kilocode:speechToText.unavailableFfmpegNotInstalled")
-											: t("kilocode:speechToText.unavailableBoth")
-									: undefined
-							}
-						/>
+						<STTSetupPopover
+							speechToTextStatus={speechToTextStatus}
+							open={sttSetupPopoverOpen}
+							onOpenChange={setSttSetupPopoverOpen}
+							setInputValue={setInputValue}
+							onSend={onSend}
+							error={sttError}>
+							<MicrophoneButton
+								isRecording={isRecording}
+								onClick={handleMicrophoneClick}
+								disabled={!speechToTextStatus?.available}
+								hasError={!!sttError}
+								onStatusChange={handleStatusChange}
+							/>
+						</STTSetupPopover>
 					)}
 					{/* kilocode_change end */}
 

+ 46 - 9
webview-ui/src/components/chat/MicrophoneButton.tsx

@@ -1,16 +1,27 @@
 // kilocode_change - new file: Microphone button component for speech-to-text recording
-import React from "react"
+import React, { useEffect, useCallback } from "react"
 import { Mic, Square } from "lucide-react"
 import { useTranslation } from "react-i18next"
 import { StandardTooltip } from "@/components/ui"
 import { cn } from "@/lib/utils"
+import { vscode } from "@/utils/vscode"
+import { useEvent } from "react-use"
+import type { ExtensionMessage } from "@roo/ExtensionMessage"
 
 interface MicrophoneButtonProps {
 	isRecording: boolean
 	onClick: () => void
 	containerWidth?: number
-	disabled?: boolean
-	tooltipContent?: string
+	disabled?: boolean // Visual disabled state only - button is always clickable
+	hasError?: boolean // Show red dot indicator when there's an error
+	onStatusChange?: (
+		status:
+			| {
+					available: boolean
+					reason?: "openaiKeyMissing" | "ffmpegNotInstalled"
+			  }
+			| undefined,
+	) => void
 }
 
 export const MicrophoneButton: React.FC<MicrophoneButtonProps> = ({
@@ -18,22 +29,42 @@ export const MicrophoneButton: React.FC<MicrophoneButtonProps> = ({
 	onClick,
 	containerWidth,
 	disabled = false,
-	tooltipContent,
+	hasError = false,
+	onStatusChange,
 }) => {
 	const { t } = useTranslation()
 
-	const defaultTooltip = isRecording
-		? t("kilocode:speechToText.stopRecording")
-		: t("kilocode:speechToText.startRecording")
+	const checkAvailability = useCallback(() => {
+		vscode.postMessage({ type: "stt:checkAvailability" })
+	}, [])
+
+	useEffect(() => {
+		checkAvailability() // Check availability on mount
+	}, [checkAvailability])
+
+	useEvent("message", (event: MessageEvent) => {
+		const message: ExtensionMessage = event.data
+		if (message.type === "stt:statusResponse" && message.speechToTextStatus) {
+			onStatusChange?.(message.speechToTextStatus)
+		}
+	})
+
+	const tooltipContent = disabled
+		? t("kilocode:speechToText.misconfiguredState")
+		: hasError
+			? t("kilocode:speechToText.errorState")
+			: isRecording
+				? t("kilocode:speechToText.stopRecording")
+				: t("kilocode:speechToText.startRecording")
 
 	return (
-		<StandardTooltip content={tooltipContent || defaultTooltip}>
+		<StandardTooltip content={tooltipContent}>
 			<button
 				aria-label={
 					isRecording ? t("kilocode:speechToText.stopRecording") : t("kilocode:speechToText.startRecording")
 				}
-				disabled={disabled}
 				onClick={onClick}
+				onMouseEnter={checkAvailability}
 				className={cn(
 					"relative inline-flex items-center justify-center",
 					"bg-transparent border-none p-1.5",
@@ -48,6 +79,12 @@ export const MicrophoneButton: React.FC<MicrophoneButtonProps> = ({
 					containerWidth !== undefined && { hidden: containerWidth < 235 },
 				)}>
 				{isRecording ? <Square className="w-4 h-4 fill-current" /> : <Mic className="w-4 h-4" />}
+				{hasError && !isRecording && (
+					<span
+						className="absolute top-0 right-0 w-2 h-2 bg-red-500 rounded-full"
+						aria-label="Error occurred"
+					/>
+				)}
 			</button>
 		</StandardTooltip>
 	)

+ 164 - 0
webview-ui/src/components/chat/STTSetupPopover.tsx

@@ -0,0 +1,164 @@
+// kilocode_change - new file: STT setup help popover
+import React, { useCallback } from "react"
+import { useTranslation, Trans } from "react-i18next"
+import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui"
+import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
+import { buildDocLink } from "@/utils/docLinks"
+import { vscode } from "@/utils/vscode"
+
+interface STTSetupPopoverProps {
+	children: React.ReactNode
+	speechToTextStatus?: {
+		available: boolean
+		reason?: "openaiKeyMissing" | "ffmpegNotInstalled"
+	}
+	open: boolean
+	onOpenChange: (open: boolean) => void
+	setInputValue: (value: string) => void
+	onSend: () => void
+	error?: string | null
+}
+
+interface STTSetupPopoverContentProps {
+	reason?: "openaiKeyMissing" | "ffmpegNotInstalled"
+	setInputValue: (value: string) => void
+	onSend: () => void
+	onOpenChange?: (open: boolean) => void
+	error?: string | null
+}
+
+export const STTSetupPopoverContent: React.FC<STTSetupPopoverContentProps> = ({
+	reason,
+	setInputValue,
+	onSend,
+	onOpenChange,
+	error,
+}) => {
+	const { t } = useTranslation()
+	const docsUrl = buildDocLink("features/experimental/voice-transcription", "stt_setup")
+
+	const handleOpenAiHelpClick = () => {
+		vscode.postMessage({ type: "openExternal", url: docsUrl })
+		onOpenChange?.(false)
+	}
+
+	// kilocode_change: FFmpeg help - send message to chat
+	const handleFfmpegHelpClick = useCallback(() => {
+		const helpMessage = t("kilocode:speechToText.setupPopover.ffmpegMessage")
+		setInputValue(helpMessage)
+
+		setTimeout(() => {
+			onSend()
+		}) // Trigger send after a brief delay to ensure input is set
+	}, [t, setInputValue, onSend])
+
+	return (
+		<div className="p-4 cursor-default">
+			<h4 className="m-0 mb-3 text-base font-semibold">{t("kilocode:speechToText.setupPopover.title")}</h4>
+
+			{error ? (
+				// When there's an error, show just the error message
+				<div
+					className="my-0 mb-3 p-2 rounded text-sm border"
+					style={{
+						backgroundColor: "var(--vscode-inputValidation-errorBackground, rgba(255, 0, 0, 0.1))",
+						borderColor: "var(--vscode-inputValidation-errorBorder, #ff0000)",
+						color: "var(--vscode-errorForeground, #ff0000)",
+					}}>
+					<strong>Error:</strong> {error}
+				</div>
+			) : (
+				// When STT is not available, show setup instructions
+				<>
+					<p className="my-0 mb-3 text-sm text-vscode-descriptionForeground">
+						<Trans
+							i18nKey="kilocode:speechToText.setupPopover.description"
+							components={{
+								moreInfoLink: (
+									<VSCodeLink
+										href={docsUrl}
+										onClick={(e) => {
+											e.preventDefault()
+											handleOpenAiHelpClick()
+										}}
+										className="inline"
+									/>
+								),
+							}}
+						/>
+					</p>
+
+					<ul className="my-0 mb-0 list-disc list-inside text-sm text-vscode-descriptionForeground space-y-1">
+						{reason === "openaiKeyMissing" || !reason ? (
+							<li>{t("kilocode:speechToText.setupPopover.openAiReason")}</li>
+						) : null}
+						{reason === "ffmpegNotInstalled" || !reason ? (
+							<li>
+								<Trans
+									i18nKey="kilocode:speechToText.setupPopover.ffmpegReason"
+									components={{
+										ffmpegLink: (
+											<VSCodeLink
+												href="#"
+												onClick={(e) => {
+													e.preventDefault()
+													handleFfmpegHelpClick()
+													onOpenChange?.(false)
+												}}
+												className="inline"
+											/>
+										),
+									}}
+								/>
+							</li>
+						) : null}
+					</ul>
+				</>
+			)}
+		</div>
+	)
+}
+
+export const STTSetupPopover: React.FC<STTSetupPopoverProps> = ({
+	children,
+	speechToTextStatus,
+	open,
+	onOpenChange,
+	setInputValue,
+	onSend,
+	error,
+}) => {
+	const portalContainer = useRooPortal("roo-portal")
+
+	// Show popover if STT is unavailable OR if there's an error
+	const shouldShowPopover = !speechToTextStatus?.available || !!error
+	if (!shouldShowPopover) {
+		return <>{children}</>
+	}
+
+	const reason = speechToTextStatus?.reason
+
+	return (
+		<Popover open={open} onOpenChange={onOpenChange}>
+			<PopoverTrigger>{children}</PopoverTrigger>
+			<PopoverContent
+				className="w-[calc(100vw-32px)] max-w-[400px] p-0"
+				align="end"
+				alignOffset={0}
+				side="bottom"
+				sideOffset={5}
+				collisionPadding={16}
+				avoidCollisions={true}
+				container={portalContainer}>
+				<STTSetupPopoverContent
+					reason={reason}
+					setInputValue={setInputValue}
+					onSend={onSend}
+					onOpenChange={onOpenChange}
+					error={error}
+				/>
+			</PopoverContent>
+		</Popover>
+	)
+}

+ 16 - 6
webview-ui/src/hooks/useSTT.ts

@@ -40,7 +40,10 @@ export interface UseSTTReturn {
 export function useSTT(options: UseSTTOptions = {}): UseSTTReturn {
 	const { onComplete, onError } = options
 
-	const [isRecording, setIsRecording] = useState(false)
+	// Optimistic state for immediate UI updates
+	const [optimisticIsRecording, setOptimisticIsRecording] = useState(false)
+	// Real state from backend (used to correct optimistic state if needed)
+	const [realIsRecording, setRealIsRecording] = useState(false)
 	const [segments, setSegments] = useState<STTSegment[]>([])
 	const [volume, setVolume] = useState(0)
 
@@ -53,6 +56,11 @@ export function useSTT(options: UseSTTOptions = {}): UseSTTReturn {
 		segmentsRef.current = segments
 	}, [segments])
 
+	// Sync optimistic state with real state when backend responds
+	useEffect(() => {
+		setOptimisticIsRecording(realIsRecording)
+	}, [realIsRecording])
+
 	useEffect(() => {
 		const handler = (event: MessageEvent) => {
 			const msg = event.data
@@ -63,7 +71,7 @@ export function useSTT(options: UseSTTOptions = {}): UseSTTReturn {
 			switch (msg.type) {
 				case "stt:started":
 					sessionIdRef.current = msg.sessionId
-					setIsRecording(true)
+					setRealIsRecording(true)
 					setSegments([])
 					break
 
@@ -81,9 +89,8 @@ export function useSTT(options: UseSTTOptions = {}): UseSTTReturn {
 					break
 
 				case "stt:stopped":
-					if (msg.sessionId !== sessionIdRef.current) return
-
-					setIsRecording(false)
+					setRealIsRecording(false)
+					setOptimisticIsRecording(false) // Immediately sync optimistic state on stop
 					setVolume(0)
 
 					if (msg.reason === "completed") {
@@ -111,19 +118,22 @@ export function useSTT(options: UseSTTOptions = {}): UseSTTReturn {
 	}, [onComplete, onError])
 
 	const start = useCallback((language?: string) => {
+		setOptimisticIsRecording(true)
 		vscode.postMessage({ type: "stt:start", language })
 	}, [])
 
 	const stop = useCallback(() => {
+		setOptimisticIsRecording(false)
 		vscode.postMessage({ type: "stt:stop" })
 	}, [])
 
 	const cancel = useCallback(() => {
+		setOptimisticIsRecording(false)
 		vscode.postMessage({ type: "stt:cancel" })
 	}, [])
 
 	return {
-		isRecording,
+		isRecording: optimisticIsRecording,
 		segments,
 		volume,
 		start,

+ 40 - 0
webview-ui/src/hooks/useSTTStatus.ts

@@ -0,0 +1,40 @@
+// kilocode_change - new file: Custom hook to manage STT status and error state
+import { useState, useCallback } from "react"
+
+export type SpeechToTextStatus =
+	| {
+			available: boolean
+			reason?: "openaiKeyMissing" | "ffmpegNotInstalled"
+	  }
+	| undefined
+
+export interface UseSTTStatusReturn {
+	status: SpeechToTextStatus
+	error: string | null
+	setError: (error: string | null) => void
+	handleStatusChange: (newStatus: SpeechToTextStatus) => void
+}
+
+/**
+ * Hook to manage STT status and error state
+ * Automatically clears error when status becomes available
+ */
+export function useSTTStatus(): UseSTTStatusReturn {
+	const [status, setStatus] = useState<SpeechToTextStatus>(undefined)
+	const [error, setError] = useState<string | null>(null)
+
+	const handleStatusChange = useCallback((newStatus: SpeechToTextStatus) => {
+		setStatus(newStatus)
+		// Clear error when STT becomes available (e.g., user fixed their API key)
+		if (newStatus?.available) {
+			setError(null)
+		}
+	}, [])
+
+	return {
+		status,
+		error,
+		setError,
+		handleStatusChange,
+	}
+}

+ 13 - 5
webview-ui/src/i18n/locales/ar/kilocode.json

@@ -322,10 +322,18 @@
 		"initiating": "جاري بدء المصادقة..."
 	},
 	"speechToText": {
-		"stopRecording": "إيقاف إدخال الصوت",
-		"startRecording": "بدء الإدخال الصوتي",
-		"unavailableFfmpegNotInstalled": "التحويل من الكلام إلى نص غير متاح. قم بتثبيت FFmpeg لاستخدام النسخ الصوتي.",
-		"unavailableBoth": "تحويل الصوت إلى نص غير متاح. يتطلب ذلك مزود OpenAI صالح ويجب تثبيت FFmpeg.",
-		"unavailableOpenAiKeyMissing": "تحويل الكلام إلى نص غير متاح. يتطلب استخدام مزود OpenAI صالح مع مفتاح API لاستخدام خاصية نسخ الصوت."
+		"stopRecording": "إيقاف الإدخال الصوتي",
+		"startRecording": "ابدأ الإدخال الصوتي",
+		"setupPopover": {
+			"title": "تحويل الكلام إلى نص",
+			"ffmpegHelp": "تحتاج إلى FFmpeg. <ffmpegLink>انقر هنا</ffmpegLink> للحصول على مساعدة Kilo في تثبيت FFmpeg.",
+			"openAiHelp": "انقر هنا لمزيد من المعلومات",
+			"ffmpegMessage": "ساعدني في تثبيت FFmpeg",
+			"openAiReason": "تحتاج إلى مزود OpenAI صالح مع مفتاح API لاستخدام نسخ الصوت.",
+			"description": "تحويل الكلام إلى نص غير متاح. يتطلب تحويل الكلام إلى نص كلاً من FFmpeg وموفر OpenAI صالح تم تكوينه. <moreInfoLink>انقر هنا لمزيد من المعلومات</moreInfoLink>.",
+			"ffmpegReason": "تحتاج إلى تثبيت FFmpeg. <ffmpegLink>انقر هنا</ffmpegLink> لمساعدتك من Kilo في تثبيت FFmpeg."
+		},
+		"misconfiguredState": "غير متاح بسبب الإعدادات",
+		"errorState": "غير متاح بسبب خطأ"
 	}
 }

+ 9 - 3
webview-ui/src/i18n/locales/ca/kilocode.json

@@ -325,8 +325,14 @@
 	"speechToText": {
 		"stopRecording": "Atura l'entrada de veu",
 		"startRecording": "Inicia l'entrada de veu",
-		"unavailableFfmpegNotInstalled": "La transcripció de veu a text no està disponible. Instal·leu FFmpeg per utilitzar la transcripció de veu.",
-		"unavailableOpenAiKeyMissing": "La transcripció de veu a text no està disponible. Requereix un proveïdor d'OpenAI vàlid amb una clau API per utilitzar la transcripció de veu.",
-		"unavailableBoth": "La conversió de veu a text no està disponible. Requereix un proveïdor vàlid d'OpenAI i FFmpeg ha d'estar instal·lat."
+		"setupPopover": {
+			"title": "Veu a text",
+			"description": "La conversió de veu a text no està disponible. La conversió de veu a text requereix tant FFmpeg com un proveïdor d'OpenAI vàlid configurat. <moreInfoLink>Feu clic aquí per a més informació</moreInfoLink>.",
+			"openAiReason": "Necessiteu un proveïdor d'OpenAI vàlid amb una clau API per utilitzar la transcripció de veu.",
+			"ffmpegReason": "Necessites tenir instal·lat FFmpeg. <ffmpegLink>Fes clic aquí</ffmpegLink> perquè Kilo t'ajudi a instal·lar FFmpeg.",
+			"ffmpegMessage": "Ajuda'm a instal·lar FFmpeg"
+		},
+		"misconfiguredState": "No disponible per la configuració",
+		"errorState": "No disponible a causa d'un error"
 	}
 }

+ 9 - 3
webview-ui/src/i18n/locales/cs/kilocode.json

@@ -325,8 +325,14 @@
 	"speechToText": {
 		"startRecording": "Spustit hlasový vstup",
 		"stopRecording": "Zastavit hlasový vstup",
-		"unavailableBoth": "Převod řeči na text není k dispozici. Vyžaduje platného poskytovatele OpenAI a musí být nainstalován FFmpeg.",
-		"unavailableFfmpegNotInstalled": "Převod řeči na text není k dispozici. Nainstalujte FFmpeg pro využití transkripce hlasu.",
-		"unavailableOpenAiKeyMissing": "Převod řeči na text není k dispozici. Pro použití hlasového přepisu vyžaduje platného poskytovatele OpenAI s API klíčem."
+		"setupPopover": {
+			"title": "Převod řeči na text",
+			"openAiReason": "Pro použití hlasové transkripce potřebujete platného poskytovatele OpenAI s API klíčem.",
+			"description": "Převod řeči na text není k dispozici. Převod řeči na text vyžaduje jak FFmpeg, tak platně nakonfigurovaného poskytovatele OpenAI. <moreInfoLink>Klikněte sem pro více informací</moreInfoLink>.",
+			"ffmpegReason": "Potřebujete nainstalovaný FFmpeg. <ffmpegLink>Klikněte zde</ffmpegLink>, aby vám Kilo pomohlo s instalací FFmpeg.",
+			"ffmpegMessage": "Pomozte mi nainstalovat FFmpeg"
+		},
+		"misconfiguredState": "Nedostupné kvůli konfiguraci",
+		"errorState": "Nedostupné kvůli chybě"
 	}
 }

+ 9 - 3
webview-ui/src/i18n/locales/de/kilocode.json

@@ -324,8 +324,14 @@
 	"speechToText": {
 		"startRecording": "Spracheingabe starten",
 		"stopRecording": "Spracheingabe beenden",
-		"unavailableBoth": "Spracherkennung ist nicht verfügbar. Es erfordert einen gültigen OpenAI-Anbieter und FFmpeg muss installiert sein.",
-		"unavailableFfmpegNotInstalled": "Sprache-zu-Text ist nicht verfügbar. Installieren Sie FFmpeg, um Sprachtranskription zu nutzen.",
-		"unavailableOpenAiKeyMissing": "Sprache-zu-Text ist nicht verfügbar. Es erfordert einen gültigen OpenAI-Anbieter mit einem API-Schlüssel, um Sprachtranskription zu nutzen."
+		"setupPopover": {
+			"title": "Sprache-zu-Text",
+			"description": "Sprache-zu-Text ist nicht verfügbar. Sprache-zu-Text erfordert sowohl FFmpeg als auch einen konfigurierten, gültigen OpenAI-Anbieter. <moreInfoLink>Klicken Sie hier für weitere Informationen</moreInfoLink>.",
+			"openAiReason": "Sie benötigen einen gültigen OpenAI-Anbieter mit einem API-Schlüssel, um die Sprachtranskription zu verwenden.",
+			"ffmpegReason": "Sie benötigen FFmpeg. <ffmpegLink>Klicken Sie hier</ffmpegLink>, damit Kilo Ihnen bei der Installation von FFmpeg hilft.",
+			"ffmpegMessage": "Hilf mir, FFmpeg zu installieren"
+		},
+		"misconfiguredState": "Nicht verfügbar aufgrund der Konfiguration",
+		"errorState": "Nicht verfügbar aufgrund eines Fehlers"
 	}
 }

+ 9 - 3
webview-ui/src/i18n/locales/en/kilocode.json

@@ -323,8 +323,14 @@
 	"speechToText": {
 		"startRecording": "Start voice input",
 		"stopRecording": "Stop voice input",
-		"unavailableOpenAiKeyMissing": "Speech-to-text is unavailable. It requires a valid OpenAI provider with an API key to use voice transcription.",
-		"unavailableFfmpegNotInstalled": "Speech-to-text is unavailable. Install FFmpeg to use voice transcription.",
-		"unavailableBoth": "Speech-to-text is unavailable. It requires a valid OpenAI provider and FFmpeg must be installed."
+		"errorState": "Unavailable because of error",
+		"misconfiguredState": "Unavailable because of config",
+		"setupPopover": {
+			"title": "Speech-to-Text",
+			"description": "Speech-to-text is unavailable. Speech-to-text requires both FFmpeg and a valid OpenAI provider configured. <moreInfoLink>Click here for more information</moreInfoLink>.",
+			"openAiReason": "You need a valid OpenAI provider with an API key to use voice transcription.",
+			"ffmpegReason": "You need FFmpeg installed. <ffmpegLink>Click here</ffmpegLink> to have Kilo help you install FFmpeg.",
+			"ffmpegMessage": "Help me install FFmpeg"
+		}
 	}
 }

+ 9 - 3
webview-ui/src/i18n/locales/es/kilocode.json

@@ -325,8 +325,14 @@
 	"speechToText": {
 		"startRecording": "Iniciar entrada de voz",
 		"stopRecording": "Detener entrada de voz",
-		"unavailableOpenAiKeyMissing": "La conversión de voz a texto no está disponible. Requiere un proveedor OpenAI válido con una clave API para utilizar la transcripción de voz.",
-		"unavailableFfmpegNotInstalled": "La conversión de voz a texto no está disponible. Instale FFmpeg para usar la transcripción de voz.",
-		"unavailableBoth": "El reconocimiento de voz a texto no está disponible. Requiere un proveedor válido de OpenAI y FFmpeg debe estar instalado."
+		"setupPopover": {
+			"title": "Voz a Texto",
+			"description": "La conversión de voz a texto no está disponible. La conversión de voz a texto requiere tanto FFmpeg como un proveedor de OpenAI válido configurado. <moreInfoLink>Haga clic aquí para más información</moreInfoLink>.",
+			"openAiReason": "Necesitas un proveedor válido de OpenAI con una clave API para usar la transcripción de voz.",
+			"ffmpegReason": "Necesitas tener FFmpeg instalado. <ffmpegLink>Haz clic aquí</ffmpegLink> para que Kilo te ayude a instalar FFmpeg.",
+			"ffmpegMessage": "Ayúdame a instalar FFmpeg"
+		},
+		"misconfiguredState": "No disponible debido a la configuración",
+		"errorState": "No disponible debido a un error"
 	}
 }

+ 9 - 3
webview-ui/src/i18n/locales/fr/kilocode.json

@@ -325,8 +325,14 @@
 	"speechToText": {
 		"startRecording": "Démarrer la saisie vocale",
 		"stopRecording": "Arrêter la saisie vocale",
-		"unavailableOpenAiKeyMissing": "La reconnaissance vocale est indisponible. Elle nécessite un fournisseur OpenAI valide avec une clé API pour utiliser la transcription vocale.",
-		"unavailableFfmpegNotInstalled": "La reconnaissance vocale n'est pas disponible. Installez FFmpeg pour utiliser la transcription vocale.",
-		"unavailableBoth": "La reconnaissance vocale n'est pas disponible. Elle nécessite un fournisseur OpenAI valide et FFmpeg doit être installé."
+		"setupPopover": {
+			"title": "Reconnaissance vocale",
+			"description": "La reconnaissance vocale est indisponible. La reconnaissance vocale nécessite à la fois FFmpeg et un fournisseur OpenAI valide configuré. <moreInfoLink>Cliquez ici pour plus d'informations</moreInfoLink>.",
+			"openAiReason": "Vous avez besoin d'un fournisseur OpenAI valide avec une clé API pour utiliser la transcription vocale.",
+			"ffmpegReason": "Vous devez installer FFmpeg. <ffmpegLink>Cliquez ici</ffmpegLink> pour que Kilo vous aide à installer FFmpeg.",
+			"ffmpegMessage": "Aidez-moi à installer FFmpeg"
+		},
+		"misconfiguredState": "Indisponible en raison de la configuration",
+		"errorState": "Indisponible en raison d'une erreur"
 	}
 }

+ 11 - 5
webview-ui/src/i18n/locales/hi/kilocode.json

@@ -323,10 +323,16 @@
 		"initiating": "प्रमाणीकरण शुरू हो रहा है..."
 	},
 	"speechToText": {
-		"stopRecording": "आवाज़ इनपुट बंद करें",
-		"startRecording": "आवाज से इनपुट शुरू करें",
-		"unavailableFfmpegNotInstalled": "स्पीच-टू-टेक्स्ट अनुपलब्ध है। वॉयस ट्रांसक्रिप्शन का उपयोग करने के लिए FFmpeg इंस्टॉल करें।",
-		"unavailableOpenAiKeyMissing": "स्पीच-से-टेक्स्ट उपलब्ध नहीं है। इसके लिए आवाज़ ट्रांसक्रिप्शन का उपयोग करने के लिए एक वैध OpenAI प्रदाता और API कुंजी की आवश्यकता है।",
-		"unavailableBoth": "स्पीच-टू-टेक्स्ट उपलब्ध नहीं है। इसके लिए एक मान्य OpenAI प्रदाता की आवश्यकता है और FFmpeg इंस्टॉल होना चाहिए।"
+		"stopRecording": "वॉयस इनपुट बंद करें",
+		"startRecording": "वॉयस इनपुट शुरू करें",
+		"setupPopover": {
+			"title": "वाक्-से-पाठ",
+			"description": "स्पीच-टू-टेक्स्ट उपलब्ध नहीं है। स्पीच-टू-टेक्स्ट के लिए FFmpeg और एक वैध OpenAI प्रोवाइडर दोनों कॉन्फ़िगर होने आवश्यक हैं। <moreInfoLink>अधिक जानकारी के लिए यहाँ क्लिक करें</moreInfoLink>।",
+			"openAiReason": "वॉयस ट्रांसक्रिप्शन का उपयोग करने के लिए आपको API key के साथ एक वैध OpenAI प्रदाता की आवश्यकता है।",
+			"ffmpegReason": "आपको FFmpeg इंस्टॉल करना होगा। FFmpeg इंस्टॉल करने में Kilo की मदद के लिए <ffmpegLink>यहाँ क्लिक करें</ffmpegLink>।",
+			"ffmpegMessage": "मुझे FFmpeg इंस्टॉल करने में मदद करें"
+		},
+		"misconfiguredState": "कॉन्फ़िग की वजह से अनुपलब्ध",
+		"errorState": "त्रुटि के कारण अनुपलब्ध"
 	}
 }

+ 10 - 4
webview-ui/src/i18n/locales/id/kilocode.json

@@ -323,10 +323,16 @@
 		"initiating": "Memulai autentikasi..."
 	},
 	"speechToText": {
-		"startRecording": "Mulai masukan suara",
+		"startRecording": "Mulai input suara",
 		"stopRecording": "Hentikan input suara",
-		"unavailableOpenAiKeyMissing": "Ucapan-ke-teks tidak tersedia. Ini memerlukan penyedia OpenAI yang valid dengan kunci API untuk menggunakan transkripsi suara.",
-		"unavailableFfmpegNotInstalled": "Ucapan-ke-teks tidak tersedia. Pasang FFmpeg untuk menggunakan transkripsi suara.",
-		"unavailableBoth": "Ucapan-ke-teks tidak tersedia. Fitur ini memerlukan penyedia OpenAI yang valid dan FFmpeg harus terpasang."
+		"setupPopover": {
+			"title": "Ucapan-ke-Teks",
+			"description": "Speech-to-text tidak tersedia. Speech-to-text memerlukan FFmpeg dan penyedia OpenAI yang valid untuk dikonfigurasi. <moreInfoLink>Klik di sini untuk informasi lebih lanjut</moreInfoLink>.",
+			"openAiReason": "Anda memerlukan penyedia OpenAI yang valid dengan kunci API untuk menggunakan transkripsi suara.",
+			"ffmpegReason": "Anda perlu menginstal FFmpeg. <ffmpegLink>Klik di sini</ffmpegLink> agar Kilo membantu Anda menginstal FFmpeg.",
+			"ffmpegMessage": "Bantu saya menginstal FFmpeg"
+		},
+		"misconfiguredState": "Tidak tersedia karena konfigurasi",
+		"errorState": "Tidak tersedia karena terjadi kesalahan"
 	}
 }

+ 10 - 4
webview-ui/src/i18n/locales/it/kilocode.json

@@ -323,10 +323,16 @@
 		"initiating": "Avvio autenticazione..."
 	},
 	"speechToText": {
-		"startRecording": "Inizia l'input vocale",
+		"startRecording": "Avvia input vocale",
 		"stopRecording": "Interrompi input vocale",
-		"unavailableOpenAiKeyMissing": "La conversione da voce a testo non è disponibile. Richiede un provider OpenAI valido con una chiave API per utilizzare la trascrizione vocale.",
-		"unavailableFfmpegNotInstalled": "La conversione vocale in testo non è disponibile. Installa FFmpeg per utilizzare la trascrizione vocale.",
-		"unavailableBoth": "Il riconoscimento vocale non è disponibile. Richiede un provider OpenAI valido e FFmpeg deve essere installato."
+		"setupPopover": {
+			"title": "Sintesi vocale",
+			"description": "La sintesi vocale non è disponibile. La sintesi vocale richiede sia FFmpeg che un provider OpenAI valido configurato. <moreInfoLink>Clicca qui per maggiori informazioni</moreInfoLink>.",
+			"openAiReason": "È necessario un provider OpenAI valido con una chiave API per utilizzare la trascrizione vocale.",
+			"ffmpegReason": "È necessario avere FFmpeg installato. <ffmpegLink>Clicca qui</ffmpegLink> per far sì che Kilo ti aiuti a installare FFmpeg.",
+			"ffmpegMessage": "Aiutami a installare FFmpeg"
+		},
+		"misconfiguredState": "Non disponibile a causa della configurazione",
+		"errorState": "Non disponibile a causa di un errore"
 	}
 }

+ 9 - 3
webview-ui/src/i18n/locales/ja/kilocode.json

@@ -325,8 +325,14 @@
 	"speechToText": {
 		"startRecording": "音声入力を開始",
 		"stopRecording": "音声入力を停止",
-		"unavailableOpenAiKeyMissing": "音声認識は利用できません。音声文字変換を使用するには、APIキーを持つ有効なOpenAIプロバイダーが必要です。",
-		"unavailableFfmpegNotInstalled": "音声からテキストへの変換は利用できません。音声の文字起こしを使用するにはFFmpegをインストールしてください。",
-		"unavailableBoth": "音声からテキストへの変換は利用できません。有効なOpenAIプロバイダーが必要で、FFmpegがインストールされている必要があります。"
+		"setupPopover": {
+			"title": "音声テキスト変換",
+			"description": "音声テキスト変換は利用できません。音声テキスト変換にはFFmpegと有効なOpenAIプロバイダーの両方が設定されている必要があります。<moreInfoLink>詳細はこちらをクリック</moreInfoLink>。",
+			"openAiReason": "音声文字起こしを使用するには、APIキーを持つ有効なOpenAIプロバイダーが必要です。",
+			"ffmpegReason": "FFmpegがインストールされている必要があります。<ffmpegLink>こちらをクリック</ffmpegLink>すると、KiloがFFmpegのインストールをサポートします。",
+			"ffmpegMessage": "FFmpegのインストールを手伝ってください"
+		},
+		"misconfiguredState": "設定により利用不可",
+		"errorState": "エラーのため利用できません"
 	}
 }

+ 9 - 3
webview-ui/src/i18n/locales/ko/kilocode.json

@@ -325,8 +325,14 @@
 	"speechToText": {
 		"stopRecording": "음성 입력 중지",
 		"startRecording": "음성 입력 시작",
-		"unavailableBoth": "음성-텍스트 변환을 사용할 수 없습니다. OpenAI 제공업체가 유효해야 하며 FFmpeg가 설치되어 있어야 합니다.",
-		"unavailableFfmpegNotInstalled": "음성-텍스트 변환을 사용할 수 없습니다. 음성 전사를 위해 FFmpeg를 설치하세요.",
-		"unavailableOpenAiKeyMissing": "음성-텍스트 변환을 사용할 수 없습니다. 음성 전사를 사용하려면 API 키가 있는 유효한 OpenAI 제공업체가 필요합니다."
+		"setupPopover": {
+			"title": "음성-텍스트 변환",
+			"description": "음성-텍스트 변환을 사용할 수 없습니다. 음성-텍스트 변환을 사용하려면 FFmpeg와 유효한 OpenAI 제공자가 모두 구성되어 있어야 합니다. <moreInfoLink>자세한 내용을 보려면 여기를 클릭하세요</moreInfoLink>.",
+			"ffmpegReason": "FFmpeg를 설치해야 합니다. <ffmpegLink>여기를 클릭</ffmpegLink>하여 Kilo가 FFmpeg 설치를 도와드립니다.",
+			"openAiReason": "음성 변환 기능을 사용하려면 API 키가 있는 유효한 OpenAI 제공업체가 필요합니다.",
+			"ffmpegMessage": "FFmpeg 설치를 도와주세요"
+		},
+		"misconfiguredState": "설정으로 인해 사용할 수 없음",
+		"errorState": "오류로 인해 사용할 수 없음"
 	}
 }

+ 10 - 4
webview-ui/src/i18n/locales/nl/kilocode.json

@@ -324,9 +324,15 @@
 	},
 	"speechToText": {
 		"startRecording": "Start spraakinvoer",
-		"stopRecording": "Stop spraakinvoer",
-		"unavailableOpenAiKeyMissing": "Spraak-naar-tekst is niet beschikbaar. Het vereist een geldige OpenAI provider met een API-sleutel om spraaktranscriptie te gebruiken.",
-		"unavailableBoth": "Spraak-naar-tekst is niet beschikbaar. Het vereist een geldige OpenAI-provider en FFmpeg moet geïnstalleerd zijn.",
-		"unavailableFfmpegNotInstalled": "Spraak-naar-tekst is niet beschikbaar. Installeer FFmpeg om spraaktranscriptie te gebruiken."
+		"stopRecording": "Stop spraakherkenning",
+		"setupPopover": {
+			"title": "Spraak-naar-Tekst",
+			"description": "Spraak-naar-tekst is niet beschikbaar. Spraak-naar-tekst vereist zowel FFmpeg als een geldige OpenAI-provider die is geconfigureerd. <moreInfoLink>Klik hier voor meer informatie</moreInfoLink>.",
+			"openAiReason": "Je hebt een geldige OpenAI-provider met een API-sleutel nodig om spraaktranscriptie te gebruiken.",
+			"ffmpegMessage": "Help me FFmpeg te installeren",
+			"ffmpegReason": "Je hebt FFmpeg nodig. <ffmpegLink>Klik hier</ffmpegLink> om Kilo te laten helpen met het installeren van FFmpeg."
+		},
+		"misconfiguredState": "Niet beschikbaar vanwege configuratie",
+		"errorState": "Niet beschikbaar vanwege een fout"
 	}
 }

+ 9 - 3
webview-ui/src/i18n/locales/pl/kilocode.json

@@ -325,8 +325,14 @@
 	"speechToText": {
 		"startRecording": "Rozpocznij wprowadzanie głosowe",
 		"stopRecording": "Zatrzymaj wprowadzanie głosowe",
-		"unavailableFfmpegNotInstalled": "Transkrypcja mowy na tekst jest niedostępna. Zainstaluj FFmpeg, aby korzystać z transkrypcji głosowej.",
-		"unavailableOpenAiKeyMissing": "Zamiana mowy na tekst jest niedostępna. Wymaga poprawnego dostawcy OpenAI z kluczem API, aby korzystać z transkrypcji głosowej.",
-		"unavailableBoth": "Funkcja zamiany mowy na tekst jest niedostępna. Wymaga ona prawidłowego dostawcy OpenAI oraz zainstalowanego FFmpeg."
+		"setupPopover": {
+			"title": "Zamiana mowy na tekst",
+			"description": "Zamiana mowy na tekst jest niedostępna. Zamiana mowy na tekst wymaga zarówno FFmpeg, jak i skonfigurowanego prawidłowego dostawcy OpenAI. <moreInfoLink>Kliknij tutaj, aby uzyskać więcej informacji</moreInfoLink>.",
+			"openAiReason": "Aby korzystać z transkrypcji głosowej, potrzebujesz ważnego dostawcy OpenAI z kluczem API.",
+			"ffmpegReason": "Musisz mieć zainstalowany FFmpeg. <ffmpegLink>Kliknij tutaj</ffmpegLink>, aby Kilo pomógł Ci zainstalować FFmpeg.",
+			"ffmpegMessage": "Pomóż mi zainstalować FFmpeg"
+		},
+		"misconfiguredState": "Niedostępne z powodu konfiguracji",
+		"errorState": "Niedostępne z powodu błędu"
 	}
 }

+ 9 - 3
webview-ui/src/i18n/locales/pt-BR/kilocode.json

@@ -325,8 +325,14 @@
 	"speechToText": {
 		"startRecording": "Iniciar entrada de voz",
 		"stopRecording": "Parar entrada de voz",
-		"unavailableOpenAiKeyMissing": "Conversão de voz para texto indisponível. É necessário um provedor OpenAI válido com uma chave de API para usar a transcrição de voz.",
-		"unavailableBoth": "O reconhecimento de voz não está disponível. Ele requer um provedor OpenAI válido e o FFmpeg deve estar instalado.",
-		"unavailableFfmpegNotInstalled": "Transcrição de voz não está disponível. Instale o FFmpeg para usar a transcrição de voz."
+		"setupPopover": {
+			"title": "Conversão de Fala em Texto",
+			"openAiReason": "Você precisa de um provedor OpenAI válido com uma chave de API para usar a transcrição de voz.",
+			"description": "A conversão de fala para texto está indisponível. A conversão de fala para texto requer o FFmpeg e um provedor OpenAI válido configurados. <moreInfoLink>Clique aqui para mais informações</moreInfoLink>.",
+			"ffmpegReason": "Você precisa ter o FFmpeg instalado. <ffmpegLink>Clique aqui</ffmpegLink> para que o Kilo ajude você a instalar o FFmpeg.",
+			"ffmpegMessage": "Ajude-me a instalar o FFmpeg"
+		},
+		"misconfiguredState": "Indisponível devido à configuração",
+		"errorState": "Indisponível devido a erro"
 	}
 }

+ 9 - 3
webview-ui/src/i18n/locales/ru/kilocode.json

@@ -325,8 +325,14 @@
 	"speechToText": {
 		"startRecording": "Начать голосовой ввод",
 		"stopRecording": "Остановить голосовой ввод",
-		"unavailableOpenAiKeyMissing": "Преобразование речи в текст недоступно. Для использования голосовой транскрипции требуется действительный поставщик OpenAI с API-ключом.",
-		"unavailableFfmpegNotInstalled": "Преобразование речи в текст недоступно. Установите FFmpeg для использования транскрипции голоса.",
-		"unavailableBoth": "Преобразование речи в текст недоступно. Для этого требуется действительный провайдер OpenAI, и должен быть установлен FFmpeg."
+		"setupPopover": {
+			"title": "Речь в текст",
+			"openAiReason": "Для использования голосовой транскрипции необходим действующий провайдер OpenAI с API-ключом.",
+			"description": "Преобразование речи в текст недоступно. Для преобразования речи в текст требуется установленный FFmpeg и настроенный провайдер OpenAI. <moreInfoLink>Нажмите здесь для получения дополнительной информации</moreInfoLink>.",
+			"ffmpegMessage": "Помогите мне установить FFmpeg",
+			"ffmpegReason": "Необходимо установить FFmpeg. <ffmpegLink>Нажмите здесь</ffmpegLink>, чтобы Kilo помог вам установить FFmpeg."
+		},
+		"misconfiguredState": "Недоступно из-за конфигурации",
+		"errorState": "Недоступно из-за ошибки"
 	}
 }

+ 11 - 5
webview-ui/src/i18n/locales/th/kilocode.json

@@ -323,10 +323,16 @@
 		"initiating": "กำลังเริ่มการยืนยันตัวตน..."
 	},
 	"speechToText": {
-		"startRecording": "เริ่มต้นป้อนข้อมูลด้วยเสียง",
-		"stopRecording": "หยุดการป้อนข้อมูลด้วยเสียง",
-		"unavailableOpenAiKeyMissing": "การแปลงเสียงเป็นข้อความไม่สามารถใช้งานได้ จำเป็นต้องมีผู้ให้บริการ OpenAI ที่ถูกต้องพร้อมกับ API key เพื่อใช้งานการถอดเสียง",
-		"unavailableBoth": "การแปลงเสียงเป็นข้อความไม่สามารถใช้งานได้ จำเป็นต้องมีผู้ให้บริการ OpenAI ที่ถูกต้องและต้องติดตั้ง FFmpeg",
-		"unavailableFfmpegNotInstalled": "ไม่สามารถใช้การแปลงเสียงเป็นข้อความได้ โปรดติดตั้ง FFmpeg เพื่อใช้งานการถอดเสียง"
+		"startRecording": "เริ่มการป้อนข้อมูลด้วยเสียง",
+		"stopRecording": "หยุดการป้อนเสียง",
+		"setupPopover": {
+			"title": "เสียงเป็นข้อความ",
+			"description": "การแปลงเสียงเป็นข้อความไม่พร้อมใช้งาน การแปลงเสียงเป็นข้อความต้องการทั้ง FFmpeg และการกำหนดค่า OpenAI provider ที่ถูกต้อง <moreInfoLink>คลิกที่นี่สำหรับข้อมูลเพิ่มเติม</moreInfoLink>",
+			"openAiReason": "คุณต้องมีผู้ให้บริการ OpenAI ที่ถูกต้องพร้อม API key เพื่อใช้งานการถอดเสียงเป็นข้อความ",
+			"ffmpegReason": "คุณจำเป็นต้องติดตั้ง FFmpeg <ffmpegLink>คลิกที่นี่</ffmpegLink> เพื่อให้ Kilo ช่วยคุณติดตั้ง FFmpeg",
+			"ffmpegMessage": "ช่วยฉันติดตั้ง FFmpeg"
+		},
+		"misconfiguredState": "ไม่พร้อมใช้งานเนื่องจากการตั้งค่า",
+		"errorState": "ไม่พร้อมใช้งานเนื่องจากข้อผิดพลาด"
 	}
 }

+ 9 - 3
webview-ui/src/i18n/locales/tr/kilocode.json

@@ -325,8 +325,14 @@
 	"speechToText": {
 		"startRecording": "Sesli girişi başlat",
 		"stopRecording": "Sesli girişi durdur",
-		"unavailableOpenAiKeyMissing": "Konuşmadan metne dönüştürme kullanılamıyor. Ses transkripsiyonunu kullanabilmek için API anahtarına sahip geçerli bir OpenAI sağlayıcısı gerekiyor.",
-		"unavailableFfmpegNotInstalled": "Konuşmayı metne dönüştürme kullanılamıyor. Ses transkripsiyonunu kullanmak için FFmpeg'i yükleyin.",
-		"unavailableBoth": "Konuşmadan metne dönüştürme özelliği kullanılamıyor. Geçerli bir OpenAI sağlayıcısı gerektirir ve FFmpeg yüklü olmalıdır."
+		"setupPopover": {
+			"title": "Konuşmadan Metne",
+			"openAiReason": "Ses transkripsiyonu kullanmak için API anahtarına sahip geçerli bir OpenAI sağlayıcısına ihtiyacınız var.",
+			"description": "Konuşmadan metne özelliği kullanılamıyor. Konuşmadan metne özelliği için hem FFmpeg hem de geçerli bir OpenAI sağlayıcısı yapılandırılması gerekir. <moreInfoLink>Daha fazla bilgi için buraya tıklayın</moreInfoLink>.",
+			"ffmpegMessage": "FFmpeg'i kurmama yardım et",
+			"ffmpegReason": "FFmpeg'in yüklü olması gerekiyor. FFmpeg'i yüklemeniz için Kilo'nun size yardımcı olması için <ffmpegLink>buraya tıklayın</ffmpegLink>."
+		},
+		"misconfiguredState": "Yapılandırma nedeniyle kullanılamıyor",
+		"errorState": "Hata nedeniyle kullanılamıyor"
 	}
 }

+ 9 - 3
webview-ui/src/i18n/locales/uk/kilocode.json

@@ -325,8 +325,14 @@
 	"speechToText": {
 		"startRecording": "Почати голосове введення",
 		"stopRecording": "Зупинити голосове введення",
-		"unavailableFfmpegNotInstalled": "Розпізнавання мовлення недоступне. Встановіть FFmpeg для використання голосової транскрипції.",
-		"unavailableBoth": "Перетворення мови в текст недоступне. Воно потребує дійсного провайдера OpenAI, а також має бути встановлено FFmpeg.",
-		"unavailableOpenAiKeyMissing": "Розпізнавання мовлення недоступне. Для використання транскрипції голосу потрібен дійсний провайдер OpenAI з API-ключем."
+		"setupPopover": {
+			"title": "Розпізнавання мовлення",
+			"openAiReason": "Для використання голосової транскрипції потрібен дійсний провайдер OpenAI з API-ключем.",
+			"description": "Мовлення-в-текст недоступне. Для роботи мовлення-в-текст потрібні як FFmpeg, так і налаштований дійсний постачальник OpenAI. <moreInfoLink>Натисніть тут для отримання додаткової інформації</moreInfoLink>.",
+			"ffmpegReason": "Вам потрібно встановити FFmpeg. <ffmpegLink>Натисніть тут</ffmpegLink>, щоб Kilo допоміг вам встановити FFmpeg.",
+			"ffmpegMessage": "Допоможіть мені встановити FFmpeg"
+		},
+		"misconfiguredState": "Недоступно через конфігурацію",
+		"errorState": "Недоступно через помилку"
 	}
 }

+ 10 - 4
webview-ui/src/i18n/locales/vi/kilocode.json

@@ -324,9 +324,15 @@
 	},
 	"speechToText": {
 		"stopRecording": "Dừng nhập giọng nói",
-		"startRecording": "Bắt đầu nhập giọng nói",
-		"unavailableBoth": "Chuyển đổi giọng nói thành văn bản không khả dụng. Nó yêu cầu nhà cung cấp OpenAI hợp lệ và FFmpeg phải được cài đặt.",
-		"unavailableFfmpegNotInstalled": "Chuyển giọng nói thành văn bản hiện không khả dụng. Hãy cài đặt FFmpeg để sử dụng tính năng chuyển giọng nói.",
-		"unavailableOpenAiKeyMissing": "Chuyển giọng nói thành văn bản không khả dụng. Tính năng này yêu cầu một nhà cung cấp OpenAI hợp lệ với khóa API để sử dụng chức năng chuyển giọng nói thành văn bản."
+		"startRecording": "Bắt đầu nhập liệu bằng giọng nói",
+		"setupPopover": {
+			"title": "Chuyển giọng nói thành văn bản",
+			"description": "Chuyển giọng nói thành văn bản không khả dụng. Chuyển giọng nói thành văn bản yêu cầu cả FFmpeg và nhà cung cấp OpenAI hợp lệ được cấu hình. <moreInfoLink>Nhấp vào đây để biết thêm thông tin</moreInfoLink>.",
+			"openAiReason": "Bạn cần một nhà cung cấp OpenAI hợp lệ với API key để sử dụng chức năng phiên âm giọng nói.",
+			"ffmpegMessage": "Giúp tôi cài đặt FFmpeg",
+			"ffmpegReason": "Bạn cần cài đặt FFmpeg. <ffmpegLink>Nhấp vào đây</ffmpegLink> để Kilo giúp bạn cài đặt FFmpeg."
+		},
+		"misconfiguredState": "Không khả dụng do cấu hình",
+		"errorState": "Không khả dụng do lỗi"
 	}
 }

+ 9 - 3
webview-ui/src/i18n/locales/zh-CN/kilocode.json

@@ -325,8 +325,14 @@
 	"speechToText": {
 		"startRecording": "开始语音输入",
 		"stopRecording": "停止语音输入",
-		"unavailableOpenAiKeyMissing": "语音转文字功能不可用。使用语音转录需要有效的OpenAI提供商及其API密钥。",
-		"unavailableFfmpegNotInstalled": "语音转文字功能不可用。请安装FFmpeg以使用语音转录功能。",
-		"unavailableBoth": "语音转文字功能不可用。它需要有效的OpenAI提供商,且必须安装FFmpeg。"
+		"setupPopover": {
+			"title": "语音转文字",
+			"openAiReason": "您需要一个有效的 OpenAI 提供商和 API 密钥才能使用语音转录功能。",
+			"description": "语音转文字功能不可用。语音转文字需要同时配置 FFmpeg 和有效的 OpenAI 提供商。<moreInfoLink>点击此处了解更多信息</moreInfoLink>。",
+			"ffmpegReason": "您需要安装 FFmpeg。<ffmpegLink>点击这里</ffmpegLink>让 Kilo 帮助您安装 FFmpeg。",
+			"ffmpegMessage": "帮我安装 FFmpeg"
+		},
+		"misconfiguredState": "因配置问题而不可用",
+		"errorState": "因错误而不可用"
 	}
 }

+ 9 - 3
webview-ui/src/i18n/locales/zh-TW/kilocode.json

@@ -325,8 +325,14 @@
 	"speechToText": {
 		"startRecording": "开始语音输入",
 		"stopRecording": "停止语音输入",
-		"unavailableFfmpegNotInstalled": "语音转文本功能不可用。请安装FFmpeg以使用语音转录功能。",
-		"unavailableOpenAiKeyMissing": "语音转文字功能不可用。使用语音转录功能需要有效的OpenAI提供商及API密钥。",
-		"unavailableBoth": "语音转文字功能不可用。它需要有效的OpenAI提供商,并且必须安装FFmpeg。"
+		"setupPopover": {
+			"title": "语音转文字",
+			"openAiReason": "您需要一个有效的 OpenAI 提供商和 API 密钥才能使用语音转录功能。",
+			"description": "语音转文字功能不可用。语音转文字需要同时配置 FFmpeg 和有效的 OpenAI 提供商。<moreInfoLink>点击此处了解更多信息</moreInfoLink>。",
+			"ffmpegReason": "您需要安装 FFmpeg。<ffmpegLink>点击这里</ffmpegLink>让 Kilo 帮助您安装 FFmpeg。",
+			"ffmpegMessage": "帮我安装 FFmpeg"
+		},
+		"misconfiguredState": "因配置问题而不可用",
+		"errorState": "因错误而不可用"
 	}
 }