Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clean-dogs-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Add a tooltip explaining why speech-to-text may be unavailable
9 changes: 6 additions & 3 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2213,8 +2213,11 @@ ${prompt}
// kilocode_change end

// kilocode_change start - checkSpeechToTextAvailable (only when experiment enabled)
let speechToTextAvailable =
experiments?.speechToText && (await checkSpeechToTextAvailable(this.providerSettingsManager))
let speechToTextStatus: { available: boolean; reason?: "openaiKeyMissing" | "ffmpegNotInstalled" } | undefined =
undefined
if (experiments?.speechToText) {
speechToTextStatus = await checkSpeechToTextAvailable(this.providerSettingsManager)
}
// kilocode_change end - checkSpeechToTextAvailable

let cloudOrganizations: CloudOrganizationMembership[] = []
Expand Down Expand Up @@ -2440,7 +2443,7 @@ ${prompt}
featureRoomoteControlEnabled,
virtualQuotaActiveModel, // kilocode_change: Include virtual quota active model in state
debug: vscode.workspace.getConfiguration(Package.name).get<boolean>("debug", false),
speechToTextAvailable, // kilocode_change: Whether speech-to-text is fully configured
speechToTextStatus, // kilocode_change: Speech-to-text availability status with failure reason
}
}

Expand Down
29 changes: 19 additions & 10 deletions src/core/webview/speechToTextCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@ import type { ProviderSettingsManager } from "../config/ProviderSettingsManager"
import { getOpenAiApiKey } from "../../services/stt/utils/getOpenAiCredentials"
import { FFmpegCaptureService } from "../../services/stt/FFmpegCaptureService"

/**
* Result type for speech-to-text availability check
*/
export type SpeechToTextAvailabilityResult = {
available: boolean
reason?: "openaiKeyMissing" | "ffmpegNotInstalled"
}

/**
* Cached availability result with timestamp
*/
let cachedResult: { available: boolean; timestamp: number } | null = null
let cachedResult: { available: boolean; reason?: "openaiKeyMissing" | "ffmpegNotInstalled"; timestamp: number } | null =
null
const CACHE_DURATION_MS = 30000 // 30 seconds

/**
Expand All @@ -21,39 +30,39 @@ const CACHE_DURATION_MS = 30000 // 30 seconds
*
* @param providerSettingsManager - Provider settings manager for API configuration
* @param forceRecheck - Force a fresh check, ignoring cache (default: false)
* @returns Promise<boolean> - true if prerequisites are met
* @returns Promise<SpeechToTextAvailabilityResult> - Result with availability status and failure reason if unavailable
*/
export async function checkSpeechToTextAvailable(
providerSettingsManager: ProviderSettingsManager,
forceRecheck = false,
): Promise<boolean> {
): 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 cachedResult.available
return { available: cachedResult.available, reason: cachedResult.reason }
}
}

try {
// Check 1: OpenAI API key
const apiKey = await getOpenAiApiKey(providerSettingsManager)
if (!apiKey) {
cachedResult = { available: false, timestamp: Date.now() }
return false
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, timestamp: Date.now() }
return false
cachedResult = { available: false, reason: "ffmpegNotInstalled", timestamp: Date.now() }
return { available: false, reason: "ffmpegNotInstalled" }
}

cachedResult = { available: true, timestamp: Date.now() }
return true
return { available: true }
} catch (error) {
cachedResult = { available: false, timestamp: Date.now() }
return false
return { available: false }
}
}
2 changes: 1 addition & 1 deletion src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ export type ExtensionState = Pick<
virtualQuotaActiveModel?: { id: string; info: ModelInfo } // kilocode_change: Add virtual quota active model for UI display
showTimestamps?: boolean // kilocode_change: Show timestamps in chat messages
debug?: boolean
speechToTextAvailable?: boolean // kilocode_change: Whether speech-to-text is fully configured (FFmpeg + OpenAI key)
speechToTextStatus?: { available: boolean; reason?: "openaiKeyMissing" | "ffmpegNotInstalled" } // kilocode_change: Speech-to-text availability status with failure reason
}

