// node/vscode_extension/webview-ui/src/App.tsx import { useEffect, useState, useCallback } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { Header } from "./components/Header"; import { ChatArea } from "./components/ChatArea"; import { InputArea } from "./components/inputarea/InputArea"; import { MCPServersModal } from "./components/MCPServersModal"; import { WorkDirModal } from "./components/WorkDirModal"; import { ConfigErrorScreen } from "./components/ConfigErrorScreen"; import { LoginScreen } from "./components/LoginScreen"; import { Toaster, toast } from "./components/ui/sonner"; import { useChatStore, useSettingsStore } from "./stores"; import { bridge, Events } from "./services"; import { useAppInit, resolveAppView } from "./hooks/useAppInit"; import { isPreflightError } from "shared/errors"; import type { UIStreamEvent, StreamError, ExtensionConfig } from "shared/types"; import "./styles/index.css"; function MainContent({ onAuthAction }: { onAuthAction: () => void }) { const { processEvent, startNewConversation, sessionId } = useChatStore(); const { setExtensionConfig, extensionConfig } = useSettingsStore(); const queryClient = useQueryClient(); useEffect(() => { return bridge.on(Events.StreamEvent, (event: UIStreamEvent) => { // 只有当前已有 session 时才过滤,确保 session_start 能正常处理 if (sessionId && "_sessionId" in event && event._sessionId && event._sessionId !== sessionId) { console.log("Ignored stream event from another session:", event._sessionId); return; } processEvent(event); if (event.type === "error") { const streamError = event as StreamError; if (isPreflightError(streamError.code || "UNKNOWN")) { toast.error(streamError.message); } } }); }, [processEvent, sessionId]); useEffect(() => { const unsubs = [ bridge.on(Events.MCPServersChanged, () => void queryClient.invalidateQueries({ queryKey: ["mcpServers"] })), bridge.on(Events.ExtensionConfigChanged, ({ config }: { config: ExtensionConfig }) => setExtensionConfig(config)), bridge.on(Events.FocusInput, () => document.querySelector("textarea")?.focus()), bridge.on(Events.NewConversation, () => { void startNewConversation().catch((error: unknown) => { toast.error(error instanceof Error ? error.message : String(error)); }); }), ]; return () => unsubs.forEach((u) => u()); }, [queryClient, setExtensionConfig, startNewConversation]); useEffect(() => { if (!extensionConfig.enableNewConversationShortcut) return; const handler = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "n") { e.preventDefault(); void startNewConversation().catch((error: unknown) => { toast.error(error instanceof Error ? error.message : String(error)); }); } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [extensionConfig.enableNewConversationShortcut, startNewConversation]); return ( <>
); } export default function App() { const { status, errorMessage, modelsCount, refresh } = useAppInit(); const [skippedLogin, setSkippedLogin] = useState(false); const [showLogin, setShowLogin] = useState(false); const handleLoginSuccess = useCallback(() => { setShowLogin(false); setSkippedLogin(false); refresh(); }, [refresh]); const handleSkip = useCallback(() => { setShowLogin(false); setSkippedLogin(true); }, []); const handleShowLogin = useCallback(() => { setSkippedLogin(false); setShowLogin(true); }, []); const handleAuthAction = useCallback(() => { setSkippedLogin(false); setShowLogin(false); refresh(); }, [refresh]); const resolution = resolveAppView({ status, modelsCount, skippedLogin, showLogin }); // 登录界面:未登录且未跳过,或用户从其他界面主动选择登录 if (resolution.view === "login") { return (
); } // 错误与设置状态界面;no-models 必须保留回到登录界面的入口 if (resolution.view === "status") { return (
); } // 正常状态 return (
); }