import { useState, useEffect, useCallback } from "react"; import { bridge, Events } from "@/services"; import { requiresManagedProviderLogin, useSettingsStore } from "@/stores"; import type { ExtensionConfig } from "shared/types"; export type AppStatus = "loading" | "no-workspace" | "runtime-error" | "not-logged-in" | "no-models" | "ready"; export type ConfigErrorStatus = "loading" | "no-workspace" | "runtime-error" | "no-models"; export type AppViewResolution = | { readonly view: "login" } | { readonly view: "status"; readonly status: ConfigErrorStatus; /** True when the status screen must offer a path to the sign-in screen. */ readonly canGoToLogin: boolean; } | { readonly view: "main" }; /** * Pure view router for App. The `no-models` status (a managed OAuth token * exists but config.toml has no models — e.g. a first login whose model * provisioning failed after the device flow already persisted the token) * must always keep a path back to the sign-in screen: Reload alone cannot * change the on-disk state, so without it the user is stranded and the * login UI becomes unreachable. */ export function resolveAppView(input: { readonly status: AppStatus; readonly modelsCount: number; readonly skippedLogin: boolean; readonly showLogin: boolean; }): AppViewResolution { const { status, modelsCount, skippedLogin, showLogin } = input; if (showLogin || (status === "not-logged-in" && !skippedLogin)) { return { view: "login" }; } if (skippedLogin && modelsCount === 0) { return { view: "status", status: "no-models", canGoToLogin: true }; } if (status !== "ready" && status !== "not-logged-in") { return { view: "status", status, canGoToLogin: status === "no-models" }; } return { view: "main" }; } export interface AppInitState { status: AppStatus; errorMessage: string | null; modelsCount: number; refresh: () => void; } export function useAppInit(): AppInitState { const [state, setState] = useState>({ status: "loading", errorMessage: null, modelsCount: 0, }); const [initKey, setInitKey] = useState(0); const { initModels, setExtensionConfig, setWireSlashCommands, setIsLoggedIn, setWorkspaceRoot } = useSettingsStore(); const refresh = useCallback(() => { setState({ status: "loading", errorMessage: null, modelsCount: 0 }); setInitKey((k) => k + 1); }, []); useEffect(() => { return bridge.on<{ config: ExtensionConfig; changedKeys: string[] }>(Events.ExtensionConfigChanged, ({ config }) => { setExtensionConfig(config); }); }, [setExtensionConfig, refresh]); useEffect(() => { let cancelled = false; async function init() { try { const workspace = await bridge.checkWorkspace(); if (cancelled) { return; } if (!workspace.hasWorkspace) { setState({ status: "no-workspace", errorMessage: null, modelsCount: 0 }); return; } setWorkspaceRoot(workspace.workspaceRoot ?? workspace.path ?? null); const [extensionConfig, slashCommands] = await Promise.all([ bridge.getExtensionConfig(), bridge.getSlashCommands(), ]); if (cancelled) { return; } setExtensionConfig(extensionConfig); setWireSlashCommands(slashCommands); const [loginStatus, kimiConfig] = await Promise.all([bridge.checkLoginStatus(), bridge.getModels()]); if (cancelled) { return; } console.log("[AppInit] Login status:", loginStatus, "kimiConfig:", kimiConfig); setIsLoggedIn(loginStatus.loggedIn); initModels(kimiConfig.models, kimiConfig.defaultModel, kimiConfig.defaultThinking, kimiConfig.defaultThinkingEffort); const modelsCount = kimiConfig.models?.length ?? 0; if (modelsCount === 0 && !loginStatus.loggedIn) { setState({ status: "not-logged-in", errorMessage: null, modelsCount }); return; } if (modelsCount === 0) { setState({ status: "no-models", errorMessage: null, modelsCount: 0 }); return; } if (requiresManagedProviderLogin(kimiConfig.models, kimiConfig.defaultModel, loginStatus.loggedIn)) { setState({ status: "not-logged-in", errorMessage: null, modelsCount }); return; } setState({ status: "ready", errorMessage: null, modelsCount }); } catch (err) { if (!cancelled) { setState({ status: "runtime-error", errorMessage: err instanceof Error ? err.message : "Failed to initialize", modelsCount: 0, }); } } } void init(); return () => { cancelled = true; }; }, [initKey, initModels, setExtensionConfig, setWireSlashCommands, setIsLoggedIn]); return { ...state, refresh }; }