import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import brandIcon from "./assets/brand-icon.png";
import type { ComponentProps, KeyboardEvent as ReactKeyboardEvent } from "react";
import type {
  AppConfig,
  ChatAttachment,
  ChatLaunchState,
  ChatMessage,
  ConfigSecretId,
  ExpertEntryMode,
  GatewayHealth,
  GatewayStatus,
  LogEntry,
  RuntimeCloudStatus,
  RuntimeStatus,
  RuntimeTelemetryStatus,
  SaveConfigInput,
  SessionSummary,
  SystemSummary,
  WorkspaceSummary
} from "@qjclaw/shared-types";
import { DEFAULT_SKILL, expertsPageCopy, homeChatCopy, ui } from "./features/app/appCopy";
import { canExchangeMessages, useAppBootstrap } from "./features/app/useAppBootstrap";
import {
  ArrowUpIcon,
  AttachmentIcon,
  CheckIcon,
  CopyIcon,
  DouyinNoteIcon,
  LobsterClawIcon,
  NavIcon,
  RedBookIcon,
  RefreshIcon,
  StopIcon,
  ThumbIcon,
  TrashIcon,
  getIntentSuggestionIcon,
  renderExpertIcon
} from "./components/icons/AppIcons";
import { ConversationWorkspaceView } from "./features/chat/ConversationWorkspaceView";
import { renderChatMessageContent as renderMarkdownContent } from "./features/chat/renderChatMessageContent";
import { useChatSessionsController } from "./features/chat/useChatSessionsController";
import { useChatStreamingController } from "./features/chat/useChatStreamingController";
import { useComposerAttachments } from "./features/chat/useComposerAttachments";
import { useComposerResize } from "./features/chat/useComposerResize";
import { useHomeIntentSuggestion } from "./features/chat/useHomeIntentSuggestion";
import { useMessageListAutoScroll } from "./features/chat/useMessageListAutoScroll";
import { useMessageTraces } from "./features/chat/useMessageTraces";
import { usePromptSubmission } from "./features/chat/usePromptSubmission";
import { useSessionMessageStore } from "./features/chat/useSessionMessageStore";
import { KnowledgeView } from "./features/knowledge/KnowledgeView";
import { TaskPanelView } from "./features/tasks/TaskPanelView";
import { AutomationTasksView } from "./features/automation/AutomationTasksView";
import { getPluginCopy, getPluginStatusLabel, getPluginTone, groupPluginsByStatus } from "./features/plugins/pluginDisplay";
import { PluginsView } from "./features/plugins/PluginsView";
import { AppSidebar } from "./features/shell/AppSidebar";
import { ExpertCategoryIcon, type SidebarExpertEntry } from "./features/shell/ExpertTree";
import {
  buildShortcutPrompt,
  resolveExpertDisplayName,
  resolveExpertProject
} from "./features/shell/expertEntries";
import { deriveSidebarSessionTitle, formatSessionTitle } from "./features/shell/sessionTitles";
import { useHomeNavigation } from "./features/shell/useHomeNavigation";
import { TopBar } from "./features/shell/TopBar";
import { useSidebarModel } from "./features/shell/useSidebarModel";
import {
  getNextStartupVisualProgress,
  getStartupCurtainFootnote,
  getStartupCurtainStatus,
  getStartupProgress,
  getWorkspaceStatusLabel as getShellWorkspaceStatusLabel,
  getWorkspaceStatusTone as getShellWorkspaceStatusTone,
  startupCurtainCopy
} from "./features/shell/startupStatus";
import { SettingsPanels } from "./features/settings/SettingsPanels";
import { SettingsView } from "./features/settings/SettingsView";
import {
  getHasPendingSettingsChange,
  getResetCopywritingSettingsDrafts,
  getResetDigitalHumanSettingsDrafts,
  getResetDouyinRuntimeSettingsDrafts,
  getResetImageSettingsDrafts,
  getResetVideoSettingsDrafts,
  getResetXhsFeishuSettingsDrafts
} from "./features/settings/settingsDrafts";
import { useSaveSettings } from "./features/settings/useSaveSettings";
import { useSettingsState } from "./features/settings/useSettingsState";
import { useSmokeActionHandlers } from "./features/smoke/useSmokeActionHandlers";
import { useSmokeActions } from "./features/smoke/useSmokeActions";
import { useSmokeSnapshot } from "./features/smoke/useSmokeSnapshot";
import { waitForSmokeConfigPublish } from "./features/smoke/smokeConfigPublish";
import type { SmokeStreamSnapshot } from "./features/smoke/types";
import {
  buildDouyinVideoStatusCard,
  resolveExpertKey,
} from "./features/experts/expertDisplay";
import { StatusChip } from "./components/ui/StatusChip";
import {
  appendSmokeStatusLabel,
  formatMessageTimestamp,
  isPrimaryChatMessage,
  mergeSessionHistory,
  resolvePreferredSessionId,
  toPlainMessages,
  toUiChatMessage
} from "./lib/chat-utils";
import {
  EMPTY_SESSION_ID,
  HOME_CHAT_PROJECT_ID,
  SUCCESS_NOTICE_TIMEOUT_MS
} from "./lib/constants";
import { desktopApi, isMockDesktopApi, smokeEnabled } from "./lib/desktop-api";
import { getHiddenMessageIds, persistHiddenMessageId } from "./lib/hiddenMessages";

type ViewMode = "chat" | "experts" | "tasks" | "automation" | "plugins" | "settings" | "knowledge";
type SendPhase = "idle" | "preparing" | "streaming" | "finalizing";
type MessageReaction = "up" | "down";

interface ActiveStreamState {
  requestId: string;
  assistantMessageId: string;
  sessionId: string;
  originSessionId: string;
  targetText: string;
  renderedText: string;
  finalReply?: ChatMessage;
  frameId?: number;
}

interface SmokeUiSnapshot {
  shellReady: boolean;
  bindingRequired: boolean;
  isBound: boolean;
  showStartupOverlay: boolean;
  showBindEntry: boolean;
  saving: boolean;
  chatLaunchState: ChatLaunchState;
  startupPhase: WorkspaceSummary["startupPhase"];
  currentProjectId?: string;
  sessionScopeProjectId?: string;
  homeIntentSuggestionVisible?: boolean;
  homeIntentSuggestionProjectId?: string;
  homeIntentSuggestionProjectName?: string;
  pendingHomeIntentPrompt?: string;
}

declare global {
  interface Window {
    __QJC_SMOKE__?: {
      usingMockApi: boolean;
      viewMode: ViewMode;
      gatewayStatus: GatewayStatus | null;
      gatewayHealth: GatewayHealth | null;
      runtimeStatus: RuntimeStatus | null;
      runtimeCloudStatus: RuntimeCloudStatus | null;
      runtimeTelemetry: RuntimeTelemetryStatus | null;
      config: AppConfig | null;
      authSession: null;
      profile: null;
      credits: null;
      skills: WorkspaceSummary["skills"];
      modelConfig: AppConfig["expertModelConfig"] | null;
      systemSummary: SystemSummary | null;
      sessions: WorkspaceSummary["sessions"];
      messages: ChatMessage[];
      messagesBySession: Record<string, ChatMessage[]>;
      logs: LogEntry[];
      activeSessionId: string;
      expertProjectIds: string[];
      workspaceSummary: WorkspaceSummary | null;
      streamSmoke: SmokeStreamSnapshot | null;
      ui: SmokeUiSnapshot;
      experts: {
        standaloneIds: string[];
        homeShortcutIds: string[];
        standalonePromptAvailableIds: string[];
      };
    };
    __QJC_SMOKE_ACTIONS__?: {
      sendConversationPrompt(
        prompt: string,
        options?: {
          mode?: "chat" | "experts";
          projectId?: string;
          skillId?: string;
          sessionId?: string;
          attachments?: ChatAttachment[];
        }
      ): Promise<{
        mode: "chat" | "experts";
        projectId?: string;
        sessionId: string;
        skillId?: string;
      }>;
      activateExpertEntry(expertId: string): Promise<{
        expertId: string;
        entryMode?: ExpertEntryMode;
        viewMode: ViewMode;
        currentProjectId?: string;
        prompt: string;
      }>;
      resolveHomeIntentSuggestion(): Promise<{
        visible: boolean;
        projectId?: string;
        projectName?: string;
        pendingPrompt?: string;
      }>;
      continueHomeIntentSuggestion(): Promise<{
        continued: boolean;
      }>;
      switchHomeIntentSuggestion(): Promise<{
        switched: boolean;
        projectId?: string;
      }>;
      dismissHomeIntentSuggestion(): Promise<{
        dismissed: boolean;
      }>;
      navigateToView(view: ViewMode): Promise<{ view: ViewMode }>;
      runHomeChatNavigationScenario(view?: "plugins" | "settings" | "knowledge"): Promise<{
        sourceView: "plugins" | "settings" | "knowledge";
        viewMode: "chat";
        sessionScopeProjectId?: string;
        activeSessionId: string;
        activeSessionProjectId: string;
        messageCount: number;
      }>;
      saveSettingsConfig(options?: {
        lobsterKey?: string;
        workspacePath?: string;
        expertModelConfig?: SaveConfigInput["expertModelConfig"];
        douyinRuntimeConfig?: SaveConfigInput["douyinRuntimeConfig"];
        xhsFeishuConfig?: SaveConfigInput["xhsFeishuConfig"];
      }): Promise<{
        workspacePath: string;
        expertModelConfig: AppConfig["expertModelConfig"];
        douyinRuntimeConfig: AppConfig["douyinRuntimeConfig"];
        xhsFeishuConfig: AppConfig["xhsFeishuConfig"];
        apiKeyConfigured: boolean;
      }>;
      createProjectSession(projectId?: string, title?: string): Promise<SessionSummary>;
      openSession(sessionId: string): Promise<{ sessionId: string }>;
      clickWindowControl(kind: "minimize" | "maximize" | "close"): Promise<{ kind: "minimize" | "maximize" | "close" }>;
    };
  }
}

const seedancePrivacyBlockedCategory = "seedance_input_image_privacy_blocked";

function normalizeAssistantErrorMessage(message: string, errorCategory?: string): string {
  const text = String(message || "").trim();
  if (errorCategory === seedancePrivacyBlockedCategory) {
    return "上传的照片包含真人信息，Seedance 当前不接受这类首帧图。请改用非真人图片，或改走数字人口播。";
  }
  if (
    text.includes("InputImageSensitiveContentDetected.PrivacyInformation")
    || text.includes("input image may contain real person")
  ) {
    return "上传的照片包含真人信息，Seedance 当前不接受这类首帧图。请改用非真人图片，或改走数字人口播。";
  }
  return text;
}

const err = (value: unknown) => normalizeAssistantErrorMessage(value instanceof Error ? value.message : String(value));

function SendRocketIcon() {
  return (
    <span className="send-rocket-pair" aria-hidden="true">
      {[0, 1].map((index) => (
        <svg key={index} viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false">
          <path d="M12.9 4.2c2.4 1.2 4.1 3.3 4.9 6.1-2.4.8-4.6 2.4-6.3 4.5l-1.9 2.5-1.9-.5.5-1.9 2.5-1.9c2.1-1.7 3.7-3.9 4.5-6.3-1.2-.3-2.4-.1-3.4.8l-4.2 4.2-2.6.7.7-2.6 4.2-4.2c1.6-1.6 3.9-2.1 6-1.4Z" fill="currentColor" />
          <path d="M8.2 15.8 6 18l-1.6-1.6 2.2-2.2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
          <path d="M14.3 5.6 18.4 9.7" stroke="#ffffff" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.72" />
        </svg>
      ))}
    </span>
  );
}