export interface ClineSayTool {
Expand Down
12 changes: 8 additions & 4 deletions webview-ui/src/components/chat/ChatTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
ghostServiceSettings, // kilocode_change
language, // User's VSCode display language
experiments, // kilocode_change: For speechToText experiment flag
speechToTextAvailable, // kilocode_change: Whether voice transcription is configured
speechToTextStatus, // kilocode_change: Speech-to-text availability status with failure reason
} = useExtensionState()

// kilocode_change start - autocomplete profile type system
Expand Down Expand Up @@ -1712,10 +1712,14 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
<MicrophoneButton
isRecording={isRecording}
onClick={handleMicrophoneClick}
disabled={!speechToTextAvailable}
disabled={!speechToTextStatus?.available}
tooltipContent={
!speechToTextAvailable
? "Configure an OpenAI API key and install FFmpeg to use voice transcription"
!speechToTextStatus?.available && speechToTextStatus
? speechToTextStatus.reason === "openaiKeyMissing"
? t("kilocode:speechToText.unavailableOpenAiKeyMissing")
: speechToTextStatus.reason === "ffmpegNotInstalled"
? t("kilocode:speechToText.unavailableFfmpegNotInstalled")
: t("kilocode:speechToText.unavailableBoth")
: undefined
}
/>
Expand Down
7 changes: 4 additions & 3 deletions webview-ui/src/components/chat/MicrophoneButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ export const MicrophoneButton: React.FC<MicrophoneButtonProps> = ({
"rounded-md min-w-[28px] min-h-[28px]",
"transition-all duration-150",
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
"cursor-pointer",
isRecording
? "opacity-100 text-red-500 animate-pulse hover:text-red-600"
: "opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] active:bg-[rgba(255,255,255,0.1)]",
? "opacity-100 text-red-500 animate-pulse hover:text-red-600 cursor-pointer"
: disabled
? "opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent text-vscode-descriptionForeground"
: "opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] active:bg-[rgba(255,255,255,0.1)] cursor-pointer",
containerWidth !== undefined && { hidden: containerWidth < 235 },
)}>
{isRecording ? <Square className="w-4 h-4 fill-current" /> : <Mic className="w-4 h-4" />}
Expand Down
2 changes: 1 addition & 1 deletion webview-ui/src/context/ExtensionStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setCustomCondensingPrompt: (value: string) => void
yoloGatekeeperApiConfigId?: string // kilocode_change: AI gatekeeper for YOLO mode
setYoloGatekeeperApiConfigId: (value: string) => void // kilocode_change: AI gatekeeper for YOLO mode
speechToTextAvailable?: boolean // kilocode_change: Whether voice transcription is fully configured
speechToTextStatus?: { available: boolean; reason?: "openaiKeyMissing" | "ffmpegNotInstalled" } // kilocode_change: Speech-to-text availability status with failure reason
marketplaceItems?: any[]
marketplaceInstalledMetadata?: MarketplaceInstalledMetadata
profileThresholds: Record<string, number>
Expand Down
7 changes: 5 additions & 2 deletions webview-ui/src/i18n/locales/ar/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion webview-ui/src/i18n/locales/ca/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion webview-ui/src/i18n/locales/cs/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion webview-ui/src/i18n/locales/de/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion webview-ui/src/i18n/locales/en/kilocode.json
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,9 @@
},
"speechToText": {
"startRecording": "Start voice input",
"stopRecording": "Stop 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."
}
}
5 changes: 4 additions & 1 deletion webview-ui/src/i18n/locales/es/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion webview-ui/src/i18n/locales/fr/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions webview-ui/src/i18n/locales/hi/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions webview-ui/src/i18n/locales/id/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions webview-ui/src/i18n/locales/it/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion webview-ui/src/i18n/locales/ja/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion webview-ui/src/i18n/locales/ko/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion webview-ui/src/i18n/locales/nl/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion webview-ui/src/i18n/locales/pl/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion webview-ui/src/i18n/locales/pt-BR/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion webview-ui/src/i18n/locales/ru/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading