diff --git a/crates/goose-sdk-types/src/custom_requests.rs b/crates/goose-sdk-types/src/custom_requests.rs index 7243fd1f8b..b1c8626912 100644 --- a/crates/goose-sdk-types/src/custom_requests.rs +++ b/crates/goose-sdk-types/src/custom_requests.rs @@ -2145,6 +2145,7 @@ pub struct DictationLocalModelStatus { pub size_mb: u32, pub downloaded: bool, pub download_in_progress: bool, + pub recommended: bool, } /// Kick off a background download of a local Whisper model. diff --git a/crates/goose/acp-schema.json b/crates/goose/acp-schema.json index 673c6dd187..f4507f5cf1 100644 --- a/crates/goose/acp-schema.json +++ b/crates/goose/acp-schema.json @@ -5548,6 +5548,9 @@ }, "downloadInProgress": { "type": "boolean" + }, + "recommended": { + "type": "boolean" } }, "required": [ @@ -5556,7 +5559,8 @@ "description", "sizeMb", "downloaded", - "downloadInProgress" + "downloadInProgress", + "recommended" ] }, "DictationModelDownloadRequest_unstable": { diff --git a/crates/goose/src/acp/server/dictation.rs b/crates/goose/src/acp/server/dictation.rs index ccafdc4e85..9444423d5a 100644 --- a/crates/goose/src/acp/server/dictation.rs +++ b/crates/goose/src/acp/server/dictation.rs @@ -160,6 +160,7 @@ impl GooseAcpAgent { use crate::download_manager::{get_download_manager, DownloadStatus}; let manager = get_download_manager(); + let recommended_id = whisper::recommend_model(); let models = whisper::available_models() .iter() .map(|model| DictationLocalModelStatus { @@ -172,6 +173,7 @@ impl GooseAcpAgent { .get_progress(model.id) .map(|progress| progress.status == DownloadStatus::Downloading) .unwrap_or(false), + recommended: model.id == recommended_id, }) .collect(); diff --git a/ui/desktop/src/acp/dictation.ts b/ui/desktop/src/acp/dictation.ts index 4901ea30ee..aebe23d754 100644 --- a/ui/desktop/src/acp/dictation.ts +++ b/ui/desktop/src/acp/dictation.ts @@ -1,9 +1,15 @@ -import type { DictationProviderStatusEntry } from '@aaif/goose-sdk'; +import type { + DictationDownloadProgress, + DictationLocalModelStatus, + DictationProviderStatusEntry, +} from '@aaif/goose-sdk'; import { getAcpClient } from './acpConnection'; export type { DictationProviderStatusEntry }; export type DictationProviders = Record; +export type LocalDictationModel = DictationLocalModelStatus; +export type LocalDictationDownloadProgress = DictationDownloadProgress; export async function getDictationConfig(): Promise { const client = await getAcpClient(); @@ -20,3 +26,32 @@ export async function transcribeDictation( const response = await client.goose.dictationTranscribe_unstable({ audio, mimeType, provider }); return response.text; } + +export async function listLocalDictationModels(): Promise { + const client = await getAcpClient(); + const response = await client.goose.dictationModelsList_unstable({}); + return response.models; +} + +export async function downloadLocalDictationModel(modelId: string): Promise { + const client = await getAcpClient(); + await client.goose.dictationModelsDownload_unstable({ modelId }); +} + +export async function getLocalDictationModelDownloadProgress( + modelId: string +): Promise { + const client = await getAcpClient(); + const response = await client.goose.dictationModelsDownloadProgress_unstable({ modelId }); + return response.progress ?? null; +} + +export async function cancelLocalDictationModelDownload(modelId: string): Promise { + const client = await getAcpClient(); + await client.goose.dictationModelsCancel_unstable({ modelId }); +} + +export async function deleteLocalDictationModel(modelId: string): Promise { + const client = await getAcpClient(); + await client.goose.dictationModelsDelete_unstable({ modelId }); +} diff --git a/ui/desktop/src/components/settings/dictation/LocalModelManager.tsx b/ui/desktop/src/components/settings/dictation/LocalModelManager.tsx index 2169ef7cdd..1be2b74d57 100644 --- a/ui/desktop/src/components/settings/dictation/LocalModelManager.tsx +++ b/ui/desktop/src/components/settings/dictation/LocalModelManager.tsx @@ -3,14 +3,14 @@ import { Download, Trash2, X, Check, ChevronDown, ChevronUp } from 'lucide-react import { Button } from '../../ui/button'; import { useConfig } from '../../ConfigContext'; import { - listModels, - downloadModel, - getDownloadProgress, - cancelDownload as cancelDownloadApi, - deleteModel as deleteModelApi, - type WhisperModelResponse, - type DownloadProgress, -} from '../../../api'; + cancelLocalDictationModelDownload, + deleteLocalDictationModel, + downloadLocalDictationModel, + getLocalDictationModelDownloadProgress, + listLocalDictationModels, + type LocalDictationDownloadProgress, + type LocalDictationModel, +} from '../../../acp/dictation'; import { defineMessages, useIntl } from '../../../i18n'; const i18n = defineMessages({ @@ -72,8 +72,10 @@ const capitalize = (str: string): string => { export const LocalModelManager = () => { const intl = useIntl(); - const [models, setModels] = useState([]); - const [downloads, setDownloads] = useState>(new Map()); + const [models, setModels] = useState([]); + const [downloads, setDownloads] = useState>( + new Map() + ); const [selectedModelId, setSelectedModelId] = useState(null); const [showAllModels, setShowAllModels] = useState(false); const { read, upsert } = useConfig(); @@ -105,10 +107,8 @@ export const LocalModelManager = () => { const loadModels = async () => { try { - const response = await listModels(); - if (response.data) { - setModels(response.data); - } + const models = await listLocalDictationModels(); + setModels(models); } catch (error) { console.error('Failed to load models:', error); } @@ -116,7 +116,7 @@ export const LocalModelManager = () => { const startDownload = async (modelId: string) => { try { - await downloadModel({ path: { model_id: modelId } }); + await downloadLocalDictationModel(modelId); pollDownloadProgress(modelId); } catch (error) { console.error('Failed to start download:', error); @@ -126,9 +126,8 @@ export const LocalModelManager = () => { const pollDownloadProgress = (modelId: string) => { const interval = setInterval(async () => { try { - const response = await getDownloadProgress({ path: { model_id: modelId } }); - if (response.data) { - const progress = response.data; + const progress = await getLocalDictationModelDownloadProgress(modelId); + if (progress) { setDownloads((prev) => new Map(prev).set(modelId, progress)); if (progress.status === 'completed') { @@ -151,7 +150,7 @@ export const LocalModelManager = () => { const cancelDownload = async (modelId: string) => { try { - await cancelDownloadApi({ path: { model_id: modelId } }); + await cancelLocalDictationModelDownload(modelId); setDownloads((prev) => { const next = new Map(prev); next.delete(modelId); @@ -167,7 +166,7 @@ export const LocalModelManager = () => { if (!window.confirm(intl.formatMessage(i18n.deleteConfirm))) return; try { - await deleteModelApi({ path: { model_id: modelId } }); + await deleteLocalDictationModel(modelId); if (selectedModelId === modelId) { await upsert(LOCAL_WHISPER_MODEL_CONFIG_KEY, '', false); setSelectedModelId(null); @@ -224,7 +223,7 @@ export const LocalModelManager = () => {

{capitalize(model.id)}

- {model.size_mb}MB + {model.sizeMb}MB {model.recommended && ( {intl.formatMessage(i18n.recommended)} @@ -264,7 +263,7 @@ export const LocalModelManager = () => { ) : isDownloading ? ( <>
- {progress.progress_percent.toFixed(0)}% + {progress.progressPercent.toFixed(0)}%