export default function App() {
  const [viewMode, setViewMode] = useState<ViewMode>("chat");
  const [sessions, setSessions] = useState<WorkspaceSummary["sessions"]>([]);
  const [activeSessionId, setActiveSessionId] = useState(EMPTY_SESSION_ID);
  const [selectedSkillId, setSelectedSkillId] = useState(DEFAULT_SKILL.id);
  const [prompt, setPrompt] = useState("");
  const [errorText, setErrorText] = useState("");
  const [infoText, setInfoText] = useState("");
  const [expandedCategories, setExpandedCategories] = useState<Record<string, boolean>>({});
  const [messageReactions, setMessageReactions] = useState<Record<string, MessageReaction | undefined>>({});
  const [sidebarSessionTitles, setSidebarSessionTitles] = useState<Record<string, string>>({});
  const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
  const [skillMenuOpen, setSkillMenuOpen] = useState(false);
  const [copiedToken, setCopiedToken] = useState("");
  const skillMenuRef = useRef<HTMLDivElement | null>(null);
  const conversationWorkspaceRef = useRef<HTMLDivElement | null>(null);
  const copiedTokenResetRef = useRef<number | null>(null);
  const lastLoadedWorkspacePathRef = useRef<string | null>(null);
  const categoryPopupRef = useRef<HTMLDivElement | null>(null);
  const isConversationView = viewMode === "chat" || viewMode === "experts";
  const {
    config,
    setConfig,
    workspace,
    setWorkspace,
    runtimeStatus,
    setRuntimeStatus,
    runtimeCloudStatus,
    runtimeTelemetry,
    setRuntimeTelemetry,
    systemSummary,
    expertDefinitions,
    gatewayStatus,
    setGatewayStatus,
    gatewayHealth,
    refreshing,
    refresh,
    shellReady,
    bindingRequired,
    isBound,
    chatLaunchState,
    chatStatusMessage,
    startupMessage,
    startupPhase,
    startupStateActive
  } = useAppBootstrap({
    desktopApi,
    selectedSkillId,
    defaultSkill: DEFAULT_SKILL,
    viewMode,
    startupCopy: {
      startingHint: ui.startingHint,
      chatNotReadyError: ui.chatNotReadyError,
      startupBooting: ui.startupBooting
    },
    setSelectedSkillId,
    setErrorText,
    normalizeError: err
  });
  const {
    attachmentInputRef,
    attachmentAccept,
    composerAttachments,
    isComposerDragOver,
    clearComposerAttachment,
    removeComposerAttachment,
    openAttachmentPicker,
    handleAttachmentSelection,
    handleComposerDragEnter,
    handleComposerDragOver,
    handleComposerDragLeave,
    handleComposerDrop
  } = useComposerAttachments({
    mode: viewMode === "chat" ? "image-only" : "all-supported",
    setErrorText
  });
  const {
    isComposerResizeActive,
    composerShellStyle,
    handleComposerResizePointerDown,
    handleComposerResizePointerMove,
    handleComposerResizePointerEnd
  } = useComposerResize({
    isConversationView,
    workspaceRef: conversationWorkspaceRef
  });
  const {
    lobsterKeyDraft,
    setLobsterKeyDraft,
    workspacePathDraft,
    setWorkspacePathDraft,
    imageModelApiKeyDraft,
    setImageModelApiKeyDraft,
    imageModelBaseUrlDraft,
    setImageModelBaseUrlDraft,
    imageModelModelIdDraft,
    setImageModelModelIdDraft,
    videoModelApiKeyDraft,
    setVideoModelApiKeyDraft,
    videoModelBaseUrlDraft,
    setVideoModelBaseUrlDraft,
    videoModelModelIdDraft,
    setVideoModelModelIdDraft,
    copywritingModelApiKeyDraft,
    setCopywritingModelApiKeyDraft,
    copywritingModelBaseUrlDraft,
    setCopywritingModelBaseUrlDraft,
    copywritingModelModelIdDraft,
    setCopywritingModelModelIdDraft,
    digitalHumanVolcAccessKeyDraft,
    setDigitalHumanVolcAccessKeyDraft,
    digitalHumanVolcSecretKeyDraft,
    setDigitalHumanVolcSecretKeyDraft,
    digitalHumanQiniuAccessKeyDraft,
    setDigitalHumanQiniuAccessKeyDraft,
    digitalHumanQiniuSecretKeyDraft,
    setDigitalHumanQiniuSecretKeyDraft,
    videoAnalyzerBaseUrlDraft,
    setVideoAnalyzerBaseUrlDraft,
    videoAnalyzerModelIdDraft,
    setVideoAnalyzerModelIdDraft,
    videoAnalyzerApiKeyDraft,
    setVideoAnalyzerApiKeyDraft,
    replicationBriefBaseUrlDraft,
    setReplicationBriefBaseUrlDraft,
    replicationBriefModelIdDraft,
    setReplicationBriefModelIdDraft,
    replicationBriefApiKeyDraft,
    setReplicationBriefApiKeyDraft,
    vectcutBaseUrlDraft,
    setVectcutBaseUrlDraft,
    vectcutFileBaseUrlDraft,
    setVectcutFileBaseUrlDraft,
    vectcutApiKeyDraft,
    setVectcutApiKeyDraft,
    xhsFeishuAppIdDraft,
    setXhsFeishuAppIdDraft,
    xhsFeishuAppSecretDraft,
    setXhsFeishuAppSecretDraft,
    xhsFeishuAppTokenDraft,
    setXhsFeishuAppTokenDraft,
    xhsFeishuTableIdDraft,
    setXhsFeishuTableIdDraft,
    saving,
    setSaving,
    hasPendingLobsterKey,
    hasPendingXhsFeishuConfig
  } = useSettingsState(config);
  const {
    messageTraces,
    appendTrace,
    renameMessageTrace,
    initializeMessageTrace,
    setMessageTraceExpanded,
    collapseMessageTrace,
    removeMessageTrace
  } = useMessageTraces();
  const {
    saveConfig,
    saveLobsterKey,
    saveXhsFeishuConfig,
    saveCopywritingConfig,
    saveImageConfig,
    saveVideoConfig,
    saveDigitalHumanConfig,
    saveDouyinRuntimeConfig,
    pickWorkspaceDirectory
  } = useSaveSettings({
    config,
    desktopApi,
    saving,
    drafts: {
      lobsterKey: lobsterKeyDraft,
      workspacePath: workspacePathDraft,
      imageModelApiKey: imageModelApiKeyDraft,
      imageModelBaseUrl: imageModelBaseUrlDraft,
      imageModelModelId: imageModelModelIdDraft,
      videoModelApiKey: videoModelApiKeyDraft,
      videoModelBaseUrl: videoModelBaseUrlDraft,
      videoModelModelId: videoModelModelIdDraft,
      copywritingModelApiKey: copywritingModelApiKeyDraft,
      copywritingModelBaseUrl: copywritingModelBaseUrlDraft,
      copywritingModelModelId: copywritingModelModelIdDraft,
      digitalHumanVolcAccessKey: digitalHumanVolcAccessKeyDraft,
      digitalHumanVolcSecretKey: digitalHumanVolcSecretKeyDraft,
      digitalHumanQiniuAccessKey: digitalHumanQiniuAccessKeyDraft,
      digitalHumanQiniuSecretKey: digitalHumanQiniuSecretKeyDraft,
      videoAnalyzerBaseUrl: videoAnalyzerBaseUrlDraft,
      videoAnalyzerModelId: videoAnalyzerModelIdDraft,
      videoAnalyzerApiKey: videoAnalyzerApiKeyDraft,
      replicationBriefBaseUrl: replicationBriefBaseUrlDraft,
      replicationBriefModelId: replicationBriefModelIdDraft,
      replicationBriefApiKey: replicationBriefApiKeyDraft,
      vectcutBaseUrl: vectcutBaseUrlDraft,
      vectcutFileBaseUrl: vectcutFileBaseUrlDraft,
      vectcutApiKey: vectcutApiKeyDraft,
      xhsFeishuAppId: xhsFeishuAppIdDraft,
      xhsFeishuAppSecret: xhsFeishuAppSecretDraft,
      xhsFeishuAppToken: xhsFeishuAppTokenDraft,
      xhsFeishuTableId: xhsFeishuTableIdDraft
    },
    setters: {
      setConfig,
      setSaving,
      setErrorText,
      setInfoText,
      setWorkspacePathDraft,
      setLobsterKeyDraft,
      setImageModelApiKeyDraft,
      setImageModelBaseUrlDraft,
      setImageModelModelIdDraft,
      setVideoModelApiKeyDraft,
      setVideoModelBaseUrlDraft,
      setVideoModelModelIdDraft,
      setCopywritingModelApiKeyDraft,
      setCopywritingModelBaseUrlDraft,
      setCopywritingModelModelIdDraft,
      setDigitalHumanVolcAccessKeyDraft,
      setDigitalHumanVolcSecretKeyDraft,
      setDigitalHumanQiniuAccessKeyDraft,
      setDigitalHumanQiniuSecretKeyDraft,
      setVideoAnalyzerBaseUrlDraft,
      setVideoAnalyzerModelIdDraft,
      setVideoAnalyzerApiKeyDraft,
      setReplicationBriefBaseUrlDraft,
      setReplicationBriefModelIdDraft,
      setReplicationBriefApiKeyDraft,
      setVectcutBaseUrlDraft,
      setVectcutFileBaseUrlDraft,
      setVectcutApiKeyDraft,
      setXhsFeishuAppIdDraft,
      setXhsFeishuAppSecretDraft,
      setXhsFeishuAppTokenDraft,
      setXhsFeishuTableIdDraft
    },
    labels: {
      saveSuccessPending: ui.saveSuccessPending,
      saveSuccessApplied: ui.saveSuccessApplied,
      workspaceSaved: "工作目录已保存，正在重新预热工作区。"
    },
    refresh,
    normalizeError: err
  });

  // 切换分类展开状态
  const toggleCategory = (categoryId: string) => {
    setExpandedCategories(prev => ({
      ...prev,
      [categoryId]: !prev[categoryId]
    }));
  };

  const minimizeWindow = () => void desktopApi.window.minimize();
  const maximizeWindow = () => void desktopApi.window.maximize();
  const catalogSkills = workspace?.skills ?? [];
  const readySkills = useMemo(() => catalogSkills.filter((skill) => skill.ready), [catalogSkills]);
  const effectiveSkills = useMemo(() => (readySkills.length ? [DEFAULT_SKILL, ...readySkills] : [DEFAULT_SKILL]), [readySkills]);
  const selectedSkill = useMemo(() => effectiveSkills.find((skill) => skill.id === selectedSkillId) ?? effectiveSkills[0] ?? DEFAULT_SKILL, [effectiveSkills, selectedSkillId]);
  const setupMode = workspace?.setupMode ?? config?.setupMode ?? "employee-key";
  const savedWorkspacePath = config?.workspacePath ?? "";
  const displayedWorkspacePath = workspacePathDraft.trim() || savedWorkspacePath || ui.none;
  const hasPendingWorkspacePathChange = Boolean(config && workspacePathDraft.trim() && workspacePathDraft.trim() !== savedWorkspacePath);
  const hasPendingBasicConfig = hasPendingLobsterKey || hasPendingWorkspacePathChange;
  const hasPendingCopywritingConfig = Boolean(
    copywritingModelBaseUrlDraft.trim() !== (config?.expertModelConfig.copywriting.baseUrl ?? "").trim()
    || copywritingModelModelIdDraft.trim() !== (config?.expertModelConfig.copywriting.modelId ?? "").trim()
    || copywritingModelApiKeyDraft.trim()
  );
  const hasPendingImageConfig = Boolean(
    imageModelBaseUrlDraft.trim() !== (config?.expertModelConfig.image.baseUrl ?? "").trim()
    || imageModelModelIdDraft.trim() !== (config?.expertModelConfig.image.modelId ?? "").trim()
    || imageModelApiKeyDraft.trim()
  );
  const hasPendingVideoConfig = Boolean(
    videoModelBaseUrlDraft.trim() !== (config?.expertModelConfig.video.baseUrl ?? "").trim()
    || videoModelModelIdDraft.trim() !== (config?.expertModelConfig.video.modelId ?? "").trim()
    || videoModelApiKeyDraft.trim()
  );
  const hasPendingDigitalHumanConfig = Boolean(
    digitalHumanVolcAccessKeyDraft.trim()
    || digitalHumanVolcSecretKeyDraft.trim()
    || digitalHumanQiniuAccessKeyDraft.trim()
    || digitalHumanQiniuSecretKeyDraft.trim()
  );
  const hasPendingDouyinRuntimeConfig = Boolean(
    videoAnalyzerBaseUrlDraft.trim() !== (config?.douyinRuntimeConfig.videoAnalyzer.baseUrl ?? "").trim()
    || videoAnalyzerModelIdDraft.trim() !== (config?.douyinRuntimeConfig.videoAnalyzer.modelId ?? "").trim()
    || videoAnalyzerApiKeyDraft.trim()
    || replicationBriefBaseUrlDraft.trim() !== (config?.douyinRuntimeConfig.replicationBrief.baseUrl ?? "").trim()
    || replicationBriefModelIdDraft.trim() !== (config?.douyinRuntimeConfig.replicationBrief.modelId ?? "").trim()
    || replicationBriefApiKeyDraft.trim()
    || vectcutBaseUrlDraft.trim() !== (config?.douyinRuntimeConfig.vectcut.baseUrl ?? "").trim()
    || vectcutFileBaseUrlDraft.trim() !== (config?.douyinRuntimeConfig.vectcut.fileBaseUrl ?? "").trim()
    || vectcutApiKeyDraft.trim()
  );
  const hasPendingSettingsChange = getHasPendingSettingsChange({
    hasPendingBasicConfig,
    hasPendingXhsFeishuConfig,
    hasPendingCopywritingConfig,
    hasPendingImageConfig,
    hasPendingVideoConfig,
    hasPendingDigitalHumanConfig,
    hasPendingDouyinRuntimeConfig
  });
  void hasPendingSettingsChange;
  const resetXhsFeishuSettingsDrafts = useCallback(() => {
    const drafts = getResetXhsFeishuSettingsDrafts();
    setXhsFeishuAppIdDraft(drafts.xhsFeishuAppId);
    setXhsFeishuAppSecretDraft(drafts.xhsFeishuAppSecret);
    setXhsFeishuAppTokenDraft(drafts.xhsFeishuAppToken);
    setXhsFeishuTableIdDraft(drafts.xhsFeishuTableId);
  }, []);
  const resetCopywritingSettingsDrafts = useCallback(() => {
    if (!config) {
      return;
    }

    const drafts = getResetCopywritingSettingsDrafts(config);
    setCopywritingModelBaseUrlDraft(drafts.copywritingModelBaseUrl);
    setCopywritingModelModelIdDraft(drafts.copywritingModelModelId);
    setCopywritingModelApiKeyDraft(drafts.copywritingModelApiKey);
  }, [config]);
  const resetImageSettingsDrafts = useCallback(() => {
    if (!config) {
      return;
    }

    const drafts = getResetImageSettingsDrafts(config);
    setImageModelBaseUrlDraft(drafts.imageModelBaseUrl);
    setImageModelModelIdDraft(drafts.imageModelModelId);
    setImageModelApiKeyDraft(drafts.imageModelApiKey);
  }, [config]);
  const resetVideoSettingsDrafts = useCallback(() => {
    if (!config) {
      return;
    }

    const drafts = getResetVideoSettingsDrafts(config);
    setVideoModelBaseUrlDraft(drafts.videoModelBaseUrl);
    setVideoModelModelIdDraft(drafts.videoModelModelId);
    setVideoModelApiKeyDraft(drafts.videoModelApiKey);
  }, [config]);
  const resetDigitalHumanSettingsDrafts = useCallback(() => {
    const drafts = getResetDigitalHumanSettingsDrafts();
    setDigitalHumanVolcAccessKeyDraft(drafts.digitalHumanVolcAccessKey);
    setDigitalHumanVolcSecretKeyDraft(drafts.digitalHumanVolcSecretKey);
    setDigitalHumanQiniuAccessKeyDraft(drafts.digitalHumanQiniuAccessKey);
    setDigitalHumanQiniuSecretKeyDraft(drafts.digitalHumanQiniuSecretKey);
  }, []);
  const resetDouyinRuntimeSettingsDrafts = useCallback(() => {
    if (!config) {
      return;
    }

    const drafts = getResetDouyinRuntimeSettingsDrafts(config);
    setVideoAnalyzerBaseUrlDraft(drafts.videoAnalyzerBaseUrl);
    setVideoAnalyzerModelIdDraft(drafts.videoAnalyzerModelId);
    setVideoAnalyzerApiKeyDraft(drafts.videoAnalyzerApiKey);
    setReplicationBriefBaseUrlDraft(drafts.replicationBriefBaseUrl);
    setReplicationBriefModelIdDraft(drafts.replicationBriefModelId);
    setReplicationBriefApiKeyDraft(drafts.replicationBriefApiKey);
    setVectcutBaseUrlDraft(drafts.vectcutBaseUrl);
    setVectcutFileBaseUrlDraft(drafts.vectcutFileBaseUrl);
    setVectcutApiKeyDraft(drafts.vectcutApiKey);
  }, [config]);
  const startupProgressTarget = chatLaunchState === "ready" ? 1 : getStartupProgress(startupPhase);
  const [visualStartupProgress, setVisualStartupProgress] = useState(startupProgressTarget);
  useEffect(() => {
    if (chatLaunchState === "error") {
      return;
    }

    setVisualStartupProgress((currentProgress) => getNextStartupVisualProgress(currentProgress, startupProgressTarget));
  }, [chatLaunchState, startupProgressTarget]);
  const startupCurtainStatus = getStartupCurtainStatus(startupPhase, chatLaunchState);
  const startupCurtainFootnote = getStartupCurtainFootnote(chatLaunchState);
  const {
    projects,
    visibleProjects,
    standaloneExpertEntries,
    projectExpertEntries,
    homeShortcutEntries,
    sidebarExpertEntries,
    expertPageProjects,
    activeProject,
    activeExpertKey,
    activeExpertVisualKey,
    activeExpertName,
    activeExpertGuide
  } = useSidebarModel({
    workspace,
    expertDefinitions,
    viewMode,
    defaultChatName: ui.defaultChat
  });
  const sessionScopeProjectId = useMemo(() => {
    if (bindingRequired) {
      return undefined;
    }
    return viewMode === "chat" ? HOME_CHAT_PROJECT_ID : activeProject?.id;
  }, [activeProject?.id, bindingRequired, viewMode]);
  const scopedSessions = useMemo(
    () => sessionScopeProjectId
      ? sessions.filter((session) => session.projectId === sessionScopeProjectId)
      : sessions,
    [sessionScopeProjectId, sessions]
  );
  const preferredSessionId = useMemo(() => resolvePreferredSessionId(scopedSessions, activeSessionId), [activeSessionId, scopedSessions]);
  const visibleSessionId = useMemo(
    () => scopedSessions.some((session) => session.id === activeSessionId) ? activeSessionId : preferredSessionId,
    [activeSessionId, preferredSessionId, scopedSessions]
  );
  const {
    messagesBySession,
    messages,
    updateSessionMessages,
    clearAllSessionMessages,
    moveSessionMessages,
    updateMessageById
  } = useSessionMessageStore(visibleSessionId ?? "");
  const {
    messageListRef,
    resetMessageListAutoScroll,
    handleMessageListScroll,
    scrollMessageListToBottom
  } = useMessageListAutoScroll();
  const upsertSession = useCallback((session: WorkspaceSummary["sessions"][number]) => {
    setSessions((current) => [session, ...current.filter((item) => item.id !== session.id)]);
  }, [setSessions]);
  const hasConversationProject = viewMode === "chat"
    ? visibleProjects.length > 0
    : Boolean(workspace?.projectReady && activeProject?.id && workspace?.currentProjectId === activeProject.id);
  const {
    sendPhase,
    sending,
    streamSmoke,
    activeStreamRef,
    submitPrompt,
    cancelActiveStream
  } = useChatStreamingController({
    desktopApi,
    viewMode,
    workspace,
    runtimeStatus,
    gatewayStatus,
    runtimeTelemetry,
    isBound,
    bindingRequired,
    saving,
    smokeEnabled,
    sessionScopeProjectId,
    visibleSessionId,
    messagesBySession,
    updateSessionMessages,
    moveSessionMessages,
    updateMessageById,
    upsertSession,
    appendTrace,
    renameMessageTrace,
    initializeMessageTrace,
    setMessageTraceExpanded,
    collapseMessageTrace,
    clearComposerAttachment,
    setPrompt,
    setSkillMenuOpen,
    setErrorText,
    setWorkspace,
    setRuntimeStatus,
    setGatewayStatus,
    setRuntimeTelemetry,
    scrollMessageListToBottom,
    normalizeAssistantErrorMessage,
    ui
  });
  const hasVisibleConversation = messages.length > 0 || sendPhase !== "idle";
  const showStartupOverlay = startupStateActive && !hasVisibleConversation;
  const hasVisibleSession = Boolean(visibleSessionId && scopedSessions.some((session) => session.id === visibleSessionId));
  const canSend = isBound && hasConversationProject && (prompt.trim().length > 0 || composerAttachments.length > 0) && !sending && !saving;
  const {
    pendingHomeIntentSuggestion,
    homeIntentDecisionPending,
    requestHomeIntentSuggestion,
    resolveHomeIntentSuggestion,
    continuePendingHomePromptInHome,
    switchExpertAndContinuePendingHomePrompt,
    dismissPendingHomeIntentSuggestion
  } = useHomeIntentSuggestion({
    desktopApi,
    viewMode,
    sessionScopeProjectId,
    prompt,
    selectedSkillId,
    defaultSkillId: DEFAULT_SKILL.id,
    composerAttachments,
    setViewMode,
    setPrompt,
    setActiveProjectSession: setActiveSessionId,
    upsertSession,
    setSkillMenuOpen,
    setErrorText,
    submitPrompt,
    switchExpert,
    normalizeError: err
  });
  const { sendPrompt } = usePromptSubmission({
    canSend,
    viewMode,
    sessionScopeProjectId,
    prompt,
    selectedSkillId,
    defaultSkillId: DEFAULT_SKILL.id,
    composerAttachments,
    submitPrompt,
    requestHomeIntentSuggestion,
    clearHomeIntentSuggestion: dismissPendingHomeIntentSuggestion,
    setErrorText,
    normalizeError: err
  });
  const sendButtonLabel = sendPhase === "preparing"
    ? "停止生成"
    : sendPhase === "streaming" || sendPhase === "finalizing"
      ? "停止生成"
      : !isBound
        ? ui.bindFirst
        : ui.send;
  const showBindEntry = !isBound && !startupStateActive;
  const showSettingsStatusHint = viewMode === "settings" && isBound && chatLaunchState !== "ready" && Boolean(startupMessage);
  const showInlineStartupNotice = startupStateActive && hasVisibleConversation && isConversationView;
  const pluginGroups = useMemo(() => groupPluginsByStatus(workspace?.plugins), [workspace?.plugins]);

  useEffect(() => {
    if (!isConversationView || !visibleSessionId) {
      return;
    }

    resetMessageListAutoScroll();
    scrollMessageListToBottom({ force: true, behavior: "auto" });
  }, [isConversationView, resetMessageListAutoScroll, scrollMessageListToBottom, visibleSessionId]);

  useEffect(() => {
    if (!isConversationView) {
      return;
    }

    scrollMessageListToBottom({
      behavior: sendPhase === "streaming" ? "auto" : "smooth"
    });
  }, [isConversationView, messageTraces, messages, scrollMessageListToBottom, sendPhase]);

  useEffect(() => {
    if (viewMode !== "chat" && viewMode !== "experts") {
      clearComposerAttachment();
    }
  }, [viewMode]);

  const pluginSections = [
    { id: "included", title: "可直接使用", description: "已经接入当前工作区，可直接参与协作。", items: pluginGroups.included },
    { id: "extension", title: "待扩展接入", description: "需要扩展插件或外部能力后启用。", items: pluginGroups.extension },
    { id: "unavailable", title: "当前不可用", description: "当前环境尚未满足使用条件。", items: pluginGroups.unavailable }
  ].filter((section) => section.items.length > 0);
  const pluginCards = pluginSections.flatMap((section) => section.items.map((plugin) => ({
    plugin,
    sectionTitle: section.title
  })));
  const workspaceStatusTone = getShellWorkspaceStatusTone(chatLaunchState, isBound);
  const workspaceStatusLabel = getShellWorkspaceStatusLabel(chatLaunchState, isBound);
  const [loadingHistorySessionId, setLoadingHistorySessionId] = useState("");
  const loadedHistorySessionIdsRef = useRef(new Set<string>());
  useEffect(() => {
    if (!infoText) {
      return;
    }

    const timer = window.setTimeout(() => {
      setInfoText("");
    }, SUCCESS_NOTICE_TIMEOUT_MS);

    return () => window.clearTimeout(timer);
  }, [infoText]);


  useEffect(() => () => {
    if (copiedTokenResetRef.current !== null) {
      window.clearTimeout(copiedTokenResetRef.current);
    }
  }, []);

  const clearSessions = useCallback(() => {
    setSessions([]);
  }, []);

  const loadMessages = useCallback(async (sessionId: string, canRead: boolean, showError = false) => {
    if (!canRead) {
      return;
    }

    setLoadingHistorySessionId(sessionId);
    try {
      const hiddenMessageIds = getHiddenMessageIds(sessionId);
      const nextMessages = (await desktopApi.chat.listMessages(sessionId))
        .filter(isPrimaryChatMessage)
        .filter((message) => !hiddenMessageIds.has(message.id))
        .map((message) => toUiChatMessage(message));
      updateSessionMessages(sessionId, (current) => {
        return mergeSessionHistory(current.filter((message) => !hiddenMessageIds.has(message.id)), nextMessages);
      });
      loadedHistorySessionIdsRef.current.add(sessionId);
    } catch (error) {
      if (showError) {
        setErrorText(err(error));
      }
    } finally {
      setLoadingHistorySessionId((current) => current === sessionId ? "" : current);
    }
  }, [desktopApi.chat, updateSessionMessages]);

  useEffect(() => {
    if (!isBound || !visibleSessionId || !hasVisibleSession) {
      return;
    }

    void loadMessages(visibleSessionId, true, false);
  }, [hasVisibleSession, isBound, loadMessages, visibleSessionId]);

  const {
    projectActionPending,
    sessionsLoading,
    switchProject,
    switchProjectPreservingMessages,
    createProjectSession,
    closeProjectSession,
    openSession
  } = useChatSessionsController({
    desktopApi,
    viewMode,
    isBound,
    bindingRequired,
    sessionScopeProjectId,
    activeSessionId,
    preferredSessionId,
    sendPhase,
    activeStreamSessionId: activeStreamRef.current?.sessionId,
    workspace,
    homeSessionTitle: homeChatCopy.title,
    setSessions,
    clearSessions,
    clearAllSessionMessages,
    setActiveProjectSession: setActiveSessionId,
    upsertSession,
    setWorkspace,
    setErrorText
  });

  const {
    activateHomeShortcut,
    handleNavSelection
  } = useHomeNavigation({
    desktopApi,
    workspace,
    projectActionPending,
    setViewMode,
    setActiveProjectSession: setActiveSessionId,
    setErrorText,
    setPrompt,
    clearSessions,
    clearAllSessionMessages,
    switchProject,
    switchProjectPreservingMessages,
    buildShortcutPrompt
  });

  const smokeActions = useSmokeActionHandlers({
    activeSessionId,
    composerAttachments,
    desktopApi,
    expertDefinitions,
    prompt,
    sessionScopeProjectId,
    workspace,
    selectedSkillId,
    defaultSkillId: DEFAULT_SKILL.id,
    setViewMode,
    handleNavSelection,
    setSelectedSkillId,
    setActiveProjectSession: setActiveSessionId,
    setPrompt,
    setPendingDrafts: {
      setWorkspacePathDraft,
      setLobsterKeyDraft,
      setImageModelApiKeyDraft,
      setVideoModelApiKeyDraft,
      setCopywritingModelApiKeyDraft,
      setDigitalHumanVolcAccessKeyDraft,
      setDigitalHumanVolcSecretKeyDraft,
      setDigitalHumanQiniuAccessKeyDraft,
      setDigitalHumanQiniuSecretKeyDraft,
      setVideoAnalyzerBaseUrlDraft,
      setVideoAnalyzerModelIdDraft,
      setVideoAnalyzerApiKeyDraft,
      setReplicationBriefBaseUrlDraft,
      setReplicationBriefModelIdDraft,
      setReplicationBriefApiKeyDraft,
      setVectcutBaseUrlDraft,
      setVectcutFileBaseUrlDraft,
      setVectcutApiKeyDraft,
      setXhsFeishuAppIdDraft,
      setXhsFeishuAppSecretDraft,
      setXhsFeishuAppTokenDraft,
      setXhsFeishuTableIdDraft
    },
    setConfig,
    upsertSession,
    submitPrompt,
    switchExpert,
    activateHomeShortcut,
    buildShortcutPrompt,
    resolveExpertProject,
    resolveExpertDisplayName,
    saveConfig,
    waitForSmokeConfigPublish,
    resolveHomeIntentSuggestion,
    continueHomeIntentSuggestion: continuePendingHomePromptInHome,
    switchHomeIntentSuggestion: switchExpertAndContinuePendingHomePrompt,
    dismissHomeIntentSuggestion: dismissPendingHomeIntentSuggestion
  });
  useSmokeActions(smokeActions);

  useEffect(() => {
    if (!skillMenuOpen) {
      return;
    }

    const handlePointerDown = (event: MouseEvent) => {
      if (skillMenuRef.current?.contains(event.target as Node)) {
        return;
      }
      setSkillMenuOpen(false);
    };

    document.addEventListener("pointerdown", handlePointerDown);
    return () => {
      document.removeEventListener("pointerdown", handlePointerDown);
    };
  }, [skillMenuOpen]);

  useEffect(() => {
    if (viewMode !== "experts" || !expertPageProjects.length || projectActionPending) {
      return;
    }

    if (expertPageProjects.some((project) => project.id === activeProject?.id)) {
      return;
    }

    void switchProject(expertPageProjects[0].id);
  }, [activeProject?.id, expertPageProjects, projectActionPending, viewMode]);

  useEffect(() => {
    if (!config) {
      return;
    }

    setWorkspacePathDraft((current) => {
      const lastLoadedWorkspacePath = lastLoadedWorkspacePathRef.current;
      lastLoadedWorkspacePathRef.current = config.workspacePath;
      if (!current || current === lastLoadedWorkspacePath) {
        return config.workspacePath;
      }
      return current;
    });
    setVideoAnalyzerBaseUrlDraft(config.douyinRuntimeConfig.videoAnalyzer.baseUrl);
    setVideoAnalyzerModelIdDraft(config.douyinRuntimeConfig.videoAnalyzer.modelId ?? "");
    setReplicationBriefBaseUrlDraft(config.douyinRuntimeConfig.replicationBrief.baseUrl);
    setReplicationBriefModelIdDraft(config.douyinRuntimeConfig.replicationBrief.modelId ?? "");
    setVectcutBaseUrlDraft(config.douyinRuntimeConfig.vectcut.baseUrl);
    setVectcutFileBaseUrlDraft(config.douyinRuntimeConfig.vectcut.fileBaseUrl);
  }, [config]);

  useEffect(() => {
    if (
      !isBound
      || !visibleSessionId
      || !hasVisibleSession
      || !workspace?.chatReady
      || !canExchangeMessages(workspace, runtimeStatus, gatewayStatus)
    ) {
      return;
    }

    void loadMessages(visibleSessionId, true, false);
  }, [gatewayStatus, hasVisibleSession, isBound, loadMessages, runtimeStatus, sendPhase, visibleSessionId, workspace?.chatReady]);

  useEffect(() => {
    let cancelled = false;

    async function hydrateSidebarSessionTitles() {
      if (!scopedSessions.length) {
        if (!cancelled) {
          setSidebarSessionTitles({});
        }
        return;
      }

      const nextEntries = await Promise.all(scopedSessions.map(async (session, index) => {
        const cachedMessages = messagesBySession[session.id];
        const hiddenMessageIds = getHiddenMessageIds(session.id);
        if (cachedMessages?.length) {
          const visibleCachedMessages = cachedMessages.filter((message) => !hiddenMessageIds.has(message.id));
          return [session.id, deriveSidebarSessionTitle(toPlainMessages(visibleCachedMessages))] as const;
        }

        try {
          const rawMessages = await desktopApi.chat.listMessages(session.id);
          const visibleMessages = rawMessages
            .filter(isPrimaryChatMessage)
            .filter((message) => !hiddenMessageIds.has(message.id));
          return [session.id, deriveSidebarSessionTitle(visibleMessages)] as const;
        } catch {
          return [session.id, formatSessionTitle(session.title, index)] as const;
        }
      }));

      if (!cancelled) {
        setSidebarSessionTitles(Object.fromEntries(nextEntries));
      }
    }

    void hydrateSidebarSessionTitles();
    return () => {
      cancelled = true;
    };
  }, [desktopApi.chat, messagesBySession, scopedSessions]);

  useEffect(() => {
    if (!effectiveSkills.some((skill) => skill.id === selectedSkillId)) {
      setSelectedSkillId(effectiveSkills[0]?.id ?? DEFAULT_SKILL.id);
    }
  }, [effectiveSkills, selectedSkillId]);

  useSmokeSnapshot({
    viewMode,
    gatewayStatus,
    gatewayHealth,
    runtimeStatus,
    runtimeCloudStatus,
    runtimeTelemetry,
    config,
    systemSummary,
    sessions: scopedSessions,
    messages: toPlainMessages(messages),
    messagesBySession: Object.fromEntries(
      Object.entries(messagesBySession).map(([sessionId, sessionMessages]) => [sessionId, toPlainMessages(sessionMessages)])
    ),
    activeSessionId: visibleSessionId ?? "",
    expertProjectIds: expertPageProjects.map((project) => project.id),
    workspaceSummary: workspace,
    streamSmoke,
    ui: {
      shellReady,
      bindingRequired,
      isBound,
      showStartupOverlay,
      showBindEntry,
      saving,
      chatLaunchState,
      startupPhase,
      currentProjectId: workspace?.currentProjectId,
      sessionScopeProjectId,
      homeIntentSuggestionVisible: Boolean(pendingHomeIntentSuggestion),
      homeIntentSuggestionProjectId: pendingHomeIntentSuggestion?.suggestion.projectId,
      homeIntentSuggestionProjectName: pendingHomeIntentSuggestion?.suggestion.projectName,
      pendingHomeIntentPrompt: pendingHomeIntentSuggestion?.prompt
    },
    experts: {
      standaloneIds: standaloneExpertEntries.map((entry) => entry.definition.id),
      homeShortcutIds: homeShortcutEntries.map((entry) => entry.definition.id),
      standalonePromptAvailableIds: standaloneExpertEntries
        .filter((entry) => entry.definition.promptAvailable)
        .map((entry) => entry.definition.id)
    }
  });

  async function handleComposerKeyDown(event: ReactKeyboardEvent<HTMLTextAreaElement>) {
    if (event.nativeEvent.isComposing || event.key !== "Enter" || event.shiftKey || event.altKey) {
      return;
    }

    event.preventDefault();
    await sendPrompt();
  }

  async function closeWindow() {
    await desktopApi.window.close();
  }

  async function handleCopyText(token: string, text: string) {
    const resolved = text.trim();
    if (!resolved) {
      return;
    }

    try {
      await navigator.clipboard.writeText(resolved);
      setCopiedToken(token);
      if (copiedTokenResetRef.current !== null) {
        window.clearTimeout(copiedTokenResetRef.current);
      }
      copiedTokenResetRef.current = window.setTimeout(() => {
        setCopiedToken("");
        copiedTokenResetRef.current = null;
      }, 2000);
    } catch (error) {
      setErrorText(err(error));
    }
  }

  function deleteMessage(messageId: string) {
    if (!visibleSessionId) {
      return;
    }

    persistHiddenMessageId(visibleSessionId, messageId);
    updateSessionMessages(visibleSessionId, (current) => {
      const nextMessages = current.filter((message) => message.id !== messageId);
      return nextMessages.length === current.length ? current : nextMessages;
    });
    removeMessageTrace(messageId);
    setMessageReactions((current) => {
      if (!(messageId in current)) {
        return current;
      }
      const { [messageId]: _removed, ...rest } = current;
      return rest;
    });
    if (copiedToken === `message:${messageId}`) {
      setCopiedToken("");
    }
  }

  async function regenerateAssistantMessage(messageId: string) {
    if (sending || !visibleSessionId || !sessionScopeProjectId) {
      return;
    }

    const targetIndex = messages.findIndex((message) => message.id === messageId);
    if (targetIndex < 0) {
      return;
    }

    const previousUserMessage = [...messages.slice(0, targetIndex)].reverse().find((message) => message.role === "user" && message.content.trim());
    if (!previousUserMessage) {
      return;
    }

    await submitPrompt({
      promptText: previousUserMessage.content,
      skillId: selectedSkill.id === DEFAULT_SKILL.id ? undefined : selectedSkill.id,
      forcedSessionId: visibleSessionId,
      forcedProjectId: sessionScopeProjectId
    });
  }

  function toggleMessageReaction(messageId: string, reaction: MessageReaction) {
    setMessageReactions((current) => ({
      ...current,
      [messageId]: current[messageId] === reaction ? undefined : reaction
    }));
  }

  function chooseSkill(skillId: string) {
    setSelectedSkillId(skillId);
    setSkillMenuOpen(false);
  }

  function applyStarterPrompt(nextPrompt: string) {
    setViewMode((current) => (current === "experts" ? "experts" : "chat"));
    setPrompt(nextPrompt);
  }

  function clearSelectedSkill() {
    setSelectedSkillId(DEFAULT_SKILL.id);
    setSkillMenuOpen(false);
  }

  async function switchExpert(projectId: string) {
    setViewMode("experts");
    if (workspace?.currentProjectId === projectId && viewMode === "experts") {
      return;
    }
    await switchProject(projectId);
  }

  // 处理分类弹出层中的专家选择
  function handleExpertSelect(entry: SidebarExpertEntry) {
    const isStandalone = entry.definition.entryMode === "standalone";
    if (isStandalone) {
      if (entry.project) {
        void switchExpert(entry.project.id);
      } else {
        setErrorText(`${entry.displayName} 项目未下发`);
      }
    } else {
      void activateHomeShortcut(entry);
    }
    setExpandedCategories({}); // 关闭所有展开的分类
  }

      // 知识库特定逻辑（如有）

  async function retryStartup() {
    setErrorText("");
    await desktopApi.workspace.warmup().catch(() => undefined);
    await refresh(false);
  }

  async function exportDiagnostics() {
    setErrorText("");
    setInfoText("");

    try {
      const result = await desktopApi.diagnostics.exportSnapshot();
      setInfoText(ui.exported + result.filePath + (result.startupLogPath ? " | startup: " + result.startupLogPath : ""));
    } catch (error) {
      setErrorText(err(error));
    }
  }

  const sidebarSessionLabel = viewMode === "experts" ? "专家会话" : "会话管理";
  const selectedSkillBadge = selectedSkillId === DEFAULT_SKILL.id ? "千匠问天" : "@" + selectedSkill.name;
  const isVisibleSessionHistoryLoading = Boolean(
    visibleSessionId
    && hasVisibleSession
    && (
      sessionsLoading
      || loadingHistorySessionId === visibleSessionId
      || !loadedHistorySessionIdsRef.current.has(visibleSessionId)
    )
  );
  const showEmptyState = viewMode === "chat"
    ? (!projectActionPending && !sessionsLoading && (!hasVisibleSession || (!isVisibleSessionHistoryLoading && messages.length === 0)))
    : messages.length === 0;
  const sidebarNewSessionAction = (
    <button
      type="button"
      className="sidebar-new-session conversation-new-session app-no-drag"
      disabled={projectActionPending || !isBound || !projects.length}
      onClick={() => void createProjectSession()}
    >
      <span className="conversation-new-session-plus" aria-hidden="true">+</span>
      <span className="conversation-new-session-label">新对话</span>
    </button>
  );
  const conversationPanelTitle = viewMode === "experts" ? activeExpertName : "对话";
  const expertWorkspaceLogo = viewMode === "experts" && (activeExpertKey === "xiaohongshu" || activeExpertKey === "douyin") ? (
    <span className={"expert-workspace-logo expert-workspace-logo-" + activeExpertKey} aria-hidden="true">
      {activeExpertKey === "xiaohongshu" ? <RedBookIcon /> : <DouyinNoteIcon />}
    </span>
  ) : null;
  const panelActions = (
    <>
      <StatusChip
        tone={workspaceStatusTone}
        title={"工作区状态：" + workspaceStatusLabel}
        ariaLabel={"工作区状态：" + workspaceStatusLabel}
      >
        {workspaceStatusLabel}
      </StatusChip>
      {isMockDesktopApi ? (
        <StatusChip tone="warning" title="当前使用模拟桌面 API" ariaLabel="当前使用模拟桌面 API">
          Mock API
        </StatusChip>
      ) : null}
    </>
  );
  const composerPlaceholder = isBound ? ui.taskPlaceholder : ui.taskDisabledPlaceholder;
  const conversationWorkspaceProps = {
    viewMode,
    workspaceRef: conversationWorkspaceRef,
    status: {
      panelActions,
      showInlineStartupNotice,
      chatLaunchState,
      startupCurtainStatus
    },
    emptyState: {
      selectedSkillBadge,
      selectedSkillIsDefault: selectedSkillId === DEFAULT_SKILL.id,
      homeLeadIcon: <LobsterClawIcon />,
      activeExpertName,
      activeExpertKey,
      activeExpertVisualKey,
      activeExpertGuide,
      expertWorkspaceLogo,
      homeIntentSuggestion: pendingHomeIntentSuggestion?.suggestion ?? null,
      homeIntentDecisionPending,
      homeIntentLabels: {
        title: ui.suggestionSwitchTitle,
        prefix: ui.suggestionSwitchPrefix,
        suffix: ui.suggestionSwitchSuffix,
        continue: ui.suggestionContinue,
        switchAction: ui.suggestionSwitchAction
      },
      showBindEntry,
      bindEntry: {
        lobsterKeyDraft,
        workspaceApiKeyConfigured: Boolean(workspace?.apiKeyConfigured),
        saving,
        bindingLabel: ui.binding,
        onLobsterKeyChange: setLobsterKeyDraft,
        onSave: () => void saveLobsterKey()
      },
      hasExpertProjects: Boolean(expertPageProjects.length),
      noExpertsLabel: expertsPageCopy.noExperts,
      starterQuestionsHint: ui.starterQuestionsHint,
      homeEmptyTitle: homeChatCopy.emptyTitle,
      homePrompts: homeChatCopy.prompts
    },
    messages: {
      messageListRef,
      messages,
      showEmptyState,
      messageTraces,
      messageReactions,
      copiedToken,
      sending,
      messageLabels: {
        thinking: ui.thinking,
        hideTrace: ui.hideTrace,
        traceCollapsed: ui.traceCollapsed
      },
      copyIcon: <CopyIcon />,
      copiedIcon: <CheckIcon />,
      deleteIcon: <TrashIcon />,
      regenerateIcon: <RefreshIcon />
    },
    composer: {
      attachmentInputRef,
      skillMenuRef,
      prompt,
      isBound,
      canSend,
      isComposerDragOver,
      isComposerResizeActive,
      composerShellStyle,
      attachmentAccept,
      attachments: composerAttachments,
      composerPlaceholder,
      sendButtonLabel,
      skillMenuTitle: ui.skillMenuTitle,
      defaultChatLabel: ui.defaultChat,
      defaultSkillId: DEFAULT_SKILL.id,
      selectedSkillId,
      selectedSkillName: selectedSkill.name,
      skills: effectiveSkills,
      skillMenuOpen,
      attachmentIcon: <AttachmentIcon />,
      submitIcon: sendPhase !== "idle" ? <StopIcon /> : <ArrowUpIcon />
    },
    actions: {
      renderExpertIcon,
      renderIntentIcon: getIntentSuggestionIcon,
      onContinueHomeIntent: continuePendingHomePromptInHome,
      onSwitchHomeIntent: switchExpertAndContinuePendingHomePrompt,
      onStarterPrompt: applyStarterPrompt,
      renderThumbIcon: (direction) => <ThumbIcon direction={direction} />,
      renderMarkdownContent,
      buildDouyinVideoStatusCard,
      formatMessageTimestamp,
      onMessageListScroll: handleMessageListScroll,
      onCopyText: handleCopyText,
      onDeleteMessage: deleteMessage,
      onTraceExpandedChange: setMessageTraceExpanded,
      onRegenerateAssistantMessage: regenerateAssistantMessage,
      onToggleMessageReaction: toggleMessageReaction,
      onSubmit: sendPrompt,
      onCancel: cancelActiveStream,
      onPromptChange: setPrompt,
      onTextareaKeyDown: handleComposerKeyDown,
      onAttachmentSelection: handleAttachmentSelection,
      onOpenAttachmentPicker: openAttachmentPicker,
      onRemoveAttachment: removeComposerAttachment,
      onToggleSkillMenu: () => setSkillMenuOpen((current) => !current),
      onClearSelectedSkill: clearSelectedSkill,
      onChooseSkill: chooseSkill,
      onDragEnter: handleComposerDragEnter,
      onDragOver: handleComposerDragOver,
      onDragLeave: handleComposerDragLeave,
      onDrop: handleComposerDrop,
      onResizePointerDown: handleComposerResizePointerDown,
      onResizePointerMove: handleComposerResizePointerMove,
      onResizePointerEnd: handleComposerResizePointerEnd
    }
  } satisfies ComponentProps<typeof ConversationWorkspaceView>;
  const settingsStatusHint = showSettingsStatusHint
    ? <div className={"inline-hint settings-runtime-hint" + (chatLaunchState === "error" ? " error" : "")}>{startupMessage}</div>
    : null;
  const revealConfigSecret = useCallback(async (secretId: ConfigSecretId) => {
    setErrorText("");
    try {
      const secretValue = await desktopApi.config.revealSecret(secretId);
      if (!secretValue) {
        setErrorText("本机未保存该密钥。");
      }
      return secretValue;
    } catch (error) {
      setErrorText(err(error));
      return null;
    }
  }, []);
  const settingsPanelsProps = {
    config,
    workspaceApiKeyConfigured: Boolean(workspace?.apiKeyConfigured),
    displayedWorkspacePath,
    saving,
    hasPendingLobsterKey,
    hasPendingWorkspacePathChange,
    hasPendingXhsFeishuConfig,
    hasPendingCopywritingConfig,
    hasPendingImageConfig,
    hasPendingVideoConfig,
    hasPendingDigitalHumanConfig,
    hasPendingDouyinRuntimeConfig,
    labels: { export: ui.export },
    drafts: {
      lobsterKey: lobsterKeyDraft,
      xhsFeishuAppId: xhsFeishuAppIdDraft,
      xhsFeishuAppSecret: xhsFeishuAppSecretDraft,
      xhsFeishuAppToken: xhsFeishuAppTokenDraft,
      xhsFeishuTableId: xhsFeishuTableIdDraft,
      copywritingModelBaseUrl: copywritingModelBaseUrlDraft,
      copywritingModelModelId: copywritingModelModelIdDraft,
      copywritingModelApiKey: copywritingModelApiKeyDraft,
      imageModelBaseUrl: imageModelBaseUrlDraft,
      imageModelModelId: imageModelModelIdDraft,
      imageModelApiKey: imageModelApiKeyDraft,
      videoModelBaseUrl: videoModelBaseUrlDraft,
      videoModelModelId: videoModelModelIdDraft,
      videoModelApiKey: videoModelApiKeyDraft,
      digitalHumanVolcAccessKey: digitalHumanVolcAccessKeyDraft,
      digitalHumanVolcSecretKey: digitalHumanVolcSecretKeyDraft,
      digitalHumanQiniuAccessKey: digitalHumanQiniuAccessKeyDraft,
      digitalHumanQiniuSecretKey: digitalHumanQiniuSecretKeyDraft,
      videoAnalyzerBaseUrl: videoAnalyzerBaseUrlDraft,
      videoAnalyzerModelId: videoAnalyzerModelIdDraft,
      videoAnalyzerApiKey: videoAnalyzerApiKeyDraft,
      replicationBriefBaseUrl: replicationBriefBaseUrlDraft,
      replicationBriefModelId: replicationBriefModelIdDraft,
      replicationBriefApiKey: replicationBriefApiKeyDraft,
      vectcutBaseUrl: vectcutBaseUrlDraft,
      vectcutFileBaseUrl: vectcutFileBaseUrlDraft,
      vectcutApiKey: vectcutApiKeyDraft
    },
    setters: {
      setLobsterKey: setLobsterKeyDraft,
      setXhsFeishuAppId: setXhsFeishuAppIdDraft,
      setXhsFeishuAppSecret: setXhsFeishuAppSecretDraft,
      setXhsFeishuAppToken: setXhsFeishuAppTokenDraft,
      setXhsFeishuTableId: setXhsFeishuTableIdDraft,
      setCopywritingModelBaseUrl: setCopywritingModelBaseUrlDraft,
      setCopywritingModelModelId: setCopywritingModelModelIdDraft,
      setCopywritingModelApiKey: setCopywritingModelApiKeyDraft,
      setImageModelBaseUrl: setImageModelBaseUrlDraft,
      setImageModelModelId: setImageModelModelIdDraft,
      setImageModelApiKey: setImageModelApiKeyDraft,
      setVideoModelBaseUrl: setVideoModelBaseUrlDraft,
      setVideoModelModelId: setVideoModelModelIdDraft,
      setVideoModelApiKey: setVideoModelApiKeyDraft,
      setDigitalHumanVolcAccessKey: setDigitalHumanVolcAccessKeyDraft,
      setDigitalHumanVolcSecretKey: setDigitalHumanVolcSecretKeyDraft,
      setDigitalHumanQiniuAccessKey: setDigitalHumanQiniuAccessKeyDraft,
      setDigitalHumanQiniuSecretKey: setDigitalHumanQiniuSecretKeyDraft,
      setVideoAnalyzerBaseUrl: setVideoAnalyzerBaseUrlDraft,
      setVideoAnalyzerModelId: setVideoAnalyzerModelIdDraft,
      setVideoAnalyzerApiKey: setVideoAnalyzerApiKeyDraft,
      setReplicationBriefBaseUrl: setReplicationBriefBaseUrlDraft,
      setReplicationBriefModelId: setReplicationBriefModelIdDraft,
      setReplicationBriefApiKey: setReplicationBriefApiKeyDraft,
      setVectcutBaseUrl: setVectcutBaseUrlDraft,
      setVectcutFileBaseUrl: setVectcutFileBaseUrlDraft,
      setVectcutApiKey: setVectcutApiKeyDraft
    },
    onSaveLobsterKey: saveLobsterKey,
    onSaveXhsFeishuConfig: saveXhsFeishuConfig,
    onResetXhsFeishuConfig: resetXhsFeishuSettingsDrafts,
    onSaveCopywritingConfig: saveCopywritingConfig,
    onResetCopywritingConfig: resetCopywritingSettingsDrafts,
    onSaveImageConfig: saveImageConfig,
    onResetImageConfig: resetImageSettingsDrafts,
    onSaveVideoConfig: saveVideoConfig,
    onResetVideoConfig: resetVideoSettingsDrafts,
    onSaveDigitalHumanConfig: saveDigitalHumanConfig,
    onResetDigitalHumanConfig: resetDigitalHumanSettingsDrafts,
    onSaveDouyinRuntimeConfig: saveDouyinRuntimeConfig,
    onResetDouyinRuntimeConfig: resetDouyinRuntimeSettingsDrafts,
    onRevealSecret: revealConfigSecret,
    pickWorkspaceDirectory,
    exportDiagnostics
  } satisfies ComponentProps<typeof SettingsPanels>;

  return (
    <div className={"shell openclaw-theme" + (isConversationView ? " conversation-shell" : "") + (viewMode === "experts" ? " conversation-shell-experts" : "") + (isSidebarCollapsed ? " sidebar-collapsed" : "")}>
      <TopBar onMinimize={minimizeWindow} onMaximize={maximizeWindow} onClose={closeWindow} />
      <AppSidebar
        viewMode={viewMode}
        prompt={prompt}
        activeProjectId={activeProject?.id}
        ui={ui}
        showBindEntry={showBindEntry}
        projectActionPending={projectActionPending}
        sessions={scopedSessions}
        activeSessionId={visibleSessionId ?? ""}
        sidebarSessionTitles={sidebarSessionTitles}
        sendPhase={sendPhase}
        activeStreamSessionId={activeStreamRef.current?.sessionId}
        sidebarSessionLabel={sidebarSessionLabel}
        formatSessionTitle={formatSessionTitle}
        isCollapsed={isSidebarCollapsed}
        onToggleCollapsed={() => setIsSidebarCollapsed((current) => !current)}
        navIcon={(kind) => <NavIcon kind={kind} />}
        onNavSelection={handleNavSelection}
        sidebarNewSessionAction={sidebarNewSessionAction}
        sidebarExpertEntries={sidebarExpertEntries}
        expandedCategories={expandedCategories}
        onToggleCategory={toggleCategory}
        onExpertSelect={handleExpertSelect}
        renderCategoryIcon={(kind) => <ExpertCategoryIcon kind={kind} />}
        renderExpertIcon={renderExpertIcon}
        onOpenSession={openSession}
        onCloseSession={(sessionId) => void closeProjectSession(sessionId)}
      />
      <div className={"main-shell" + (isConversationView ? " conversation-main-layout" : "") + (viewMode === "settings" ? " settings-main-shell" : "")}>
        {infoText ? <div className="notice toast-notice" role="status" aria-live="polite">{infoText}</div> : null}
        {errorText ? <div className="notice error" role="alert">{errorText}</div> : null}
        <main className={"content-area" + (isConversationView ? " conversation-content-area" : "")}>
          {isConversationView ? (
            <ConversationWorkspaceView {...conversationWorkspaceProps} />
          ) : null}
          {viewMode === "plugins" ? (
            <PluginsView
              pluginCards={pluginCards}
              noPluginsLabel={ui.noPlugins}
              getPluginCopy={getPluginCopy}
              getPluginTone={getPluginTone}
              getPluginStatusLabel={getPluginStatusLabel}
              renderStatusChip={(tone, label) => <StatusChip tone={tone}>{label}</StatusChip>}
              renderPluginIcon={() => <LobsterClawIcon />}
            />
          ) : null}
          {viewMode === "knowledge" ? (
            <KnowledgeView />
          ) : null}
          {viewMode === "tasks" ? (
            <TaskPanelView />
          ) : null}
          {viewMode === "automation" ? (
            <AutomationTasksView projects={projects} />
          ) : null}
          {viewMode === "settings" ? (
            <SettingsView statusHint={settingsStatusHint}>
              <SettingsPanels {...settingsPanelsProps} />
            </SettingsView>
          ) : null}
        </main>
      </div>
      {showStartupOverlay ? (
        <div className={"startup-overlay" + (chatLaunchState === "error" ? " error" : "")} role="dialog" aria-modal="true" aria-live="polite">
          <div className="startup-overlay-panel">
            <div className="startup-overlay-copy">
              <div className="startup-overlay-brand-line">
                <div className="startup-overlay-mark-shell" aria-hidden="true">
                  <img src={brandIcon} alt="" className="startup-overlay-logo" />
                </div>
                <div className="startup-overlay-brand-copy">
                  <h1>{startupCurtainCopy.brandTitle}</h1>
                  <p className="startup-overlay-tagline">{startupCurtainCopy.brandTagline}</p>
                </div>
              </div>
            </div>
            <div className="startup-overlay-body">
              {chatLaunchState !== "error" ? (
                <div className="startup-overlay-progress" aria-hidden="true">
                  <span style={{ width: String(Math.round(visualStartupProgress * 100)) + "%" }} />
                </div>
              ) : null}
              <div className="startup-overlay-status">
                <strong>{startupCurtainStatus}</strong>
                {startupCurtainFootnote ? <span>{startupCurtainFootnote}</span> : null}
                {chatLaunchState === "error" && startupMessage ? (
                  <p className="startup-overlay-detail">{startupMessage}</p>
                ) : null}
              </div>
              {chatLaunchState === "error" ? (
                <div className="button-row startup-overlay-actions">
                  <button type="button" disabled={refreshing} onClick={() => void retryStartup()}>{refreshing ? ui.preparing : ui.startupRetry}</button>
                  <button type="button" className="secondary" onClick={() => setViewMode("settings")}>{ui.openSettings}</button>
                  <button type="button" className="secondary" onClick={() => void exportDiagnostics()}>{ui.export}</button>
                </div>
              ) : null}
            </div>
          </div>
        </div>
      ) : null}
    </div>
  );
}
