diff --git a/apps/x/apps/renderer/src/components/apps/app-frame.tsx b/apps/x/apps/renderer/src/components/apps/app-frame.tsx index 3492e67d..c406b903 100644 --- a/apps/x/apps/renderer/src/components/apps/app-frame.tsx +++ b/apps/x/apps/renderer/src/components/apps/app-frame.tsx @@ -11,12 +11,29 @@ import { AppDetail } from '@/components/apps/app-detail' export function AppFrame({ app, onBack }: { app: rowboatApp.AppSummary; onBack: () => void }) { const [reloadNonce, setReloadNonce] = useState(0) const [showDetail, setShowDetail] = useState(false) + // Load watchdog: if the iframe hasn't fired `load` within the deadline, + // surface a visible retry state instead of a silent blank pane. + const [loadState, setLoadState] = useState<'loading' | 'ok' | 'stuck'>('loading') const title = app.manifest?.name ?? app.folder useEffect(() => { appOpened(app.folder) }, [app.folder]) + // Reset the watchdog when the target changes (adjust-during-render pattern). + const [watchKey, setWatchKey] = useState(`${app.folder}:${reloadNonce}`) + if (watchKey !== `${app.folder}:${reloadNonce}`) { + setWatchKey(`${app.folder}:${reloadNonce}`) + setLoadState('loading') + } + + useEffect(() => { + const timer = window.setTimeout(() => { + setLoadState((s) => (s === 'loading' ? 'stuck' : s)) + }, 6000) + return () => window.clearTimeout(timer) + }, [watchKey]) + return (