+ {/* key on the chip itself remounts the typewriter for each new bubble */}
+
))}
@@ -187,10 +405,8 @@ export default function OverlayApp() {
diff --git a/app/src/pages/Home.tsx b/app/src/pages/Home.tsx
index 45f9f4f0a..c3a8e5938 100644
--- a/app/src/pages/Home.tsx
+++ b/app/src/pages/Home.tsx
@@ -9,13 +9,20 @@ import {
triggerLocalAiAssetBootstrap,
} from '../utils/localAiBootstrap';
import { formatBytes, formatEta, progressFromStatus } from '../utils/localAiHelpers';
-import { isTauri, type LocalAiStatus, openhumanLocalAiStatus } from '../utils/tauriCommands';
+import {
+ isTauri,
+ type LocalAiAssetsStatus,
+ type LocalAiStatus,
+ openhumanLocalAiAssetsStatus,
+ openhumanLocalAiStatus,
+} from '../utils/tauriCommands';
const Home = () => {
const { user } = useUser();
const navigate = useNavigate();
const userName = user?.firstName || 'User';
const [localAiStatus, setLocalAiStatus] = useState(null);
+ const [localAiAssets, setLocalAiAssets] = useState(null);
const [downloadBusy, setDownloadBusy] = useState(false);
const [bootstrapMessage, setBootstrapMessage] = useState('');
const autoRetryDoneRef = useRef(false);
@@ -73,9 +80,16 @@ const Home = () => {
let mounted = true;
const load = async () => {
try {
- const status = await openhumanLocalAiStatus();
+ const [status, assets] = await Promise.all([
+ openhumanLocalAiStatus(),
+ openhumanLocalAiAssetsStatus().catch(err => {
+ console.warn('[Home] failed to load local AI assets status:', err);
+ return null;
+ }),
+ ]);
if (mounted) {
setLocalAiStatus(status.result);
+ setLocalAiAssets(assets?.result ?? null);
// Auto-retry bootstrap once if Ollama is degraded (install/server issue).
if (status.result?.state === 'degraded' && !autoRetryDoneRef.current) {
@@ -155,6 +169,29 @@ const Home = () => {
}, []);
const modelProgress = useMemo(() => progressFromStatus(localAiStatus), [localAiStatus]);
+ // Hide the Local Model Runtime card once every capability's model file is
+ // present on disk. We use `assets_status` (which inspects the filesystem)
+ // instead of the in-memory `LocalAiStatus` sub-states, because the latter
+ // stay at `idle` until a capability is first exercised — even when the
+ // underlying model has already been downloaded.
+ //
+ // A capability is considered "done" when its asset state is:
+ // - `ready` → model file exists on disk
+ // - `disabled` → not applicable for the selected preset
+ // - `ondemand` → vision preset intentionally defers download until first use
+ const allModelsDownloaded = useMemo(() => {
+ if (!localAiStatus || !localAiAssets) return false;
+ if (localAiStatus.state !== 'ready') return false;
+ const isDone = (state: string | undefined | null): boolean =>
+ state === 'ready' || state === 'disabled' || state === 'ondemand';
+ return (
+ isDone(localAiAssets.chat?.state) &&
+ isDone(localAiAssets.vision?.state) &&
+ isDone(localAiAssets.embedding?.state) &&
+ isDone(localAiAssets.stt?.state) &&
+ isDone(localAiAssets.tts?.state)
+ );
+ }, [localAiStatus, localAiAssets]);
const isInstalling = localAiStatus?.state === 'installing';
const indeterminateDownload =
isInstalling ||
@@ -233,8 +270,8 @@ const Home = () => {
- {/* Local AI card (desktop only) */}
- {isTauri() && (
+ {/* Local AI card (desktop only) — hidden once all models are fully downloaded */}
+ {isTauri() && !allModelsDownloaded && (