'use client';

/**
 * FF AI Agent — 主工作面（ChatGPT + Claude 风格重设计）
 *
 * 布局：
 *   ┌──────────┬─────────────────────────────────┐
 *   │ 会话侧栏  │  对话主区（max-w-3xl 居中）         │
 *   │ 260px    │  ・空态：hero 标题 + composer 同框 │
 *   │          │  ・非空：消息流 + 底部 sticky 输入 │
 *   └──────────┴─────────────────────────────────┘
 *     + 工件抽屉（slide-in 从右，artifacts.length>0 或显式打开时显示）
 *
 * 设计 token：
 *   - 主区底色 stone-50（off-white #fafaf9，比纯白更柔和）
 *   - composer rounded-3xl + 软阴影；助手消息裸文本无气泡；用户消息右侧胶囊
 *   - 暗色模式靠 dark: 变体（Tailwind v4 默认走 prefers-color-scheme）
 */

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { MarkdownContent } from '@/components/agent/MarkdownContent';
import { ToolUseCard, ToolResultCard } from '@/components/agent/ToolCard';
import { useTranslation } from '@/hooks/useTranslation';
import type { Locale } from '@/lib/i18n';
import {
  MessageSquarePlus,
  Globe,
  Send,
  Sparkles,
  Briefcase,
  FileCheck2,
  BookOpenText,
  Loader2,
  Trash2,
  Wrench,
  CheckCircle2,
  XCircle,
  Copy,
  Check,
  RefreshCw,
  X,
  PanelRightOpen,
  ArrowLeft,
  Folder,
  Plus,
  Brain,
  Bot,
  ChevronRight,
  ChevronDown,
  type LucideIcon,
} from 'lucide-react';
import Link from 'next/link';
import apiClient from '@/lib/api-client';
import {
  listAgentSessions,
  createAgentSession,
  getAgentSession,
  deleteAgentSession,
  postAgentMessageStream,
  listAgentArtifacts,
  rewindAgentSession,
  type AgentSession,
  type AgentMessage,
  type AgentArtifact,
} from '@/services/api/agent';
import {
  listAgentProjects,
  createAgentProject,
  deleteAgentProject,
  type AgentProject,
} from '@/services/api/agent-project';
import {
  listAgentMemories,
  createAgentMemory,
  deleteAgentMemory,
  MEMORY_CATEGORIES,
  type AgentMemory,
  type MemoryCategory,
} from '@/services/api/agent-memory';
import {
  listAgentPersonas,
  createAgentPersona,
  deleteAgentPersona,
  isSystemPersona,
  type AgentPersona,
} from '@/services/api/agent-persona';

type CardKey = 'projectQuery' | 'approvalSubmit' | 'knowledgeQA';

const CARD_ORDER: CardKey[] = ['projectQuery', 'approvalSubmit', 'knowledgeQA'];

const CARD_ICONS: Record<CardKey, LucideIcon> = {
  projectQuery: Briefcase,
  approvalSubmit: FileCheck2,
  knowledgeQA: BookOpenText,
};

