
/* ===== components/Composer.tsx · SHA256 3061ed7c3635505bc7fad0451576eee764b7c11186aa9906565a021e5b318076 ===== */
import { HoverInfo } from "./ui/HoverInfo";
import { useState, useRef, useEffect, useCallback, useLayoutEffect, useMemo, useId } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Brain, Check, ChevronDown, CircleAlert, Feather, Hand, Lightbulb, Loader2, ShieldHalf } from "lucide-react";
import { useTranslation } from "react-i18next";
import { ContextCapacityIndicator } from "./ContextCapacityIndicator";
import { ComposerSuggestPanel } from "./ComposerSuggestPanel";
import { ModelThinkingSelector } from "./ModelThinkingSelector";
import { InputSurface } from "./ui/InputSurface";
import { useMessageSendMotion } from "./ui/MessageSendMotionContext";
import { ComposerAttachButton } from "./ComposerAttachButton";
import { ComposerAttachmentList } from "./ComposerAttachmentList";
import { MorphingMenuShell } from "./ui/MorphingMenuShell";
import { MOTION } from "./ui/motion";
import { MENU_ITEM_ATTR, SlidingMenuList } from "./ui/SlidingMenuList";
import { cn } from "./shadcn/utils";
import {
  SETTINGS_CHANGED_EVENT,
  attachmentIngest,
  attachmentRemove,
  getActivePermissionPreset,
  getCommands,
  getPermissionPresetVisibility,
  getSettings,
  listSkills,
  searchProjectFiles,
  setActivePermissionPreset,
  setChatModel,
  setThinkingDepth,
} from "../api";
import { message } from "./message";
import type {
  Command,
  ComposerAttachment,
  ComposerMention,
  ComposerSkillSelection,
  ContextUsageSnapshot,
  ModelCapability,
  ProjectFileEntry,
  Skill,
} from "../types";

const LONG_TEXT_ATTACHMENT_THRESHOLD = 8 * 1024;
const MAX_COMPOSER_ATTACHMENTS = 5;
const COMPOSER_TEXTAREA_MIN_HEIGHT = 60;
const COMPOSER_TEXTAREA_DEFAULT_MAX_HEIGHT = 300;
const SKILL_MARKER = "\uE000";
const MENTION_MARKER = "\uE001";

/** A slash-dropdown row. `insertText` is used for built-in slash commands;
 *  `skillName` / `skillId` mark a row as a skill picker. */
type SlashChoice = Command & { insertText?: string; skillId?: string; skillName?: string };
type SlashSection = {
  key: string;
  label: string;
  items: SlashChoice[];
};
type SlashToken = {
  start: number;
  end: number;
  query: string;
};
type AtToken = SlashToken;
type SelectionRange = {
  start: number;
  end: number;
};
type AtChoice =
  | {
      id: "add-files";
      type: "add-files";
      title: string;
      description: string;
      icon: "paperclip";
    }
  | {
      id: "goal";
      type: "goal";
      title: string;
      description: string;
      icon: "bullseye";
    }
  | {
      id: "plan-mode";
      type: "plan-mode";
      title: string;
      description: string;
      icon: "list-check";
    }
  | {
      id: string;
      type: "file";
      title: string;
      description: string;
      icon: "file" | "folder";
      entry: ProjectFileEntry;
    }
  | {
      id: string;
      type: "skill";
      title: string;
      description: string;
      icon: "cube";
      skill: Skill;
    };
type AtSection = {
  key: string;
  label: string;
  items: AtChoice[];
};

/** Map composer dropdown option id -> backend permission preset label. */
const OPTION_TO_PRESET: Record<string, string> = {
  default: "default",
  auto: "auto_review",
  full: "full_access",
};
const PRESET_TO_OPTION: Record<string, string> = {
  default: "default",
  auto_review: "auto",
  full_access: "full",
};

function stripSkillMarkers(value: string): string {
  return value.replace(/[\uE000\uE001]/g, "");
}

function countSkillMarkers(value: string): number {
  return (value.match(/\uE000/g) ?? []).length;
}

function countMentionMarkers(value: string): number {
  return (value.match(/\uE001/g) ?? []).length;
}

function normalizeSelectionRange(start: number, end: number): SelectionRange | null {
  if (start === end) return null;
  return {
    start: Math.min(start, end),
    end: Math.max(start, end),
  };
}

function rangesOverlap(range: SelectionRange | null, start: number, end: number): boolean {
  return Boolean(range && start < range.end && end > range.start);
}

function findActiveSlashToken(value: string, cursor: number): SlashToken | null {
  const beforeCursor = value.slice(0, cursor);
  const match = beforeCursor.match(/(^|[\s\uE000\uE001])\/([a-zA-Z0-9_:-]*)$/);
  if (!match || match.index === undefined) return null;
  const start = match.index + match[1].length;
  const after = value.slice(start + 1);
  const fullCommand = after.match(/^[a-zA-Z0-9_:-]*/)?.[0] ?? "";
  if (cursor > start + 1 + fullCommand.length) return null;
  return {
    start,
    end: start + 1 + fullCommand.length,
    query: fullCommand,
  };
}

function findActiveAtToken(value: string, cursor: number): AtToken | null {
  const beforeCursor = value.slice(0, cursor);
  const match = beforeCursor.match(/(^|[\s\uE000\uE001])@([^\s\uE000\uE001]*)$/);
  if (!match || match.index === undefined) return null;
  const start = match.index + match[1].length;
  const after = value.slice(start + 1);
  const fullMention = after.match(/^[^\s\uE000\uE001]*/)?.[0] ?? "";
  if (cursor > start + 1 + fullMention.length) return null;
  return {
    start,
    end: start + 1 + fullMention.length,
    query: fullMention,
  };
}

function mentionLabel(mention: ComposerMention): string {
  if (mention.kind === "plan_mode") return "计划模式";
  if (mention.kind === "goal") return "目标";
  return mention.relPath || mention.name;
}

interface Props {
  value: string;
  onChange: (v: string) => void;
  onSubmit: (
    attachments?: ComposerAttachment[],
    selectedSkills?: ComposerSkillSelection[],
    mentions?: ComposerMention[],
    displayText?: string,
  ) => void;
  placeholder?: string;
  /** True while a run is streaming: disables submit and shows a busy button. */
  busy?: boolean;
  /** Stop the in-flight run (turns the busy button into a stop button). */
  onStop?: () => void;
  /** Cancellation was accepted and the backend is draining the run. */
  stopping?: boolean;
  /** True when the current session is in read-only Plan mode. */
  planMode?: boolean;
  /** Active project path used for @ file/folder search. */
  activeProjectPath?: string | null;
  /** Optional footer content rendered seamlessly at the bottom of the composer. */
  footer?: React.ReactNode;
  /** Latest backend context-usage snapshot for the current run/session. */
  contextUsage?: ContextUsageSnapshot | null;
  /** Fallback prompt tokens from persisted/live usage when context_usage was missed. */
  contextUsageFallbackTokens?: number;
  /** Maximum auto-expanded textarea height in pixels. */
  textareaMaxHeight?: number;
  /** Increment to force-close composer toolbar overlays (model / approval). */
  overlayCloseSignal?: number;
  /** Fired when any composer toolbar overlay opens or closes. */
  onOverlayOpenChange?: (open: boolean) => void;
}

export function Composer({
  value,
  onChange,
  onSubmit,
  placeholder,
  busy = false,
  onStop,
  stopping = false,
  planMode = false,
  activeProjectPath = null,
  footer,
  contextUsage = null,
  contextUsageFallbackTokens = 0,
  textareaMaxHeight = COMPOSER_TEXTAREA_DEFAULT_MAX_HEIGHT,
  overlayCloseSignal = 0,
  onOverlayOpenChange,
}: Props) {
  const { t } = useTranslation();
  const attachmentLimitMessage = t("composer.attachments.limit", {
    count: MAX_COMPOSER_ATTACHMENTS,
  });
  const [isModelDropdownOpen, setIsModelDropdownOpen] = useState(false);
  const [isApprovalDropdownOpen, setIsApprovalDropdownOpen] = useState(false);
  const [isContextPopoverOpen, setIsContextPopoverOpen] = useState(false);
  const dropdownRef = useRef<HTMLDivElement>(null);
  const approvalDropdownRef = useRef<HTMLDivElement>(null);
  const approvalMorphLayoutId = useId().replace(/:/g, "");
  const suggestId = useId();
  const lastOverlayCloseSignal = useRef(overlayCloseSignal);
  const lastReportedOverlayOpen = useRef(false);
  const fileInputRef = useRef<HTMLInputElement>(null);
  const textareaRef = useRef<HTMLTextAreaElement>(null);
  const mirrorRef = useRef<HTMLDivElement>(null);
  const composerRef = useRef<HTMLDivElement>(null);
  const messageSendMotion = useMessageSendMotion();
  const [attachments, setAttachments] = useState<ComposerAttachment[]>([]);
  // Real discovered models + the active chat model, loaded from the backend
  // settings (populated by API-key validation at login).
  const [models, setModels] = useState<string[]>([]);
  const [selectedModel, setSelectedModel] = useState<string>("");
  // Resolved capability (context window / max output) per discovered model,
  // straight from the backend resolver the runtime itself uses. Keeps the
  // capacity indicator from guessing from the model id.
  const [modelCapabilities, setModelCapabilities] = useState<ModelCapability[]>([]);
  const [selectedThinking, setSelectedThinking] = useState<"simple" | "medium" | "deep">("medium");
  const [switching, setSwitching] = useState(false);
  const [slashResults, setSlashResults] = useState<SlashChoice[]>([]);
  const [slashSelected, setSlashSelected] = useState(0);
  const [atSections, setAtSections] = useState<AtSection[]>([]);
  const [atSelected, setAtSelected] = useState(0);
  const [selectedSkills, setSelectedSkills] = useState<ComposerSkillSelection[]>([]);
  const [selectedMentions, setSelectedMentions] = useState<ComposerMention[]>([]);
  const [draftValue, setDraftValue] = useState(value);
  const [cursorPos, setCursorPos] = useState(value.length);
  const [selectionRange, setSelectionRange] = useState<SelectionRange | null>(null);
  const [editorFocused, setEditorFocused] = useState(false);
  const pendingDraftValueRef = useRef(value);
  // Skill registry mirror, used to populate slash candidates (Channel C v1).
  // Reloads on `SETTINGS_CHANGED_EVENT` so install/uninstall mid-session is
  // reflected without a chat-page refresh.
  const [skills, setSkills] = useState<Skill[]>([]);

  const reloadSettings = useCallback((cancelled?: () => boolean) => {
    getSettings()
      .then((s) => {
        if (cancelled?.() || !s) return;
        setModels(s.available_models);
        setModelCapabilities(s.model_capabilities ?? []);
        setSelectedModel(s.chat_model);
        setSelectedThinking(s.thinking_depth ?? "medium");
      })
      .catch(() => {
        /* browser preview / uninitialized: leave the list empty */
      });
  }, []);

  useEffect(() => {
    let cancelled = false;
    reloadSettings(() => cancelled);
    const onSettingsChanged = () => reloadSettings();
    window.addEventListener(SETTINGS_CHANGED_EVENT, onSettingsChanged);
    return () => {
      cancelled = true;
      window.removeEventListener(SETTINGS_CHANGED_EVENT, onSettingsChanged);
    };
  }, [reloadSettings]);

  // Load the skill registry for the slash-command candidate list, and refresh
  // when settings change (catches install / uninstall / reload mid-session).
  useEffect(() => {
    let cancelled = false;
    const load = () => {
      listSkills()
        .then((list) => {
          if (!cancelled) setSkills(list);
        })
        .catch(() => {
          /* browser preview / uninitialized: keep prior list */
        });
    };
    load();
    const onSettingsChanged = () => load();
    window.addEventListener(SETTINGS_CHANGED_EVENT, onSettingsChanged);
    return () => {
      cancelled = true;
      window.removeEventListener(SETTINGS_CHANGED_EVENT, onSettingsChanged);
    };
  }, []);

  useEffect(() => {
    if (value === pendingDraftValueRef.current) return;
    if (stripSkillMarkers(draftValue) === value) {
      pendingDraftValueRef.current = value;
      return;
    }
    setDraftValue(value);
    setCursorPos(value.length);
    setSelectionRange(null);
    setSlashResults([]);
    setSlashSelected(0);
    setSelectedSkills([]);
    setAtSections([]);
    setAtSelected(0);
    setSelectedMentions([]);
    pendingDraftValueRef.current = value;
  }, [draftValue, value]);

  const chooseModel = async (id: string) => {
    if (id === selectedModel || switching) return;
    const prev = selectedModel;
    setSelectedModel(id); // optimistic
    setSwitching(true);
    try {
      const view = await setChatModel(id);
      setSelectedModel(view.chat_model);
      message.success(t("composer.modelSwitched", { model: view.chat_model }));
    } catch (e) {
      setSelectedModel(prev); // revert on failure
      message.error(t("composer.modelSwitchFailed"));
      console.error("set_chat_model failed:", e);
    } finally {
      setSwitching(false);
    }
  };

  const THINKING_OPTIONS = [
    { id: "simple", label: "composer.thinkingSimple", icon: Feather },
    { id: "medium", label: "composer.thinkingMedium", icon: Lightbulb },
    { id: "deep", label: "composer.thinkingDeep", icon: Brain },
  ] as const;

  const chooseThinking = async (id: "simple" | "medium" | "deep") => {
    if (id === selectedThinking) return;
    const prev = selectedThinking;
    setSelectedThinking(id);
    try {
      const view = await setThinkingDepth(id);
      setSelectedThinking(view.thinking_depth ?? id);
    } catch (e) {
      setSelectedThinking(prev);
      message.error(t("composer.thinkingSwitchFailed"));
      console.error("set_thinking_depth failed:", e);
    }
  };

  const ALL_APPROVAL_OPTIONS = [
    { id: "default", label: "composer.defaultPermission", desc: "composer.defaultPermissionDesc", icon: Hand },
    { id: "auto", label: "composer.autoReview", desc: "composer.autoReviewDesc", icon: ShieldHalf },
    { id: "full", label: "composer.fullAccess", desc: "composer.fullAccessDesc", icon: CircleAlert },
  ];

  const [visibleOptions, setVisibleOptions] = useState(ALL_APPROVAL_OPTIONS);
  const [selectedApproval, setSelectedApproval] = useState(ALL_APPROVAL_OPTIONS[0]);

  // Load the current backend permission preset and reflethe dropdown.
  useEffect(() => {
    let cancelled = false;
    getActivePermissionPreset()
      .then((preset) => {
        if (cancelled) return;
        const optId = PRESET_TO_OPTION[preset] ?? "default";
        const opt = ALL_APPROVAL_OPTIONS.find((o) => o.id === optId);
        if (opt) setSelectedApproval(opt);
      })
      .catch(() => {
        /* browser preview / uninitialized */
      });
    return () => {
      cancelled = true;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Apply the chosen permission preset to the backend, then reflect it locally.
  const chooseApproval = async (opt: (typeof ALL_APPROVAL_OPTIONS)[number]) => {
    setIsApprovalDropdownOpen(false);
    const prev = selectedApproval;
    setSelectedApproval(opt); // optimistic
    try {
      await setActivePermissionPreset((OPTION_TO_PRESET[opt.id] ?? "default") as any);
    } catch (e) {
      setSelectedApproval(prev); // revert on failure
      message.error(t("composer.approvalSwitchFailed"));
      console.error("set_active_permission_preset failed:", e);
    }
  };

  useEffect(() => {
    const updateVisibleOptions = async () => {
      try {
        const vis = await getPermissionPresetVisibility();
        const newOptions: typeof ALL_APPROVAL_OPTIONS[number][] = [];
        if (vis.default_enabled) newOptions.push(ALL_APPROVAL_OPTIONS[0]);
        if (vis.auto_review_enabled) newOptions.push(ALL_APPROVAL_OPTIONS[1]);
        if (vis.full_access_enabled) newOptions.push(ALL_APPROVAL_OPTIONS[2]);

        setVisibleOptions(newOptions);

        setSelectedApproval((prev) => {
          if (newOptions.find((o) => o.id === prev.id)) return prev;
          return newOptions.length > 0 ? newOptions[0] : prev;
        });
      } catch {
        /* fallback: show all */
      }
    };

    updateVisibleOptions();
    window.addEventListener("approvalMenuChanged", updateVisibleOptions);
    return () => window.removeEventListener("approvalMenuChanged", updateVisibleOptions);
  }, []);

  const slashSections = useMemo<SlashSection[]>(() => {
    const systemCommands = slashResults.filter((cmd) => cmd.skillName === undefined);
    const skillsGroup = slashResults.filter((cmd) => cmd.skillName !== undefined);
    const sections: SlashSection[] = [];
    if (systemCommands.length > 0) {
      sections.push({
        key: "system",
        label: t("composer.systemCommands", { defaultValue: "System Commands" }),
        items: systemCommands,
      });
    }
    if (skillsGroup.length > 0) {
      sections.push({
        key: "skills",
        label: t("composer.skillsGroup", { defaultValue: "Skills" }),
        items: skillsGroup,
      });
    }
    return sections;
  }, [slashResults, t]);

  const slashEntries = useMemo(
    () => slashSections.flatMap((section) => section.items),
    [slashSections],
  );

  const atEntries = useMemo(
    () => atSections.flatMap((section) => section.items),
    [atSections],
  );

  const activeSlashToken = useMemo(
    () => findActiveSlashToken(draftValue, cursorPos),
    [cursorPos, draftValue],
  );

  const activeAtToken = useMemo(
    () => findActiveAtToken(draftValue, cursorPos),
    [cursorPos, draftValue],
  );

  const slashOpen = Boolean((draftValue.startsWith("/") || activeSlashToken) && slashEntries.length > 0);
  const atOpen = Boolean(!slashOpen && activeAtToken && atEntries.length > 0);

  const resizeTextarea = useCallback(() => {
    const textarea = textareaRef.current;
    if (!textarea) return;

    textarea.style.height = "auto";
    const nextHeight = Math.min(
      Math.max(textarea.scrollHeight, COMPOSER_TEXTAREA_MIN_HEIGHT),
      textareaMaxHeight,
    );
    textarea.style.height = `${nextHeight}px`;
    textarea.style.overflowY = textarea.scrollHeight > textareaMaxHeight ? "auto" : "hidden";
  }, [textareaMaxHeight]);

  useLayoutEffect(() => {
    resizeTextarea();
  }, [draftValue, resizeTextarea]);

  useEffect(() => {
    if (draftValue.startsWith("/")) {
      const body = draftValue.slice(1);
      const firstSpace = body.search(/\s/);
      const commandName = firstSpace >= 0 ? body.slice(0, firstSpace) : body;
      const argQuery = firstSpace >= 0 ? body.slice(firstSpace + 1).trim().toLowerCase() : "";
      const exactArgCommand = firstSpace < 0 && (commandName === "model" || commandName === "thinking");

      if (commandName === "model" && (firstSpace >= 0 || exactArgCommand)) {
        const choices = models
          .filter((id) => id.toLowerCase().includes(argQuery))
          .map((id) => ({
            id: `slash-arg.model.${id}`,
            title: id,
            description: id === selectedModel ? "当前模型" : "切换到这个 DeepSeek 模型",
            category: "模型",
            shortcut: null,
            insertText: `/model ${id} `,
          }));
        setSlashResults(choices);
        setSlashSelected(0);
        return;
      }

      if (commandName === "thinking" && (firstSpace >= 0 || exactArgCommand)) {
        const choices = [
          { id: "simple", title: "simple", description: "简单思考，响应更快", category: "思考" },
          { id: "medium", title: "medium", description: "中度思考，默认平衡模式", category: "思考" },
          { id: "deep", title: "deep", description: "深度思考，给复杂任务更多推理预留", category: "思考" },
        ]
          .filter((opt) => opt.title.includes(argQuery) || opt.description.includes(argQuery))
          .map((opt) => ({
            ...opt,
            id: `slash-arg.thinking.${opt.id}`,
            shortcut: null,
            insertText: `/thinking ${opt.id} `,
          }));
        setSlashResults(choices);
        setSlashSelected(0);
        return;
      }
    }

    if (!activeSlashToken) {
      setSlashResults([]);
      setSlashSelected(0);
      return;
    }

    let cancelled = false;
    const q = activeSlashToken.query.trim().toLowerCase();
    const selectedSkillIds = new Set(selectedSkills.map((skill) => skill.id));
    const skillChoices: SlashChoice[] = skills
      .filter(
        (sk) =>
          !selectedSkillIds.has(sk.id) &&
          (!q ||
            sk.id.toLowerCase().includes(q) ||
            sk.name.toLowerCase().includes(q)),
      )
      .sort((a, b) => a.id.localeCompare(b.id))
      .slice(0, 8)
      .map((sk) => ({
        id: `slash-skill.${sk.id}`,
        title: `/${sk.id}`,
        description: sk.description,
        category: "Skill",
        shortcut: null,
        skillId: sk.id,
        skillName: sk.name,
      }));
    getCommands("")
      .then((commands) => {
        if (cancelled) return;
        const slash = commands.filter((c) => {
          if (!c.id.startsWith("slash.")) return false;
          const name = c.title.replace(/^\//, "").toLowerCase();
          return !q || name.includes(q);
        });
        setSlashResults([...slash, ...skillChoices]);
        setSlashSelected(0);
      })
      .catch(() => {
        if (!cancelled) {
          setSlashResults(skillChoices);
          setSlashSelected(0);
        }
      });
    return () => {
      cancelled = true;
    };
  }, [activeSlashToken, draftValue, models, selectedModel, selectedSkills, skills]);

  useEffect(() => {
    if (!activeAtToken || slashOpen) {
      setAtSections([]);
      setAtSelected(0);
      return;
    }

    let cancelled = false;
    const q = activeAtToken.query.trim().toLowerCase();
    const matches = (value: string) => !q || value.toLowerCase().includes(q);
    const sections: AtSection[] = [];
    const addChoices: AtChoice[] = [
      {
        id: "add-files",
        type: "add-files",
        title: "文件和文件夹",
        description: q ? "继续输入以搜索项目文件和文件夹" : "",
        icon: "paperclip",
      },
      {
        id: "goal",
        type: "goal",
        title: "目标",
        description: "设置要持续追求的目标",
        icon: "bullseye",
      },
      {
        id: "plan-mode",
        type: "plan-mode",
        title: "计划模式",
        description: "开启计划模式",
        icon: "list-check",
      },
    ];
    const addItems = addChoices.filter((item) => matches(item.title) || matches(item.description));
    if (addItems.length > 0) {
      sections.push({ key: "add", label: "添加", items: addItems });
    }

    const selectedSkillIds = new Set(selectedSkills.map((skill) => skill.id));
    const skillItems: AtChoice[] = skills
      .filter(
        (skill) =>
          !selectedSkillIds.has(skill.id) &&
          (matches(skill.id) || matches(skill.name) || matches(skill.description ?? "")),
      )
      .sort((a, b) => a.id.localeCompare(b.id))
      .slice(0, 6)
      .map((skill) => ({
        id: `at-skill.${skill.id}`,
        type: "skill",
        title: skill.name || skill.id,
        description: skill.description,
        icon: "cube",
        skill,
      }));
    if (skillItems.length > 0) {
      sections.push({ key: "plugins", label: "插件", items: skillItems });
    } else if (!q) {
      sections.push({
        key: "plugins",
        label: "插件",
        items: [
          {
            id: "add-files",
            type: "add-files",
            title: "正在加载插件…",
            description: "",
            icon: "paperclip",
          },
        ],
      });
    }

    if (!q) {
      sections.push({
        key: "files",
        label: "文件和任务",
        items: [
          {
            id: "add-files",
            type: "add-files",
            title: "输入以搜索文件或任务",
            description: "",
            icon: "paperclip",
          },
        ],
      });
      setAtSections(sections);
      setAtSelected(0);
      return;
    }

    const timer = window.setTimeout(() => {
      searchProjectFiles(activeProjectPath, q, 24).then((result) => {
        if (cancelled) return;
        const mentionedPaths = new Set(
          selectedMentions.flatMap((mention) =>
            mention.kind === "file" || mention.kind === "folder" ? [mention.path] : [],
          ),
        );
        const fileItems: AtChoice[] = result.entries
          .filter((entry) => !mentionedPaths.has(entry.path))
          .slice(0, 12)
          .map((entry) => ({
            id: `at-file.${entry.path}`,
            type: "file",
            title: entry.name,
            description: entry.rel_path,
            icon: entry.is_dir ? "folder" : "file",
            entry,
          }));
        if (fileItems.length > 0) {
          sections.push({ key: "files", label: "文件和任务", items: fileItems });
        }
        setAtSections(sections);
        setAtSelected(0);
      })
      .catch(() => {
        if (!cancelled) {
          setAtSections(sections);
          setAtSelected(0);
        }
      });
    }, 80);

    return () => {
      cancelled = true;
      window.clearTimeout(timer);
    };
  }, [activeAtToken, activeProjectPath, selectedMentions, selectedSkills, skills, slashOpen]);

  const chooseSlash = (cmd: SlashChoice) => {
    const replaceWholeRootSlash = !activeSlashToken && draftValue.startsWith("/");
    const rawStart = activeSlashToken
      ? activeSlashToken.start
      : replaceWholeRootSlash
      ? 0
      : cursorPos;
    const rawEnd = activeSlashToken
      ? activeSlashToken.end
      : replaceWholeRootSlash
      ? draftValue.length
      : cursorPos;
    const prefix = draftValue.slice(0, rawStart);
    const suffix = draftValue.slice(rawEnd);

    if (cmd.skillName !== undefined) {
      if (selectedSkills.some((skill) => skill.id === (cmd.skillId ?? cmd.id))) {
        setSlashResults([]);
        setSlashSelected(0);
        requestAnimationFrame(() => textareaRef.current?.focus());
        return;
      }
      const nextValue = `${prefix}${SKILL_MARKER}${suffix}`;
      const insertedSkill = {
        id: cmd.skillId ?? cmd.id,
        name: cmd.skillName,
      };
      const insertAt = countSkillMarkers(prefix);
      setSelectedSkills((prev) => {
        const next = [...prev];
        next.splice(insertAt, 0, insertedSkill);
        return next;
      });
      setDraftValue(nextValue);
      pendingDraftValueRef.current = stripSkillMarkers(nextValue);
      onChange(stripSkillMarkers(nextValue));
      setCursorPos(rawStart + 1);
      setSelectionRange(null);
      requestAnimationFrame(() => {
        textareaRef.current?.setSelectionRange(rawStart + 1, rawStart + 1);
        textareaRef.current?.focus();
      });
    } else {
      const insertText = cmd.insertText ?? `${cmd.title} `;
      const nextValue = `${prefix}${insertText}${suffix}`;
      setDraftValue(nextValue);
      pendingDraftValueRef.current = stripSkillMarkers(nextValue);
      onChange(stripSkillMarkers(nextValue));
      setCursorPos(rawStart + insertText.length);
      setSelectionRange(null);
      requestAnimationFrame(() => {
        const caret = rawStart + insertText.length;
        textareaRef.current?.setSelectionRange(caret, caret);
        textareaRef.current?.focus();
      });
    }
    setSlashResults([]);
    setSlashSelected(0);
  };

  const insertSkillMarker = (
    rawStart: number,
    rawEnd: number,
    skill: ComposerSkillSelection,
  ) => {
    if (selectedSkills.some((item) => item.id === skill.id)) {
      requestAnimationFrame(() => textareaRef.current?.focus());
      return;
    }
    const prefix = draftValue.slice(0, rawStart);
    const suffix = draftValue.slice(rawEnd);
    const spacer = suffix && !/^\s/.test(suffix) ? " " : "";
    const nextValue = `${prefix}${SKILL_MARKER}${spacer}${suffix}`;
    const insertAt = countSkillMarkers(prefix);
    setSelectedSkills((prev) => {
      const next = [...prev];
      next.splice(insertAt, 0, skill);
      return next;
    });
    setDraftValue(nextValue);
    pendingDraftValueRef.current = stripSkillMarkers(nextValue);
    onChange(stripSkillMarkers(nextValue));
    const caret = rawStart + 1 + spacer.length;
    setCursorPos(caret);
    setSelectionRange(null);
    requestAnimationFrame(() => {
      textareaRef.current?.setSelectionRange(caret, caret);
      textareaRef.current?.focus();
    });
  };

  const insertMentionMarker = (
    rawStart: number,
    rawEnd: number,
    mention: ComposerMention,
  ) => {
    const prefix = draftValue.slice(0, rawStart);
    const suffix = draftValue.slice(rawEnd);
    const spacer = suffix && !/^\s/.test(suffix) ? " " : " ";
    const nextValue = `${prefix}${MENTION_MARKER}${spacer}${suffix}`;
    const insertAt = countMentionMarkers(prefix);
    setSelectedMentions((prev) => {
      const next = [...prev];
      next.splice(insertAt, 0, mention);
      return next;
    });
    setDraftValue(nextValue);
    pendingDraftValueRef.current = stripSkillMarkers(nextValue);
    onChange(stripSkillMarkers(nextValue));
    const caret = rawStart + 1 + spacer.length;
    setCursorPos(caret);
    setSelectionRange(null);
    requestAnimationFrame(() => {
      textareaRef.current?.setSelectionRange(caret, caret);
      textareaRef.current?.focus();
    });
  };

  const chooseAt = (choice: AtChoice) => {
    if (!activeAtToken) return;
    const rawStart = activeAtToken.start;
    const rawEnd = activeAtToken.end;

    if (choice.type === "add-files") {
      message.info("继续在 @ 后输入文件名或文件夹名即可搜索");
      setAtSelected(0);
      requestAnimationFrame(() => textareaRef.current?.focus());
      return;
    }

    if (choice.type === "goal") {
      const goal = window.prompt("请输入要持续追踪的目标");
      if (!goal?.trim()) {
        requestAnimationFrame(() => textareaRef.current?.focus());
        return;
      }
      insertMentionMarker(rawStart, rawEnd, {
        kind: "goal",
        text: goal.trim(),
      });
    } else if (choice.type === "plan-mode") {
      if (!selectedMentions.some((mention) => mention.kind === "plan_mode")) {
        insertMentionMarker(rawStart, rawEnd, { kind: "plan_mode" });
      }
    } else if (choice.type === "file") {
      insertMentionMarker(rawStart, rawEnd, {
        kind: choice.entry.is_dir ? "folder" : "file",
        path: choice.entry.path,
        relPath: choice.entry.rel_path,
        name: choice.entry.name,
        isDir: choice.entry.is_dir,
      });
    } else if (choice.type === "skill") {
      insertSkillMarker(rawStart, rawEnd, {
        id: choice.skill.id,
        name: choice.skill.name,
      });
    }

    setAtSections([]);
    setAtSelected(0);
  };

  useEffect(() => {
    const handleClickOutside = (e: MouseEvent) => {
      if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
        setIsModelDropdownOpen(false);
      }
      if (approvalDropdownRef.current && !approvalDropdownRef.current.contains(e.target as Node)) {
        setIsApprovalDropdownOpen(false);
      }
    };
    if (isModelDropdownOpen || isApprovalDropdownOpen) {
      document.addEventListener("mousedown", handleClickOutside);
    }
    return () => document.removeEventListener("mousedown", handleClickOutside);
  }, [isModelDropdownOpen, isApprovalDropdownOpen]);

  useEffect(() => {
    const open = isModelDropdownOpen || isApprovalDropdownOpen || isContextPopoverOpen;
    if (open === lastReportedOverlayOpen.current) return;
    lastReportedOverlayOpen.current = open;
    onOverlayOpenChange?.(open);
  }, [isModelDropdownOpen, isApprovalDropdownOpen, isContextPopoverOpen, onOverlayOpenChange]);

  useEffect(() => {
    if (overlayCloseSignal > lastOverlayCloseSignal.current) {
      lastOverlayCloseSignal.current = overlayCloseSignal;
      setIsModelDropdownOpen(false);
      setIsApprovalDropdownOpen(false);
      setIsContextPopoverOpen(false);
    }
  }, [overlayCloseSignal]);

  const submitWithAttachments = () => {
    const readyAttachments = attachments.filter((item) => item.status === "ready");
    const surface = composerRef.current?.querySelector<HTMLElement>("[data-message-composer-surface]");
    if (textareaRef.current && mirrorRef.current && surface) {
      messageSendMotion?.prepare(textareaRef.current, mirrorRef.current, surface, draftValue);
    }
    onSubmit(readyAttachments, selectedSkills, selectedMentions, draftValue);
    if (readyAttachments.length > 0) setAttachments([]);
    if (selectedSkills.length > 0) setSelectedSkills([]);
    if (selectedMentions.length > 0) setSelectedMentions([]);
    setDraftValue("");
    setCursorPos(0);
    setSelectionRange(null);
    pendingDraftValueRef.current = "";
  };

  const hasComposerContent = Boolean(
    stripSkillMarkers(draftValue).trim() || attachments.length > 0 || selectedSkills.length > 0 || selectedMentions.length > 0,
  );

  const addAttachment = (attachment: ComposerAttachment) => {
    setAttachments((prev) => [...prev, attachment]);
  };

  const showAttachmentLimitWarning = () => {
    message.warning(attachmentLimitMessage);
  };

  const removeAttachment = (id: string) => {
    setAttachments((prev) => prev.filter((item) => item.id !== id));
    void attachmentRemove(id).catch(() => {
      /* The UI removal should not be blocked by cleanup failures. */
    });
  };

  const attachmentId = () =>
    globalThis.crypto?.randomUUID?.() ?? `att_${Date.now()}_${Math.random().toString(16).slice(2)}`;

  const readFileAsDataUrl = (file: File) =>
    new Promise<string>((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(String(reader.result ?? ""));
      reader.onerror = () => reject(reader.error ?? new Error(t("composer.attachments.readFailed")));
      reader.readAsDataURL(file);
    });

  const readFileAsText = (file: File) =>
    new Promise<string>((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(String(reader.result ?? ""));
      reader.onerror = () => reject(reader.error ?? new Error(t("composer.attachments.readFailed")));
      reader.readAsText(file);
    });

  const addTextAttachment = async (text: string, source: ComposerAttachment["source"], name?: string) => {
    if (attachments.length >= MAX_COMPOSER_ATTACHMENTS) {
      showAttachmentLimitWarning();
      return;
    }
    const id = attachmentId();
    const attachmentName =
      name ?? `pasted-text-${new Date().toISOString().replace(/[:.]/g, "-")}.txt`;
    const size = new Blob([text]).size;
    addAttachment({
      id,
      kind: "text",
      name: attachmentName,
      mime: "text/plain",
      size,
      source,
      status: "processing",
    });
    try {
      const persisted = await attachmentIngest({
        id,
        session_id: null,
        kind: "text",
        name: attachmentName,
        mime: "text/plain",
        source,
        text,
      });
      markAttachmentReady(id, {
        size: persisted.size_bytes || size,
        extractedText: persisted.extracted_text ?? text,
        storageDir: persisted.storage_dir,
        originalPath: persisted.original_path ?? undefined,
        sha256: persisted.sha256 ?? undefined,
        backendMessage: persisted.message ?? undefined,
      });
    } catch (error) {
      markAttachmentError(id, error);
    }
  };

  const isTextLikeFile = (file: File) =>
    file.type.startsWith("text/") ||
    /\.(txt|md|markdown|json|log|yaml|yml|xml|ts|tsx|js|jsx|mjs|cjs|rs|go|py|java|kt|css|scss|html|htm|env|gitignore|npmrc|toml|ini|conf|cfg|sh|bash|zsh|fish|ps1|bat|cmd)$/i.test(
      file.name,
    );

  const markAttachmentReady = (id: string, patch: Partial<ComposerAttachment>) => {
    setAttachments((prev) =>
      prev.map((item) => (item.id === id ? { ...item, ...patch, status: "ready" } : item)),
    );
  };

  const markAttachmentError = (id: string, error: unknown) => {
    setAttachments((prev) =>
      prev.map((item) =>
        item.id === id
          ? { ...item, status: "error", error: error instanceof Error ? error.message : String(error) }
          : item,
      ),
    );
  };

  const addFiles = async (files: FileList | File[], source: ComposerAttachment["source"]) => {
    const incoming = Array.from(files);
    const remaining = MAX_COMPOSER_ATTACHMENTS - attachments.length;
    if (remaining <= 0) {
      showAttachmentLimitWarning();
      return;
    }
    const accepted = incoming.slice(0, remaining);
    if (incoming.length > accepted.length) {
      showAttachmentLimitWarning();
    }

    for (const file of accepted) {
      const id = attachmentId();
      const isImage = file.type.startsWith("image/");
      const isText = isTextLikeFile(file);
      const base: ComposerAttachment = {
        id,
        kind: isImage ? "image" : isText ? "text" : "file",
        name: file.name || (isImage ? "pasted-image.png" : "attachment"),
        mime: file.type || "application/octet-stream",
        size: file.size,
        source,
        status: "processing",
      };
      addAttachment(base);
      try {
        if (isImage) {
          const dataUrl = await readFileAsDataUrl(file);
          const persisted = await attachmentIngest({
            id,
            session_id: null,
            kind: "image",
            name: base.name,
            mime: base.mime,
            source,
            data_url: dataUrl,
          });
          const imagePath = persisted.original_path ?? undefined;
          markAttachmentReady(id, {
            dataUrl,
            size: persisted.size_bytes || file.size,
            storageDir: persisted.storage_dir,
            originalPath: imagePath,
            extractedText: persisted.extracted_text ?? undefined,
            sha256: persisted.sha256 ?? undefined,
            backendMessage: persisted.message ?? undefined,
          });
        } else if (isText) {
          const extractedText = await readFileAsText(file);
          const persisted = await attachmentIngest({
            id,
            session_id: null,
            kind: "text",
            name: base.name,
            mime: base.mime,
            source,
            text: extractedText,
          });
          markAttachmentReady(id, {
            kind: "text",
            size: persisted.size_bytes || file.size,
            extractedText: persisted.extracted_text ?? extractedText,
            storageDir: persisted.storage_dir,
            originalPath: persisted.original_path ?? undefined,
            sha256: persisted.sha256 ?? undefined,
            backendMessage: persisted.message ?? undefined,
          });
        } else {
          const dataUrl = await readFileAsDataUrl(file);
          const persisted = await attachmentIngest({
            id,
            session_id: null,
            kind: "file",
            name: base.name,
            mime: base.mime,
            source,
            data_url: dataUrl,
          });
          markAttachmentReady(id, {
            size: persisted.size_bytes || file.size,
            extractedText: persisted.extracted_text ?? undefined,
            storageDir: persisted.storage_dir,
            originalPath: persisted.original_path ?? undefined,
            sha256: persisted.sha256 ?? undefined,
            backendMessage: persisted.message ?? undefined,
          });
        }
      } catch (error) {
        markAttachmentError(id, error);
      }
    }
  };

  const onPaste = (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
    const text = event.clipboardData.getData("text/plain");
    const files = Array.from(event.clipboardData.files);
    if (files.length > 0) {
      event.preventDefault();
      void addFiles(files, "paste");
      return;
    }
    if (text.length > LONG_TEXT_ATTACHMENT_THRESHOLD) {
      event.preventDefault();
      void addTextAttachment(text, "paste");
    }
  };

  const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
    if (event.dataTransfer.files.length === 0) return;
    event.preventDefault();
    void addFiles(event.dataTransfer.files, "drop");
  };

  const syncCursorFromTextarea = () => {
    const textarea = textareaRef.current;
    if (!textarea) return;
    const start = textarea.selectionStart ?? textarea.value.length;
    const end = textarea.selectionEnd ?? start;
    setCursorPos(start);
    setSelectionRange(normalizeSelectionRange(start, end));
  };

  const handleEditorChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
    const next = event.currentTarget.value;
    const stripped = stripSkillMarkers(next);
    const caret = event.currentTarget.selectionStart ?? next.length;
    const previousDraft = draftValue;
    setDraftValue(next);
    setCursorPos(caret);
    setSelectionRange(
      normalizeSelectionRange(
        caret,
        event.currentTarget.selectionEnd ?? caret,
      ),
    );
    const previousMarkerCount = countSkillMarkers(previousDraft);
    const nextMarkerCount = countSkillMarkers(next);
    if (nextMarkerCount !== previousMarkerCount) {
      setSelectedSkills((prev) => {
        if (nextMarkerCount <= 0) return [];
        if (nextMarkerCount >= previousMarkerCount) return prev.slice(0, nextMarkerCount);
        const removed = previousMarkerCount - nextMarkerCount;
        const removeAt = Math.min(countSkillMarkers(next.slice(0, caret)), Math.max(prev.length - removed, 0));
        const reconciled = [...prev];
        reconciled.splice(removeAt, removed);
        return reconciled.slice(0, nextMarkerCount);
      });
    }
    const previousMentionCount = countMentionMarkers(previousDraft);
    const nextMentionCount = countMentionMarkers(next);
    if (nextMentionCount !== previousMentionCount) {
      setSelectedMentions((prev) => {
        if (nextMentionCount <= 0) return [];
        if (nextMentionCount >= previousMentionCount) return prev.slice(0, nextMentionCount);
        const removed = previousMentionCount - nextMentionCount;
        const removeAt = Math.min(
          countMentionMarkers(next.slice(0, caret)),
          Math.max(prev.length - removed, 0),
        );
        const reconciled = [...prev];
        reconciled.splice(removeAt, removed);
        return reconciled.slice(0, nextMentionCount);
      });
    }
    pendingDraftValueRef.current = stripped;
    onChange(stripped);
  };

  const editorPlaceholder = placeholder ?? t("composer.placeholder");
  const mirrorContent = (() => {
    const showMirrorCaret = editorFocused && !selectionRange;
    let caretRendered = false;
    const renderCaretAt = (position: number, key: string) => {
      if (!showMirrorCaret || caretRendered || cursorPos !== position) return null;
      caretRendered = true;
      return <span key={key} className="composer-mirror-caret" aria-hidden="true" />;
    };
    const renderMirrorText = (text: string, startOffset: number, keyPrefix: string) => {
      if (!text) return [];
      if (!selectionRange || !rangesOverlap(selectionRange, startOffset, startOffset + text.length)) {
        const caret = cursorPos >= startOffset && cursorPos <= startOffset + text.length
          ? renderCaretAt(cursorPos, `${keyPrefix}-caret`)
          : null;
        if (!caret) return [<span key={`${keyPrefix}-plain`}>{text}</span>];
        const caretIndex = cursorPos - startOffset;
        return [
          caretIndex > 0 ? <span key={`${keyPrefix}-before-caret`}>{text.slice(0, caretIndex)}</span> : null,
          caret,
          caretIndex < text.length ? <span key={`${keyPrefix}-after-caret`}>{text.slice(caretIndex)}</span> : null,
        ].filter(Boolean);
      }

      const nodes: React.ReactNode[] = [];
      const selectedStart = Math.max(selectionRange.start, startOffset) - startOffset;
      const selectedEnd = Math.min(selectionRange.end, startOffset + text.length) - startOffset;
      if (selectedStart > 0) {
        nodes.push(<span key={`${keyPrefix}-before`}>{text.slice(0, selectedStart)}</span>);
      }
      nodes.push(
        <span key={`${keyPrefix}-selected`} className="rounded-[2px] bg-primary text-white">
          {text.slice(selectedStart, selectedEnd)}
        </span>,
      );
      if (selectedEnd < text.length) {
        nodes.push(<span key={`${keyPrefix}-after`}>{text.slice(selectedEnd)}</span>);
      }
      return nodes;
    };

    if (!draftValue) {
      return <span className="text-gray-400">{editorPlaceholder}</span>;
    }
    const markerRegex = /[\uE000\uE001]/g;
    let offset = 0;
    let skillIndex = 0;
    let mentionIndex = 0;
    const nodes: React.ReactNode[] = [];
    let match: RegExpExecArray | null;
    let segmentIndex = 0;
    while ((match = markerRegex.exec(draftValue)) !== null) {
      const markerStart = match.index;
      const text = draftValue.slice(offset, markerStart);
      if (text) nodes.push(...renderMirrorText(text, offset, `text-${segmentIndex}`));
      offset = markerStart;
      const marker = match[0];
      const markerSelected = rangesOverlap(selectionRange, offset, offset + 1);
      const caretBefore = renderCaretAt(offset, `marker-${segmentIndex}-before-caret`);
      if (caretBefore) nodes.push(caretBefore);

      if (marker === SKILL_MARKER) {
        const skill = selectedSkills[skillIndex];
        if (skill) {
          const skillLabel = skill.name || skill.id;
          nodes.push(
            <HoverInfo key={`skill-${segmentIndex}`} content={`${skill.name} (${skill.id})`}><span
              className={`mx-0.5 inline-flex h-[20px] max-w-[180px] items-center rounded-[3px] px-1 text-[13px] font-medium leading-none align-baseline ${
                markerSelected
                  ? "bg-primary text-white"
                  : "border border-primary/20 bg-primary/10 text-primary"
              }`}

            >
              <FontAwesomeIcon icon={["fas", "cube"]} className="mr-1 text-[11px]" />
              <span className="truncate">{skillLabel}</span>
            </span></HoverInfo>,
          );
        }
        skillIndex += 1;
      } else {
        const mention = selectedMentions[mentionIndex];
        if (mention) {
          const icon =
            mention.kind === "folder"
              ? "folder"
              : mention.kind === "file"
              ? "file"
              : mention.kind === "goal"
              ? "bullseye"
              : "list-check";
          const label = mentionLabel(mention);
          const title =
            mention.kind === "goal"
              ? mention.text
              : mention.kind === "plan_mode"
              ? "开启计划模式"
              : mention.path;
          nodes.push(
            <HoverInfo key={`mention-${segmentIndex}`} content={title}><span
              className={`mx-0.5 inline-flex h-[20px] max-w-[220px] items-center rounded-[3px] px-1 text-[13px] font-medium leading-none align-baseline ${
                markerSelected
                  ? "bg-primary text-white"
                  : "bg-black/5 text-text-base"
              }`}

            >
              <FontAwesomeIcon icon={["fas", icon as any]} className="mr-1 text-[11px] text-text-secondary" />
              <span className="truncate">{label}</span>
            </span></HoverInfo>,
          );
        }
        mentionIndex += 1;
      }

      offset += 1;
      const caretAfter = renderCaretAt(offset, `marker-${segmentIndex}-after-caret`);
      if (caretAfter) nodes.push(caretAfter);
      segmentIndex += 1;
    }
    const tail = draftValue.slice(offset);
    if (tail) nodes.push(...renderMirrorText(tail, offset, `text-${segmentIndex}`));
    return nodes;
  })();

  const handleCopy = (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
    const textarea = event.currentTarget;
    const start = textarea.selectionStart ?? 0;
    const end = textarea.selectionEnd ?? start;
    if (start === end) return;
    const selectedText = stripSkillMarkers(textarea.value.slice(start, end));
    event.preventDefault();
    event.clipboardData.setData("text/plain", selectedText);
  };

  const handleCut = (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
    const textarea = event.currentTarget;
    const start = textarea.selectionStart ?? 0;
    const end = textarea.selectionEnd ?? start;
    if (start === end) return;
    const selectedText = stripSkillMarkers(textarea.value.slice(start, end));
    const nextValue = `${textarea.value.slice(0, start)}${textarea.value.slice(end)}`;
    const stripped = stripSkillMarkers(nextValue);
    event.preventDefault();
    event.clipboardData.setData("text/plain", selectedText);
    setDraftValue(nextValue);
    setCursorPos(start);
    setSelectionRange(null);
    const removedMarkers = countSkillMarkers(textarea.value.slice(start, end));
    if (removedMarkers > 0) {
      const removeAt = countSkillMarkers(textarea.value.slice(0, start));
      setSelectedSkills((prev) => {
        const next = [...prev];
        next.splice(removeAt, removedMarkers);
        return next;
      });
    }
    const removedMentionMarkers = countMentionMarkers(textarea.value.slice(start, end));
    if (removedMentionMarkers > 0) {
      const removeAt = countMentionMarkers(textarea.value.slice(0, start));
      setSelectedMentions((prev) => {
        const next = [...prev];
        next.splice(removeAt, removedMentionMarkers);
        return next;
      });
    }
    pendingDraftValueRef.current = stripped;
    onChange(stripped);
    requestAnimationFrame(() => {
      textareaRef.current?.setSelectionRange(start, start);
      textareaRef.current?.focus();
    });
  };

  const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (e.nativeEvent.isComposing) return;
    if (atOpen && e.key === "ArrowDown") {
      e.preventDefault();
      setAtSelected((s) => Math.min(s + 1, atEntries.length - 1));
      return;
    }
    if (atOpen && e.key === "ArrowUp") {
      e.preventDefault();
      setAtSelected((s) => Math.max(s - 1, 0));
      return;
    }
    if (atOpen && (e.key === "Tab" || (e.key === "Enter" && !e.shiftKey))) {
      const selected = atEntries[atSelected];
      if (selected) {
        e.preventDefault();
        chooseAt(selected);
        return;
      }
    }
    if (atOpen && e.key === "Escape") {
      e.preventDefault();
      setAtSections([]);
      return;
    }
    if (slashOpen && e.key === "ArrowDown") {
      e.preventDefault();
      setSlashSelected((s) => Math.min(s + 1, slashEntries.length - 1));
      return;
    }
    if (slashOpen && e.key === "ArrowUp") {
      e.preventDefault();
      setSlashSelected((s) => Math.max(s - 1, 0));
      return;
    }
    if (slashOpen && (e.key === "Tab" || (e.key === "Enter" && !e.shiftKey))) {
      const selected = slashEntries[slashSelected];
      const exact = selected?.skillName ? false : selected?.title === stripSkillMarkers(draftValue).trim();
      if (selected && !exact) {
        e.preventDefault();
        chooseSlash(selected);
        return;
      }
    }
    if (slashOpen && e.key === "Escape") {
      e.preventDefault();
      setSlashResults([]);
      return;
    }
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      if (!busy) submitWithAttachments();
    }
  };

  return (
    <div ref={composerRef} className="group/composer relative w-full">
      <InputSurface
        data-message-composer-surface
        className="flex flex-col overflow-visible rounded-[24px] shadow-[0_8px_24px_rgba(31,38,48,0.09)] focus-within:shadow-[0_10px_30px_rgba(31,38,48,0.13)]"
        onDragOver={(event) => event.preventDefault()}
        onDrop={onDrop}
      >
        <div className="relative flex w-full flex-col p-3.5 pb-2.5">
      <ComposerSuggestPanel
        id={`${suggestId}-at`}
        open={atOpen}
        sections={atSections}
        selectedIndex={atSelected}
        getKey={(item) => item.id}
        onSelect={chooseAt}
        onHover={setAtSelected}
        renderItem={(item, selected) => (
          <div
            className={`grid w-full grid-cols-[18px_minmax(0,1fr)] items-center gap-2.5 px-2 py-2 text-left text-[13px] transition-colors duration-150 ${
              selected ? "font-medium text-text-base" : "text-text-secondary"
            }`}
          >
            <span className="flex h-4 w-4 items-center justify-center text-text-base">
              <FontAwesomeIcon icon={["fas", item.icon as any]} className="text-[12px]" />
            </span>
            <span className="min-w-0">
              <span className="block truncate font-medium text-text-base">{item.title}</span>
              {item.description && (
                <span className="block truncate text-[12px] text-text-secondary">
                  {item.description}
                </span>
              )}
            </span>
          </div>
        )}
      />
      <ComposerSuggestPanel
        id={`${suggestId}-slash`}
        open={slashOpen}
        sections={slashSections}
        selectedIndex={slashSelected}
        getKey={(cmd) => cmd.id}
        onSelect={chooseSlash}
        onHover={setSlashSelected}
        renderTooltip={(cmd) => (
          <>
            <div className="font-medium">{cmd.title}</div>
            {cmd.description && (
              <div className="mt-1 max-w-sm text-bg-base/80">{cmd.description}</div>
            )}
          </>
        )}
        renderItem={(cmd, selected) => (
          <div
            className={`composer-slash-row grid w-full grid-cols-[130px_minmax(0,1fr)_64px] items-center gap-2.5 px-2 py-2 text-left text-[13px] transition-colors duration-150 ${
              selected ? "font-medium text-text-base" : "text-text-secondary"
            }`}
          >
            <span className="truncate font-medium text-text-base">{cmd.title}</span>
            <span className="truncate text-[12px] leading-snug text-text-secondary">
              {cmd.description}
            </span>
            <span className="composer-slash-category truncate text-right text-[11px] text-text-secondary">
              {cmd.category}
            </span>
          </div>
        )}
      />
      {planMode && (
        <div className="mb-2 inline-flex w-fit items-center rounded-full bg-amber-500/10 px-3 py-1.5 text-[11px] font-medium text-amber-700">
          <FontAwesomeIcon icon={["fas", "list-check"]} className="mr-1.5 text-[10px]" />
          Plan Mode
        </div>
      )}

      <ComposerAttachmentList attachments={attachments} onRemove={removeAttachment} />

      <div className="relative w-full">
        <div
          ref={mirrorRef}
          data-message-composer-content
          aria-hidden="true"
          className="pointer-events-none absolute inset-0 overflow-hidden custom-scrollbar whitespace-pre-wrap break-words bg-transparent text-sm leading-6 text-text-base"
          style={{
            minHeight: `${COMPOSER_TEXTAREA_MIN_HEIGHT}px`,
            maxHeight: `${textareaMaxHeight}px`,
          }}
        >
          {mirrorContent}
        </div>
        <textarea
          ref={textareaRef}
          aria-label={editorPlaceholder}
          aria-autocomplete="list"
          aria-controls={atOpen ? `${suggestId}-at` : slashOpen ? `${suggestId}-slash` : undefined}
          aria-activedescendant={atOpen ? `${suggestId}-at-option-${atSelected}` : slashOpen ? `${suggestId}-slash-option-${slashSelected}` : undefined}
          className={`composer-textarea custom-scrollbar relative z-10 w-full resize-none bg-transparent text-sm leading-6 text-transparent outline-none placeholder-transparent ${
            draftValue ? "" : "composer-textarea--empty"
          }`}
          style={{
            minHeight: `${COMPOSER_TEXTAREA_MIN_HEIGHT}px`,
            maxHeight: `${textareaMaxHeight}px`,
          }}
          placeholder={editorPlaceholder}
          value={draftValue}
          onChange={handleEditorChange}
          onPaste={onPaste}
          onCopy={handleCopy}
          onCut={handleCut}
          onKeyDown={onKeyDown}
          onSelect={syncCursorFromTextarea}
          onClick={syncCursorFromTextarea}
          onKeyUp={syncCursorFromTextarea}
          onFocus={() => {
            setEditorFocused(true);
            syncCursorFromTextarea();
          }}
          onBlur={() => {
            setEditorFocused(false);
            setSelectionRange(null);
          }}
          onScroll={(event) => {
            if (!mirrorRef.current) return;
            mirrorRef.current.scrollTop = event.currentTarget.scrollTop;
            mirrorRef.current.scrollLeft = event.currentTarget.scrollLeft;
          }}
        />
      </div>

      <div className="composer-toolbar mt-2.5 flex h-8 items-center gap-3">
        <div className="composer-primary-tools flex min-w-0 flex-1 items-center gap-1.5 overflow-visible">
          <input
            ref={fileInputRef}
            type="file"
            multiple
            className="hidden"
            onChange={(event) => {
              if (event.target.files) void addFiles(event.target.files, "picker");
              event.currentTarget.value = "";
            }}
          />
          <ComposerAttachButton
            disabled={attachments.length >= MAX_COMPOSER_ATTACHMENTS}
            disabledTitle={attachmentLimitMessage}
            onClick={() => fileInputRef.current?.click()}
          />

          {visibleOptions.length > 0 && (
            <div ref={approvalDropdownRef} className="min-w-0 max-w-full">
              <MorphingMenuShell
                open={isApprovalDropdownOpen}
                onOpenChange={(next) => {
                  if (next) {
                    setIsModelDropdownOpen(false);
                    setIsContextPopoverOpen(false);
                  }
                  setIsApprovalDropdownOpen(next);
                }}
                layoutId={approvalMorphLayoutId}
                trigger={
                  <button
                    type="button"
                    className={cn(
                      "flex h-8 max-w-full min-w-0 flex-shrink items-center whitespace-nowrap rounded-full px-3 text-[11px] font-medium",
                      MOTION.fast,
                      selectedApproval?.id === "full"
                        ? cn(
                            "text-[#e25507] bg-[#e25507]/10 hover:bg-[#e25507]/18 active:bg-[#e25507]/22",
                            isApprovalDropdownOpen && "bg-[#e25507]/18",
                          )
                        : cn(
                            "text-text-secondary bg-black/5 hover:bg-black/10 hover:text-text-base active:bg-black/[0.12]",
                            isApprovalDropdownOpen && "bg-black/10 text-text-base",
                          ),
                    )}
                    onClick={() => {
                      const next = !isApprovalDropdownOpen;
                      if (next) {
                        setIsModelDropdownOpen(false);
                        setIsContextPopoverOpen(false);
                      }
                      setIsApprovalDropdownOpen(next);
                    }}
                  >
                    {selectedApproval && (
                      <>
                        <selectedApproval.icon
                          size={12}
                          className={cn(
                            "mr-1.5 shrink-0",
                            selectedApproval.id === "full" ? "text-[#e25507]" : "text-text-secondary",
                          )}
                        />
                        <span className="truncate">{t(selectedApproval.label)}</span>
                      </>
                    )}
                    <ChevronDown size={10} className="ml-1 shrink-0" />
                  </button>
                }
                className="min-w-0 max-w-full"
                panelClassName="w-[min(288px,calc(100vw-48px))]"
                panelShadow="0 4px 12px rgba(0,0,0,0.08)"
                panelAlign="left"
              >
                <div className="p-1.5">
                  <SlidingMenuList
                    activeId={selectedApproval?.id ?? ""}
                    pillClassName={cn(
                      "left-0 right-0 rounded-xl bg-[#f5f5f5]",
                      selectedApproval?.id === "full" && "bg-[#e25507]/10",
                    )}
                    className="flex w-full flex-col gap-0.5"
                  >
                    {visibleOptions.map((opt) => {
                      const selected = selectedApproval?.id === opt.id;
                      const isFull = opt.id === "full";
                      return (
                        <div
                          key={opt.id}
                          {...{ [MENU_ITEM_ATTR]: opt.id }}
                          className={cn(
                            "relative z-[1] flex w-full cursor-pointer items-start gap-[7px] rounded-xl px-2 py-2 text-left",
                            MOTION.fast,
                            selected
                              ? isFull
                                ? "font-medium text-[#e25507]"
                                : "font-medium text-text-base"
                              : "text-text-base",
                          )}
                          onClick={() => chooseApproval(opt)}
                        >
                          <div className="mt-0.5 flex w-3 shrink-0 justify-center">
                            <opt.icon
                              size={12}
                              className={cn(
                                selected && isFull ? "text-[#e25507]" : "text-text-secondary",
                              )}
                            />
                          </div>
                          <div className="min-w-0 flex-1">
                            <div className={cn("pr-3.5 text-[12px] font-medium leading-[17px]", selected && !isFull && "text-text-base")}>
                              {t(opt.label)}
                            </div>
                            <div
                              className={cn(
                                "mt-0.5 text-[11px] leading-[15px]",
                                selected && isFull ? "text-[#e25507]/80" : "text-[#8c8c8c]",
                              )}
                            >
                              {t(opt.desc)}
                            </div>
                          </div>
                          {selected && (
                            <Check
                              size={10}
                              className={cn("absolute right-2 top-[11px] shrink-0", isFull ? "text-[#e25507]" : "text-text-secondary")}
                            />
                          )}
                        </div>
                      );
                    })}
                  </SlidingMenuList>
                </div>
              </MorphingMenuShell>
            </div>
          )}
        </div>

        <div className="composer-model-tools flex h-8 shrink-0 items-center gap-2">
          <ContextCapacityIndicator
            snapshot={contextUsage}
            contextWindow={
              modelCapabilities.find((c) => c.model_id === selectedModel)?.context_window
            }
            fallbackPromptTokens={contextUsageFallbackTokens}
            popoverSuppressed={isModelDropdownOpen || isApprovalDropdownOpen}
            overlayCloseSignal={overlayCloseSignal}
            onPopoverOpenChange={(next) => {
              if (next) {
                setIsModelDropdownOpen(false);
                setIsApprovalDropdownOpen(false);
              }
              setIsContextPopoverOpen(next);
            }}
          />
          <div className="relative flex h-8 items-center" ref={dropdownRef}>
            <ModelThinkingSelector
              open={isModelDropdownOpen}
              disabled={busy && !onStop}
              switching={switching}
              models={models}
              selectedModel={selectedModel}
              selectedThinking={selectedThinking}
              thinkingOptions={THINKING_OPTIONS.map((opt) => ({
                ...opt,
                label: t(opt.label),
              }))}
              selectModelLabel={t("composer.selectModel")}
              noModelsLabel={t("composer.noModels")}
              onOpenChange={(next) => {
                if (next) {
                  setIsApprovalDropdownOpen(false);
                  setIsContextPopoverOpen(false);
                }
                setIsModelDropdownOpen(next);
              }}
              onChooseModel={chooseModel}
              onChooseThinking={chooseThinking}
            />
          </div>
          <HoverInfo content={busy ? t(stopping ? "composer.stopping" : "composer.stop") : undefined}><button
            onClick={() => {
              if (busy) {
                onStop?.();
              } else {
                submitWithAttachments();
              }
            }}
            disabled={busy ? !onStop || stopping : !hasComposerContent}
            aria-label={busy ? t(stopping ? "composer.stopping" : "composer.stop") : t("composer.send")}

            className={cn(
              "composer-send-btn",
              busy
                ? onStop
                  ? "composer-send-btn--stop"
                  : "composer-send-btn--idle"
                : hasComposerContent
                  ? planMode
                    ? "composer-send-btn--plan"
                    : "composer-send-btn--ready"
                  : "composer-send-btn--idle",
            )}
          >
            {stopping
              ? <Loader2 className="h-3.5 w-3.5 animate-spin" />
              : <FontAwesomeIcon icon={busy ? ["fas", "stop"] : ["fas", "arrow-up"]} className="text-[11px]" />}
          </button></HoverInfo>
        </div>
      </div>
        </div>
      </InputSurface>

      {footer && (
        <div
          className={cn(
            "relative mt-2 flex min-h-[48px] w-full items-center overflow-visible rounded-[20px] bg-gradient-to-r from-hover-bg via-selection-bg/70 to-primary/10 px-3.5 py-2 shadow-[0_6px_20px_rgba(0,0,0,0.06)] group-focus-within/composer:shadow-[0_8px_24px_rgba(0,0,0,0.10)]",
            MOTION.standard,
          )}
        >
          {footer}
        </div>
      )}
    </div>
  );
}





/* ===== components/ui/HoverInfo.tsx · SHA256 0f2bd618089688ecbdb0a321f40e110a9e2ee7292ec9fe685c592cc62e50b1d2 ===== */
import { cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState, type HTMLAttributes, type ReactElement, type ReactNode } from "react";

import { createHoverCardHandle, HoverCard, HoverCardContent, HoverCardTrigger, type HoverCardHandle } from "../shadcn/hover-card";
import { MenuTriggerOpenContext } from "./MorphPanelLayoutContext";

interface HoverInfoProps {
  content: ReactNode;
  children: ReactElement;
  side?: "top" | "right" | "bottom" | "left";
}

type HintPayload = { content: ReactNode; side?: HoverInfoProps["side"] };
const HoverInfoGroupContext = createContext<{
  handle: HoverCardHandle<HintPayload>;
  dismiss: () => void;
  delay: number;
  closeDelay: number;
} | null>(null);

/** One information surface for one navigation/toolbar region. First hover waits;
 * sibling triggers update the existing payload and anchor without reopening it. */
export function HoverInfoGroup({ children, side, delay = 350, closeDelay = 150 }: {
  children: ReactElement<HTMLAttributes<HTMLElement>>;
  side?: HoverInfoProps["side"];
  delay?: number;
  closeDelay?: number;
}) {
  const [handle] = useState(() => createHoverCardHandle<HintPayload>());
  const [open, setOpen] = useState(false);
  const inside = useRef(false);
  const dismiss = useCallback(() => handle.close(), [handle]);
  const context = useMemo(() => ({ handle, dismiss, delay, closeDelay }), [handle, dismiss, delay, closeDelay]);
  const region = cloneElement(children, {
    onPointerEnter: event => { inside.current = true; children.props.onPointerEnter?.(event); },
    onPointerLeave: event => { inside.current = false; dismiss(); children.props.onPointerLeave?.(event); },
    onPointerDownCapture: event => { dismiss(); children.props.onPointerDownCapture?.(event); },
    onBlurCapture: event => {
      if (!event.currentTarget.contains(event.relatedTarget)) dismiss();
      children.props.onBlurCapture?.(event);
    },
  });
  return <HoverInfoGroupContext.Provider value={context}>
    <HoverCard handle={handle} open={open} onOpenChange={(next, details) => {
      if ((next && details.trigger?.hasAttribute("data-hover-info-disabled"))
        || (!next && details.reason === "trigger-hover" && inside.current)) {
        details.cancel(); return;
      }
      setOpen(next);
    }}>
      {({ payload }) => <>{region}<HoverCardContent side={payload?.side ?? side} continuous>{payload?.content}</HoverCardContent></>}
    </HoverCard>
  </HoverInfoGroupContext.Provider>;
}

/** A short, non-underlined information card for an existing UI trigger. */
export function HoverInfo({ content, children, side }: HoverInfoProps) {
  const menuOpen = useContext(MenuTriggerOpenContext);
  const group = useContext(HoverInfoGroupContext);
  const [open, setOpen] = useState(false);
  useEffect(() => { if (menuOpen) { setOpen(false); group?.dismiss(); } }, [menuOpen, group]);
  if (content == null || content === "") return children;
  const disabled = isValidElement<{ disabled?: boolean; className?: string }>(children)
    && children.props.disabled === true;
  const ariaLabel = typeof content === "string"
    && children.type === "button"
    && isValidElement<{ "aria-label"?: string }>(children)
    && !children.props["aria-label"]
    ? content
    : undefined;
  const trigger = disabled ? (
    <span
      className={children.props.className?.includes("w-full") ? "flex w-full" : "inline-flex max-w-full"}
      tabIndex={0}
      aria-label={typeof content === "string" ? content : undefined}
    >
      {children}
    </span>
  ) : children;

  if (group) return <HoverCardTrigger
    handle={group.handle}
    payload={{ content, side }}
    render={trigger}
    delay={group.delay}
    closeDelay={group.closeDelay}
    aria-label={ariaLabel}
    data-hover-info-disabled={menuOpen || undefined}
    className="!no-underline hover:!no-underline"
  />;

  return (
    <HoverCard open={open && !menuOpen} onOpenChange={(next, details) => {
      if (details.reason === "trigger-press" || (next && menuOpen)) details.cancel();
      else setOpen(next);
    }}>
      <HoverCardTrigger
        render={trigger}
        delay={350}
        closeDelay={150}
        aria-label={ariaLabel}
        className="!no-underline hover:!no-underline"
      />
      <HoverCardContent side={side}>{content}</HoverCardContent>
    </HoverCard>
  );
}


/* ===== components/shadcn/hover-card.tsx · SHA256 661fbe94b95c45e9642822bda7d4adbaa91d0248d289cb73c9602df2d2204e9e ===== */
import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card";

import { cn } from "./utils";
import { SpringPopupContext, SpringSurface, usePopupMotionRoot } from "../ui/SpringSurface";
import { useHintPositionMotion } from "../ui/useHintPositionMotion";

function HoverCard<Payload = unknown>({ actionsRef, onOpenChange, ...props }: PreviewCardPrimitive.Root.Props<Payload>) {
  const motion = usePopupMotionRoot(actionsRef);
  return <SpringPopupContext.Provider value={motion.context}>
    <PreviewCardPrimitive.Root data-slot="hover-card" {...props} actionsRef={motion.actionsRef} onOpenChange={(open, details) => {
      onOpenChange?.(open, details);
      motion.onChange(open, details);
    }} />
  </SpringPopupContext.Provider>;
}

function HoverCardTrigger<Payload = unknown>(props: PreviewCardPrimitive.Trigger.Props<Payload>) {
  return <PreviewCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />;
}

function HoverCardContent({
  className,
  side = "bottom",
  align = "center",
  sideOffset = 6,
  alignOffset = 0,
  motionTone = "hint",
  continuous = false,
  ...props
}: PreviewCardPrimitive.Popup.Props & { motionTone?: "hint" | "menu"; continuous?: boolean } &
  Pick<PreviewCardPrimitive.Positioner.Props, "side" | "align" | "sideOffset" | "alignOffset">) {
  const { positionerRef, shiftRef } = useHintPositionMotion(continuous);
  const popup = <PreviewCardPrimitive.Popup
    render={<SpringSurface motionTone={motionTone} />}
    data-slot="hover-card-content"
    data-hover-info-continuous={continuous || undefined}
    className={cn(
      "z-[70] max-w-sm origin-(--transform-origin) rounded-xl border border-border-theme bg-elevated-bg px-3 py-2 text-[12px] leading-snug text-text-base shadow-[0_6px_24px_rgba(0,0,0,0.12)] outline-none",
      className,
    )}
    {...props}
  />;
  return (
    <PreviewCardPrimitive.Portal data-slot="hover-card-portal">
      <PreviewCardPrimitive.Positioner
        ref={positionerRef}
        side={side}
        align={align}
        sideOffset={sideOffset}
        alignOffset={alignOffset}
        className="isolate z-[70]"
      >
        {continuous ? <div ref={shiftRef}>{popup}</div> : popup}
      </PreviewCardPrimitive.Positioner>
    </PreviewCardPrimitive.Portal>
  );
}

const createHoverCardHandle = PreviewCardPrimitive.createHandle;
type HoverCardHandle<Payload> = PreviewCardPrimitive.Handle<Payload>;
export { HoverCard, HoverCardTrigger, HoverCardContent, createHoverCardHandle, type HoverCardHandle };


/* ===== components/shadcn/utils.ts · SHA256 c0adf27efbfa148d5abcf99e860f8e3e6c8fc77a1f99e023c08ef4f12e6ece29 ===== */
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}


/* ===== components/ui/SpringSurface.tsx · SHA256 8a9491b262ea2c1da9ad0c6d77b77bb92fa85cb51b3dfc2e4b690154f87a5bea ===== */
import {
  createContext, forwardRef, useCallback, useContext, useImperativeHandle,
  useLayoutEffect, useMemo, useRef, type HTMLAttributes, type Ref,
} from "react";
import gsap from "gsap";
import { continuousRadius, computedCornerCurvature } from "../../theme/continuousCorners";
import { SHARE_MOTION, motionColor, prefersReducedMotion, springEase } from "./springMotion";
import "./menuSurface.css";

type PopupActions = { unmount: () => void; close: () => void };
type PopupChange = { isCanceled: boolean; trigger?: Element | null; preventUnmountOnClose: () => void };
type MotionContext = { trigger: { current: HTMLElement | null }; onExited: () => void };
export const SpringPopupContext = createContext<MotionContext | null>(null);

// Base UI's documented external-animation lifecycle; business open state closes immediately.
export function usePopupMotionRoot(externalActions?: Ref<PopupActions | null>) {
  const actionsRef = useRef<PopupActions | null>(null);
  const trigger = useRef<HTMLElement | null>(null);
  useImperativeHandle(externalActions, () => ({
    unmount: () => actionsRef.current?.unmount(), close: () => actionsRef.current?.close(),
  }), []);
  const context = useMemo(() => ({ trigger, onExited: () => actionsRef.current?.unmount() }), []);
  const onChange = (open: boolean, details: PopupChange) => {
    if (details.isCanceled) return;
    if (open && details.trigger instanceof HTMLElement) trigger.current = details.trigger;
    if (!open && !prefersReducedMotion()) details.preventUnmountOnClose();
  };
  return { actionsRef, context, onChange };
}

type Props = HTMLAttributes<HTMLDivElement> & {
  motionTone?: "menu" | "hint";
  anchor?: { current: HTMLElement | null };
  openingPointer?: { current: boolean };
  onMotionExit?: () => void;
};

/** Keep library positioning/focus intact; morph one visible shell from its trigger. */
export const SpringSurface = forwardRef<HTMLDivElement, Props>(function SpringSurface(
  { children, className, motionTone = "menu", anchor, openingPointer, onMotionExit, ...props }, forwardedRef,
) {
  const nodeRef = useRef<HTMLDivElement | null>(null);
  const surfaceRef = useRef<HTMLDivElement>(null);
  const compactRef = useRef<HTMLDivElement>(null);
  const bodyRef = useRef<HTMLDivElement>(null);
  const popup = useContext(SpringPopupContext);
  const latest = useRef({ anchor, openingPointer, popup, onMotionExit });
  latest.current = { anchor, openingPointer, popup, onMotionExit };
  const setRef = useCallback((node: HTMLDivElement | null) => {
    nodeRef.current = node;
    if (typeof forwardedRef === "function") forwardedRef(node);
    else if (forwardedRef) forwardedRef.current = node;
  }, [forwardedRef]);

  useLayoutEffect(() => {
    const node = nodeRef.current, surface = surfaceRef.current, compact = compactRef.current, body = bodyRef.current;
    if (!node || !surface || !compact || !body) return;
    let timeline: gsap.core.Timeline | undefined;
    let resize: gsap.core.Tween | undefined;
    let previousOpen: boolean | undefined;
    let disposed = false;
    const targetWidth = node.offsetWidth;
    let targetHeight = node.offsetHeight;
    const css = getComputedStyle(node);
    const paddingX = parseFloat(css.paddingLeft) + parseFloat(css.paddingRight);
    const paddingY = parseFloat(css.paddingTop) + parseFloat(css.paddingBottom);
    const borderX = parseFloat(css.borderLeftWidth) + parseFloat(css.borderRightWidth);
    const borderY = parseFloat(css.borderTopWidth) + parseFloat(css.borderBottomWidth);
    const borderLeft = parseFloat(css.borderLeftWidth);
    const borderTop = parseFloat(css.borderTopWidth);
    const radius = parseFloat(css.borderTopLeftRadius) || continuousRadius(16);
    const curvature = computedCornerCurvature(css);
    const maxHeight = parseFloat(css.maxHeight);
    const context = gsap.context(() => {}, node);
    const trigger = (latest.current.anchor ?? latest.current.popup?.trigger)?.current;
    const triggerStyle = trigger?.style.opacity ?? "";
    let ownsTriggerOpacity = false;
    const triggerCss = trigger ? getComputedStyle(trigger) : null;
    const buttonRadius = parseFloat(triggerCss?.borderTopLeftRadius ?? "0");
    const buttonCurvature = triggerCss ? computedCornerCurvature(triggerCss) : curvature;
    const buttonBackground = motionColor(triggerCss?.backgroundColor ?? "transparent");
    const initial = node.getBoundingClientRect();
    const origin = trigger?.getBoundingClientRect();
    const side = node.getAttribute("data-side") ?? (origin && initial.bottom <= origin.top + 12 ? "top" : origin && initial.left >= origin.right ? "right" : "bottom");
    const align = node.getAttribute("data-align") ?? (origin && Math.abs(initial.right - origin.right) < Math.abs(initial.left - origin.left) ? "end" : "start");
    const sideways = side === "left" || side === "right";
    const background = motionColor(css.backgroundColor), shadow = css.boxShadow, borderColor = motionColor(css.borderTopColor);
    const naturalOverflowX = css.overflowX, naturalOverflowY = css.overflowY;
    const restoreTrigger = () => {
      if (trigger && ownsTriggerOpacity) trigger.style.opacity = triggerStyle;
      ownsTriggerOpacity = false;
    };
    context.add(() => gsap.set(surface, { visibility: "visible" }));
    // The library's outer box remains at its natural size, so collision detection
    // never chases the animated dimensions. Only this inner shell is visible.
    if (motionTone === "menu") context.add(() => {
      gsap.set(node, { position: "relative", width: targetWidth, height: targetHeight, backgroundColor: "transparent",
        borderColor: "transparent", boxShadow: "none", overflow: "visible", pointerEvents: "none" });
      gsap.set(surface, { position: "absolute", boxSizing: "border-box", width: targetWidth, height: targetHeight,
        padding: css.padding, borderWidth: css.borderWidth, borderStyle: css.borderStyle,
        borderColor, borderRadius: radius, "--spring-corner-curvature": curvature, backgroundColor: background, boxShadow: shadow });
      body.style.width = `${Math.max(0, targetWidth - paddingX - borderX)}px`;
      if (!sideways && trigger && origin && origin.width > 1 && origin.height > 1) {
        const face = trigger.cloneNode(true) as HTMLElement;
        face.removeAttribute("id");
        for (const element of face.querySelectorAll("[id], [autofocus]")) {
          element.removeAttribute("id"); element.removeAttribute("autofocus");
        }
        Object.assign(face.style, { width: `${origin.width}px`, height: `${origin.height}px`,
          margin: "0", maxWidth: "none", opacity: triggerCss?.opacity ?? "1", transform: "none" });
        compact.replaceChildren(face);
        compact.inert = true;
      }
    });
    const dimensions = () => {
      const rect = trigger?.getBoundingClientRect();
      const heightLimit = rect && side === "top" ? rect.bottom - 16
        : rect && side === "bottom" ? window.innerHeight - rect.top - 16 : window.innerHeight - 32;
      return { width: Math.min(targetWidth, window.innerWidth - 32), height: Math.min(targetHeight, Math.max(24, heightLimit)) };
    };
    const settleOverflow = () => gsap.set(surface, { overflowX: naturalOverflowX,
      overflowY: dimensions().height < targetHeight ? "auto" : naturalOverflowY });
    const place = () => {
      if (motionTone !== "menu" || disposed) return;
      const base = node.getBoundingClientRect(), rect = trigger?.getBoundingClientRect();
      const width = surface.offsetWidth, height = surface.offsetHeight;
      let left = base.left, top = base.top;
      if (rect) {
        if (side === "top" || side === "bottom") {
          left = align === "end" ? rect.right - width : align === "center" ? rect.left + (rect.width - width) / 2 : rect.left;
          top = side === "top" ? rect.bottom - height : rect.top;
        } else {
          // Navigation and cascading menus keep their original side placement.
          left = base.left + (side === "left" ? node.offsetWidth - width : 0);
          top = base.top;
        }
        left = Math.max(8, Math.min(left, window.innerWidth - width - 8));
        top = Math.max(8, Math.min(top, window.innerHeight - height - 8));
        compact.style.left = `${rect.left - left - borderLeft}px`;
        compact.style.top = `${rect.top - top - borderTop}px`;
      }
      surface.style.left = `${left - base.left - borderLeft}px`;
      surface.style.top = `${top - base.top - borderTop}px`;
    };
    const isOpen = () => node.getAttribute("data-state") === "open" || node.hasAttribute("data-open");
    let faceReady = false, pointerHeld = false;
    let releaseFrame = 0;
    const enablePointer = () => {
      if (motionTone === "menu") body.style.pointerEvents = isOpen() && faceReady && !pointerHeld ? "auto" : "none";
    };
    const releasePointer = () => {
      cancelAnimationFrame(releaseFrame);
      releaseFrame = requestAnimationFrame(() => {
        pointerHeld = false;
        if (latest.current.openingPointer) latest.current.openingPointer.current = false;
        enablePointer();
      });
    };
    document.addEventListener("pointerup", releasePointer, true);
    document.addEventListener("pointercancel", releasePointer, true);
    const finishExit = () => {
      if (!disposed && !isOpen()) {
        restoreTrigger();
        surface.style.visibility = "hidden";
        gsap.set(node, { autoAlpha: 0 });
        (latest.current.onMotionExit ?? latest.current.popup?.onExited)?.();
      }
    };
    const play = () => context.add(() => {
      const open = isOpen();
      if (open === previousOpen) return;
      const first = previousOpen === undefined;
      previousOpen = open;
      timeline?.kill(); resize?.kill();
      node.inert = !open;
      faceReady = false;
      pointerHeld = open && !!latest.current.openingPointer?.current;
      enablePointer();
      // A primitive can remount its closed popup during the final unmount commit.
      // That instance has no open face to fold; never paint its natural size.
      if (first && !open) {
        restoreTrigger();
        gsap.set(node, { autoAlpha: 0 });
        gsap.set(surface, { visibility: "hidden" });
        return;
      }
      if (motionTone === "hint") {
        if (prefersReducedMotion()) {
          gsap.set(node, { autoAlpha: open ? 1 : 0 });
          if (!open) finishExit();
          return;
        }
        if (open) gsap.set(node, { opacity: 0, scale: 0.98, y: 2 });
        timeline = gsap.timeline({ onComplete: open ? undefined : finishExit });
        timeline.to(node, { autoAlpha: open ? 1 : 0, scale: open ? 1 : 0.98,
          y: open ? 0 : 2, duration: open ? 0.18 : 0.12, ease: "power2.out" });
        return;
      }
      const triggerRect = trigger?.getBoundingClientRect();
      const fromWidth = sideways ? 4 : triggerRect?.width ?? targetWidth * 0.7;
      const fromHeight = triggerRect?.height ?? 32;
      const expanded = dimensions();
      const motion = open ? SHARE_MOTION.open : SHARE_MOTION.close;
      if (trigger && !sideways) { trigger.style.opacity = "0"; ownsTriggerOpacity = true; }
      gsap.set(node, { visibility: "visible", opacity: 1 });
      gsap.set(surface, { visibility: "visible", overflow: "hidden", pointerEvents: open ? "auto" : "none" });
      if (prefersReducedMotion()) {
        gsap.set(surface, { width: open ? expanded.width : fromWidth, height: open ? expanded.height : fromHeight,
          borderRadius: open ? radius : buttonRadius, "--spring-corner-curvature": open ? curvature : buttonCurvature,
          backgroundColor: open ? background : buttonBackground,
          boxShadow: open ? shadow : "none", borderColor: open ? borderColor : "transparent" });
        gsap.set(compact, { opacity: open ? 0 : 1 });
        gsap.set(body, { opacity: open ? 1 : 0, filter: "blur(0px)" });
        place();
        faceReady = open; enablePointer();
        if (!open) finishExit();
        else settleOverflow();
        return;
      }
      if (open && timeline === undefined) {
        gsap.set(surface, { width: fromWidth, height: fromHeight, borderRadius: buttonRadius,
          "--spring-corner-curvature": buttonCurvature,
          backgroundColor: buttonBackground, boxShadow: "none", borderColor: "transparent" });
        gsap.set(compact, { opacity: 1 });
        gsap.set(body, { opacity: 0, filter: "blur(3px)" });
      }
      timeline = gsap.timeline({ onComplete: () => {
        if (!open) { finishExit(); return; }
        settleOverflow();
      } });
      timeline.to(surface, { width: open ? expanded.width : fromWidth,
        duration: motion.width.duration, ease: springEase(motion.width.stiffness, motion.width.damping, motion.width.duration) }, motion.width.delay);
      timeline.to(surface, { height: open ? expanded.height : fromHeight,
        duration: motion.height.duration, ease: springEase(motion.height.stiffness, motion.height.damping, motion.height.duration) }, motion.height.delay);
      timeline.to(surface, { borderRadius: open ? radius : buttonRadius,
        "--spring-corner-curvature": open ? curvature : buttonCurvature,
        backgroundColor: open ? background : buttonBackground, boxShadow: open ? shadow : "none",
        borderColor: open ? borderColor : "transparent", duration: 0.22, ease: "power2.out" }, 0);
      timeline.to(compact, { opacity: open ? 0 : 1, duration: 0.14, ease: "power2.out" }, open ? 0 : 0.2);
      timeline.to(body, { opacity: open ? 1 : 0,
        filter: open ? "blur(0px)" : "blur(3px)", duration: open ? 0.25 : 0.14, ease: "power2.out" }, open ? 0.12 : 0);
      if (open) timeline.call(() => { faceReady = true; enablePointer(); }, [], 0.37);
      if (open) {
        const rows = Array.from(body.querySelectorAll<HTMLElement>("[role=menuitem], [role=menuitemradio], [data-menu-item]")).slice(0, 24);
        timeline.fromTo(rows, { opacity: 0, y: 4 }, { opacity: 1, y: 0,
          duration: 0.25, stagger: Math.min(0.018, 0.16 / Math.max(1, rows.length)), ease: "power2.out" }, 0.14);
      }
    });
    play(); place();
    if (motionTone === "menu") gsap.ticker.add(place);
    // Floating primitives position their parent after this child's layout effect.
    // Re-anchor before paint when that transform arrives, including production
    // builds where StrictMode does not replay layout effects.
    const positionObserver = new MutationObserver(place);
    if (motionTone === "menu" && node.parentElement) positionObserver.observe(node.parentElement,
      { attributes: true, attributeFilter: ["style"] });
    const observer = new MutationObserver(play);
    observer.observe(node, { attributes: true, attributeFilter: ["data-state", "data-open", "data-closed"] });
    let rendered = dimensions();
    const retarget = () => {
      if (!isOpen() || motionTone === "hint" || disposed) return;
      const width = dimensions().width;
      body.style.width = `${Math.max(0, width - paddingX - borderX)}px`;
      const contentHeight = body.offsetHeight + paddingY + borderY;
      const nextHeight = Number.isFinite(maxHeight) ? Math.min(contentHeight, maxHeight) : contentHeight;
      const contentChanged = Math.abs(nextHeight - targetHeight) >= 1;
      targetHeight = nextHeight;
      const next = dimensions();
      const widthChanged = Math.abs(next.width - rendered.width) >= 1;
      if (!contentChanged && !widthChanged && Math.abs(next.height - rendered.height) < 1) return;
      rendered = next;
      context.add(() => {
        resize?.kill(); gsap.killTweensOf(surface, widthChanged ? "width,height" : "height");
        gsap.set(node, { width, height: targetHeight });
        const m = SHARE_MOTION.resize;
        resize = gsap.to(surface, { ...(widthChanged ? {width} : {}), height: next.height, duration: prefersReducedMotion() ? 0 : m.duration,
          ease: springEase(m.stiffness, m.damping, m.duration),
          onComplete: settleOverflow,
        });
      });
    };
    const contentResize = new ResizeObserver(retarget);
    contentResize.observe(body);
    window.addEventListener("resize", retarget);
    return () => {
      disposed = true;
      observer.disconnect(); contentResize.disconnect(); positionObserver.disconnect();
      gsap.ticker.remove(place);
      window.removeEventListener("resize", retarget);
      document.removeEventListener("pointerup", releasePointer, true);
      document.removeEventListener("pointercancel", releasePointer, true);
      cancelAnimationFrame(releaseFrame);
      restoreTrigger(); compact.replaceChildren();
      timeline?.kill(); resize?.kill();
      context.revert();
      node.style.visibility = "hidden";
      surface.style.visibility = "hidden";
    };
  }, [motionTone]);

  return <div {...props} ref={setRef} data-spring-tone={motionTone} className={`spring-surface ui-menu-surface ${className ?? ""}`}>
    <div ref={surfaceRef} data-spring-shell>
      <div ref={compactRef} aria-hidden="true" className="pointer-events-none absolute" />
      <div ref={bodyRef} className="spring-surface-content">{children}</div>
    </div>
  </div>;
});


/* ===== theme/continuousCorners.ts · SHA256 7cdd2ff0332d8279b4c7161abff0df381b1d0850750090fea53d1d89a2a7e96c ===== */
import type { CSSProperties } from "react";

/** Shared Apple-style corner policy for CSS, inline styles, and animation targets. */
export const CONTINUOUS_CORNERS = Object.freeze({ radius: 24, smoothing: 0.6 });
export const CORNER_RADIUS_SCALE = 1 + CONTINUOUS_CORNERS.smoothing;

// Native superellipses approximate the approved Figma curve. Match its straight-edge
// extent and 45-degree inset rather than replacing it with the squircle keyword.
export const CORNER_CURVATURE = Math.log2(
  -Math.LN2 / Math.log(1 - (1 - Math.SQRT1_2) / CORNER_RADIUS_SCALE),
);

export function supportsContinuousCorners() {
  return typeof CSS !== "undefined" && CSS.supports("corner-shape", "superellipse(2)");
}

export function continuousRadius(radius: number, supported = supportsContinuousCorners()) {
  return supported ? radius * CORNER_RADIUS_SCALE : radius;
}

export function continuousCornerStyle(
  radius: number | string,
  supported = supportsContinuousCorners(),
): CSSProperties & { cornerShape?: string } {
  // These values intentionally describe circles/capsules, including canvas handles.
  if (typeof radius === "string") {
    return radius.includes("%")
      ? { borderRadius: radius, cornerShape: "round" }
      : { borderRadius: supported ? `calc((${radius}) * ${CORNER_RADIUS_SCALE})` : radius };
  }
  if (radius >= 999) return { borderRadius: radius, cornerShape: "round" };
  return { borderRadius: continuousRadius(radius, supported) };
}

export function computedCornerCurvature(style: CSSStyleDeclaration) {
  const shape = style.getPropertyValue("corner-shape");
  if (shape === "round" || !supportsContinuousCorners()) return 1;
  const match = shape.match(/superellipse\(([\d.]+)\)/);
  return match ? Number(match[1]) : CORNER_CURVATURE;
}


/* ===== components/ui/springMotion.ts · SHA256 f2c799c9c0d0850bedc969e6db2b2eb8b98f2c7b45545a9b00cdc1749c6b2ecd ===== */
// One motion source for the reference demo and the production UI.
export const SHARE_MOTION = {
  open: {
    width: { stiffness: 306.25, damping: 28, duration: 0.7, delay: 0 },
    height: { stiffness: 196, damping: 18.34, duration: 0.86, delay: 0.057 },
  },
  close: {
    width: { stiffness: 400, damping: 33, duration: 0.64, delay: 0.07 },
    height: { stiffness: 600.25, damping: 40, duration: 0.62, delay: 0 },
  },
  resize: { stiffness: 280, damping: 24, duration: 0.64 },
} as const;

export function springEase(stiffness: number, damping: number, duration: number) {
  const decay = damping / 2;
  const frequency = Math.sqrt(stiffness - decay * decay);
  return (progress: number) => {
    if (progress === 0 || progress === 1) return progress;
    const time = progress * duration;
    return 1 - Math.exp(-decay * time) *
      (Math.cos(frequency * time) + decay / frequency * Math.sin(frequency * time));
  };
}

export function prefersReducedMotion() {
  return typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}

/** GSAP's color parser expects RGB/HSL; themes may resolve to OKLab/color(). */
export function motionColor(color: string): string {
  if (/^rgba?\(/i.test(color)) return color;
  const canvas = document.createElement("canvas");
  canvas.width = canvas.height = 1;
  const context = canvas.getContext("2d");
  if (!context) return color;
  context.fillStyle = color;
  context.fillRect(0, 0, 1, 1);
  const [red, green, blue, alpha] = context.getImageData(0, 0, 1, 1).data;
  return `rgba(${red}, ${green}, ${blue}, ${alpha / 255})`;
}

export const SOFT_SPRING = { stiffness: 360, damping: 34, duration: 0.42 } as const;


/* ===== components/ui/menuSurface.css · SHA256 21ce96ec107bc5dfef77f1502d741ee5cbb39feee118243d6173a56d0c70bff2 ===== */
:root {
  --ui-menu-border:color-mix(in srgb, var(--theme-border, #dfe2e7) 75%, transparent);
}
/* An inset outline stays inside the existing geometry and keeps the menu's
   continuous corners without adding a second line or changing its dimensions. */
.ui-menu-surface {
  border-width:0;
  outline:1px solid var(--ui-menu-border);
  outline-offset:-1px;
}
/* A morphing menu paints through its inner shell, not its positioning box. */
.spring-surface.ui-menu-surface[data-spring-tone="menu"] {
  outline:none;
}
.spring-surface.ui-menu-surface[data-spring-tone="menu"] > [data-spring-shell] {
  outline:1px solid var(--ui-menu-border);
  outline-offset:-1px;
}


/* ===== components/ui/useHintPositionMotion.ts · SHA256 136307d819ae37fdf35fa3f7438b3a5189205d240e09abfa6f3c7b1344b60677 ===== */
import { useLayoutEffect, useState } from "react";
import gsap from "gsap";
import { prefersReducedMotion } from "./springMotion";

/** Keep the popup mounted; smooth the positioner's anchor changes on a separate
 * wrapper so Base UI positioning and the hint's entrance transform stay intact. */
export function useHintPositionMotion(enabled: boolean) {
  const [positioner, positionerRef] = useState<HTMLDivElement | null>(null);
  const [shift, shiftRef] = useState<HTMLDivElement | null>(null);
  useLayoutEffect(() => {
    if (!enabled || !positioner || !shift) return;
    let previous: DOMRect | null = null;
    let tween: gsap.core.Tween | undefined;
    let frame = 0;
    let disposed = false;
    const remember = () => {
      if (disposed) return;
      previous = positioner.getBoundingClientRect();
    };
    // Initial anchor positioning happens after this child's layout effect.
    frame = requestAnimationFrame(() => { frame = requestAnimationFrame(remember); });
    const observer = new MutationObserver(() => {
      const next = positioner.getBoundingClientRect();
      if (previous && (Math.abs(next.left - previous.left) > .5 || Math.abs(next.top - previous.top) > .5)) {
        const x = previous.left - next.left + Number(gsap.getProperty(shift, "x"));
        const y = previous.top - next.top + Number(gsap.getProperty(shift, "y"));
        tween?.kill();
        gsap.set(shift, { x, y });
        tween = gsap.to(shift, { x: 0, y: 0, duration: prefersReducedMotion() ? 0 : .16, ease: "power2.out" });
      }
      if (previous) previous = next;
    });
    observer.observe(positioner, { attributes: true, attributeFilter: ["style"] });
    return () => {
      disposed = true;
      cancelAnimationFrame(frame);
      observer.disconnect(); tween?.kill();
      gsap.set(shift, { clearProps: "transform" });
    };
  }, [enabled, positioner, shift]);
  return { positionerRef, shiftRef };
}


/* ===== components/ui/MorphPanelLayoutContext.tsx · SHA256 e83748217bf31c93303d5591e6673937d4abcadba5ea9066c5f0b273e1f29bb2 ===== */
import { createContext, useContext } from "react";

/** Morph 面板是否正在 layout 形变；为 true 时药丸每帧重测以跟随行位 */
export const MorphPanelLayoutContext = createContext(false);

/** Keep a trigger's informational hint out of the way of its active menu. */
export const MenuTriggerOpenContext = createContext(false);

export function useMorphPanelLayoutAnimating() {
  return useContext(MorphPanelLayoutContext);
}


/* ===== components/ContextCapacityIndicator.tsx · SHA256 0278112e28646aeed49a5b9b00a606e5e846528a5afac28e2ab07451771d18f2 ===== */
import { HoverCard, HoverCardContent, HoverCardTrigger } from "./shadcn/hover-card";
import { useMemo, useState, useEffect, useRef } from "react";
import { CircularProgressbar, buildStyles } from "react-circular-progressbar";
import { Button } from "./shadcn/button";
import type { ContextUsageSnapshot } from "../types";
import { formatTokens } from "./chat-timeline/format";
import { MOTION } from "./ui/motion";

interface Props {
  snapshot?: ContextUsageSnapshot | null;
  /**
   * Provider-resolved context window for the active model, from the backend
   * capability resolver. Used when there is no snapshot yet (e.g. an empty
   * session) so the denominator is the model's real capacity, never a
   * hardcoded guess. See deepagent-models::ModelCapabilityResolver.
   */
  contextWindow?: number;
  fallbackPromptTokens?: number;
  /** Hide popover while another toolbar overlay is open. */
  popoverSuppressed?: boolean;
  /** Increment to force-close the popover. */
  overlayCloseSignal?: number;
  onPopoverOpenChange?: (open: boolean) => void;
}

function capacityTone(ratio: number, isEmptySession: boolean): string {
  if (isEmptySession) return "var(--theme-text-secondary, #6b7280)";
  if (ratio >= 0.92) return "#dc4b41";
  if (ratio >= 0.8) return "#d99015";
  return "var(--theme-text-secondary, #6b7280)";
}

/** Projected next request; legacy snapshots retain their original fallback. */
export function contextOccupancyTokens(snapshot: ContextUsageSnapshot): number {
  return snapshot.projected_request_tokens ?? snapshot.measured_prompt_tokens ?? snapshot.estimated_prompt_tokens;
}

export function ContextCapacityIndicator({
  snapshot,
  contextWindow: capabilityContextWindow = 0,
  fallbackPromptTokens = 0,
  popoverSuppressed = false,
  overlayCloseSignal = 0,
  onPopoverOpenChange,
}: Props) {
  const [open, setOpen] = useState(false);
  const lastOverlayCloseSignal = useRef(overlayCloseSignal);
  const lastReportedOpen = useRef(false);
  // The run's own snapshot wins (the runtime produced it from the same policy),
  // then the provider-resolved capability. Never a hardcoded cap.
  const contextWindow = snapshot?.context_window ?? capabilityContextWindow;
  const hasWindow = contextWindow > 0;
  const usedTokens = snapshot
    ? contextOccupancyTokens(snapshot)
    : Math.max(0, Math.round(fallbackPromptTokens));
  const isEmptySession = !snapshot && usedTokens === 0;
  // Without a denominator there is no meaningful occupancy — show an empty ring
  // rather than dividing by a placeholder.
  const ratio = !hasWindow
    ? 0
    : Math.max(0, Math.min(1, usedTokens / contextWindow));
  const percent = Math.round(ratio * 100);
  const stroke = capacityTone(ratio, isEmptySession);
  const blocks = useMemo(
    () => [...(snapshot?.blocks ?? [])].sort((a, b) => b.tokens - a.tokens),
    [snapshot?.blocks],
  );
  // Legacy snapshots may lack a complete block partition.
  const unattributed = snapshot
    ? Math.max(
        0,
        contextOccupancyTokens(snapshot) -
          blocks.reduce((sum, block) => sum + block.tokens, 0),
      )
    : 0;
  const cacheTotal = (snapshot?.cache_hit_tokens ?? 0) + (snapshot?.cache_miss_tokens ?? 0);
  const cacheRatio =
    snapshot?.cache_hit_ratio ?? (cacheTotal > 0 ? (snapshot?.cache_hit_tokens ?? 0) / cacheTotal : undefined);

  useEffect(() => {
    if (popoverSuppressed && open) setOpen(false);
  }, [popoverSuppressed, open]);

  useEffect(() => {
    if (overlayCloseSignal > lastOverlayCloseSignal.current) {
      lastOverlayCloseSignal.current = overlayCloseSignal;
      setOpen(false);
    }
  }, [overlayCloseSignal]);

  useEffect(() => {
    if (open === lastReportedOpen.current) return;
    lastReportedOpen.current = open;
    onPopoverOpenChange?.(open);
  }, [open, onPopoverOpenChange]);

  const showPopover = open && !popoverSuppressed;

  return (
    <div className="relative flex h-8 w-8 shrink-0 items-center justify-center">
      <HoverCard open={showPopover} onOpenChange={(next, details) => {
        if (details.reason === "trigger-press") details.cancel();
        else if (!popoverSuppressed) setOpen(next);
      }}>
      <HoverCardTrigger delay={120} closeDelay={150} render={<Button
        variant="ghost"
        size="icon"
        className={`h-8 w-8 rounded-full hover:bg-ui-tint ${MOTION.fast}`}
        aria-label={`Context ${percent}%`}
        onClick={() => {
          if (!popoverSuppressed) setOpen((value) => !value);
        }}
      >
        <span className="block h-4 w-4 shrink-0" aria-hidden="true">
          <CircularProgressbar
            value={ratio}
            maxValue={1}
            strokeWidth={12.5}
            className="block h-full w-full [&_.CircularProgressbar-path]:transition-[stroke-dashoffset] [&_.CircularProgressbar-path]:duration-300 motion-reduce:[&_.CircularProgressbar-path]:transition-none"
            styles={buildStyles({
              pathColor: stroke,
              trailColor: "color-mix(in srgb, var(--theme-text-secondary, #6b7280) 30%, var(--theme-bg, #ffffff))",
              strokeLinecap: "round",
            })}
          />
        </span>
      </Button>} />

      <HoverCardContent motionTone="menu" side="top" align="end" className="w-[240px] px-3.5 py-3">
          <div className="flex items-baseline justify-between gap-2">
            <span className="font-semibold">预计下一次请求</span>
            <span className="text-[18px] font-semibold leading-none">{percent}%</span>
          </div>
          <div className="mt-1.5 text-[13px] text-text-secondary">
            {hasWindow
              ? `${formatTokens(usedTokens)} / ${formatTokens(contextWindow)}`
              : formatTokens(usedTokens)}
          </div>

          {snapshot?.last_request_prompt_tokens != null && (
            <div className="mt-1.5 flex justify-between gap-2 text-[12px] text-text-secondary">
              <span>上一次请求</span><span>{formatTokens(snapshot.last_request_prompt_tokens)}</span>
            </div>
          )}
          {!!snapshot?.compaction_threshold && (
            <div className="mt-1.5 flex justify-between gap-2 text-[12px] text-text-secondary">
              <span>自动压缩阈值</span><span>{formatTokens(snapshot.compaction_threshold)}</span>
            </div>
          )}

          {blocks.length > 0 && (
            <div className="mt-2 space-y-1.5 border-t border-border-theme pt-2">
              <div className="text-[11px] text-text-tertiary">上下文组成（估算）</div>
              {blocks.map((block) => (
                <div key={`${block.kind}-${block.source}`} className="flex items-center justify-between gap-2">
                  <span className="min-w-0 truncate text-text-secondary">{block.name}</span>
                  <span className="shrink-0 font-medium">{formatTokens(block.tokens)}</span>
                </div>
              ))}
              {unattributed > 0 && (
                <div className="flex items-center justify-between gap-2">
                  <span className="min-w-0 truncate text-text-secondary">其他/未归类</span>
                  <span className="shrink-0 font-medium">{formatTokens(unattributed)}</span>
                </div>
              )}
            </div>
          )}

          {(cacheRatio != null || snapshot?.cache_hit_tokens) && (
            <div className="mt-2 flex items-center justify-between border-t border-border-theme pt-2 text-text-secondary">
              <span>上次缓存命中</span>
              <span className="font-medium text-text-base">
                {cacheRatio == null ? formatTokens(snapshot?.cache_hit_tokens ?? 0) : `${Math.round(cacheRatio * 100)}%`}
              </span>
            </div>
          )}
      </HoverCardContent>
      </HoverCard>
    </div>
  );
}


/* ===== components/shadcn/button.tsx · SHA256 fcb6ec544f5f3d2fe534bbedac5a4a429a3feb9b35dcb264af2c6dc5978d5ba9 ===== */
import * as React from "react";

import { cn } from "./utils";
import { useSpringPress } from "../ui/useSpringPress";

type ButtonVariant = "default" | "secondary" | "outline" | "ghost" | "destructive";
type ButtonSize = "default" | "sm" | "icon";

export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: ButtonVariant;
  size?: ButtonSize;
}

const variantClasses: Record<ButtonVariant, string> = {
  default: "bg-text-base text-white shadow-sm hover:bg-black",
  secondary: "bg-gray-100 text-text-base hover:bg-gray-200",
  outline: "border border-border-theme bg-white text-text-base shadow-sm hover:bg-gray-50",
  ghost: "text-text-secondary hover:bg-gray-100 hover:text-text-base",
  destructive: "bg-red-600 text-white shadow-sm hover:bg-red-700",
};

const sizeClasses: Record<ButtonSize, string> = {
  default: "h-9 px-3.5 py-2 text-[13px]",
  sm: "h-8 px-3 text-[12px]",
  icon: "h-9 w-9 p-0",
};

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant = "default", size = "default", type = "button", ...props }, ref) => {
    const pressRef = useSpringPress(ref);
    return (
    <button
      ref={pressRef}
      type={type}
      className={cn(
        "inline-flex shrink-0 items-center justify-center gap-2 rounded-lg font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:opacity-45",
        variantClasses[variant],
        sizeClasses[size],
        className,
      )}
      {...props}
    />
    );
  },
);
Button.displayName = "Button";

export { Button };


/* ===== components/ui/useSpringPress.ts · SHA256 e597399ef7eccf2572b7d7d8d30e6529ff4dcfb1a7e9f08f371cab00313b4143 ===== */
import { useCallback, useEffect, useRef, type ForwardedRef } from "react";
import gsap from "gsap";
import { prefersReducedMotion, springEase } from "./springMotion";

/** Pointer press/release only: never animate a hover or move an icon separately. */
export function useSpringPress<T extends HTMLElement>(forwardedRef?: ForwardedRef<T>) {
  const local = useRef<T | null>(null);
  const ref = useCallback((node: T | null) => {
    local.current = node;
    if (typeof forwardedRef === "function") forwardedRef(node);
    else if (forwardedRef) forwardedRef.current = node;
  }, [forwardedRef]);
  useEffect(() => {
    const node = local.current;
    if (!node) return;
    let pressed = false;
    let tween: gsap.core.Tween | undefined;
    const release = () => {
      if (!pressed) return;
      pressed = false;
      tween?.kill();
      tween = gsap.to(node, { scale: 1, duration: prefersReducedMotion() ? 0 : 0.38,
        ease: springEase(420, 24, 0.38) });
    };
    const down = () => {
      if (node.matches(":disabled") || prefersReducedMotion()) return;
      pressed = true; tween?.kill();
      tween = gsap.to(node, { scale: 0.97, duration: 0.075, ease: "power2.out" });
    };
    node.addEventListener("pointerdown", down);
    window.addEventListener("pointerup", release);
    window.addEventListener("pointercancel", release);
    window.addEventListener("blur", release);
    return () => {
      node.removeEventListener("pointerdown", down);
      window.removeEventListener("pointerup", release);
      window.removeEventListener("pointercancel", release);
      window.removeEventListener("blur", release);
      tween?.kill(); gsap.set(node, { clearProps: "scale" });
    };
  }, []);
  return ref;
}


/* ===== components/chat-timeline/format.ts · SHA256 44e1335aeac093927315c9fa1029a97f024e640cc23e9c040f3690221c68c6ce ===== */
const CNY_SYMBOL = "\uFFE5";

export function formatMs(ms: number): string {
  if (ms < 1000) return `${ms}ms`;
  const seconds = ms / 1000;
  if (seconds < 60) return `${seconds.toFixed(1)}s`;
  const minutes = Math.floor(seconds / 60);
  const rest = Math.round(seconds % 60);
  return `${minutes}m${rest}s`;
}

/**
 * The app's one token formatter: the per-turn footer, the session totals bar and
 * the context-capacity ring all render the same number, so they must not carry
 * separate rounding rules (they previously disagreed — `16k` vs `15.8k`).
 */
export function formatTokens(value: number): string {
  const n = Math.max(0, Math.round(value));
  if (n < 1000) return `${n}`;
  // 999_500 rounds to "1M" rather than the "1000k" a plain 1e6 cutoff produces.
  if (n < 999_500) return `${formatUnit(n / 1000)}k`;
  return `${formatUnit(n / 1_000_000)}M`;
}

/** One decimal place, without a trailing `.0`. */
function formatUnit(value: number): string {
  const rounded = Math.round(value * 10) / 10;
  return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
}

export function formatCny(n: number): string {
  if (n <= 0) return `${CNY_SYMBOL}0`;
  if (n < 0.01) return `${CNY_SYMBOL}${n.toFixed(6)}`;
  if (n < 1) return `${CNY_SYMBOL}${n.toFixed(4)}`;
  return `${CNY_SYMBOL}${n.toFixed(2)}`;
}

export const cnySymbol = CNY_SYMBOL;


/* ===== components/ui/motion.ts · SHA256 b9bdc69fce350cefb8676c959bf51b10cb0f2ca8b461136cc8343ac3b357fa9f ===== */
/**
 * 统一动效语言（全局唯一来源）：
 * - fast：150ms —— 按钮、列表行等小元素的 hover/激活反馈
 * - standard：300ms ease-out（cubic-bezier(0.4,0,0.2,1)）—— 滑块、面板等中等动效
 * - smooth：400ms —— 大面板展开、较大过渡
 * ease-out 即 Tailwind 默认的 cubic-bezier(0.4, 0, 0.2, 1)。
 */
export const MOTION = {
  fast: "transition-colors duration-150",
  standard: "transition-all duration-300 ease-out",
  smooth: "transition-all duration-[400ms] ease-out",
} as const;

/** Selected navigation feel: one interruptible highlight, with a short exit. */
export const NAVIGATION_MOTION = {
  duration: 0.22,
  ease: "back.out(0.7)",
  exitDuration: 0.07,
  enterDuration: 0.055,
  tooltipDelay: 160,
} as const;

/** 二级浮层菜单（A 视觉 + H 动效）—— 配合 styles.css 中 .floating-menu-panel */
export const FLOATING_MENU = {
  panel: "floating-menu-panel ui-menu-surface",
  item: "floating-menu-item",
  shell:
    "floating-menu-panel rounded-2xl bg-elevated-bg p-1.5 shadow-[0_6px_24px_rgba(0,0,0,0.10)]",
  row:
    "floating-menu-item flex w-full cursor-pointer items-center rounded-lg px-2 py-1.5 text-[12px] transition-colors duration-150 hover:bg-ui-tint",
} as const;

/** 浮层内列表（方案 A）—— scroll/actions 同 gutter，行同 padding，hover 等宽 */
export const MENU_LIST = {
  block: "p-2",
  gutter: "px-1.5 py-1",
  /** 滑动药丸内缩 —— 与 shell p-1.5 叠加，行 hover 不贴边 */
  pillInset: "left-1.5 right-1.5",
  row: "w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-[12px]",
  rowCompact: "w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-[11px]",
  section: "px-2.5 pb-0.5 pt-1.5 text-[10px] font-medium text-text-secondary",
  icon: "mr-2 w-3.5 shrink-0 text-[13px] text-text-secondary",
  empty: "px-2.5 py-2 text-[12px] text-text-secondary",
  search: "flex items-center text-text-secondary",
  searchInput: "w-full bg-transparent outline-none placeholder:text-text-secondary",
  /** 搜索行 —— 与项目下拉对齐：13px、充足上下留白 */
  searchBar: "flex items-center text-[13px] text-text-secondary",
  searchBarPad: "px-3 pb-2.5 pt-3",
  searchBarPadInShell: "px-2 py-2",
  /** 方案 D：内缩短线，弱化全宽 border-t */
  divider: "mx-3.5 my-1.5 h-px shrink-0 bg-border-theme opacity-[0.55]",
} as const;


/* ===== components/ComposerSuggestPanel.tsx · SHA256 ce190e949289b7d82e2eccb90acbe71d022158b2e5da88902e7f30b67ab57e0d ===== */
import { useLayoutEffect, useMemo, useRef, type ReactNode } from "react";
import { Panel } from "./ui/Panel";
import { HoverInfo } from "./ui/HoverInfo";
import { MENU_ITEM_ATTR, SlidingMenuList } from "./ui/SlidingMenuList";

export type ComposerSuggestSection<T> = {
  key: string;
  label: string;
  items: T[];
};



interface Props<T> {
  id: string;
  open: boolean;
  sections: ComposerSuggestSection<T>[];
  selectedIndex: number;
  getKey: (item: T) => string;
  renderItem: (item: T, selected: boolean) => ReactNode;
  renderTooltip?: (item: T) => ReactNode;
  onSelect: (item: T) => void;
  onHover: (index: number) => void;
  className?: string;
}

export function ComposerSuggestPanel<T>({
  id,
  open,
  sections,
  selectedIndex,
  getKey,
  renderItem,
  renderTooltip,
  onSelect,
  onHover,
  className = "",
}: Props<T>) {
  const panelRef = useRef<HTMLDivElement>(null);
  const scrollRef = useRef<HTMLDivElement>(null);
  const activeId = useMemo(() => {
    let index = 0;
    for (const section of sections) {
      for (const item of section.items) {
        if (index === selectedIndex) return getKey(item);
        index += 1;
      }
    }
    return "";
  }, [sections, selectedIndex, getKey]);

  useLayoutEffect(() => {
    const panel = panelRef.current;
    const anchor = panel?.parentElement;
    if (!open || !panel || !anchor) return;
    const resize = () => {
      const top = anchor.closest(".app-main-surface")?.getBoundingClientRect().top ?? 0;
      // Leave 8px above/below the shell, plus its two 6px padding gutters.
      const available = Math.max(0, anchor.getBoundingClientRect().top - Math.max(0, top) - 28);
      panel.style.setProperty("--suggest-list-height", `${Math.min(320, available)}px`);
    };
    resize();
    const observer = new ResizeObserver(resize);
    observer.observe(anchor);
    window.addEventListener("resize", resize);
    return () => { observer.disconnect(); window.removeEventListener("resize", resize); };
  }, [open]);

  useLayoutEffect(() => {
    const scroll = scrollRef.current;
    const selected = scroll?.querySelector<HTMLElement>('[aria-selected="true"]');
    if (!open || !scroll || !selected) return;
    const bounds = scroll.getBoundingClientRect(), row = selected.getBoundingClientRect();
    if (row.top < bounds.top) scroll.scrollTop -= bounds.top - row.top;
    else if (row.bottom > bounds.bottom) scroll.scrollTop += row.bottom - bounds.bottom;
  }, [open, activeId]);

  if (!open) return null;
  return (
    <>
      <Panel
        ref={panelRef}
        data-composer-suggest
        className={`absolute inset-x-0 bottom-full z-50 mb-2 w-auto overflow-hidden p-1.5 ${className}`}
      >
      <div ref={scrollRef} className="composer-suggest-scroll custom-scrollbar max-h-[var(--suggest-list-height,240px)] overflow-y-auto overscroll-contain">
      <SlidingMenuList id={id} role="listbox" aria-label={sections.map(section => section.label).join("、")} activeId={activeId} pillClassName="left-0 right-0">
        {sections.map((section, sectionIndex) => {
          let baseIndex = 0;
          for (let i = 0; i < sectionIndex; i += 1) {
            baseIndex += sections[i].items.length;
          }
          return (
            <div key={section.key} className={sectionIndex > 0 ? "mt-1" : ""}>
              <div className="px-2 py-1.5 text-[12px] font-medium text-text-secondary">
                {section.label}
              </div>
              {section.items.map((item, index) => {
                const flatIndex = baseIndex + index;
                const selected = flatIndex === selectedIndex;
                const itemKey = getKey(item);
                const button = (
                  <button
                    key={itemKey}
                    type="button"
                    role="option"
                    id={`${id}-option-${flatIndex}`}
                    aria-selected={selected}
                    tabIndex={-1}
                    {...{ [MENU_ITEM_ATTR]: itemKey }}
                    onMouseDown={(event) => {
                      event.preventDefault();
                      onSelect(item);
                    }}
                    onMouseEnter={() => onHover(flatIndex)}
                    className="relative z-[1] w-full text-left"
                  >
                    {renderItem(item, selected)}
                  </button>
                );
                return renderTooltip ? (
                  <HoverInfo key={itemKey} content={renderTooltip(item)}>{button}</HoverInfo>
                ) : (
                  button
                );
              })}
            </div>
          );
        })}
      </SlidingMenuList>
      </div>
      </Panel>
    </>
  );
}


/* ===== components/ui/Panel.tsx · SHA256 bd91784bf10998950b2fb823c5e1c9bd49969c2b3599ae0888d0123397a232b2 ===== */
import { forwardRef, type HTMLAttributes } from "react";
import { cn } from "../shadcn/utils";
import { FLOATING_MENU } from "./motion";

/**
 * 浮层容器（下拉菜单/弹层面板）—— 浅色细边线、bg-elevated-bg + 柔和阴影。
 * menu=true 时附加入场动效（方案 H）；模态框等传 menu={false}。
 */
export const Panel = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement> & { menu?: boolean }>(
  function Panel({ className, menu = true, ...rest }, ref) {
    return (
      <div
        ref={ref}
        className={cn(
          "rounded-2xl bg-elevated-bg shadow-[0_6px_24px_rgba(0,0,0,0.10)]",
          menu && FLOATING_MENU.panel,
          className,
        )}
        {...rest}
      />
    );
  },
);


/* ===== components/ui/SlidingMenuList.tsx · SHA256 0b94108f7a0397b9cd17b70655a7b88404428516f1c7ff7c91ed398f9520f7b2 ===== */
import type { HTMLAttributes, ReactNode } from "react";
import { cn } from "../shadcn/utils";
import { MENU_LIST } from "./motion";
import { useSlidingIndicator, SlidingPill } from "./SlidingPill";

/** 浮层菜单行统一 data 属性，配合 SlidingMenuList 使用 */
export const MENU_ITEM_ATTR = "data-menu-item";

type Props = HTMLAttributes<HTMLDivElement> & {
  activeId: string;
  pillClassName?: string;
  children: ReactNode;
};

/**
 * 浮层内列表滑动药丸 —— 与侧栏/设置侧栏同款交互：
 * 悬停跟随，离开滑回当前选中项；行元素须带 `data-menu-item={id}` 与 `relative z-[1]`。
 */
export function SlidingMenuList({
  activeId,
  pillClassName,
  className,
  children,
  ...rest
}: Props) {
  const { containerRef, containerProps, indicatorStyle } = useSlidingIndicator({
    hoverSelector: `[${MENU_ITEM_ATTR}]`,
    activeSelector: `[${MENU_ITEM_ATTR}="${activeId}"]`,
  });

  return (
    <div ref={containerRef} {...containerProps} className={cn("relative w-full overflow-hidden", className)} {...rest}>
      {children}
      <SlidingPill
        className={cn("rounded-lg", MENU_LIST.pillInset, pillClassName)}
        style={indicatorStyle}
      />
    </div>
  );
}


/* ===== components/ui/SlidingPill.tsx · SHA256 51272803185667843f31840c32128c742d8d59947f9fbcfa4e29c365f78833ff ===== */
import { useCallback, useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type FocusEvent, type MouseEvent } from "react";
import gsap from "gsap";
import { cn } from "../shadcn/utils";
import { MOTION } from "./motion";

/**
 * 滑动指示器（静默着色药丸）—— 统一侧栏/导航的悬停滑动交互。
 *
 * 用法：
 *   const { containerRef, containerProps, indicatorStyle } = useSlidingIndicator({
 *     hoverSelector: "[data-nav]",            // 悬停时药丸跟随的目标
 *     activeSelector: `[data-nav="${active}"]` // 鼠标离开后药丸停靠的位置
 *   });
 *   return (
 *     <div ref={containerRef} {...containerProps} className="relative ...">
 *       ...按钮（须带 data-* 属性与 z-[1]）...
 *       <SlidingPill style={indicatorStyle} />
 *     </div>
 *   );
 *
 * 注意：容器内按钮必须 `relative z-[1]`（在药丸 z-0 之上），否则药丸会遮住文字。
 *
 * 测量用布局坐标（offsetTop/offsetHeight）而非 getBoundingClientRect：
 * morph 形变中容器被 framer-motion 以 transform: scale 变换，rect 返回被
 * 缩放的视口坐标（实测行高被压缩数倍），offset* 不受 transform 影响，
 * 任何时刻都返回真实布局值，因此药丸在面板打开瞬间即可正确显示。
 */
export function useSlidingIndicator({
  hoverSelector,
  activeSelector,
  followFocus = false,
}: {
  hoverSelector: string;
  activeSelector: string;
  followFocus?: boolean;
}) {
  const containerRef = useRef<HTMLDivElement>(null);
  const hoveredRef = useRef<Element | null>(null);
  const focusedRef = useRef<Element | null>(null);
  const [pill, setPill] = useState<{ top: number; height: number } | null>(null);
  /* 首帧禁用过渡：面板打开时滑块直接以最终形态出现（跳过 height 0→67 的过渡），
     下一帧后恢复 class 过渡，hover 跟随/切换行仍平滑 */
  const [settled, setSettled] = useState(false);
  useEffect(() => {
    const t = window.setTimeout(() => setSettled(true), 0);
    return () => clearTimeout(t);
  }, []);

  const positionPillOn = useCallback((el: Element | null | undefined) => {
    if (!(el instanceof HTMLElement)) return;
    const container = containerRef.current;
    if (!container) return;
    setPill({
      top: el.offsetTop + container.scrollTop,
      height: el.offsetHeight,
    });
  }, []);

  const positionOnActive = useCallback(() => {
    const c = containerRef.current;
    if (!c) return;
    const el = c.querySelector(activeSelector);
    if (!el) {
      setPill(null);
      return;
    }
    positionPillOn(el);
  }, [activeSelector, positionPillOn]);

  const repositionPill = useCallback(() => {
    const target = hoveredRef.current ?? focusedRef.current;
    if (target) {
      positionPillOn(target);
    } else {
      positionOnActive();
    }
  }, [positionOnActive, positionPillOn]);

  /* 挂载 & 激活项变化时，药丸滑到激活项。
     useLayoutEffect：面板打开的首帧（paint 前）就同步测好位置，
     滑块直接以最终形态出现，不会先渲染空状态再过渡 */
  useLayoutEffect(() => {
    if (followFocus) repositionPill();
    else positionOnActive();
  }, [followFocus, positionOnActive, repositionPill]);

  /* 容器尺寸变化时重新测量（hover 时跟 hover 行） */
  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;
    const ro = new ResizeObserver(() => repositionPill());
    ro.observe(container);
    return () => ro.disconnect();
  }, [repositionPill]);

  const handleMouseOver = useCallback(
    (e: MouseEvent<HTMLDivElement>) => {
      const btn = (e.target as HTMLElement).closest(hoverSelector);
      if (btn) {
        hoveredRef.current = btn;
        positionPillOn(btn);
      }
    },
    [hoverSelector, positionPillOn],
  );

  const handleMouseLeave = useCallback(() => {
    hoveredRef.current = null;
    repositionPill();
  }, [repositionPill]);

  const handleFocus = useCallback((event: FocusEvent<HTMLDivElement>) => {
    const target = event.target.closest(hoverSelector);
    focusedRef.current = target?.matches(":focus-visible") ? target : null;
    if (focusedRef.current) hoveredRef.current = null;
    repositionPill();
  }, [hoverSelector, repositionPill]);

  const handleBlur = useCallback((event: FocusEvent<HTMLDivElement>) => {
    if (!event.currentTarget.contains(event.relatedTarget)) {
      focusedRef.current = null;
      repositionPill();
    }
  }, [repositionPill]);

  return {
    containerRef,
    containerProps: {
      onMouseOver: handleMouseOver, onMouseLeave: handleMouseLeave,
      onFocusCapture: followFocus ? handleFocus : undefined,
      onBlurCapture: followFocus ? handleBlur : undefined,
    },
    indicatorStyle: {
      top: pill?.top ?? 0,
      height: pill?.height ?? 0,
      opacity: pill ? 1 : 0,
      /* 首帧无过渡（直接显示最终形态），settled 后走 class 的 transition-all */
      transition: settled ? undefined : "none",
    } as CSSProperties,
  };
}

/** 静默着色药丸 —— 默认 ui-tint；侧栏传 className="bg-sidebar-highlight" */
const PILL_BASE = `ui-hover-highlight absolute left-3 right-3 z-0 rounded-md pointer-events-none ${MOTION.standard}`;

type PillMotion = { duration: number; ease: string; exitDuration: number; enterDuration: number };
type PillTarget = { top: number; height: number; visible: boolean; immediate: boolean };

function createPillMotion(node: HTMLDivElement, motion: PillMotion) {
  const media = window.matchMedia("(prefers-reduced-motion: reduce)");
  const context = gsap.context(() => {}, node);
  let target: PillTarget = { top: 0, height: 0, visible: false, immediate: true };
  const paint = () => context.add(() => {
    gsap.killTweensOf(node);
    if (media.matches || target.immediate) {
      gsap.set(node, { y: target.top, height: target.height, opacity: target.visible ? 1 : 0 });
    } else if (!target.visible) {
      gsap.to(node, { opacity: 0, duration: motion.exitDuration, overwrite: true });
    } else if (Number(gsap.getProperty(node, "opacity")) <= .01) {
      gsap.set(node, { y: target.top, height: target.height, opacity: 0 });
      gsap.to(node, { opacity: 1, duration: motion.enterDuration, overwrite: true });
    } else {
      gsap.to(node, { y: target.top, height: target.height, opacity: 1,
        duration: motion.duration, ease: motion.ease, overwrite: true });
    }
  });
  const reduce = () => {
    gsap.killTweensOf(node);
    context.add(() => gsap.set(node, { y: target.top, height: target.height, opacity: target.visible ? 1 : 0 }));
  };
  media.addEventListener("change", reduce);
  return {
    moveTo(next: PillTarget) { target = next; paint(); },
    dispose() { media.removeEventListener("change", reduce); context.revert(); },
  };
}

export function SlidingPill({ style, className, motion }: { style: CSSProperties; className?: string; motion?: PillMotion }) {
  const nodeRef = useRef<HTMLDivElement>(null);
  const controller = useRef<ReturnType<typeof createPillMotion> | null>(null);
  useLayoutEffect(() => {
    if (!motion || !nodeRef.current) return;
    const instance = createPillMotion(nodeRef.current, motion);
    controller.current = instance;
    return () => { instance.dispose(); controller.current = null; };
  }, [motion]);
  useLayoutEffect(() => {
    controller.current?.moveTo({ top: Number(style.top ?? 0), height: Number(style.height ?? 0),
      visible: Number(style.opacity ?? 0) > 0, immediate: style.transition === "none" });
  }, [motion, style.top, style.height, style.opacity, style.transition]);
  const hasBgClass = Boolean(className && /\bbg-/.test(className));
  const { top: _top, height: _height, opacity: _opacity, ...rest } = style;
  return (
    <div
      ref={nodeRef}
      aria-hidden
      className={cn(PILL_BASE, !hasBgClass && "bg-ui-tint", className, motion && "transition-none")}
      style={motion ? { ...rest, top: 0, transition: "none", willChange: "transform" } : style}
    />
  );
}


/* ===== components/ModelThinkingSelector.tsx · SHA256 5e6926fd47780fd7977255f997226b8a13a234cac3cac3683d2627eced4ce7ff ===== */
import { HoverInfo } from "./ui/HoverInfo";
import { useId } from "react";
import { Check, ChevronDown, type LucideIcon } from "lucide-react";

import { EffortSlider } from "./ui/EffortSlider";

import { ListItem } from "./ui/ListItem";

import { TintButton } from "./ui/TintButton";

import { MorphingMenuShell } from "./ui/MorphingMenuShell";

import { MENU_ITEM_ATTR, SlidingMenuList } from "./ui/SlidingMenuList";

import { cn } from "./shadcn/utils";



type ThinkingDepth = "simple" | "medium" | "deep";



type ThinkingOption = {

  id: ThinkingDepth;

  label: string;

  icon: LucideIcon;

};



type Props = {

  open: boolean;

  disabled?: boolean;

  switching?: boolean;

  models: string[];

  selectedModel: string;

  selectedThinking: ThinkingDepth;

  thinkingOptions: readonly ThinkingOption[];

  selectModelLabel: string;

  noModelsLabel: string;

  onOpenChange: (open: boolean) => void;

  onChooseModel: (id: string) => void;

  onChooseThinking: (id: ThinkingDepth) => void;

  triggerClassName?: string;

};



function modelLabel(id: string): string {
  if (!id) return "";
  return id;
}


/** 标题、选项容器与滑轨统一外沿，选项文字保留独立内边距。 */
const MODEL_MENU = {
  padX: "px-3",
  divider: "mx-3 my-2 h-px shrink-0 bg-border-theme opacity-[0.55]",
  section: "flex items-center py-1 text-[12px]",
  pill: "left-0 right-0 rounded-lg",
} as const;



export function ModelThinkingSelector({

  open,

  disabled = false,

  switching = false,

  models,

  selectedModel,

  selectedThinking,

  thinkingOptions,

  selectModelLabel,

  noModelsLabel,

  onOpenChange,

  onChooseModel,

  onChooseThinking,

  triggerClassName,

}: Props) {

  const morphLayoutId = useId().replace(/:/g, "");

  const selectedThinkingOption =

    thinkingOptions.find((option) => option.id === selectedThinking) ?? thinkingOptions[1];

  const pillModel = selectedModel ? modelLabel(selectedModel) : selectModelLabel;

  const trigger = (
    <HoverInfo content={`${selectModelLabel} / ${selectedThinkingOption.label}`}><TintButton
      type="button"
      disabled={disabled}
      onClick={() => onOpenChange(!open)}
      className={cn("flex h-8 max-w-[210px] flex-shrink-0 items-center rounded-full px-3 text-xs", triggerClassName)}

    >
      <selectedThinkingOption.icon size={12} className="mr-1.5 text-text-secondary" />
      <span className="font-medium">{pillModel}</span>
      <span className="ml-1.5 shrink-0 text-text-secondary">{selectedThinkingOption.label}</span>
      <ChevronDown size={10} className="ml-2 text-text-secondary" />
    </TintButton></HoverInfo>
  );

  return (
    <MorphingMenuShell
      open={open}
      onOpenChange={onOpenChange}
      layoutId={morphLayoutId}
      trigger={trigger}
      className="flex h-8 min-w-0 max-w-full items-center"
      panelClassName="w-[264px]"
      panelAlign="right"
      zIndex={50}
    >
          <div className="py-2">

          <div className={cn(MODEL_MENU.padX, "pb-1")}>

            <div className={MODEL_MENU.section}>

              <span className="font-medium text-text-base">模型</span>


            </div>



            <div className="max-h-[96px] overflow-y-auto">

              {models.length === 0 ? (

                <div className="rounded-lg px-2.5 py-2 text-[12px] text-text-secondary">{noModelsLabel}</div>

              ) : (

                <SlidingMenuList activeId={selectedModel} pillClassName={MODEL_MENU.pill} className="w-full py-0.5">

                  {models.map((id) => {

                    const selected = id === selectedModel;

                    return (

                      <ListItem

                        key={id}

                        {...{ [MENU_ITEM_ATTR]: id }}

                        sliding

                        selected={selected}

                        onClick={switching ? undefined : () => onChooseModel(id)}

                        className={cn(
                          "cursor-pointer px-2.5 py-2 text-left",
                          selected ? "text-text-base" : "text-text-secondary",
                        )}

                      >

                        <span className="font-medium">{modelLabel(id)}</span>

                        {selected && <Check size={10} className="ml-3 text-text-base" />}

                      </ListItem>

                    );

                  })}

                </SlidingMenuList>

              )}

            </div>

          </div>



          <div className={MODEL_MENU.divider} />



          <div className="px-3 pb-1 pt-0.5">

            <div className="mb-2.5 flex items-center text-[12px]">

              <span className="font-medium text-text-base">推理强度</span>

            </div>

            <EffortSlider

              stops={thinkingOptions.map((option) => ({
                value: option.id,
                label: option.label,
                icon: option.icon,
              }))}

              value={selectedThinking}

              onChange={(value) => onChooseThinking(value as ThinkingDepth)}

              ariaLabel="推理强度"
              disabled={disabled}

            />

          </div>

          </div>
    </MorphingMenuShell>
  );

}



/* ===== components/ui/EffortSlider.tsx · SHA256 d5f6bc4a829aa2fdca121677f6c2f3ac0257a820700012c7cdd16fb80659efa3 ===== */
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { Slider as SliderPrimitive } from "../shadcn/slider";
import { Button } from "../shadcn/button";
import type { SliderStop } from "./Slider";
import { createEffortMotion } from "./effortMotion";

type Gesture = { id: number; startX: number; lastX: number; time: number; moved: boolean };

type Props<T extends string> = {
  stops: SliderStop<T>[];
  value: T;
  onChange: (value: T) => void;
  ariaLabel: string;
  disabled?: boolean;
};

export function EffortSlider<T extends string>({ stops, value, onChange, ariaLabel, disabled }: Props<T>) {
  const [reducedMotion, setReducedMotion] = useState(() => window.matchMedia("(prefers-reduced-motion: reduce)").matches);
  const active = Math.max(0, stops.findIndex(stop => stop.value === value));
  const max = Math.max(0, stops.length - 1);
  const [preview, setPreview] = useState(active);
  const [dragging, setDragging] = useState(false);
  const previewRef = useRef(active);
  const draggingRef = useRef(false);
  const gesture = useRef<Gesture | null>(null);
  const rootRef = useRef<HTMLDivElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const motion = useRef<ReturnType<typeof createEffortMotion> | null>(null);
  const selected = Math.min(max, Math.max(0, Math.round(preview)));
  const ratio = max ? preview / max : 0;

  const updatePreview = (next: number) => {
    previewRef.current = Math.max(0, Math.min(max, next));
    setPreview(previewRef.current);
  };
  const commit = (next: number) => {
    const index = Math.max(0, Math.min(max, Math.round(next)));
    updatePreview(index);
    if (stops[index] && stops[index].value !== value) onChange(stops[index].value);
  };
  const cancel = () => {
    draggingRef.current = false;
    gesture.current = null;
    setDragging(false);
    updatePreview(active);
    motion.current?.release();
  };

  useEffect(() => {
    const media = window.matchMedia("(prefers-reduced-motion: reduce)");
    const update = () => setReducedMotion(media.matches);
    update();
    media.addEventListener("change", update);
    return () => media.removeEventListener("change", update);
  }, []);

  useEffect(() => {
    // Respect optimistic updates and a parent's rollback after a failed save.
    if (!draggingRef.current) {
      previewRef.current = active;
      setPreview(active);
    }
  }, [active]);

  useLayoutEffect(() => {
    const root = rootRef.current;
    const canvas = canvasRef.current;
    if (!root || !canvas) return;
    const controller = createEffortMotion(root, canvas, ratio, reducedMotion);
    motion.current = controller;
    return () => {
      controller.dispose();
      motion.current = null;
    };
  }, []);

  useLayoutEffect(() => { motion.current?.moveTo(ratio, dragging); }, [ratio, dragging]);
  useLayoutEffect(() => { motion.current?.setReducedMotion(reducedMotion); }, [reducedMotion]);

  return (
    <div ref={rootRef} className="reasoning-effort" data-dragging={dragging || undefined}
      data-maximum={selected === max || undefined} data-disabled={disabled || undefined}>
      <SliderPrimitive appearance="effort" min={0} max={Math.max(1, max)} step={0.001}
        value={[preview]} disabled={disabled || max === 0}
        thumbProps={{ "aria-label": ariaLabel, "aria-valuenow": selected, "aria-valuetext": stops[selected]?.label }}
        onValueChange={([next]) => updatePreview(next)} onValueCommit={([next]) => commit(next)}
        onPointerDown={event => { if (!event.isPrimary || event.button !== 0) event.preventDefault(); }}
        onPointerDownCapture={event => {
          if (disabled || max === 0 || !event.isPrimary || event.button !== 0) return;
          gesture.current = { id: event.pointerId, startX: event.clientX, lastX: event.clientX, time: event.timeStamp, moved: false };
          motion.current?.press();
        }}
        onPointerMoveCapture={event => {
          const current = gesture.current;
          if (!current || event.pointerId !== current.id) return;
          if (!current.moved && Math.abs(event.clientX - current.startX) < 4) return;
          current.moved = true;
          draggingRef.current = true;
          setDragging(true);
          const velocity = (event.clientX - current.lastX) / Math.max(8, event.timeStamp - current.time);
          motion.current?.drag(velocity);
          current.lastX = event.clientX;
          current.time = event.timeStamp;
        }}
        onPointerUpCapture={event => {
          if (event.pointerId !== gesture.current?.id) return;
          gesture.current = null;
          draggingRef.current = false;
          setDragging(false);
          updatePreview(Math.round(previewRef.current));
          motion.current?.release();
        }}
        onPointerCancel={cancel}
        onLostPointerCapture={() => { if (gesture.current) cancel(); }}
        onKeyDownCapture={event => {
          let next = selected;
          if (["ArrowRight", "ArrowUp", "PageUp"].includes(event.key)) next++;
          else if (["ArrowLeft", "ArrowDown", "PageDown"].includes(event.key)) next--;
          else if (event.key === "Home") next = 0;
          else if (event.key === "End") next = max;
          else return;
          event.preventDefault();
          event.stopPropagation();
          if (!disabled) commit(next);
        }}>
        <div className="reasoning-effort-fill" aria-hidden="true"><canvas ref={canvasRef} /></div>
        <div className="reasoning-effort-dots" aria-hidden="true">
          {stops.map(stop => <span key={stop.value} />)}
        </div>
        <span className="reasoning-effort-thumb" aria-hidden="true" />
      </SliderPrimitive>
      <div className="reasoning-effort-stops">
        {stops.map((stop, index) => (
          <Button key={stop.value} variant="ghost" size="sm" disabled={disabled} aria-pressed={index === selected}
            className="h-auto rounded px-0.5 py-[3px] text-[11px] font-normal hover:bg-transparent"
            onClick={() => commit(index)}>{stop.label}</Button>
        ))}
      </div>
    </div>
  );
}


/* ===== components/shadcn/slider.tsx · SHA256 574612f1a7ae603fccae8023a351adc1a8ddf9f9e2a6c7e8fb8d06c3010d4d22 ===== */
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "./utils";

type SliderProps = React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & {
  appearance?: "default" | "effort";
  thumbProps?: React.ComponentPropsWithoutRef<typeof SliderPrimitive.Thumb>;
};

const Slider = React.forwardRef<
  React.ElementRef<typeof SliderPrimitive.Root>,
  SliderProps
>(({ className, appearance = "default", children, thumbProps, ...props }, ref) => (
  <SliderPrimitive.Root
    ref={ref}
    className={cn("relative flex w-full touch-none select-none items-center", appearance === "effort" && "reasoning-effort-input", className)}
    {...props}
  >
    <SliderPrimitive.Track className={appearance === "effort" ? "reasoning-effort-track" : "relative h-1.5 w-full grow overflow-hidden rounded-full bg-white/10"}>
      <SliderPrimitive.Range className={appearance === "effort" ? "sr-only" : "absolute h-full bg-white/80"} />
      {children}
    </SliderPrimitive.Track>
    <SliderPrimitive.Thumb
      {...thumbProps}
      className={appearance === "effort" ? "reasoning-effort-input-thumb" : cn(
        "block h-3.5 w-3.5 rounded-full border border-white/40 bg-white shadow-[0_2px_8px_rgba(0,0,0,0.35)] transition-transform",
        "hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40",
        "disabled:pointer-events-none disabled:opacity-50",
      )}
    />
  </SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;

export { Slider };


/* ===== components/ui/effortMotion.ts · SHA256 49f87a7098c84e2e18ef520398c54eccace5d74ceef5a15a964cc088c5852bb8 ===== */
import { gsap } from "gsap";
import { createEffortField } from "./effortField";
import { springEase } from "./springMotion";

// Position keeps A's measured, non-overshooting 500 ms glide.
const SPRING = [[0, 0], [.02, .015], [.04, .0532], [.06, .1065], [.08, .1686],
  [.1, .2351], [.13, .3363], [.16, .4332], [.2, .5495], [.24, .648], [.28, .7287],
  [.33, .807], [.38, .8646], [.44, .9128], [.5, .9447], [.57, .968], [.65, .9834],
  [.74, .9924], [.85, .9975], [1, 1]];
function glideEase(progress: number) {
  const end = SPRING.findIndex(([x]) => x >= progress);
  if (end <= 0) return end === 0 ? 0 : 1;
  const [x0, y0] = SPRING[end - 1];
  const [x1, y1] = SPRING[end];
  return y0 + (y1 - y0) * (progress - x0) / (x1 - x0);
}

// Local design: material deformation springs back while the selected stop stays fixed.
const returnEase = springEase(400, 23, 0.72);

/** One motion implementation shared by the desktop component and its review file. */
export function createEffortMotion(
  root: HTMLElement,
  canvas: HTMLCanvasElement,
  initialRatio: number,
  initialReducedMotion: boolean,
) {
  const state = { ratio: initialRatio, press: 1, body: 0, lean: 0, boost: 1 };
  let targetRatio = initialRatio;
  let reduced = initialReducedMotion;
  let attached = false;
  let disposed = false;
  let positionTween: gsap.core.Tween | undefined;
  let pressTween: gsap.core.Tween | undefined;
  let shapeTween: gsap.core.Timeline | gsap.core.Tween | undefined;
  let boostTween: gsap.core.Tween | undefined;
  const field = createEffortField(canvas, () => state.ratio);
  const variables = () => ({
    "--effort-ratio": String(state.ratio),
    "--effort-press": String(state.press),
    "--effort-jelly-x": String(1 + state.body),
    "--effort-jelly-y": String(1 - state.body * 0.3),
    "--effort-jelly-offset": `${-state.lean * 9}px`,
    "--effort-jelly-skew": `${-state.lean * 12}deg`,
    "--effort-jelly-left": `${50 + state.lean * 32}%`,
    "--effort-jelly-right": `${50 - state.lean * 24}%`,
  });
  const context = gsap.context(() => { gsap.set(root, variables()); }, root);
  const paint = () => {
    if (disposed) return;
    for (const [name, value] of Object.entries(variables())) root.style.setProperty(name, value);
    if (reduced) field.draw(gsap.ticker.time, true);
  };
  const draw = (time: number) => {
    if (!document.hidden) field.draw(time, false, state.boost);
  };
  const syncTicker = () => {
    if (attached) gsap.ticker.remove(draw);
    attached = !reduced;
    field.draw(gsap.ticker.time, true);
    if (attached) gsap.ticker.add(draw);
  };
  const stopTweens = () => {
    positionTween?.kill();
    pressTween?.kill();
    shapeTween?.kill();
    boostTween?.kill();
  };
  syncTicker();

  return {
    moveTo(ratio: number, immediate: boolean) {
      targetRatio = Math.max(0, Math.min(1, ratio));
      positionTween?.kill();
      positionTween = gsap.to(state, {
        ratio: targetRatio, duration: immediate || reduced ? 0 : 0.5,
        ease: glideEase, onUpdate: paint,
      });
    },
    press() {
      pressTween?.kill();
      pressTween = gsap.to(state, { press: reduced ? 1 : 0.96, duration: reduced ? 0 : 0.08, onUpdate: paint });
    },
    drag(velocity: number) {
      if (reduced || disposed) return;
      const speed = Math.min(3.5, Math.abs(velocity));
      const deformation = Math.min(0.46, 0.045 + speed * 0.17);
      const lean = Math.sign(velocity) * deformation;
      shapeTween?.kill();
      // On reversal, lean crosses through its previous value instead of flipping instantly.
      // When the pointer pauses, the same material relaxes without waiting for pointerup.
      shapeTween = gsap.timeline({ onUpdate: paint })
        .to(state, { body: deformation, lean, duration: 0.09, ease: "power2.out" })
        .to(state, { body: 0, lean: 0, duration: 0.72, ease: returnEase }, ">0.07");
      boostTween?.kill();
      boostTween = gsap.to(state, {
        boost: 1 + Math.min(2.2, 0.55 + speed * 0.6), duration: 0.12,
        ease: "power2.out",
      });
    },
    release() {
      pressTween?.kill();
      shapeTween?.kill();
      boostTween?.kill();
      pressTween = gsap.to(state, { press: 1, duration: reduced ? 0 : 0.32, ease: "power2.out", onUpdate: paint });
      shapeTween = gsap.to(state, {
        body: 0, lean: 0, duration: reduced ? 0 : 0.72, ease: returnEase, onUpdate: paint,
      });
      boostTween = gsap.to(state, { boost: 1, duration: reduced ? 0 : 0.7, ease: "power2.out" });
    },
    setReducedMotion(next: boolean) {
      if (next === reduced) return;
      reduced = next;
      stopTweens();
      Object.assign(state, { ratio: targetRatio, press: 1, body: 0, lean: 0, boost: 1 });
      paint();
      syncTicker();
    },
    dispose() {
      disposed = true;
      stopTweens();
      gsap.ticker.remove(draw);
      field.dispose();
      context.revert();
    },
  };
}


/* ===== components/ui/effortField.ts · SHA256 5518873bb82caae654895b423e6e36fdccbcfb223d07c2d2bbc8c717090169d0 ===== */
/** Canvas field recreated from the public Hexta Models preview, without its Pro source. */
export function createEffortField(canvas: HTMLCanvasElement, getRatio: () => number) {
  const ctx = canvas.getContext("2d");
  let width = 0;
  let height = 0;
  let phase = 0;
  let lastTime = 0;
  const stars = Array.from({ length: 64 }, (_, i) => ({
    x: ((i * 0.61803398875) % 1),
    y: ((i * 0.754877666) % 1),
    size: 0.35 + (i % 5) * 0.13,
    speed: 0.3 + (i % 7) * 0.1,
  }));

  const resize = () => {
    // The enclosing model menu morphs with a CSS transform; use layout pixels.
    width = canvas.clientWidth;
    height = canvas.clientHeight;
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = Math.max(1, Math.round(width * dpr));
    canvas.height = Math.max(1, Math.round(height * dpr));
    ctx?.setTransform(dpr, 0, 0, dpr, 0, 0);
  };

  const draw = (time = 0, still = false, motionBoost = 1) => {
    if (!ctx || width <= 0 || height <= 0) return;
    const ratio = getRatio();
    // Integrate speed so changing effort never jumps the particles' positions.
    if (!still) phase += Math.min(Math.max(time - lastTime, 0), 0.05)
      * (0.16 + ratio * 0.85) * Math.max(1, motionBoost);
    lastTime = time;
    const hue = 210 + ratio * ratio * 63;
    const fill = 32 + (width - 32) * ratio;
    ctx.clearRect(0, 0, width, height);
    const base = ctx.createLinearGradient(0, 0, Math.max(fill, 32), 0);
    base.addColorStop(0, "#30343a");
    base.addColorStop(0.28, `hsl(${hue - 12} 38% 47%)`);
    base.addColorStop(0.65, `hsl(${hue} 79% 70%)`);
    base.addColorStop(1, `hsl(${hue + 5} 88% 70%)`);
    ctx.fillStyle = base;
    ctx.fillRect(0, 0, width, height);

    ctx.save();
    ctx.globalCompositeOperation = "screen";
    // Soft overlapping ribbons create the drifting, diagonal nebula texture.
    for (let band = 0; band < 7; band++) {
      ctx.beginPath();
      for (let x = 0; x <= width + 4; x += 4) {
        const y = height * (0.12 + band * 0.15)
          + Math.sin(x * 0.034 + phase + band * 1.8) * 5
          + Math.sin(x * 0.079 - phase * 0.6 + band) * 2 - x * 0.035;
        if (x === 0) ctx.moveTo(x, y);
        else ctx.lineTo(x, y);
      }
      ctx.lineWidth = 2 + (band % 3) * 2;
      ctx.strokeStyle = `hsla(${hue + band * 2} 90% 87% / ${0.06 + ratio * 0.025})`;
      ctx.stroke();
    }
    const glowX = fill * (0.6 + Math.sin(phase * 0.7) * 0.12);
    const glow = ctx.createRadialGradient(glowX, height * 0.7, 0, glowX, height * 0.7, 46);
    glow.addColorStop(0, `hsla(${hue - 12} 100% 88% / .35)`);
    glow.addColorStop(1, `hsla(${hue} 100% 80% / 0)`);
    ctx.fillStyle = glow;
    ctx.fillRect(0, 0, width, height);

    for (const [i, star] of stars.entries()) {
      const x = ((star.x + phase * star.speed * 0.08) % 1) * width;
      const y = ((star.y - phase * star.speed * 0.04) % 1 + 1) % 1 * height;
      const fade = Math.min(1, x / 55);
      const twinkle = 0.45 + Math.sin(phase * 2 + i * 1.9) * 0.25;
      ctx.fillStyle = `rgba(255,255,255,${fade * twinkle * (0.4 + ratio * 0.6)})`;
      ctx.beginPath();
      ctx.ellipse(x, y, star.size * (1 + ratio * 0.5), star.size * 0.65, -0.45, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.restore();
    const shade = ctx.createLinearGradient(0, 0, Math.max(fill, 32), 0);
    shade.addColorStop(0, "rgba(36,39,44,.8)");
    shade.addColorStop(0.45, "rgba(36,39,44,0)");
    ctx.fillStyle = shade;
    ctx.fillRect(0, 0, width, height);
  };

  const observer = new ResizeObserver(() => { resize(); draw(lastTime, true); });
  observer.observe(canvas);
  resize();
  return { draw, dispose: () => observer.disconnect() };
}


/* ===== components/ui/ListItem.tsx · SHA256 6b37ab90dc15d2c4ef87dc8b1cf457e594026d92abe9dd995824018226b9810f ===== */
import type { HTMLAttributes } from "react";
import { cn } from "../shadcn/utils";
import { FLOATING_MENU, MOTION } from "./motion";
import { HoverInfo } from "./HoverInfo";

/**
 * 列表项 / 下拉行 —— 统一语言：hover 与激活同为 bg-ui-tint（主题感知静默着色）。
 * selected 时加粗（与侧栏激活项一致）；不需要浮层/边框。
 */
export function ListItem({
  selected = false,
  sliding = false,
  title,
  className,
  ...rest
}: HTMLAttributes<HTMLDivElement> & { selected?: boolean; sliding?: boolean }) {
  return (
    <HoverInfo content={title}><div
      className={cn(
        "flex w-full items-center justify-between rounded-lg px-2 py-1.5 text-[12px]",
        !sliding && FLOATING_MENU.item,
        MOTION.fast,
        sliding
          ? "relative z-[1]"
          : selected
            ? "bg-ui-tint font-medium"
            : "hover:bg-ui-tint",
        sliding && selected && "font-medium text-text-base",
        className,
      )}
      {...rest}
    /></HoverInfo>
  );
}


/* ===== components/ui/TintButton.tsx · SHA256 401c85d7320b6db702ce1760744eece8eb4454a49919ab8837cd3f8107282c3c ===== */
import { forwardRef, type ButtonHTMLAttributes } from "react";
import { cn } from "../shadcn/utils";
import { MOTION } from "./motion";
import { useSpringPress } from "./useSpringPress";

/**
 * 着色按钮 —— 统一语言：无边框、bg-black/5 着色底、hover 同色加深。
 * variant="primary" 为品牌色实心按钮（发送/主操作）。
 */
export const TintButton = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement> & { variant?: "default" | "primary" }>(function TintButton({
  variant = "default",
  className,
  ...rest
}, ref) {
  const pressRef = useSpringPress(ref);
  return (
    <button
      ref={pressRef}
      className={cn(
        "rounded-full px-2 py-1.5 text-[12px] text-text-base",
        MOTION.fast,
        variant === "primary"
          ? "bg-primary text-white hover:bg-primary-hover"
          : "bg-ui-tint hover:bg-ui-tint-strong disabled:opacity-50 disabled:cursor-not-allowed",
        className,
      )}
      {...rest}
    />
  );
});


/* ===== components/ui/MorphingMenuShell.tsx · SHA256 70d3e7d2898bcb3e7c89b0d5258177986ae05c1b185667db11a33f88928c2963 ===== */
import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from "react";
import gsap from "gsap";
import { continuousRadius, computedCornerCurvature, CORNER_CURVATURE } from "../../theme/continuousCorners";
import { cn } from "../shadcn/utils";
import { MenuTriggerOpenContext, MorphPanelLayoutContext } from "./MorphPanelLayoutContext";
import { SHARE_MOTION, motionColor, prefersReducedMotion, springEase } from "./springMotion";
import "./menuSurface.css";

type Props = {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  layoutId: string;
  trigger: ReactNode;
  className?: string;
  panelClassName?: string;
  panelShadow?: string;
  panelAlign?: "left" | "right";
  panelPlacement?: "above" | "below";
  zIndex?: number;
  staggerContent?: boolean;
  unstyled?: boolean;
  triggerLayout?: "inline" | "block";
  children: ReactNode;
};

const OPEN_SHADOW = "0 6px 24px rgba(0,0,0,0.10)";
const CLOSED_SHADOW = "0 0 0 rgba(0,0,0,0)";

/** The approved share demo's single shell, compact face and expanded face. */
export function MorphingMenuShell({ open, onOpenChange, layoutId, trigger, className,
  panelClassName, panelShadow = OPEN_SHADOW, panelAlign = "left", panelPlacement = "above", zIndex = 50,
  staggerContent = false, unstyled = false, triggerLayout = "inline", children,
}: Props) {
  const containerRef = useRef<HTMLDivElement>(null);
  const anchorRef = useRef<HTMLDivElement>(null);
  const shellRef = useRef<HTMLDivElement>(null);
  const compactRef = useRef<HTMLDivElement>(null);
  const panelRef = useRef<HTMLDivElement>(null);
  const contentRef = useRef<HTMLDivElement>(null);
  const previousOpen = useRef<boolean>();
  const latest = useRef({ open, onOpenChange });
  latest.current = { open, onOpenChange };
  const [present, setPresent] = useState(open);
  const [layoutAnimating, setLayoutAnimating] = useState(false);
  const showPanel = open || present;

  useEffect(() => {
    if (!open) return;
    const outside = (event: MouseEvent) => {
      if (!containerRef.current?.contains(event.target as Node)) latest.current.onOpenChange(false);
    };
    const escape = (event: KeyboardEvent) => {
      if (event.key === "Escape") latest.current.onOpenChange(false);
    };
    document.addEventListener("mousedown", outside);
    document.addEventListener("keydown", escape);
    return () => {
      document.removeEventListener("mousedown", outside);
      document.removeEventListener("keydown", escape);
    };
  }, [open]);

  useLayoutEffect(() => {
    const shell = shellRef.current, anchor = anchorRef.current, compact = compactRef.current;
    const panel = panelRef.current, content = contentRef.current;
    if (!shell || !anchor || !compact) return;
    const first = previousOpen.current === undefined;
    previousOpen.current = open;
    let disposed = false;
    let timeline: gsap.core.Timeline | undefined;
    let resize: gsap.core.Tween | undefined;
    let restoreFocus = shell.contains(document.activeElement);

    const compactSize = () => {
      if (triggerLayout === "block") compact.style.width = `${anchor.clientWidth}px`;
      const button = compact.querySelector("button");
      const css = getComputedStyle(button ?? compact);
      const width = compact.offsetWidth, height = compact.offsetHeight;
      return { width, height, radius: Math.min(parseFloat(css.borderTopLeftRadius) || height / 2, height / 2),
        curvature: computedCornerCurvature(css),
        background: motionColor(css.backgroundColor) };
    };
    let closed = compactSize();
    const restClosed = () => {
      gsap.set(compact, { opacity: 1, clearProps: "position,left,right,top,bottom,transform" });
      gsap.set(shell, { clearProps: "position,left,right,top,bottom,width,height,maxWidth,overflow,borderRadius,backgroundColor,boxShadow,transform,--morph-corner-curvature" });
      anchor.style.width = ""; anchor.style.height = "";
      if (triggerLayout === "block") compact.style.width = "100%";
    };
    compact.inert = open;
    if (panel) panel.inert = !open;
    if (!showPanel || !panel || !content) {
      restClosed();
      return;
    }
    if (open) setPresent(true);

    // Reserve the compact control's layout slot. The same shell owns both sizes.
    anchor.style.width = triggerLayout === "block" ? "100%" : `${closed.width}px`;
    anchor.style.height = `${closed.height}px`;
    if (shell.style.position !== "absolute") {
      gsap.set(shell, { position: "absolute", width: closed.width, height: closed.height,
        maxWidth: "none", borderRadius: closed.radius, "--morph-corner-curvature": closed.curvature, backgroundColor: closed.background });
    }
    gsap.set(shell, { left: panelAlign === "left" ? 0 : "auto", right: panelAlign === "right" ? 0 : "auto",
      bottom: panelPlacement === "above" ? 0 : "auto", top: panelPlacement === "below" ? 0 : "auto" });
    gsap.set(compact, { position: "absolute", left: panelAlign === "left" ? 0 : "auto",
      right: panelAlign === "right" ? 0 : "auto", bottom: panelPlacement === "above" ? 0 : "auto",
      top: panelPlacement === "below" ? 0 : "auto" });
    const expandedSize = () => {
      panel.style.width = "";
      const scrollTop = content.scrollTop;
      content.style.maxHeight = "";
      content.style.overflowY = "";
      const width = Math.min(content.offsetWidth, window.innerWidth - 32);
      panel.style.width = `${width}px`;
      const rect = anchor.getBoundingClientRect();
      const availableHeight = panelPlacement === "above" ? rect.bottom - 16 : window.innerHeight - rect.top - 16;
      if (panel.offsetHeight > availableHeight) {
        content.style.maxHeight = `${Math.max(24, availableHeight)}px`;
        content.style.overflowY = "auto";
        content.scrollTop = scrollTop;
      }
      const left = panelAlign === "right" ? rect.right - width : rect.left;
      return { width, height: panel.offsetHeight,
        x: Math.max(16, Math.min(left, window.innerWidth - width - 16)) - left };
    };
    let expanded = expandedSize();
    const finish = () => {
      if (disposed || latest.current.open !== open) return;
      setLayoutAnimating(false);
      if (open) shell.style.overflow = "visible";
      else {
        restClosed();
        setPresent(false);
      }
    };
    const focus = () => {
      if (!disposed && restoreFocus && document.activeElement === document.body && latest.current.open === open) {
        (open ? panel : compact.querySelector<HTMLButtonElement>("button"))?.focus({ preventScroll: true });
      }
    };
    const play = () => {
      timeline?.kill(); resize?.kill();
      const reduced = prefersReducedMotion() || (first && open);
      if (reduced) {
        gsap.set(shell, { width: open ? expanded.width : closed.width, height: open ? expanded.height : closed.height,
          x: open ? expanded.x : 0, borderRadius: open ? continuousRadius(16) : closed.radius,
          "--morph-corner-curvature": open ? CORNER_CURVATURE : closed.curvature,
          backgroundColor: open ? motionColor(getComputedStyle(panel).backgroundColor) : closed.background,
          boxShadow: open ? panelShadow : CLOSED_SHADOW });
        gsap.set(compact, { opacity: open ? 0 : 1, scale: 1 });
        gsap.set(panel, { opacity: open ? 1 : 0, filter: "blur(0px)" });
        finish(); focus();
        return;
      }
      setLayoutAnimating(true);
      shell.style.overflow = "hidden";
      const motion = open ? SHARE_MOTION.open : SHARE_MOTION.close;
      timeline = gsap.timeline({ onComplete: finish });
      timeline.to(shell, { width: open ? expanded.width : closed.width, duration: motion.width.duration,
        ease: springEase(motion.width.stiffness, motion.width.damping, motion.width.duration) }, motion.width.delay);
      timeline.to(shell, { height: open ? expanded.height : closed.height, duration: motion.height.duration,
        ease: springEase(motion.height.stiffness, motion.height.damping, motion.height.duration) }, motion.height.delay);
      timeline.to(shell, { x: open ? expanded.x : 0, borderRadius: open ? continuousRadius(16) : closed.radius,
        "--morph-corner-curvature": open ? CORNER_CURVATURE : closed.curvature,
        backgroundColor: open ? motionColor(getComputedStyle(panel).backgroundColor) : closed.background,
        boxShadow: open ? panelShadow : CLOSED_SHADOW, duration: 0.22, ease: "power2.out" }, 0);
      timeline.to(compact, { opacity: open ? 0 : 1, duration: 0.14, ease: "power2.out" }, open ? 0 : 0.2);
      if (!open) timeline.fromTo(compact, { scale: 0.95 }, { scale: 1, duration: 0.48,
        ease: springEase(300, 21, 0.48) }, 0.2);
      timeline.to(panel, { opacity: open ? 1 : 0, filter: open ? "blur(0px)" : "blur(5px)",
        duration: open ? 0.24 : 0.18, ease: "power2.out" }, open ? 0.12 : 0);
      if (open && staggerContent) {
        const rows = Array.from(content.querySelectorAll("[role=menuitem], [data-menu-item]")).slice(0, 24);
        timeline.fromTo(rows, { opacity: 0, y: 6 }, { opacity: 1, y: 0,
          duration: 0.5, stagger: Math.min(0.025, 0.16 / Math.max(1, rows.length)), ease: springEase(280, 24, 0.5) }, 0.13);
      }
      timeline.call(focus, [], 0.36);
    };
    play();
    const retarget = () => {
      if (disposed) return;
      closed = compactSize();
      if (!latest.current.open) return;
      const next = expandedSize();
      if (Math.abs(next.width - expanded.width) < 0.5 && Math.abs(next.height - expanded.height) < 0.5 && Math.abs(next.x - expanded.x) < 0.5) return;
      const widthChanged = Math.abs(next.width - expanded.width) >= 0.5;
      expanded = next;
      resize?.kill();
      gsap.killTweensOf(shell, widthChanged ? "width,height,x" : "height,x");
      const motion = SHARE_MOTION.resize;
      resize = gsap.to(shell, { ...(widthChanged ? { width: expanded.width } : {}), x: expanded.x, height: expanded.height,
        duration: prefersReducedMotion() ? 0 : motion.duration,
        ease: springEase(motion.stiffness, motion.damping, motion.duration) });
    };
    const observer = new ResizeObserver(retarget);
    observer.observe(content); observer.observe(compact);
    const outsideFocus = (event: FocusEvent) => { if (!shell.contains(event.target as Node)) restoreFocus = false; };
    const media = window.matchMedia("(prefers-reduced-motion: reduce)");
    document.addEventListener("focusin", outsideFocus);
    window.addEventListener("resize", retarget);
    media.addEventListener("change", play);
    return () => {
      disposed = true; timeline?.kill(); resize?.kill(); observer.disconnect();
      document.removeEventListener("focusin", outsideFocus);
      window.removeEventListener("resize", retarget);
      media.removeEventListener("change", play);
    };
  }, [open, showPanel, panelAlign, panelPlacement, panelShadow, triggerLayout]);

  return <div ref={containerRef} className={cn("relative", className)}>
    <div ref={anchorRef} className={cn("relative max-w-full align-top", triggerLayout === "block" ? "flex w-full" : "inline-flex")}>
      <div ref={shellRef} data-motion-id={layoutId} data-state={open ? "open" : "closed"}
        data-morph-placement={panelPlacement} className={cn("morph-menu-shell relative inline-flex max-w-full", triggerLayout === "block" && "w-full")}
        style={{ zIndex: showPanel ? zIndex : undefined, pointerEvents: !open && present ? "none" : undefined }}>
        <div ref={compactRef} className={cn("morph-menu-compact pointer-events-auto inline-flex max-w-full", triggerLayout === "block" ? "w-full" : "w-max")}
          aria-hidden={open}>
          <MenuTriggerOpenContext.Provider value={open}>{trigger}</MenuTriggerOpenContext.Provider>
        </div>
        {showPanel && <div ref={panelRef} data-morph-panel aria-hidden={!open} tabIndex={-1}
          className="morph-menu-expanded ui-menu-surface absolute left-0 top-0 w-max max-w-[calc(100vw-32px)] rounded-2xl bg-elevated-bg"
          style={{ opacity: 0, pointerEvents: open ? "auto" : "none" }}>
          <MorphPanelLayoutContext.Provider value={layoutAnimating}>
            <div ref={contentRef} className={cn("morph-menu-content max-w-[calc(100vw-32px)]", !unstyled && "bg-elevated-bg", panelClassName)}>{children}</div>
          </MorphPanelLayoutContext.Provider>
        </div>}
      </div>
    </div>
  </div>;
}


/* ===== components/ui/InputSurface.tsx · SHA256 b7953cd138b3f8e63e92d158457e93c2e7d30e48232ce004f4b677f1b9562f5f ===== */
import type { HTMLAttributes } from "react";
import { cn } from "../shadcn/utils";
import { MOTION } from "./motion";

/**
 * 输入区容器（Composer 等）—— 统一语言：无边框、bg-elevated-bg + 柔和阴影，
 * 聚焦时阴影增强作为反馈（不靠边框）。
 */
export function InputSurface({ className, ...rest }: HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={cn(
        "relative w-full rounded-[24px] bg-elevated-bg shadow-[0_6px_20px_rgba(31,38,48,0.08)]",
        MOTION.standard,
        "focus-within:shadow-[0_8px_26px_rgba(31,38,48,0.12)]",
        className,
      )}
      {...rest}
    />
  );
}


/* ===== components/ui/MessageSendMotionContext.tsx · SHA256 ef313a23bea029b66cd0a61f09abc4e9f9dc9ae4203271b24b2ef2178f83d83d ===== */
import { createContext, useCallback, useContext, useEffect, useRef } from "react";
import { MessageSendMotion } from "./messageSendMotion";
import "./messageSendMotion.css";

export const MessageSendMotionContext = createContext<MessageSendMotion | null>(null);
export const useMessageSendMotion = () => useContext(MessageSendMotionContext);

export function useMessageSendMotionController() {
  const controller = useRef<MessageSendMotion>();
  if (!controller.current) controller.current = new MessageSendMotion();
  const motion = controller.current;
  useEffect(() => () => motion.finish(), [motion]);
  return motion;
}

export function useMessageSendTarget(id: string) {
  const motion = useMessageSendMotion();
  const release = useRef<() => void>();
  // Register through the stable row. Framer Motion 11 popLayout replaces its
  // direct child's ref, so a ref placed on that motion.div is never called.
  return useCallback((row: HTMLDivElement | null) => {
    release.current?.();
    const target = row?.querySelector<HTMLElement>("[data-user-message-id]");
    release.current = target ? motion?.register(id, target) : undefined;
  }, [motion, id]);
}


/* ===== components/ui/messageSendMotion.ts · SHA256 fad6078596729a1b05cebc538b56b1ffbb6faefcf69950032d3a8745eb2ef5b0 ===== */
import { playMessageSendFlight, type SendBox, type SendFlight } from "./messageSendFlight";
import { motionColor, prefersReducedMotion } from "./springMotion";

type PendingSend = {
  text: string;
  id: string | null;
  taken: boolean;
  nextFrame: string | null;
  source: DOMRect;
  composer: HTMLElement;
  root: HTMLDivElement;
  carrier: HTMLDivElement;
  font: { fontSize: number; lineHeight: number };
  timeout: number;
  flight?: SendFlight;
  reveal?: () => void;
};

/** Visual state only; message identity comes from App's existing seeded turn.
 * Never matches a history bubble by its text or changes the send/run protocol. */
export class MessageSendMotion {
  private current: PendingSend | null = null;
  private targets = new Map<string, HTMLElement>();
  private frame: string | null = null;

  setFrame(frame: string) {
    if (this.frame === frame) return;
    this.frame = frame;
    if (this.current && this.current.nextFrame !== frame) this.finish();
  }

  prepare(editor: HTMLTextAreaElement, mirror: HTMLElement, composer: HTMLElement, text: string) {
    this.finish();
    if (prefersReducedMotion() || !text.trim()) return;
    const source = editor.getBoundingClientRect();
    if (!source.width || !source.height) return;
    const style = getComputedStyle(mirror);
    const root = document.createElement("div");
    root.className = "message-send-flight-root";
    root.setAttribute("aria-hidden", "true");
    const carrier = document.createElement("div");
    carrier.className = "message-send-carrier";
    Object.assign(carrier.style, {
      left: "0px", top: "0px", width: `${source.width}px`, height: `${source.height}px`,
      transform: `translate(${source.left}px, ${source.top}px)`, padding: "0px",
      fontFamily: style.fontFamily, fontSize: style.fontSize, lineHeight: style.lineHeight,
      fontWeight: style.fontWeight, letterSpacing: style.letterSpacing, color: style.color,
    });
    const content = mirror.cloneNode(true) as HTMLElement;
    content.removeAttribute("id");
    content.querySelectorAll(".composer-mirror-caret").forEach(node => node.remove());
    Object.assign(content.style, { position: "relative", inset: "auto", maxHeight: "none",
      minHeight: "0", height: "auto", overflow: "visible" });
    content.style.transform = `translateY(${-editor.scrollTop}px)`;
    carrier.append(content); root.append(carrier); document.body.append(root);
    const record: PendingSend = { text: text.trim(), id: null, taken: false, nextFrame: this.frame, source, composer, root, carrier,
      font: { fontSize: parseFloat(style.fontSize), lineHeight: parseFloat(style.lineHeight) },
      timeout: window.setTimeout(() => { if (this.current === record) this.finish(); }, 1800) };
    this.current = record;
  }

  /** Consume synchronously before App awaits preflight, then bind the actual id. */
  take(text: string) {
    const record = this.current;
    if (!record || record.taken || record.text !== text.trim()) return null;
    record.taken = true;
    return { bind: (id: string, nextFrame = this.frame) => {
      if (this.current !== record) return;
      record.id = id;
      record.nextFrame = nextFrame;
      // A cold, lazy-loaded ChatView may take longer than preflight. Keep the
      // source until that exact bubble mounts, without delaying the actual run.
      window.clearTimeout(record.timeout);
      record.timeout = window.setTimeout(() => { if (this.current === record) this.finish(); }, 5000);
      const target = this.targets.get(id);
      if (target) this.start(record, target);
    } };
  }

  register(id: string, target: HTMLElement) {
    this.targets.set(id, target);
    if (this.current?.id === id && !this.current.flight) this.start(this.current, target);
    return () => {
      if (this.targets.get(id) !== target) return;
      this.targets.delete(id);
      if (this.current?.id === id) this.finish();
    };
  }

  finish = () => {
    const record = this.current;
    if (!record) return;
    this.current = null;
    window.clearTimeout(record.timeout);
    record.flight?.cancel();
    record.reveal?.();
    record.root.remove();
    window.removeEventListener("resize", this.finish);
  };

  private start(record: PendingSend, target: HTMLElement) {
    const end = target.getBoundingClientRect();
    if (!end.width || !end.height || end.bottom < 0 || end.top > window.innerHeight || prefersReducedMotion()) {
      this.finish(); return;
    }
    const style = getComputedStyle(target);
    const paddingX = parseFloat(style.paddingLeft), paddingY = parseFloat(style.paddingTop);
    // Framer's layout projection writes inline visibility while the textarea
    // shrinks / timeline scrolls. A scoped CSS attribute must own this handoff.
    const previouslyHidden = target.hasAttribute("data-message-send-hidden");
    record.reveal = () => { if (!previouslyHidden) target.removeAttribute("data-message-send-hidden"); };
    target.setAttribute("data-message-send-hidden", "");
    record.carrier.replaceChildren(...Array.from(target.cloneNode(true).childNodes));
    Object.assign(record.carrier.style, {
      padding: style.padding, fontFamily: style.fontFamily, fontWeight: style.fontWeight,
      letterSpacing: style.letterSpacing, color: style.color, cornerShape: style.getPropertyValue("corner-shape"),
    });
    // Inline content retains its actual chips; attachments remain in their
    // existing message attachment row, including attachment-only submissions.
    const start: SendBox = { x: record.source.left-paddingX, y: record.source.top-paddingY,
      width: record.source.width+paddingX*2, height: Math.max(40, Math.min(record.source.height+paddingY*2, end.height)) };
    window.clearTimeout(record.timeout);
    window.addEventListener("resize", this.finish);
    record.flight = playMessageSendFlight({ carrier: record.carrier, composer: record.composer, start,
      getEnd: () => {
        if (!target.isConnected) return null;
        const rect = target.getBoundingClientRect();
        const scroll = target.closest<HTMLElement>("[data-chat-scroll]");
        const viewport = scroll?.getBoundingClientRect();
        if (viewport && (rect.bottom < viewport.top || rect.top > viewport.bottom)) return null;
        return { x: rect.left, y: rect.top, width: rect.width, height: rect.height };
      },
      appearance: { start: record.font, end: {
        fontSize: parseFloat(style.fontSize), lineHeight: parseFloat(style.lineHeight),
        background: motionColor(style.backgroundColor),
        radii: [style.borderTopLeftRadius, style.borderTopRightRadius, style.borderBottomRightRadius, style.borderBottomLeftRadius].map(value => parseFloat(value)),
      } }, onFinish: () => { if (this.current === record) this.finish(); },
    });
  }
}


/* ===== components/ui/messageSendFlight.ts · SHA256 60efe4b61ca0f8e62837cb2e23c37c0da60017985982a407f00d44360b454a79 ===== */
import gsap from "gsap";
import { continuousRadius } from "../../theme/continuousCorners";
import { SHARE_MOTION, springEase } from "./springMotion";

export type SendBox = { x: number; y: number; width: number; height: number };
export type SendFlight = { finish: () => void; cancel: () => void };
type Appearance = { fontSize: number; lineHeight: number; radii: number[]; background: string };
const mix = (a: number, b: number, p: number) => a + (b - a) * p;
const clamp = (p: number) => Math.max(0, Math.min(1, p));

/** The approved F preview: gentle preload, the model menu's two spring axes,
 * and one small arrival overshoot. Preview and production share this timeline. */
export function playMessageSendFlight({
  carrier, composer, start, getEnd, appearance, boundsWidth = window.innerWidth, onFinish,
}: {
  carrier: HTMLElement;
  composer: HTMLElement;
  start: SendBox;
  getEnd: () => SendBox | null;
  appearance: { start?: Partial<Appearance>; end: Appearance };
  boundsWidth?: number;
  onFinish: () => void;
}): SendFlight & { timeline: gsap.core.Timeline } {
  const progress = { width: 0, height: 0, squash: 1 };
  const axes = SHARE_MOTION.open;
  const from = { fontSize: appearance.end.fontSize, lineHeight: appearance.end.lineHeight,
    radii: Array(4).fill(continuousRadius(9)) as number[], ...appearance.start };
  const background = gsap.utils.interpolate("rgba(239,241,244,0)", appearance.end.background);
  const originalTransition = composer.style.transition;
  const originalTransform = composer.style.transform;
  const originalOrigin = composer.style.transformOrigin;
  let finished = false;
  const finish = () => {
    if (finished) return;
    finished = true;
    timeline.kill();
    gsap.set(composer, { clearProps: "transform,transformOrigin" });
    composer.style.transform = originalTransform;
    composer.style.transformOrigin = originalOrigin;
    composer.style.transition = originalTransition;
    onFinish();
  };
  const paint = () => {
    const end = getEnd();
    if (!end) { finish(); return; }
    const p = progress.height, bounded = clamp(p), widthP = progress.width;
    const width = mix(start.width, end.width, clamp(widthP));
    const height = mix(start.height, end.height, bounded);
    const centerX = mix(start.x + start.width/2, end.x + end.width/2, widthP) + 8 * Math.sin(Math.PI * bounded);
    const centerY = mix(start.y + start.height/2, end.y + end.height/2, p);
    // Compress the settled layout rather than rewrapping its last character
    // into a clipped extra line when the spring passes the final width.
    const springX = Math.max(24, mix(start.width, end.width, widthP)) / width;
    const springY = Math.max(24, mix(start.height, end.height, p)) / height;
    const lag = Math.max(-.15, Math.min(.4, widthP - p));
    const arrival = Math.max(0, p - 1);
    const radii = from.radii.map((radius, index) => mix(radius, appearance.end.radii[index], bounded));
    gsap.set(carrier, {
      x: Math.max(3, Math.min(centerX - width/2, boundsWidth - width - 3)),
      y: centerY - height/2, width, height,
      scaleX: springX * (1 + (1-progress.squash)*.15 - lag*.035 + arrival*.45),
      scaleY: springY * progress.squash * (1 + lag*.07 - arrival*.3),
      borderRadius: radii.map(radius => `${radius}px`).join(" "),
      backgroundColor: background(clamp(widthP * 5)),
      fontSize: mix(from.fontSize, appearance.end.fontSize, bounded),
      lineHeight: `${mix(from.lineHeight, appearance.end.lineHeight, bounded)}px`,
    });
  };
  const timeline = gsap.timeline({ onComplete: finish });
  composer.style.transition = "none";
  paint();
  timeline.to(composer, { scaleY: .975, y: 1.5, duration: .09, transformOrigin: "50% 100%" }, 0);
  timeline.to(composer, { scaleY: 1, y: 0, duration: axes.height.duration,
    ease: springEase(axes.height.stiffness, axes.height.damping, axes.height.duration) }, .09);
  timeline.to(progress, { squash: .94, duration: .09, ease: "power2.in", onUpdate: paint }, 0);
  timeline.to(progress, { squash: 1, duration: .22, ease: "power2.out", onUpdate: paint }, .09);
  for (const axis of ["width", "height"] as const) {
    const motion = axes[axis];
    timeline.to(progress, { [axis]: 1, duration: motion.duration,
      ease: springEase(motion.stiffness, motion.damping, motion.duration), onUpdate: paint }, .08 + motion.delay);
  }
  return { timeline, finish: () => { timeline.progress(1); finish(); }, cancel: finish };
}


/* ===== components/ui/messageSendMotion.css · SHA256 44e2c23ae55bc1250bc4e249a0794a5ca0e308bf61f2f4315f44e461cd52dd6a ===== */
.message-send-flight-root {
  position:fixed;
  inset:0;
  z-index:80;
  pointer-events:none;
  overflow:hidden;
}
.message-send-carrier {
  position:absolute;
  box-sizing:border-box;
  overflow:hidden;
  white-space:pre-wrap;
  overflow-wrap:anywhere;
  transform-origin:50% 50%;
  will-change:transform;
}
[data-message-send-hidden] {
  visibility:hidden !important;
}


/* ===== components/ComposerAttachButton.tsx · SHA256 6e901cbceb8584121795564ab48a05283ea91948ddbb1b6ee8e0d8a15bfa4565 ===== */
import { HoverInfo } from "./ui/HoverInfo";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useTranslation } from "react-i18next";
import { cn } from "./shadcn/utils";
import { Button } from "./shadcn/button";

type Props = {
  disabled?: boolean;
  disabledTitle?: string;
  onClick: () => void;
};

/** Composer attachment entry with a small press response. */
export function ComposerAttachButton({ disabled = false, disabledTitle, onClick }: Props) {
  const { t } = useTranslation();

  return (
    <HoverInfo content={disabled ? disabledTitle : t("composer.addMenu.hoverLabel")}><Button
      type="button"
      disabled={disabled}

      aria-label={t("composer.addMenu.hoverLabel")}
      onClick={onClick}
      className={cn(
        "group/add flex h-8 w-8 shrink-0 items-center gap-0 overflow-hidden rounded-full bg-ui-tint p-0 text-text-secondary shadow-none",
        "hover:bg-ui-tint-strong hover:text-text-base",
        disabled && "cursor-not-allowed opacity-40",
      )}
    >
      <span
        className={cn(
          "flex h-8 w-8 shrink-0 items-center justify-center",
        )}
      >
        <FontAwesomeIcon icon={["fas", "plus"]} className="text-[11px]" />
      </span>
      <span
        className={cn(
          "sr-only",
        )}
      >
        {t("composer.addMenu.hoverLabel")}
      </span>
    </Button></HoverInfo>
  );
}


/* ===== components/ComposerAttachmentList.tsx · SHA256 f87eec74ef38ca5db99d1e3b8c0f884fa29bf701390248e29c36ff99675e803f ===== */
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CircleAlert, Image, LoaderCircle, X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from "react";
import gsap from "gsap";
import type { ComposerAttachment } from "../types";
import { ImagePreviewTrigger } from "./ImagePreviewTrigger";
import { Button } from "./shadcn/button";
import { cn } from "./shadcn/utils";
import { HoverInfo } from "./ui/HoverInfo";
import { IconButton } from "./ui/IconButton";
import { SpringDisclosure } from "./ui/SpringDisclosure";
import { prefersReducedMotion, springEase } from "./ui/springMotion";

function AttachmentMotionItem({ exiting, onExited, children }: {
  exiting: boolean; onExited: () => void; children: ReactNode;
}) {
  const ref = useRef<HTMLDivElement>(null);
  const latestExit = useRef(onExited);
  latestExit.current = onExited;
  useLayoutEffect(() => {
    const node = ref.current;
    if (!node) return;
    const reduced = prefersReducedMotion();
    node.inert = exiting;
    const tween = exiting
      ? gsap.to(node, { opacity: 0, scale: 0.95, duration: reduced ? 0 : 0.16,
          onComplete: () => latestExit.current() })
      : gsap.fromTo(node, { opacity: 0, y: 6, scale: 0.96 }, { opacity: 1, y: 0, scale: 1,
          duration: reduced ? 0 : 0.42, ease: springEase(360, 24, 0.42) });
    return () => { tween.kill(); };
  }, [exiting]);
  return <div ref={ref} className="shrink-0">{children}</div>;
}

interface ComposerAttachmentListProps {
  attachments: ComposerAttachment[];
  onRemove: (id: string) => void;
}

export function ComposerAttachmentList({ attachments, onRemove }: ComposerAttachmentListProps) {
  const { t } = useTranslation();
  const [retained, setRetained] = useState(attachments);
  const activeIds = new Set(attachments.map((item) => item.id));
  const shown = [...attachments, ...retained.filter((item) => !activeIds.has(item.id))];
  useEffect(() => {
    setRetained((previous) => [...attachments, ...previous.filter((item) => !attachments.some((active) => active.id === item.id))]);
  }, [attachments]);
  const removeExited = (id: string) => setRetained((items) => items.filter((item) => item.id !== id));

  return (
    <SpringDisclosure open={shown.length > 0} unmountOnExit animateOnMount>
    <div className="-mx-1 mb-2 flex max-w-full items-start gap-2.5 overflow-x-auto px-1 pb-1 pt-1.5">
      {shown.map((item) => {
        if (item.kind === "image") {
          const failed = item.status === "error";
          const status = failed
            ? t("composer.attachments.failed")
            : item.status === "processing"
            ? t("composer.attachments.recognizing")
            : null;
          const description = status
            ? t("composer.attachments.statusLabel", {
                name: item.name,
                status: failed ? item.error ?? status : status,
              })
            : item.name;

          return (
            <AttachmentMotionItem key={item.id} exiting={!activeIds.has(item.id)} onExited={() => removeExited(item.id)}><HoverInfo
              key={item.id}
              content={description}
            >
              <div className="group/attachment relative flex h-[68px] w-[68px] shrink-0">
                {item.dataUrl ? (
                  <ImagePreviewTrigger
                    src={item.dataUrl}
                    alt={item.name}
                    triggerClassName="h-full w-full rounded-[10px] border border-border-theme bg-ui-tint"
                    imageClassName="h-full w-full object-cover"
                  />
                ) : (
                  <div className="flex h-full w-full items-center justify-center rounded-[10px] border border-border-theme bg-ui-tint text-text-secondary">
                    <Image aria-hidden="true" className="h-5 w-5" />
                  </div>
                )}
                {status && (
                  <span
                    role={failed ? "alert" : "status"}
                    aria-label={description}
                    className={cn(
                      "pointer-events-none absolute inset-x-1 bottom-1 flex items-center justify-center gap-0.5 whitespace-nowrap rounded-md bg-elevated-bg px-0.5 py-0.5 text-[10px] leading-4",
                      failed ? "text-red-500" : "text-text-secondary",
                    )}
                  >
                    {failed ? (
                      <CircleAlert aria-hidden="true" className="h-3 w-3 shrink-0" />
                    ) : (
                      <LoaderCircle aria-hidden="true" className="h-3 w-3 shrink-0 motion-safe:animate-spin" />
                    )}
                    <span className="min-w-0 truncate">{status}</span>
                  </span>
                )}
                <Button
                  variant="ghost"
                  size="icon"
                  aria-label={t("composer.attachments.removeImage", { name: item.name })}
                  onClick={() => onRemove(item.id)}
                  className="absolute -right-1 -top-1 z-10 h-5 w-5 rounded-full border-2 border-elevated-bg bg-neutral-600 p-0 text-white opacity-0 transition-opacity hover:bg-neutral-800 hover:text-white group-hover/attachment:opacity-100 group-focus-within/attachment:opacity-100 motion-reduce:transition-none [@media(hover:none)]:opacity-100"
                >
                  <X aria-hidden="true" className="h-2.5 w-2.5" />
                </Button>
              </div>
            </HoverInfo></AttachmentMotionItem>
          );
        }

        return (
          <AttachmentMotionItem key={item.id} exiting={!activeIds.has(item.id)} onExited={() => removeExited(item.id)}><div
            key={item.id}
            className="flex h-14 max-w-[220px] shrink-0 items-center gap-2 rounded-2xl bg-black/5 px-2 text-left"
          >
            <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-white text-text-secondary">
              <FontAwesomeIcon icon={["fas", item.kind === "text" ? "file-lines" : "file"]} />
            </div>
            <div className="min-w-0 flex-1">
              <div className="truncate text-[12px] font-medium text-text-base">{item.name}</div>
              <div className="truncate text-[11px] text-text-secondary">
                {item.status === "processing"
                  ? t("composer.attachments.processing")
                  : item.status === "error"
                  ? item.error ?? t("composer.attachments.failed")
                  : item.kind === "text"
                  ? t("composer.attachments.text")
                  : t("composer.attachments.file")}
              </div>
            </div>
            <IconButton
              type="button"
              aria-label={t("composer.attachments.removeAttachment", { name: item.name })}
              onClick={() => onRemove(item.id)}
              className="h-6 w-6 rounded-full"
            >
              <FontAwesomeIcon icon={["fas", "xmark"]} className="text-[11px]" />
            </IconButton>
          </div></AttachmentMotionItem>
        );
      })}
    </div>
    </SpringDisclosure>
  );
}


/* ===== components/ImagePreviewTrigger.tsx · SHA256 df188af48ef47b0ae0fc16b52b5ec5e153d0b01dcca3819bcbcb13f1cec766b4 ===== */
import { X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { cn } from "./shadcn/utils";
import {
  Dialog,
  DialogCloseIcon,
  DialogContent,
  DialogTrigger,
} from "./shadcn/dialog";

interface ImagePreviewTriggerProps {
  src: string;
  alt: string;
  triggerClassName?: string;
  imageClassName?: string;
}

export function ImagePreviewTrigger({
  src,
  alt,
  triggerClassName,
  imageClassName,
}: ImagePreviewTriggerProps) {
  const { t } = useTranslation();
  const label = alt.trim()
    ? t("composer.attachments.previewNamedImage", { name: alt })
    : t("composer.attachments.previewImage");

  return (
    <Dialog>
      <DialogTrigger
        aria-label={label}
        aria-haspopup="dialog"
        onClick={(event) => event.stopPropagation()}
        className={cn(
          "group/image-preview relative inline-flex shrink-0 cursor-zoom-in overflow-hidden border-0 bg-transparent p-0 outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2",
          triggerClassName,
        )}
      >
        <img src={src} alt={alt} className={imageClassName} />
        <span
          aria-hidden="true"
          className="pointer-events-none absolute inset-0 bg-black/0 transition-colors group-hover/image-preview:bg-black/10"
        />
      </DialogTrigger>
      <DialogContent
        aria-label={label}
        backdropClassName="!bg-black/85"
        className="!h-fit !max-h-none !w-fit !max-w-none !overflow-visible !rounded-none !bg-transparent !shadow-none"
      >
        <div className="relative flex items-center justify-center">
          <img
            src={src}
            alt={alt}
            className="block max-h-[min(75dvh,760px)] max-w-[min(80vw,1120px)] rounded-lg object-contain shadow-2xl"
          />
          <DialogCloseIcon
            aria-label={t("composer.attachments.closePreview")}
            className="absolute right-3 top-3 z-10 h-9 w-9 rounded-full bg-black/55 text-white hover:bg-black/75 hover:text-white"
          >
            <X aria-hidden="true" className="h-5 w-5" />
          </DialogCloseIcon>
        </div>
      </DialogContent>
    </Dialog>
  );
}


/* ===== components/shadcn/dialog.tsx · SHA256 861e4cb12da5ee2db664e2f1d920e7c46d24aaf542be798c578698317c8e2ce6 ===== */
import * as React from "react";

import {
  GlobalModal,
  GlobalModalDescription,
  GlobalModalFooter,
  GlobalModalHeader,
  GlobalModalTitle,
  modalOriginFromElement,
  type ModalTriggerOrigin,
} from "../ui/GlobalModal";
import { MOTION } from "../ui/motion";
import { cn } from "./utils";

interface DialogContextValue {
  open: boolean;
  setOpen: (open: boolean) => void;
  origin: ModalTriggerOrigin | null;
  setOrigin: (origin: ModalTriggerOrigin | null) => void;
}

const DialogContext = React.createContext<DialogContextValue | null>(null);

function useDialogContext() {
  const context = React.useContext(DialogContext);
  if (!context) {
    throw new Error("Dialog components must be used inside Dialog");
  }
  return context;
}

export interface DialogProps {
  open?: boolean;
  defaultOpen?: boolean;
  onOpenChange?: (open: boolean) => void;
  children: React.ReactNode;
}

function Dialog({ open, defaultOpen = false, onOpenChange, children }: DialogProps) {
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
  const [origin, setOrigin] = React.useState<ModalTriggerOrigin | null>(null);
  const controlled = typeof open === "boolean";
  const actualOpen = controlled ? open : internalOpen;
  const setOpen = React.useCallback(
    (next: boolean) => {
      if (!controlled) setInternalOpen(next);
      onOpenChange?.(next);
    },
    [controlled, onOpenChange],
  );

  return (
    <DialogContext.Provider value={{ open: actualOpen, setOpen, origin, setOrigin }}>
      {children}
    </DialogContext.Provider>
  );
}

interface DialogTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  asChild?: boolean;
}

const DialogTrigger = React.forwardRef<HTMLButtonElement, DialogTriggerProps>(
  ({ asChild: _asChild, onClick, ...props }, ref) => {
    const { setOpen, setOrigin } = useDialogContext();
    return (
      <button
        ref={ref}
        type="button"
        onClick={(event) => {
          onClick?.(event);
          if (!event.defaultPrevented) {
            setOrigin(modalOriginFromElement(event.currentTarget));
            setOpen(true);
          }
        }}
        {...props}
      />
    );
  },
);
DialogTrigger.displayName = "DialogTrigger";

/** @deprecated Prefer GlobalModal overlay; kept for API compatibility. */
const DialogOverlay = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div ref={ref} className={cn("hidden", className)} {...props} />
  ),
);
DialogOverlay.displayName = "DialogOverlay";

interface DialogContentProps extends React.HTMLAttributes<HTMLDivElement> {
  /** Override the global modal shell layer for surfaces that must cover canvas overlays. */
  zIndexClass?: string;
  /** Optional overlay styling for focused viewers such as image preview. */
  backdropClassName?: string;
}

const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
  ({ className, children, zIndexClass, backdropClassName, ...props }, ref) => {
    const { open, setOpen, origin } = useDialogContext();

    return (
      <GlobalModal
        open={open}
        origin={origin}
        onClose={() => setOpen(false)}
        zIndexClass={zIndexClass}
        backdropClassName={backdropClassName}
        panelClassName={className}
        panelProps={{ role: "dialog", "aria-modal": true, ...props }}
        panelRef={ref}
      >
        {children}
      </GlobalModal>
    );
  },
);
DialogContent.displayName = "DialogContent";

const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
  <GlobalModalHeader className={className} {...props} />
);
DialogHeader.displayName = "DialogHeader";

const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
  <GlobalModalFooter className={className} {...props} />
);
DialogFooter.displayName = "DialogFooter";

const DialogTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
  ({ className, ...props }, ref) => (
    <GlobalModalTitle ref={ref} className={className} {...props} />
  ),
);
DialogTitle.displayName = "DialogTitle";

const DialogDescription = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
  <GlobalModalDescription ref={ref} className={className} {...props} />
));
DialogDescription.displayName = "DialogDescription";

interface DialogCloseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  asChild?: boolean;
}

const DialogClose = React.forwardRef<HTMLButtonElement, DialogCloseProps>(
  ({ asChild: _asChild, onClick, ...props }, ref) => {
    const { setOpen } = useDialogContext();
    return (
      <button
        ref={ref}
        type="button"
        onClick={(event) => {
          onClick?.(event);
          if (!event.defaultPrevented) setOpen(false);
        }}
        {...props}
      />
    );
  },
);
DialogClose.displayName = "DialogClose";

/** 弹窗右上角关闭 —— 6px 圆角 tint 底（非正圆 pill） */
const DialogCloseIcon = React.forwardRef<HTMLButtonElement, DialogCloseProps>(
  ({ className, children, onClick, ...props }, ref) => {
    const { setOpen } = useDialogContext();
    return (
      <button
        ref={ref}
        type="button"
        className={cn(
          "flex h-8 w-8 flex-shrink-0 appearance-none items-center justify-center rounded-md border-0 bg-transparent text-text-secondary",
          MOTION.fast,
          "hover:bg-ui-tint-strong hover:text-text-base active:bg-ui-tint-strong",
          className,
        )}
        onClick={(event) => {
          onClick?.(event);
          if (!event.defaultPrevented) setOpen(false);
        }}
        {...props}
      >
        {children}
      </button>
    );
  },
);
DialogCloseIcon.displayName = "DialogCloseIcon";

export {
  Dialog,
  DialogClose,
  DialogCloseIcon,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogOverlay,
  DialogTitle,
  DialogTrigger,
};


/* ===== components/ui/GlobalModal.tsx · SHA256 888a99dd9a45b6693e5e67368eee16fcb31f49c9df34869a67b61717eb72a37e ===== */
import {
  forwardRef,
  useCallback,
  useEffect,
  useLayoutEffect,
  useRef,
  useState,
  type HTMLAttributes,
  type MouseEvent,
  type MutableRefObject,
  type ReactNode,
  type Ref,
} from "react";
import { createPortal } from "react-dom";

import { cn } from "../shadcn/utils";
import {
  playModalOriginClose,
  playModalOriginOpen,
  prefersReducedMotion,
  type ModalTriggerOrigin,
} from "./modalFromOriginMotion";
import { Panel } from "./Panel";

export type { ModalTriggerOrigin } from "./modalFromOriginMotion";
export { modalOriginFromElement } from "./modalFromOriginMotion";

export interface GlobalModalProps {
  open: boolean;
  onClose: () => void;
  children: ReactNode;
  /** 触发按钮中心；方案 A 从该点 scale 展开/收回 */
  origin?: ModalTriggerOrigin | null;
  className?: string;
  panelClassName?: string;
  panelProps?: Omit<HTMLAttributes<HTMLDivElement>, "children" | "className">;
  panelRef?: Ref<HTMLDivElement>;
  backdropClassName?: string;
  closeOnBackdrop?: boolean;
  closeOnEscape?: boolean;
  zIndexClass?: string;
}

/**
 * 全局居中弹窗 —— portal + GSAP 从触发原点 scale 展开/收回（方案 A）。
 */
export function GlobalModal({
  open,
  onClose,
  children,
  origin = null,
  className,
  panelClassName,
  panelProps,
  panelRef,
  backdropClassName,
  closeOnBackdrop = true,
  closeOnEscape = true,
  zIndexClass = "z-[200]",
}: GlobalModalProps) {
  const [mounted, setMounted] = useState(open);
  const backdropRef = useRef<HTMLDivElement>(null);
  const panelElementRef = useRef<HTMLDivElement | null>(null);
  const originRef = useRef<ModalTriggerOrigin | null>(origin);
  const tlRef = useRef<ReturnType<typeof playModalOriginOpen> | null>(null);
  const closingRef = useRef(false);

  useEffect(() => {
    if (open && origin) {
      originRef.current = origin;
    }
  }, [open, origin]);

  const finishClose = useCallback(() => {
    closingRef.current = false;
    setMounted(false);
    originRef.current = null;
    onClose();
  }, [onClose]);

  const beginClose = useCallback(() => {
    if (closingRef.current || !mounted) return;

    const backdrop = backdropRef.current;
    const panel = panelElementRef.current;
    if (!backdrop || !panel) {
      finishClose();
      return;
    }

    closingRef.current = true;
    tlRef.current?.kill();
    if (prefersReducedMotion()) {
      finishClose();
      return;
    }
    tlRef.current = playModalOriginClose(backdrop, panel, originRef.current);
    tlRef.current.eventCallback("onComplete", finishClose);
  }, [finishClose, mounted]);

  useEffect(() => {
    if (open) {
      setMounted(true);
      closingRef.current = false;
      return;
    }
    if (mounted && !closingRef.current) {
      beginClose();
    }
  }, [beginClose, mounted, open]);

  useLayoutEffect(() => {
    if (!mounted || !open || closingRef.current) return;

    const backdrop = backdropRef.current;
    const panel = panelElementRef.current;
    if (!backdrop || !panel) return;

    tlRef.current?.kill();
    tlRef.current = playModalOriginOpen(backdrop, panel, originRef.current);

    return () => {
      tlRef.current?.kill();
    };
  }, [mounted, open]);

  useEffect(() => {
    if (!mounted || !closeOnEscape) return;
    const onKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") beginClose();
    };
    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, [beginClose, closeOnEscape, mounted]);

  useEffect(
    () => () => {
      tlRef.current?.kill();
    },
    [],
  );

  if (!mounted || typeof document === "undefined") return null;

  const handleShellMouseDown = (event: MouseEvent<HTMLDivElement>) => {
    if (!closeOnBackdrop || closingRef.current) return;
    if (event.target === event.currentTarget || event.target === backdropRef.current) {
      beginClose();
    }
  };

  const { onMouseDown: onPanelMouseDown, ...restPanelProps } = panelProps ?? {};

  const setPanelRef = (element: HTMLDivElement | null) => {
    panelElementRef.current = element;
    if (typeof panelRef === "function") {
      panelRef(element);
    } else if (panelRef) {
      (panelRef as MutableRefObject<HTMLDivElement | null>).current = element;
    }
  };

  return createPortal(
    <div
      className={cn("fixed inset-0 flex items-center justify-center px-4", zIndexClass, className)}
      onMouseDown={handleShellMouseDown}
    >
      <div
        ref={backdropRef}
        className={cn("absolute inset-0 bg-black/20", backdropClassName)}
        aria-hidden="true"
      />
      <Panel
        ref={setPanelRef}
        menu={false}
        {...restPanelProps}
        className={cn(
          "relative flex max-h-[88vh] w-full max-w-[620px] flex-col overflow-hidden",
          panelClassName,
        )}
        onMouseDown={(event) => {
          event.stopPropagation();
          onPanelMouseDown?.(event);
        }}
      >
        {children}
      </Panel>
    </div>,
    document.body,
  );
}

export function GlobalModalHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
  return <div className={cn("flex items-start justify-between gap-4 px-6 py-5", className)} {...props} />;
}

export function GlobalModalFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
  return <div className={cn("flex justify-end gap-2 px-6 py-4", className)} {...props} />;
}

export const GlobalModalTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement>>(
  function GlobalModalTitle({ className, ...props }, ref) {
    return <h2 ref={ref} className={cn("text-xl font-semibold text-text-base", className)} {...props} />;
  },
);

export const GlobalModalDescription = forwardRef<
  HTMLParagraphElement,
  HTMLAttributes<HTMLParagraphElement>
>(function GlobalModalDescription({ className, ...props }, ref) {
  return <p ref={ref} className={cn("mt-1 text-[13px] leading-5 text-text-secondary", className)} {...props} />;
});

export function GlobalModalBody({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
  return <div className={cn("px-6 py-6", className)} {...props} />;
}


/* ===== components/ui/modalFromOriginMotion.ts · SHA256 639524f0669815c8152b087a36e193c09fff58c70f73ae9754049b7943247070 ===== */
import gsap from "gsap";
import { prefersReducedMotion } from "./springMotion";
export { prefersReducedMotion } from "./springMotion";

/** 方案 A：从触发按钮中心 scale 展开 / 收回 */
export const MODAL_ORIGIN_MOTION = {
  openBackdropDuration: 0.22,
  openPanelDuration: 0.32,
  openPanelDelay: 0.02,
  closePanelDuration: 0.2,
  closeBackdropDuration: 0.18,
  closeBackdropDelay: 0.02,
  scaleFrom: 0.96,
  openPanelEase: "power3.out",
  openBackdropEase: "power2.out",
  closePanelEase: "power2.inOut",
  closeBackdropEase: "power2.in",
} as const;

export interface ModalTriggerOrigin {
  centerX: number;
  centerY: number;
}

export function originTransformFromPanel(panel: HTMLElement, origin: ModalTriggerOrigin): string {
  const rect = panel.getBoundingClientRect();
  const originX = origin.centerX - rect.left;
  const originY = origin.centerY - rect.top;
  return `${originX}px ${originY}px`;
}

export function modalOriginFromElement(element: HTMLElement): ModalTriggerOrigin {
  const rect = element.getBoundingClientRect();
  return {
    centerX: rect.left + rect.width / 2,
    centerY: rect.top + rect.height / 2,
  };
}

export function playModalOriginOpen(
  backdrop: HTMLElement,
  panel: HTMLElement,
  origin: ModalTriggerOrigin | null,
): gsap.core.Timeline {
  const tl = gsap.timeline();

  if (prefersReducedMotion()) {
    gsap.set(backdrop, { autoAlpha: 1 });
    gsap.set(panel, { autoAlpha: 1, scale: 1, clearProps: "transform,transformOrigin" });
    return tl;
  }

  const transformOrigin = origin ? originTransformFromPanel(panel, origin) : "center center";
  gsap.set(backdrop, { autoAlpha: 0 });
  gsap.set(panel, {
    opacity: 0,
    visibility: "visible",
    scale: MODAL_ORIGIN_MOTION.scaleFrom,
    transformOrigin,
  });

  tl.to(backdrop, {
    autoAlpha: 1,
    duration: MODAL_ORIGIN_MOTION.openBackdropDuration,
    ease: MODAL_ORIGIN_MOTION.openBackdropEase,
  }, 0).to(
    panel,
    {
      opacity: 1,
      scale: 1,
      duration: MODAL_ORIGIN_MOTION.openPanelDuration,
      ease: MODAL_ORIGIN_MOTION.openPanelEase,
    },
    MODAL_ORIGIN_MOTION.openPanelDelay,
  );

  return tl;
}

export function playModalOriginClose(
  backdrop: HTMLElement,
  panel: HTMLElement,
  origin: ModalTriggerOrigin | null,
): gsap.core.Timeline {
  const tl = gsap.timeline();

  if (prefersReducedMotion()) {
    gsap.set([backdrop, panel], { autoAlpha: 0 });
    return tl;
  }

  const transformOrigin = origin ? originTransformFromPanel(panel, origin) : "center center";
  gsap.set(panel, { transformOrigin });

  tl.to(
    panel,
    {
      autoAlpha: 0,
      scale: MODAL_ORIGIN_MOTION.scaleFrom,
      duration: MODAL_ORIGIN_MOTION.closePanelDuration,
      ease: MODAL_ORIGIN_MOTION.closePanelEase,
    },
    0,
  ).to(
    backdrop,
    {
      autoAlpha: 0,
      duration: MODAL_ORIGIN_MOTION.closeBackdropDuration,
      ease: MODAL_ORIGIN_MOTION.closeBackdropEase,
    },
    MODAL_ORIGIN_MOTION.closeBackdropDelay,
  );

  return tl;
}


/* ===== components/ui/IconButton.tsx · SHA256 8441e89fb66d94d047d0529737da2999e82755c93d12d8b5d271c5531aaff117 ===== */
import { forwardRef, type ButtonHTMLAttributes } from "react";
import { cn } from "../shadcn/utils";
import { MOTION } from "./motion";
import { useSpringPress } from "./useSpringPress";

/**
 * 图标按钮 —— 统一语言：无边框、透明底、hover 静默着色。
 * 用于工具栏、行内操作等小图标按钮。
 */
export const IconButton = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>>(
  function IconButton({ className, ...rest }, ref) {
    const pressRef = useSpringPress(ref);
    return (
      <button
        ref={pressRef}
        className={cn(
          "ui-circle-icon-btn flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full text-text-secondary",
          MOTION.fast,
          "hover:bg-ui-tint hover:text-text-base",
          "disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
          className,
        )}
        {...rest}
      />
    );
  },
);


/* ===== components/ui/SpringDisclosure.tsx · SHA256 e3428445ed16330bbfc1294dcb3fc3ab6c0f5736efbc7bfba2c4df454408c269 ===== */
import { useLayoutEffect, useRef, useState, type ReactNode } from "react";
import gsap from "gsap";
import { SOFT_SPRING, prefersReducedMotion, springEase } from "./springMotion";
import { cn } from "../shadcn/utils";

/** Layout-size animation shared by process details, attachments and sidebars. */
export function SpringDisclosure({ open, children, className, axis = "height", size,
  unmountOnExit = false, animateOnMount = false,
}: { open: boolean; children: ReactNode; className?: string; axis?: "height" | "width";
  size?: number; unmountOnExit?: boolean; animateOnMount?: boolean }) {
  const shellRef = useRef<HTMLDivElement>(null);
  const bodyRef = useRef<HTMLDivElement>(null);
  const lastSize = useRef(0);
  const initialized = useRef(false);
  const tweenRef = useRef<gsap.core.Timeline>();
  const [present, setPresent] = useState(open);

  useLayoutEffect(() => {
    const first = !initialized.current;
    initialized.current = true;
    const shell = shellRef.current, body = bodyRef.current;
    if (!shell || !body) return;
    if (open) setPresent(true);
    body.inert = !open;
    let disposed = false;
    const animate = () => {
      const natural = axis === "width" ? size ?? body.offsetWidth : body.offsetHeight;
      const target = open ? natural : 0;
      if (shell.style[axis] === "auto" && !open) gsap.set(shell, { [axis]: lastSize.current });
      lastSize.current = natural || lastSize.current;
      tweenRef.current?.kill();
      const reduced = prefersReducedMotion() || (first && !animateOnMount);
      const m = SOFT_SPRING;
      tweenRef.current = gsap.timeline({ onComplete: () => {
        if (disposed) return;
        if (open && axis === "height") shell.style.height = "auto";
        if (!open && unmountOnExit) setPresent(false);
      } });
      tweenRef.current.to(shell, { [axis]: target, duration: reduced ? 0 : m.duration,
        ease: springEase(m.stiffness, m.damping, m.duration) }, 0);
      tweenRef.current.to(body, { opacity: open ? 1 : 0, y: open ? 0 : -3,
        filter: open ? "blur(0px)" : "blur(2px)", duration: reduced ? 0 : open ? 0.24 : 0.14,
        ease: "power2.out" }, 0);
    };
    animate();
    const observer = new ResizeObserver(() => {
      const next = axis === "width" ? size ?? body.offsetWidth : body.offsetHeight;
      if (open && Math.abs(next - lastSize.current) > 0.5) animate();
    });
    observer.observe(body);
    const media = window.matchMedia("(prefers-reduced-motion: reduce)");
    media.addEventListener("change", animate);
    return () => { disposed = true; observer.disconnect(); media.removeEventListener("change", animate); tweenRef.current?.kill(); };
  }, [open, axis, size, unmountOnExit, animateOnMount]);

  if (unmountOnExit && !open && !present) return null;
  return <div ref={shellRef} data-spring-disclosure={axis} aria-hidden={!open}
    className={cn("overflow-hidden", axis === "width" && "h-full shrink-0", className)} style={{ [axis]: 0 }}>
    <div ref={bodyRef} className={axis === "height" ? "flow-root" : "h-full"}
      style={axis === "width" ? { width: size } : undefined}>{children}</div>
  </div>;
}


/* ===== components/message.tsx · SHA256 6c10339bf3215e88d096b2e11d72d599246a99382f54da30582c33a308a90246 ===== */
import { createRoot, type Root } from "react-dom/client";
import { useEffect, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import type { IconProp } from "@fortawesome/fontawesome-svg-core";

// A dependency-free, imperative toast/message API (antd-style):
//   message.error("..."), message.success("..."), message.info("..."),
//   message.warning("...").
// It lazily mounts a single fixed container at the top-center of the window and
// renders an animated stack of auto-dismissing notices.

type MessageType = "success" | "error" | "info" | "warning";

interface Notice {
  id: number;
  type: MessageType;
  text: string;
  duration: number;
}

const TYPE_META: Record<MessageType, { icon: IconProp; color: string }> = {
  success: { icon: ["fas", "circle-check"], color: "text-green-500" },
  error: { icon: ["fas", "circle-info"], color: "text-red-500" },
  info: { icon: ["fas", "circle-info"], color: "text-blue-500" },
  warning: { icon: ["fas", "circle-info"], color: "text-amber-500" },
};

// Module-level bridge so the imperative API can push into the mounted host.
let pushNotice: ((n: Omit<Notice, "id">) => void) | null = null;
let seq = 0;
let root: Root | null = null;

function ensureHost() {
  if (root) return;
  const el = document.createElement("div");
  el.id = "message-root";
  document.body.appendChild(el);
  root = createRoot(el);
  root.render(<MessageHost />);
}

function MessageHost() {
  const [notices, setNotices] = useState<Notice[]>([]);

  useEffect(() => {
    pushNotice = (n) => {
      const id = ++seq;
      setNotices((prev) => [...prev, { ...n, id }]);
      window.setTimeout(() => {
        setNotices((prev) => prev.filter((x) => x.id !== id));
      }, n.duration);
    };
    return () => {
      pushNotice = null;
    };
  }, []);

  return (
    <div className="fixed top-4 inset-x-0 z-[9999] flex flex-col items-center gap-2 pointer-events-none">
      {notices.map((n) => {
        const meta = TYPE_META[n.type];
        return (
          <div
            key={n.id}
            className="message-notice pointer-events-auto flex items-center gap-2.5 bg-white border border-border-theme rounded-lg shadow-[0_6px_24px_rgb(0,0,0,0.12)] px-4 py-2.5 max-w-[480px]"
          >
            <FontAwesomeIcon icon={meta.icon} className={`${meta.color} text-sm`} />
            <span className="text-[13px] text-text-base">{n.text}</span>
          </div>
        );
      })}
    </div>
  );
}

function show(type: MessageType, text: string, duration = 3000) {
  ensureHost();
  // Defer one tick so a freshly-created host has registered its pusher.
  if (pushNotice) {
    pushNotice({ type, text, duration });
  } else {
    setTimeout(() => pushNotice?.({ type, text, duration }), 0);
  }
}

export const message = {
  success: (text: string, duration?: number) => show("success", text, duration),
  error: (text: string, duration?: number) => show("error", text, duration),
  info: (text: string, duration?: number) => show("info", text, duration),
  warning: (text: string, duration?: number) => show("warning", text, duration),
};


/* ===== types.ts · SHA256 d8cd71c710e6bd3d2fcef75cde87255508319ee2b76cbda08f50a776b6ec123b ===== */
// DTOs mirroring deepagent-app-core::dto (the kernel↔UI contract).

export interface SessionSummary {
  id: string;
  project?: string;
  title: string | null;
  mode: string;
  created_at: number;
  updated_at: number;
  ended: boolean;
  pinned: boolean;
}

export interface SessionSearchHit {
  session: SessionSummary;
  sequence: number;
  role: string | null;
  timestamp: number;
  snippet: string;
}

export interface SessionUiPrefs {
  env_panel_auto_open: boolean;
}

export interface SidebarSection { id: string; name: string; pinned?: boolean }
export interface SidebarPreferences {
  version: number;
  revision: number;
  sections: SidebarSection[];
  unread: Record<string, boolean>;
  session_sections: Record<string, string>;
  default_open_target: string;
}
export type SidebarPreferenceAction =
  | { type: "set_unread"; session_id: string; unread: boolean }
  | { type: "set_section"; session_id: string; section_id: string | null }
  | { type: "create_section"; name: string; session_id: string | null }
  | { type: "set_section_pinned"; section_id: string; pinned: boolean }
  | { type: "rename_section"; section_id: string; name: string }
  | { type: "remove_section"; section_id: string }
  | { type: "set_open_target"; target: string };

export interface TimelineEntry {
  sequence: number;
  timestamp: number;
  kind: string;
  icon: string;
  label: string;
  detail: string | null;
  duration_ms: number | null;
}

export interface SessionStats {
  event_count: number;
  messages: number;
  tool_calls: number;
  tool_successes: number;
  tool_failures: number;
  total_tool_ms: number;
  tool_success_rate: number | null;
  duration_ms: number;
}

export interface SessionDetail {
  version?: number;
  project_path?: string | null;
  summary: SessionSummary;
  timeline: TimelineEntry[];
  stats: SessionStats;
  conversation?: ConversationMessage[];
  context_usage?: ContextUsageSnapshot;
}

export interface RuntimeLogEntry {
  id: number;
  ts_ms: number;
  level: string;
  category: string;
  event: string;
  run_id: string | null;
  session_id: string | null;
  task_id: string | null;
  correlation_id: string | null;
  source: string | null;
  message: string | null;
  data: unknown;
}

export interface Command {
  id: string;
  title: string;
  description: string;
  category: string;
  shortcut: string | null;
}

export interface ComposerSkillSelection {
  id: string;
  name: string;
}

export interface ApprovalRequest {
  call_id: string;
  tool: string;
  risk: string;
  arguments: string;
  reason: string;
  /**
   * Scopes an approval may be widened to, decided by the backend gate. Buttons are
   * drawn from this list so the UI never offers a choice the server would reject.
   */
  scopes?: string[];
  /**
   * The `Bash(prefix:*)` rule a `command_prefix` answer would store, shown on the
   * button so a standing allowance is named before it is granted.
   */
  command_prefix?: string | null;
  run_id?: string;
}

/** One choice inside a question the model asked mid-run. */
export interface QuestionOption {
  label: string;
  description: string;
}

/** A single question with its options (`ask_user_question`). */
export interface AskUserQuestion {
  question: string;
  header: string;
  options: QuestionOption[];
  multi_select: boolean;
}

/** A question set the run is blocked on until the user answers it. */
export interface QuestionRequest {
  question_id: string;
  /** Session that owns this pending question. */
  session_id: string;
  questions: AskUserQuestion[];
}

export type DiffKind = "context" | "added" | "removed";

export interface DiffLine {
  kind: DiffKind;
  old_line: number | null;
  new_line: number | null;
  content: string;
}

export interface DiffResult {
  lines: DiffLine[];
  added: number;
  removed: number;
}

/** A live tool-call card shown inline in an assistant message. */
export interface ToolCall {
  /** Correlation id from the runtime (matches started→completed events). */
  call_id: string;
  /** Tool name, e.g. "web_search". */
  name: string;
  /** JSON-stringified arguments (rendered, streamed as they arrive). */
  args: string;
  /** Lifecycle status of this call. */
  status: "running" | "ok" | "error" | "blocked";
  /** Wall-clock duration in ms once completed. */
  durationMs?: number;
  /** Short result/error summary once completed. */
  detail?: string;
  /** Raw tool output, when available, for richer tool cards. */
  output?: unknown;
  /** Coarse backend-provided category for row-based rendering. */
  toolKind?: string;
  /** Primary file path / URL / target, when known. */
  filePath?: string;
  /** Compact backend-provided one-line summary. */
  summary?: string;
  /** Lightweight structured backend hints for the row renderer. */
  meta?: Record<string, unknown>;
}

/** One ordered segment of an assistant turn, in the order it streamed in. */
export type MessagePart =
  | { kind: "reasoning"; text: string }
  | { kind: "text"; text: string; tone?: "normal" | "error" }
  | { kind: "tool"; tool: ToolCall }
  | {
      kind: "compact_boundary";
      tokensBefore: number;
      tokensAfter: number;
      strategy: string;
      summary: string;
    }
  | { kind: "memories"; count: number; ids: string[]; latencyMs?: number };

/** A reconstructed conversation part from a replayed session (mirrors
 * deepagent-app-core::ConversationPartDto). */
export type ConversationPart =
  | { kind: "text"; text: string }
  | { kind: "reasoning"; text: string }
  | {
      kind: "compact_boundary";
      tokens_before: number;
      tokens_after: number;
      strategy: string;
      summary: string;
      first_kept_sequence?: number | null;
    }
  | {
      kind: "memories";
      count: number;
      ids?: string[];
      latency_ms?: number;
    }
  | {
      kind: "tool";
      call_id: string;
      name: string;
      args: string;
      status: string;
      duration_ms: number | null;
      detail: string | null;
      output?: unknown | null;
      tool_kind?: string | null;
      file_path?: string | null;
      summary?: string | null;
      meta?: Record<string, unknown> | null;
    };

/** A reconstructed conversation message (mirrors ConversationMessageDto).
 * `system` marks a session-level event row (compaction boundary, injected
 * memories) rather than something either side said. */
export interface ConversationMessage {
  id?: string;
  turn?: { version: 1; id: string; terminal_kind?: string | null; final_answer?: string | null };
  role: "user" | "assistant" | "system";
  content: string;
  attachments?: PersistedAttachment[];
  parts: ConversationPart[];
  usage?: ConversationUsage;
}

/** Persisted per-turn usage (mirrors ConversationUsageDto). */
export interface ConversationUsage {
  prompt_tokens: number;
  completion_tokens: number;
  reasoning_tokens: number;
  total_tokens: number;
  prompt_cache_hit_tokens: number;
  prompt_cache_miss_tokens: number;
  duration_ms: number;
  raw_responses_usage?: unknown;
  cost_yuan?: number;
}

/**
 * Token usage for THIS assistant turn (per-turn, not cumulative).
 *
 * For stateless models like DeepSeek, every request resends the full context:
 * - `promptTokens`: current context window size (grows until compaction)
 * - `completionTokens`: THIS turn's output tokens (NOT cumulative)
 * - `totalTokens`: should equal `promptTokens + completionTokens`
 *
 * For cumulative session-wide statistics, see `CostSummary` (billing ledger).
 * For current window occupancy, see `ContextUsageSnapshot.projected_request_tokens`.
 */
export interface TokenUsage {
  /** Prompt (input) tokens from THIS turn (current context window size). */
  promptTokens: number;
  /** Completion (output) tokens from THIS turn (NOT cumulative). */
  completionTokens: number;
  /** Reasoning tokens from THIS turn (subset of completionTokens). */
  reasoningTokens: number;
  /** Total tokens for THIS turn (should equal promptTokens + completionTokens). */
  totalTokens: number;
  /** Cache hit tokens from THIS turn (last-wins). */
  cacheHitTokens: number;
  /** Cache miss tokens from THIS turn (last-wins). */
  cacheMissTokens: number;
  /** Cost in CNY/RMB for THIS turn. undefined when pricing unavailable. */
  costYuan?: number;
}

/** Where a resolved model capability came from (mirrors CapabilitySource). */
export type CapabilitySource =
  | "provider_metadata"
  | "bundled_official_snapshot"
  | "user_override"
  | "conservative_fallback";

/**
 * Resolved capability for one model (mirrors deepagent-models::ModelCapability).
 * Resolved by the backend through the same resolver the runtime uses, so the UI
 * shows the real context capacity instead of guessing it from the model id.
 */
export interface ModelCapability {
  model_id: string;
  context_window: number;
  max_output_tokens: number;
  supports_tools: boolean;
  supports_thinking: boolean;
  supports_json_output: boolean;
  capability_source: CapabilitySource;
  fallback_reason?: string;
}

export type ContextBlockKind =
  | "stable_prefix"
  | "dynamic_runtime"
  | "task_summary"
  | "recent_conversation"
  | "retrieved_knowledge"
  | "retrieved_code"
  | "tool_schemas"
  | "tool_result_refs"
  | "attachments"
  | "user_goal"
  | "other";

export type ContextCacheScope = "stable_prefix" | "dynamic";

export interface ContextBlockUsage {
  name: string;
  kind: ContextBlockKind;
  tokens: number;
  cache_scope: ContextCacheScope;
  source: string;
}

export interface ContextUsageSnapshot {
  model_id: string;
  context_window: number;
  prompt_budget: number;
  /** Assembly estimate by the local counter, block by block. */
  estimated_prompt_tokens: number;
  /** Legacy alias for the last provider request; not current occupancy. */
  measured_prompt_tokens?: number;
  projected_request_tokens?: number;
  last_request_prompt_tokens?: number;
  compaction_threshold?: number;
  surface_tokens?: number;
  tool_schema_tokens?: number;
  attachment_tokens?: number;
  used_ratio: number;
  reserved_output_tokens: number;
  reserved_tool_tokens: number;
  cache_hit_tokens: number;
  cache_miss_tokens: number;
  cache_hit_ratio?: number;
  compacted: boolean;
  blocks: ContextBlockUsage[];
}

/** Budget configuration (mirrors deepagent-app-core::BudgetConfig). */
export interface BudgetConfig {
  daily_limit: number | null;
  monthly_limit: number | null;
}

/** Accumulated cost summary (mirrors deepagent-app-core::CostSummary). */
/**
 * Session-wide cost and token statistics (billing ledger, cumulative).
 *
 * All token fields are CUMULATIVE across all requests in the session:
 * - `input_tokens`: sum of all `prompt_tokens` (billing dimension)
 * - `output_tokens`: sum of all `completion_tokens`
 *
 * This is the source for `SessionUsageBar` (bottom bar statistics).
 * For per-turn usage, see `TokenUsage`. For current window occupancy,
 * see `ContextUsageSnapshot.projected_request_tokens`.
 */
export interface CostSummary {
  session_cost: number;
  today_cost: number;
  month_cost: number;
  total_cost: number;
  currency: string;
  budget: BudgetConfig;
  /** Cumulative input tokens across ALL requests (billing ledger). */
  input_tokens: number;
  /** Cumulative output tokens across ALL requests (billing ledger). */
  output_tokens: number;
  /** Cumulative cache hit tokens across ALL requests. */
  cache_hit_tokens: number;
  /** Cumulative cache miss tokens across ALL requests. */
  cache_miss_tokens: number;
}

/** One per-currency balance row (mirrors deepagent-app-core::BalanceInfoDto). */
export interface BalanceInfo {
  currency: string;
  total_balance: string;
  granted_balance: string;
  topped_up_balance: string;
}

/** Account balance summary (mirrors deepagent-app-core::BalanceDto). */
export interface Balance {
  is_available: boolean;
  infos: BalanceInfo[];
}

export type DiagStatus = "ok" | "warning" | "error";

export interface DiagnosticResult {
  name: string;
  status: DiagStatus;
  detail: string;
  fix_hint: string | null;
}

/** File that was edited during a run (mirrors deepagent-runtime::EditedFile). */
export interface EditedFile {
  path: string;
  lines_added: number;
  lines_removed: number;
}

/** A message rendered in the chat view. */
export interface ChatMessage {
  id?: string;
  /** Transient presentation state; restored from the durable turn projection. */
  turn?: import("./components/chat-timeline/processDisclosure").TurnActivity;
  /** `system` rows are session-level markers (compaction boundary, injected
   * memories) that neither the user nor the model produced. */
  role: "user" | "assistant" | "system";
  content: string;
  /** Inline skill chips selected from slash/typeahead for this user turn. */
  selectedSkills?: ComposerSkillSelection[];
  /** Inline @ mentions selected as context for this user turn. */
  mentions?: ComposerMention[];
  /** Attachments displayed with a user turn. Model-readable attachment text is
   * kept out of this field and injected only into the backend prompt. */
  attachments?: ComposerAttachment[];
  tone?: "normal" | "error";
  /** Tool-call cards attached to an assistant turn (in invocation order). */
  tools?: ToolCall[];
  /** DeepSeek Thinking-Mode reasoning trace (shown as a collapsible block). */
  reasoning?: string;
  /**
   * Ordered parts of a live assistant turn (reasoning / tool / text) in the
   * exact order they streamed. When present, the view renders these in
   * sequence (preserving chronology); otherwise it falls back to the legacy
   * tools/reasoning/content fields (used by replayed timeline messages).
   */
  parts?: MessagePart[];
  /** Accumulated token usage for this turn (shown under the final answer). */
  usage?: TokenUsage;
  /** Wall-clock duration of the run that produced this turn, in ms. */
  runMs?: number;
  /** Files edited during this run (shown at turn completion). */
  editedFiles?: EditedFile[];
}

export type ComposerAttachmentKind = "text" | "image" | "file";
export type ComposerAttachmentSource = "paste" | "drop" | "picker";
export type ComposerAttachmentStatus = "ready" | "processing" | "error";

/**
 * Structured attachment reference sent with a chat run. Mirrors the Rust
 * `InputAttachment` tagged enum (`type` discriminator, snake_case fields).
 * Only the reference travels; image bytes are materialized into provider image
 * parts server-side at request time.
 */
export type RunAttachment =
  | { type: "image"; id: string; path: string; media_type?: string | null }
  | { type: "file"; id: string; path: string }
  | { type: "text"; id: string; content: string };

export interface ComposerAttachment {
  id: string;
  kind: ComposerAttachmentKind;
  name: string;
  mime: string;
  size: number;
  source: ComposerAttachmentSource;
  localPath?: string;
  dataUrl?: string;
  extractedText?: string;
  storageDir?: string;
  originalPath?: string;
  sha256?: string;
  backendMessage?: string;
  status: ComposerAttachmentStatus;
  error?: string;
}

export interface AttachmentIngestInput {
  id?: string | null;
  session_id?: string | null;
  kind: ComposerAttachmentKind;
  name: string;
  mime: string;
  source: ComposerAttachmentSource;
  local_path?: string | null;
  data_url?: string | null;
  text?: string | null;
}

export interface PersistedAttachment {
  id: string;
  session_id?: string | null;
  kind: ComposerAttachmentKind;
  name: string;
  mime: string;
  size_bytes: number;
  source: ComposerAttachmentSource;
  storage_dir: string;
  original_path?: string | null;
  extracted_text?: string | null;
  preview?: PreviewResult | null;
  sha256?: string | null;
  status: "ready" | "error";
  message?: string | null;
}

/** A skill's Level-1 metadata (mirrors deepagent-app-core::SkillDto). */
export interface Skill {
  id: string;
  name: string;
  description: string;
  version: string | null;
  origin: string; // "workspace" | "user" | "installed" | "built_in"
  triggers: string[];
}

/** A disclosed skill body (mirrors deepagent-app-core::SkillActivationDto). */
export interface SkillActivation {
  id: string;
  body: string;
}

/** An MCP server entry (mirrors deepagent-app-core::McpServerDto). */
export interface McpServer {
  name: string;
  transport: string; // "stdio" | "sse" | "http" | "ws"
  enabled: boolean;
  command: string | null;
  args: string[];
  env: Record<string, string>;
  cwd?: string | null;
  url: string | null;
  headers: Record<string, string>;
  source: "user" | "plugin" | string;
  source_plugin_id?: string | null;
  source_plugin_name?: string | null;
  declared_name?: string | null;
  source_path?: string | null;
  read_only: boolean;
  conflict?: string | null;
}

/** A tool exposed by a connected MCP server (mirrors McpToolInfoDto). */
export interface McpToolInfo {
  name: string;
  description: string;
}

/** Host-published connection status of an MCP server (mirrors McpConnectionStatusDto). */
export interface McpConnectionStatus {
  name: string;
  status: "connected" | "failed" | "disabled" | "preparing";
  error: string | null;
  tools: McpToolInfo[];
}

/** How much a plugin load-time finding affects usability (Agent Plugins §11.3). */
export type PluginDiagnosticSeverity = "info" | "warning" | "error";

export interface PluginLoadError {
  kind: string;
  plugin?: string | null;
  source: string;
  path?: string | null;
  component?: string | null;
  message: string;
  /**
   * Absent on payloads predating this field; treat a missing value as "error"
   * so a real failure is never presented as a note.
   */
  severity?: PluginDiagnosticSeverity;
}

export interface PluginSourceInfo {
  kind: string;
  name: string;
  marketplace?: string | null;
  path?: string | null;
}

export interface PluginDependent {
  id: string;
  name: string;
  display_name: string;
}

export type PluginExecutionKind =
  | "host_backed"
  | "skill_only"
  | "subprocess"
  | "managed_runtime"
  | "mcp_sidecar";

export type PluginLifecycleState =
  | "preparing"
  | "discovered"
  | "parsed"
  | "installed"
  | "runtime_ready"
  | "executable"
  | "verified"
  | "incomplete"
  | "failed";

export type PluginHealthStatus =
  | "ready"
  | "needs_configuration"
  | "needs_authorization"
  | "connection_unavailable"
  | "runtime_unavailable"
  | "incomplete"
  | "failed"
  | "unknown";

export type PluginLicenseStatus =
  | "first_party"
  | "bundled_third_party"
  | "marketplace_only"
  | "missing"
  | "unknown";

export interface Plugin {
  id: string;
  name: string;
  display_name: string;
  description: string;
  long_description?: string | null;
  version?: string | null;
  local_version?: string | null;
  developer?: string | null;
  source: PluginSourceInfo;
  origin: string;
  dialect: string;
  path?: string | null;
  data_dir: string;
  manifest_path?: string | null;
  installed: boolean;
  enabled: boolean;
  available: boolean;
  update_available: boolean;
  overridden_by?: string | null;
  category?: string | null;
  keywords: string[];
  capabilities: string[];
  permissions: string[];
  skill_count: number;
  mcp_server_count: number;
  hook_count: number;
  command_count: number;
  app_count: number;
  output_style_count: number;
  state: PluginLifecycleState;
  execution_kind: PluginExecutionKind;
  runtime_required: boolean;
  runtime_available: boolean;
  entrypoints: string[];
  has_runtime_payload: boolean;
  license_status: PluginLicenseStatus;
  health_status: PluginHealthStatus;
  last_health_check?: string | null;
  health_error?: string | null;
  icon_path?: string | null;
  logo_path?: string | null;
  brand_color?: string | null;
  required_by: PluginDependent[];
  errors: PluginLoadError[];
}

/** The catalog payload returned by list_plugins; runtime details use read_plugin. */
export interface PluginSummary {
  id: string;
  name: string;
  display_name: string;
  description: string;
  developer?: string | null;
  origin: string;
  category?: string | null;
  keywords: string[];
  capabilities: string[];
  installed: boolean;
  enabled: boolean;
  available: boolean;
  skill_count: number;
  mcp_server_count: number;
  hook_count: number;
  output_style_count: number;
  icon_path?: string | null;
  logo_path?: string | null;
  brand_color?: string | null;
  required_by: PluginDependent[];
}

export interface PluginApp {
  plugin_id: string;
  plugin_name: string;
  id: string;
  title: string;
  description?: string | null;
  placement: string;
  component: string;
  icon?: string | null;
  category?: string | null;
  source_path?: string | null;
}

export interface PluginOutputStyle {
  plugin_id: string;
  plugin_name: string;
  name: string;
  description: string;
  prompt: string;
  force_for_plugin?: boolean | null;
  source_path?: string | null;
}

export interface CreatePluginDraft {
  name: string;
  description?: string | null;
  directory?: string | null;
  category?: string | null;
}

export interface PluginMarketplace {
  name: string;
  source: string;
  git_ref?: string | null;
  sparse_path?: string | null;
  install_location?: string | null;
  last_updated?: string | null;
}

export interface PluginMarketplaceComponentSummary {
  skills: number;
  commands: number;
  agents: number;
  hooks: number;
  mcp: number;
  apps: number;
  outputStyles: number;
}

export interface PluginMarketplaceRuntimeSummary {
  required: boolean;
  requirements: string[];
  hasRuntimePayload: boolean;
}

export interface PluginMarketplaceEntry {
  marketplace: string;
  name: string;
  display_name: string;
  description: string;
  repository_full_name?: string | null;
  stargazers_count: number;
  topics: string[];
  version?: string | null;
  category?: string | null;
  license?: string | null;
  content_hash?: string | null;
  components: PluginMarketplaceComponentSummary;
  skill_count: number;
  command_count: number;
  agent_count: number;
  hook_count: number;
  mcp_count: number;
  app_count: number;
  output_style_count: number;
  runtime: PluginMarketplaceRuntimeSummary;
  runtime_required: boolean;
  runtime_requirements: string[];
  has_runtime_payload: boolean;
  source_commit?: string | null;
  source_kind: string;
  source: string;
  installable: boolean;
  install_block_reason?: string | null;
  installed: boolean;
  enabled: boolean;
  update_available: boolean;
  policy_installation?: string | null;
  policy_authentication?: string | null;
  authentication_required: boolean;
  authentication_hint?: string | null;
}

export interface PluginMarketplaceEntriesQuery {
  marketplace?: string | null;
  query?: string;
  page?: number;
  per_page?: number;
}

export interface PluginMarketplacePage {
  entries: PluginMarketplaceEntry[];
  total_count: number;
  page: number;
  per_page: number;
  has_next: boolean;
  query: string;
}

export interface PluginRiskItem {
  severity: string;
  category: string;
  title: string;
  detail: string;
  path?: string | null;
}

export interface PluginComponentSummary {
  kind: string;
  name: string;
  description: string;
  path?: string | null;
  details: string[];
}

export interface PluginScanReport {
  source_dir: string;
  manifest_ok: boolean;
  plugin_name?: string | null;
  file_count: number;
  total_bytes: number;
  component_summaries: PluginComponentSummary[];
  risks: PluginRiskItem[];
  errors: string[];
}

export interface PreparedPluginInstall {
  token: string;
  marketplace: string;
  plugin: string;
  plugin_id: string;
  version?: string | null;
  source_kind: string;
  source: string;
  content_hash: string;
  staging_path: string;
  plugin_root: string;
  destination_path: string;
  scan_report: PluginScanReport;
  runtime_inspection?: unknown;
}

/** One row in a structured slash-command panel (mirrors SlashPanelItem). */
export interface SlashPanelItem {
  label: string;
  value?: string | null;
  status?: "ok" | "warn" | "error" | "muted" | "info" | null;
  badges?: string[];
  mono?: boolean;
  children?: SlashPanelItem[];
}

/** A group of rows under an optional heading (mirrors SlashSection). */
export interface SlashSection {
  heading?: string | null;
  items: SlashPanelItem[];
}

/** Structured info panel for slash commands (mirrors SlashPanel). */
export interface SlashPanel {
  title: string;
  subtitle?: string | null;
  sections: SlashSection[];
}

/** Declarative permission rules (mirrors deepagent-hooks::PermissionRules). */
export interface PermissionRules {
  allow: string[];
  ask: string[];
  deny: string[];
}

/** Result of forking a session (mirrors deepagent-app-core::ForkResultDto). */
export interface ForkResult {
  new_session_id: string;
  source_session_id: string;
  forked_at: number;
  restored_paths: string[];
}

/** Result of rewinding a session (mirrors deepagent-app-core::RewindResultDto). */
export interface RewindResult {
  session_id: string;
  kept_through: number;
  events_removed: number;
  restored_paths: string[];
}

/** An exported session transcript (mirrors deepagent-app-core::TranscriptDto). */
export interface Transcript {
  session_id: string;
  format: string;
  extension: string;
  content: string;
}

/** The active project folder (mirrors deepagent-app-core::WorkspaceInfoDto). */
export interface WorkspaceInfo {
  name: string;
  path: string;
}

export interface SandboxieStatus {
  supported: boolean;
  ready: boolean;
  box_name: string;
  install_dir?: string | null;
  start_exe?: string | null;
  sbie_ini_exe?: string | null;
  bundled_installer?: string | null;
  message: string;
}

export type PermissionPreset = "default" | "auto_review" | "full_access";

export interface PermissionPresetVisibility {
  default_enabled: boolean;
  auto_review_enabled: boolean;
  full_access_enabled: boolean;
}

/** Redacted settings view (mirrors deepagent-app-core::SettingsView). */
export interface SettingsView {
  api_key_masked: string;
  base_url: string;
  available_models: string[];
  /**
   * Resolved capability (context window / max output) for every discovered
   * model, resolved by the runtime's own resolver. Lets the UI show the model's
   * real context capacity instead of guessing it from the model id.
   */
  model_capabilities: ModelCapability[];
  chat_model: string;
  reasoner_model: string;
  configured: boolean;
  approval_policy: string;
  sandbox_mode: "read_only" | "workspace_write" | "full_access";
  terminal_shell: "powershell" | "command_prompt" | "git_bash" | "wsl";
  thinking_depth: "simple" | "medium" | "deep";
  responses: ResponsesApiSettings;
  web_search: WebSearchSettings;
  vision: VisionSettings;
  execution_features: ExecutionFeatures;
  /** Built-in output style (§7.1): default | explanatory | learning. */
  output_style: string;
}

/**
 * Opt-in advanced execution safeguards (§2.2/§2.3/§6.1/§6.2). All default OFF.
 * The matching `DEEPAGENT_*` env var force-enables each one regardless.
 */
export interface ExecutionFeatures {
  /** §2.3 stall/laziness detector: audit final answers for false completion. */
  stall_detector: boolean;
  /** §6.1 LLM command-injection guard on suspicious shell commands. */
  command_guard: boolean;
  /** §6.2 per-project trust gate: untrusted dirs escalate bash to approval. */
  project_trust: boolean;
  /** §2.2 adversarial goal verifier: skeptic panel audits goal coverage. */
  adversarial_verify: boolean;
}

export type WebSearchProvider = "deepseek_first" | "searxng" | "duckduckgo";

export interface WebSearchSettings {
  enabled: boolean;
  provider: WebSearchProvider;
  searxng_url: string | null;
  anysearch_enabled: boolean;
  anysearch_base_url: string | null;
  anysearch_api_key_configured: boolean;
}

export interface ResponsesApiSettings {
  creativity: number | null;
  scene: "code" | "email" | "analysis" | "creative" | null;
  temperature: number | null;
  top_p: number | null;
  max_output_tokens: number | null;
  top_logprobs: number | null;
  reasoning_effort: string | null;
  text: unknown | null;
  tool_choice: unknown | null;
  user: string | null;
  developer: Record<string, unknown>;
  ineffective: Record<string, unknown>;
}

export interface AnySearchApiKeyInfo {
  has_user_key: boolean;
}

export interface AnySearchTestResult {
  ok: boolean;
  error: string | null;
  provider: string | null;
  count: number | null;
}

export type VisionMode = "off" | "system";

export interface VisionSettings {
  mode: VisionMode;
  provider: string;
  base_url: string;
  api_key?: string | null;
  api_key_configured: boolean;
  system_model: string;
  timeout_ms: number;
  auto_analyze_pasted_images: boolean;
}

export interface VisionRecognizeRequest {
  image_path: string;
  prompt?: string | null;
}

export interface VisionRecognizeResult {
  model_id: string;
  model_path: string;
  text: string;
  raw_json: string;
}

/** A project folder in the sidebar (mirrors deepagent-app-core::ProjectDto). */
export interface Project {
  name: string;
  path: string;
  pinned: boolean;
  session_count: number;
  updated_at: number;
}

export interface GitProjectStatus {
  project_path: string;
  repo_root: string | null;
  repo_id: string | null;
  is_repo: boolean;
  current_branch: string | null;
  detached_head: boolean;
  upstream: string | null;
  ahead: number;
  behind: number;
  has_changes: boolean;
  files_changed: number;
  additions: number;
  deletions: number;
  rebase_state: string | null;
  merge_state: boolean;
  gh_available: boolean;
}

export interface GitBranch {
  name: string;
  full_name: string;
  kind: "local" | "remote" | string;
  current: boolean;
  upstream: string | null;
  ahead: number;
  behind: number;
  commit: string | null;
  subject: string | null;
  worktree_path: string | null;
}

export interface GitChangedFile {
  path: string;
  old_path: string | null;
  status: string;
  category: "staged" | "unstaged" | "untracked" | "conflicted" | string;
  additions: number;
  deletions: number;
  /** Additive v1 field; false/missing means counts are not authoritative. */
  line_stats_available?: boolean;
}

export interface GitChanges {
  /** v1 provides line_stats_available; older backends omit this field. */
  version?: number;
  project_path: string;
  repo_root: string | null;
  is_repo: boolean;
  files: GitChangedFile[];
  additions: number;
  deletions: number;
}

export interface GitDiff {
  project_path: string;
  repo_root: string | null;
  file_path: string;
  staged: boolean;
  is_repo: boolean;
  text: string;
  truncated: boolean;
}

export interface GitCommitFile {
  path: string;
  old_path: string | null;
  status: string;
  additions: number;
  deletions: number;
}

export interface GitLogEntry {
  hash: string;
  full_hash: string;
  parents: string[];
  author_name: string;
  author_email: string;
  date: string;
  refs: string[];
  subject: string;
  files: GitCommitFile[];
}

export interface GitOperationResult {
  ok: boolean;
  command: string;
  stdout: string;
  stderr: string;
}

export interface GitCommitMessageDraft {
  project_path: string;
  repo_root: string | null;
  is_repo: boolean;
  source: "staged" | "working_tree" | "none" | string;
  title: string;
  body: string;
  files: GitChangedFile[];
  blocked_reason: string | null;
}

export interface GitPushCommit {
  hash: string;
  full_hash: string;
  author_name: string;
  date: string;
  subject: string;
}

export interface GitPushPreview {
  project_path: string;
  repo_root: string | null;
  is_repo: boolean;
  current_branch: string | null;
  upstream: string | null;
  remote: string | null;
  remote_branch: string | null;
  ahead: number;
  behind: number;
  commits: GitPushCommit[];
  blocked_reason: string | null;
}

export interface GitPushRiskItem {
  severity: "high" | "medium" | "low" | string;
  category: string;
  title: string;
  detail: string;
  file_path: string | null;
}

export interface GitPushRiskScan {
  project_path: string;
  repo_root: string | null;
  is_repo: boolean;
  current_branch: string | null;
  upstream: string | null;
  ahead: number;
  scanned_files: number;
  risks: GitPushRiskItem[];
  blocked_reason: string | null;
}

export interface GitCompareCommit {
  side: "base" | "target" | string;
  hash: string;
  full_hash: string;
  author_name: string;
  date: string;
  subject: string;
}

export interface GitRefCompare {
  project_path: string;
  repo_root: string | null;
  is_repo: boolean;
  base_ref: string;
  target_ref: string;
  merge_base: string | null;
  ahead: number;
  behind: number;
  commits: GitCompareCommit[];
  files: GitCommitFile[];
  blocked_reason: string | null;
}

export interface GitBatchCommitTarget {
  project_path: string;
  message: string | null;
}

export interface GitBatchCommitPreviewItem {
  project_path: string;
  repo_root: string | null;
  is_repo: boolean;
  current_branch: string | null;
  files_changed: number;
  staged_files: number;
  additions: number;
  deletions: number;
  ahead: number;
  behind: number;
  blocked_reason: string | null;
}

export interface GitBatchProjectResult {
  project_path: string;
  current_branch: string | null;
  ok: boolean;
  committed: boolean;
  pushed: boolean;
  skipped: boolean;
  message: string;
  commit_result: GitOperationResult | null;
  push_result: GitOperationResult | null;
}

export interface GitWorktree {
  path: string;
  head: string | null;
  branch: string | null;
  detached: boolean;
  bare: boolean;
}

/** §6.2 per-project workspace trust status. */
export interface ProjectTrust {
  project: string;
  trusted: boolean;
  /** Whether trust enforcement (untrusted → bash approval) is currently active. */
  enforced: boolean;
}

export interface ArchivedConversation {
  session_id: string;
  title: string | null;
  project: string | null;
  project_path: string | null;
  archived_at: number;
  deleted_at: number | null;
  updated_at: number;
}

export interface ArchiveProjectResult {
  project_path: string;
  project_name: string;
  archived_count: number;
}

export interface ProjectMapStatus {
  status: "missing" | "ready" | "stale" | "updating" | "failed" | string;
  source: string | null;
  graph_path: string | null;
  updated_at: number | null;
  nodes: number;
  edges: number;
  files: number;
  functions: number;
  classes: number;
  last_error: string | null;
}

export interface ProjectMapHit {
  node_id: string;
  node_type: string;
  name: string;
  file_path: string | null;
  summary: string;
  complexity: "simple" | "moderate" | "complex" | string;
  score: number;
}

export interface ProjectMapEdge {
  source: string;
  target: string;
  edge_type: string;
  weight: number;
}

export interface ProjectMapGraph {
  nodes: ProjectMapHit[];
  edges: ProjectMapEdge[];
}

export interface ProjectMapOverview {
  status: ProjectMapStatus;
  project_name: string | null;
  description: string | null;
  languages: string[];
  frameworks: string[];
  complex_nodes: ProjectMapHit[];
}

export interface ProjectMapNode {
  id: string;
  node_type: string;
  name: string;
  file_path: string | null;
  line_range: [number, number] | null;
  summary: string;
  tags: string[];
  complexity: string;
  language_notes: string | null;
}

export interface ProjectMapNeighbor {
  edge_type: string;
  direction: string;
  node: ProjectMapHit;
}

export interface ProjectMapNeighbors {
  node: ProjectMapNode | null;
  imports: ProjectMapNeighbor[];
  imported_by: ProjectMapNeighbor[];
  calls: ProjectMapNeighbor[];
  called_by: ProjectMapNeighbor[];
  related: ProjectMapNeighbor[];
}

export interface ProjectMapImpact {
  target: ProjectMapNode | null;
  direct: ProjectMapHit[];
  indirect: ProjectMapHit[];
}

export interface ProjectMapRefresh {
  ok: boolean;
  graph_path: string;
  files: number;
  nodes: number;
  edges: number;
  duration_ms: number;
  truncated: boolean;
  message: string;
  status: ProjectMapStatus;
}

/** A knowledge entry (mirrors deepagent-app-core::KnowledgeDto). */
export interface KnowledgeEntry {
  id: string;
  title: string;
  kind: string; // "pitfall" | "solution" | "command" | "config" | "note" | "memory"
  tags: string[];
  scope: string; // "project" | "global"
  created_at: number;
  updated_at: number;
  source_session: string | null;
  body: string;
  /** Memory tier when `kind === "memory"` (semantic/episodic/procedural/...). */
  tier?: string | null;
  /** Baseline importance used by `memory_recall` ranking. */
  importance?: number | null;
}

/** A knowledge search hit (mirrors deepagent-app-core::KnowledgeHitDto). */
export interface KnowledgeHit {
  id: string;
  title: string;
  kind: string;
  scope: string;
  score: number;
  excerpt: string;
  /** Entry last-update time (Unix ms); the kernel adds the staleness caveat. */
  updated_at?: number;
}

/** A draft to create/update a knowledge entry (mirrors KnowledgeDraftDto). */
export interface KnowledgeDraft {
  title: string;
  body: string;
  kind?: string | null;
  tags: string[];
  scope?: string | null;
  source_session?: string | null;
}

// ===========================================================================
// Skill Marketplace (skillsmp.com + GitHub install flow)
// ---------------------------------------------------------------------------
// Mirrors the Rust types reachable via the `skill_market_*` Tauri commands.
// Field-naming is verified against the actual `#[serde(...)]` attributes:
//   - MarketSkill / Pagination use field-level `rename = "camelCase"` for a
//     handful of keys (the rest stay snake_case).
//   - ScanReport / FileInfo / RiskItem stay snake_case (no crate-wide rename),
//     except `FileInfo` renames its Rust field `kind` to `type` on the wire.
//   - RiskCategory / RiskSeverity / SortBy / SkillsMpKeySource serialize as
//     lowercase string variants (`#[serde(rename_all = "lowercase")]`).
//   - MarketSearchInput is the only marketplace command struct with crate-wide
//     `rename_all = "camelCase"` — so its `sortBy` field is camelCase on the
//     wire.
// ===========================================================================

/** One row from `GET /api/v1/skills/search` (mirrors deepagent-skills::MarketSkill). */
export interface MarketSkill {
  id: string;
  name: string;
  author: string;
  description: string;
  /** GitHub source URL (`https://github.com/{owner}/{repo}/tree/{branch}/{path}`). */
  githubUrl: string;
  /** Canonical skillsmp.com page URL for the "view on web" link. */
  skillUrl: string;
  stars: number;
  /** Last update — unix epoch seconds. */
  updatedAt: number;
}

/** Pagination block from `data.pagination` (mirrors deepagent-skills::Pagination). */
export interface Pagination {
  page: number;
  limit: number;
  total: number;
  hasNext: boolean;
  hasPrev: boolean;
}

/** Search-response body envelope (mirrors deepagent-skills::MarketSearchData). */
export interface MarketSearchData {
  skills: MarketSkill[];
  pagination: Pagination;
}

/** Sort order. Lowercased on the wire. */
export type SortBy = "stars" | "recent";

/** Input to `skillMarketSearch`. Mirrors the desktop-side
 *  `MarketSearchInput` struct, which is `#[serde(rename_all = "camelCase")]`,
 *  so `sortBy` is camelCase on the wire. */
export interface MarketSearchInput {
  q?: string;
  page?: number;
  limit?: number;
  sortBy?: SortBy;
  category?: string;
  occupation?: string;
}

/** Coarse category of a static-scan finding. */
export type RiskCategory =
  | "shell"
  | "execution"
  | "network"
  | "credential"
  | "filesystem"
  | "exfiltration";

/** Severity tier of a static-scan finding. */
export type RiskSeverity = "safe" | "warning" | "danger";

/** One file entry (mirrors deepagent-skills::FileInfo).
 *  The Rust field `kind` is `#[serde(rename = "type")]` on the wire. */
export interface FileInfo {
  name: string;
  /** Lower-cased extension including the leading dot (e.g. `.py`), or `""`. */
  type: string;
  size: number;
}

/** One scan finding (mirrors deepagent-skills::RiskItem). */
export interface RiskItem {
  category: RiskCategory;
  severity: RiskSeverity;
  /** Forward-slashed path relative to the skill root. */
  file: string;
  /** 1-based line number; `null` for file-level rules. */
  line: number | null;
  detail: string;
}

/** Static-scan summary (mirrors deepagent-skills::ScanReport).
 *  All field names are snake_case to match the Rust struct (no rename). */
export interface ScanReport {
  name: string;
  skill_md_content: string;
  files: FileInfo[];
  risks: RiskItem[];
}

/** Result of `skill_market_scan` (mirrors the desktop-side `ScanResult`).
 *  Snake_case on the wire — no crate-wide rename. */
export interface ScanResult {
  temp_id: string;
  report: ScanReport;
}

/** AI security review verdict (mirrors deepagent-app-core::AiReviewResult).
 *  Snake_case on the wire. */
export interface AiReviewResult {
  passed: boolean;
  /** Full LLM output (analysis + verdict line). */
  raw_text: string;
  /** Captured one-line reason after `FAIL:`; `null` on PASS / parse error. */
  failure_reason: string | null;
}

/** Streaming-token event payload (Tauri event `skill-ai-review`). */
export interface SkillAiReviewToken {
  temp_id: string;
  token: string;
}

/** Final review event payload (Tauri event `skill-ai-review-done`).
 *  Exactly one of `result` / `error` is non-null. */
export interface SkillAiReviewDone {
  temp_id: string;
  result: AiReviewResult | null;
  error: string | null;
}

/** Provenance of the SkillsMP API key currently in use.
 *  Lowercased on the wire (mirrors deepagent-skills::ApiKeySource). */
export type SkillsMpKeySource = "user" | "builtin" | "none";

/** Result of `skill_market_get_api_key` (mirrors the desktop-side `ApiKeyInfo`).
 *  Never carries the key value itself. Snake_case on the wire. */
export interface ApiKeyInfo {
  has_user_key: boolean;
  source: SkillsMpKeySource;
}

/** Result of `skill_market_test_key` (mirrors the desktop-side `TestKeyResult`).
 *  Snake_case on the wire. */
export interface TestKeyResult {
  ok: boolean;
  /** `X-RateLimit-Daily-Remaining` from the most recent response, or `null`. */
  daily_remaining: number | null;
  /** Error message when `ok === false`; `null` on success. */
  error: string | null;
}

/** Tool-call result shape disclosed by the `skill` tool (channel B).
 *  Mirrors deepagent-skills::SkillToolOutput. `base_dir` is omitted on the
 *  wire when the skill carries no on-disk root. */
export interface SkillToolOutput {
  id: string;
  name: string;
  body: string;
  base_dir?: string;
  resources: string[];
}

// ---- file preview (office-agent) -----------------------------------------

/** Metadata about a file selected for preview (mirrors PreviewMetadataDto). */
export interface PreviewMetadata {
  path: string;
  name: string;
  /** Lowercased extension without the dot, "" if none. */
  ext: string;
  size_bytes: number;
  /** "text" | "image" | "pdf" | "docx" | "xlsx" | "pptx" | "csv" | "unknown". */
  kind: string;
}

/** One xlsx sheet's preview (first N rows) (mirrors SheetPreviewDto). */
export interface SheetPreview {
  name: string;
  rows: string[][];
  truncated: boolean;
}

/** Result of extracting a previewable representation (mirrors PreviewResultDto). */
export interface PreviewResult {
  metadata: PreviewMetadata;
  text: string | null;
  sheets: SheetPreview[] | null;
  truncated: boolean;
  message: string | null;
}

/** Result of a PDF render request (mirrors PdfRenderResultDto). */
export interface PdfRenderResult {
  rendered: boolean;
  pages: string[];
  text: string | null;
  message: string | null;
}

/** One directory entry for the active project's file browser. */
export interface ProjectFileEntry {
  path: string;
  name: string;
  rel_path: string;
  is_dir: boolean;
  size_bytes: number | null;
  ext: string;
}

/** One lazy-loaded directory listing for the active project. */
export interface ProjectFileListResult {
  root_path: string;
  entries: ProjectFileEntry[];
}

/** Recursive fuzzy-search result for @ file/folder mentions. */
export interface ProjectFileSearchResult {
  root_path: string;
  entries: ProjectFileEntry[];
}

export type ComposerMention =
  | {
      kind: "file" | "folder";
      path: string;
      relPath: string;
      name: string;
      isDir: boolean;
    }
  | {
      kind: "goal";
      text: string;
    }
  | {
      kind: "plan_mode";
    };

// ---- recording + transcription (office-agent) -----------------------------

/** A recording session's lifecycle (mirrors RecordingSessionDto). */
export interface RecordingSession {
  id: string;
  /** "idle" | "recording" | "paused" | "transcribing" | "done" | "error". */
  status: string;
  started_at: number;
  duration_ms: number;
  audio_path: string | null;
  transcript_path: string | null;
  error: string | null;
}

/** One transcript segment (mirrors TranscriptSegmentDto). */
export interface TranscriptSegment {
  start_ms: number;
  end_ms: number;
  text: string;
  speaker: string | null;
  confidence: number | null;
}

// ---- managed runtimes (office-agent) --------------------------------------

/** Status of one managed runtime (mirrors RuntimeStatusDto). */
export interface RuntimeStatus {
  id: string;
  name: string;
  version: string;
  capability: string;
  size_bytes: number;
  installed: boolean;
  available_for_platform: boolean;
  checksum_pinned: boolean;
  install_path: string | null;
  install_source?: "active" | "fallback" | "system" | null;
}

export interface RuntimeRoots {
  active_root: string;
  fallback_roots: string[];
}

/** Progress payload for `runtime:progress` events (mirrors RuntimeProgressDto). */
export interface RuntimeProgress {
  id: string;
  downloaded: number;
  total: number | null;
  /** "downloading" | "verifying" | "extracting" | "done" | "error". */
  phase: string;
}

// --- SSH long-lived connection types (Phase SSH) ---

export type SshAuthType = "agent" | "key_file" | "password";
export type SshStatus = "disconnected" | "connecting" | "connected" | "error";

export interface SshConnection {
  id: string;
  name: string;
  host: string;
  port: number;
  username: string;
  auth_type: SshAuthType;
  key_path?: string;
  status: SshStatus;
  last_error?: string;
  latency_ms?: number;
}

export interface SshExecResult {
  exit_code: number;
  stdout: string;
  stderr: string;
  duration_ms: number;
}

export interface SshDirEntry {
  name: string;
  path: string;
  is_dir: boolean;
  is_symlink: boolean;
  size?: number;
  modified_ms?: number;
}

export interface SshDirListing {
  connection_id: string;
  path: string;
  canonical_path: string;
  entries: SshDirEntry[];
}

export interface SshTestResult {
  ok: boolean;
  latency_ms?: number;
  banner?: string;
  error?: string;
}

export interface SshServiceHandle {
  connection_id: string;
  token: string;
  cols: number;
  rows: number;
}

export interface RemoteProbeResult {
  os?: string;
  distro?: string;
  distro_version?: string;
  arch?: string;
  shell?: string;
  user?: string;
  cwd?: string;
  path?: string;
  package_managers: string[];
  commands: Record<string, boolean>;
  runtimes: Record<string, string>;
  probed_at_ms: number;
}

export interface RemotePushFileRequest {
  local_path: string;
  remote_path: string;
  create_parent?: boolean;
  overwrite?: boolean;
  verify_mode?: "none" | "size" | "sha256";
}

export interface RemotePushFileResult {
  ok: boolean;
  remote_path: string;
  bytes: number;
  local_sha256?: string;
  remote_sha256?: string;
  integrity_verified: boolean;
  duration_ms: number;
}

export interface RemoteBundleRequest {
  local_path: string;
  remote_path: string;
  create_parent?: boolean;
  overwrite?: boolean;
  verify_mode?: "none" | "size" | "sha256";
  remove_archive_after_extract?: boolean;
}

export interface RemoteBundleResult {
  ok: boolean;
  remote_path: string;
  remote_archive_path: string;
  remote_manifest_path: string;
  files: number;
  bytes: number;
  local_archive_sha256?: string;
  remote_archive_sha256?: string;
  integrity_verified: boolean;
  extract_verified: boolean;
  duration_ms: number;
}

export interface RemoteRuntimeRequirement {
  name: string;
  version?: string;
}

export interface RemoteRequireRequest {
  commands?: string[];
  runtimes?: RemoteRuntimeRequirement[];
  archives?: string[];
}

export interface RemoteRequireResult {
  package_manager?: string;
  package_managers: string[];
  missing_commands: string[];
  missing_runtimes: string[];
  missing_archive_tools: string[];
  install_commands: string[];
  can_install: boolean;
  probe: RemoteProbeResult;
}

export interface RemoteInstallRequest {
  package_manager?: string;
  commands?: string[];
  runtimes?: RemoteRuntimeRequirement[];
  packages?: string[];
  update_index?: boolean;
}

export interface RemoteInstallResult {
  ok: boolean;
  package_manager?: string;
  commands_run: string[];
  stdout: string;
  stderr: string;
  installed_packages: string[];
  probe?: RemoteProbeResult;
}

// ─── Mobile DevTools DTOs ────────────────────────────────────────────────────

export interface BackendStatusDto {
  platform: string;
  available: boolean;
  toolchain_version: string | null;
  tool_paths: string[];
  diagnostics: string[];
}

export interface DeviceDto {
  id: string;
  name: string;
  platform: string;
  kind: string;
  connection: string;
  state: string;
  os_version: string | null;
  can_screenshot: boolean;
  can_ui_tree: boolean;
  can_input: boolean;
  can_logs: boolean;
  can_install: boolean;
}

export interface UiSnapshotSummaryDto {
  snapshot_id: string;
  device_id: string;
  node_count: number;
  max_depth: number;
  root_node_id: string;
  captured_at_ms: number;
}

export interface Bounds {
  x: number;
  y: number;
  width: number;
  height: number;
}

export type UiRole =
  | "page"
  | "button"
  | "text"
  | "text_box"
  | "password"
  | "image"
  | "list"
  | "list_item"
  | "checkbox"
  | "switch"
  | "dialog"
  | "web_view"
  | "unknown";

export type UiNodeSource = "android_ui_automator" | "ios_xctest" | "app_sdk";

export interface UiNode {
  node_id: string;
  parent_id: string | null;
  role: UiRole;
  text: string | null;
  label: string | null;
  accessibility_id: string | null;
  bounds: Bounds;
  visible: boolean;
  enabled: boolean;
  clickable: boolean;
  editable: boolean;
  children: string[];
  source: UiNodeSource;
}

export interface UiSnapshot {
  snapshot_id: string;
  device_id: string;
  root_node_id: string;
  nodes: UiNode[];
  captured_at_ms: number;
}

export interface ArtifactRefDto {
  artifact_id: string;
  mime: string;
  size_bytes: number;
  sha256?: string;
  storage_path: string;
}

export interface AvdInfo {
  name: string;
  path?: string;
  target?: string;
  running: boolean;
  serial?: string;
}

export interface NetworkRequest {
  method: string;
  url: string;
  headers: Record<string, string>;
  body: string | null;
  content_type: string | null;
}

export interface NetworkResponse {
  status_code: number;
  status_text: string;
  headers: Record<string, string>;
  body: string | null;
  content_type: string | null;
}

export interface NetworkRecord {
  record_id: string;
  device_id: string;
  package: string | null;
  request: NetworkRequest;
  response: NetworkResponse | null;
  timestamp_ms: number;
  duration_ms: number | null;
}

export type InputAction =
  | { tap: { x: number; y: number } }
  | { long_press: { x: number; y: number; duration_ms: number } }
  | { swipe: { x1: number; y1: number; x2: number; y2: number; duration_ms: number } }
  | { input_text: { text: string } }
  | { press_back: Record<string, never> };

export interface InputResult {
  accepted: boolean;
}

export interface LogRecord {
  timestamp_ms: number;
  level: string;
  tag?: string;
  message: string;
}

export interface LogPage {
  device_id: string;
  records: LogRecord[];
  truncated: boolean;
}

// DAG Execution types for plan_execute tool visualization
export type { DagNodeStatus, DagNode, DagExecution, DagStatusUpdate } from './types/dag';


/* ===== styles.css · SHA256 525105efc9ea809969db6d537ef33ada4a8c892b6bbf0be2c9ccdbc014e22c6a ===== */
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --ui-interactive-radius: 12px;
  --app-rail-width: 56px;
  --app-titlebar-height: 56px;
  --app-nav-button-size: 36px;
  --app-nav-icon-size: 20px;
  --app-nav-gap: 10px;
}

/* One policy for Tailwind, authored CSS, portal surfaces and canvas controls. */
@continuous-corners;

/* Match the interactive face and the sliding hover background. Keep explicit
   circles, capsules, square edges and larger surface radii as authored. */
:where(
  button, [role="button"], [role="tab"], [role="option"],
  [role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"],
  [data-menu-item], .ui-hover-highlight, .cursor-pointer
):is(.rounded, .rounded-sm, .rounded-md, .rounded-lg, .rounded-xl):not(.rounded-full, .rounded-none) {
  border-radius: var(--ui-interactive-radius, 12px);
}

.onboarding-primary {
  corner-shape: superellipse(var(--connect-corner-curvature, var(--continuous-corner-curvature)));
}
.morph-menu-shell {
  corner-shape: superellipse(var(--morph-corner-curvature, var(--continuous-corner-curvature)));
}
[data-spring-shell] {
  corner-shape: superellipse(var(--spring-corner-curvature, var(--continuous-corner-curvature)));
}

@layer base {
  html,
  body,
  #root {
    width: 100%;
    height: 100%;
    margin: 0;
    overflow: hidden;
  }

  body {
    background-color: var(--theme-bg, #ffffff);
    color: var(--theme-fg, #1F2937);
  }

  html {
    overflow-x: hidden;
  }
}

/* The context sidebar and work surface share one frame beside the feature rail. */
.app-workspace {
  position: relative;
  display: flex;
  flex: 1;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  padding: 8px 8px 8px 0;
}
.app-titlebar-spacer { height: var(--app-titlebar-height); }
.app-titlebar-navigation {
  display: flex;
  align-items: center;
  gap: var(--app-nav-gap);
  height: 100%;
  padding-left: calc((var(--app-rail-width) - var(--app-nav-button-size)) / 2);
}
.app-titlebar-navigation .app-titlebar-button {
  width: var(--app-nav-button-size);
  height: var(--app-nav-button-size);
  padding: 0;
  border-radius: 10px;
}
.app-titlebar-button > svg,
.app-navigation-rail button > svg {
  width: var(--app-nav-icon-size);
  height: var(--app-nav-icon-size);
  flex-shrink: 0;
}
.app-navigation-rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  flex-shrink: 0;
  width: var(--app-rail-width);
  padding: 12px 4px 8px;
  gap: var(--app-nav-gap);
}
.app-navigation-rail .app-navigation-home {
  width: var(--app-nav-button-size);
  height: var(--app-nav-button-size);
  padding: 0;
  border-radius: 10px;
  color: var(--theme-text-secondary);
  background: transparent;
}
.app-navigation-rail .app-navigation-home:hover,
.app-navigation-rail .app-navigation-home[aria-current="page"] { color: var(--theme-fg); background: var(--theme-hover); }
.app-navigation-items, .app-navigation-bottom {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--app-nav-gap);
  width: var(--app-nav-button-size);
}
.app-navigation-bottom { margin-top: auto; }
.app-navigation-rail .app-navigation-button {
  width: var(--app-nav-button-size);
  height: var(--app-nav-button-size);
  padding: 0;
  border-radius: 10px;
  color: var(--theme-text-secondary);
  background: transparent;
}
.app-navigation-button[aria-current="page"] { color: var(--theme-fg); }
.app-navigation-rail .app-navigation-button:hover { background: var(--theme-hover); color: var(--theme-fg); }
.app-navigation-rail .app-navigation-items .app-navigation-button:hover { background: transparent; }
.app-navigation-rail button:focus-visible, .app-conversation-sidebar button:focus-visible {
  outline: 2px solid var(--theme-accent);
  outline-offset: 2px;
}
.app-content-frame {
  position: relative;
  display: flex;
  flex: 1;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  border: 1px solid var(--theme-border);
  border-radius: 24px;
}
.app-sidebar-region { display: flex; min-height: 0; flex-shrink: 0; }
.app-workspace[data-sidebar-open="true"] .app-sidebar-region { border-right: 1px solid var(--theme-border); }
.app-conversation-sidebar {
  display: flex;
  flex-direction: column;
  width: 256px;
  height: 100%;
  min-height: 0;
  flex-shrink: 0;
  padding-top: 12px;
  background: transparent;
  font-size: 14px;
  font-weight: 400;
}
.app-conversation-sidebar-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 44px;
  padding: 0 10px 10px 18px;
  margin-bottom: 8px;
}
.app-conversation-sidebar-header strong { font-size: 18px; font-weight: 600; line-height: 26px; letter-spacing: -.025em; }
.sidebar-section-heading { min-height: 28px; font-size: 13px; font-weight: 500; line-height: 20px; color: var(--theme-text-secondary); }
.sidebar-project-row { min-height: 34px; border-radius: 8px; font-size: 14px; font-weight: 500; line-height: 20px; }
.sidebar-project-row:hover .sidebar-project-title,
.sidebar-project-row:focus-within .sidebar-project-title,
.sidebar-project-row[data-actions-visible="true"] .sidebar-project-title { padding-right: 64px; }
.sidebar-session-row { border-radius: 8px; }
.sidebar-session-trigger { font-size: 14px; line-height: 20px; font-weight: 400; }
.sidebar-session-row:hover .sidebar-session-content,
.sidebar-session-row:focus-within .sidebar-session-content,
.sidebar-session-row[data-running="true"] .sidebar-session-content { padding-right: 44px; }
.app-content-frame > .app-main-surface {
  min-width: 0;
  min-height: 0;
  border: 0;
  border-radius: 0;
}
/* The selected variant has no rule underneath the conversation title. */
.app-chat-header { border-bottom: 0; box-shadow: none; }
.app-sidebar-backdrop { display: none; }
@media (max-width: 740px) {
  :root { --app-rail-width: 48px; --app-titlebar-height: 48px; }
  .app-workspace { padding-right: 6px; }
  .app-navigation-rail { padding-top: 10px; }
  .app-sidebar-region { position: absolute; inset: 0 auto 0 0; z-index: 80; }
  .app-workspace[data-sidebar-open="true"] .app-sidebar-region { border-right: 0; }
  .app-sidebar-region > [data-spring-disclosure] { max-width: calc(100vw - 72px); background: var(--theme-sidebar); box-shadow: 8px 0 30px #0002; }
  .app-workspace .app-sidebar-backdrop { display: block; position: absolute; inset: 0; width: auto; height: auto; padding: 0; border: 0; border-radius: 0; background: #0006; z-index: 70; }
  .app-content-frame { border-radius: 22px; }
}
.composer-suggest-scroll { scrollbar-gutter: stable; }
@media (max-width: 640px) {
  .composer-slash-row { grid-template-columns: 88px minmax(0, 1fr); }
  .composer-slash-category { display: none; }
}
@media (max-width: 950px) {
  .app-workspace .composer-toolbar { height: auto; min-height: 32px; flex-wrap: wrap; gap: 6px; }
  .app-workspace .composer-primary-tools { flex: 1 0 auto; }
  .app-workspace .composer-model-tools { margin-left: auto; }
}

/* Hide scrollbar on creative library sidebar */
.creative-sidebar-scroll::-webkit-scrollbar {
  display: none;
}
.creative-sidebar-scroll {
  -ms-overflow-style: none;
  scrollbar-width: none;
}

.node-picker-scroll {
  -ms-overflow-style: none;
  scrollbar-width: none;
}
.node-picker-scroll::-webkit-scrollbar {
  display: none;
  width: 0;
  height: 0;
}

.theme-switch-reveal-overlay {
  position: fixed;
  inset: 0;
  z-index: 2147483000;
  pointer-events: none;
  contain: strict;
  will-change: mask-image, -webkit-mask-image;
}

html.theme-color-transition body,
html.theme-color-transition #root,
html.theme-color-transition .app-main-surface,
html.theme-color-transition .bg-white,
html.theme-color-transition .bg-gray-50,
html.theme-color-transition .bg-gray-100,
html.theme-color-transition .bg-sidebar-bg,
html.theme-color-transition .border-border-theme,
html.theme-color-transition .text-text-base,
html.theme-color-transition .text-text-secondary {
  transition-property: background-color, border-color, color, fill, stroke;
  transition-duration: 180ms;
  transition-timing-function: ease-out;
}

.dark .bg-white { background-color: var(--theme-bg) !important; }
.dark .canvas-field { background-color: rgba(255, 255, 255, 0.06) !important; }
.dark .text-gray-900, .dark .text-gray-800, .dark .text-black { color: var(--theme-fg) !important; }
.dark .bg-gray-50, .dark .bg-gray-100 { background-color: var(--theme-sidebar) !important; }
.dark .hover\:bg-gray-100:hover, .dark .hover\:bg-gray-50:hover { background-color: var(--theme-border) !important; }
.dark .border-gray-200, .dark .border-gray-300 { border-color: var(--theme-border) !important; }
.dark .text-gray-500, .dark .text-gray-600 { color: var(--theme-text-secondary) !important; }

/* Theme-aware silent tints — replace bg-black/N which vanish in dark mode */
@layer utilities {
  .bg-ui-tint {
    background-color: color-mix(in srgb, var(--theme-fg) 5%, transparent);
  }
  .bg-ui-tint-strong {
    background-color: color-mix(in srgb, var(--theme-fg) 10%, transparent);
  }
  .bg-ui-tint-medium {
    background-color: color-mix(in srgb, var(--theme-fg) 20%, transparent);
  }
  .hover\:bg-ui-tint:hover {
    background-color: color-mix(in srgb, var(--theme-fg) 5%, transparent);
  }
  .hover\:bg-ui-tint-strong:hover {
    background-color: color-mix(in srgb, var(--theme-fg) 10%, transparent);
  }

  /* WebView 原生 button 会盖掉 Tailwind 圆角，hover 底变方 —— 强制正圆 */
  .ui-circle-icon-btn {
    appearance: none;
    -webkit-appearance: none;
    border: none;
    border-radius: 9999px;
    overflow: hidden;
    background-color: transparent;
  }

  /* Sidebar highlight — 与 Composer chip 同语言（中性 fg 5%）；暗色略加强 */
  .bg-sidebar-highlight {
    background-color: color-mix(in srgb, var(--theme-fg) 5%, transparent);
  }
  html.dark .bg-sidebar-highlight {
    background-color: color-mix(in srgb, var(--theme-fg) 10%, transparent);
  }
  .hover\:bg-sidebar-highlight:hover {
    background-color: color-mix(in srgb, var(--theme-fg) 5%, transparent);
  }
  html.dark .hover\:bg-sidebar-highlight:hover {
    background-color: color-mix(in srgb, var(--theme-fg) 10%, transparent);
  }
}

/* Composer send — fg/bg invert per theme (ref: light=dark btn, dark=light btn) */
.composer-send-btn {
  flex-shrink: 0;
  display: inline-flex;
  height: 2rem;
  width: 2rem;
  align-items: center;
  justify-content: center;
  border-radius: 9999px;
  transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
    color 150ms cubic-bezier(0.4, 0, 0.2, 1);
}
.composer-send-btn--idle {
  cursor: not-allowed;
  background-color: color-mix(in srgb, var(--theme-fg) 22%, transparent);
  color: color-mix(in srgb, var(--theme-bg) 90%, var(--theme-fg));
}
.composer-send-btn--ready,
.composer-send-btn--stop {
  cursor: pointer;
  background-color: var(--theme-fg);
  color: var(--theme-bg);
}
.composer-send-btn--ready:hover,
.composer-send-btn--stop:hover {
  background-color: color-mix(in srgb, var(--theme-fg) 88%, var(--theme-bg));
}
.composer-send-btn--plan {
  cursor: pointer;
  background-color: #d97706;
  color: #fff;
}
.composer-send-btn--plan:hover {
  background-color: #b45309;
}

/* 联系开发者 · 复制状态 */
.contact-copy-field.is-copied {
  background: color-mix(in srgb, #22c55e 14%, transparent);
}
.contact-copy-field.is-copied .block {
  color: color-mix(in srgb, #15803d 88%, var(--theme-fg));
}
html.dark .contact-copy-field.is-copied .block {
  color: #86efac;
}

/* Codex-style polish: slim scrollbars, no resize handles, focus rings off. */
::-webkit-scrollbar {
  width: 4px;
  height: 4px;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background: #e5e7eb;
  border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
  background: #d1d5db;
}

textarea {
  resize: none;
}
textarea:focus,
input:focus,
button:focus {
  outline: none;
}

/*
 * Translucent sidebar: when enabled on <html>, the window uses Tauri's Mica
 * effect and sidebar surfaces render at reduced opacity so the OS material
 * shows through. Body/root are transparent only in that mode; the main
 * content column keeps its own opaque background (bg-white / bg-bg-base),
 * so only the sidebar band actually reveals Mica.
 */
html.theme-translucent-sidebar,
html.theme-translucent-sidebar body,
html.theme-translucent-sidebar #root {
  background-color: transparent !important;
}

html.theme-translucent-sidebar .bg-sidebar-bg {
  background-color: color-mix(in srgb, var(--theme-sidebar) 55%, transparent) !important;
  backdrop-filter: saturate(180%) blur(18px);
  -webkit-backdrop-filter: saturate(180%) blur(18px);
}
html.theme-translucent-sidebar.dark .bg-sidebar-bg {
  background-color: color-mix(in srgb, var(--theme-sidebar) 65%, transparent) !important;
}

/*
 * The composer renders rich inline context chips in a mirror layer above a
 * transparent textarea. Hide the textarea's native selection so it cannot leak
 * private marker characters; Composer.tsx paints the visible selection itself.
 */
.composer-textarea {
  caret-color: transparent;
}

.composer-textarea.composer-textarea--empty {
  caret-color: var(--theme-fg, #111827);
}

.composer-textarea::selection {
  background: transparent;
  color: transparent;
}

.composer-textarea::-moz-selection {
  background: transparent;
  color: transparent;
}

.composer-mirror-caret {
  display: inline-block;
  width: 1px;
  height: 1.25em;
  margin: 0 -0.5px;
  vertical-align: -0.2em;
  background: var(--theme-fg, #111827);
  animation: composer-caret-blink 1.05s steps(1) infinite;
}

@keyframes composer-caret-blink {
  0%,
  52% {
    opacity: 1;
  }
  53%,
  100% {
    opacity: 0;
  }
}

.no-select {
  user-select: none;
}

.stable-scrollbar-gutter {
  scrollbar-gutter: stable;
}

.terminal-surface .xterm {
  height: 100%;
  width: 100%;
}

.terminal-surface .xterm-viewport {
  overflow-y: auto !important;
  background: var(--theme-bg, #ffffff) !important;
  scrollbar-width: thin;
  scrollbar-color: rgba(148, 163, 184, 0.7) transparent;
}

.terminal-surface .xterm-viewport::-webkit-scrollbar {
  width: 10px;
}

.terminal-surface .xterm-viewport::-webkit-scrollbar-track {
  background: transparent;
}

.terminal-surface .xterm-viewport::-webkit-scrollbar-thumb {
  background: rgba(148, 163, 184, 0.7);
  border: 3px solid transparent;
  border-radius: 9999px;
  background-clip: content-box;
  min-height: 48px;
}

.terminal-surface .xterm-viewport::-webkit-scrollbar-thumb:hover {
  background: rgba(100, 116, 139, 0.78);
  border: 3px solid transparent;
  background-clip: content-box;
}

.terminal-surface .xterm-screen canvas {
  background: var(--theme-bg, #ffffff) !important;
}

.panel-resize-handle-col {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 10px;
  margin-left: -5px;
  cursor: col-resize;
  z-index: 50;
}

.panel-resize-handle-col::after {
  content: "";
  position: absolute;
  top: 12px;
  bottom: 12px;
  left: 50%;
  width: 1px;
  transform: translateX(-50%);
  border-radius: 9999px;
  background: rgba(148, 163, 184, 0.28);
  transition:
    background-color 160ms ease,
    box-shadow 160ms ease,
    width 160ms ease,
    opacity 160ms ease;
}

.panel-resize-handle-col:hover::after {
  background: rgba(100, 116, 139, 0.42);
}

.panel-resize-handle-col.is-active::after {
  width: 1px;
  background: rgba(100, 116, 139, 0.42);
  box-shadow: none;
}

.panel-resize-handle-row {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 10px;
  margin-top: -5px;
  cursor: row-resize;
  z-index: 50;
}

.panel-resize-handle-row::after {
  content: "";
  position: absolute;
  left: 12px;
  right: 12px;
  top: 50%;
  height: 1px;
  transform: translateY(-50%);
  border-radius: 9999px;
  background: rgba(148, 163, 184, 0.24);
  transition:
    background-color 160ms ease,
    box-shadow 160ms ease,
    height 160ms ease,
    opacity 160ms ease;
}

.panel-resize-handle-row:hover::after {
  background: rgba(100, 116, 139, 0.36);
}

.panel-resize-handle-row.is-active::after {
  height: 1px;
  background: rgba(100, 116, 139, 0.36);
  box-shadow: none;
}

.view-frame {
  display: flex;
  width: 100%;
  height: 100%;
  min-width: 0;
  flex-direction: column;
  animation: view-frame-in 220ms cubic-bezier(0.16, 1, 0.3, 1);
  will-change: opacity, transform;
}

.app-main-surface {
  contain: layout paint;
  view-transition-name: app-main-surface;
}

.popover-menu,
.floating-menu-panel {
  animation: floating-menu-panel-in 300ms cubic-bezier(0.4, 0, 0.2, 1);
  will-change: opacity, transform;
}

/* 入场动画只配 open 态：关闭时必须显式清掉 animation，否则 Radix Presence
   检测到 animationName 非 none 会一直等 animationend（已播完不再触发），
   面板永不卸载并持续占据焦点（行内输入框 autoFocus 被吞的根因）。 */
.popover-menu[data-state="closed"],
.floating-menu-panel[data-state="closed"] {
  animation: none;
  will-change: auto;
}

.floating-menu-item {
  animation: floating-menu-item-in 220ms cubic-bezier(0.4, 0, 0.2, 1) 60ms both;
}

/* These surfaces use GSAP; do not run a second CSS entrance on their children. */
.spring-surface,
.spring-surface .floating-menu-panel,
.spring-surface .floating-menu-item {
  animation: none !important;
  transition: none;
}
.spring-surface-content { min-width: 0; }
.spring-settings-section { animation: none !important; }
.spring-surface[data-state="closed"],
.spring-surface[data-closed] { pointer-events: none; }

.morph-menu-shell .floating-menu-panel,
.morph-menu-shell .floating-menu-item {
  animation: none !important;
}
.morph-menu-expanded .morph-menu-content { border-radius: inherit; }

@keyframes floating-menu-panel-in {
  from {
    opacity: 0;
    transform: translateY(8px) scale(0.96);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes floating-menu-item-in {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* MorphingSelect — goo morph + stagger items + label roll */
.morph-select-item-in {
  animation: morph-select-item-in 360ms cubic-bezier(0.19, 1, 0.22, 1) both;
}

@keyframes morph-select-item-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.morph-select-label {
  display: inline-block;
  animation: morph-select-label-roll 320ms cubic-bezier(0.19, 1, 0.22, 1) both;
}

@keyframes morph-select-label-roll {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .morph-select-item-in,
  .morph-select-label {
    animation: none !important;
  }
}

.message-notice {
  animation: message-notice-in 160ms ease-out;
  will-change: opacity, transform;
}

.right-sidebar-workbench {
  display: flex;
  flex-shrink: 0;
  transition: width 400ms cubic-bezier(0.22, 1, 0.36, 1);
  will-change: width;
}

.right-sidebar-workbench > div {
  transition: opacity 320ms ease 60ms;
  will-change: opacity;
}

.right-sidebar-workbench .panel-resize-handle-col::after {
  top: 0;
  bottom: 0;
  width: 1px;
  border-radius: 0;
  background: rgba(15, 23, 42, 0.08);
}

.right-sidebar-workbench .panel-resize-handle-col:hover::after,
.right-sidebar-workbench .panel-resize-handle-col.is-active::after {
  width: 1px;
  background: rgba(15, 23, 42, 0.14);
}

.right-sidebar-workbench.is-maximized {
  position: absolute;
  inset: 0;
  z-index: 40;
  width: 100% !important;
  animation: none;
  transition: opacity 180ms ease;
  will-change: opacity;
}

.right-sidebar-workbench.is-maximized > div {
  transform: none;
  transition: opacity 180ms ease;
  will-change: opacity;
}

.right-sidebar-workbench.is-resizing,
.right-sidebar-workbench.is-resizing > div {
  transition: none;
  cursor: col-resize;
}

.right-sidebar-workbench.is-resizing * {
  cursor: col-resize;
  user-select: none;
}

.right-sidebar-workbench .panel-resize-handle-col {
  width: 12px;
  margin-left: -6px;
}

.right-sidebar-workbench.is-opening > div,
.right-sidebar-workbench:not(.is-closing):not(.is-resizing) > div {
  opacity: 1;
}

.right-sidebar-workbench.is-closing {
  opacity: 1;
}

.right-sidebar-workbench.is-closing > div {
  opacity: 0;
  transition: opacity 220ms ease;
}

.right-sidebar-workbench[data-spring-panel],
.right-sidebar-workbench[data-spring-panel] > div {
  transition: none;
}

.bottom-panel-workbench {
  transition:
    height 400ms cubic-bezier(0.22, 1, 0.36, 1),
    min-height 400ms cubic-bezier(0.22, 1, 0.36, 1);
  will-change: height;
}

.bottom-panel-workbench.is-resizing {
  transition: none;
}

.bottom-panel-workbench.is-resizing .bottom-panel-workbench-inner {
  transition: none;
}

.bottom-panel-workbench .bottom-panel-workbench-inner {
  transition: opacity 320ms ease 60ms;
  will-change: opacity;
}

.bottom-panel-workbench.is-opening .bottom-panel-workbench-inner,
.bottom-panel-workbench:not(.is-closing):not(.is-resizing) .bottom-panel-workbench-inner {
  opacity: 1;
}

.bottom-panel-workbench.is-closing .bottom-panel-workbench-inner {
  opacity: 0;
  transition: opacity 220ms ease;
}

.project-map-view {
  animation: view-frame-in 120ms ease-out;
}

.settings-section {
  animation: view-frame-in 180ms cubic-bezier(0.16, 1, 0.3, 1);
  will-change: opacity, transform;
}

.approval-card {
  animation: approval-card-in 140ms ease-out;
}

.modal-layer {
  animation: modal-layer-in 120ms ease-out;
}

.modal-panel {
  animation: modal-panel-in 140ms ease-out;
  will-change: opacity, transform;
}

.onboarding-door-line {
  height: 100%;
  opacity: 0;
}

.onboarding-transition-dot {
  opacity: 0;
  transform: translate(-50%, -50%);
  transform-origin: center;
}

.onboarding-welcome-text {
  visibility: hidden;
  opacity: 0;
  white-space: nowrap;
  transform-origin: center;
  font-size: clamp(18px, 2.4vw, 30px);
}

.onboarding-card {
  opacity: 1;
  transform: scale(1);
  filter: blur(0);
  width: min(512px, calc(100% - 40px));
  padding: 40px;
  animation: onboarding-card-in 350ms ease-out;
}

.onboarding-panel-header { margin-bottom: 32px; }
.onboarding-emblem {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 64px;
  margin: 0 auto 24px;
  border: 1px solid var(--theme-border);
  border-radius: 16px;
  background: var(--theme-sidebar);
}
.onboarding-emblem > svg { width: 24px; height: 24px; }
.onboarding-brand { width: 80px; height: 80px; border: 0; background: transparent; }
.onboarding-brand img { width: 80px; height: 80px; object-fit: contain; }
.onboarding-actions { display: flex; justify-content: center; gap: 12px; margin-top: 32px; }
.onboarding-actions-connect { margin-top: 24px; }
.onboarding-primary, .onboarding-back { height: 46px; border-radius: 12px; font-size: 14px; }
.onboarding-primary { color: var(--theme-bg); background: var(--theme-fg); }
.onboarding-primary:hover { background: var(--theme-fg); opacity: .9; }
.onboarding-back { padding-inline: 24px; color: var(--theme-text-secondary); background: transparent; }
.onboarding-back:hover { background: var(--theme-sidebar); }
.onboarding-key-field {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 46px;
  padding: 0 13px;
  border: 1px solid var(--theme-border);
  border-radius: 12px;
  transition: border-color 150ms, box-shadow 150ms;
}
.onboarding-key-field:focus-within { border-color: var(--theme-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--theme-accent) 12%, transparent); }
.onboarding-key-field.onboarding-key-invalid { border-color: #c44949; }
.onboarding-inline-error { display: grid; grid-template-rows: 0fr; opacity: 0; color: #c44949; font-size: 11px; line-height: 19px; transition: grid-template-rows 200ms, opacity 200ms; }
.onboarding-inline-error > div { min-height: 0; overflow: hidden; }
.onboarding-error-visible { grid-template-rows: 1fr; opacity: 1; }
.onboarding-error-visible > div { padding-top: 8px; }
.dark .onboarding-inline-error { color: #f18c8c; }
.dark .onboarding-key-field.onboarding-key-invalid { border-color: #f18c8c; }
.onboarding-choices { display: grid; gap: 10px; }
.onboarding-choices-theme { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.onboarding-choices-work { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.onboarding-choice {
  position: relative;
  display: flex;
  align-items: flex-start;
  justify-content: flex-start;
  gap: 12px;
  width: 100%;
  height: auto;
  padding: 16px;
  border: 1px solid var(--theme-border);
  border-radius: 12px;
  background: transparent;
  color: var(--theme-text-secondary);
  text-align: left;
  white-space: normal;
}
.onboarding-choice:hover { background: transparent; color: var(--theme-fg); }
.onboarding-choice[aria-checked=true] { border-color: transparent; color: var(--theme-fg); }
.onboarding-choice > * { position: relative; z-index: 1; }
.onboarding-choice > .onboarding-choice-surface { position: absolute; z-index: 0; inset: -1px; border: 1px solid var(--theme-fg); border-radius: 12px; background: var(--theme-sidebar); pointer-events: none; }
.onboarding-choice-copy { min-width: 0; flex: 1; order: 1; }
.onboarding-choice-title { display: block; font-size: 14px; font-weight: 500; line-height: 20px; }
.onboarding-choice-description { display: block; margin-top: 4px; font-size: 12px; font-weight: 400; line-height: 20px; }
.onboarding-choice-icon { width: 16px; height: 16px; margin-top: 1px; flex-shrink: 0; order: 2; }
.onboarding-choices-theme .onboarding-choice { flex-direction: column; align-items: center; gap: 8px; padding: 24px 8px; text-align: center; }
.onboarding-choices-theme .onboarding-choice-icon { width: 18px; height: 18px; margin: 0; order: 0; }
.onboarding-choices-work .onboarding-choice { display: grid; grid-template-columns: 1fr auto; gap: 11px; padding: 16px; }
.onboarding-choices-work .onboarding-choice-copy { grid-column: 1 / -1; order: 2; }
.onboarding-choices-work .onboarding-choice-icon { order: 1; }
.onboarding-radio { display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin-top: 1px; border: 2px solid var(--theme-border); border-radius: 50%; flex-shrink: 0; transition: border-color 160ms; }
.onboarding-radio::after { content: ""; width: 10px; height: 10px; border-radius: 50%; background: var(--theme-fg); transform: scale(0); transition: transform 180ms; }
.onboarding-choice[aria-checked=true] .onboarding-radio { border-color: var(--theme-fg); }
.onboarding-choice[aria-checked=true] .onboarding-radio::after { transform: scale(1); }
.onboarding-language-menu[data-state=open] { animation: onboarding-menu-in 160ms ease-out; }
.onboarding-language-menu[data-state=closed] { animation: onboarding-menu-out 140ms ease-in; }
.onboarding-language-trigger { color: var(--theme-fg); }
.onboarding-overlay { transition: color 180ms; }
.onboarding-overlay .onboarding-card, .onboarding-overlay .onboarding-door, .onboarding-overlay .onboarding-emblem { transition: background-color 180ms, border-color 180ms, color 180ms; }

@keyframes onboarding-card-in { from { opacity: 0; transform: translateY(10px) scale(.985); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes onboarding-menu-in { from { opacity: 0; transform: translateY(-4px) scale(.98); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes onboarding-menu-out { from { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 0; transform: translateY(-4px) scale(.98); } }
@media (max-width: 560px) {
  .onboarding-card { width: calc(100% - 24px); padding: 24px; }
}
@media (max-height: 690px) {
  .onboarding-card { max-height: calc(100% - 24px); overflow-y: auto; padding-block: 24px; }
  .onboarding-panel-header { margin-bottom: 24px; }
}
@media (prefers-reduced-motion: reduce) {
  .onboarding-card, .onboarding-language-menu { animation: none !important; }
  .onboarding-overlay *, .onboarding-language-menu { transition: none !important; }
}

@keyframes view-frame-in {
  from {
    opacity: 0;
    transform: translateY(8px) scale(0.995);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

::view-transition-old(app-main-surface),
::view-transition-new(app-main-surface) {
  height: 100%;
  overflow: hidden;
  animation-duration: 240ms;
  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
  mix-blend-mode: normal;
}

::view-transition-old(app-main-surface) {
  animation-name: app-main-surface-out;
}

::view-transition-new(app-main-surface) {
  animation-name: app-main-surface-in;
}

@keyframes app-main-surface-out {
  from {
    opacity: 1;
    transform: translateX(0) scale(1);
    filter: blur(0);
  }
  to {
    opacity: 0.42;
    transform: translateX(-10px) scale(0.996);
    filter: blur(0.35px);
  }
}

@keyframes app-main-surface-in {
  from {
    opacity: 0;
    transform: translateX(14px) scale(0.996);
    filter: blur(0.35px);
  }
  to {
    opacity: 1;
    transform: translateX(0) scale(1);
    filter: blur(0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .view-frame,
  .settings-section,
  .project-map-view,
  .right-sidebar-workbench,
  .right-sidebar-workbench > div,
  .bottom-panel-workbench,
  .bottom-panel-workbench .bottom-panel-workbench-inner,
  .modal-layer,
  .modal-panel,
  .popover-menu,
  .floating-menu-panel,
  .floating-menu-item,
  .message-notice,
  .approval-card {
    animation: none !important;
    transition: none !important;
  }

  ::view-transition-old(app-main-surface),
  ::view-transition-new(app-main-surface) {
    animation: none !important;
  }
}

@keyframes popover-menu-in {
  from {
    opacity: 0;
    transform: translateY(5px) scale(0.98);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes message-notice-in {
  from {
    opacity: 0;
    transform: translateY(-10px) scale(0.98);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes right-sidebar-workbench-in {
  from {
    opacity: 0;
    transform: translateX(18px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes approval-card-in {
  from {
    opacity: 0;
    transform: translateY(8px) scale(0.99);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes modal-layer-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes modal-panel-in {
  from {
    opacity: 0;
    transform: translateY(-8px) scale(0.98);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

.studio-infinite-canvas {
  background: var(--theme-bg, #ffffff);
}

.studio-infinite-canvas.is-tool-pan .react-flow__pane {
  cursor: grab;
}

.studio-infinite-canvas.is-tool-pan .react-flow__pane.dragging {
  cursor: grabbing;
}

.studio-infinite-canvas.is-tool-select .react-flow__pane {
  cursor: default;
}

.studio-canvas-toolbar {
  display: flex;
  align-items: center;
  gap: 2px;
  margin: 12px 0 0 12px !important;
  padding: 4px;
  border: 1px solid var(--theme-border, #e5e7eb);
  border-radius: 10px;
  background: color-mix(in srgb, var(--theme-bg, #ffffff) 94%, transparent);
  box-shadow: 0 8px 24px rgba(15, 23, 42, 0.09);
  backdrop-filter: blur(12px);
  transition: box-shadow 160ms ease, background-color 160ms ease;
}

.studio-canvas-toolbar.is-collapsed {
  gap: 0;
}

.studio-canvas-toolbar button {
  display: grid;
  width: 30px;
  height: 30px;
  place-items: center;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--theme-text-secondary, #6b7280);
  font-size: 12px;
  transition: background-color 140ms ease, color 140ms ease, transform 140ms ease;
}

.studio-canvas-toolbar button:hover:not(:disabled) {
  background: var(--theme-sidebar, #f8f7f5);
  color: var(--theme-fg, #1f2937);
}

.studio-canvas-toolbar button:active:not(:disabled) {
  transform: scale(0.94);
}

.studio-canvas-toolbar button.is-active {
  background: color-mix(in srgb, var(--theme-primary, #3b82f6) 12%, transparent);
  color: var(--theme-primary, #2563eb);
}

.studio-canvas-toolbar .studio-toolbar-collapse {
  color: color-mix(in srgb, var(--theme-text-secondary, #6b7280) 82%, transparent);
}

.studio-canvas-toolbar button:disabled {
  opacity: 0.3;
  cursor: default;
}

.studio-toolbar-divider {
  width: 1px;
  height: 18px;
  margin: 0 2px;
  background: var(--theme-border, #e5e7eb);
}

.studio-note-node {
  position: relative;
  display: flex;
  width: 100%;
  height: 100%;
  min-width: 192px;
  min-height: 120px;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--theme-border, #e5e7eb) 88%, transparent);
  border-radius: 10px;
  background: color-mix(in srgb, var(--theme-bg, #ffffff) 97%, #fef3c7 3%);
  box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
  transition: border-color 140ms ease, box-shadow 140ms ease;
}

.studio-note-node:hover {
  border-color: color-mix(in srgb, var(--theme-text-secondary, #6b7280) 40%, transparent);
}

.studio-note-node.is-selected {
  border-color: var(--theme-primary, #3b82f6);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--theme-primary, #3b82f6) 22%, transparent),
    0 12px 32px rgba(15, 23, 42, 0.1);
}

.studio-note-header {
  display: flex;
  width: 100%;
  flex: 0 0 auto;
  align-items: center;
  border-bottom: 1px solid color-mix(in srgb, var(--theme-border, #e5e7eb) 70%, transparent);
}

.studio-note-drag-handle {
  display: grid;
  width: 30px;
  height: 40px;
  flex: 0 0 30px;
  place-items: center;
  color: var(--theme-text-secondary, #6b7280);
  cursor: grab;
  font-size: 12px;
  opacity: 0.42;
  transition: color 140ms ease, opacity 140ms ease;
}

.studio-note-node:hover .studio-note-drag-handle,
.studio-note-node.is-selected .studio-note-drag-handle {
  color: var(--theme-primary, #3b82f6);
  opacity: 0.85;
}

.studio-note-drag-handle:active {
  cursor: grabbing;
}

.studio-note-title {
  width: 100%;
  min-width: 0;
  border: 0;
  background: transparent;
  padding: 12px 14px 10px 2px;
  color: var(--theme-fg, #1f2937);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.2;
}

.studio-note-body {
  width: 100%;
  min-height: 0;
  flex: 1 1 auto;
  border: 0;
  background: transparent;
  padding: 10px 14px 14px;
  color: var(--theme-fg, #1f2937);
  font-family: inherit;
  font-size: 12px;
  line-height: 1.55;
}

.studio-note-title::placeholder,
.studio-note-body::placeholder {
  color: var(--theme-text-secondary, #6b7280);
  opacity: 0.68;
}

.studio-note-handle {
  width: 8px !important;
  height: 8px !important;
  border: 2px solid var(--theme-bg, #ffffff) !important;
  background: var(--theme-primary, #3b82f6) !important;
  opacity: 0;
  transition: opacity 140ms ease, transform 140ms ease;
}

.studio-note-node:hover .studio-note-handle,
.studio-note-node.is-selected .studio-note-handle {
  opacity: 1;
}

.studio-note-handle:hover {
  transform: scale(1.28);
}

.studio-note-resize-line {
  border-color: transparent !important;
}

.studio-note-resize-handle {
  width: 7px !important;
  height: 7px !important;
  border: 1px solid var(--theme-bg, #ffffff) !important;
  border-radius: 2px !important;
  background: var(--theme-primary, #3b82f6) !important;
}

.studio-infinite-canvas .react-flow__selection,
.studio-infinite-canvas .react-flow__nodesselection-rect {
  border: 1px solid color-mix(in srgb, var(--theme-primary, #3b82f6) 70%, transparent);
  background: color-mix(in srgb, var(--theme-primary, #3b82f6) 7%, transparent);
}

.studio-infinite-canvas .react-flow__edge-path {
  stroke: color-mix(in srgb, var(--theme-text-secondary, #6b7280) 70%, transparent);
  stroke-width: 1.5;
}

.studio-infinite-canvas .react-flow__edge.selected .react-flow__edge-path {
  stroke: var(--theme-primary, #3b82f6);
  stroke-width: 2;
}

.studio-canvas-toolbar {
  z-index: 40;
}

.studio-node-toolbar {
  display: flex;
  overflow: hidden;
  border: 1px solid var(--theme-border, #e5e7eb);
  border-radius: 8px;
  background: color-mix(in srgb, var(--theme-bg, #ffffff) 96%, transparent);
  box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
  backdrop-filter: blur(12px);
}

.studio-node-toolbar button {
  border: 0;
  border-right: 1px solid var(--theme-border, #e5e7eb);
  background: transparent;
  padding: 6px 9px;
  color: var(--theme-text-secondary, #6b7280);
  font-size: 10px;
  line-height: 1;
}

.studio-node-toolbar button:last-child {
  border-right: 0;
}

.studio-node-toolbar button:hover {
  background: var(--theme-sidebar, #f8f7f5);
  color: var(--theme-fg, #1f2937);
}

.studio-note-node.is-locked,
.studio-text-node.is-locked,
.studio-shape-node.is-locked,
.studio-drawing-node.is-locked,
.studio-group-node.is-locked,
.studio-miniapp-node.is-locked {
  cursor: not-allowed;
}

.studio-note-node.is-locked::after,
.studio-text-node.is-locked::after,
.studio-shape-node.is-locked::after,
.studio-drawing-node.is-locked::after,
.studio-group-node.is-locked::after,
.studio-miniapp-node.is-locked::after {
  content: "已锁定";
  position: absolute;
  z-index: 3;
  top: 6px;
  right: 7px;
  border-radius: 9999px;
  background: color-mix(in srgb, var(--theme-bg, #ffffff) 90%, transparent);
  padding: 3px 6px;
  color: var(--theme-text-secondary, #6b7280);
  font-size: 8px;
  font-weight: 600;
  line-height: 1;
  pointer-events: none;
}

.studio-text-node {
  position: relative;
  display: flex;
  width: 100%;
  height: 100%;
  min-width: 120px;
  min-height: 48px;
  align-items: flex-start;
  border: 1px solid transparent;
  border-radius: 8px;
  background: transparent;
}

.studio-text-node:hover,
.studio-text-node.is-selected {
  border-color: color-mix(in srgb, var(--theme-primary, #3b82f6) 55%, transparent);
  background: color-mix(in srgb, var(--theme-bg, #ffffff) 82%, transparent);
}

.studio-text-node .studio-note-drag-handle {
  height: 34px;
  flex-basis: 26px;
  width: 26px;
}

.studio-text-editor {
  width: 100%;
  height: 100%;
  min-height: 48px;
  border: 0;
  background: transparent;
  padding: 10px 12px 10px 0;
  color: var(--theme-fg, #1f2937);
  font-family: inherit;
  font-size: 15px;
  line-height: 1.5;
}

.studio-shape-node {
  position: relative;
  display: grid;
  width: 100%;
  height: 100%;
  min-width: 48px;
  min-height: 48px;
  place-items: center;
  border: 1.5px solid color-mix(in srgb, var(--theme-primary, #3b82f6) 58%, var(--theme-border, #e5e7eb));
  border-radius: 8px;
  background: color-mix(in srgb, var(--theme-primary, #3b82f6) 7%, var(--theme-bg, #ffffff));
  color: var(--theme-text-secondary, #6b7280);
}

.studio-shape-node.is-ellipse {
  border-radius: 9999px;
}

.studio-shape-node.is-selected,
.studio-drawing-node.is-selected,
.studio-group-node.is-selected {
  border-color: var(--theme-primary, #3b82f6);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--theme-primary, #3b82f6) 22%, transparent);
}

.studio-shape-drag-label {
  padding: 12px;
  font-size: 12px;
  pointer-events: none;
}

.studio-drawing-node {
  position: relative;
  width: 100%;
  height: 100%;
  min-width: 24px;
  min-height: 24px;
  border: 1px solid transparent;
}

.studio-drawing-node svg {
  width: 100%;
  height: 100%;
  overflow: visible;
}

.studio-drawing-node polyline,
.studio-whiteboard-overlay polyline {
  fill: none;
  stroke: var(--theme-fg, #1f2937);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.studio-group-node {
  position: relative;
  width: 100%;
  height: 100%;
  min-width: 160px;
  min-height: 120px;
  border: 1px dashed color-mix(in srgb, var(--theme-text-secondary, #6b7280) 45%, transparent);
  border-radius: 12px;
  background: color-mix(in srgb, var(--theme-sidebar, #f8f7f5) 30%, transparent);
}

.studio-group-title {
  display: flex;
  height: 34px;
  align-items: center;
  gap: 7px;
  padding: 0 11px;
  color: var(--theme-text-secondary, #6b7280);
  cursor: grab;
  font-size: 11px;
  font-weight: 600;
}

.studio-miniapp-node {
  position: relative;
  display: flex;
  width: 100%;
  height: 100%;
  min-width: 340px;
  min-height: 500px;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--theme-border, #e5e7eb) 92%, transparent);
  border-radius: 14px;
  background: color-mix(in srgb, var(--theme-bg, #ffffff) 98%, #eff6ff 2%);
  box-shadow: 0 16px 44px rgba(15, 23, 42, 0.13);
  transition: border-color 150ms ease, box-shadow 150ms ease;
}

.studio-miniapp-node:hover {
  border-color: color-mix(in srgb, var(--theme-text-secondary, #6b7280) 42%, transparent);
}

.studio-miniapp-node.is-selected {
  border-color: var(--theme-primary, #3b82f6);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--theme-primary, #3b82f6) 22%, transparent),
    0 18px 50px rgba(15, 23, 42, 0.16);
}

.studio-miniapp-header {
  display: flex;
  height: 48px;
  flex: 0 0 48px;
  align-items: center;
  gap: 9px;
  border-bottom: 1px solid color-mix(in srgb, var(--theme-border, #e5e7eb) 76%, transparent);
  padding: 0 8px 0 3px;
  cursor: grab;
  user-select: none;
}

.studio-miniapp-header:active {
  cursor: grabbing;
}

.studio-miniapp-grip {
  display: grid;
  width: 22px;
  flex: 0 0 22px;
  place-items: center;
  color: var(--theme-text-secondary, #6b7280);
  font-size: 11px;
  opacity: 0.45;
}

.studio-miniapp-app-icon {
  display: grid;
  width: 28px;
  height: 28px;
  flex: 0 0 28px;
  place-items: center;
  border-radius: 8px;
  background: color-mix(in srgb, var(--theme-primary, #3b82f6) 12%, transparent);
  color: var(--theme-primary, #2563eb);
  font-size: 12px;
}

.studio-miniapp-heading {
  display: flex;
  min-width: 0;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 1px;
  line-height: 1.15;
}

.studio-miniapp-heading strong {
  overflow: hidden;
  color: var(--theme-fg, #1f2937);
  font-size: 12px;
  font-weight: 700;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.studio-miniapp-heading small {
  overflow: hidden;
  color: var(--theme-text-secondary, #6b7280);
  font-size: 9px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.studio-miniapp-runtime-status {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 5px;
  border-radius: 999px;
  background: color-mix(in srgb, #22c55e 9%, transparent);
  padding: 4px 7px;
  color: #15803d;
  font-size: 9px;
  font-weight: 600;
}

.studio-miniapp-runtime-status i {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: #22c55e;
  box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.12);
}

.studio-miniapp-header-actions {
  display: flex;
  flex: 0 0 auto;
  gap: 2px;
}

.studio-miniapp-header-actions button {
  display: grid;
  width: 25px;
  height: 25px;
  place-items: center;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--theme-text-secondary, #6b7280);
  font-size: 10px;
  transition: background-color 140ms ease, color 140ms ease, transform 140ms ease;
}

.studio-miniapp-header-actions button:hover,
.studio-miniapp-header-actions button.is-active {
  background: color-mix(in srgb, var(--theme-primary, #3b82f6) 10%, transparent);
  color: var(--theme-primary, #2563eb);
}

.studio-miniapp-header-actions button:active {
  transform: scale(0.92);
}

.studio-miniapp-controls {
  display: flex;
  min-height: 42px;
  flex: 0 0 auto;
  align-items: center;
  gap: 8px;
  border-bottom: 1px solid color-mix(in srgb, var(--theme-border, #e5e7eb) 70%, transparent);
  background: color-mix(in srgb, var(--theme-sidebar, #f8f7f5) 64%, transparent);
  padding: 6px 10px;
}

.studio-miniapp-controls label {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: 5px;
}

.studio-miniapp-controls label > span {
  color: var(--theme-text-secondary, #6b7280);
  font-size: 9px;
  font-weight: 600;
}

.studio-miniapp-controls select {
  min-width: 0;
  max-width: 116px;
  height: 27px;
  border: 1px solid var(--theme-border, #e5e7eb);
  border-radius: 6px;
  outline: none;
  background: var(--theme-bg, #ffffff);
  padding: 0 22px 0 7px;
  color: var(--theme-fg, #1f2937);
  font-size: 9px;
}

.studio-miniapp-controls select:focus {
  border-color: color-mix(in srgb, var(--theme-primary, #3b82f6) 65%, var(--theme-border, #e5e7eb));
}

.studio-miniapp-viewport-size {
  margin-left: auto;
  color: var(--theme-text-secondary, #6b7280);
  font-family: "Cascadia Mono", "SFMono-Regular", Consolas, monospace;
  font-size: 8px;
  white-space: nowrap;
}

.studio-miniapp-stage {
  position: relative;
  display: flex;
  min-height: 0;
  flex: 1 1 auto;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background:
    radial-gradient(circle at 50% 18%, rgba(59, 130, 246, 0.07), transparent 38%),
    color-mix(in srgb, var(--theme-sidebar, #f8f7f5) 72%, var(--theme-bg, #ffffff));
  padding: 14px;
  transition: opacity 160ms ease, filter 160ms ease;
}

.studio-miniapp-stage.is-refreshing {
  opacity: 0.55;
  filter: saturate(0.72);
}

.studio-device-shell {
  position: relative;
  display: flex;
  overflow: hidden;
  border: 7px solid #111827;
  border-radius: 27px;
  background: #111827;
  box-shadow: 0 18px 36px rgba(15, 23, 42, 0.22), 0 0 0 1px rgba(255, 255, 255, 0.08) inset;
  transition: width 220ms ease, height 220ms ease, aspect-ratio 220ms ease;
}

.studio-device-shell.is-portrait {
  width: auto;
  height: 100%;
  max-width: 100%;
}

.studio-device-shell.is-landscape {
  width: 100%;
  max-height: 100%;
}

.studio-device-screen {
  position: relative;
  display: flex;
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  flex-direction: column;
  overflow: hidden;
  border-radius: 20px;
  background: #f5f7fb;
  color: #172033;
  container-type: size;
}

.studio-device-statusbar {
  z-index: 3;
  display: flex;
  height: 24px;
  flex: 0 0 24px;
  align-items: center;
  justify-content: space-between;
  padding: 0 13px;
  background: rgba(255, 255, 255, 0.94);
  color: #1f2937;
  font-size: 7px;
  font-weight: 700;
}

.studio-device-statusbar span:last-child {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 8px;
}

.studio-mock-app-page {
  display: flex;
  min-height: 0;
  flex: 1 1 auto;
  flex-direction: column;
  overflow: hidden;
  background: #f5f7fb;
}

.studio-mock-app-page > header {
  display: flex;
  height: 48px;
  flex: 0 0 48px;
  align-items: center;
  justify-content: space-between;
  background: rgba(255, 255, 255, 0.94);
  padding: 0 12px;
}

.studio-mock-app-page > header div {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.studio-mock-app-page > header small {
  color: #94a3b8;
  font-size: 6px;
}

.studio-mock-app-page > header strong {
  display: flex;
  align-items: center;
  gap: 4px;
  color: #182033;
  font-size: 10px;
}

.studio-mock-app-page > header button {
  display: grid;
  width: 28px;
  height: 28px;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: #eff6ff;
  color: #2563eb;
  font-size: 9px;
}

.studio-mock-app-page > main {
  min-height: 0;
  flex: 1 1 auto;
  overflow: auto;
  padding: 9px 10px 16px;
  scrollbar-width: none;
}

.studio-mock-app-page > main::-webkit-scrollbar {
  display: none;
}

.studio-mock-hero {
  position: relative;
  min-height: 80px;
  overflow: hidden;
  border-radius: 14px;
  background: linear-gradient(135deg, #1d4ed8 0%, #3b82f6 58%, #60a5fa 100%);
  padding: 13px 66px 13px 14px;
  color: #ffffff;
  box-shadow: 0 8px 22px rgba(37, 99, 235, 0.22);
}

.studio-mock-hero > svg {
  position: absolute;
  right: 19px;
  bottom: 12px;
  color: rgba(255, 255, 255, 0.9);
  font-size: 34px;
  transform: rotate(-8deg);
}

.studio-mock-hero small {
  font-size: 6px;
  font-weight: 700;
  letter-spacing: 0.08em;
  opacity: 0.76;
}

.studio-mock-hero h3 {
  margin: 5px 0 3px;
  font-size: 15px;
  font-weight: 800;
  line-height: 1.15;
}

.studio-mock-hero p {
  margin: 0;
  font-size: 7px;
  opacity: 0.86;
}

.studio-mock-search {
  display: flex;
  height: 30px;
  align-items: center;
  gap: 7px;
  margin-top: 9px;
  border: 1px solid #e5eaf1;
  border-radius: 9px;
  background: #ffffff;
  padding: 0 10px;
  color: #94a3b8;
  font-size: 7px;
}

.studio-mock-section {
  margin-top: 11px;
}

.studio-mock-section-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 7px;
}

.studio-mock-section-title strong {
  font-size: 10px;
}

.studio-mock-section-title span {
  color: #94a3b8;
  font-size: 6px;
}

.studio-mock-products {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 7px;
}

.studio-mock-products button {
  display: flex;
  min-width: 0;
  flex-direction: column;
  align-items: flex-start;
  border: 1px solid #e8edf4;
  border-radius: 10px;
  background: #ffffff;
  padding: 7px;
  color: #172033;
  text-align: left;
}

.studio-product-art {
  display: grid;
  width: 100%;
  height: 48px;
  place-items: center;
  border-radius: 8px;
  margin-bottom: 6px;
  color: #ffffff;
  font-size: 22px;
}

.studio-product-art.is-coconut {
  background: linear-gradient(145deg, #1e3a5f, #3b82f6);
}

.studio-product-art.is-orange {
  background: linear-gradient(145deg, #c2410c, #fb923c);
}

.studio-mock-products strong {
  font-size: 8px;
}

.studio-mock-products small {
  margin-top: 2px;
  color: #94a3b8;
  font-size: 6px;
}

.studio-mock-products b {
  margin-top: 5px;
  color: #2563eb;
  font-size: 8px;
}

.studio-order-tabs {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border-bottom: 1px solid #e5eaf1;
  margin: -3px 0 9px;
}

.studio-order-tabs button {
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  padding: 8px 2px 6px;
  color: #94a3b8;
  font-size: 7px;
}

.studio-order-tabs button.is-active {
  border-bottom-color: #2563eb;
  color: #2563eb;
  font-weight: 700;
}

.studio-order-card {
  border: 1px solid #e5eaf1;
  border-radius: 11px;
  background: #ffffff;
  padding: 9px;
  box-shadow: 0 5px 15px rgba(15, 23, 42, 0.05);
}

.studio-order-card > div,
.studio-order-card p,
.studio-order-card footer {
  display: flex;
  align-items: center;
}

.studio-order-card > div {
  justify-content: space-between;
  font-size: 8px;
  font-weight: 700;
}

.studio-order-card > div small {
  border-radius: 999px;
  background: #eff6ff;
  padding: 3px 5px;
  color: #2563eb;
  font-size: 6px;
}

.studio-order-card p {
  gap: 7px;
  margin: 9px 0;
}

.studio-order-card p strong {
  flex: 1;
  font-size: 7px;
}

.studio-order-card p b {
  font-size: 8px;
}

.studio-order-thumb {
  display: grid;
  width: 30px;
  height: 30px;
  place-items: center;
  border-radius: 8px;
  background: linear-gradient(145deg, #1d4ed8, #60a5fa);
  color: #ffffff;
}

.studio-order-card footer {
  gap: 5px;
  border-top: 1px solid #eef2f7;
  padding-top: 7px;
  color: #64748b;
  font-size: 6px;
}

.studio-order-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  padding: 28px 10px 8px;
  color: #94a3b8;
  text-align: center;
}

.studio-order-empty > svg {
  margin-bottom: 3px;
  font-size: 20px;
}

.studio-order-empty strong {
  color: #475569;
  font-size: 8px;
}

.studio-order-empty small {
  font-size: 6px;
}

.studio-mock-tabbar {
  z-index: 2;
  display: grid;
  height: 42px;
  flex: 0 0 42px;
  grid-template-columns: repeat(3, 1fr);
  border-top: 1px solid #e5eaf1;
  background: rgba(255, 255, 255, 0.96);
}

.studio-mock-tabbar button {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  border: 0;
  background: transparent;
  color: #94a3b8;
  font-size: 8px;
}

.studio-mock-tabbar button span {
  font-size: 5px;
}

.studio-mock-tabbar button.is-active {
  color: #2563eb;
}

.studio-device-screen [data-mini-node] {
  transition: outline-color 120ms ease, background-color 120ms ease;
}

.studio-device-screen [data-mini-selected="true"] {
  outline: 2px solid #2563eb;
  outline-offset: -2px;
  background-color: rgba(59, 130, 246, 0.08);
}

.studio-inspector-hint {
  position: absolute;
  z-index: 8;
  right: 7px;
  bottom: 48px;
  left: 7px;
  display: flex;
  align-items: center;
  gap: 5px;
  border: 1px solid rgba(59, 130, 246, 0.2);
  border-radius: 7px;
  background: rgba(239, 246, 255, 0.96);
  padding: 6px 8px;
  color: #1d4ed8;
  font-size: 7px;
  font-weight: 600;
  box-shadow: 0 6px 18px rgba(15, 23, 42, 0.12);
  pointer-events: none;
}

.studio-miniapp-footer {
  display: flex;
  height: 27px;
  flex: 0 0 27px;
  align-items: center;
  justify-content: space-between;
  border-top: 1px solid color-mix(in srgb, var(--theme-border, #e5e7eb) 68%, transparent);
  padding: 0 10px;
  color: var(--theme-text-secondary, #6b7280);
  font-size: 8px;
}

@container (max-height: 390px) {
  .studio-mock-hero {
    min-height: 60px;
    padding-top: 9px;
    padding-bottom: 9px;
  }

  .studio-mock-hero h3 {
    font-size: 12px;
  }

  .studio-product-art {
    height: 34px;
  }
}

.studio-search-panel {
  width: 260px;
  margin: 12px 12px 0 0 !important;
  overflow: hidden;
  border: 1px solid var(--theme-border, #e5e7eb);
  border-radius: 10px;
  background: color-mix(in srgb, var(--theme-bg, #ffffff) 96%, transparent);
  box-shadow: 0 12px 34px rgba(15, 23, 42, 0.13);
  backdrop-filter: blur(14px);
}

.studio-search-panel > input {
  width: 100%;
  border: 0;
  border-bottom: 1px solid var(--theme-border, #e5e7eb);
  background: transparent;
  padding: 10px 12px;
  color: var(--theme-fg, #1f2937);
  font-size: 12px;
}

.studio-search-results {
  max-height: 280px;
  overflow: auto;
  padding: 4px;
}

.studio-search-results button {
  display: flex;
  width: 100%;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  padding: 8px;
  color: var(--theme-fg, #1f2937);
  text-align: left;
}

.studio-search-results button:hover {
  background: var(--theme-sidebar, #f8f7f5);
}

.studio-search-results small,
.studio-search-results p {
  margin: 0;
  color: var(--theme-text-secondary, #6b7280);
  font-size: 10px;
}

.studio-more-menu {
  z-index: 45;
  display: grid;
  width: 650px;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 1px;
  margin: 58px 0 0 12px !important;
  overflow: hidden;
  border: 1px solid var(--theme-border, #e5e7eb);
  border-radius: 11px;
  background: var(--theme-border, #e5e7eb);
  box-shadow: 0 14px 38px rgba(15, 23, 42, 0.15);
}

.studio-menu-section {
  display: flex;
  flex-direction: column;
  gap: 1px;
  background: var(--theme-bg, #ffffff);
  padding: 7px;
}

.studio-menu-section > span {
  padding: 5px 7px;
  color: var(--theme-text-secondary, #6b7280);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.studio-menu-section button,
.studio-context-menu button {
  display: flex;
  min-height: 28px;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  padding: 6px 7px;
  color: var(--theme-fg, #1f2937);
  font-size: 10px;
  text-align: left;
}

.studio-menu-section button:hover,
.studio-context-menu button:hover {
  background: var(--theme-sidebar, #f8f7f5);
}

.studio-menu-section button:disabled {
  background: transparent;
  color: var(--theme-text-secondary, #6b7280);
  cursor: not-allowed;
  opacity: 0.38;
}

.studio-menu-section button.is-danger {
  color: #dc2626;
}

.studio-menu-section kbd {
  color: var(--theme-text-secondary, #6b7280);
  font-family: inherit;
  font-size: 8px;
}

.studio-context-menu {
  position: fixed;
  z-index: 100;
  display: flex;
  width: 150px;
  flex-direction: column;
  gap: 1px;
  border: 1px solid var(--theme-border, #e5e7eb);
  border-radius: 9px;
  background: var(--theme-bg, #ffffff);
  padding: 5px;
  box-shadow: 0 12px 34px rgba(15, 23, 42, 0.16);
}

.studio-context-menu button {
  width: 100%;
}

.studio-command-notice {
  position: absolute;
  z-index: 80;
  left: 50%;
  bottom: 42px;
  transform: translateX(-50%);
  border: 1px solid var(--theme-border, #e5e7eb);
  border-radius: 9999px;
  background: color-mix(in srgb, var(--theme-bg, #ffffff) 94%, transparent);
  padding: 7px 12px;
  color: var(--theme-fg, #1f2937);
  box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
  backdrop-filter: blur(12px);
  font-size: 11px;
  line-height: 1;
  pointer-events: none;
  animation: studio-command-notice-in 160ms ease-out;
}

@keyframes studio-command-notice-in {
  from { opacity: 0; transform: translate(-50%, 5px); }
  to { opacity: 1; transform: translate(-50%, 0); }
}

.studio-canvas-minimap {
  right: 54px !important;
  bottom: 12px !important;
  overflow: hidden;
  border: 1px solid var(--theme-border, #e5e7eb);
  border-radius: 10px;
  background: color-mix(in srgb, var(--theme-bg, #ffffff) 94%, transparent) !important;
  box-shadow: 0 8px 24px rgba(15, 23, 42, 0.1);
}

.studio-whiteboard-overlay {
  position: absolute;
  z-index: 20;
  inset: 0;
  cursor: crosshair;
  touch-action: none;
}

.studio-whiteboard-overlay > svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.studio-shape-draft {
  position: absolute;
  border: 1.5px solid var(--theme-primary, #3b82f6);
  border-radius: 8px;
  background: color-mix(in srgb, var(--theme-primary, #3b82f6) 8%, transparent);
  pointer-events: none;
}

.studio-lasso-draft {
  fill: color-mix(in srgb, var(--theme-primary, #3b82f6) 8%, transparent);
  stroke: var(--theme-primary, #3b82f6);
  stroke-width: 1.5;
  stroke-dasharray: 6 4;
}

.studio-whiteboard-overlay .studio-eraser-draft {
  stroke: rgba(239, 68, 68, 0.28);
  stroke-width: 14;
}

@media (max-width: 820px) {
  .studio-more-menu {
    width: min(650px, calc(100vw - 24px));
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.studio-infinite-canvas .react-flow__controls {
  overflow: hidden;
  border: 1px solid var(--theme-border, #e5e7eb);
  border-radius: 10px;
  background: color-mix(in srgb, var(--theme-bg, #ffffff) 92%, transparent);
  box-shadow: 0 10px 30px rgba(15, 23, 42, 0.1);
  backdrop-filter: blur(12px);
}

.studio-infinite-canvas .react-flow__controls-button {
  width: 30px;
  height: 30px;
  border: 0;
  border-bottom: 1px solid var(--theme-border, #e5e7eb);
  background: transparent;
  color: var(--theme-text-secondary, #6b7280);
  transition: background-color 150ms ease, color 150ms ease;
}

.studio-infinite-canvas .react-flow__controls-button:last-child {
  border-bottom: 0;
}

.studio-infinite-canvas .react-flow__controls-button:hover {
  background: var(--theme-sidebar, #f8f7f5);
  color: var(--theme-fg, #1f2937);
}

.studio-infinite-canvas .react-flow__controls-button svg {
  fill: currentColor;
}

.studio-canvas-hint {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 12px 12px !important;
  color: var(--theme-text-secondary, #6b7280);
  font-size: 11px;
  line-height: 1;
  user-select: none;
  pointer-events: none;
}

.studio-canvas-hint-separator {
  width: 2px;
  height: 2px;
  border-radius: 9999px;
  background: currentColor;
  opacity: 0.55;
}

.reasoning-effort {
  --effort-ratio: 0;
  --effort-hue: calc(210 + var(--effort-ratio) * var(--effort-ratio) * 63);
  min-width: 0;
}

.reasoning-effort-input { height: 30px; cursor: grab; }
.reasoning-effort[data-dragging] .reasoning-effort-input { cursor: grabbing; }
.reasoning-effort-track {
  position: relative;
  width: 100%;
  height: 30px;
  overflow: hidden;
  border-radius: 999px;
  background: color-mix(in srgb, var(--theme-fg) 10%, var(--theme-bg));
}

.reasoning-effort-fill {
  position: absolute;
  inset: 0;
  pointer-events: none;
  clip-path: inset(0 calc((100% - 32px) * (1 - var(--effort-ratio))) 0 0 round 999px);
}

.reasoning-effort-fill canvas { width: 100%; height: 100%; }
.reasoning-effort-dots {
  position: absolute;
  inset: 0 16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  pointer-events: none;
  transition: opacity 300ms;
}
.reasoning-effort-dots span {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--theme-fg) 30%, transparent);
}
.reasoning-effort[data-maximum] .reasoning-effort-dots { opacity: 0; }
.reasoning-effort-thumb {
  position: absolute;
  left: calc((100% - 32px) * var(--effort-ratio) + 3px);
  top: 2px;
  width: 26px;
  height: 26px;
  border-radius: var(--effort-jelly-left, 50%) var(--effort-jelly-right, 50%)
    var(--effort-jelly-right, 50%) var(--effort-jelly-left, 50%) / 50%;
  border: 1px solid color-mix(in srgb, var(--theme-fg) 10%, transparent);
  background: #fafafa;
  box-shadow: 0 1px 3px #0002;
  pointer-events: none;
  scale: var(--effort-press, 1);
  transform: translateX(var(--effort-jelly-offset, 0px))
    skewX(var(--effort-jelly-skew, 0deg))
    scale(var(--effort-jelly-x, 1), var(--effort-jelly-y, 1));
  transform-origin: center;
  transition: border-color 180ms, box-shadow 180ms;
}
.reasoning-effort-input:hover .reasoning-effort-thumb {
  border-color: color-mix(in srgb, var(--theme-fg) 20%, transparent);
}
.reasoning-effort[data-dragging] .reasoning-effort-thumb {
  transition-duration: 0ms;
  box-shadow: 0 2px 5px #0003;
}
.reasoning-effort-input-thumb {
  display: block;
  width: 32px;
  height: 30px;
  background: transparent;
  outline: none;
}
.reasoning-effort-input:has(:focus-visible) {
  border-radius: 999px;
  outline: 2px solid color-mix(in srgb, var(--theme-fg) 60%, transparent);
  outline-offset: 2px;
}
.reasoning-effort-stops {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: center;
  margin-top: 6px;
  font-size: 11px;
}
.reasoning-effort-stops button {
  min-width: 32px;
  justify-self: center;
  padding: 3px 2px;
  background: transparent;
  color: var(--theme-text-secondary);
  cursor: pointer;
  border-radius: 3px;
}
.reasoning-effort-stops button:first-child { justify-self: start; }
.reasoning-effort-stops button:last-child { justify-self: end; }
.reasoning-effort-stops button[aria-pressed="true"] { color: hsl(var(--effort-hue) 75% 43%); font-weight: 600; }
.dark .reasoning-effort-stops button[aria-pressed="true"] { color: hsl(var(--effort-hue) 75% 70%); }
.reasoning-effort-stops button:focus-visible { outline: 2px solid var(--theme-fg); outline-offset: 2px; }
.reasoning-effort[data-disabled] { opacity: .5; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
  .reasoning-effort *, .reasoning-effort *::after { transition: none !important; }
}

.reasoning-slider-track {
  background: #f2f2f2;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.06);
}

.reasoning-slider-fill {
  isolation: isolate;
  overflow: hidden;
  filter: saturate(1.02);
}

.reasoning-slider-d-energy {
  z-index: 0;
  background:
    radial-gradient(ellipse 58% 190% at 78% 112%, rgba(62, 123, 98, 0.72) 0%, rgba(45, 86, 69, 0.5) 39%, rgba(31, 55, 47, 0.22) 67%, transparent 84%),
    radial-gradient(ellipse 34% 130% at 96% 70%, rgba(38, 117, 109, 0.38), transparent 76%),
    linear-gradient(90deg,
      #214a4a 0%,
      #2d686a 12%,
      #32797c 18%,
      #2e6a68 27%,
      #285550 36%,
      #24433b 45%,
      #263f34 55%,
      #31513c 65%,
      #3c644a 75%,
      #3f6d55 84%,
      #3a645c 92%,
      #334948 100%);
}

.reasoning-slider-d-flow {
  position: absolute;
  inset: -45% -18%;
  z-index: 1;
  pointer-events: none;
  background:
    radial-gradient(ellipse 20% 52% at 42% 74%, rgba(72, 138, 103, 0.24), transparent 72%),
    radial-gradient(ellipse 24% 58% at 68% 28%, rgba(85, 159, 117, 0.3), transparent 70%),
    radial-gradient(ellipse 28% 54% at 91% 76%, rgba(47, 139, 126, 0.28), transparent 74%);
  background-size: 132% 118%, 145% 132%, 138% 126%;
  mix-blend-mode: screen;
  will-change: transform, background-position;
  animation: reasoning-irregular-flow 8.2s cubic-bezier(0.45, 0.05, 0.35, 1) infinite alternate;
}

.reasoning-slider-d-particles {
  z-index: 3;
  pointer-events: none;
  mask-image: linear-gradient(90deg, #000 0%, rgba(0, 0, 0, 0.82) 45%, rgba(0, 0, 0, 0.22) 78%, transparent 100%);
  will-change: background-position;
}

.reasoning-slider-d-particles-1 {
  opacity: 0.82;
  background-image:
    radial-gradient(circle, rgba(220, 255, 248, 0.72) 0 0.7px, transparent 1px),
    radial-gradient(circle, rgba(255, 255, 255, 0.5) 0 0.55px, transparent 0.9px);
  background-size: 41px 24px, 57px 33px;
  background-position: 0 0, 14px 11px;
  animation: reasoning-g-drift-1 7.3s linear infinite;
}

.reasoning-slider-d-particles-2 {
  opacity: 0.68;
  background-image:
    radial-gradient(circle, rgba(171, 230, 221, 0.58) 0 0.65px, transparent 1px),
    radial-gradient(circle, rgba(255, 255, 255, 0.42) 0 0.5px, transparent 0.85px);
  background-size: 53px 29px, 47px 21px;
  background-position: 8px 6px, 22px 18px;
  animation: reasoning-g-drift-2 11.7s linear infinite reverse;
}

.reasoning-slider-d-particles-3 {
  opacity: 0.58;
  background-image:
    radial-gradient(circle, rgba(255, 255, 255, 0.55) 0 0.5px, transparent 0.85px),
    radial-gradient(circle, rgba(200, 240, 230, 0.48) 0 0.45px, transparent 0.8px);
  background-size: 37px 19px, 63px 35px;
  background-position: 5px 14px, 19px 4px;
  animation: reasoning-g-drift-3 13.9s linear infinite;
}

.reasoning-slider-d-particles-4 {
  opacity: 0.48;
  background-image: radial-gradient(circle, rgba(255, 255, 255, 0.46) 0 0.45px, transparent 0.8px);
  background-size: 49px 27px;
  background-position: 11px 9px;
  animation: reasoning-g-drift-4 17.3s linear infinite reverse;
}

.reasoning-slider-d-particles-5 {
  opacity: 0.38;
  background-image: radial-gradient(circle, rgba(186, 240, 228, 0.42) 0 0.4px, transparent 0.75px);
  background-size: 31px 17px;
  background-position: 3px 7px;
  animation: reasoning-g-drift-5 19.1s linear infinite;
}

.reasoning-slider-d-breathe {
  inset: -55% -12%;
  z-index: 2;
  pointer-events: none;
  opacity: 0.42;
  background:
    radial-gradient(ellipse 34% 78% at 76% 70%, rgba(94, 171, 125, 0.32), transparent 72%),
    radial-gradient(ellipse 25% 68% at 94% 34%, rgba(55, 150, 139, 0.24), transparent 74%);
  animation: reasoning-d-green-breathe 5.8s ease-in-out infinite alternate;
}

.reasoning-slider-fill::after {
  content: none;
}

html:not(.dark) .reasoning-slider-fill {
  filter: saturate(0.96) brightness(1.18);
}

.reasoning-slider-thumb {
  background: var(--theme-elevated);
  box-shadow:
    0 3px 9px color-mix(in srgb, var(--theme-fg) 18%, transparent),
    0 0 0 1px color-mix(in srgb, var(--theme-fg) 3%, transparent);
}

.dark .reasoning-slider-thumb {
  background: color-mix(in srgb, var(--theme-fg) 94%, var(--theme-bg));
  box-shadow:
    0 3px 12px color-mix(in srgb, var(--theme-bg) 78%, transparent),
    0 0 0 1px color-mix(in srgb, var(--theme-fg) 18%, transparent),
    0 0 14px color-mix(in srgb, var(--theme-fg) 8%, transparent);
}

.reasoning-slider-thumb-settle {
  animation: reasoning-thumb-elastic-land 280ms cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes reasoning-g-drift-1 {
  to {
    background-position: 41px 24px, 71px 44px;
  }
}

@keyframes reasoning-g-drift-2 {
  to {
    background-position: -45px 23px, -25px 39px;
  }
}

@keyframes reasoning-g-drift-3 {
  to {
    background-position: 37px -19px, 63px 35px;
  }
}

@keyframes reasoning-g-drift-4 {
  to {
    background-position: -49px 27px;
  }
}

@keyframes reasoning-g-drift-5 {
  to {
    background-position: 31px 17px;
  }
}

@keyframes reasoning-irregular-flow {
  0% {
    background-position: -16% 24%, 38% -12%, 112% 32%;
    transform: translate3d(-4%, 4%, 0) scale(0.94) rotate(-1deg);
  }

  37% {
    background-position: 18% 72%, 62% 24%, 82% -16%;
    transform: translate3d(3%, -7%, 0) scale(1.05) rotate(1.4deg);
  }

  68% {
    background-position: 44% 8%, 28% 78%, 66% 48%;
    transform: translate3d(-2%, 5%, 0) scale(1.01) rotate(-0.8deg);
  }

  100% {
    background-position: 72% 60%, 8% 18%, 36% 84%;
    transform: translate3d(5%, -3%, 0) scale(1.08) rotate(1deg);
  }
}

@keyframes reasoning-d-green-breathe {
  from {
    opacity: 0.28;
    transform: translate3d(-4%, 5%, 0) scale(0.9);
  }

  to {
    opacity: 0.56;
    transform: translate3d(5%, -4%, 0) scale(1.1);
  }
}

@keyframes reasoning-thumb-elastic-land {
  0% {
    transform: translate(-50%, -50%) scale(1.06);
  }

  100% {
    transform: translate(-50%, -50%) scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .reasoning-slider-fill,
  .reasoning-slider-d-energy,
  .reasoning-slider-d-flow,
  .reasoning-slider-d-breathe,
  .reasoning-slider-d-particles-1,
  .reasoning-slider-d-particles-2,
  .reasoning-slider-d-particles-3,
  .reasoning-slider-d-particles-4,
  .reasoning-slider-d-particles-5,
  .reasoning-slider-thumb-settle {
    animation: none !important;
  }
}

/* ── Workflow Canvas (无限画布) ─────────────────────────────── */

@property --glow-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@keyframes wfGlowRotate {
  to { --glow-angle: 360deg; }
}

@keyframes wfSlideIn {
  from { transform: translateX(100%); opacity: 0; }
  to { transform: translateX(0); opacity: 1; }
}

@keyframes wfPulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

.studio-workflow-canvas {
  background: transparent !important;
}

.studio-workflow-canvas .react-flow__node {
  cursor: pointer;
}

.studio-workflow-canvas .react-flow__node.selected {
  z-index: 20 !important;
}

.studio-workflow-canvas .react-flow__edge.selected .react-flow__edge-path {
  stroke: rgba(139, 124, 247, 0.7) !important;
  stroke-width: 2.4 !important;
  filter: drop-shadow(0 0 5px rgba(139, 124, 247, 0.3));
}

.studio-workflow-canvas .react-flow__handle {
  opacity: 0;
  width: 16px !important;
  height: 16px !important;
  background: rgba(156,163,175,0.8);
  border: 2px solid rgba(30,30,35,0.95);
  border-radius: 50%;
  transition: opacity 0.2s, transform 0.15s, background 0.2s;
  cursor: crosshair;
}

.studio-workflow-canvas .react-flow__handle::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: transparent;
  pointer-events: auto;
}

.studio-workflow-canvas .react-flow__node:hover .react-flow__handle {
  opacity: 1;
  transform: scale(1.15);
  background: rgba(95,201,209,0.9);
  border-color: rgba(95,201,209,0.6);
}

.studio-workflow-canvas .react-flow__background {
  background: transparent !important;
}

.studio-workflow-canvas .react-flow__node:hover {
  z-index: 15 !important;
}

.studio-workflow-canvas .react-flow__edge:hover .react-flow__edge-path {
  stroke: rgba(255, 255, 255, 0.48) !important;
  stroke-width: 2.2 !important;
  filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.22));
}

.studio-workflow-canvas .react-flow__edge-path {
  stroke: rgba(255, 255, 255, 0.18);
  stroke-width: 1.8;
  transition: stroke 0.3s, stroke-width 0.2s, filter 0.3s;
}

@keyframes wfEdgeFlow {
  0% { stroke-dashoffset: 24; }
  100% { stroke-dashoffset: 0; }
}

.studio-workflow-canvas .wf-edge-running .react-flow__edge-path {
  stroke: rgba(59, 130, 246, 0.7);
  stroke-width: 2;
  stroke-dasharray: 8 4;
  animation: wfEdgeFlow 0.6s linear infinite;
  filter: drop-shadow(0 0 6px rgba(59, 130, 246, 0.3));
}

/* System default cursors (ReactFlow defaults to grab/grabbing hand icons) */
.studio-workflow-canvas .react-flow__pane,
.studio-workflow-canvas .react-flow__pane.draggable,
.studio-workflow-canvas .react-flow__pane.dragging,
.studio-workflow-canvas .react-flow__pane.selection,
.studio-workflow-canvas .react-flow__node,
.studio-workflow-canvas .react-flow__node.selectable,
.studio-workflow-canvas .react-flow__node.draggable,
.studio-workflow-canvas .react-flow__node.draggable.dragging,
.studio-workflow-canvas .react-flow__nodesselection-rect {
  cursor: default;
}

/* Selection box */
.wf-selection-box {
  position: absolute;
  border: 2px solid rgba(255, 255, 255, 0.52);
  background-color: rgba(255, 255, 255, 0.1);
  border-radius: 4px;
  pointer-events: none;
  z-index: 50;
}

/* ===== tauri-plugin-decorum titlebar ===== */
div[data-tauri-decorum-tb] {
  height: var(--app-titlebar-height) !important;
  background: transparent !important;
  border-bottom: none !important;
}

button.decorum-tb-btn {
  height: var(--app-titlebar-height) !important;
  color: color-mix(in srgb, var(--theme-fg) 55%, transparent) !important;
  transition: background-color 150ms, color 150ms;
}

button.decorum-tb-btn:hover {
  color: var(--theme-fg) !important;
  background-color: color-mix(in srgb, var(--theme-fg) 8%, transparent) !important;
}

button#decorum-tb-close:hover {
  color: #fff !important;
  background-color: #e81123 !important;
}


/* ===== theme/themePresets.ts · SHA256 9fe013e86b0502c94158b3b245a15c2f42f37e52247aafb920668ca4c6a9d46b ===== */
import type { ThemePalette, ThemePreset, ThemeVariant } from "./themeTypes";

function palette(p: ThemePalette): ThemePalette {
  return Object.freeze({ ...p });
}

function preset(id: string, name: string, variants: Partial<Record<ThemeVariant, ThemePalette>>): ThemePreset {
  return Object.freeze({
    id,
    name,
    source: "builtin",
    variants: Object.freeze({ ...variants }) as Partial<Record<ThemeVariant, ThemePalette>>,
  }) as ThemePreset;
}

const CODEX_LIGHT = palette({
  accent: "#339CFF",
  accentHover: "#2563EB",
  background: "#FFFFFF",
  sidebar: "#F9F8F6",
  elevated: "#FFFFFF",
  foreground: "#1F2937",
  foregroundMuted: "#6B7280",
  border: "#E5E7EB",
  hover: "#F3F4F6",
  selection: "#DBEAFE",
  contrast: 45,
});

const CODEX_DARK = palette({
  accent: "#339CFF",
  accentHover: "#60A5FA",
  background: "#000000",
  sidebar: "#121212",
  elevated: "#1A1A1A",
  foreground: "#F9FAFB",
  foregroundMuted: "#9CA3AF",
  border: "#27272A",
  hover: "#1F1F23",
  selection: "#1E3A8A",
  contrast: 50,
});

const ABSOLUTELY_LIGHT = palette({
  accent: "#7C3AED",
  accentHover: "#6D28D9",
  background: "#FAFAFA",
  sidebar: "#F4F4F5",
  elevated: "#FFFFFF",
  foreground: "#18181B",
  foregroundMuted: "#71717A",
  border: "#E4E4E7",
  hover: "#F4F4F5",
  selection: "#EDE9FE",
  contrast: 45,
});

const ABSOLUTELY_DARK = palette({
  accent: "#A78BFA",
  accentHover: "#C4B5FD",
  background: "#09090B",
  sidebar: "#18181B",
  elevated: "#27272A",
  foreground: "#FAFAFA",
  foregroundMuted: "#A1A1AA",
  border: "#3F3F46",
  hover: "#27272A",
  selection: "#2E1065",
  contrast: 50,
});

const AYU_LIGHT = palette({
  accent: "#FF9940",
  accentHover: "#F08030",
  background: "#FAFAFA",
  sidebar: "#F3F4F5",
  elevated: "#FFFFFF",
  foreground: "#1A1A1A",
  foregroundMuted: "#8A9199",
  border: "#E7E8E9",
  hover: "#EEF0F2",
  selection: "#FFEACC",
  contrast: 45,
});

const AYU_DARK = palette({
  accent: "#FF9940",
  accentHover: "#FFB366",
  background: "#1F2430",
  sidebar: "#191E2A",
  elevated: "#2A3040",
  foreground: "#CBCCC6",
  foregroundMuted: "#707A8C",
  border: "#2D3347",
  hover: "#2A3040",
  selection: "#3D4A5C",
  contrast: 50,
});

const CATPPUCCIN_LIGHT = palette({
  accent: "#8839EF",
  accentHover: "#7527D7",
  background: "#EFF1F5",
  sidebar: "#E6E9EF",
  elevated: "#FFFFFF",
  foreground: "#4C4F69",
  foregroundMuted: "#9CA0B0",
  border: "#CCD0DA",
  hover: "#DCE0E8",
  selection: "#E0D9F5",
  contrast: 45,
});

const CATPPUCCIN_DARK = palette({
  accent: "#CBA6F7",
  accentHover: "#D8B4FE",
  background: "#1E1E2E",
  sidebar: "#181825",
  elevated: "#313244",
  foreground: "#CDD6F4",
  foregroundMuted: "#6C7086",
  border: "#45475A",
  hover: "#313244",
  selection: "#45475A",
  contrast: 50,
});

const DRACULA_DARK = palette({
  accent: "#BD93F9",
  accentHover: "#CFA9FF",
  background: "#282A36",
  sidebar: "#21222C",
  elevated: "#343746",
  foreground: "#F8F8F2",
  foregroundMuted: "#6272A4",
  border: "#44475A",
  hover: "#44475A",
  selection: "#44475A",
  contrast: 50,
});

const EVERFOREST_LIGHT = palette({
  accent: "#8DA101",
  accentHover: "#7A8E00",
  background: "#FDF6E3",
  sidebar: "#F4EDD3",
  elevated: "#FFFFFF",
  foreground: "#5C6A72",
  foregroundMuted: "#939F91",
  border: "#E0DCC7",
  hover: "#EDE8D4",
  selection: "#D5E8C8",
  contrast: 45,
});

const EVERFOREST_DARK = palette({
  accent: "#A7C080",
  accentHover: "#B8D094",
  background: "#2D353B",
  sidebar: "#272E33",
  elevated: "#343F44",
  foreground: "#D3C6AA",
  foregroundMuted: "#7A8478",
  border: "#414B50",
  hover: "#3D484D",
  selection: "#3D484D",
  contrast: 50,
});

const GITHUB_LIGHT = palette({
  accent: "#0969DA",
  accentHover: "#0550AE",
  background: "#FFFFFF",
  sidebar: "#F6F8FA",
  elevated: "#FFFFFF",
  foreground: "#1F2328",
  foregroundMuted: "#656D76",
  border: "#D0D7DE",
  hover: "#F3F4F6",
  selection: "#DDF4FF",
  contrast: 45,
});

const GITHUB_DARK = palette({
  accent: "#58A6FF",
  accentHover: "#79B8FF",
  background: "#0D1117",
  sidebar: "#161B22",
  elevated: "#21262D",
  foreground: "#E6EDF3",
  foregroundMuted: "#8B949E",
  border: "#30363D",
  hover: "#21262D",
  selection: "#1F3A5F",
  contrast: 50,
});

const GRUVBOX_LIGHT = palette({
  accent: "#D65D0E",
  accentHover: "#AF3A03",
  background: "#FBF1C7",
  sidebar: "#EBDBB2",
  elevated: "#FFFFFF",
  foreground: "#3C3836",
  foregroundMuted: "#928374",
  border: "#D5C4A1",
  hover: "#EBDBB2",
  selection: "#FBD796",
  contrast: 45,
});

const GRUVBOX_DARK = palette({
  accent: "#FE8019",
  accentHover: "#FABD2F",
  background: "#282828",
  sidebar: "#1D2021",
  elevated: "#3C3836",
  foreground: "#EBDBB2",
  foregroundMuted: "#928374",
  border: "#504945",
  hover: "#3C3836",
  selection: "#504945",
  contrast: 50,
});

const LINEAR_LIGHT = palette({
  accent: "#5E6AD2",
  accentHover: "#4A55C0",
  background: "#FFFFFF",
  sidebar: "#F7F8F8",
  elevated: "#FFFFFF",
  foreground: "#1A1A1A",
  foregroundMuted: "#8A8F98",
  border: "#E5E5E5",
  hover: "#F2F2F2",
  selection: "#EAEBF8",
  contrast: 45,
});

const LINEAR_DARK = palette({
  accent: "#5E6AD2",
  accentHover: "#7B84E0",
  background: "#0F0F0F",
  sidebar: "#1A1A1A",
  elevated: "#252525",
  foreground: "#F2F2F2",
  foregroundMuted: "#6B6F76",
  border: "#2E2E2E",
  hover: "#252525",
  selection: "#2A2D5A",
  contrast: 50,
});

const NOTION_LIGHT = palette({
  accent: "#2EAADC",
  accentHover: "#1A8FBF",
  background: "#FFFFFF",
  sidebar: "#F7F6F3",
  elevated: "#FFFFFF",
  foreground: "#37352F",
  foregroundMuted: "#9B9A97",
  border: "#E9E9E7",
  hover: "#F1F1EF",
  selection: "#D3EAF5",
  contrast: 45,
});

const NOTION_DARK = palette({
  accent: "#2EAADC",
  accentHover: "#4BBDE8",
  background: "#191919",
  sidebar: "#202020",
  elevated: "#2F2F2F",
  foreground: "#CFCFCF",
  foregroundMuted: "#787774",
  border: "#373737",
  hover: "#2F2F2F",
  selection: "#1A3A4A",
  contrast: 50,
});

const ONE_LIGHT = palette({
  accent: "#4078F2",
  accentHover: "#2D65E0",
  background: "#FAFAFA",
  sidebar: "#F0F0F0",
  elevated: "#FFFFFF",
  foreground: "#383A42",
  foregroundMuted: "#A0A1A7",
  border: "#D3D3D3",
  hover: "#EAEAEA",
  selection: "#D7E5FF",
  contrast: 45,
});

const ONE_DARK = palette({
  accent: "#61AFEF",
  accentHover: "#7BBFF5",
  background: "#282C34",
  sidebar: "#21252B",
  elevated: "#2C313A",
  foreground: "#ABB2BF",
  foregroundMuted: "#5C6370",
  border: "#3E4451",
  hover: "#2C313A",
  selection: "#3E4451",
  contrast: 50,
});

export const BUILTIN_PRESETS: readonly ThemePreset[] = Object.freeze([
  preset("codex", "Codex", { light: CODEX_LIGHT, dark: CODEX_DARK }),
  preset("absolutely", "Absolutely", { light: ABSOLUTELY_LIGHT, dark: ABSOLUTELY_DARK }),
  preset("ayu", "Ayu", { light: AYU_LIGHT, dark: AYU_DARK }),
  preset("catppuccin", "Catppuccin", { light: CATPPUCCIN_LIGHT, dark: CATPPUCCIN_DARK }),
  preset("dracula", "Dracula", { dark: DRACULA_DARK }),
  preset("everforest", "Everforest", { light: EVERFOREST_LIGHT, dark: EVERFOREST_DARK }),
  preset("github", "GitHub", { light: GITHUB_LIGHT, dark: GITHUB_DARK }),
  preset("gruvbox", "Gruvbox", { light: GRUVBOX_LIGHT, dark: GRUVBOX_DARK }),
  preset("linear", "Linear", { light: LINEAR_LIGHT, dark: LINEAR_DARK }),
  preset("notion", "Notion", { light: NOTION_LIGHT, dark: NOTION_DARK }),
  preset("one", "One", { light: ONE_LIGHT, dark: ONE_DARK }),
]);

export function getPresetById(id: string): ThemePreset | undefined {
  return BUILTIN_PRESETS.find((p) => p.id === id);
}

export const DEFAULT_LIGHT_PRESET_ID = "codex";
export const DEFAULT_DARK_PRESET_ID = "codex";


/* ===== theme/themeTokens.ts · SHA256 84f12a635a81715c0f3283d0e3de02f0287cd28762b507378caa45627976d752 ===== */
import { mixHex } from "./colorUtils";
import type { ThemePalette } from "./themeTypes";

// The contrast slider (0-100) reshapes derived tokens around a neutral of 50.
// Below 50 we pull derived colors toward the background (softer separation).
// Above 50 we push them toward the foreground (crisper separation).
function contrastBlend(
  base: string,
  towardFg: string,
  towardBg: string,
  contrast: number,
  fgWeight: number,
  bgWeight: number,
): string {
  const c = Math.max(0, Math.min(100, contrast));
  const delta = (c - 50) / 50;
  if (delta === 0) return base;
  if (delta > 0) {
    return mixHex(base, towardFg, delta * fgWeight);
  }
  return mixHex(base, towardBg, -delta * bgWeight);
}

export function paletteToTokens(palette: ThemePalette): Record<string, string> {
  const { background, foreground, contrast } = palette;
  const foregroundMuted = contrastBlend(
    palette.foregroundMuted,
    foreground,
    background,
    contrast,
    0.45,
    0.35,
  );
  const border = contrastBlend(
    palette.border,
    foreground,
    background,
    contrast,
    0.35,
    0.45,
  );
  const hover = contrastBlend(
    palette.hover,
    foreground,
    background,
    contrast,
    0.25,
    0.45,
  );
  const selection = contrastBlend(
    palette.selection,
    palette.accent,
    background,
    contrast,
    0.2,
    0.35,
  );
  const sidebar = contrastBlend(
    palette.sidebar,
    foreground,
    background,
    contrast,
    0.12,
    0.35,
  );
  return {
    "--theme-accent": palette.accent,
    "--theme-accent-hover": palette.accentHover,
    "--theme-bg": palette.background,
    "--theme-sidebar": sidebar,
    "--theme-elevated": palette.elevated,
    "--theme-fg": palette.foreground,
    "--theme-text-secondary": foregroundMuted,
    "--theme-border": border,
    "--theme-hover": hover,
    "--theme-selection": selection,
  };
}

export function applyTokens(
  tokens: Record<string, string>,
  fontTokens: { uiFont: string; codeFont: string },
  isDark: boolean
): void {
  const root = document.documentElement;
  for (const [k, v] of Object.entries(tokens)) {
    root.style.setProperty(k, v);
  }
  root.style.setProperty("--ui-font", fontTokens.uiFont);
  root.style.setProperty("--code-font", fontTokens.codeFont);
  if (isDark) {
    root.classList.add("dark");
  } else {
    root.classList.remove("dark");
  }
}


/* ===== theme/themeTypes.ts · SHA256 09984375dd953369cdaee7e28d7cdbb70be0e59ec8650adf08393aecaa79db12 ===== */
export type ThemeMode = "light" | "dark" | "system";
export type ThemeVariant = "light" | "dark";
export type ThemePresetSource = "builtin" | "custom" | "imported";

export interface ThemeSwitchOrigin {
  x: number;
  y: number;
}

export interface ThemePalette {
  accent: string;
  accentHover: string;
  background: string;
  sidebar: string;
  elevated: string;
  foreground: string;
  foregroundMuted: string;
  border: string;
  hover: string;
  selection: string;
  contrast: number;
}

export interface ThemePreset {
  id: string;
  name: string;
  source: ThemePresetSource;
  variants: Partial<Record<ThemeVariant, ThemePalette>>;
  createdAt?: string;
  updatedAt?: string;
}

export interface ThemePreferences {
  uiFont: string;
  codeFont: string;
  translucentSidebar: boolean;
}

export interface PersistedThemeStateV2 {
  schemaVersion: 2;
  mode: ThemeMode;
  selectedPresetIds: Record<ThemeVariant, string>;
  workingPalettes: Record<ThemeVariant, ThemePalette>;
  preferences: ThemePreferences;
  customPresets: ThemePreset[];
  revision: number;
}

export interface ThemeImportResult {
  ok: boolean;
  presetId?: string;
  variant?: ThemeVariant;
  error?: string;
}

export const DEFAULT_UI_FONT =
  'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto';
export const DEFAULT_CODE_FONT =
  'ui-monospace, "SFMono-Regular", Menlo, Monaco, Consolas';

export const THEME_STORAGE_KEY = "deepagent:theme:v2";
export const LEGACY_THEME_STORAGE_KEY = "codex-theme-config";
export const THEME_BROADCAST_CHANNEL = "deepagent-theme";


/* ===== theme/colorUtils.ts · SHA256 7d6c3b9b0ff276ba621f37c7c45c2cf3a4e8fc0b2e5f0a9647276c5cc733e770 ===== */
export interface Rgb {
  r: number;
  g: number;
  b: number;
}

export function parseHex(hex: string): Rgb | null {
  if (typeof hex !== "string") return null;
  const m = hex.trim().match(/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/);
  if (!m) return null;
  let h = m[1];
  if (h.length === 3) {
    h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
  }
  return {
    r: parseInt(h.slice(0, 2), 16),
    g: parseInt(h.slice(2, 4), 16),
    b: parseInt(h.slice(4, 6), 16),
  };
}

export function formatHex({ r, g, b }: Rgb): string {
  const c = (v: number) =>
    Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0");
  return `#${c(r)}${c(g)}${c(b)}`.toUpperCase();
}

export function mixHex(a: string, b: string, t: number): string {
  const ca = parseHex(a);
  const cb = parseHex(b);
  if (!ca || !cb) return a;
  const k = Math.max(0, Math.min(1, t));
  return formatHex({
    r: ca.r + (cb.r - ca.r) * k,
    g: ca.g + (cb.g - ca.g) * k,
    b: ca.b + (cb.b - ca.b) * k,
  });
}

function channelLuminance(v: number): number {
  const s = v / 255;
  return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
}

export function relativeLuminance(rgb: Rgb): number {
  return (
    0.2126 * channelLuminance(rgb.r) +
    0.7152 * channelLuminance(rgb.g) +
    0.0722 * channelLuminance(rgb.b)
  );
}

export function contrastRatio(hexA: string, hexB: string): number {
  const a = parseHex(hexA);
  const b = parseHex(hexB);
  if (!a || !b) return 21;
  const la = relativeLuminance(a);
  const lb = relativeLuminance(b);
  const lighter = Math.max(la, lb);
  const darker = Math.min(la, lb);
  return (lighter + 0.05) / (darker + 0.05);
}


/* ===== locales/zh.json · SHA256 a72b36a5b9ecedc92ce0a7dee0a761fba1d1e299c018451ff87c0fd8dfe36bdd ===== */
{
  "sidebar": {
    "home": "首页",
    "recent": "最近",
    "navigation": "功能导航",
    "projectsAndChats": "项目与对话",
    "collapseSidebar": "收起项目与对话",
    "expandSidebar": "展开项目与对话",
    "projects": "项目",
    "recentChats": "最近对话",
    "collapseRecentChats": "收起最近对话",
    "expandRecentChats": "展开最近对话",
    "pinned": "置顶",
    "search": "搜索",
    "skills": "技能",
    "knowledge": "知识库",
    "plugins": "插件",
    "automation": "自动化",
    "running": "执行中",
    "newChat": "新对话",
    "settings": "设置",
    "collapseProjects": "收起项目列表",
    "expandProjects": "展开项目列表",
    "more": "更多",
    "other": "其他",
    "canvas": "无限画布",
    "remote": "远程连接",
    "newProject": "新建项目",
    "noProjects": "暂无项目",
    "noChats": "暂无对话",
    "archiveAll": "归档所有聊天",
    "organizeSidebar": "整理侧边栏",
    "sortCriteria": "排序条件",
    "organizeByProject": "按项目",
    "organizeRecentProjects": "近期项目",
    "organizeByTime": "按时间顺序",
    "organizeMoveDown": "下移",
    "sortByCreated": "创建时间",
    "sortByUpdated": "更新时间",
    "newBlankProject": "新建空白项目",
    "useExistingFolder": "使用现有文件夹",
    "pin": "置顶",
    "unpin": "取消置顶",
    "archive": "归档对话",
    "pinProject": "置顶项目",
    "unpinProject": "取消置顶项目",
    "openInExplorer": "在资源管理器中打开",
    "edit": "编辑",
    "addSection": "添加分区",
    "archiveChats": "归档聊天",
    "removeProject": "移除项目",
    "renameProject": "重命名项目",
    "remove": "移除",
    "renameProjectDialog": {
      "title": "重命名项目",
      "placeholder": "项目名称",
      "cancel": "取消",
      "confirm": "保存"
    },
    "removeProjectDialog": {
      "title": "移除项目？",
      "description": "这会从侧边栏移除 {{project}}，不会删除本地文件或历史对话。",
      "cancel": "取消",
      "confirm": "移除"
    },
    "archiveProjectDialog": {
      "title": "归档当前项目对话？",
      "description": "这会将 {{project}} 项目中的对话归档。之后你可以在已归档对话中找到它们。",
      "cancel": "取消",
      "confirm": "归档"
    },
    "loginApi": "已通过 API 密钥登录",
    "logout": "退出登录"
  },
  "remote": {
    "back": "返回主窗口",
    "newConnection": "新建连接",
    "selectConnection": "选择连接",
    "backToList": "返回列表",
    "connecting": "正在连接，即将进入终端...",
    "reconnectHint": "请从左侧重新选择连接后重试",
    "allConnections": "所有连接",
    "searchFiles": "搜索文件",
    "searchPlaceholder": "搜索已加载的文件...",
    "noConnections": "尚无 SSH 连接，点击“新建连接”添加",
    "noMatch": "无匹配文件",
    "emptyTree": "连接后展示远程目录",
    "loadFailed": "目录加载失败",
    "createFailed": "创建连接失败",
    "connectFailed": "连接失败",
    "emptyTitle": "未选择连接",
    "emptyDesc": "从左侧选择一个 SSH 连接，或新建连接开始浏览远程文件",
    "tabTerminal": "终端",
    "previewLoading": "正在读取文件...",
    "previewTruncated": "文件较大，仅显示前 1 MB",
    "previewTooLarge": "文件超过 10 MB，远程预览暂不支持",
    "previewLoadFailed": "文件读取失败",
    "notConnected": "请先连接远程服务器",
    "treeNewFolder": "新建文件夹",
    "treeNewFile": "新建文件",
    "treeRefresh": "刷新",
    "treeRename": "重命名",
    "treeDelete": "删除",
    "treeConfirmDelete": "再次点击确认删除",
    "treeConfirmDeleteDir": "确认删除整个目录",
    "treeNamePlaceholder": "输入名称，Enter 确认",
    "treeOpFailed": "操作失败",
    "uploads": "上传",
    "connEdit": "编辑",
    "editConnection": "编辑连接",
    "connConfirmDelete": "再次点击确认删除连接",
    "editPasswordHint": "保存前需重新输入密码",
    "updateFailed": "保存失败",
    "deleteFailed": "删除失败",
    "tabRename": "重命名",
    "tabClose": "关闭",
    "tabNewTerminal": "新建终端"
  },
  "contextMenu": {
    "copy": "复制",
    "selectAll": "全选"
  },
  "settings": {
    "general": {
      "title": "常规",
      "workMode": {
        "title": "工作模式",
        "desc": "选择 Codex 显示多少技术细节",
        "code": "适用于编程",
        "codeDesc": "更具技术性的回复和控制",
        "daily": "适用于日常工作",
        "dailyDesc": "同样强大，技术细节更少"
      },
      "permissions": {
        "title": "权限",
        "default": "默认权限",
        "defaultDesc": "默认情况下，Codex 可以读取并编辑其工作区中的文件。必要时，它可以请求额外的访问权限。",
        "auto": "自动审核",
        "autoDesc": "Codex 可以读取和编辑其工作区中的文件。Codex 会自动审核额外访问权限请求。自动审核可能会出错。了解更多有关风险的信息。",
        "full": "完全访问权限",
        "fullDesc": "当 Codex 以完全访问权限运行时，无需你批准，即可编辑你的电脑上的任何文件并运行联网命令。这会显著增加数据丢失、泄露或意外行为的风险。了解更多有关高风险的信息。"
      },
      "generalSection": {
        "title": "常规",
        "defaultTarget": "默认打开目标",
        "defaultTargetDesc": "默认打开文件和文件夹的位置",
        "agentEnv": "智能体环境",
        "agentEnvDesc": "选择智能体在 Windows 上的运行位置",
        "shell": "集成终端 Shell",
        "shellDesc": "选择要在集成终端中打开的 Shell。",
        "language": "语言",
        "languageDesc": "应用 UI 语言",
        "searchLanguage": "搜索语言",
        "noLanguageFound": "未找到相关语言",
        "longText": "需按 ^ - 回车键发送长文本提示",
        "longTextDesc": "启用后，长文本提示需按 ^ + 回车键发送。",
        "followUp": "跟进行为",
        "followUpDesc": "在 Codex 运行时将后续操作加入队列，或引导当前运行。按下“Ctrl+↵”可对单条消息执行相反操作",
        "queue": "排队",
        "guide": "引导",
        "codeReview": "代码审查",
        "codeReviewDesc": "尽可能在当前对话中启动 /review，或发起单独的审查对话",
        "inline": "行内视图",
        "split": "分离视图",
        "suggestions": "建议提示",
        "suggestionsDesc": "搜索项目文件和已连接应用，建议下一步操作",
        "import": "从其他 AI 应用导入工作内容",
        "importDesc": "导入您的设置、项目和最近聊天记录",
        "importBtn": "导入"
      },
      "popup": {
        "title": "弹出窗口",
        "shortcut": "弹出窗口快捷键",
        "shortcutDesc": "为弹出窗口设置全局快捷键。留空则保持关闭。",
        "disabled": "禁用",
        "set": "设置",
        "noProject": "默认使用无项目聊天",
        "noProjectDesc": "无需项目即可开始新聊天"
      },
      "dictation": {
        "title": "听写",
        "hold": "按住听写快捷键",
        "holdDesc": "在桌面任意位置按住，即可在光标处听写",
        "off": "关闭",
        "set": "设置",
        "toggle": "切换听写快捷键",
        "toggleDesc": "在桌面任意位置按一次开始听写，再按一次停止",
        "dictionary": "听写词典",
        "dictionaryDesc": "听写应能识别的单词或短语",
        "recent": "最近的听写记录",
        "recentDesc": "你最近的听写记录会显示在这里，便于在文本没有出现在预期位置时找回内容"
      },
      "notifications": {
        "title": "通知",
        "turnDone": "轮次完成通知",
        "turnDoneDesc": "设置 Codex 完成任务时的提醒",
        "always": "始终",
        "unfocused": "仅当应用失焦时",
        "never": "从不",
        "permission": "启用权限通知",
        "permissionDesc": "在审查通知限时显示提醒",
        "issue": "启用问题通知",
        "issueDesc": "需要输入或发生错误时显示提醒"
      }
    },
    "tabs": {
      "back": "返回应用",
      "general": "常规",
      "appearance": "外观",
      "config": "配置",
      "personalize": "个性化",
      "shortcuts": "键盘快捷键",
      "mcp": "MCP 服务器",
      "hooks": "钩子",
      "connections": "连接",
      "git": "Git",
      "env": "环境",
      "worktree": "工作树",
      "browser": "浏览器",
      "computer": "电脑操控",
      "project_map_debug": "项目地图调试",
      "archive": "已归档对话",
      "about": "关于"
    },
    "about": {
      "title": "关于 DeepAgent Studio",
      "subtitle": "面向 DeepSeek 的 Agent 运行时平台与桌面工作台。",
      "project": {
        "title": "项目信息",
        "version": "桌面端 v0.0.8",
        "description": "DeepAgent Studio 不只是聊天应用，而是一个可验证、可回放的 Agent 运行时平台，提供上下文工程、工具执行、权限控制、MCP 扩展与桌面工作流。本系统已软著登记。"
      },
      "stack": {
        "title": "技术栈"
      },
      "developer": {
        "title": "开发者",
        "name": "程序员Eighteen",
        "role": "DeepAgent Studio 发起人与主要开发者"
      },
      "contact": {
        "title": "快速联系",
        "github": "在 GitHub 查看项目、提交 Issue 或参与贡献",
        "repository": "个人代码仓库首页"
      },
      "actions": {
        "contact": "快速联系",
        "support": "项目支持",
        "close": "关闭"
      },
      "contactDialog": {
        "title": "联系开发者",
        "email": "邮箱",
        "phone": "手机号",
        "qq": "QQ",
        "wechat": "微信",
        "clickToCopy": "点击复制",
        "copied": "已复制"
      },
      "developers": {
        "eighteen": {
          "name": "程序员Eighteen",
          "role": "全栈开发",
          "bio": "负责 DeepAgent Studio 的全栈开发，构建桌面应用、Agent 运行时和工具生态。",
          "repository": "https://github.com/eighteendreamer",
          "contact": {
            "email": "eighteendreamer@gmail.com",
            "phone": "待配置",
            "qq": "3273495516",
            "wechat": "Eighteenth_year"
          }
        },
        "designer": {
          "name": "设计师雷师傅",
          "role": "系统设计",
          "bio": "负责系统架构与产品设计，关注 Agent 能力、交互流程和长期可维护性。",
          "repository": "https://github.com/leishifu666",
          "contact": {
            "email": "2934546967@qq.com",
            "phone": "19277413890",
            "qq": "2934546967@qq.com",
            "wechat": "Capper2934546967"
          }
        }
      },
      "support": {
        "title": "项目支持",
        "heading": "支持 DeepAgent Studio 持续开发",
        "description": "你的支持会用于项目维护、运行时能力建设和桌面端体验改进。",
        "pending": "付款码资源待配置"
      },
      "supportDialog": {
        "title": "项目支持",
        "heading": "支持 DeepAgent Studio 持续开发",
        "pending": "感谢你的支持。",
        "wechat": "微信支付",
        "alipay": "支付宝",
        "notAvailable": "该开发者的付款码待配置"
      }
    },
    "hooks": {
      "title": "钩子",
      "desc": "通过配置和已启用的插件管理生命周期钩子。",
      "learnMore": "了解更多。",
      "noHooks": "未找到钩子",
      "noHooksDesc": "已配置钩子的项目将显示在此处",
      "editorTitle": "hooks.json",
      "editorDesc": "声明在生命周期节点（PreToolUse / PostToolUse / UserPromptSubmit / Stop）运行的外部命令钩子。PreToolUse 命令以退出码 2 结束时，会阻断该工具调用并把 stderr 回喂给模型。",
      "save": "保存",
      "saved": "已保存",
      "invalidJson": "JSON 无效",
      "format": "格式化",
      "actionsRegistered": "已声明 {{count}} 个钩子动作"
    },
    "mcp": {
      "back": "返回",
      "connectCustom": "连接至自定义 MCP",
      "connectCustomDesc": "配置一个外部 MCP 服务器（stdio 或流式 HTTP）。",
      "name": "名称",
      "streamingHttp": "流式 HTTP",
      "startCommand": "启动命令",
      "args": "参数",
      "addArg": "+ 添加参数",
      "serverUrl": "服务器 URL",
      "urlDesc": "必须使用 https://（localhost 可用 http://）。",
      "envVars": "环境变量",
      "key": "键",
      "value": "值",
      "addEnvVar": "+ 添加环境变量",
      "save": "保存",
      "title": "MCP 服务器",
      "desc": "连接外部工具和数据源。",
      "learnMore": "了解更多。",
      "servers": "服务器",
      "addServer": "添加服务器",
      "noServers": "还没有 MCP 服务器。点击「添加服务器」连接外部工具。",
      "edit": "编辑",
      "delete": "删除",
      "testConnection": "测试连接",
      "refreshStatus": "刷新状态",
      "statusConnected": "已连接",
      "statusFailed": "连接失败",
      "statusDisabled": "已禁用",
      "statusPreparing": "准备中",
      "toolsFound": "{{count}} 个工具"
    },
    "personalize": {
      "title": "个性化",
      "greeting": "欢迎语",
      "greetingDesc": "设置进入应用时欢迎语中显示的自定义名称",
      "greetingPrefix": "欢迎使用，",
      "greetingPlaceholder": "自定义名称",
      "greetingDefaultName": "开发者",
      "personality": "个性",
      "personalityDesc": "选择 Codex 回复的默认语气",
      "affable": "亲和",
      "affableDesc": "温暖、协作、贴心",
      "pragmatic": "务实",
      "pragmaticDesc": "简洁、专注、直接",
      "customInstructions": "自定义指令",
      "customInstructionsDesc": "为你的项目向 Codex 提供额外说明和上下文。",
      "learnMore": "了解更多",
      "customInstructionsPlaceholder": "添加自定义指令..",
      "save": "保存",
      "memory": "记忆（实验性）",
      "memoryDesc": "设置 Codex 如何收集、保留和整合记忆。",
      "enableMemory": "启用记忆",
      "enableMemoryDesc": "从聊天中生成新记忆，并将其带入新聊天",
      "skipToolMemory": "跳过工具辅助对话",
      "skipToolMemoryDesc": "请勿从使用了 MCP 工具或网页搜索的对话中生成记忆",
      "resetMemory": "重置记忆",
      "resetMemoryDesc": "删除所有 Codex 记忆",
      "reset": "重置"
    },
    "shortcuts": {
      "title": "键盘快捷键",
      "searchPlaceholder": "搜索快捷键",
      "command": "命令",
      "keybinding": "按键绑定",
      "unassigned": "未指定",
      "noShortcuts": "没有找到匹配的快捷键",
      "archiveChat": "归档聊天",
      "archiveChatDesc": "归档当前聊天",
      "newChat": "新对话",
      "newChatDesc": "开始新聊天",
      "openSideChat": "打开侧边聊天",
      "openSideChatDesc": "在侧边栏打开当前聊天",
      "openNewWindow": "在新窗口中打开",
      "openNewWindowDesc": "在新窗口中打开当前聊天",
      "newQuickChat": "新建快速对话",
      "newQuickChatDesc": "在快速组件中开始轻量级聊天",
      "togglePin": "切换置顶状态",
      "togglePinDesc": "置顶或取消置顶当前聊天",
      "search": "查找",
      "searchDesc": "搜索当前聊天",
      "focusBrowserAddress": "聚焦浏览器地址栏",
      "focusBrowserAddressDesc": "聚焦内置浏览器地址栏",
      "goBack": "返回",
      "goBackDesc": "在导航历史记录中返回",
      "goForward": "前进",
      "goForwardDesc": "在导航历史记录中前进",
      "nextChat": "下一个对话",
      "nextChatDesc": "切换到下一个对话",
      "prevChat": "上一个对话",
      "prevChatDesc": "切换到上一个对话",
      "openBrowserTab": "打开浏览器标签页",
      "openBrowserTabDesc": "打开浏览器标签页",
      "openReviewTab": "打开审查选项卡",
      "openReviewTabDesc": "打开审查选项卡",
      "toggleBrowserPanel": "显示/隐藏浏览器面板",
      "toggleBrowserPanelDesc": "显示或隐藏浏览器面板",
      "toggleSidebar": "切换边栏",
      "toggleSidebarDesc": "显示或隐藏侧边栏",
      "toggleSidePanel": "切换侧边面板",
      "toggleSidePanelDesc": "显示或隐藏侧边面板",
      "toggleTerminal": "切换终端",
      "toggleTerminalDesc": "显示或隐藏终端面板",
      "openFolder": "打开文件夹",
      "openFolderDesc": "向 Codex 添加本地项目",
      "forceReloadSkill": "强制重新加载技能",
      "forceReloadSkillDesc": "刷新当前上下文的技能目录",
      "goToSkill": "前往技能",
      "goToSkillDesc": "浏览已安装和推荐的技能",
      "installCodexWorkspace": "安装 Codex 工作空间",
      "installCodexWorkspaceDesc": "安装高级本地功能的依赖项",
      "keyboardShortcuts": "键盘快捷方式",
      "keyboardShortcutsDesc": "自定义键盘快捷键",
      "mcp": "MCP",
      "mcpDesc": "配置 MCP 服务器",
      "personality": "个性",
      "personalityDesc": "调整语气和响应风格",
      "feedback": "反馈",
      "feedbackDesc": "向 Codex 团队发送产品反馈",
      "signOut": "注销",
      "signOutDesc": "注销 Codex",
      "manageAutomations": "管理自动化功能",
      "manageAutomationsDesc": "创建或管理自动化",
      "wakePet": "唤醒宠物",
      "wakePetDesc": "唤醒宠物",
      "openControlWindow": "打开控制窗口",
      "openControlWindowDesc": "打开控制窗口",
      "settings": "设置",
      "settingsDesc": "打开 Codex 设置",
      "approveRequest": "批准请求",
      "approveRequestDesc": "批准活跃请求",
      "declineRequest": "拒绝请求",
      "declineRequestDesc": "拒绝活跃请求",
      "close": "关闭",
      "closeDesc": "关闭活跃的标签页或窗口",
      "openModelPicker": "打开模型选择器",
      "openModelPickerDesc": "打开模型选择器",
      "startDictation": "开始听写",
      "startDictationDesc": "开始听写",
      "toggleVoiceMode": "切换语音模式",
      "toggleVoiceModeDesc": "开始或停止语音模式",
      "copyAsMarkdown": "复制为 Markdown",
      "copyAsMarkdownDesc": "将当前聊天复制为 Markdown",
      "copyConversationPath": "复制对话路径",
      "copyConversationPathDesc": "复制对话路径",
      "copyDeeplink": "复制深层链接",
      "copyDeeplinkDesc": "复制到当前聊天的深层链接",
      "copySessionId": "复制会话 ID",
      "copySessionIdDesc": "复制当前聊天的会话 ID",
      "copyWorkingDirectory": "复制工作目录",
      "copyWorkingDirectoryDesc": "复制工作目录",
      "holdDictationShortcut": "按住听写快捷键",
      "holdDictationShortcutDesc": "在桌面任意位置按住以听写",
      "toggleDictationShortcut": "切换听写快捷键",
      "toggleDictationShortcutDesc": "在桌面任意位置按一次听写，再按一次停止",
      "forceReloadBrowserPage": "强制重新加载浏览器页面",
      "forceReloadBrowserPageDesc": "强制重新加载活跃的浏览器页面",
      "popupShortcut": "弹出窗口快捷键",
      "popupShortcutDesc": "从任何地方显示或隐藏弹出窗口",
      "newWindow": "新窗口",
      "newWindowDesc": "打开新窗口",
      "nextRecentChat": "下一个最近查看的聊天",
      "nextRecentChatDesc": "循环到下一个最近查看的聊天",
      "openCommandMenu": "打开命令菜单",
      "openCommandMenuDesc": "打开命令菜单",
      "prevRecentChat": "上一个最近查看的聊天",
      "prevRecentChatDesc": "循环到上一个最近查看的聊天",
      "reloadBrowserPage": "重新加载浏览器页面",
      "reloadBrowserPageDesc": "重新加载活跃的浏览器页面",
      "renameChat": "重命名聊天",
      "renameChatDesc": "重命名当前聊天",
      "searchChats": "搜索对话...",
      "searchChatsDesc": "搜索对话",
      "searchFiles": "搜索文件...",
      "searchFilesDesc": "搜索文件",
      "showShortcuts": "显示键盘快捷键",
      "showShortcutsDesc": "显示快捷键",
      "goToChat1": "转到聊天 1",
      "goToChat1Desc": "打开该快捷槽中的可见聊天",
      "goToChat2": "转到聊天 2",
      "goToChat2Desc": "打开该快捷槽中的可见聊天",
      "goToChat3": "转到聊天 3",
      "goToChat3Desc": "打开该快捷槽中的可见聊天",
      "goToChat4": "转到聊天 4",
      "goToChat4Desc": "打开该快捷槽中的可见聊天",
      "goToChat5": "转到聊天 5",
      "goToChat5Desc": "打开该快捷槽中的可见聊天",
      "goToChat6": "转到聊天 6",
      "goToChat6Desc": "打开该快捷槽中的可见聊天",
      "goToChat7": "转到聊天 7",
      "goToChat7Desc": "打开该快捷槽中的可见聊天",
      "goToChat8": "转到聊天 8",
      "goToChat8Desc": "打开该快捷槽中的可见聊天",
      "goToChat9": "转到聊天 9",
      "goToChat9Desc": "打开该快捷槽中的可见聊天",
      "toggleFileTree": "切换文件树",
      "toggleFileTreeDesc": "切换文件树",
      "startTraceRecording": "开始录制跟踪",
      "startTraceRecordingDesc": "开始录制跟踪"
    },
    "worktrees": {
      "title": "工作树",
      "noWorktrees": "尚无工作树",
      "noWorktreesDesc": "Codex 创建的工作树将显示在此处。"
    },
    "appearance": {
      "selected": "已选",
      "select": "选择",
      "title": "外观",
      "theme": "主题",
      "themeDesc": "使用浅色、深色，或匹配系统设置",
      "light": "浅色",
      "dark": "深色",
      "system": "系统",
      "lightTheme": "浅色主题",
      "import": "导入",
      "copyTheme": "复制主题",
      "accentColor": "强调色",
      "background": "背景",
      "foreground": "前景",
      "uiFont": "UI 字体",
      "codeFont": "代码字体",
      "translucentSidebar": "半透明侧边栏",
      "contrast": "对比度",
      "darkTheme": "深色主题",
      "usePointerCursor": "使用指针光标",
      "usePointerCursorDesc": "悬停交互元素时切换为指针光标",
      "reduceMotion": "减少动态效果",
      "reduceMotionDesc": "减少动画效果或匹配系统设置",
      "on": "开启",
      "off": "关闭",
      "uiFontSize": "UI 字号",
      "uiFontSizeDesc": "调整 Codex UI 使用的基础字号",
      "codeFontSize": "代码字体大小",
      "codeFontSizeDesc": "调整聊天和差异视图中代码使用的基础字号",
      "diffMarkers": "差异标记",
      "diffMarkersDesc": "使用彩色条和背景，或在每个更改行上显示 \"+\" 和 \"-\" 符号",
      "color": "颜色",
      "pet": "宠物",
      "selectedPet": "已选择 {{pet}}",
      "createPet": "创建自己的宠物",
      "refresh": "刷新",
      "wakePet": "唤醒宠物"
    },
    "config": {
      "title": "配置",
      "desc": "管理高级配置。",
      "learnMore": "了解更多。",
      "customSettings": "自定义设置",
      "userConfig": "用户配置",
      "openConfig": "打开配置",
      "approvalStrategy": "审批策略",
      "approvalDesc": "设置 Codex 如何请求工具和命令审批。",
      "sandboxSettings": "沙盒设置",
      "sandboxDesc": "配置沙盒隔离级别。",
      "dependencies": "依赖",
      "currentVersion": "当前版本",
      "codexDependencies": "Codex 依赖",
      "codexDependenciesDesc": "重新安装或验证系统依赖。",
      "diagnose": "诊断",
      "diagnoseDesc": "运行诊断检查。",
      "diagnoseBtn": "诊断",
      "resetWorkspace": "重置工作区",
      "resetDesc": "重置你的工作区。",
      "reinstall": "重新安装",
      "untrusted": "不可信",
      "untrustedDesc": "所有操作都询问",
      "onFailure": "失败时",
      "onFailureDesc": "如果失败则询问",
      "onDemand": "按需",
      "onDemandDesc": "需要时询问",
      "never": "从不",
      "neverDesc": "从不询问",
      "readOnly": "只读",
      "readOnlyDesc": "仅读取文件",
      "workspaceWrite": "工作区写入",
      "workspaceWriteDesc": "允许写入工作区",
      "fullAccess": "完全访问",
      "fullAccessDesc": "完全系统访问",
      "webSearch": {
        "title": "网页搜索",
        "descBefore": "配置内置",
        "descAfter": "工具。DeepSeek first 会优先与 DeepSeek 保持一致，SearXNG 可作为完整网页搜索桥接。",
        "enableTitle": "启用 web_search",
        "enableDescBefore": "关闭后，仅保留",
        "enableDescAfter": "工具注册。",
        "providerTitle": "提供方",
        "providerDesc": "DeepSeek first 使用已配置的 DeepSeek key，必要时回退到 SearXNG 和 DuckDuckGo。",
        "searxngUrlTitle": "SearXNG URL",
        "searxngUrlDescBefore": "可选的桥接基础 URL，例如",
        "saveFailed": "web_search 设置保存失败",
        "providers": {
          "deepseekFirst": "DeepSeek 优先",
          "deepseekFirstDesc": "优先使用 DeepSeek 服务端网页搜索，再使用已配置的桥接",
          "searxng": "SearXNG 桥接",
          "searxngDesc": "使用你的 SearXNG 完整网页桥接，然后回退到 DuckDuckGo",
          "duckduckgo": "DuckDuckGo",
          "duckduckgoDesc": "使用无需 key 的 DuckDuckGo HTML 作为最终回退"
        }
      },
      "responses": {
        "title": "DeepSeek Responses 参数",
        "desc": "所有生成请求使用 Responses API；系统统一校验参数并记录生效状态。",
        "creativity": "创意程度",
        "creativityDesc": "影响默认 temperature；代码、分类等内部调用仍受后端硬上限保护。",
        "scenes": {
          "code": "写代码",
          "email": "写邮件",
          "analysis": "分析",
          "creative": "创意写作"
        },
        "advanced": "高级参数",
        "advancedDesc": "temperature · top_p · 输出上限 · reasoning · text.format",
        "advancedMeta": "6 项",
        "temperature": "温度",
        "topP": "Top P",
        "maxOutputTokens": "最大输出 Token",
        "topLogprobs": "Top logprobs",
        "reasoningEffort": "推理强度",
        "textFormat": "文本格式",
        "providerDefault": "提供方默认",
        "developer": "开发者 Responses JSON",
        "developerDesc": "严格校验；未知和系统管理字段会拒绝，无效果字段会警告。",
        "developerMeta": "JSON",
        "validateSave": "校验并保存",
        "saved": "Responses JSON 已保存",
        "saveFailed": "Responses 设置保存失败：{{error}}",
        "ineffective": "已保存但 DeepSeek 不产生实际效果：{{fields}}",
        "previewTitle": "生效请求预览",
        "previewDesc": "按系统字段、开发者 JSON、高级参数、场景和创意程度的真实合并顺序展示。",
        "previewReady": "Responses",
        "requestTitle": "下一次请求骨架"
      }
    },
    "git": {
      "title": "Git",
      "branchPrefix": "分支前缀",
      "branchPrefixDesc": "Codex 创建分支的前缀",
      "prMergeMethod": "PR 合并方式",
      "prMergeMethodDesc": "合并 PR 时使用的方式",
      "merge": "Merge",
      "squash": "Squash",
      "showPrIcon": "显示 PR 图标",
      "showPrIconDesc": "在聊天中显示 PR 图标",
      "alwaysForcePush": "始终强制推送",
      "forcePushDesc": "更新分支时始终强制推送",
      "draftPr": "草稿 PR",
      "draftPrDesc": "默认创建草稿 PR",
      "autoDelete": "自动删除",
      "autoDeleteDesc": "合并后自动删除分支",
      "autoDeleteLimit": "自动删除限制",
      "autoDeleteLimitDesc": "保留合并的分支数量",
      "commitInstructions": "提交说明",
      "commitInstructionsDesc": "生成提交信息的额外说明",
      "save": "保存",
      "commitInstructionsPlaceholder": "添加提交说明...",
      "prInstructions": "PR 说明",
      "prInstructionsDesc": "生成 PR 描述的额外说明",
      "prInstructionsPlaceholder": "添加 PR 说明..."
    },
    "browser": {
      "title": "浏览器",
      "desc1": "Codex 可以浏览网络并与页面交互。",
      "desc2": "了解更多。",
      "desc3": "",
      "allowControl": "允许控制",
      "dataTitle": "数据",
      "browsingData": "浏览数据",
      "clearDataDesc": "清除 Cookie、网站数据和缓存图片",
      "clearAllData": "清除所有数据",
      "cookie": "Cookies",
      "deleteCookie": "删除 Cookies",
      "websiteData": "网站数据",
      "deleteWebsiteData": "删除网站数据",
      "cachedImages": "缓存图片",
      "deleteCachedImages": "删除缓存图片",
      "screenshots": "屏幕截图",
      "screenshotsDesc": "在聊天记录中包含浏览器截图",
      "alwaysInclude": "始终包含",
      "onlySelected": "仅选定",
      "permissions": "权限",
      "approval": "审批",
      "approvalDesc": "设置 Codex 如何请求浏览域名。",
      "learnMore": "了解更多。",
      "alwaysAsk": "始终询问",
      "alwaysAllow": "始终允许",
      "askBeforeOpen": "打开新域名之前询问",
      "allowWithoutAsking": "允许不经询问浏览",
      "highRisk": "高风险",
      "blockedDomains": "受阻域名",
      "blockedDesc": "Codex 不被允许浏览的域名",
      "add": "添加",
      "noBlocked": "无受阻域名",
      "allowedDomains": "允许域名",
      "allowedDesc": "Codex 可以不经询问浏览的域名",
      "noAllowed": "无允许域名"
    },
    "connections": {
      "title": "连接",
      "desc1": "将 Codex 连接到外部服务。",
      "desc2": "了解更多。",
      "desc3": "",
      "connected": "已连接",
      "connecting": "连接中...",
      "disconnected": "未连接",
      "error": "错误",
      "available": "可用",
      "notConnected": "未连接",
      "configure": "配置",
      "connect": "连接",
      "disconnect": "断开",
      "noConnections": "尚无连接",
      "sshConnections": "SSH 连接",
      "sshDesc": "通过 SSH 连接到远程服务器",
      "add": "添加",
      "addSsh": "添加 SSH 连接",
      "addManually": "手动添加",
      "displayName": "显示名称",
      "hostname": "主机名",
      "hostPlaceholder": "例如 user@example.com",
      "sshPort": "SSH 端口",
      "optional": "(可选)",
      "username": "用户名",
      "password": "密码",
      "noAuth": "无认证",
      "identityFile": "密钥文件",
      "identityFilePath": "密钥文件路径",
      "selectIdentityFile": "点击浏览选择密钥文件",
      "browse": "浏览",
      "testing": "测试连接中...",
      "test": "测试",
      "testOk": "连接成功",
      "testFailed": "连接失败",
      "online": "在线",
      "offline": "未在线",
      "checking": "检查中...",
      "unknown": "未知",
      "edit": "编辑",
      "delete": "删除",
      "cancel": "取消",
      "save": "保存",
      "errors": {
        "generic": "无法连接，请检查 SSH 配置。",
        "refused": "连接被拒绝。请检查主机和端口是否正确，以及远程 SSH 服务是否已启动。",
        "timeout": "连接超时。请检查网络、防火墙、主机和端口。",
        "auth": "认证失败。请检查用户名、密码或密钥文件。",
        "resolve": "无法解析主机。请检查主机名或 IP 地址。",
        "unreachable": "主机不可达。请检查网络路由、防火墙和服务器状态。"
      }
    },
    "env": {
      "title": "环境变量",
      "desc1": "管理你的项目的环境变量。",
      "desc2": "了解更多。",
      "desc3": "",
      "add": "添加变量",
      "key": "键",
      "value": "值",
      "save": "保存",
      "delete": "删除",
      "edit": "编辑",
      "noVars": "没有设置环境变量",
      "back": "返回",
      "project": "项目",
      "envDetails": "环境详情",
      "noLocalEnv": "未配置本地环境",
      "createLocalEnv": "创建本地环境",
      "localEnv": "本地环境",
      "name": "名称",
      "setupScript": "启动脚本",
      "setupScriptDesc": "用于初始化环境的脚本",
      "default": "默认",
      "variables": "变量",
      "cleanupScript": "清理脚本",
      "cleanupScriptDesc": "用于清理环境的脚本",
      "actions": "操作",
      "actionsDesc": "此环境的自定义操作",
      "addAction": "添加操作",
      "addActionDesc": "创建自定义操作，如构建、测试或部署",
      "learnMore": "了解更多。",
      "selectProject": "选择项目",
      "addProject": "添加项目"
    },
    "archive": {
      "title": "归档",
      "deleteAll": "全部删除",
      "unarchive": "取消归档",
      "noArchivedChats": "暂无已归档对话",
      "untitled": "未命名对话",
      "unknownProject": "未知项目",
      "chatUnarchived": "对话已取消归档",
      "archiveDeleted": "已从归档中删除",
      "archiveCleared": "已清空归档对话",
      "loadFailed": "加载归档对话失败",
      "actionFailed": "操作失败"
    },
    "computer": {
      "title": "电脑控制",
      "desc": "允许 Codex 控制你的电脑。",
      "control": "控制",
      "extensionNotConnected": "扩展未连接",
      "install": "安装",
      "alwaysAllowed": "始终允许",
      "none": "无",
      "installChrome": "安装 Chrome 集成",
      "close": "关闭",
      "developedBy": "由 OpenAI 开发",
      "providedBy": "提供者: OpenAI",
      "category": "类别: 浏览器集成",
      "about": "关于",
      "includes": "包含",
      "browserExtension": "浏览器扩展",
      "skills": "技能",
      "features": "特性"
    }
  },
  "titleBar": {
    "file": "文件",
    "edit": "编辑",
    "view": "查看",
    "window": "窗口",
    "help": "帮助",
    "update": "更新",
    "downloadUpdate": "下载更新",
    "downloadingUpdate": "下载中",
    "downloadingUpdatePercent": "下载中 {{percent}}%",
    "downloadingUpdateMB": "{{mb}} MB",
    "updateReady": "更新已下载，关闭应用后将自动安装",
    "updateDownloadFailed": "下载更新失败，请稍后重试",
    "updateInstallFailed": "安装更新失败，请关闭应用后重试"
  },
  "startView": {
    "greeting": "我们应该在{{projectName}}中做些什么？",
    "greetingNoProject": "我们应该做些什么？",
    "placeholder": "尽管问...",
    "searchProject": "搜索项目",
    "addNewProject": "添加新项目",
    "noProject": "不使用项目"
  },
  "composer": {
    "attachments": {
      "limit": "最多可添加 {{count}} 个附件",
      "recognizing": "识别中",
      "processing": "处理中",
      "failed": "处理失败",
      "readFailed": "读取文件失败",
      "text": "文本",
      "file": "文件",
      "removeImage": "移除图片：{{name}}",
      "removeAttachment": "移除附件：{{name}}",
      "previewImage": "预览图片",
      "previewNamedImage": "预览图片：{{name}}",
      "closePreview": "关闭图片预览",
      "statusLabel": "{{name}}：{{status}}"
    },
    "placeholder": "尽管问...",
    "selectModel": "选择模型",
    "noModels": "暂无可用模型，请先在登录时填写 API Key",
    "modelSwitched": "已切换模型：{{model}}",
    "modelSwitchFailed": "切换模型失败",
    "approvalSwitchFailed": "切换权限级别失败",
    "selectThinking": "思考",
    "thinkingSimple": "简单",
    "thinkingMedium": "中度",
    "thinkingDeep": "深度",
    "effortFaster": "更快",
    "effortSmarter": "更深入",
    "thinkingSwitchFailed": "切换思考档位失败",
    "send": "发送",
    "stop": "停止",
    "stopping": "正在停止…",
    "cancelFailed": "停止请求失败：{{error}}",
    "cancelNotFound": "运行已结束或不再可用",
    "systemCommands": "系统命令",
    "skillsGroup": "技能",
    "models": {
      "lingsiAPI": "灵思API",
      "lingsiAPI_5_5_zh": "5.5 中"
    },
    "defaultPermission": "请求批准",
    "defaultPermissionDesc": "编辑外部文件和使用互联网时始终询问",
    "autoReview": "替我审批",
    "autoReviewDesc": "仅对检测到的风险操作请求批准",
    "fullAccess": "完全访问权限",
    "fullAccessDesc": "可不受限制地访问互联网和您电脑上的任何文件",
    "addMenu": {
      "addFiles": "添加文件",
      "addFilesDesc": "从本机选择",
      "hoverLabel": "添加文件",
      "tooltipDesc": "从本机选择文件，或粘贴图片与文本"
    }
  },
  "balanceChip": {
    "label": "余额",
    "loading": "查询余额…",
    "unavailable": "余额不可用",
    "notSet": "请先配置 API Key",
    "granted": "赠送额度",
    "toppedUp": "充值额度",
    "depleted": "余额已用尽，请充值后再继续",
    "refreshHint": "点击刷新余额"
  },
  "toolCard": {
    "running": "执行中",
    "ok": "完成",
    "error": "失败",
    "blocked": "已拦截",
    "arguments": "参数",
    "result": "结果",
    "errorDetail": "错误"
  },
  "approvalDialog": {
    "risk": "{{risk}} 风险",
    "pending": "还有 {{count}} 个待审批",
    "requestTool": "请求执行工具：",
    "approve": "批准",
    "approveSession": "本会话内允许该工具",
    "approvePrefix": "始终允许「{{prefix}}」",
    "grantRefused": "无法保存这条放行规则：{{error}}",
    "reject": "拒绝"
  },
  "askUserDialog": {
    "of": "{{current}} / {{total}}",
    "previous": "上一题",
    "next": "下一题",
    "multiHint": "（可多选）",
    "recommended": "推荐",
    "pending": "还有 {{count}} 个问题待回答",
    "other": "其他",
    "otherPlaceholder": "输入你的答案",
    "submit": "提交答案",
    "skip": "跳过"
  },
  "automationView": {
    "viewTemplates": "查看模板",
    "createViaChat": "通过聊天创建",
    "title": "自动化",
    "subtitle": "按计划或按需运行聊天。 ",
    "learnMore": "了解更多",
    "createFirst": "创建首个自动化",
    "dailyBriefing": "每日简报",
    "weeklyReview": "每周回顾",
    "projectMonitoring": "项目监控"
  },
  "chatView": {
    "tools": {
      "files": "文件",
      "filesDesc": "浏览项目文件",
      "chat": "侧边聊天",
      "chatDesc": "发起侧边对话",
      "browser": "浏览器",
      "browserDesc": "打开网站",
      "terminal": "终端",
      "terminalDesc": "启动交互式 shell",
      "project_map": "项目地图",
      "project_mapDesc": "查看模块关系",
      "recording": "录音",
      "recordingDesc": "会议录音与转写",
      "file_preview": "文件预览",
      "file_previewDesc": "预览办公文件",
      "canvas": "工作画布",
      "canvasDesc": "打开可扩展工作区"
    },
    "chat": "对话",
    "pinChat": "置顶对话",
    "renameChat": "重命名对话",
    "archiveChat": "归档对话",
    "openSideChat": "打开侧边聊天",
    "copy": "复制",
    "exportJson": "导出为 JSON",
    "branch": "分支",
    "rewind": "回退到…",
    "noRewindPoints": "暂无时间线节点",
    "addAutomation": "添加自动化...",
    "openInNewWindow": "在新窗口中打开",
    "requestFollowUp": "要求后续变更",
    "working": "处理中…",
    "assistant": "助手",
    "reasoning": "推理过程",
    "processSteps": "执行过程 · {{count}} 次工具调用",
    "processStepsNoTools": "执行过程",
    "clickToExpand": "点击展开",
    "tokensInput": "输入",
    "tokensOutput": "输出",
    "tokensTotal": "总计",
    "tokensCacheHit": "命中缓存",
    "totalDuration": "总耗时",
    "turnCost": "本轮花费",
    "copyAnswer": "复制",
    "addWorktree": "添加工作树",
    "environmentInfo": "环境信息",
    "environmentSettings": "环境设置",
    "changes": "变更",
    "local": "本地",
    "localMode": "本地模式",
    "remoteMode": "远程模式",
    "existingSshConnections": "已有 SSH 连接",
    "noSshConnections": "暂无 SSH 连接",
    "noProject": "暂无项目",
    "noGitRepository": "无 Git 仓库",
    "commitOrPush": "提交或推送",
    "githubCliAvailable": "GitHub CLI 可用",
    "githubCliUnavailable": "GitHub CLI 不可用",
    "loading": "加载中…",
    "output": "输出",
    "noOutput": "暂无输出",
    "outputCreated": "新建",
    "outputEdited": "修改",
    "sources": "来源",
    "noSources": "暂无来源",
    "recommended": "推荐"
  },
  "git": {
    "title": "Git",
    "close": "关闭 Git",
    "closeChanges": "关闭 Git 变更",
    "branchChangedSummary": "{{branch}} · {{count}} 个变更",
    "tabs": {
      "changes": "变更",
      "branches": "分支",
      "push": "上传",
      "log": "修改记录"
    },
    "changesTitle": "Git 变更",
    "filesCount": "{{count}} 个文件",
    "groups": {
      "conflicted": "冲突",
      "staged": "已暂存",
      "unstaged": "未暂存",
      "untracked": "未跟踪"
    },
    "loadingChanges": "正在读取 Git 变更...",
    "workingTreeClean": "工作区干净",
    "draftedFromStaged": "基于已暂存变更生成",
    "draftedFromWorkingTree": "基于工作区生成",
    "commitMessage": "提交信息",
    "draftMessage": "生成信息",
    "draftingMessage": "生成中...",
    "committing": "提交中...",
    "commitStagedFiles": "提交 {{count}} 个已暂存文件",
    "commitMessageRequired": "提交信息不能为空。",
    "stageBeforeCommit": "请至少暂存一个文件后再提交。",
    "selectFilesBeforeStage": "请至少选择一个文件后再暂存。",
    "selectStagedFilesBeforeUnstage": "请至少选择一个已暂存文件后再取消暂存。",
    "selectStagedFilesBeforeCommit": "请至少选择一个已暂存文件后再提交。",
    "selectedFileHasUnstagedChanges": "选中的文件同时存在未暂存修改。请先暂存或丢弃这部分修改后，再提交选中的文件。",
    "commitConfirm": "确认基于 {{count}} 个已暂存文件创建本地提交吗？",
    "selectAll": "全选",
    "deselectAll": "取消全选",
    "selectFileToViewDiff": "选择一个文件查看 diff。",
    "operationFailed": "{{action}}失败。",
    "operationCompleted": "{{action}}完成。",
    "actions": {
      "stage": "暂存",
      "unstage": "取消暂存",
      "commit": "提交",
      "checkout": "检出",
      "createBranch": "创建分支",
      "fetch": "拉取"
    },
    "stage": "暂存",
    "unstage": "取消暂存",
    "stageSelected": "暂存 {{count}}",
    "unstageSelected": "取消 {{count}}",
    "applyHunkFailed": "应用 hunk 失败。",
    "truncated": "已截断",
    "loadingDiff": "正在加载 diff...",
    "noUntrackedDiff": "这个未跟踪文件没有可显示的文本 diff。",
    "noDisplayableDiff": "这个文件没有可显示的 diff。",
    "diffMode": {
      "unified": "统一",
      "split": "分栏"
    },
    "diffColumns": {
      "old": "旧",
      "before": "变更前",
      "new": "新",
      "after": "变更后"
    },
    "stageHunk": "暂存 hunk",
    "unstageHunk": "取消暂存 hunk",
    "branchAlreadyCheckedOut": "该分支已在 {{path}} 检出",
    "switchBranchConfirm": "切换到 {{branch}} 吗？如果本地改动会被覆盖，Git 会阻止这次切换。",
    "newBranchName": "新分支名称",
    "searchBranches": "搜索分支",
    "detachedHead": "分离头指针",
    "rebasing": "变基中",
    "merging": "合并中",
    "changedFilesCount": "{{count}} 个变更文件",
    "loadingState": "正在加载 Git 状态...",
    "noBranchesFound": "没有找到分支",
    "localBranches": "本地分支",
    "remoteBranches": "远程分支",
    "fetch": "拉取",
    "fetching": "拉取中...",
    "aheadCount": "领先 {{count}}",
    "behindCount": "落后 {{count}}",
    "worktreeAt": "工作树：{{path}}",
    "createBranchDialog": {
      "title": "创建分支",
      "cancel": "取消",
      "placeholder": "请输入分支名称"
    },
    "pushPanel": {
      "title": "上传",
      "refresh": "刷新",
      "loadingPreview": "正在加载上传预览...",
      "currentBranch": "当前分支",
      "upstream": "上游分支",
      "target": "目标",
      "aheadBehind": "领先 / 落后",
      "notConfigured": "未配置",
      "confirmHighRisk": "本次上传包含 {{count}} 条高风险发现。请先确认风险，仍要继续吗？",
      "confirmPush": "确认向 {{target}} 上传 {{count}} 个提交吗？",
      "pushFailed": "上传失败",
      "pushing": "上传中...",
      "pushCommits": "上传 {{count}} 个提交",
      "pushCompleted": "上传完成。",
      "commitsTitle": "待上传提交",
      "commitsCount": "{{count}} 个提交",
      "noOutgoingCommits": "没有可上传的提交。",
      "noPreview": "暂无上传预览。",
      "riskScanTitle": "上传风险扫描",
      "riskSummary": "{{files}} 个文件，{{findings}} 条发现",
      "noLocalRisks": "未发现本地风险。",
      "severity": {
        "high": "高",
        "medium": "中",
        "low": "低"
      },
      "reasons": {
        "nothingToPush": "没有可上传的内容",
        "notGitRepository": "当前目录不是 Git 仓库",
        "noUpstream": "当前分支未配置上游分支",
        "upstreamAhead": "上游分支已有新提交，请先更新后再上传",
        "detachedHead": "当前为分离头指针状态，无法在此视图上传",
        "finishMerge": "请先完成或终止当前合并，再执行上传",
        "finishRebase": "请先完成或终止当前变基，再执行上传"
      }
    },
    "logPanel": {
      "title": "修改记录",
      "commitsCount": "{{count}} 个提交",
      "loadingLog": "正在加载 Git 提交记录...",
      "noCommitsFound": "没有找到提交记录。",
      "newBranchName": "新分支名称",
      "branchCreated": "已创建并检出 {{name}}",
      "createBranch": "创建分支",
      "noFileList": "这个提交没有文件列表。",
      "loadingCommitDiff": "正在加载提交 diff...",
      "selectFile": "选择一个文件查看该提交的 diff。",
      "selectCommit": "选择一个提交。",
      "mergeCount": "合并 {{count}}",
      "filesCount": "{{count}} 个文件"
    },
    "projectsPanel": {
      "title": "分支管理",
      "gitProjectsCount": "{{count}} 个 Git 项目",
      "commitSelected": "提交选中",
      "commitAndPushSelected": "提交并上传",
      "handleIndividually": "逐个处理",
      "fetchingAll": "拉取中...",
      "fetchAll": "全部拉取",
      "refresh": "刷新",
      "loadingBranches": "正在读取 Git 分支状态...",
      "noOpenGitProjects": "没有检测到已打开的 Git 项目。",
      "branchGroupsCount": "{{count}} 个分支组",
      "projectCount": "{{count}} 个项目",
      "commit": "提交",
      "commitAndPush": "提交并上传",
      "updating": "更新中...",
      "update": "更新",
      "pushing": "上传中...",
      "push": "上传",
      "current": "当前",
      "selectProject": "选择 {{path}}",
      "metrics": {
        "changed": "变更",
        "ahead": "领先",
        "behind": "落后"
      },
      "badges": {
        "merge": "合并",
        "rebase": "变基",
        "detached": "分离"
      },
      "baseRef": "基准引用",
      "targetRef": "目标引用",
      "compare": "比较",
      "comparing": "比较中...",
      "mergeBase": "公共基线 {{hash}}",
      "targetOnly": "仅目标分支",
      "noTargetOnlyCommits": "没有仅目标分支的提交",
      "baseOnly": "仅基准分支",
      "noBaseOnlyCommits": "没有仅基准分支的提交",
      "changedFiles": "变更文件",
      "noFileChanges": "没有文件变更",
      "loadingDiff": "正在加载 diff...",
      "noDiffForFile": "该文件没有 diff。",
      "selectFileToViewDiff": "选择一个文件查看 diff。",
      "moreCount": "+{{count}} 条更多",
      "batchCommit": "批量提交",
      "batchCommitAndPush": "批量提交并上传",
      "batchSummary": "{{projects}} 个项目，{{files}} 个变更文件",
      "closeBatchCommit": "关闭批量提交",
      "autoStageAll": "自动暂存每个项目的全部变更",
      "blockedProjectsCount": "当前有 {{count}} 个项目被阻止，执行后会逐项显示原因。",
      "running": "执行中...",
      "refreshPreview": "刷新预览",
      "projectPreview": "项目预览",
      "loading": "加载中...",
      "noTargets": "没有目标项目。",
      "previewChanged": "变更 {{count}}",
      "previewStaged": "暂存 {{count}}",
      "singleProjectCommitNote": "单项目提交说明，可选",
      "executionResults": "执行结果",
      "ok": "成功",
      "failed": "失败",
      "batchPushRiskScan": "批量上传风险扫描",
      "targetCount": "{{count}} 个目标",
      "severityCount": {
        "high": "高 {{count}}",
        "medium": "中 {{count}}",
        "low": "低 {{count}}"
      },
      "noLocalRiskFindingsBeforePush": "在上传 {{branch}} 前未发现本地风险。",
      "moreFindings": "+{{count}} 条更多发现",
      "confirm": {
        "pushRiskHigh": "上传风险扫描在 {{targets}} 个目标中发现 {{findings}} 条高风险问题。仍要继续吗？",
        "pushRiskAny": "上传风险扫描在 {{targets}} 个目标中发现 {{findings}} 条问题。继续吗？",
        "pushBranchGroup": "确认在 {{branch}} 上从 {{targets}} 个项目目标上传 {{commits}} 个外发提交吗？",
        "fetchRepos": "确认拉取 {{count}} 个唯一仓库吗？",
        "updateBranchGroup": "确认在 {{branch}} 上快进更新 {{count}} 个项目目标吗？",
        "continuePushingHighRisk": "上传风险扫描发现 {{findings}} 条高风险问题。继续上传 {{targets}} 个目标吗？",
        "continuePushingRisk": "上传风险扫描发现 {{findings}} 条问题。继续上传 {{targets}} 个目标吗？",
        "pushCommittedTargets": "确认上传 {{count}} 个已提交的项目目标吗？",
        "commitThenPushRiskScan": "确认先提交 {{count}} 个项目目标，再执行上传风险扫描吗？"
      },
      "errors": {
        "noProjectTargetsSelected": "未选择项目目标。",
        "noUniqueProjectTargets": "该分支组里没有可唯一处理的项目目标。",
        "noOutgoingCommitsToPush": "没有可上传的外发提交。",
        "pushRiskScanUnavailable": "无法执行上传风险扫描。",
        "onePushFailed": "有一个上传失败。",
        "noGitRepositoriesToFetch": "没有可拉取的 Git 仓库。",
        "oneFetchFailed": "有一个拉取失败。",
        "noProjectTargetsUpdatable": "该分支组里没有可更新的项目目标。",
        "noBehindCommits": "没有需要更新的落后提交。",
        "oneUpdateFailed": "有一个更新失败。",
        "noProjectTargetInRepositoryGroup": "该仓库组里没有可用的项目目标。"
      },
      "results": {
        "pushedTargets": "已上传 {{count}} 个目标。",
        "fetchedRepos": "已拉取 {{count}} 个仓库。",
        "updatedTargets": "已更新 {{count}} 个目标。",
        "commitPushRiskScanFailed": "已提交，但上传风险扫描失败：{{reason}}",
        "commitPushCancelledAfterRiskScan": "已提交，风险扫描后取消上传。",
        "commitPushCancelled": "已提交，已取消上传。",
        "committedAndPushed": "已提交并上传",
        "committedButPushFailed": "已提交，但上传失败：{{message}}"
      },
      "reasons": {
        "notGitRepository": "当前目录不是 Git 仓库",
        "nothingToPush": "没有可上传的内容",
        "noUpstream": "当前分支未配置上游分支",
        "upstreamAhead": "上游分支已有新提交，请先更新后再上传",
        "detachedHeadPush": "当前为分离头指针状态，无法在此视图上传",
        "finishMergePush": "请先完成或终止当前合并，再执行上传",
        "finishRebasePush": "请先完成或终止当前变基，再执行上传",
        "chooseBaseRef": "请选择比较的基准引用",
        "chooseTargetRef": "请选择比较的目标引用",
        "baseRefMissing": "基准引用不存在",
        "targetRefMissing": "目标引用不存在",
        "detachedHeadUpdate": "当前为分离头指针状态，无法在此视图更新。",
        "finishMergeUpdate": "请先完成或终止当前合并，再执行更新。",
        "finishRebaseUpdate": "请先完成或终止当前变基，再执行更新。",
        "commitOrStashBeforeUpdate": "请先提交或储藏本地改动，再执行更新。",
        "noUpstreamUpdate": "当前分支未配置上游分支。",
        "finishMergeCommit": "请先完成或终止当前合并，再执行提交",
        "finishRebaseCommit": "请先完成或终止当前变基，再执行提交",
        "noGitChangesToCommit": "没有可提交的 Git 变更",
        "noStagedFiles": "没有已暂存文件，请启用全部暂存或先手动暂存文件"
      }
    },
    "createAndCheckoutBranch": "创建并检出新分支...",
    "openGit": "打开 Git",
    "openGitUnavailable": "当前视图未挂载 Git 面板。"
  },
  "searchModal": {
    "searchChats": "搜索对话",
    "recentChats": "近期对话"
  },
  "skillsView": {
    "origin": {
      "installed": "已安装",
      "workspace": "工作区",
      "user": "用户",
      "builtIn": "内置"
    },
    "title": "技能",
    "subtitle1": "系统从 ",
    "subtitle2": " 自动发现技能，按需渐进式披露。",
    "refresh": "刷新",
    "searchPlaceholder": "搜索技能或触发短语",
    "discoveredCount": "已发现 {{count}} 个技能",
    "clickToView": "点击查看技能正文（Level 2 披露）；触发短语用于被动激活。",
    "loading": "加载中…",
    "noSkills": "没有技能。把 SKILL.md 放进 .deepagent/skills/<name>/ 即可被自动发现。",
    "uninstall": "卸载",
    "recommended": "推荐",
    "add": "添加",
    "triggerPhrases": "触发短语 ({{count}})",
    "body": "正文（Level 2 披露）",
    "noBody": "（无正文）",
    "tab_market": "市场",
    "tab_installed": "已安装",
    "market_loading": "加载中…",
    "market_no_results": "没有匹配的技能。",
    "market_load_more": "加载更多",
    "market_already_installed": "已安装",
    "market_install_button": "安装",
    "market_provider_config": "配置 SkillsMP API Key",
    "market_search_placeholder": "搜索技能…"
  },
  "skillsmp": {
    "title": "SkillsMP 市场",
    "description": "从 skillsmp.com 拉取 170 万+ 技能。",
    "api_key_label": "API Key（可选）",
    "api_key_placeholder": "sk_live_…",
    "save": "保存",
    "saving": "保存中…",
    "saved": "已保存",
    "test_connection": "测试连接",
    "testing": "测试中…",
    "test_ok": "连接成功",
    "test_failed": "连接失败",
    "daily_remaining": "今日剩余配额：{{n}}",
    "clear_key": "清除 Key",
    "clearing": "清除中…",
    "cleared": "已清除",
    "clear_key_confirm": "确定要删除自定义 API Key 吗？将回退到内置 Key。",
    "using_user_key": "使用自定义 Key",
    "using_builtin_key": "使用内置 Key（500/天）",
    "using_anonymous": "匿名（50/天）",
    "get_api_key": "获取 API Key",
    "view_docs": "查看文档",
    "empty_key_error": "请输入 API Key。",
    "close": "关闭"
  },
  "skillRisk": {
    "category": {
      "shell": "Shell 命令",
      "execution": "代码执行",
      "network": "网络访问",
      "credential": "凭证读取",
      "filesystem": "文件系统",
      "exfiltration": "数据外发"
    },
    "severity": {
      "safe": "安全",
      "warning": "警告",
      "danger": "危险"
    }
  },
  "skillInstallDialog": {
    "title": "安装技能：{{name}}",
    "scanning": "正在下载与扫描…",
    "ai_review": "AI 安全复审",
    "ai_review_running": "审查中…",
    "ai_review_passed": "通过",
    "ai_review_failed": "未通过",
    "ai_review_show_details": "查看详情",
    "ai_review_hide_details": "隐藏详情",
    "ai_review_error": "AI 审查失败：{{error}}",
    "static_scan": "静态扫描",
    "no_risks": "未检测到风险。",
    "risk_count": "{{danger}} 危险 · {{warning}} 警告 · {{safe}} 安全",
    "files": "文件",
    "files_summary": "{{count}} 个文件,{{size}}",
    "skill_md_preview": "SKILL.md 预览",
    "cancel": "取消",
    "install_safe": "安全安装",
    "install_warning": "安装",
    "install_danger": "仍要安装",
    "installing": "安装中…",
    "install_failed": "安装失败：{{error}}"
  },
  "skillsPage": {
    "market": "市场",
    "installed": "已安装",
    "marketDescription": "从社区市场发现并安装新技能。",
    "skillCount": "{{count}} 个技能",
    "searchPlaceholder": "搜索技能或描述…",
    "install": "安装",
    "installing": "安装中…",
    "installSafe": "安全安装",
    "installAnyway": "仍要安装",
    "alreadyInstalled": "已安装",
    "scanning": "扫描中…",
    "agentReview": "AI 安全复审",
    "agentReviewing": "正在审查…",
    "agentReviewPassed": "通过 AI 安全审查",
    "agentReviewFailed": "AI 审查发现风险",
    "securityReview": "安全审查",
    "noRisks": "未检测到明显风险。",
    "filesLabel": "文件",
    "filesCount": "{{count}} 个文件,共 {{size}}",
    "riskSafe": "安全",
    "riskWarning": "警告",
    "riskDanger": "危险",
    "riskShellCommand": "Shell 命令",
    "riskExecution": "代码执行",
    "riskNetwork": "网络访问",
    "riskCredential": "凭证读取",
    "riskFileSystem": "文件系统",
    "riskExfiltration": "数据外发"
  },
  "skillsmarket": {
    "title": "技能市场",
    "skillsmpDesc": "由 skillsmp.com 提供 1.7M+ 社区技能。",
    "apiKey": "API Key",
    "apiKeyPlaceholder": "sk_live_…",
    "getApiKey": "获取 API Key",
    "openDocs": "查看文档",
    "test": "测试连接",
    "testOk": "连接成功,剩余配额 {{remaining}}/天",
    "testFail": "连接失败:{{error}}",
    "usingBuiltinKey": "使用内置 Key(500/天)",
    "usingUserKey": "使用自定义 Key"
  },
  "knowledgeView": {
    "title": "知识库",
    "subtitle1": "把沉淀的经验以 Markdown 笔记保存在 ",
    "subtitle2": "，相关时自动注入到对话上下文。",
    "passive": "自动注入",
    "passiveHint": "开启后，每轮对话会自动带入相关的知识条目。",
    "passiveOn": "已开启自动注入",
    "passiveOff": "已关闭自动注入",
    "autoCapture": "自动沉淀",
    "autoCaptureHint": "开启后，每一次实质性的会话结束都会让模型判断是否有可复用经验：报错→恢复弧自动沉淀，普通会话由模型决定是否保存（无价值的不存）。",
    "autoCaptureOn": "已开启自动沉淀",
    "autoCaptureOff": "已关闭自动沉淀",
    "graphView": "图谱",
    "listView": "列表",
    "tagNode": "标签",
    "noDrafts": "暂无待确认草稿。",
    "draftsTitle": "待确认草稿（{{count}}）",
    "draftsHint": "这里显示需要人工确认的草稿。自动沉淀的失败恢复经验会直接进入知识库。",
    "accept": "采纳",
    "discard": "丢弃",
    "draftAccepted": "草稿已采纳",
    "draftDiscarded": "草稿已丢弃",
    "refresh": "刷新",
    "searchPlaceholder": "搜索标题、正文或标签",
    "new": "新建条目",
    "count": "共 {{count}} 条",
    "clickToView": "点击条目查看详情；相关条目也会自动注入到对话中。",
    "loading": "加载中…",
    "empty": "暂无知识。可在此添加；开启自动沉淀后，失败后恢复的经验会自动保存。或者让智能体用 knowledge_write 主动保存。",
    "delete": "删除",
    "edit": "编辑",
    "close": "关闭",
    "newTitle": "新建条目",
    "editTitle": "编辑条目",
    "fieldTitle": "标题",
    "fieldTitlePlaceholder": "一个具体、可检索的标题",
    "fieldKind": "类型",
    "fieldScope": "作用域",
    "fieldTags": "标签",
    "fieldTagsPlaceholder": "用逗号分隔的标签",
    "fieldBody": "正文",
    "fieldBodyPlaceholder": "记录这个坑/解决方法/命令/配置是什么，以及如何应用。",
    "cancel": "取消",
    "save": "保存",
    "saved": "条目已保存",
    "deleted": "条目已删除",
    "titleRequired": "标题不能为空",
    "bodyRequired": "正文不能为空",
    "saveFailed": "保存失败：{{error}}",
    "deleteFailed": "删除失败：{{error}}",
    "loadFailed": "加载知识失败：{{error}}",
    "kind": {
      "pitfall": "坑",
      "solution": "解决方法",
      "command": "命令",
      "config": "配置",
      "note": "笔记",
      "memory": "记忆"
    },
    "scope": {
      "project": "项目",
      "global": "全局"
    }
  },
  "plugins": {
    "files": {
      "specTitle": "CPI Trinity 鉴权规范",
      "specDesc": "本文档面向子系统开发者，描述如何对接 Trinity 统一鉴权。",
      "corePrinciples": "核心原则",
      "p1": "Trinity 负责签发：用户在 Trinity 门户登录，获得 JWT Token",
      "p2": "子系统负责验证：子系统后端只需验证 Token 签名，不需要维护用户登录逻辑",
      "p3": "Token 统一传递：门户跳转时通过 URL/IPC 传递 Token，子系统前端存储后使用",
      "tokenSpec": "Token 规格",
      "field": "字段",
      "value": "值",
      "format": "格式",
      "filterPlaceholder": "筛选文件..."
    },
    "terminal": {
      "copyright": "版权所有 (C) Microsoft Corporation。保留所有权利。",
      "tryNew": "安装最新的 PowerShell，了解新功能和改进！https://aka.ms/PSWindows"
    },
    "filePreview": {
      "choose": "选择文件",
      "loading": "正在加载预览…",
      "empty": "选择一个文件在此预览",
      "unsupported": "该文件类型暂不支持预览",
      "imageUnavailable": "图片无法加载",
      "noSheets": "未找到工作表",
      "truncated": "预览内容已截断",
      "truncatedRows": "（仅显示前若干行）",
      "sendToChat": "发送到对话"
    },
    "recording": {
      "title": "录音",
      "idle": "准备录音",
      "recording": "录音中",
      "paused": "已暂停",
      "transcribing": "转写中…",
      "done": "已完成",
      "error": "出错",
      "start": "开始",
      "pause": "暂停",
      "resume": "继续",
      "stop": "停止",
      "transcribe": "转写为文字",
      "generateMinutes": "生成会议纪要",
      "modelNeededTitle": "需要语音模型",
      "modelNeededDesc": "转写需要 Whisper 模型。模型会下载到应用自身目录，并在使用前校验。",
      "downloadModel": "下载模型",
      "downloading": "下载中…",
      "transcript": "转写文本",
      "noSpeech": "未检测到语音",
      "minutes": "会议纪要",
      "exportWord": "导出 Word",
      "exportedTo": "已保存到",
      "sendToChat": "发送到对话"
    }
  },
  "commandPalette": {
    "placeholder": "输入命令…",
    "noMatch": "没有匹配的命令"
  },
  "chat": {
    "nativeWebSearchCompleted": "DeepSeek 网页搜索完成（{{count}} 个查询）",
    "nativeWebSearchStatus": "DeepSeek 网页搜索：{{status}}"
  },
  "general": {
    "languages": {
      "autoDetect": "自动检测",
      "sq": "阿尔巴尼亚语 (阿尔巴尼亚)",
      "is": "冰岛语 (冰岛)",
      "ka": "格鲁吉亚语 (格鲁吉亚)",
      "mk": "马其顿语 (北马其顿)",
      "mn": "蒙古语",
      "my": "缅甸语 (缅甸)",
      "ja": "日本语 (日本) · 日语 (日本)",
      "so": "索马里语 (索马里)",
      "hy": "亚美尼亚语 (亚美尼亚)",
      "zhTW": "中文 (台湾) · 中文 (台湾)",
      "zhHK": "中文 (香港)",
      "zhCN": "中文 (简体)",
      "enUS": "English (US)"
    }
  }
}


/* ===== locales/en.json · SHA256 9a879e7d57091508dee0a8e8e1dc552f5b627818c5deb37af5ec2348e5350de8 ===== */
{
  "sidebar": {
    "home": "Home",
    "recent": "Recent",
    "navigation": "Feature navigation",
    "projectsAndChats": "Projects and chats",
    "collapseSidebar": "Collapse projects and chats",
    "expandSidebar": "Expand projects and chats",
    "projects": "Projects",
    "recentChats": "Recent Chats",
    "collapseRecentChats": "Collapse recent chats",
    "expandRecentChats": "Expand recent chats",
    "pinned": "Pinned",
    "search": "Search",
    "skills": "Skills",
    "knowledge": "Knowledge",
    "plugins": "Plugins",
    "automation": "Automation",
    "running": "Running",
    "newChat": "New Chat",
    "settings": "Settings",
    "collapseProjects": "Collapse projects",
    "expandProjects": "Expand projects",
    "more": "More",
    "other": "Other",
    "canvas": "Infinite Canvas",
    "remote": "Remote Connections",
    "newProject": "New Project",
    "noProjects": "No projects",
    "noChats": "No chats",
    "archiveAll": "Archive all chats",
    "organizeSidebar": "Organize sidebar",
    "sortCriteria": "Sort criteria",
    "organizeByProject": "By project",
    "organizeRecentProjects": "Recent projects",
    "organizeByTime": "Chronological",
    "organizeMoveDown": "Move empty down",
    "sortByCreated": "Created time",
    "sortByUpdated": "Updated time",
    "newBlankProject": "New blank project",
    "useExistingFolder": "Use existing folder",
    "pin": "Pin",
    "unpin": "Unpin",
    "archive": "Archive chat",
    "pinProject": "Pin project",
    "unpinProject": "Unpin project",
    "openInExplorer": "Open in Explorer",
    "edit": "Edit",
    "addSection": "Add section",
    "archiveChats": "Archive chats",
    "removeProject": "Remove project",
    "renameProject": "Rename project",
    "remove": "Remove",
    "renameProjectDialog": {
      "title": "Rename project",
      "placeholder": "Project name",
      "cancel": "Cancel",
      "confirm": "Save"
    },
    "removeProjectDialog": {
      "title": "Remove project?",
      "description": "This removes {{project}} from the sidebar. It will not delete local files or conversation history.",
      "cancel": "Cancel",
      "confirm": "Remove"
    },
    "archiveProjectDialog": {
      "title": "Archive this project's conversations?",
      "description": "This will archive the conversations in {{project}}. You can find them later in Archived conversations.",
      "cancel": "Cancel",
      "confirm": "Archive"
    },
    "loginApi": "Logged in via API Key",
    "logout": "Logout"
  },
  "remote": {
    "back": "Back to Main",
    "newConnection": "New Connection",
    "selectConnection": "Select Connection",
    "backToList": "Back to List",
    "connecting": "Connecting, entering terminal...",
    "reconnectHint": "Pick a connection on the left to retry",
    "allConnections": "All Connections",
    "searchFiles": "Search Files",
    "searchPlaceholder": "Search loaded files...",
    "noConnections": "No SSH connections yet. Click \"New Connection\" to add one.",
    "noMatch": "No matching files",
    "emptyTree": "Remote directories appear after connecting",
    "loadFailed": "Failed to load directory",
    "createFailed": "Failed to create connection",
    "connectFailed": "Failed to connect",
    "emptyTitle": "No connection selected",
    "emptyDesc": "Pick an SSH connection on the left, or create a new one to browse remote files",
    "tabTerminal": "Terminal",
    "previewLoading": "Reading file...",
    "previewTruncated": "Large file — showing first 1 MB",
    "previewTooLarge": "File exceeds 10 MB — remote preview not supported yet",
    "previewLoadFailed": "Failed to read file",
    "notConnected": "Please connect to a remote server first",
    "treeNewFolder": "New Folder",
    "treeNewFile": "New File",
    "treeRefresh": "Refresh",
    "treeRename": "Rename",
    "treeDelete": "Delete",
    "treeConfirmDelete": "Click again to delete",
    "treeConfirmDeleteDir": "Delete entire folder",
    "treeNamePlaceholder": "Enter name, press Enter",
    "treeOpFailed": "Operation failed",
    "uploads": "Uploads",
    "connEdit": "Edit",
    "editConnection": "Edit Connection",
    "connConfirmDelete": "Click again to delete connection",
    "editPasswordHint": "Re-enter password to save",
    "updateFailed": "Save failed",
    "deleteFailed": "Delete failed",
    "tabRename": "Rename",
    "tabClose": "Close",
    "tabNewTerminal": "New Terminal"
  },
  "contextMenu": {
    "copy": "Copy",
    "selectAll": "Select All"
  },
  "settings": {
    "general": {
      "title": "General",
      "workMode": {
        "title": "Work Mode",
        "desc": "Choose how much technical detail Codex shows",
        "code": "For Coding",
        "codeDesc": "More technical responses and control",
        "daily": "For Daily Work",
        "dailyDesc": "Equally capable, fewer technical details"
      },
      "permissions": {
        "title": "Permissions",
        "default": "Default Permissions",
        "defaultDesc": "By default, Codex can read and edit files in its workspace. It may request additional access if needed.",
        "auto": "Auto Review",
        "autoDesc": "Codex can read and edit files in its workspace. Extra access requests are auto-reviewed. Auto review may make mistakes. Learn more about risks.",
        "full": "Full Access",
        "fullDesc": "When running with full access, Codex can edit any file on your computer and run network commands without your approval. This significantly increases the risk of data loss, leaks, or unintended behavior. Learn more about high risks."
      },
      "generalSection": {
        "title": "General",
        "defaultTarget": "Default Open Target",
        "defaultTargetDesc": "Default location to open files and folders",
        "agentEnv": "Agent Environment",
        "agentEnvDesc": "Choose where the agent runs on Windows",
        "shell": "Integrated Terminal Shell",
        "shellDesc": "Choose the shell to open in the integrated terminal.",
        "language": "Language",
        "languageDesc": "App UI language",
        "searchLanguage": "Search language",
        "noLanguageFound": "No language found",
        "longText": "Require ^ - Enter to send long text prompts",
        "longTextDesc": "When enabled, long text prompts require ^ + Enter to send.",
        "followUp": "Follow-up Behavior",
        "followUpDesc": "Queue follow-up actions while Codex is running, or guide current run. Press 'Ctrl+↵' to do the opposite for a single message",
        "queue": "Queue",
        "guide": "Guide",
        "codeReview": "Code Review",
        "codeReviewDesc": "Trigger /review in current chat if possible, or start a separate review chat",
        "inline": "Inline View",
        "split": "Split View",
        "suggestions": "Suggestion Prompts",
        "suggestionsDesc": "Search project files and connected apps, suggest next steps",
        "import": "Import from other AI apps",
        "importDesc": "Import your settings, projects, and recent chat history",
        "importBtn": "Import"
      },
      "popup": {
        "title": "Popups",
        "shortcut": "Popup Shortcut",
        "shortcutDesc": "Set global shortcut for popup. Leave blank to keep closed.",
        "disabled": "Disabled",
        "set": "Set",
        "noProject": "Default to no-project chat",
        "noProjectDesc": "Start new chats without needing a project"
      },
      "dictation": {
        "title": "Dictation",
        "hold": "Hold Dictation Shortcut",
        "holdDesc": "Hold anywhere on desktop to dictate at cursor",
        "off": "Off",
        "set": "Set",
        "toggle": "Toggle Dictation Shortcut",
        "toggleDesc": "Press once to start dictation, press again to stop",
        "dictionary": "Dictation Dictionary",
        "dictionaryDesc": "Words or phrases dictation should recognize",
        "recent": "Recent Dictation Records",
        "recentDesc": "Your recent dictations will appear here, in case text doesn't appear where expected"
      },
      "notifications": {
        "title": "Notifications",
        "turnDone": "Turn Completion Notification",
        "turnDoneDesc": "Alert when Codex finishes a task",
        "always": "Always",
        "unfocused": "Only when app is unfocused",
        "never": "Never",
        "permission": "Enable Permission Notifications",
        "permissionDesc": "Show alert for limited time during review",
        "issue": "Enable Issue Notifications",
        "issueDesc": "Show alert when input is required or an error occurs"
      }
    },
    "tabs": {
      "back": "Back to App",
      "general": "General",
      "appearance": "Appearance",
      "config": "Config",
      "personalize": "Personalize",
      "shortcuts": "Keyboard Shortcuts",
      "mcp": "MCP Servers",
      "hooks": "Hooks",
      "connections": "Connections",
      "git": "Git",
      "env": "Environment",
      "worktree": "Worktree",
      "browser": "Browser",
      "computer": "Computer Control",
      "project_map_debug": "Project Map Debug",
      "archive": "Archived Chats",
      "about": "About"
    },
    "about": {
      "title": "About DeepAgent Studio",
      "subtitle": "An Agent runtime platform and desktop workspace for DeepSeek.",
      "project": {
        "title": "Project",
        "version": "Desktop v0.0.8",
        "description": "DeepAgent Studio is more than a chat app: it is a verifiable, replayable Agent runtime platform with context engineering, tool execution, permissions, MCP extensions, and desktop workflows. This system has completed software copyright registration."
      },
      "stack": {
        "title": "Technology Stack"
      },
      "developer": {
        "title": "Developer",
        "name": "Eighteen",
        "role": "Creator and primary developer of DeepAgent Studio"
      },
      "contact": {
        "title": "Quick Contact",
        "github": "View the project, open an issue, or contribute on GitHub",
        "repository": "Personal code repository"
      },
      "actions": {
        "contact": "Quick Contact",
        "support": "Support Project",
        "close": "Close"
      },
      "contactDialog": {
        "title": "Contact Developer",
        "email": "Email",
        "phone": "Phone",
        "qq": "QQ",
        "wechat": "WeChat",
        "clickToCopy": "Click to copy",
        "copied": "Copied"
      },
      "developers": {
        "eighteen": {
          "name": "Eighteen",
          "role": "Full-stack Development",
          "bio": "Leads full-stack development for DeepAgent Studio, including the desktop app, Agent runtime, and tool ecosystem.",
          "repository": "https://github.com/eighteendreamer",
          "contact": {
            "email": "eighteendreamer@gmail.com",
            "phone": "To be configured",
            "qq": "3273495516",
            "wechat": "Eighteenth_year"
          }
        },
        "designer": {
          "name": "Designer Lei Shifu",
          "role": "System Design",
          "bio": "Leads system architecture and product design, with a focus on Agent capabilities, interaction flows, and maintainability.",
          "repository": "https://github.com/leishifu666",
          "contact": {
            "email": "2934546967@qq.com",
            "phone": "19277413890",
            "qq": "2934546967@qq.com",
            "wechat": "Capper2934546967"
          }
        }
      },
      "support": {
        "title": "Support the Project",
        "heading": "Help DeepAgent Studio keep improving",
        "description": "Your support helps fund maintenance, runtime capabilities, and desktop experience improvements.",
        "pending": "Payment QR code will be added here"
      },
      "supportDialog": {
        "title": "Support the Project",
        "heading": "Help DeepAgent Studio keep improving",
        "pending": "Thank you for your support.",
        "wechat": "WeChat Pay",
        "alipay": "Alipay",
        "notAvailable": "Payment QR code not configured for this developer"
      }
    },
    "hooks": {
      "title": "Hooks",
      "desc": "Manage lifecycle hooks through configuration and enabled plugins.",
      "learnMore": "Learn more.",
      "noHooks": "No hooks found",
      "noHooksDesc": "Projects with configured hooks will be displayed here",
      "editorTitle": "hooks.json",
      "editorDesc": "Declare external command hooks that run at lifecycle points (PreToolUse / PostToolUse / UserPromptSubmit / Stop). A PreToolUse command that exits with code 2 blocks the tool call and feeds its stderr back to the model.",
      "save": "Save",
      "saved": "Saved",
      "invalidJson": "Invalid JSON",
      "format": "Format",
      "actionsRegistered": "{{count}} hook action(s) declared"
    },
    "mcp": {
      "back": "Back",
      "connectCustom": "Connect to custom MCP",
      "connectCustomDesc": "Configure an external MCP server (stdio or streaming HTTP).",
      "name": "Name",
      "streamingHttp": "Streaming HTTP",
      "startCommand": "Start command",
      "args": "Arguments",
      "addArg": "+ Add argument",
      "serverUrl": "Server URL",
      "urlDesc": "Must use https:// (http:// is allowed for localhost).",
      "envVars": "Environment variables",
      "key": "Key",
      "value": "Value",
      "addEnvVar": "+ Add environment variable",
      "save": "Save",
      "title": "MCP Servers",
      "desc": "Connect external tools and data sources.",
      "learnMore": "Learn more.",
      "servers": "Servers",
      "addServer": "Add Server",
      "noServers": "No MCP servers yet. Click \"Add Server\" to connect external tools.",
      "edit": "Edit",
      "delete": "Delete",
      "testConnection": "Test connection",
      "refreshStatus": "Refresh status",
      "statusConnected": "Connected",
      "statusFailed": "Failed",
      "statusDisabled": "Disabled",
      "statusPreparing": "Preparing",
      "toolsFound": "{{count}} tool(s)"
    },
    "personalize": {
      "title": "Personalization",
      "greeting": "Welcome message",
      "greetingDesc": "Set the custom name shown in the welcome message when entering the app",
      "greetingPrefix": "Welcome, ",
      "greetingPlaceholder": "Custom name",
      "greetingDefaultName": "Developer",
      "personality": "Personality",
      "personalityDesc": "Choose the default tone for Codex's replies",
      "affable": "Affable",
      "affableDesc": "Warm, collaborative, caring",
      "pragmatic": "Pragmatic",
      "pragmaticDesc": "Concise, focused, direct",
      "customInstructions": "Custom instructions",
      "customInstructionsDesc": "Provide additional instructions and context to Codex for your projects.",
      "learnMore": "Learn more",
      "customInstructionsPlaceholder": "Add custom instructions..",
      "save": "Save",
      "memory": "Memory (Experimental)",
      "memoryDesc": "Set how Codex collects, retains, and integrates memories.",
      "enableMemory": "Enable memory",
      "enableMemoryDesc": "Generate new memories from chats and bring them into new chats",
      "skipToolMemory": "Skip tool-assisted conversations",
      "skipToolMemoryDesc": "Do not generate memories from conversations that used MCP tools or web search",
      "resetMemory": "Reset memory",
      "resetMemoryDesc": "Delete all Codex memories",
      "reset": "Reset"
    },
    "shortcuts": {
      "title": "Keyboard Shortcuts",
      "searchPlaceholder": "Search shortcuts",
      "command": "Command",
      "keybinding": "Keybinding",
      "unassigned": "Unassigned",
      "noShortcuts": "No matching shortcuts found",
      "archiveChat": "Archive Chat",
      "archiveChatDesc": "Archive the current chat",
      "newChat": "New Chat",
      "newChatDesc": "Start a new chat",
      "openSideChat": "Open Side Chat",
      "openSideChatDesc": "Open the current chat in a side chat",
      "openNewWindow": "Open in New Window",
      "openNewWindowDesc": "Open the current chat in a new window",
      "newQuickChat": "New Quick Chat",
      "newQuickChatDesc": "Start a lightweight chat in the quick composer",
      "togglePin": "Toggle Pin",
      "togglePinDesc": "Pin or unpin the current chat",
      "search": "Search",
      "searchDesc": "Search the current chat",
      "focusBrowserAddress": "Focus Browser Address",
      "focusBrowserAddressDesc": "Focus the in-app browser address bar",
      "goBack": "Go Back",
      "goBackDesc": "Go back in navigation history",
      "goForward": "Go Forward",
      "goForwardDesc": "Go forward in navigation history",
      "nextChat": "Next Chat",
      "nextChatDesc": "Switch to the next chat",
      "prevChat": "Previous Chat",
      "prevChatDesc": "Switch to the previous chat",
      "openBrowserTab": "Open Browser Tab",
      "openBrowserTabDesc": "Open a browser tab",
      "openReviewTab": "Open Review Tab",
      "openReviewTabDesc": "Open the review tab",
      "toggleBrowserPanel": "Toggle Browser Panel",
      "toggleBrowserPanelDesc": "Show or hide the browser panel",
      "toggleSidebar": "Toggle Sidebar",
      "toggleSidebarDesc": "Show or hide the sidebar",
      "toggleSidePanel": "Toggle Side Panel",
      "toggleSidePanelDesc": "Show or hide the side panel",
      "toggleTerminal": "Toggle Terminal",
      "toggleTerminalDesc": "Show or hide the terminal panel",
      "openFolder": "Open Folder",
      "openFolderDesc": "Add a local project to Codex",
      "forceReloadSkill": "Force Reload Skill",
      "forceReloadSkillDesc": "Refresh the skill catalog for the current context",
      "goToSkill": "Go To Skill",
      "goToSkillDesc": "Browse installed and recommended skills",
      "installCodexWorkspace": "Install Codex Workspace",
      "installCodexWorkspaceDesc": "Install dependencies for advanced local features",
      "keyboardShortcuts": "Keyboard Shortcuts",
      "keyboardShortcutsDesc": "Customize keyboard shortcuts",
      "mcp": "MCP",
      "mcpDesc": "Configure MCP servers",
      "personality": "Personality",
      "personalityDesc": "Adjust tone and response style",
      "feedback": "Feedback",
      "feedbackDesc": "Send product feedback to the Codex team",
      "signOut": "Sign Out",
      "signOutDesc": "Sign out of Codex",
      "manageAutomations": "Manage Automations",
      "manageAutomationsDesc": "Create or manage automations from the current state",
      "wakePet": "Wake Pet",
      "wakePetDesc": "Open the pet overlay",
      "openControlWindow": "Open Control Window",
      "openControlWindowDesc": "Open the voice control window",
      "settings": "Settings",
      "settingsDesc": "Open Codex settings",
      "approveRequest": "Approve Request",
      "approveRequestDesc": "Approve the active request",
      "declineRequest": "Decline Request",
      "declineRequestDesc": "Decline the active request",
      "close": "Close",
      "closeDesc": "Close the active tab or window",
      "openModelPicker": "Open Model Picker",
      "openModelPickerDesc": "Open the composer model picker",
      "startDictation": "Start Dictation",
      "startDictationDesc": "Start dictation in the current composer",
      "toggleVoiceMode": "Toggle Voice Mode",
      "toggleVoiceModeDesc": "Start or stop voice mode",
      "copyAsMarkdown": "Copy as Markdown",
      "copyAsMarkdownDesc": "Copy the current chat as Markdown",
      "copyConversationPath": "Copy conversation path",
      "copyConversationPathDesc": "Copy the current chat path",
      "copyDeeplink": "Copy deeplink",
      "copyDeeplinkDesc": "Copy a deeplink to the current chat",
      "copySessionId": "Copy session id",
      "copySessionIdDesc": "Copy the current chat session ID",
      "copyWorkingDirectory": "Copy working directory",
      "copyWorkingDirectoryDesc": "Copy the current chat working directory",
      "holdDictationShortcut": "Hold Dictation Shortcut",
      "holdDictationShortcutDesc": "Hold anywhere on desktop to dictate where you want",
      "toggleDictationShortcut": "Toggle Dictation Shortcut",
      "toggleDictationShortcutDesc": "Press once anywhere on desktop to dictate, then again to stop",
      "forceReloadBrowserPage": "Force Reload Browser Page",
      "forceReloadBrowserPageDesc": "Force reload the active browser page",
      "popupShortcut": "Popup Shortcut",
      "popupShortcutDesc": "Show or hide Popout Window from anywhere",
      "newWindow": "New Window",
      "newWindowDesc": "Open a new window",
      "nextRecentChat": "Next Recent Chat",
      "nextRecentChatDesc": "Cycle to the next recently viewed chat",
      "openCommandMenu": "Open command menu",
      "openCommandMenuDesc": "Open the command menu",
      "prevRecentChat": "Previous Recent Chat",
      "prevRecentChatDesc": "Cycle to the previous recently viewed chat",
      "reloadBrowserPage": "Reload Browser Page",
      "reloadBrowserPageDesc": "Reload the active browser page",
      "renameChat": "Rename chat",
      "renameChatDesc": "Rename the current chat",
      "searchChats": "Search Chats...",
      "searchChatsDesc": "Search chats",
      "searchFiles": "Search Files...",
      "searchFilesDesc": "Search files",
      "showShortcuts": "Show Shortcuts",
      "showShortcutsDesc": "Show the shortcuts available right now",
      "goToChat1": "Go To Chat 1",
      "goToChat1Desc": "Open the visible chat in this shortcut slot",
      "goToChat2": "Go To Chat 2",
      "goToChat2Desc": "Open the visible chat in this shortcut slot",
      "goToChat3": "Go To Chat 3",
      "goToChat3Desc": "Open the visible chat in this shortcut slot",
      "goToChat4": "Go To Chat 4",
      "goToChat4Desc": "Open the visible chat in this shortcut slot",
      "goToChat5": "Go To Chat 5",
      "goToChat5Desc": "Open the visible chat in this shortcut slot",
      "goToChat6": "Go To Chat 6",
      "goToChat6Desc": "Open the visible chat in this shortcut slot",
      "goToChat7": "Go To Chat 7",
      "goToChat7Desc": "Open the visible chat in this shortcut slot",
      "goToChat8": "Go To Chat 8",
      "goToChat8Desc": "Open the visible chat in this shortcut slot",
      "goToChat9": "Go To Chat 9",
      "goToChat9Desc": "Open the visible chat in this shortcut slot",
      "toggleFileTree": "Toggle File Tree",
      "toggleFileTreeDesc": "Toggle the file tree panel",
      "startTraceRecording": "Start Trace Recording",
      "startTraceRecordingDesc": "Start or stop trace recording"
    },
    "worktrees": {
      "title": "Worktrees",
      "noWorktrees": "No worktrees yet",
      "noWorktreesDesc": "Worktrees created by Codex will be displayed here."
    },
    "appearance": {
      "selected": "Selected",
      "select": "Select",
      "title": "Appearance",
      "theme": "Theme",
      "themeDesc": "Use light, dark, or match system settings",
      "light": "Light",
      "dark": "Dark",
      "system": "System",
      "lightTheme": "Light Theme",
      "import": "Import",
      "copyTheme": "Copy Theme",
      "accentColor": "Accent Color",
      "background": "Background",
      "foreground": "Foreground",
      "uiFont": "UI Font",
      "codeFont": "Code Font",
      "translucentSidebar": "Translucent Sidebar",
      "contrast": "Contrast",
      "darkTheme": "Dark Theme",
      "usePointerCursor": "Use Pointer Cursor",
      "usePointerCursorDesc": "Switch to pointer cursor when hovering over interactive elements",
      "reduceMotion": "Reduce Motion",
      "reduceMotionDesc": "Reduce animation effects or match system settings",
      "on": "On",
      "off": "Off",
      "uiFontSize": "UI Font Size",
      "uiFontSizeDesc": "Adjust the base font size used by Codex UI",
      "codeFontSize": "Code Font Size",
      "codeFontSizeDesc": "Adjust the base font size used for code in chat and diff views",
      "diffMarkers": "Diff Markers",
      "diffMarkersDesc": "Use colored bars and backgrounds, or show \"+\" and \"-\" symbols on each changed line",
      "color": "Color",
      "pet": "Pet",
      "selectedPet": "Selected {{pet}}",
      "createPet": "Create your own pet",
      "refresh": "Refresh",
      "wakePet": "Wake Pet"
    },
    "config": {
      "title": "Config",
      "desc": "Manage advanced configuration settings.",
      "learnMore": "Learn more.",
      "customSettings": "Custom Settings",
      "userConfig": "User Config",
      "openConfig": "Open Config",
      "approvalStrategy": "Approval Strategy",
      "approvalDesc": "Set how Codex asks for approval for tools and commands.",
      "sandboxSettings": "Sandbox Settings",
      "sandboxDesc": "Configure sandbox isolation level.",
      "dependencies": "Dependencies",
      "currentVersion": "Current Version",
      "codexDependencies": "Codex Dependencies",
      "codexDependenciesDesc": "Reinstall or verify the system dependencies Codex uses.",
      "diagnose": "Diagnose",
      "diagnoseDesc": "Run diagnostic checks to identify environment issues.",
      "diagnoseBtn": "Diagnose",
      "resetWorkspace": "Reset Workspace",
      "resetDesc": "Reset your workspace if things get into a bad state.",
      "reinstall": "Reinstall",
      "untrusted": "Untrusted",
      "untrustedDesc": "Ask for everything",
      "onFailure": "On Failure",
      "onFailureDesc": "Ask if something fails",
      "onDemand": "On Demand",
      "onDemandDesc": "Ask when needed",
      "never": "Never",
      "neverDesc": "Never ask",
      "readOnly": "Read Only",
      "readOnlyDesc": "Only read files",
      "workspaceWrite": "Workspace Write",
      "workspaceWriteDesc": "Can write to workspace",
      "fullAccess": "Full Access",
      "fullAccessDesc": "Full system access",
      "webSearch": {
        "title": "Web Search",
        "descBefore": "Configure the built-in",
        "descAfter": "tool. DeepSeek first keeps the system aligned with DeepSeek, while SearXNG can act as a full-web bridge.",
        "enableTitle": "Enable web_search",
        "enableDescBefore": "When disabled, only",
        "enableDescAfter": "remains registered.",
        "providerTitle": "Provider",
        "providerDesc": "DeepSeek first uses the configured DeepSeek key, then falls back to SearXNG and DuckDuckGo when available.",
        "searxngUrlTitle": "SearXNG URL",
        "searxngUrlDescBefore": "Optional bridge base URL, for example",
        "saveFailed": "web_search settings save failed",
        "providers": {
          "deepseekFirst": "DeepSeek first",
          "deepseekFirstDesc": "DeepSeek server web-search first, then configured bridges",
          "searxng": "SearXNG bridge",
          "searxngDesc": "Use your SearXNG full-web bridge, then DuckDuckGo",
          "duckduckgo": "DuckDuckGo",
          "duckduckgoDesc": "Use keyless DuckDuckGo HTML as the final fallback"
        }
      },
      "responses": {
        "title": "DeepSeek Responses parameters",
        "desc": "All generation requests use the Responses API; parameters are validated and their effective status is recorded centrally.",
        "creativity": "Creativity",
        "creativityDesc": "Controls the default temperature; internal code and classification calls keep backend output limits.",
        "scenes": {
          "code": "Write code",
          "email": "Write email",
          "analysis": "Analyze",
          "creative": "Creative writing"
        },
        "advanced": "Advanced parameters",
        "advancedDesc": "temperature · top_p · output limit · reasoning · text.format",
        "advancedMeta": "6 fields",
        "temperature": "Temperature",
        "topP": "Top P",
        "maxOutputTokens": "Max output tokens",
        "topLogprobs": "Top logprobs",
        "reasoningEffort": "Reasoning effort",
        "textFormat": "Text format",
        "providerDefault": "Provider default",
        "developer": "Developer Responses JSON",
        "developerDesc": "Strict validation rejects unknown and system-managed fields and warns about ineffective fields.",
        "developerMeta": "JSON",
        "validateSave": "Validate and save",
        "saved": "Responses JSON saved",
        "saveFailed": "Unable to save Responses settings: {{error}}",
        "ineffective": "Saved but ineffective for DeepSeek: {{fields}}",
        "previewTitle": "Effective request preview",
        "previewDesc": "Shows the real merge order across system fields, developer JSON, advanced settings, scenes, and creativity.",
        "previewReady": "Responses",
        "requestTitle": "Next request skeleton"
      }
    },
    "git": {
      "title": "Git",
      "branchPrefix": "Branch Prefix",
      "branchPrefixDesc": "Prefix for branches created by Codex",
      "prMergeMethod": "PR Merge Method",
      "prMergeMethodDesc": "Method to use when merging PRs",
      "merge": "Merge",
      "squash": "Squash",
      "showPrIcon": "Show PR Icon",
      "showPrIconDesc": "Show the PR icon on the chat view",
      "alwaysForcePush": "Always Force Push",
      "forcePushDesc": "Always force push when updating branches",
      "draftPr": "Draft PR",
      "draftPrDesc": "Create PRs as drafts by default",
      "autoDelete": "Auto Delete",
      "autoDeleteDesc": "Automatically delete branches when merged",
      "autoDeleteLimit": "Auto Delete Limit",
      "autoDeleteLimitDesc": "Number of merged branches to keep",
      "commitInstructions": "Commit Instructions",
      "commitInstructionsDesc": "Extra instructions for generating commit messages",
      "save": "Save",
      "commitInstructionsPlaceholder": "Add commit instructions...",
      "prInstructions": "PR Instructions",
      "prInstructionsDesc": "Extra instructions for generating PR descriptions",
      "prInstructionsPlaceholder": "Add PR instructions..."
    },
    "browser": {
      "title": "Browser",
      "desc1": "Codex can browse the web and interact with pages.",
      "desc2": "Learn more.",
      "desc3": "",
      "allowControl": "Allow Control",
      "dataTitle": "Data",
      "browsingData": "Browsing Data",
      "clearDataDesc": "Clear cookies, website data, and cached images",
      "clearAllData": "Clear All Data",
      "cookie": "Cookies",
      "deleteCookie": "Delete Cookies",
      "websiteData": "Website Data",
      "deleteWebsiteData": "Delete Website Data",
      "cachedImages": "Cached Images",
      "deleteCachedImages": "Delete Cached Images",
      "screenshots": "Screenshots",
      "screenshotsDesc": "Include screenshots of the browser in chat history",
      "alwaysInclude": "Always Include",
      "onlySelected": "Only Selected",
      "permissions": "Permissions",
      "approval": "Approval",
      "approvalDesc": "Set how Codex asks for approval to browse domains.",
      "learnMore": "Learn more.",
      "alwaysAsk": "Always Ask",
      "alwaysAllow": "Always Allow",
      "askBeforeOpen": "Ask before opening new domains",
      "allowWithoutAsking": "Allow browsing without asking",
      "highRisk": "High Risk",
      "blockedDomains": "Blocked Domains",
      "blockedDesc": "Domains Codex is not allowed to browse",
      "add": "Add",
      "noBlocked": "No blocked domains",
      "allowedDomains": "Allowed Domains",
      "allowedDesc": "Domains Codex is allowed to browse without asking",
      "noAllowed": "No allowed domains"
    },
    "connections": {
      "title": "Connections",
      "desc1": "Connect Codex to external services.",
      "desc2": "Learn more.",
      "desc3": "",
      "connected": "Connected",
      "connecting": "Checking...",
      "disconnected": "Not checked",
      "error": "Offline",
      "available": "Available",
      "notConnected": "Not connected",
      "configure": "Configure",
      "connect": "Connect",
      "disconnect": "Disconnect",
      "noConnections": "No connections yet",
      "sshConnections": "SSH Connections",
      "sshDesc": "Connect to remote servers via SSH",
      "add": "Add",
      "addSsh": "Add SSH Connection",
      "addManually": "Add manually",
      "displayName": "Display Name",
      "hostname": "Hostname",
      "hostPlaceholder": "e.g. user@example.com",
      "sshPort": "SSH Port",
      "optional": "(optional)",
      "username": "Username",
      "password": "Password",
      "noAuth": "No Auth",
      "identityFile": "Identity File",
      "identityFilePath": "Identity File Path",
      "selectIdentityFile": "Choose an identity file",
      "browse": "Browse",
      "testing": "Testing...",
      "test": "Test",
      "testOk": "Connection successful",
      "testFailed": "Connection failed",
      "online": "Online",
      "offline": "Offline",
      "checking": "Checking...",
      "unknown": "Not checked",
      "edit": "Edit",
      "delete": "Delete",
      "cancel": "Cancel",
      "save": "Save",
      "errors": {
        "generic": "Unable to connect. Please check the SSH configuration.",
        "refused": "Connection refused. Check that the host and port are correct, and that the SSH service is running.",
        "timeout": "Connection timed out. Check the network, firewall, host, and port.",
        "auth": "Authentication failed. Check the username, password, or key file.",
        "resolve": "Host could not be resolved. Check the host name or IP address.",
        "unreachable": "Host is unreachable. Check the network route, firewall, and server status."
      }
    },
    "env": {
      "title": "Environment Variables",
      "desc1": "Manage environment variables for your projects.",
      "desc2": "Learn more.",
      "desc3": "",
      "add": "Add Variable",
      "key": "Key",
      "value": "Value",
      "save": "Save",
      "delete": "Delete",
      "edit": "Edit",
      "noVars": "No environment variables set",
      "back": "Back",
      "project": "Project",
      "envDetails": "Environment Details",
      "noLocalEnv": "No local environment configured",
      "createLocalEnv": "Create Local Environment",
      "localEnv": "Local Environment",
      "name": "Name",
      "setupScript": "Setup Script",
      "setupScriptDesc": "Script to initialize the environment",
      "default": "Default",
      "variables": "Variables",
      "cleanupScript": "Cleanup Script",
      "cleanupScriptDesc": "Script to clean up the environment",
      "actions": "Actions",
      "actionsDesc": "Custom actions for this environment",
      "addAction": "Add Action",
      "addActionDesc": "Create custom actions like build, test, or deploy",
      "learnMore": "Learn more.",
      "selectProject": "Select Project",
      "addProject": "Add Project"
    },
    "archive": {
      "title": "Archive",
      "deleteAll": "Delete All",
      "unarchive": "Unarchive",
      "noArchivedChats": "No archived conversations",
      "untitled": "Untitled conversation",
      "unknownProject": "Unknown project",
      "chatUnarchived": "Conversation unarchived",
      "archiveDeleted": "Removed from archive",
      "archiveCleared": "Archived conversations cleared",
      "loadFailed": "Failed to load archived conversations",
      "actionFailed": "Action failed"
    },
    "computer": {
      "title": "Computer Control",
      "desc": "Allow Codex to control your computer.",
      "control": "Control",
      "extensionNotConnected": "Extension not connected",
      "install": "Install",
      "alwaysAllowed": "Always Allowed",
      "none": "None",
      "installChrome": "Install Chrome Integration",
      "close": "Close",
      "developedBy": "Developed by OpenAI",
      "providedBy": "Provided by: OpenAI",
      "category": "Category: Browser Integration",
      "about": "About",
      "includes": "Includes",
      "browserExtension": "Browser Extension",
      "skills": "Skills",
      "features": "Features"
    }
  },
  "titleBar": {
    "file": "File",
    "edit": "Edit",
    "view": "View",
    "window": "Window",
    "help": "Help",
    "update": "Update",
    "downloadUpdate": "Download update",
    "downloadingUpdate": "Downloading",
    "downloadingUpdatePercent": "Downloading {{percent}}%",
    "downloadingUpdateMB": "{{mb}} MB",
    "updateReady": "Update downloaded. It will install automatically when the app closes.",
    "updateDownloadFailed": "Failed to download update. Please try again.",
    "updateInstallFailed": "Failed to install update. Please close the app and try again."
  },
  "startView": {
    "greeting": "What should we do in {{projectName}}?",
    "greetingNoProject": "What should we do?",
    "placeholder": "Ask anything...",
    "searchProject": "Search projects",
    "addNewProject": "Add new project",
    "noProject": "No project"
  },
  "composer": {
    "attachments": {
      "limit": "Maximum {{count}} attachments allowed",
      "recognizing": "Analyzing",
      "processing": "Processing",
      "failed": "Processing failed",
      "readFailed": "Failed to read file",
      "text": "Text",
      "file": "File",
      "removeImage": "Remove image: {{name}}",
      "removeAttachment": "Remove attachment: {{name}}",
      "previewImage": "Preview image",
      "previewNamedImage": "Preview image: {{name}}",
      "closePreview": "Close image preview",
      "statusLabel": "{{name}}: {{status}}"
    },
    "placeholder": "Ask anything...",
    "selectModel": "Select Model",
    "noModels": "No models available — enter an API key at login first",
    "modelSwitched": "Switched model: {{model}}",
    "modelSwitchFailed": "Failed to switch model",
    "approvalSwitchFailed": "Failed to change permission level",
    "selectThinking": "Thinking",
    "thinkingSimple": "Simple",
    "thinkingMedium": "Medium",
    "thinkingDeep": "Deep",
    "effortFaster": "Faster",
    "effortSmarter": "Deeper",
    "thinkingSwitchFailed": "Failed to change thinking level",
    "send": "Send",
    "stop": "Stop",
    "stopping": "Stopping…",
    "cancelFailed": "Could not stop the run: {{error}}",
    "cancelNotFound": "The run has ended or is no longer available",
    "systemCommands": "System Commands",
    "skillsGroup": "Skills",
    "models": {
      "lingsiAPI": "Lingsi API",
      "lingsiAPI_5_5_zh": "5.5 ZH"
    },
    "defaultPermission": "Ask to Approve",
    "defaultPermissionDesc": "Always ask when editing external files or using the internet",
    "autoReview": "Auto Review",
    "autoReviewDesc": "Only ask for approval on detected risky actions",
    "fullAccess": "Full Access",
    "fullAccessDesc": "Unrestricted access to the internet and any file on your computer",
    "addMenu": {
      "addFiles": "Add files",
      "addFilesDesc": "Choose from device",
      "hoverLabel": "Add files",
      "tooltipDesc": "Pick files from your device, or paste images and text"
    }
  },
  "balanceChip": {
    "label": "Balance",
    "loading": "Loading balance…",
    "unavailable": "Balance unavailable",
    "notSet": "Set API key to see balance",
    "granted": "Granted",
    "toppedUp": "Topped up",
    "depleted": "Balance is depleted; top up to continue",
    "refreshHint": "Click to refresh"
  },
  "toolCard": {
    "running": "Running",
    "ok": "Done",
    "error": "Failed",
    "blocked": "Blocked",
    "arguments": "Arguments",
    "result": "Result",
    "errorDetail": "Error"
  },
  "approvalDialog": {
    "risk": "{{risk}} Risk",
    "pending": "{{count}} pending",
    "requestTool": "Request to execute tool: ",
    "approve": "Approve",
    "approveSession": "Allow for this session",
    "approvePrefix": "Always allow \"{{prefix}}\"",
    "grantRefused": "Could not save that allowance: {{error}}",
    "reject": "Reject"
  },
  "askUserDialog": {
    "of": "{{current}} of {{total}}",
    "previous": "Previous question",
    "next": "Next question",
    "multiHint": "(multi-select)",
    "recommended": "Recommended",
    "pending": "{{count}} more to answer",
    "other": "Other",
    "otherPlaceholder": "Type your answer",
    "submit": "Submit",
    "skip": "Skip"
  },
  "automationView": {
    "viewTemplates": "View Templates",
    "createViaChat": "Create via Chat",
    "title": "Automation",
    "subtitle": "Run chats on schedule or on-demand.",
    "learnMore": "Learn more",
    "createFirst": "Create your first automation",
    "dailyBriefing": "Daily Briefing",
    "weeklyReview": "Weekly Review",
    "projectMonitoring": "Project Monitoring"
  },
  "chatView": {
    "tools": {
      "files": "Files",
      "filesDesc": "Browse project files",
      "chat": "Side Chat",
      "chatDesc": "Start a side chat",
      "browser": "Browser",
      "browserDesc": "Open website",
      "terminal": "Terminal",
      "terminalDesc": "Launch interactive shell",
      "project_map": "Project Map",
      "project_mapDesc": "Inspect module relationships",
      "recording": "Recording",
      "recordingDesc": "Meeting recording & transcription",
      "file_preview": "File Preview",
      "file_previewDesc": "Preview office documents",
      "canvas": "Studio Canvas",
      "canvasDesc": "Open extensible workspace"
    },
    "chat": "Chat",
    "pinChat": "Pin Chat",
    "renameChat": "Rename Chat",
    "archiveChat": "Archive Chat",
    "openSideChat": "Open Side Chat",
    "copy": "Copy",
    "exportJson": "Export as JSON",
    "branch": "Branch",
    "rewind": "Rewind to…",
    "noRewindPoints": "No timeline points yet",
    "addAutomation": "Add Automation...",
    "openInNewWindow": "Open in New Window",
    "requestFollowUp": "Request follow-up changes",
    "working": "Working…",
    "assistant": "Assistant",
    "reasoning": "Reasoning",
    "processSteps": "Process · {{count}} tool call(s)",
    "processStepsNoTools": "Process",
    "clickToExpand": "click to expand",
    "tokensInput": "in",
    "tokensOutput": "out",
    "tokensTotal": "total",
    "tokensCacheHit": "cached",
    "totalDuration": "total time",
    "turnCost": "cost this turn",
    "copyAnswer": "Copy",
    "addWorktree": "Add worktree",
    "environmentInfo": "Environment",
    "environmentSettings": "Environment settings",
    "changes": "Changes",
    "local": "Local",
    "localMode": "Local mode",
    "remoteMode": "Remote mode",
    "existingSshConnections": "Existing SSH connections",
    "noSshConnections": "No SSH connections",
    "noProject": "No project",
    "noGitRepository": "No Git repository",
    "commitOrPush": "Commit or push",
    "githubCliAvailable": "GitHub CLI available",
    "githubCliUnavailable": "GitHub CLI unavailable",
    "loading": "Loading...",
    "output": "Output",
    "noOutput": "No output yet",
    "outputCreated": "created",
    "outputEdited": "edited",
    "sources": "Sources",
    "noSources": "No sources yet",
    "recommended": "Recommended"
  },
  "git": {
    "title": "Git",
    "close": "Close Git",
    "closeChanges": "Close Git Changes",
    "branchChangedSummary": "{{branch}} · {{count}} changed",
    "tabs": {
      "changes": "Changes",
      "branches": "Branches",
      "push": "Upload",
      "log": "History"
    },
    "changesTitle": "Git Changes",
    "filesCount": "{{count}} files",
    "groups": {
      "conflicted": "Conflicts",
      "staged": "Staged",
      "unstaged": "Unstaged",
      "untracked": "Untracked"
    },
    "loadingChanges": "Loading Git changes...",
    "workingTreeClean": "Working tree clean",
    "draftedFromStaged": "Drafted from staged changes",
    "draftedFromWorkingTree": "Drafted from working tree",
    "commitMessage": "Commit message",
    "draftMessage": "Draft message",
    "draftingMessage": "Drafting...",
    "committing": "Committing...",
    "commitStagedFiles": "Commit {{count}} staged file(s)",
    "commitMessageRequired": "Commit message must not be empty.",
    "stageBeforeCommit": "Stage at least one file before committing.",
    "selectFilesBeforeStage": "Select at least one file before staging.",
    "selectStagedFilesBeforeUnstage": "Select at least one staged file before unstaging.",
    "selectStagedFilesBeforeCommit": "Select at least one staged file before committing.",
    "selectedFileHasUnstagedChanges": "One selected file also has unstaged changes. Stage or discard those changes before committing selected files.",
    "commitConfirm": "Create a local commit with {{count}} staged file(s)?",
    "selectAll": "Select all",
    "deselectAll": "Deselect all",
    "selectFileToViewDiff": "Select a file to view diff.",
    "operationFailed": "{{action}} failed.",
    "operationCompleted": "{{action}} completed.",
    "actions": {
      "stage": "Stage",
      "unstage": "Unstage",
      "commit": "Commit",
      "checkout": "Checkout",
      "createBranch": "Create branch",
      "fetch": "Fetch"
    },
    "stage": "Stage",
    "unstage": "Unstage",
    "stageSelected": "Stage {{count}}",
    "unstageSelected": "Unstage {{count}}",
    "applyHunkFailed": "Failed to apply hunk.",
    "truncated": "truncated",
    "loadingDiff": "Loading diff...",
    "noUntrackedDiff": "No text diff is available for this untracked file.",
    "noDisplayableDiff": "This file has no displayable diff.",
    "diffMode": {
      "unified": "Unified",
      "split": "Split"
    },
    "diffColumns": {
      "old": "Old",
      "before": "Before",
      "new": "New",
      "after": "After"
    },
    "stageHunk": "Stage hunk",
    "unstageHunk": "Unstage hunk",
    "branchAlreadyCheckedOut": "Branch is already checked out in {{path}}",
    "switchBranchConfirm": "Switch to {{branch}}? Git will stop the checkout if local changes would be overwritten.",
    "newBranchName": "New branch name",
    "searchBranches": "Search branches",
    "detachedHead": "Detached HEAD",
    "rebasing": "Rebasing",
    "merging": "Merging",
    "changedFilesCount": "{{count}} changed file(s)",
    "loadingState": "Loading Git state...",
    "noBranchesFound": "No branches found",
    "localBranches": "Local branches",
    "remoteBranches": "Remote branches",
    "fetch": "Fetch",
    "fetching": "Fetching...",
    "aheadCount": "ahead {{count}}",
    "behindCount": "behind {{count}}",
    "worktreeAt": "Worktree: {{path}}",
    "createBranchDialog": {
      "title": "Create branch",
      "cancel": "Cancel",
      "placeholder": "Enter branch name"
    },
    "pushPanel": {
      "title": "Push",
      "refresh": "Refresh",
      "loadingPreview": "Loading push preview...",
      "currentBranch": "Current branch",
      "upstream": "Upstream",
      "target": "Target",
      "aheadBehind": "Ahead / Behind",
      "notConfigured": "Not configured",
      "confirmHighRisk": "Push includes {{count}} high-risk finding(s). Review them before pushing. Continue anyway?",
      "confirmPush": "Push {{count}} commit(s) to {{target}}?",
      "pushFailed": "Push failed",
      "pushing": "Pushing...",
      "pushCommits": "Push {{count}} commit(s)",
      "pushCompleted": "Push completed.",
      "commitsTitle": "Commits to push",
      "commitsCount": "{{count}} commits",
      "noOutgoingCommits": "No outgoing commits.",
      "noPreview": "No push preview available.",
      "riskScanTitle": "Push risk scan",
      "riskSummary": "{{files}} file(s), {{findings}} finding(s)",
      "noLocalRisks": "No local risks found.",
      "severity": {
        "high": "High",
        "medium": "Medium",
        "low": "Low"
      },
      "reasons": {
        "nothingToPush": "nothing to push",
        "notGitRepository": "not a git repository",
        "noUpstream": "no upstream branch is configured",
        "upstreamAhead": "upstream has new commits; update before pushing",
        "detachedHead": "detached HEAD cannot be pushed from this view",
        "finishMerge": "finish or abort the active merge before pushing",
        "finishRebase": "finish or abort the active rebase before pushing"
      }
    },
    "logPanel": {
      "title": "History",
      "commitsCount": "{{count}} commits",
      "loadingLog": "Loading Git log...",
      "noCommitsFound": "No commits found.",
      "newBranchName": "New branch name",
      "branchCreated": "Created and checked out {{name}}",
      "createBranch": "Create branch",
      "noFileList": "This commit has no file list.",
      "loadingCommitDiff": "Loading commit diff...",
      "selectFile": "Select a file to inspect this commit diff.",
      "selectCommit": "Select a commit.",
      "mergeCount": "merge {{count}}",
      "filesCount": "{{count}} files"
    },
    "projectsPanel": {
      "title": "Branch Manager",
      "gitProjectsCount": "{{count}} Git projects",
      "commitSelected": "Commit selected",
      "commitAndPushSelected": "Commit and push selected",
      "handleIndividually": "Handle individually",
      "fetchingAll": "Fetching...",
      "fetchAll": "Fetch all",
      "refresh": "Refresh",
      "loadingBranches": "Loading Git branch status...",
      "noOpenGitProjects": "No opened Git projects detected.",
      "branchGroupsCount": "{{count}} branch groups",
      "projectCount": "{{count}} projects",
      "commit": "Commit",
      "commitAndPush": "Commit and push",
      "updating": "Updating...",
      "update": "Update",
      "pushing": "Pushing...",
      "push": "Push",
      "current": "Current",
      "selectProject": "Select {{path}}",
      "metrics": {
        "changed": "changed",
        "ahead": "ahead",
        "behind": "behind"
      },
      "badges": {
        "merge": "merge",
        "rebase": "rebase",
        "detached": "detached"
      },
      "baseRef": "Base ref",
      "targetRef": "Target ref",
      "compare": "Compare",
      "comparing": "Comparing...",
      "mergeBase": "base {{hash}}",
      "targetOnly": "Target only",
      "noTargetOnlyCommits": "No target-only commits",
      "baseOnly": "Base only",
      "noBaseOnlyCommits": "No base-only commits",
      "changedFiles": "Changed files",
      "noFileChanges": "No file changes",
      "loadingDiff": "Loading diff...",
      "noDiffForFile": "No diff for this file.",
      "selectFileToViewDiff": "Select a file to view diff.",
      "moreCount": "+{{count}} more",
      "batchCommit": "Batch commit",
      "batchCommitAndPush": "Batch commit and push",
      "batchSummary": "{{projects}} projects, {{files}} changed files",
      "closeBatchCommit": "Close batch commit",
      "autoStageAll": "Automatically stage all changes in each project",
      "blockedProjectsCount": "{{count}} projects are currently blocked. Reasons will be shown per project after execution.",
      "running": "Running...",
      "refreshPreview": "Refresh preview",
      "projectPreview": "Project preview",
      "loading": "Loading...",
      "noTargets": "No targets.",
      "previewChanged": "changed {{count}}",
      "previewStaged": "staged {{count}}",
      "singleProjectCommitNote": "Per-project commit note, optional",
      "executionResults": "Execution results",
      "ok": "OK",
      "failed": "Failed",
      "batchPushRiskScan": "Batch push risk scan",
      "targetCount": "{{count}} target(s)",
      "severityCount": {
        "high": "high {{count}}",
        "medium": "medium {{count}}",
        "low": "low {{count}}"
      },
      "noLocalRiskFindingsBeforePush": "No local risk findings before pushing {{branch}}.",
      "moreFindings": "+{{count}} more finding(s)",
      "confirm": {
        "pushRiskHigh": "Push risk scan found {{findings}} high-risk finding(s) across {{targets}} target(s). Continue anyway?",
        "pushRiskAny": "Push risk scan found {{findings}} finding(s) across {{targets}} target(s). Continue?",
        "pushBranchGroup": "Push {{commits}} outgoing commit(s) from {{targets}} project target(s) on {{branch}}?",
        "fetchRepos": "Fetch {{count}} unique repo(s)?",
        "updateBranchGroup": "Fast-forward update {{count}} project target(s) on {{branch}}?",
        "continuePushingHighRisk": "Push risk scan found {{findings}} high-risk finding(s). Continue pushing {{targets}} target(s)?",
        "continuePushingRisk": "Push risk scan found {{findings}} finding(s). Continue pushing {{targets}} target(s)?",
        "pushCommittedTargets": "Push {{count}} committed project target(s)?",
        "commitThenPushRiskScan": "Commit {{count}} project target(s), then run push risk scan?"
      },
      "errors": {
        "noProjectTargetsSelected": "No project targets selected.",
        "noUniqueProjectTargets": "No unique project targets in this branch group.",
        "noOutgoingCommitsToPush": "No outgoing commits to push.",
        "pushRiskScanUnavailable": "Push risk scan could not run.",
        "onePushFailed": "One push failed.",
        "noGitRepositoriesToFetch": "No Git repositories to fetch.",
        "oneFetchFailed": "One fetch failed.",
        "noProjectTargetsUpdatable": "No project targets in this branch group can be updated.",
        "noBehindCommits": "No behind commits to update.",
        "oneUpdateFailed": "One update failed.",
        "noProjectTargetInRepositoryGroup": "No project target in this repository group."
      },
      "results": {
        "pushedTargets": "Pushed {{count}} target(s).",
        "fetchedRepos": "Fetched {{count}} repo(s).",
        "updatedTargets": "Updated {{count}} target(s).",
        "commitPushRiskScanFailed": "Committed, but push risk scan failed: {{reason}}",
        "commitPushCancelledAfterRiskScan": "Committed, push cancelled after risk scan.",
        "commitPushCancelled": "Committed, push cancelled.",
        "committedAndPushed": "Committed and pushed",
        "committedButPushFailed": "Committed, but push failed: {{message}}"
      },
      "reasons": {
        "notGitRepository": "not a git repository",
        "nothingToPush": "nothing to push",
        "noUpstream": "no upstream branch is configured",
        "upstreamAhead": "upstream has new commits; update before pushing",
        "detachedHeadPush": "detached HEAD cannot be pushed from this view",
        "finishMergePush": "finish or abort the active merge before pushing",
        "finishRebasePush": "finish or abort the active rebase before pushing",
        "chooseBaseRef": "choose a base ref to compare",
        "chooseTargetRef": "choose a target ref to compare",
        "baseRefMissing": "base ref does not exist",
        "targetRefMissing": "target ref does not exist",
        "detachedHeadUpdate": "Detached HEAD cannot be updated from this view.",
        "finishMergeUpdate": "Finish or abort the active merge before updating.",
        "finishRebaseUpdate": "Finish or abort the active rebase before updating.",
        "commitOrStashBeforeUpdate": "Commit or stash local changes before updating.",
        "noUpstreamUpdate": "No upstream branch is configured.",
        "finishMergeCommit": "finish or abort the active merge before committing",
        "finishRebaseCommit": "finish or abort the active rebase before committing",
        "noGitChangesToCommit": "no Git changes to commit",
        "noStagedFiles": "no staged files; enable stage all or stage files first"
      }
    },
    "createAndCheckoutBranch": "Create and checkout new branch...",
    "openGit": "Open Git",
    "openGitUnavailable": "Git panel is unavailable in the current view."
  },
  "searchModal": {
    "searchChats": "Search chats",
    "recentChats": "Recent chats"
  },
  "skillsView": {
    "origin": {
      "installed": "Installed",
      "workspace": "Workspace",
      "user": "User",
      "builtIn": "Built-in"
    },
    "title": "Skills",
    "subtitle1": "The system automatically discovers skills from ",
    "subtitle2": ", disclosed progressively on demand.",
    "refresh": "Refresh",
    "searchPlaceholder": "Search skills or trigger phrases",
    "discoveredCount": "Discovered {{count}} skills",
    "clickToView": "Click to view skill body (Level 2 disclosure); trigger phrases are used for passive activation.",
    "loading": "Loading...",
    "noSkills": "No skills. Place SKILL.md in .deepagent/skills/<name>/ to be automatically discovered.",
    "uninstall": "Uninstall",
    "recommended": "Recommended",
    "add": "Add",
    "triggerPhrases": "Trigger phrases ({{count}})",
    "body": "Body (Level 2 disclosure)",
    "noBody": "(No body)",
    "tab_market": "Market",
    "tab_installed": "Installed",
    "market_loading": "Loading skills...",
    "market_no_results": "No skills found.",
    "market_load_more": "Load More",
    "market_already_installed": "Installed",
    "market_install_button": "Install",
    "market_provider_config": "Configure SkillsMP API key",
    "market_search_placeholder": "Search skills..."
  },
  "skillsmp": {
    "title": "SkillsMP Marketplace",
    "description": "Pulls 1.7M+ skills from skillsmp.com.",
    "api_key_label": "API Key (optional)",
    "api_key_placeholder": "sk_live_...",
    "save": "Save",
    "saving": "Saving...",
    "saved": "Saved",
    "test_connection": "Test connection",
    "testing": "Testing...",
    "test_ok": "Connection OK",
    "test_failed": "Connection failed",
    "daily_remaining": "{{n}} requests/day remaining",
    "clear_key": "Clear key",
    "clearing": "Clearing...",
    "cleared": "Cleared",
    "clear_key_confirm": "Remove your custom API key? Will fall back to the built-in key.",
    "using_user_key": "Using custom key",
    "using_builtin_key": "Using built-in key (500/day)",
    "using_anonymous": "Anonymous (50/day)",
    "get_api_key": "Get API Key",
    "view_docs": "View docs",
    "empty_key_error": "Please enter an API key.",
    "close": "Close"
  },
  "skillRisk": {
    "category": {
      "shell": "Shell",
      "execution": "Execution",
      "network": "Network",
      "credential": "Credential",
      "filesystem": "Filesystem",
      "exfiltration": "Exfiltration"
    },
    "severity": {
      "safe": "Safe",
      "warning": "Warning",
      "danger": "Danger"
    }
  },
  "skillInstallDialog": {
    "title": "Install Skill: {{name}}",
    "scanning": "Downloading and scanning...",
    "ai_review": "AI Security Review",
    "ai_review_running": "Reviewing...",
    "ai_review_passed": "PASSED",
    "ai_review_failed": "FAILED",
    "ai_review_show_details": "Show details",
    "ai_review_hide_details": "Hide details",
    "ai_review_error": "AI review failed: {{error}}",
    "static_scan": "Security Scan",
    "no_risks": "No risks detected.",
    "risk_count": "{{danger}} Danger · {{warning}} Warning · {{safe}} Safe",
    "files": "Files",
    "files_summary": "{{count}} files, {{size}}",
    "skill_md_preview": "SKILL.md preview",
    "cancel": "Cancel",
    "install_safe": "Install Safe",
    "install_warning": "Install",
    "install_danger": "Install Anyway",
    "installing": "Installing...",
    "install_failed": "Install failed: {{error}}"
  },
  "skillsPage": {
    "market": "Market",
    "installed": "Installed",
    "marketDescription": "Discover and install new skills from the community marketplace.",
    "skillCount": "{{count}} skills",
    "searchPlaceholder": "Search skills or descriptions...",
    "install": "Install",
    "installing": "Installing...",
    "installSafe": "Install Safe",
    "installAnyway": "Install Anyway",
    "alreadyInstalled": "Installed",
    "scanning": "Scanning...",
    "agentReview": "AI Security Review",
    "agentReviewing": "Reviewing...",
    "agentReviewPassed": "Passed AI security review",
    "agentReviewFailed": "AI review found risks",
    "securityReview": "Security Review",
    "noRisks": "No obvious risks detected.",
    "filesLabel": "Files",
    "filesCount": "{{count}} files, {{size}} total",
    "riskSafe": "Safe",
    "riskWarning": "Warning",
    "riskDanger": "Danger",
    "riskShellCommand": "Shell Command",
    "riskExecution": "Code Execution",
    "riskNetwork": "Network Access",
    "riskCredential": "Credential Access",
    "riskFileSystem": "Filesystem",
    "riskExfiltration": "Data Exfiltration"
  },
  "skillsmarket": {
    "title": "Skill Market",
    "skillsmpDesc": "Powered by skillsmp.com — 1.7M+ community skills.",
    "apiKey": "API Key",
    "apiKeyPlaceholder": "sk_live_...",
    "getApiKey": "Get API Key",
    "openDocs": "View Docs",
    "test": "Test Connection",
    "testOk": "Connected. {{remaining}}/day remaining",
    "testFail": "Connection failed: {{error}}",
    "usingBuiltinKey": "Using built-in key (500/day)",
    "usingUserKey": "Using custom key"
  },
  "knowledgeView": {
    "title": "Knowledge Base",
    "subtitle1": "Accumulated experience saved as Markdown notes in ",
    "subtitle2": ", auto-injected into chat when relevant.",
    "passive": "Auto-inject",
    "passiveHint": "When on, relevant knowledge is automatically added to each chat turn.",
    "passiveOn": "Auto-injection enabled",
    "passiveOff": "Auto-injection disabled",
    "autoCapture": "Auto-capture",
    "autoCaptureHint": "When on, every substantive session is reviewed at end-of-run: recovery arcs auto-save, and the model decides whether anything else is worth saving (nothing is saved for trivial chatter).",
    "autoCaptureOn": "Auto-capture enabled",
    "autoCaptureOff": "Auto-capture disabled",
    "graphView": "Graph",
    "listView": "List",
    "tagNode": "Tag",
    "noDrafts": "No pending drafts.",
    "draftsTitle": "Pending drafts ({{count}})",
    "draftsHint": "Drafts that still need manual review. Auto-captured failure recovery lessons are saved directly to the knowledge base.",
    "accept": "Accept",
    "discard": "Discard",
    "draftAccepted": "Draft accepted",
    "draftDiscarded": "Draft discarded",
    "refresh": "Refresh",
    "searchPlaceholder": "Search title, body, or tags",
    "new": "New entry",
    "count": "{{count}} entries",
    "clickToView": "Click an entry to view it; relevant entries are also auto-injected into chat.",
    "loading": "Loading...",
    "empty": "No knowledge yet. Add entries here; with auto-capture on, recovered failures are saved automatically. The agent can also save notes with knowledge_write.",
    "delete": "Delete",
    "edit": "Edit",
    "close": "Close",
    "newTitle": "New entry",
    "editTitle": "Edit entry",
    "fieldTitle": "Title",
    "fieldTitlePlaceholder": "A specific, searchable title",
    "fieldKind": "Kind",
    "fieldScope": "Scope",
    "fieldTags": "Tags",
    "fieldTagsPlaceholder": "comma, separated, tags",
    "fieldBody": "Body",
    "fieldBodyPlaceholder": "What the pitfall/fix/command/config is, and how to apply it.",
    "cancel": "Cancel",
    "save": "Save",
    "saved": "Entry saved",
    "deleted": "Entry deleted",
    "titleRequired": "Title is required",
    "bodyRequired": "Body is required",
    "saveFailed": "Save failed: {{error}}",
    "deleteFailed": "Delete failed: {{error}}",
    "loadFailed": "Failed to load knowledge: {{error}}",
    "kind": {
      "pitfall": "Pitfall",
      "solution": "Solution",
      "command": "Command",
      "config": "Config",
      "note": "Note",
      "memory": "Memory"
    },
    "scope": {
      "project": "Project",
      "global": "Global"
    }
  },
  "plugins": {
    "files": {
      "specTitle": "CPI Trinity Auth Spec",
      "specDesc": "This document is intended for subsystem developers to describe how to connect to Trinity unified auth.",
      "corePrinciples": "Core Principles",
      "p1": "Trinity issues: User logs in at Trinity portal, receives JWT Token",
      "p2": "Subsystem verifies: Subsystem backend only needs to verify Token signature, no need to maintain login logic",
      "p3": "Token passing: Pass Token via URL/IPC when jumping from portal, subsystem frontend stores and uses it",
      "tokenSpec": "Token Specification",
      "field": "Field",
      "value": "Value",
      "format": "Format",
      "filterPlaceholder": "Filter files..."
    },
    "terminal": {
      "copyright": "Copyright (C) Microsoft Corporation. All rights reserved.",
      "tryNew": "Install the latest PowerShell for new features and improvements! https://aka.ms/PSWindows"
    },
    "filePreview": {
      "choose": "Choose file",
      "loading": "Loading preview…",
      "empty": "Choose a file to preview it here",
      "unsupported": "Preview is not supported for this file type",
      "imageUnavailable": "Image could not be loaded",
      "noSheets": "No sheets found",
      "truncated": "Content truncated for preview",
      "truncatedRows": "(first rows shown)",
      "sendToChat": "Send to chat"
    },
    "recording": {
      "title": "Recording",
      "idle": "Ready to record",
      "recording": "Recording",
      "paused": "Paused",
      "transcribing": "Transcribing…",
      "done": "Done",
      "error": "Error",
      "start": "Start",
      "pause": "Pause",
      "resume": "Resume",
      "stop": "Stop",
      "transcribe": "Transcribe",
      "generateMinutes": "Generate minutes",
      "modelNeededTitle": "Speech model required",
      "modelNeededDesc": "Transcription needs the Whisper model. It downloads into the app's own folder and is verified before use.",
      "downloadModel": "Download model",
      "downloading": "Downloading…",
      "transcript": "Transcript",
      "noSpeech": "No speech detected",
      "minutes": "Meeting minutes",
      "exportWord": "Export Word",
      "exportedTo": "Saved to",
      "sendToChat": "Send to chat"
    }
  },
  "commandPalette": {
    "placeholder": "Type a command…",
    "noMatch": "No matching commands"
  },
  "chat": {
    "nativeWebSearchCompleted": "DeepSeek web search completed ({{count}} queries)",
    "nativeWebSearchStatus": "DeepSeek web search: {{status}}"
  },
  "general": {
    "languages": {
      "autoDetect": "Auto Detect",
      "sq": "Albanian (Albania)",
      "is": "Icelandic (Iceland)",
      "ka": "Georgian (Georgia)",
      "mk": "Macedonian (North Macedonia)",
      "mn": "Mongolian",
      "my": "Burmese (Myanmar)",
      "ja": "Japanese (Japan)",
      "so": "Somali (Somalia)",
      "hy": "Armenian (Armenia)",
      "zhTW": "Chinese (Taiwan)",
      "zhHK": "Chinese (Hong Kong)",
      "zhCN": "Chinese (Simplified)",
      "enUS": "English (US)"
    }
  }
}