export default function AgentPage() {
  const { t, locale, changeLocale } = useTranslation();

  const [sessions, setSessions] = useState<AgentSession[]>([]);
  const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
  const [messages, setMessages] = useState<AgentMessage[]>([]);
  const [composerValue, setComposerValue] = useState('');
  const [loadingSessions, setLoadingSessions] = useState(false);
  const [sending, setSending] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [streamingText, setStreamingText] = useState<string>('');
  const [artifacts, setArtifacts] = useState<AgentArtifact[]>([]);
  const [pendingAsk, setPendingAsk] = useState<{ question: string; options?: string[] } | null>(null);
  const [drawerOpen, setDrawerOpen] = useState(false);
  const [lastUserPrompt, setLastUserPrompt] = useState<string | null>(null);
  // ChatGPT-style 用户自创 folder
  const [projects, setProjects] = useState<AgentProject[]>([]);
  // null = 全部对话；string = 指定项目；'unassigned' = 未分组
  const [activeProjectFilter, setActiveProjectFilter] = useState<string | null>(null);
  // 用户记忆（Phase 1 仅 GLOBAL scope）
  const [memories, setMemories] = useState<AgentMemory[]>([]);
  // 智能体（系统预设 + 用户自创）
  const [personas, setPersonas] = useState<AgentPersona[]>([]);
  const [activePersonaId, setActivePersonaId] = useState<string | null>(null);
  // 当前正在跑的 turn —— 用于 Stop 按钮调后端 cancel + abort fetch
  const abortRef = useRef<AbortController | null>(null);
  const currentTurnIdRef = useRef<string | null>(null);

  const refreshSessions = useCallback(async () => {
    setLoadingSessions(true);
    try {
      const { items } = await listAgentSessions({ limit: 50 });
      setSessions(items);
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setLoadingSessions(false);
    }
  }, []);

  useEffect(() => {
    void refreshSessions();
  }, [refreshSessions]);

  const refreshProjects = useCallback(async () => {
    try {
      const { items } = await listAgentProjects();
      setProjects(items);
    } catch (err) {
      // 后端可能还没重启完，单独失败不阻 sessions
      console.warn('listAgentProjects failed', err);
    }
  }, []);

  useEffect(() => {
    void refreshProjects();
  }, [refreshProjects]);

  const handleCreateProject = async (name: string) => {
    try {
      const created = await createAgentProject({ name });
      setProjects((prev) => [created, ...prev]);
      setActiveProjectFilter(created.id);
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    }
  };

  const handleDeleteProject = async (id: string) => {
    try {
      await deleteAgentProject(id);
      setProjects((prev) => prev.filter((p) => p.id !== id));
      if (activeProjectFilter === id) setActiveProjectFilter(null);
      void refreshSessions(); // session.projectId 已被后端置 null
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    }
  };

  const refreshMemories = useCallback(async () => {
    try {
      const { items } = await listAgentMemories({ scope: 'GLOBAL' });
      setMemories(items);
    } catch (err) {
      console.warn('listAgentMemories failed', err);
    }
  }, []);

  useEffect(() => {
    void refreshMemories();
  }, [refreshMemories]);

  const handleCreateMemory = async (content: string, category: MemoryCategory) => {
    try {
      const created = await createAgentMemory({ content, scope: 'GLOBAL', category });
      setMemories((prev) => [created, ...prev]);
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    }
  };

  const handleDeleteMemory = async (id: string) => {
    try {
      await deleteAgentMemory(id);
      setMemories((prev) => prev.filter((m) => m.id !== id));
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    }
  };

  const refreshPersonas = useCallback(async () => {
    try {
      const { items } = await listAgentPersonas();
      setPersonas(items);
      // 默认选中 general 预设（首次进入有体感）
      if (!activePersonaId) {
        const general = items.find((p) => p.systemKey === 'general');
        if (general) setActivePersonaId(general.id);
      }
    } catch (err) {
      console.warn('listAgentPersonas failed', err);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    void refreshPersonas();
  }, [refreshPersonas]);

  const handleCreatePersona = async (input: { name: string; description?: string; instructions?: string }) => {
    try {
      const created = await createAgentPersona(input);
      setPersonas((prev) => [created, ...prev]);
      setActivePersonaId(created.id);
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    }
  };

  const handleDeletePersona = async (id: string) => {
    try {
      const res = await deleteAgentPersona(id);
      if (res.soft) {
        // 系统预设 → 后端 enabled=false，前端刷新列表
        void refreshPersonas();
      } else {
        setPersonas((prev) => prev.filter((p) => p.id !== id));
      }
      if (activePersonaId === id) setActivePersonaId(null);
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    }
  };

  const refreshArtifacts = useCallback(async (sessionId: string) => {
    try {
      const { items } = await listAgentArtifacts(sessionId);
      setArtifacts(items);
      // 自动开抽屉：拿到 ≥1 个 artifact 且当前关着 → 打开（用户可关）
      if (items.length > 0) setDrawerOpen(true);
    } catch {
      setArtifacts([]);
    }
  }, []);

  const loadSession = useCallback(
    async (sessionId: string) => {
      setError(null);
      try {
        const full = await getAgentSession(sessionId);
        setActiveSessionId(sessionId);
        setMessages(full.messages);
        await refreshArtifacts(sessionId);
      } catch (err) {
        setError(err instanceof Error ? err.message : String(err));
      }
    },
    [refreshArtifacts],
  );

  /** 当前 filter 指向一个项目时，新会话自动归属该项目（unassigned / null 时不归） */
  const projectIdFromFilter = (): string | undefined =>
    activeProjectFilter && activeProjectFilter !== 'unassigned' ? activeProjectFilter : undefined;

  /**
   * "新对话"按钮 = lazy create：只清前端状态，回 hero 空态。
   * 用户输入第一句 prompt 时 handleSend 才真的调 createAgentSession——避免
   * 用户随手点了 "New session" 又退出 留一堆 "Session xxx" 空壳孤儿在 sidebar。
   */
  const handleNewSession = () => {
    setError(null);
    setActiveSessionId(null);
    setMessages([]);
    setArtifacts([]);
    setDrawerOpen(false);
  };

  const handleDeleteSession = async (id: string) => {
    setError(null);
    try {
      await deleteAgentSession(id);
      setSessions((prev) => prev.filter((s) => s.id !== id));
      if (activeSessionId === id) {
        setActiveSessionId(null);
        setMessages([]);
        setArtifacts([]);
        setDrawerOpen(false);
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    }
  };

  const fireExample = (cardKey: CardKey) => {
    setComposerValue(t.agent.onboarding.cards[cardKey].prompt);
  };

  // PR7 Slash Commands
  const handleSlashCommand = async (raw: string): Promise<boolean> => {
    const [cmd, ...rest] = raw.slice(1).split(/\s+/);
    void rest; // 当前命令都不带参数；保留分词逻辑给 plan/readonly 等后续扩展
    switch (cmd) {
      case 'clear': {
        if (activeSessionId) await deleteAgentSession(activeSessionId).catch(() => undefined);
        const created = await createAgentSession({});
        setSessions((prev) => [created, ...prev.filter((s) => s.id !== activeSessionId)]);
        setActiveSessionId(created.id);
        setMessages([]);
        setArtifacts([]);
        return true;
      }
      case 'help': {
        const lines = [
          t.agent.slash.helpHeader,
          ...Object.values(t.agent.slash.commands).map((c) => `${c.name} — ${c.description}`),
        ].join('\n');
        if (typeof window !== 'undefined') window.alert(lines);
        return true;
      }
      case 'plan': {
        if (!activeSessionId) return true;
        await apiClient.post('/agent/tools/EnterPlanMode', {
          input: { sessionId: activeSessionId },
          sessionId: activeSessionId,
        });
        return true;
      }
      case 'normal': {
        if (!activeSessionId) return true;
        await apiClient.post('/agent/tools/ExitPlanMode', {
          input: { sessionId: activeSessionId },
          sessionId: activeSessionId,
        });
        await apiClient.post('/agent/tools/SetPermissionMode', {
          input: { sessionId: activeSessionId, mode: 'DEFAULT' },
          sessionId: activeSessionId,
        });
        return true;
      }
      case 'readonly': {
        if (!activeSessionId) return true;
        await apiClient.post('/agent/tools/SetPermissionMode', {
          input: { sessionId: activeSessionId, mode: 'READ_ONLY' },
          sessionId: activeSessionId,
        });
        return true;
      }
      case 'compact': {
        if (activeSessionId) await loadSession(activeSessionId);
        return true;
      }
      default: {
        setError(t.agent.slash.notFound.replace('{cmd}', cmd));
        return true;
      }
    }
  };

  const handleSend = async (override?: string) => {
    const prompt = (override ?? composerValue).trim();
    if (!prompt || sending) return;

    if (prompt.startsWith('/')) {
      setComposerValue('');
      try {
        await handleSlashCommand(prompt);
      } catch (err) {
        setError(err instanceof Error ? err.message : String(err));
      }
      return;
    }

    setError(null);
    setSending(true);
    setStreamingText('');
    setLastUserPrompt(prompt);

    let sessionId = activeSessionId;
    try {
      if (!sessionId) {
        const created = await createAgentSession({
          title: prompt.slice(0, 40),
          projectId: projectIdFromFilter(),
          // M1-step2: 把 sidebar 当前选中的 persona 绑到 session，激活 PERSONA-scoped memory 注入
          personaId: activePersonaId ?? undefined,
        });
        sessionId = created.id;
        setSessions((prev) => [created, ...prev]);
        setActiveSessionId(created.id);
      }
      setComposerValue('');

      // 乐观渲染：用户消息立刻入流，不等后端 USER_PROMPT 事件回来。
      // 体感"点了就发了"，移除原本的"发送中..."占位。
      // 收到真实 USER_PROMPT 事件后，按 id 前缀替换掉这条 optimistic。
      const optimisticId = `optimistic-${Date.now()}`;
      const optimisticUser: AgentMessage = {
        id: optimisticId,
        sessionId,
        turnId: 'optimistic',
        type: 'USER_PROMPT',
        content: prompt,
        payload: null,
        model: null,
        sequence: 0,
        createdAt: new Date().toISOString(),
      };
      setMessages((prev) => [...prev, optimisticUser]);

      // Stop / Cancel：每个 turn 一个 AbortController；handleStop 触发 abort + 后端 cancel
      const controller = new AbortController();
      abortRef.current = controller;
      currentTurnIdRef.current = null;

      await postAgentMessageStream({ sessionId, prompt }, (ev) => {
        if (ev.type === 'message') {
          // 第一条消息携带 turnId → 存下来，Stop 时调后端 cancel
          if (!currentTurnIdRef.current && ev.message.turnId !== 'optimistic') {
            currentTurnIdRef.current = ev.message.turnId;
          }
          setMessages((prev) => {
            // 真实 USER_PROMPT 到达 → 替换掉同 turn 的 optimistic 占位
            if (ev.message.type === 'USER_PROMPT') {
              return [...prev.filter((m) => !m.id.startsWith('optimistic-')), ev.message];
            }
            return [...prev, ev.message];
          });
          if (ev.message.type === 'ASSISTANT_TEXT') setStreamingText('');
        } else if (ev.type === 'text_delta') {
          setStreamingText((prev) => prev + ev.text);
        } else if (ev.type === 'ask_user') {
          if (!currentTurnIdRef.current) currentTurnIdRef.current = ev.turnId;
          setPendingAsk({ question: ev.question, options: ev.options });
        } else if (ev.type === 'done') {
          if (sessionId) void refreshArtifacts(sessionId);
        } else if (ev.type === 'error') {
          setError(ev.message);
        }
      }, { signal: controller.signal });
    } catch (err) {
      // AbortError 是用户主动 Stop，不算 error
      const name = (err as Error)?.name;
      if (name !== 'AbortError') {
        setError(err instanceof Error ? err.message : String(err));
      }
    } finally {
      setSending(false);
      setStreamingText('');
      abortRef.current = null;
      currentTurnIdRef.current = null;
    }
  };

  /**
   * 编辑历史 USER_PROMPT 后重发：
   * 1) 后端 rewind 删除 sequence > 该用户消息 sequence 的所有 message（含 assistant + tool）
   * 2) 本地立刻把 messages 截断到 sequence ≤ N（不等 refresh）
   * 3) 像普通发送一样调 handleSend(newPrompt) —— 复用乐观渲染 + 流式
   */
  const handleEditUserMessage = async (msg: AgentMessage, newPrompt: string) => {
    const np = newPrompt.trim();
    if (!np || !activeSessionId) return;
    try {
      await rewindAgentSession(activeSessionId, msg.sequence);
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
      return;
    }
    // 本地截断到 sequence < msg.sequence（旧用户消息本身也删，由新 prompt 替代）
    setMessages((prev) => prev.filter((m) => m.sequence < msg.sequence));
    void handleSend(np);
  };

  const handleStop = async () => {
    const turnId = currentTurnIdRef.current;
    // 后端先停 TAOR loop（节省 LLM 成本 / 工具执行），后中断 SSE 读取
    if (turnId) {
      try {
        await apiClient.post(`/agent/messages/${turnId}/cancel`, {});
      } catch {
        // 后端 cancel 失败也得中断前端 fetch，不阻塞 abort
      }
    }
    abortRef.current?.abort();
  };

  const toggleLocale = () => {
    const next: Locale = locale === 'zh' ? 'en' : 'zh';
    changeLocale(next);
  };

  return (
    <div className="grid h-full w-full grid-cols-[260px_1fr] bg-stone-50 text-neutral-900 dark:bg-neutral-950 dark:text-neutral-100">
      <SessionPane
        t={t}
        sessions={sessions}
        activeSessionId={activeSessionId}
        loading={loadingSessions}
        onNewSession={handleNewSession}
        onSelectSession={loadSession}
        onDeleteSession={handleDeleteSession}
        projects={projects}
        activeProjectFilter={activeProjectFilter}
        onSelectProjectFilter={setActiveProjectFilter}
        onCreateProject={handleCreateProject}
        onDeleteProject={handleDeleteProject}
        memories={memories}
        onCreateMemory={handleCreateMemory}
        onDeleteMemory={handleDeleteMemory}
        personas={personas}
        activePersonaId={activePersonaId}
        onSelectPersona={setActivePersonaId}
        onCreatePersona={handleCreatePersona}
        onDeletePersona={handleDeletePersona}
      />
      <ConversationPane
        t={t}
        composerValue={composerValue}
        onComposerChange={setComposerValue}
        onFireExample={fireExample}
        onToggleLocale={toggleLocale}
        locale={locale}
        messages={messages}
        sending={sending}
        error={error}
        onSend={handleSend}
        hasActiveSession={activeSessionId !== null}
        streamingText={streamingText}
        pendingAsk={pendingAsk}
        onPickAskOption={(answer) => {
          setPendingAsk(null);
          setComposerValue(answer);
          void handleSend(answer);
        }}
        artifactsCount={artifacts.length}
        onToggleDrawer={() => setDrawerOpen((v) => !v)}
        onRetry={() => lastUserPrompt && void handleSend(lastUserPrompt)}
        onStop={handleStop}
        onEditUserMessage={handleEditUserMessage}
      />
      <ArtifactDrawer
        t={t}
        artifacts={artifacts}
        open={drawerOpen}
        onClose={() => setDrawerOpen(false)}
      />
    </div>
  );
}

// ---------------------------------------------------------------------------
// 左：会话侧栏（含 Today / 7d / 30d / Older 分组）
// ---------------------------------------------------------------------------

interface PaneProps {
  t: ReturnType<typeof useTranslation>['t'];
}

interface SessionPaneProps extends PaneProps {
  sessions: AgentSession[];
  activeSessionId: string | null;
  loading: boolean;
  onNewSession: () => void;
  onSelectSession: (id: string) => void;
  onDeleteSession: (id: string) => void;
  projects: AgentProject[];
  /** null = 全部对话；'unassigned' = 未分组；其他 = 指定项目 id */
  activeProjectFilter: string | null;
  onSelectProjectFilter: (filter: string | null) => void;
  onCreateProject: (name: string) => void;
  onDeleteProject: (id: string) => void;
  memories: AgentMemory[];
  onCreateMemory: (content: string, category: MemoryCategory) => void;
  onDeleteMemory: (id: string) => void;
  personas: AgentPersona[];
  activePersonaId: string | null;
  onSelectPersona: (id: string | null) => void;
  onCreatePersona: (input: { name: string; description?: string; instructions?: string }) => void;
  onDeletePersona: (id: string) => void;
}

type SessionGroupKey = 'today' | 'past7' | 'past30' | 'older';

function groupSessions(
  sessions: AgentSession[],
): Array<{ key: SessionGroupKey; items: AgentSession[] }> {
  const now = Date.now();
  const dayMs = 24 * 60 * 60 * 1000;
  const groups: Record<SessionGroupKey, AgentSession[]> = {
    today: [],
    past7: [],
    past30: [],
    older: [],
  };
  for (const s of sessions) {
    const ts = new Date(s.updatedAt ?? s.createdAt ?? Date.now()).getTime();
    const ageDays = (now - ts) / dayMs;
    if (ageDays < 1) groups.today.push(s);
    else if (ageDays < 7) groups.past7.push(s);
    else if (ageDays < 30) groups.past30.push(s);
    else groups.older.push(s);
  }
  return (['today', 'past7', 'past30', 'older'] as const)
    .map((key) => ({ key, items: groups[key] }))
    .filter((g) => g.items.length > 0);
}

function SessionPane({
  t,
  sessions,
  activeSessionId,
  loading,
  onNewSession,
  onSelectSession,
  onDeleteSession,
  projects,
  activeProjectFilter,
  onSelectProjectFilter,
  onCreateProject,
  onDeleteProject,
  memories,
  onCreateMemory,
  onDeleteMemory,
  personas,
  activePersonaId,
  onSelectPersona,
  onCreatePersona,
  onDeletePersona,
}: SessionPaneProps) {
  const [creatingProject, setCreatingProject] = useState(false);
  const [newProjectName, setNewProjectName] = useState('');

  // 按当前过滤器过滤 sessions
  const filtered = useMemo(() => {
    if (activeProjectFilter === null) return sessions;
    if (activeProjectFilter === 'unassigned') return sessions.filter((s) => !s.projectId);
    return sessions.filter((s) => s.projectId === activeProjectFilter);
  }, [sessions, activeProjectFilter]);

  const grouped = useMemo(() => groupSessions(filtered), [filtered]);

  const submitCreateProject = () => {
    const n = newProjectName.trim();
    if (!n) return;
    onCreateProject(n);
    setNewProjectName('');
    setCreatingProject(false);
  };

  return (
    <aside
      className="flex h-full flex-col border-r border-stone-200 bg-white dark:border-neutral-800 dark:bg-neutral-900"
      data-testid="agent-session-pane"
    >
      <div className="flex items-center justify-between px-3 py-3">
        <Link
          href="/overview"
          className="flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-neutral-500 transition hover:bg-stone-100 hover:text-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-100"
          aria-label="Back to workspace"
          data-testid="agent-back-to-workspace"
        >
          <ArrowLeft className="h-3.5 w-3.5" aria-hidden />
          Workspace
        </Link>
        <div className="flex items-center gap-1.5 pr-1 text-sm font-semibold text-neutral-900 dark:text-neutral-100">
          <Sparkles className="h-4 w-4 text-blue-600" aria-hidden />
          {t.agent.title}
        </div>
      </div>
      <div className="px-3 pb-2">
        <button
          type="button"
          onClick={onNewSession}
          className="flex w-full items-center justify-center gap-2 rounded-lg border border-stone-200 bg-white px-3 py-2 text-sm font-medium text-neutral-800 transition hover:bg-stone-100 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100 dark:hover:bg-neutral-800"
          data-testid="agent-new-session"
        >
          <MessageSquarePlus className="h-4 w-4" aria-hidden />
          {t.agent.nav.newSession}
        </button>
      </div>
      <div className="flex-1 overflow-y-auto px-2 pb-4">
        {/* Memory 段：最顶部，agent 每次自动读 */}
        <MemorySection
          t={t}
          memories={memories}
          onCreate={onCreateMemory}
          onDelete={onDeleteMemory}
        />

        <div className="mx-2 mb-3 border-t border-stone-200 dark:border-neutral-800" />

        {/* Personas 段：在 Memory 下 / Projects 上 */}
        <PersonaSection
          t={t}
          personas={personas}
          activePersonaId={activePersonaId}
          onSelect={onSelectPersona}
          onCreate={onCreatePersona}
          onDelete={onDeletePersona}
        />

        <div className="mx-2 mb-3 border-t border-stone-200 dark:border-neutral-800" />

        {/* Projects 段（ChatGPT 风：在 Recent chats 之上）*/}
        <div className="mb-3 px-1">
          <div className="mb-1 flex items-center justify-between px-1">
            <div className="text-[11px] font-medium uppercase tracking-wide text-neutral-400 dark:text-neutral-500">
              {t.agent.projects.sectionTitle}
            </div>
            <button
              type="button"
              onClick={() => setCreatingProject(true)}
              className="rounded p-0.5 text-neutral-400 transition hover:bg-stone-100 hover:text-neutral-700 dark:hover:bg-neutral-800 dark:hover:text-neutral-200"
              aria-label={t.agent.projects.newProject}
              title={t.agent.projects.newProject}
              data-testid="agent-new-project"
            >
              <Plus className="h-3.5 w-3.5" aria-hidden />
            </button>
          </div>

          {creatingProject ? (
            <div className="mb-1 px-1">
              <input
                value={newProjectName}
                onChange={(e) => setNewProjectName(e.target.value)}
                onKeyDown={(e) => {
                  if (e.key === 'Enter') submitCreateProject();
                  if (e.key === 'Escape') {
                    setCreatingProject(false);
                    setNewProjectName('');
                  }
                }}
                placeholder={t.agent.projects.newProjectPlaceholder}
                autoFocus
                className="w-full rounded border border-stone-300 bg-white px-2 py-1 text-xs text-neutral-900 placeholder:text-neutral-400 focus:border-neutral-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100 dark:placeholder:text-neutral-500"
                data-testid="agent-new-project-input"
              />
              <div className="mt-1 flex gap-1">
                <button
                  type="button"
                  onClick={submitCreateProject}
                  disabled={newProjectName.trim().length === 0}
                  className="rounded bg-neutral-900 px-2 py-0.5 text-[11px] text-white transition hover:bg-neutral-700 disabled:bg-neutral-300 dark:bg-neutral-100 dark:text-neutral-900 dark:disabled:bg-neutral-700 dark:disabled:text-neutral-500"
                >
                  {t.agent.projects.create}
                </button>
                <button
                  type="button"
                  onClick={() => {
                    setCreatingProject(false);
                    setNewProjectName('');
                  }}
                  className="rounded border border-stone-300 px-2 py-0.5 text-[11px] text-neutral-700 transition hover:bg-stone-100 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800"
                >
                  {t.agent.projects.cancel}
                </button>
              </div>
            </div>
          ) : null}

          <ul className="space-y-0.5" data-testid="agent-project-list">
            <li>
              <ProjectRow
                label={t.agent.projects.allLabel}
                icon="all"
                selected={activeProjectFilter === null}
                onClick={() => onSelectProjectFilter(null)}
              />
            </li>
            {projects.map((p) => (
              <li key={p.id}>
                <ProjectRow
                  label={p.name}
                  icon={p.icon ?? 'folder'}
                  selected={activeProjectFilter === p.id}
                  onClick={() => onSelectProjectFilter(p.id)}
                  onDelete={() => {
                    const msg = t.agent.projects.deleteConfirm.replace('{name}', p.name);
                    if (typeof window !== 'undefined' && window.confirm(msg)) {
                      onDeleteProject(p.id);
                    }
                  }}
                />
              </li>
            ))}
            {sessions.some((s) => !s.projectId) ? (
              <li>
                <ProjectRow
                  label={t.agent.projects.unassignedLabel}
                  icon="unassigned"
                  selected={activeProjectFilter === 'unassigned'}
                  onClick={() => onSelectProjectFilter('unassigned')}
                />
              </li>
            ) : null}
          </ul>
        </div>

        {/* 分隔线 */}
        <div className="mx-2 mb-3 border-t border-stone-200 dark:border-neutral-800" />

        {/* Recent chats 段 */}
        {loading ? (
          <div className="flex items-center justify-center py-6 text-neutral-400">
            <Loader2 className="h-4 w-4 animate-spin" aria-hidden />
          </div>
        ) : filtered.length === 0 ? (
          <div
            className="mx-2 mt-4 rounded-md border border-dashed border-stone-200 px-3 py-6 text-center text-xs text-neutral-400 dark:border-neutral-800"
            data-testid="agent-history-empty"
          >
            {t.agent.nav.empty}
          </div>
        ) : (
          <ul className="space-y-3" data-testid="agent-session-list">
            {grouped.map((g) => (
              <li key={g.key}>
                <div className="px-2 pb-1 text-[11px] font-medium text-neutral-400 dark:text-neutral-500">
                  {t.agent.nav.groups[g.key]}
                </div>
                <ul className="space-y-0.5">
                  {g.items.map((s) => (
                    <li key={s.id}>
                      <div
                        className={`group flex items-center gap-1 rounded-md px-2 py-1.5 text-sm transition ${
                          s.id === activeSessionId
                            ? 'bg-stone-200/70 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100'
                            : 'text-neutral-700 hover:bg-stone-100 dark:text-neutral-300 dark:hover:bg-neutral-800/60'
                        }`}
                      >
                        <button
                          type="button"
                          onClick={() => onSelectSession(s.id)}
                          className="flex-1 truncate text-left"
                          title={s.title ?? s.id}
                        >
                          {s.parentSessionId ? <span className="mr-1 text-neutral-400" title="Sub-agent of parent session">↳</span> : null}
                          {s.title || `Session ${s.id.slice(0, 8)}`}
                        </button>
                        <button
                          type="button"
                          onClick={() => onDeleteSession(s.id)}
                          className="opacity-0 transition group-hover:opacity-100"
                          aria-label="Delete"
                        >
                          <Trash2 className="h-3.5 w-3.5 text-neutral-400 hover:text-red-500" />
                        </button>
                      </div>
                    </li>
                  ))}
                </ul>
              </li>
            ))}
          </ul>
        )}
      </div>
    </aside>
  );
}

interface MemorySectionProps extends PaneProps {
  memories: AgentMemory[];
  onCreate: (content: string, category: MemoryCategory) => void;
  onDelete: (id: string) => void;
}

function MemorySection({ t, memories, onCreate, onDelete }: MemorySectionProps) {
  const [adding, setAdding] = useState(false);
  const [draft, setDraft] = useState('');
  const [draftCategory, setDraftCategory] = useState<MemoryCategory>('USER');
  /** 默认折叠：sidebar 干净；点 header 或 ➕ 自动展开 */
  const [expanded, setExpanded] = useState(false);

  const submit = () => {
    const c = draft.trim();
    if (!c) return;
    onCreate(c, draftCategory);
    setDraft('');
    setAdding(false);
    setDraftCategory('USER');
  };

  const grouped = useMemo(
    () =>
      MEMORY_CATEGORIES.map((cat) => ({
        cat,
        items: memories.filter((m) => m.category === cat),
      })).filter((g) => g.items.length > 0),
    [memories],
  );

  const Chevron = expanded ? ChevronDown : ChevronRight;

  return (
    <div className="mb-3 px-1" data-testid="agent-memory-section">
      {/* Header（左侧切换折叠 + 右侧 ➕） */}
      <div className="flex items-center gap-1 px-1 py-0.5 text-[11px] font-medium uppercase tracking-wide text-neutral-400 dark:text-neutral-500">
        <button
          type="button"
          onClick={() => setExpanded((v) => !v)}
          className="flex flex-1 items-center gap-1 rounded transition hover:text-neutral-600 dark:hover:text-neutral-300"
          aria-expanded={expanded}
          data-testid="agent-memory-toggle"
        >
          <Chevron className="h-3 w-3 shrink-0" aria-hidden />
          <Brain className="h-3 w-3 shrink-0" aria-hidden />
          <span>{t.agent.memory.sectionTitle}</span>
          {memories.length > 0 ? (
            <span className="ml-0.5" aria-hidden>
              · {memories.length}
            </span>
          ) : null}
        </button>
        <button
          type="button"
          onClick={() => {
            setExpanded(true);
            setAdding(true);
          }}
          className="rounded p-0.5 transition hover:bg-stone-100 hover:text-neutral-700 dark:hover:bg-neutral-800 dark:hover:text-neutral-200"
          aria-label={t.agent.memory.add}
          title={t.agent.memory.add}
          data-testid="agent-memory-add"
        >
          <Plus className="h-3.5 w-3.5" aria-hidden />
        </button>
      </div>

      {/* Body（仅展开时显示）*/}
      {expanded ? (
        <div className="mt-1">
          {adding ? (
            <div className="mb-1 px-1">
              <textarea
                value={draft}
                onChange={(e) => setDraft(e.target.value)}
                onKeyDown={(e) => {
                  if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) submit();
                  if (e.key === 'Escape') {
                    setAdding(false);
                    setDraft('');
                  }
                }}
                placeholder={t.agent.memory.placeholder}
                autoFocus
                rows={3}
                className="w-full resize-none rounded border border-stone-300 bg-white px-2 py-1 text-xs text-neutral-900 placeholder:text-neutral-400 focus:border-neutral-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100 dark:placeholder:text-neutral-500"
                data-testid="agent-memory-input"
              />
              <div className="mt-1 flex flex-wrap items-center gap-1" data-testid="agent-memory-category-picker">
                <span className="text-[10px] text-neutral-500 dark:text-neutral-400">
                  {t.agent.memory.categoryLabel}
                </span>
                {MEMORY_CATEGORIES.map((cat) => (
                  <button
                    key={cat}
                    type="button"
                    onClick={() => setDraftCategory(cat)}
                    aria-pressed={draftCategory === cat}
                    className={`rounded px-1.5 py-0.5 text-[10px] transition ${
                      draftCategory === cat
                        ? 'bg-neutral-900 text-white dark:bg-neutral-100 dark:text-neutral-900'
                        : 'border border-stone-300 text-neutral-600 hover:bg-stone-100 dark:border-neutral-700 dark:text-neutral-400 dark:hover:bg-neutral-800'
                    }`}
                  >
                    {t.agent.memory.categories[cat]}
                  </button>
                ))}
              </div>
              <div className="mt-1 flex gap-1">
                <button
                  type="button"
                  onClick={submit}
                  disabled={draft.trim().length === 0}
                  className="rounded bg-neutral-900 px-2 py-0.5 text-[11px] text-white transition hover:bg-neutral-700 disabled:bg-neutral-300 dark:bg-neutral-100 dark:text-neutral-900 dark:disabled:bg-neutral-700 dark:disabled:text-neutral-500"
                >
                  {t.agent.memory.save}
                </button>
                <button
                  type="button"
                  onClick={() => {
                    setAdding(false);
                    setDraft('');
                  }}
                  className="rounded border border-stone-300 px-2 py-0.5 text-[11px] text-neutral-700 transition hover:bg-stone-100 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800"
                >
                  {t.agent.memory.cancel}
                </button>
              </div>
            </div>
          ) : null}

          {memories.length === 0 ? (
            <div className="mx-1 mt-1 rounded border border-dashed border-stone-200 px-2 py-2 text-[11px] leading-snug text-neutral-400 dark:border-neutral-800">
              {t.agent.memory.empty}
            </div>
          ) : (
            <div className="space-y-2" data-testid="agent-memory-list">
              {grouped.map((group) => (
                <div key={group.cat} data-category={group.cat}>
                  <div className="px-2 pb-0.5 text-[10px] uppercase tracking-wide text-neutral-400 dark:text-neutral-500">
                    {t.agent.memory.categories[group.cat]}
                    <span className="ml-1 opacity-70">· {group.items.length}</span>
                  </div>
                  <ul className="space-y-0.5">
                    {group.items.map((m) => (
                      <li
                        key={m.id}
                        className="group flex items-start gap-1 rounded-md px-2 py-1 text-xs leading-snug text-neutral-700 hover:bg-stone-100 dark:text-neutral-300 dark:hover:bg-neutral-800/60"
                      >
                        <span className="line-clamp-2 flex-1 break-words" title={m.content}>
                          {m.content}
                          {m.source === 'ai-detected' ? (
                            <span className="ml-1 rounded bg-stone-100 px-1 text-[9px] text-neutral-500 dark:bg-neutral-800 dark:text-neutral-400">
                              {t.agent.memory.sourceAiDetected}
                            </span>
                          ) : null}
                          {m.ownerScope === 'ORG' ? (
                            <span className="ml-1 rounded bg-blue-50 px-1 text-[9px] text-blue-600 dark:bg-blue-950 dark:text-blue-300">
                              {t.agent.memory.sourceSystem}
                            </span>
                          ) : null}
                        </span>
                        {m.ownerScope === 'USER' ? (
                          <button
                            type="button"
                            onClick={() => {
                              if (typeof window !== 'undefined' && window.confirm(t.agent.memory.deleteConfirm)) {
                                onDelete(m.id);
                              }
                            }}
                            className="opacity-0 transition group-hover:opacity-100"
                            aria-label={t.agent.memory.delete}
                          >
                            <Trash2 className="h-3 w-3 text-neutral-400 hover:text-red-500" />
                          </button>
                        ) : null}
                      </li>
                    ))}
                  </ul>
                </div>
              ))}
            </div>
          )}
        </div>
      ) : null}
    </div>
  );
}

interface PersonaSectionProps extends PaneProps {
  personas: AgentPersona[];
  activePersonaId: string | null;
  onSelect: (id: string | null) => void;
  onCreate: (input: { name: string; description?: string; instructions?: string }) => void;
  onDelete: (id: string) => void;
}

function PersonaSection({
  t,
  personas,
  activePersonaId,
  onSelect,
  onCreate,
  onDelete,
}: PersonaSectionProps) {
  const [adding, setAdding] = useState(false);
  const [name, setName] = useState('');
  const [desc, setDesc] = useState('');
  const [instr, setInstr] = useState('');
  /** 默认折叠；header 显示当前激活 persona 摘要 */
  const [expanded, setExpanded] = useState(false);

  const submit = () => {
    const n = name.trim();
    if (!n) return;
    onCreate({ name: n, description: desc.trim() || undefined, instructions: instr.trim() || undefined });
    setName('');
    setDesc('');
    setInstr('');
    setAdding(false);
  };

  const active = personas.find((p) => p.id === activePersonaId) ?? null;
  const Chevron = expanded ? ChevronDown : ChevronRight;

  return (
    <div className="mb-3 px-1" data-testid="agent-persona-section">
      {/* Header（左切折叠 + 显示当前 active persona + 右 ➕）*/}
      <div className="flex items-center gap-1 px-1 py-0.5 text-[11px] font-medium uppercase tracking-wide text-neutral-400 dark:text-neutral-500">
        <button
          type="button"
          onClick={() => setExpanded((v) => !v)}
          className="flex flex-1 items-center gap-1 truncate rounded transition hover:text-neutral-600 dark:hover:text-neutral-300"
          aria-expanded={expanded}
          data-testid="agent-persona-toggle"
        >
          <Chevron className="h-3 w-3 shrink-0" aria-hidden />
          <Bot className="h-3 w-3 shrink-0" aria-hidden />
          <span>{t.agent.personas.sectionTitle}</span>
          {active ? (
            <span className="ml-0.5 flex items-center gap-1 truncate normal-case tracking-normal text-neutral-500 dark:text-neutral-400" aria-hidden>
              · <span className="text-sm leading-none">{active.icon ?? '🤖'}</span>
              <span className="truncate">{active.name}</span>
            </span>
          ) : null}
        </button>
        <button
          type="button"
          onClick={() => {
            setExpanded(true);
            setAdding(true);
          }}
          className="rounded p-0.5 transition hover:bg-stone-100 hover:text-neutral-700 dark:hover:bg-neutral-800 dark:hover:text-neutral-200"
          aria-label={t.agent.personas.newPersona}
          title={t.agent.personas.newPersona}
          data-testid="agent-new-persona"
        >
          <Plus className="h-3.5 w-3.5" aria-hidden />
        </button>
      </div>

      {/* Body（仅展开时显示）*/}
      {expanded ? (
        <div className="mt-1">

      {adding ? (
        <div className="mb-1 space-y-1 px-1">
          <input
            value={name}
            onChange={(e) => setName(e.target.value)}
            placeholder={t.agent.personas.namePlaceholder}
            autoFocus
            className="w-full rounded border border-stone-300 bg-white px-2 py-1 text-xs text-neutral-900 placeholder:text-neutral-400 focus:border-neutral-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100 dark:placeholder:text-neutral-500"
          />
          <input
            value={desc}
            onChange={(e) => setDesc(e.target.value)}
            placeholder={t.agent.personas.descriptionPlaceholder}
            className="w-full rounded border border-stone-300 bg-white px-2 py-1 text-xs text-neutral-900 placeholder:text-neutral-400 focus:border-neutral-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100 dark:placeholder:text-neutral-500"
          />
          <textarea
            value={instr}
            onChange={(e) => setInstr(e.target.value)}
            placeholder={t.agent.personas.instructionsPlaceholder}
            rows={3}
            className="w-full resize-none rounded border border-stone-300 bg-white px-2 py-1 text-xs text-neutral-900 placeholder:text-neutral-400 focus:border-neutral-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100 dark:placeholder:text-neutral-500"
          />
          <div className="flex gap-1">
            <button
              type="button"
              onClick={submit}
              disabled={name.trim().length === 0}
              className="rounded bg-neutral-900 px-2 py-0.5 text-[11px] text-white transition hover:bg-neutral-700 disabled:bg-neutral-300 dark:bg-neutral-100 dark:text-neutral-900 dark:disabled:bg-neutral-700 dark:disabled:text-neutral-500"
            >
              {t.agent.personas.create}
            </button>
            <button
              type="button"
              onClick={() => {
                setAdding(false);
                setName('');
                setDesc('');
                setInstr('');
              }}
              className="rounded border border-stone-300 px-2 py-0.5 text-[11px] text-neutral-700 transition hover:bg-stone-100 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800"
            >
              {t.agent.personas.cancel}
            </button>
          </div>
        </div>
      ) : null}

      {personas.length === 0 ? (
        <div className="mx-1 mt-1 rounded border border-dashed border-stone-200 px-2 py-2 text-[11px] leading-snug text-neutral-400 dark:border-neutral-800">
          {t.agent.personas.empty}
        </div>
      ) : (
        <ul className="space-y-0.5" data-testid="agent-persona-list">
          {personas.map((p) => {
            const isSys = isSystemPersona(p);
            const selected = activePersonaId === p.id;
            return (
              <li key={p.id}>
                <div
                  className={`group flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm transition ${
                    selected
                      ? 'bg-stone-200/70 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100'
                      : 'text-neutral-700 hover:bg-stone-100 dark:text-neutral-300 dark:hover:bg-neutral-800/60'
                  }`}
                >
                  <button
                    type="button"
                    onClick={() => onSelect(selected ? null : p.id)}
                    className="flex flex-1 items-center gap-2 truncate text-left"
                    title={p.description ?? p.name}
                  >
                    <span className="text-sm leading-none">{p.icon ?? '🤖'}</span>
                    <span className="truncate">{p.name}</span>
                    {isSys ? (
                      <span className="rounded bg-stone-200 px-1 py-0.5 text-[9px] font-medium text-neutral-500 dark:bg-neutral-800 dark:text-neutral-400">
                        {t.agent.personas.systemBadge}
                      </span>
                    ) : null}
                  </button>
                  <button
                    type="button"
                    onClick={(e) => {
                      e.stopPropagation();
                      const msg = (isSys
                        ? t.agent.personas.hideConfirm
                        : t.agent.personas.deleteConfirm
                      ).replace('{name}', p.name);
                      if (typeof window !== 'undefined' && window.confirm(msg)) {
                        onDelete(p.id);
                      }
                    }}
                    className="opacity-0 transition group-hover:opacity-100"
                    aria-label={isSys ? t.agent.personas.hide : t.agent.personas.delete}
                  >
                    <Trash2 className="h-3.5 w-3.5 text-neutral-400 hover:text-red-500" />
                  </button>
                </div>
              </li>
            );
          })}
        </ul>
      )}
        </div>
      ) : null}
    </div>
  );
}

interface ProjectRowProps {
  label: string;
  /** "all" / "unassigned" / "folder" / 其他（未来支持 emoji） */
  icon: string;
  selected: boolean;
  onClick: () => void;
  onDelete?: () => void;
}

function ProjectRow({ label, icon, selected, onClick, onDelete }: ProjectRowProps) {
  const IconEl =
    icon === 'all' ? Sparkles : icon === 'unassigned' ? MessageSquarePlus : Folder;
  return (
    <div
      className={`group flex items-center gap-1 rounded-md px-2 py-1.5 text-sm transition ${
        selected
          ? 'bg-stone-200/70 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100'
          : 'text-neutral-700 hover:bg-stone-100 dark:text-neutral-300 dark:hover:bg-neutral-800/60'
      }`}
    >
      <button type="button" onClick={onClick} className="flex flex-1 items-center gap-2 truncate text-left">
        <IconEl
          className={`h-3.5 w-3.5 shrink-0 ${
            selected ? 'text-blue-600' : 'text-neutral-400 dark:text-neutral-500'
          }`}
          aria-hidden
        />
        <span className="truncate">{label}</span>
      </button>
      {onDelete ? (
        <button
          type="button"
          onClick={(e) => {
            e.stopPropagation();
            onDelete();
          }}
          className="opacity-0 transition group-hover:opacity-100"
          aria-label="Delete project"
        >
          <Trash2 className="h-3.5 w-3.5 text-neutral-400 hover:text-red-500" />
        </button>
      ) : null}
    </div>
  );
}

// ---------------------------------------------------------------------------
// 中：对话主区（hero 空态 / 消息流 + composer）
// ---------------------------------------------------------------------------

interface ConversationPaneProps extends PaneProps {
  composerValue: string;
  onComposerChange: (v: string) => void;
  onFireExample: (k: CardKey) => void;
  onToggleLocale: () => void;
  locale: Locale;
  messages: AgentMessage[];
  sending: boolean;
  error: string | null;
  onSend: () => void;
  hasActiveSession: boolean;
  streamingText: string;
  pendingAsk: { question: string; options?: string[] } | null;
  onPickAskOption: (answer: string) => void;
  artifactsCount: number;
  onToggleDrawer: () => void;
  onRetry: () => void;
  onStop: () => void;
  onEditUserMessage: (msg: AgentMessage, newPrompt: string) => void;
}

function ConversationPane({
  t,
  composerValue,
  onComposerChange,
  onFireExample,
  onToggleLocale,
  locale,
  messages,
  sending,
  error,
  onSend,
  hasActiveSession,
  streamingText,
  pendingAsk,
  onPickAskOption,
  artifactsCount,
  onToggleDrawer,
  onRetry,
  onStop,
  onEditUserMessage,
}: ConversationPaneProps) {
  const visibleMessages = messages.filter(
    (m) => m.type !== 'TURN_DONE' && !(m.type === 'ASSISTANT_TEXT' && !m.content),
  );

  const isEmpty = !(hasActiveSession && visibleMessages.length > 0);

  // 自动滚到底：消息追加 / streaming 增量到达时跟随光标在底部。
  // 仅在用户当前已贴近底部时执行（避免打断用户向上看历史）。
  const scrollRef = useRef<HTMLDivElement>(null);
  useEffect(() => {
    const el = scrollRef.current;
    if (!el) return;
    const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
    if (distanceFromBottom < 120) {
      el.scrollTop = el.scrollHeight;
    }
  }, [visibleMessages.length, streamingText]);

  return (
    <section className="relative flex h-full min-h-0 flex-col overflow-hidden" data-testid="agent-conversation-pane">
      {/* 顶部 floating 控制条（无 border 无背景，悬浮在主区右上） */}
      <div className="pointer-events-none absolute right-4 top-3 z-10 flex items-center gap-2">
        {artifactsCount > 0 ? (
          <button
            type="button"
            onClick={onToggleDrawer}
            className="pointer-events-auto flex items-center gap-1.5 rounded-full border border-stone-200 bg-white/80 px-3 py-1.5 text-xs font-medium text-neutral-700 backdrop-blur transition hover:bg-stone-100 dark:border-neutral-800 dark:bg-neutral-900/80 dark:text-neutral-300 dark:hover:bg-neutral-800"
            data-testid="agent-artifact-toggle"
          >
            <PanelRightOpen className="h-3.5 w-3.5" aria-hidden />
            {t.agent.artifact.title} · {artifactsCount}
          </button>
        ) : null}
        <button
          type="button"
          onClick={onToggleLocale}
          className="pointer-events-auto flex items-center gap-1.5 rounded-full border border-stone-200 bg-white/80 px-3 py-1.5 text-xs font-medium text-neutral-700 backdrop-blur transition hover:bg-stone-100 dark:border-neutral-800 dark:bg-neutral-900/80 dark:text-neutral-300 dark:hover:bg-neutral-800"
          data-testid="agent-locale-toggle"
          aria-label={t.agent.language.label}
        >
          <Globe className="h-3.5 w-3.5" aria-hidden />
          {locale === 'zh' ? t.agent.language.en : t.agent.language.zh}
        </button>
      </div>

      {isEmpty ? (
        <EmptyHero
          t={t}
          composerValue={composerValue}
          onComposerChange={onComposerChange}
          onSend={onSend}
          sending={sending}
          onFireExample={onFireExample}
        />
      ) : (
        <>
          <div ref={scrollRef} className="min-h-0 flex-1 overflow-y-auto px-6 pt-16 pb-6" data-testid="agent-conversation-body">
            <div className="mx-auto max-w-3xl space-y-6">
              {visibleMessages.map((m) => (
                <MessageBubble
                  key={m.id}
                  message={m}
                  t={t}
                  onRetry={onRetry}
                  onEditUserMessage={onEditUserMessage}
                />
              ))}
              {streamingText ? (
                <AssistantBlock data-testid="agent-streaming-preview">
                  <span className="whitespace-pre-wrap">{streamingText}</span>
                  <span className="ml-0.5 inline-block h-3 w-1.5 animate-pulse bg-blue-500 align-middle" />
                </AssistantBlock>
              ) : null}
              {pendingAsk ? (
                <div className="flex" data-testid="agent-ask-user">
                  <div className="max-w-[80%] rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm dark:border-amber-900/40 dark:bg-amber-950/30">
                    <div className="mb-2 font-medium text-amber-900 dark:text-amber-200">{pendingAsk.question}</div>
                    {pendingAsk.options && pendingAsk.options.length > 0 ? (
                      <div className="flex flex-wrap gap-2">
                        {pendingAsk.options.map((opt) => (
                          <button
                            key={opt}
                            onClick={() => onPickAskOption(opt)}
                            className="rounded-full border border-amber-300 bg-white px-3 py-1 text-xs text-amber-900 hover:bg-amber-100 dark:border-amber-800/50 dark:bg-neutral-900 dark:text-amber-200"
                          >
                            {opt}
                          </button>
                        ))}
                      </div>
                    ) : (
                      <div className="text-xs text-amber-700 dark:text-amber-300/80">{t.agent.askUser.openEndedHint}</div>
                    )}
                  </div>
                </div>
              ) : null}
            </div>
          </div>

          {error ? (
            <div className="mx-auto w-full max-w-3xl px-6">
              <div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-300">
                {error}
              </div>
            </div>
          ) : null}

          <Composer
            t={t}
            value={composerValue}
            onChange={onComposerChange}
            onSend={onSend}
            onStop={onStop}
            sending={sending}
            autoFocus={false}
          />
        </>
      )}
    </section>
  );
}

// ---------------------------------------------------------------------------
// Hero 空态（学 ChatGPT 居中 heading + composer 同框 + scenario chips）
// ---------------------------------------------------------------------------

interface EmptyHeroProps extends PaneProps {
  composerValue: string;
  onComposerChange: (v: string) => void;
  onSend: () => void;
  sending: boolean;
  onFireExample: (k: CardKey) => void;
}

function EmptyHero({ t, composerValue, onComposerChange, onSend, sending, onFireExample }: EmptyHeroProps) {
  return (
    <div className="flex flex-1 flex-col items-center justify-center px-6">
      <div className="w-full max-w-2xl">
        <h1
          className="mb-8 text-center text-3xl font-semibold tracking-tight text-neutral-900 dark:text-neutral-100"
          data-testid="agent-empty-hero-heading"
        >
          {t.agent.emptyHero.heading}
        </h1>
        <Composer
          t={t}
          value={composerValue}
          onChange={onComposerChange}
          onSend={onSend}
          sending={sending}
          autoFocus
          standalone
        />
        <div
          className="mt-6 flex flex-wrap justify-center gap-2"
          data-testid="agent-onboarding-cards"
        >
          {CARD_ORDER.map((key) => {
            const card = t.agent.onboarding.cards[key];
            const Icon = CARD_ICONS[key];
            return (
              <button
                key={key}
                type="button"
                onClick={() => onFireExample(key)}
                className="flex items-center gap-1.5 rounded-full border border-stone-200 bg-white px-3 py-1.5 text-xs text-neutral-700 transition hover:border-neutral-400 hover:bg-stone-100 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:border-neutral-600 dark:hover:bg-neutral-800"
                data-testid={`agent-onboarding-card-${key}`}
                title={card.prompt}
              >
                <Icon className="h-3.5 w-3.5 text-blue-600" aria-hidden />
                {card.title}
              </button>
            );
          })}
        </div>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
// Composer（圆角 rounded-3xl + 自动撑高 + 内置 send 按钮 + 底部 disclaimer）
// ---------------------------------------------------------------------------

// 同时给 EmptyHero 加 onStop（保持接口一致；空态时永远 sending=false 实际用不上）
interface ComposerProps extends PaneProps {
  value: string;
  onChange: (v: string) => void;
  onSend: () => void;
  /** 流式中点 Send 按钮位置（变 Stop）触发；hero 空态没传，永不会显示 */
  onStop?: () => void;
  sending: boolean;
  autoFocus?: boolean;
  /** standalone = hero 模式（不 sticky，不显示 disclaimer footer） */
  standalone?: boolean;
}

function Composer({ t, value, onChange, onSend, onStop, sending, autoFocus, standalone }: ComposerProps) {
  const ref = useRef<HTMLTextAreaElement>(null);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    el.style.height = 'auto';
    const max = 240;
    el.style.height = `${Math.min(el.scrollHeight, max)}px`;
  }, [value]);

  useEffect(() => {
    if (autoFocus) ref.current?.focus();
  }, [autoFocus]);

  return (
    <div
      className={
        standalone
          ? ''
          : 'sticky bottom-0 bg-gradient-to-t from-stone-50 via-stone-50 to-transparent px-6 pb-6 pt-2 dark:from-neutral-950 dark:via-neutral-950'
      }
    >
      <div className="mx-auto w-full max-w-3xl">
        <div className="flex items-center gap-2 rounded-3xl border border-stone-200 bg-white px-4 py-3 shadow-sm transition focus-within:border-neutral-400 focus-within:shadow-md dark:border-neutral-800 dark:bg-neutral-900 dark:focus-within:border-neutral-600">
          <textarea
            ref={ref}
            value={value}
            onChange={(e) => onChange(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                onSend();
              }
            }}
            placeholder={t.agent.composer.placeholder}
            rows={1}
            disabled={sending}
            className="flex-1 resize-none overflow-y-auto bg-transparent text-sm text-neutral-900 placeholder:text-neutral-400 focus:outline-none disabled:opacity-50 dark:text-neutral-100 dark:placeholder:text-neutral-500"
            data-testid="agent-composer"
            aria-label={t.agent.composer.placeholder}
          />
          {sending && onStop ? (
            <button
              type="button"
              onClick={onStop}
              className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-neutral-900 text-white transition hover:bg-neutral-700 dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-neutral-300"
              data-testid="agent-stop"
              aria-label={t.agent.composer.stop}
              title={t.agent.composer.stop}
            >
              {/* 方块 stop icon —— 不引入新 lucide 依赖；用 span 画 6×6 白方块 */}
              <span className="block h-2.5 w-2.5 rounded-[1px] bg-white dark:bg-neutral-900" aria-hidden />
            </button>
          ) : (
            <button
              type="button"
              onClick={() => onSend()}
              disabled={value.trim().length === 0 || sending}
              className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-neutral-900 text-white transition hover:bg-neutral-700 disabled:cursor-not-allowed disabled:bg-neutral-300 dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-neutral-300 dark:disabled:bg-neutral-700 dark:disabled:text-neutral-500"
              data-testid="agent-send"
              aria-label={t.agent.composer.send}
            >
              <Send className="h-4 w-4" aria-hidden />
            </button>
          )}
        </div>
        <p className="mt-2 text-center text-[11px] text-neutral-400 dark:text-neutral-500">
          {t.agent.composer.disclaimer}
        </p>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
// 消息渲染
// ---------------------------------------------------------------------------

interface MessageBubbleProps extends PaneProps {
  message: AgentMessage;
  onRetry: () => void;
  onEditUserMessage: (msg: AgentMessage, newPrompt: string) => void;
}

function MessageBubble({ message, t, onRetry, onEditUserMessage }: MessageBubbleProps) {
  if (message.type === 'USER_PROMPT') {
    return <UserBubble message={message} t={t} onEdit={onEditUserMessage} />;
  }

  if (message.type === 'ASSISTANT_TEXT') {
    return (
      <AssistantBlock
        data-testid="agent-msg-assistant_text"
        onCopy={() => navigator.clipboard?.writeText(message.content ?? '')}
        onRetry={onRetry}
        copyLabel={t.agent.msgActions.copy}
        copiedLabel={t.agent.msgActions.copied}
        retryLabel={t.agent.msgActions.retry}
        footer={message.model ? <span>{message.model}</span> : null}
      >
        <MarkdownContent content={message.content ?? ''} />
      </AssistantBlock>
    );
  }

  if (message.type === 'TOOL_USE') {
    const p = (message.payload ?? {}) as { toolName?: string; input?: Record<string, unknown> };
    return <ToolUseCard toolName={p.toolName} input={p.input} />;
  }

  if (message.type === 'TOOL_RESULT') {
    const p = (message.payload ?? {}) as {
      toolName?: string;
      result?: { ok: boolean; output?: unknown; errorMessage?: string };
    };
    const ok = p.result?.ok ?? false;
    return (
      <ToolResultCard
        toolName={p.toolName}
        ok={ok}
        output={p.result?.output}
        errorMessage={p.result?.errorMessage}
      />
    );
  }

  return (
    <div className="text-xs text-neutral-400" data-testid={`agent-msg-${message.type.toLowerCase()}`}>
      [{message.type}]
    </div>
  );
}

// 用户消息气泡 + hover Edit 按钮 → inline textarea → Save 触发 rewind & re-submit
interface UserBubbleProps extends PaneProps {
  message: AgentMessage;
  onEdit: (msg: AgentMessage, newPrompt: string) => void;
}

function UserBubble({ message, t, onEdit }: UserBubbleProps) {
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(message.content ?? '');
  const ref = useRef<HTMLTextAreaElement>(null);

  useEffect(() => {
    if (!editing) return;
    const el = ref.current;
    if (!el) return;
    el.style.height = 'auto';
    el.style.height = `${Math.min(el.scrollHeight, 240)}px`;
  }, [editing, draft]);

  if (editing) {
    return (
      <div className="flex justify-end" data-testid="agent-msg-user_prompt-editing">
        <div className="w-full max-w-[80%] rounded-2xl bg-stone-200/70 px-4 py-3 dark:bg-neutral-800">
          <textarea
            ref={ref}
            value={draft}
            onChange={(e) => setDraft(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
                e.preventDefault();
                onEdit(message, draft);
                setEditing(false);
              }
              if (e.key === 'Escape') {
                setEditing(false);
                setDraft(message.content ?? '');
              }
            }}
            autoFocus
            rows={2}
            className="w-full resize-none bg-transparent text-sm text-neutral-900 focus:outline-none dark:text-neutral-100"
          />
          <div className="mt-2 flex justify-end gap-1">
            <button
              type="button"
              onClick={() => {
                setEditing(false);
                setDraft(message.content ?? '');
              }}
              className="rounded-md border border-stone-300 px-2 py-0.5 text-[11px] text-neutral-700 transition hover:bg-stone-100 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-700"
            >
              {t.agent.composer.cancel ?? '取消'}
            </button>
            <button
              type="button"
              onClick={() => {
                if (draft.trim().length === 0) return;
                onEdit(message, draft);
                setEditing(false);
              }}
              disabled={draft.trim().length === 0}
              className="rounded-md bg-neutral-900 px-2 py-0.5 text-[11px] text-white transition hover:bg-neutral-700 disabled:bg-neutral-400 dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-neutral-300"
            >
              {t.agent.msgActions.resubmit ?? '保存重发'}
            </button>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div className="group flex justify-end" data-testid="agent-msg-user_prompt">
      <div className="flex max-w-[80%] flex-col items-end gap-1">
        <div className="rounded-2xl bg-stone-200/70 px-4 py-2 text-sm text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100">
          {message.content}
        </div>
        {/* 不渲染 optimistic-* 的 edit（DB 还没存）*/}
        {message.id.startsWith('optimistic-') ? null : (
          <button
            type="button"
            onClick={() => {
              setDraft(message.content ?? '');
              setEditing(true);
            }}
            className="rounded-md px-1.5 py-0.5 text-[11px] text-neutral-500 opacity-0 transition hover:bg-stone-200/60 group-hover:opacity-100 dark:text-neutral-400 dark:hover:bg-neutral-700"
            aria-label={t.agent.msgActions.edit ?? '编辑'}
          >
            ✎ {t.agent.msgActions.edit ?? '编辑'}
          </button>
        )}
      </div>
    </div>
  );
}

// 助手裸文本块 + hover action bar（Copy / Retry）
interface AssistantBlockProps {
  children: React.ReactNode;
  onCopy?: () => void;
  onRetry?: () => void;
  copyLabel?: string;
  copiedLabel?: string;
  retryLabel?: string;
  footer?: React.ReactNode;
  'data-testid'?: string;
}

function AssistantBlock({
  children,
  onCopy,
  onRetry,
  copyLabel,
  copiedLabel,
  retryLabel,
  footer,
  'data-testid': dt,
}: AssistantBlockProps) {
  const [copied, setCopied] = useState(false);
  return (
    <div className="group" data-testid={dt}>
      <div className="text-sm leading-7 text-neutral-900 dark:text-neutral-100">
        {children}
      </div>
      {footer ? (
        <div className="mt-1 text-[10px] text-neutral-400 dark:text-neutral-500">{footer}</div>
      ) : null}
      {(onCopy || onRetry) ? (
        <div className="mt-1.5 flex items-center gap-1 opacity-0 transition group-hover:opacity-100">
          {onCopy ? (
            <button
              type="button"
              onClick={() => {
                onCopy();
                setCopied(true);
                setTimeout(() => setCopied(false), 1200);
              }}
              className="flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] text-neutral-500 transition hover:bg-stone-200/60 dark:text-neutral-400 dark:hover:bg-neutral-800"
              aria-label={copyLabel}
            >
              {copied ? (
                <>
                  <Check className="h-3 w-3" aria-hidden />
                  {copiedLabel}
                </>
              ) : (
                <>
                  <Copy className="h-3 w-3" aria-hidden />
                  {copyLabel}
                </>
              )}
            </button>
          ) : null}
          {onRetry ? (
            <button
              type="button"
              onClick={onRetry}
              className="flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] text-neutral-500 transition hover:bg-stone-200/60 dark:text-neutral-400 dark:hover:bg-neutral-800"
              aria-label={retryLabel}
            >
              <RefreshCw className="h-3 w-3" aria-hidden />
              {retryLabel}
            </button>
          ) : null}
        </div>
      ) : null}
    </div>
  );
}

// ---------------------------------------------------------------------------
// 右：Artifact 抽屉（slide-in，有 artifact 自动开，用户可关）
// ---------------------------------------------------------------------------

interface ArtifactDrawerProps extends PaneProps {
  artifacts: AgentArtifact[];
  open: boolean;
  onClose: () => void;
}

function ArtifactDrawer({ t, artifacts, open, onClose }: ArtifactDrawerProps) {
  return (
    <aside
      className={`fixed right-0 top-0 z-20 h-full w-[360px] transform border-l border-stone-200 bg-white shadow-xl transition-transform duration-300 dark:border-neutral-800 dark:bg-neutral-900 ${
        open ? 'translate-x-0' : 'translate-x-full'
      }`}
      data-testid="agent-context-pane"
      aria-hidden={!open}
    >
      <div className="flex items-center justify-between border-b border-stone-200 px-4 py-3 dark:border-neutral-800">
        <div className="text-sm font-semibold text-neutral-900 dark:text-neutral-100">
          {t.agent.artifact.title}
          {artifacts.length > 0 ? (
            <span className="ml-2 text-xs font-normal text-neutral-400">· {artifacts.length}</span>
          ) : null}
        </div>
        <button
          type="button"
          onClick={onClose}
          className="rounded-md p-1 text-neutral-500 transition hover:bg-stone-100 dark:text-neutral-400 dark:hover:bg-neutral-800"
          aria-label={t.agent.artifact.close}
        >
          <X className="h-4 w-4" aria-hidden />
        </button>
      </div>
      <div className="flex-1 space-y-3 overflow-y-auto px-4 py-4">
        {artifacts.length === 0 ? (
          <p className="text-xs leading-relaxed text-neutral-500 dark:text-neutral-400">
            {t.agent.context.empty}
          </p>
        ) : (
          artifacts.map((a) => <ArtifactCard key={a.id} artifact={a} />)
        )}
      </div>
    </aside>
  );
}

function ArtifactCard({ artifact }: { artifact: AgentArtifact }) {
  const wrap = (body: React.ReactNode) => (
    <div
      className="rounded-lg border border-stone-200 bg-stone-50 p-3 dark:border-neutral-800 dark:bg-neutral-900/40"
      data-testid={`agent-artifact-${artifact.id}`}
    >
      <div className="mb-1.5 text-[11px] font-semibold text-neutral-700 dark:text-neutral-300">
        {artifact.title}
      </div>
      {body}
    </div>
  );

  if (artifact.type === 'TABLE') {
    const data = artifact.data as {
      columns: Array<{ key: string; label: string }>;
      rows: Array<Record<string, unknown>>;
    };
    return wrap(
      <div className="max-h-72 overflow-auto">
        <table className="w-full text-[10px] text-neutral-700 dark:text-neutral-300">
          <thead className="bg-stone-100 dark:bg-neutral-800">
            <tr>
              {data.columns?.map((c) => (
                <th key={c.key} className="px-1.5 py-1 text-left font-medium">
                  {c.label}
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            {(data.rows ?? []).slice(0, 20).map((row, i) => (
              <tr key={i} className="border-t border-stone-200 dark:border-neutral-800">
                {data.columns?.map((c) => (
                  <td key={c.key} className="px-1.5 py-1 align-top">
                    {formatCell(row[c.key])}
                  </td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </div>,
    );
  }

  if (artifact.type === 'CHART') {
    const raw = artifact.data as Record<string, unknown>;
    const series = (raw.series as Array<{ label: string; value: number }> | undefined)
      ?? ((raw.labels as string[] | undefined)?.map((label, i) => ({
        label,
        value: Number((raw.values as number[] | undefined)?.[i] ?? 0),
      })) ?? []);
    const unit = String(raw.unit ?? '');
    const max = Math.max(1, ...series.map((s) => Number(s.value) || 0));
    return wrap(
      <div className="max-h-72 space-y-1 overflow-auto">
        {series.length === 0 ? (
          <div className="text-[10px] text-neutral-500">（图表无数据）</div>
        ) : (
          series.map((s, i) => (
            <div key={i} className="flex items-center gap-1 text-[10px] text-neutral-700 dark:text-neutral-300">
              <div className="w-20 truncate">{s.label}</div>
              <div className="flex-1 bg-stone-200 dark:bg-neutral-800">
                <div
                  className="h-3 bg-blue-500"
                  style={{ width: `${Math.max(2, (Number(s.value) / max) * 100)}%` }}
                />
              </div>
              <div className="w-12 text-right tabular-nums">
                {Number(s.value).toLocaleString()}
                {unit}
              </div>
            </div>
          ))
        )}
      </div>,
    );
  }

  if (artifact.type === 'FILE') {
    const data = artifact.data as { url?: string; mime?: string; size?: number };
    const sizeKB = data.size ? (data.size / 1024).toFixed(1) : null;
    const href = artifact.previewUrl ?? data.url ?? null;
    return wrap(
      <div className="text-[11px] text-neutral-700 dark:text-neutral-300">
        <div className="flex items-center gap-2">
          <span className="rounded bg-stone-200 px-1 py-0.5 text-[9px] uppercase dark:bg-neutral-800">
            {(data.mime ?? artifact.mimeType ?? 'file').split('/').pop()}
          </span>
          {href ? (
            <a
              href={href}
              target="_blank"
              rel="noopener noreferrer"
              className="truncate text-blue-600 hover:underline"
            >
              {data.url ?? '下载/查看'}
            </a>
          ) : (
            <span className="text-neutral-500">（无下载链接）</span>
          )}
        </div>
        {sizeKB ? <div className="text-neutral-500">{sizeKB} KB</div> : null}
      </div>,
    );
  }

  if (artifact.type === 'LINK') {
    const data = artifact.data as { url?: string };
    return wrap(
      data.url ? (
        <a
          href={data.url}
          target="_blank"
          rel="noopener noreferrer"
          className="block break-all text-[11px] text-blue-600 hover:underline"
        >
          {data.url}
        </a>
      ) : (
        <div className="text-[11px] text-neutral-500">（缺 url）</div>
      ),
    );
  }

  if (artifact.type === 'TEXT') {
    const data = artifact.data as { body?: string };
    return wrap(
      <pre className="max-h-72 overflow-auto whitespace-pre-wrap break-words text-[10px] text-neutral-700 dark:text-neutral-300">
        {data.body ?? '(空)'}
      </pre>,
    );
  }

  return wrap(<div className="text-[11px] text-neutral-500">未知类型：{artifact.type}</div>);
}

function formatCell(v: unknown): string {
  if (v == null) return '—';
  if (typeof v === 'string') return v.length > 60 ? v.slice(0, 60) + '…' : v;
  if (typeof v === 'object') return JSON.stringify(v).slice(0, 60);
  return String(v);
}


