import { type EditorEngineContract, useCoreServices } from '@learnitive/context';
import { MARKDOWN_AI_REQUEST_EVENT, MARKDOWN_CODE_RUN_EVENT, type MarkdownAiRequestDetail } from '@learnitive/editor';
import { waitForSaveQueueToDrain } from '@learnitive/service-core';
import { EDITOR_GUTTER_PADDING_LEFT } from '@learnitive/constants';
import { isProjectCodeTextPath, isLearnitiveEditableTextPath, projectFilePath } from '@learnitive/project-storage';
import { projectCapabilitiesForProfile, projectModeForProfile, type ProjectProfile } from '@learnitive/project-runtime';
import { Button, toast } from '@learnitive/ui-components';
import { WsPath } from '@learnitive/ws-path';
import { useAtom, useAtomValue } from 'jotai';
import { Code2, Download, Eye, FileDown, FlaskConical, GitCompareArrows, ListTree, MessageSquare, MessageSquarePlus, MessageSquareQuote, PanelRightOpen, PenTool, RefreshCw, Sparkles, SquareTerminal, Wrench } from 'lucide-react';
import React, {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  useSyncExternalStore,
} from 'react';
import { LatexDocumentController } from '../latex-document-controller';
import { InteractiveDocProjectEditor } from '../interactive-content/project-editor';
import { migrateLegacyInteractiveDocProject } from '../interactive-content/legacy-project-migration';
import { LinkedMentions } from '../components/backlinks/linked-mentions';
import { AiChatShell } from '../components/ai-chat-shell';
import { AiReviewDialog } from '../components/ai-review-dialog';
import { CodeAiReviewDialog } from '../components/code-ai-review-dialog';
import { BibliographyVisualEditor } from '../components/bibliography-visual-editor';
import { EditorSurface } from '../components/editor-surface';
import { EditorFileTabs } from '../components/editor-file-tabs';
import { FloatingResearchDock } from '../components/floating-research-dock';
import { NoteNotFoundView } from '../components/feedback/note-not-found-view';
import { WorkspaceNotFoundView } from '../components/feedback/workspace-not-found-view';
import { LatexVisualEditor } from '../components/latex-visual-editor';
import { LinuxTerminalPro } from '../components/linux-terminal-pro';
import { PdfPreviewPane } from '../components/pdf-preview-pane';
import { ProjectOutlinePanel } from '../components/project-outline-panel';
import { ProjectExecutionPane } from '../components/project-execution-pane';
import { MarkdownCodeRunDialog, type MarkdownCodeRunRequest } from '../components/markdown-code-run-dialog';
import { ProjectToolsDialog } from '../components/project-tools-dialog';
import { TikzVisualEditor } from '../components/tikz-visual-editor';
import { ProjectTerminal } from '../components/project-terminal';
import { ReviewSidebar } from '../components/review-sidebar';
import { ReviewCommentDialog, type ReviewCommentTarget } from '../components/review-comment-dialog';
import { AppHeader } from '../layout/app-header';
import { PageContentContainer } from '../layout/main-content-container';
import type { PaperMemoryManuscriptActionRequest, PaperMemoryManuscriptSelection, PaperMemoryOpenPaperRequest } from '../paper-memory-manuscript';
import { synchronizeSandboxProject } from '../saas/sandbox-project-sync';
import { ensureCompileAuthentication } from '../compile-auth-gate';

const MAIN_EDITOR_NAME = 'main-editor';
const EDITOR_CONTENT_PADDING = `pr-4 md:pr-6 ${EDITOR_GUTTER_PADDING_LEFT}`;
const LATEX_SOURCE_CONTENT_PADDING = 'px-2 md:px-2';
const EDITOR_PDF_SPLIT_STORAGE_KEY = 'learnitive:editor-pdf-split:v2';
const DEFAULT_EDITOR_SPLIT_PERCENT = 60;
const MIN_EDITOR_SPLIT_PERCENT = 30;
const MAX_EDITOR_SPLIT_PERCENT = 80;

type MobileOutputTabRequest = {
  id: number;
  tab: 'pdf' | 'log';
};

function clampEditorSplit(value: number): number {
  return Math.min(MAX_EDITOR_SPLIT_PERCENT, Math.max(MIN_EDITOR_SPLIT_PERCENT, value));
}

function readEditorSplit(): number {
  if (typeof window === 'undefined') return DEFAULT_EDITOR_SPLIT_PERCENT;
  const stored = window.localStorage.getItem(EDITOR_PDF_SPLIT_STORAGE_KEY);
  if (stored === null) return DEFAULT_EDITOR_SPLIT_PERCENT;
  const parsed = Number(stored);
  return Number.isFinite(parsed)
    ? clampEditorSplit(parsed)
    : DEFAULT_EDITOR_SPLIT_PERCENT;
}

function splitGridColumns(editorPercent: number): string {
  const editor = clampEditorSplit(editorPercent);
  return `minmax(0, ${editor}fr) auto minmax(0, ${100 - editor}fr)`;
}

function EditorPdfResizer({
  containerRef,
  value,
  onPreview,
  onCommit,
  onReset,
  secondaryLabel = 'PDF',
  testId = 'interactive-editor-pdf-resizer',
}: {
  containerRef: React.RefObject<HTMLDivElement | null>;
  value: number;
  onPreview: (value: number) => void;
  onCommit: (value: number) => void;
  onReset: () => void;
  secondaryLabel?: string;
  testId?: string;
}) {
  const dragRef = useRef<
    { pointerId: number; value: number; startValue: number } | undefined
  >(undefined);
  const documentStyleRef = useRef<{ cursor: string; userSelect: string } | undefined>(undefined);
  const [isDragging, setIsDragging] = useState(false);

  const ratioFromClientX = useCallback(
    (clientX: number) => {
      const rect = containerRef.current?.getBoundingClientRect();
      if (!rect || rect.width <= 0) return value;
      return clampEditorSplit(((clientX - rect.left) / rect.width) * 100);
    },
    [containerRef, value],
  );

  const startDocumentDrag = useCallback(() => {
    if (typeof document === 'undefined' || documentStyleRef.current) return;
    documentStyleRef.current = {
      cursor: document.body.style.cursor,
      userSelect: document.body.style.userSelect,
    };
    document.body.style.cursor = 'col-resize';
    document.body.style.userSelect = 'none';
  }, []);

  const stopDocumentDrag = useCallback(() => {
    if (typeof document === 'undefined') return;
    const previous = documentStyleRef.current;
    documentStyleRef.current = undefined;
    if (!previous) return;
    document.body.style.cursor = previous.cursor;
    document.body.style.userSelect = previous.userSelect;
  }, []);

  useEffect(() => stopDocumentDrag, [stopDocumentDrag]);

  const finishDrag = useCallback(
    (target: HTMLButtonElement, pointerId: number) => {
      const drag = dragRef.current;
      if (!drag || drag.pointerId !== pointerId) return;
      dragRef.current = undefined;
      if (target.hasPointerCapture(pointerId)) target.releasePointerCapture(pointerId);
      setIsDragging(false);
      stopDocumentDrag();
      onCommit(drag.value);
    },
    [onCommit, stopDocumentDrag],
  );

  const cancelDrag = useCallback(
    (target: HTMLButtonElement, pointerId: number) => {
      const drag = dragRef.current;
      if (!drag || drag.pointerId !== pointerId) return;
      dragRef.current = undefined;
      if (target.hasPointerCapture(pointerId)) target.releasePointerCapture(pointerId);
      setIsDragging(false);
      stopDocumentDrag();
      onPreview(drag.startValue);
    },
    [onPreview, stopDocumentDrag],
  );

  const roundedEditorPercent = Math.round(value);
  const roundedSecondaryPercent = 100 - roundedEditorPercent;

  return (
    <button
      aria-label={`Resize editor and ${secondaryLabel} panes`}
      aria-orientation="vertical"
      aria-valuemax={MAX_EDITOR_SPLIT_PERCENT}
      aria-valuemin={MIN_EDITOR_SPLIT_PERCENT}
      aria-valuenow={roundedEditorPercent}
      aria-valuetext={`${roundedEditorPercent}% editor, ${roundedSecondaryPercent}% ${secondaryLabel}`}
      className={`latex-bedrive-resizer interactive-drive-browser-editor-resizer hidden lg:flex${isDragging ? ' is-dragging' : ''}`}
      data-testid={testId}
      role="separator"
      title={`Resize editor and ${secondaryLabel} panes — ${roundedEditorPercent}% editor, ${roundedSecondaryPercent}% ${secondaryLabel}`}
      type="button"
      onDoubleClick={onReset}
      onKeyDown={(event) => {
        if (event.key === 'Home') {
          event.preventDefault();
          onReset();
          return;
        }
        if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
        event.preventDefault();
        const step = event.shiftKey ? 5 : 2;
        onCommit(
          clampEditorSplit(
            value + (event.key === 'ArrowRight' ? step : -step),
          ),
        );
      }}
      onPointerCancel={(event) => cancelDrag(event.currentTarget, event.pointerId)}
      onPointerDown={(event) => {
        if (event.button !== 0) return;
        event.preventDefault();
        const next = ratioFromClientX(event.clientX);
        dragRef.current = {
          pointerId: event.pointerId,
          value: next,
          startValue: value,
        };
        event.currentTarget.setPointerCapture(event.pointerId);
        setIsDragging(true);
        startDocumentDrag();
        onPreview(next);
      }}
      onPointerMove={(event) => {
        const drag = dragRef.current;
        if (!drag || drag.pointerId !== event.pointerId) return;
        const next = ratioFromClientX(event.clientX);
        drag.value = next;
        onPreview(next);
      }}
      onPointerUp={(event) => finishDrag(event.currentTarget, event.pointerId)}
    >
      <span className="latex-bedrive-resizer-handle" aria-hidden="true">
        <svg
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2.5"
          strokeLinecap="round"
          strokeLinejoin="round"
        >
          <title>{`Resize editor and ${secondaryLabel} pane`}</title>
          <polyline points="15 6 9 12 15 18" />
        </svg>
        <svg
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2.5"
          strokeLinecap="round"
          strokeLinejoin="round"
        >
          <title>{`Resize editor and ${secondaryLabel} pane`}</title>
          <polyline points="9 6 15 12 9 18" />
        </svg>
      </span>
    </button>
  );
}

type SaveStatusSource = Pick<
  EditorEngineContract,
  'hasPendingOrFailedSave' | 'subscribeToSaveStatus'
>;

function useHasPendingSave(
  saveStatus: SaveStatusSource,
  wsPath: string | undefined,
): boolean {
  const subscribe = useCallback(
    (onStoreChange: () => void) =>
      wsPath === undefined
        ? () => {}
        : saveStatus.subscribeToSaveStatus(onStoreChange, wsPath),
    [saveStatus, wsPath],
  );
  const getSnapshot = useCallback(
    () => wsPath !== undefined && saveStatus.hasPendingOrFailedSave(wsPath),
    [saveStatus, wsPath],
  );

  return useSyncExternalStore(subscribe, getSnapshot);
}

function LatexDocumentTransition({
  wsPath,
  status,
  onRetry,
}: {
  wsPath: string;
  status: 'loading' | 'error';
  onRetry: () => void;
}) {
  const fileName = WsPath.safeParseFile(wsPath).data?.fileName ?? wsPath;
  if (status === 'loading') {
    return (
      <div
        className="flex min-h-0 flex-1 items-center justify-center p-6 text-muted-foreground"
        data-testid="latex-document-loading"
        data-requested-ws-path={wsPath}
      >
        Loading {fileName}…
      </div>
    );
  }

  return (
    <div
      className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 p-6"
      data-testid="latex-document-load-error"
      data-requested-ws-path={wsPath}
      role="alert"
    >
      <p className="font-medium">Unable to load {fileName}.</p>
      <Button variant="outline" onClick={onRetry}>
        Retry
      </Button>
    </div>
  );
}

export function PageEditor() {
  const coreServices = useCoreServices();
  const [latexMode, setLatexMode] = useAtom(coreServices.workbenchState.$latexEditorMode);
  const [outlineOpen, setOutlineOpen] = useAtom(coreServices.workbenchState.$projectNavigatorOpen);
  const [projectToolsOpen, setProjectToolsOpen] = useAtom(coreServices.workbenchState.$projectToolsOpen);
  const projectToolsTab = useAtomValue(coreServices.workbenchState.$projectToolsTab);
  const projectToolsTextScope = useAtomValue(coreServices.workbenchState.$projectToolsTextScope);
  const pdfFocusRequest = useAtomValue(coreServices.workbenchState.$pdfFocusRequest);
  const [editorSplitPercent, setEditorSplitPercent] = useState(readEditorSplit);
  const [mobileOutputOpen, setMobileOutputOpen] = useState(false);
  const [mobileOutputTabRequest, setMobileOutputTabRequest] =
    useState<MobileOutputTabRequest>({ id: 0, tab: 'pdf' });
  const [paperMemorySelection, setPaperMemorySelection] = useState<PaperMemoryManuscriptSelection>();
  const [reviewOpen, setReviewOpen] = useState(false);
  const [aiReviewOpen, setAiReviewOpen] = useState(false);
  const [activeReviewThreadId, setActiveReviewThreadId] = useState<string>();
  const [reviewCommentTarget, setReviewCommentTarget] = useState<ReviewCommentTarget>();
  const [paperMemoryActionRequest, setPaperMemoryActionRequest] = useState<PaperMemoryManuscriptActionRequest>();
  const [aiTabRequest, setAiTabRequest] = useState<{ id: number; tab: 'assistant' | 'papers' }>();
  const [paperOpenRequest, setPaperOpenRequest] = useState<PaperMemoryOpenPaperRequest>();
  const [aiChatOpen, setAiChatOpen] = useState(false);
  const [markdownCodeRunRequest, setMarkdownCodeRunRequest] = useState<MarkdownCodeRunRequest>();
  const [markdownAiInputRequest, setMarkdownAiInputRequest] = useState<{ id: number; text: string }>();
  const [markdownExportBusy, setMarkdownExportBusy] = useState<'html' | 'pdf'>();
  const [legacyDocMigration, setLegacyDocMigration] = useState<{ wsName: string; status: 'migrating' | 'failed'; error?: string }>();
  const [aiAgentRequest, setAiAgentRequest] = useState<{ id: number; text: string }>();
  const consumeAiAgentRequest = useCallback((id: number) => {
    setAiAgentRequest((current) => current?.id === id ? undefined : current);
  }, []);
  const [terminalOpen, setTerminalOpen] = useState(false);
  const [linuxTerminalOpen, setLinuxTerminalOpen] = useState(false);
  const [tikzEditorOpen, setTikzEditorOpen] = useState(false);
  const [projectProfile, setProjectProfile] = useState<ProjectProfile>();
  const initializedProjectOutputRef = useRef<string>();
  const openLocalPaper = useCallback((paperId: string, page?: number) => {
    setPaperOpenRequest((current) => ({ id: (current?.id ?? 0) + 1, paperId, ...(page ? { page } : {}) }));
    setAiTabRequest((current) => ({ id: (current?.id ?? 0) + 1, tab: 'papers' }));
    setAiChatOpen(true);
  }, []);
  const splitWorkspaceRef = useRef<HTMLDivElement>(null);
  const previewEditorSplit = useCallback((percent: number) => {
    setEditorSplitPercent(clampEditorSplit(percent));
  }, []);
  const commitEditorSplit = useCallback((percent: number) => {
    const next = clampEditorSplit(percent);
    setEditorSplitPercent(next);
    if (typeof window !== 'undefined') {
      window.localStorage.setItem(EDITOR_PDF_SPLIT_STORAGE_KEY, String(next));
    }
  }, []);
  const resetEditorSplit = useCallback(() => {
    commitEditorSplit(DEFAULT_EDITOR_SPLIT_PERCENT);
  }, [commitEditorSplit]);
  const requestPaperMemoryAction = useCallback((action: PaperMemoryManuscriptActionRequest['action']) => {
    setPaperMemoryActionRequest((current) => ({ id: (current?.id ?? 0) + 1, action }));
  }, []);
  const changeAiChatOpen = useCallback((open: boolean) => {
    setAiChatOpen(open);
    if (open) { setTerminalOpen(false); setLinuxTerminalOpen(false); }
  }, []);
  const changeTerminalOpen = useCallback((open: boolean) => {
    setTerminalOpen(open);
    if (open) { setAiChatOpen(false); setLinuxTerminalOpen(false); }
  }, []);
  const toggleAiChat = useCallback(() => {
    const next = !aiChatOpen;
    if (next) {
      // Manual chat opening always starts in Assistant. Paper/Evidence actions
      // still explicitly request the Papers tab, but a stale request from an
      // earlier session must never change the default on reopen.
      setPaperMemoryActionRequest(undefined);
      setAiTabRequest((current) => ({ id: (current?.id ?? 0) + 1, tab: 'assistant' }));
      setTerminalOpen(false);
      setLinuxTerminalOpen(false);
    }
    setAiChatOpen(next);
  }, [aiChatOpen]);
  const toggleTerminal = useCallback(() => {
    const next = !terminalOpen;
    setTerminalOpen(next);
    if (next) { setAiChatOpen(false); setLinuxTerminalOpen(false); }
  }, [terminalOpen]);
  const changeLinuxTerminalOpen = useCallback((open: boolean) => {
    setLinuxTerminalOpen(open);
    if (open) {
      setAiChatOpen(false);
      setTerminalOpen(false);
    }
  }, []);
  const currentWsPath = useAtomValue(coreServices.workspaceState.$currentWsPath);
  const currentWsFilePath = useAtomValue(coreServices.workspaceState.$currentWsFilePath);
  const currentWsName = useAtomValue(coreServices.workspaceState.$currentWsName);
  useEffect(() => {
    if (!currentWsName) return;
    let cancelled = false;
    void coreServices.workspaceOps.getWorkspaceInfo(currentWsName).then(async (workspace) => {
      if (cancelled) return;
      const request = workspace?.metadata?.aiBuildRequest;
      if (!request || typeof request !== 'object') return;
      const id = Number((request as { id?: unknown }).id);
      const text = String((request as { text?: unknown }).text ?? '').trim();
      if (!Number.isFinite(id) || !text) return;
      await coreServices.workspaceOps.updateWorkspaceMetadata(currentWsName, (metadata) => {
        const { aiBuildRequest: _consumed, ...rest } = metadata as Record<string, unknown>;
        return rest;
      });
      if (cancelled) return;
      setAiAgentRequest({ id, text });
      setAiTabRequest((current) => ({ id: (current?.id ?? 0) + 1, tab: 'assistant' }));
      setAiChatOpen(true);
      setTerminalOpen(false);
      setLinuxTerminalOpen(false);
    }).catch((error) => console.warn('[Learnitive] Unable to start Build with AI request.', error));
    return () => { cancelled = true; };
  }, [coreServices.workspaceOps, currentWsName]);
  useEffect(() => {
    if (!currentWsName) return;
    let cancelled = false;
    let timer: ReturnType<typeof setInterval> | undefined;
    let syncing = false;
    const sync = async () => {
      if (syncing || cancelled || (typeof document !== 'undefined' && document.visibilityState === 'hidden')) return;
      syncing = true;
      try {
        const workspace = await coreServices.workspaceOps.getWorkspaceInfo(currentWsName).catch(() => undefined);
        const projectId = typeof workspace?.metadata?.projectId === 'string' ? workspace.metadata.projectId : undefined;
        if (!projectId || cancelled) return;
        const result = await synchronizeSandboxProject(coreServices, currentWsName, projectId);
        if (result.conflicts.length) console.warn('[Learnitive] Sandbox sync conflicts require user review:', result.conflicts);
      } catch (error) {
        console.warn('[Learnitive] Development environment sync unavailable; local project remains authoritative.', error);
      } finally {
        syncing = false;
      }
    };
    void sync();
    timer = setInterval(() => void sync(), 5000);
    return () => { cancelled = true; if (timer) clearInterval(timer); };
  }, [coreServices, currentWsName]);
  const compileState = useAtomValue(coreServices.projectCompile.$state);
  const universalCompileState = useAtomValue(coreServices.projectCompileOrchestrator.$state);
  const livePdfState = useAtomValue(coreServices.projectLiveDraft.$livePdfState);
  const reviewVersion = useAtomValue(coreServices.projectReview.$version);
  const lastAiSuggestion = useAtomValue(coreServices.projectAiWriting.$lastSuggestion);
  const handledAiSuggestionId = useRef(lastAiSuggestion?.id);
  const collaborationState = useAtomValue(coreServices.projectCollaboration.$state);
  const trackChangesEnabled = currentWsName
    ? coreServices.projectReview.getTrackChangesEnabled(currentWsName)
    : false;
  const openReviewThreadCount = currentWsName
    ? coreServices.projectReview.getThreads(currentWsName, { status: 'open' }).length
    : 0;
  const pendingReviewChangeCount = currentWsName
    ? coreServices.projectReview.getChanges(currentWsName, { status: 'pending' }).length
    : 0;
  const reviewAttentionCount = openReviewThreadCount + pendingReviewChangeCount;
  const collaborationActiveHere = Boolean(
    currentWsName && collaborationState.active && collaborationState.wsName === currentWsName,
  );
  void reviewVersion;
  useAtomValue(coreServices.projectPdf.$version);
  const forceReloadCounter = useAtomValue(coreServices.editorService.$forceReloadCounter);
  const routeWsPath = useAtomValue(coreServices.navigation.$wsFilePath);
  const routeWsName = useAtomValue(coreServices.navigation.$wsName);
  const workspaceFilePaths = useAtomValue(coreServices.workspaceState.$wsPaths).map((item) => item.path);
  const isLatexSource = Boolean(
    routeWsPath && isLearnitiveEditableTextPath(routeWsPath.path),
  );
  const isProjectCodeSource = Boolean(
    routeWsPath && isProjectCodeTextPath(routeWsPath.path),
  );

  useEffect(() => {
    if (!currentWsName) { setProjectProfile(undefined); return; }
    let cancelled = false;
    void coreServices.projectExecution.resolveProfile(currentWsName, routeWsPath?.path).then(async (resolvedProfile) => {
      let profile = resolvedProfile;
      const legacyInteractiveDoc = resolvedProfile.modeId === 'interactive-doc' || resolvedProfile.projectType === 'interactive-doc';
      if (legacyInteractiveDoc) {
        setLegacyDocMigration({ wsName: currentWsName, status: 'migrating' });
        try {
          const migration = await migrateLegacyInteractiveDocProject(coreServices, currentWsName, resolvedProfile);
          if (cancelled) return;
          profile = migration.profile;
          setLegacyDocMigration(undefined);
          if (migration.migrated) {
            toast.success('Interactive Doc migrated to Markdown. The original structured source was backed up.');
            if (!routeWsPath || routeWsPath.path === 'document.interactive.json') {
              coreServices.navigation.goWsFile(WsPath.fromParts(currentWsName, migration.entry).wsPath);
            }
          }
        } catch (error) {
          if (cancelled) return;
          const message = error instanceof Error ? error.message : String(error);
          // Keep the legacy editor available only as a recovery fallback. New
          // projects never expose Interactive Doc in the creation catalog.
          setLegacyDocMigration({ wsName: currentWsName, status: 'failed', error: message });
          toast.error(message);
        }
      } else {
        setLegacyDocMigration((current) => current?.wsName === currentWsName ? undefined : current);
      }
      if (cancelled) return;
      setProjectProfile(profile);
      const key = `${currentWsName}:${projectModeForProfile(profile)?.id || profile.projectType}`;
      if (initializedProjectOutputRef.current !== key) {
        initializedProjectOutputRef.current = key;
        const desktop = typeof window !== 'undefined' && window.matchMedia('(min-width: 1024px)').matches;
        const mode = projectModeForProfile(profile);
        coreServices.projectCompileOrchestrator.setOutputVisibility(currentWsName, profile, desktop && mode?.id !== 'markdown');
      } else if (universalCompileState.wsName !== currentWsName) {
        coreServices.projectCompileOrchestrator.setOutputVisibility(currentWsName, profile, universalCompileState.outputOpen);
      }
    }).catch(() => { if (!cancelled) setProjectProfile(undefined); });
    return () => { cancelled = true; };
  }, [coreServices.projectCompileOrchestrator, coreServices.projectExecution, currentWsName, routeWsPath?.path, universalCompileState.outputOpen, universalCompileState.wsName]);

  useEffect(() => {
    if (!currentWsName) return;
    void collaborationState.active;
    void collaborationState.roomId;
    void coreServices.projectReview.refresh(currentWsName);
  }, [collaborationState.active, collaborationState.roomId, coreServices.projectReview, currentWsName]);

  useEffect(() => {
    if (!currentWsName || collaborationState.active) return;
    void coreServices.projectCollaboration.reconnectJoinedProject(currentWsName);
  }, [collaborationState.active, coreServices.projectCollaboration, currentWsName]);

  const openReviewThread = useCallback((threadId: string) => {
    if (!currentWsName || !coreServices.projectReview.getThread(currentWsName, threadId)) return;
    setActiveReviewThreadId(threadId);
    setReviewOpen(true);
  }, [coreServices.projectReview, currentWsName]);

  const toggleTrackChanges = useCallback(() => {
    if (!currentWsName) return;
    if (trackChangesEnabled && coreServices.projectReview.getChanges(currentWsName, { status: 'pending' }).length > 0) {
      setReviewOpen(true);
      toast.error('Accept or reject pending changes before turning Track Changes off.');
      return;
    }
    void coreServices.projectReview.setTrackChangesEnabled(currentWsName, !trackChangesEnabled).then(() => {
      if (!trackChangesEnabled) setReviewOpen(true);
    });
  }, [coreServices.projectReview, currentWsName, trackChangesEnabled]);

  useEffect(() => {
    if (!lastAiSuggestion || handledAiSuggestionId.current === lastAiSuggestion.id) return;
    handledAiSuggestionId.current = lastAiSuggestion.id;
    if (!currentWsName || lastAiSuggestion.wsName !== currentWsName) return;
    setActiveReviewThreadId(lastAiSuggestion.reviewThreadId);
    // AI suggestions should never sit hidden behind the editor. Open Review
    // only when needed; if it is already open, keep it open and focus the new item.
    setReviewOpen(true);
  }, [currentWsName, lastAiSuggestion]);

  useEffect(() => {
    if (!activeReviewThreadId || !currentWsName) return;
    if (!coreServices.projectReview.getThread(currentWsName, activeReviewThreadId)) {
      setActiveReviewThreadId(undefined);
    }
  }, [activeReviewThreadId, coreServices.projectReview, currentWsName, reviewVersion]);

  const latexDocumentController = useMemo(
    () => new LatexDocumentController(coreServices.latexSource),
    [coreServices.latexSource],
  );
  const latexDocument = useSyncExternalStore(
    latexDocumentController.subscribe,
    latexDocumentController.getSnapshot,
    latexDocumentController.getSnapshot,
  );

  const requestedLatexWsPath = isLatexSource ? routeWsPath?.wsPath : undefined;

  useEffect(() => {
    if (requestedLatexWsPath) {
      void latexDocumentController.requestPath(requestedLatexWsPath);
    } else {
      latexDocumentController.clear();
    }
  }, [latexDocumentController, requestedLatexWsPath]);

  useEffect(
    () => () => latexDocumentController.dispose(),
    [latexDocumentController],
  );

  const hasPendingMarkdownSave = useHasPendingSave(
    coreServices.editorEngine,
    isLatexSource || isProjectCodeSource ? undefined : routeWsPath?.wsPath,
  );
  const markdownEditorWsPath =
    currentWsPath ?? (hasPendingMarkdownSave ? routeWsPath : undefined);

  // A source document is renderable only when the controller says the exact
  // route request is active. An older active/load result can never be reused
  // as a fallback for a newer filename.
  const sourceRouteWsPath =
    isLatexSource &&
    routeWsPath &&
    latexDocument.status === 'ready' &&
    latexDocument.requestedPath === routeWsPath.wsPath &&
    latexDocument.activePath === routeWsPath.wsPath
      ? routeWsPath
      : undefined;
  const editorWsPath = isLatexSource
    ? sourceRouteWsPath
    : isProjectCodeSource
      ? currentWsFilePath
      : markdownEditorWsPath;

  const editorKey = useMemo(() => {
    if (!editorWsPath) return `${MAIN_EDITOR_NAME}:${forceReloadCounter}`;
    if (isLatexSource || isProjectCodeSource) {
      return `editor::${MAIN_EDITOR_NAME}:${editorWsPath.wsPath}`;
    }
    return `editor::${MAIN_EDITOR_NAME}:${editorWsPath.wsPath}:${forceReloadCounter}`;
  }, [editorWsPath, forceReloadCounter, isLatexSource, isProjectCodeSource]);

  const visualSupported = Boolean(
    editorWsPath && /\.(?:tex|ltx|bib)$/i.test(editorWsPath.fileName),
  );

  const editorWsPathValue = editorWsPath?.wsPath;
  const editorProjectPath = editorWsPath?.path;

  useEffect(() => {
    if (editorWsPathValue && (!visualSupported || collaborationActiveHere)) setLatexMode('source');
  }, [collaborationActiveHere, editorWsPathValue, setLatexMode, visualSupported]);


  useEffect(() => {
    setPaperMemorySelection(undefined);
  }, [editorWsPathValue, latexMode]);

  const currentPdfWorkspace = currentWsName
    ? coreServices.projectPdf.getWorkspaceState(currentWsName)
    : undefined;
  const currentLivePdf = livePdfState.wsName === currentWsName ? livePdfState : undefined;
  const hasCompiledPdf = Boolean(
    currentPdfWorkspace?.pdf?.byteLength ||
      (currentLivePdf?.enabled && currentLivePdf.pdf?.byteLength),
  );

  const requestMobileOutput = useCallback((tab: 'pdf' | 'log') => {
    setMobileOutputTabRequest((current) => ({ id: current.id + 1, tab }));
    setMobileOutputOpen(true);
  }, []);

  useEffect(() => {
    if (!pdfFocusRequest) return;
    if (typeof document === 'undefined') return;
    if (window.matchMedia('(max-width: 1023px)').matches) requestMobileOutput('pdf');
    window.requestAnimationFrame(() => {
      document.querySelector<HTMLElement>('[data-testid="interactive-pdf-preview"]')?.focus();
    });
  }, [pdfFocusRequest, requestMobileOutput]);

  const previousWsNameRef = useRef(currentWsName);

  useEffect(() => {
    if (previousWsNameRef.current === currentWsName) return;
    previousWsNameRef.current = currentWsName;
    setMobileOutputOpen(false);
  }, [currentWsName]);

  useEffect(() => {
    if (!currentWsName || compileState.wsName !== currentWsName) return;
    if (compileState.status === 'initializing' || compileState.status === 'compiling') {
      setMobileOutputOpen(true);
      return;
    }
    if (compileState.status === 'success') requestMobileOutput('pdf');
    if (compileState.status === 'error') requestMobileOutput('log');
  }, [
    compileState.status,
    compileState.wsName,
    currentWsName,
    requestMobileOutput,
  ]);

  const currentProjectProfile = projectProfile || (universalCompileState.wsName === currentWsName ? universalCompileState.profile : undefined);
  const currentProjectMode = projectModeForProfile(currentProjectProfile);
  const currentProjectCapabilities = projectCapabilitiesForProfile(currentProjectProfile);
  const useLocalProjectTerminal = Boolean(
    currentProjectMode?.id === 'latex'
    || currentProjectMode?.id === 'markdown'
    || currentProjectProfile?.projectType === 'markdown',
  );
  const toggleFloatingTerminal = useCallback(() => {
    if (useLocalProjectTerminal) toggleTerminal();
    else changeLinuxTerminalOpen(!linuxTerminalOpen);
  }, [changeLinuxTerminalOpen, linuxTerminalOpen, toggleTerminal, useLocalProjectTerminal]);
  useEffect(() => {
    // Local Terminal is intentionally reserved for LaTeX/Markdown projects.
    // All normal code projects use the authenticated Cloud Terminal.
    if (useLocalProjectTerminal) setLinuxTerminalOpen(false);
    else setTerminalOpen(false);
  }, [currentWsName, useLocalProjectTerminal]);
  const projectOutputOpen = universalCompileState.wsName === currentWsName && universalCompileState.outputOpen;
  const isMarkdownProject = currentProjectMode?.id === 'markdown' || currentProjectProfile?.projectType === 'markdown';
  const projectOutputLabel = isMarkdownProject ? 'Preview' : currentProjectMode?.projectType === 'web' ? 'Web Preview' : 'Console';

  useEffect(() => {
    if (!currentWsName || !isMarkdownProject) return;
    const onRun = (event: Event) => {
      const detail = (event as CustomEvent<MarkdownCodeRunRequest>).detail;
      if (!detail?.code || !detail.language) return;
      void ensureCompileAuthentication().then((authenticated) => {
        if (authenticated) setMarkdownCodeRunRequest(detail);
      });
    };
    const openAi = (selectedText?: string) => {
      const text = String(selectedText || '').trim();
      setAiTabRequest((current) => ({ id: (current?.id ?? 0) + 1, tab: 'assistant' }));
      setMarkdownAiInputRequest((current) => ({
        id: (current?.id ?? 0) + 1,
        text: text
          ? `Improve this Markdown selection while preserving valid Markdown and the author's meaning:

${text}`
          : 'Help me write or improve this Markdown document. Preserve valid portable Markdown.',
      }));
      setAiChatOpen(true);
      setTerminalOpen(false);
      setLinuxTerminalOpen(false);
    };
    const onAi = (event: Event) => {
      const detail = (event as CustomEvent<MarkdownAiRequestDetail>).detail;
      openAi(detail?.text);
    };
    const onKeyDown = (event: KeyboardEvent) => {
      if (!(event.metaKey || event.ctrlKey) || event.shiftKey || event.altKey || event.key.toLowerCase() !== 'j') return;
      const target = event.target instanceof Element ? event.target : undefined;
      if (!target?.closest('[data-editor-engine]')) return;
      event.preventDefault();
      openAi(window.getSelection()?.toString());
    };
    document.addEventListener(MARKDOWN_CODE_RUN_EVENT, onRun);
    document.addEventListener(MARKDOWN_AI_REQUEST_EVENT, onAi);
    document.addEventListener('keydown', onKeyDown);
    return () => {
      document.removeEventListener(MARKDOWN_CODE_RUN_EVENT, onRun);
      document.removeEventListener(MARKDOWN_AI_REQUEST_EVENT, onAi);
      document.removeEventListener('keydown', onKeyDown);
    };
  }, [currentWsName, isMarkdownProject]);

  const compileMarkdownPreview = useCallback(async () => {
    if (!currentWsName || !currentProjectProfile || !isMarkdownProject) return;
    if (!(await ensureCompileAuthentication())) return;
    if (editorWsPathValue) {
      const drained = await waitForSaveQueueToDrain(coreServices.editorEngine, 5000, editorWsPathValue);
      if (!drained) {
        toast.error('Save the current Markdown changes before previewing.');
        return;
      }
    }
    await coreServices.projectCompileOrchestrator.compile(currentWsName, { activePath: editorProjectPath });
  }, [coreServices.editorEngine, coreServices.projectCompileOrchestrator, currentProjectProfile, currentWsName, editorProjectPath, editorWsPathValue, isMarkdownProject]);

  const exportMarkdownArtifact = useCallback(async (kind: 'html' | 'pdf') => {
    if (!currentWsName || !isMarkdownProject || markdownExportBusy) return;
    if (!(await ensureCompileAuthentication())) return;
    if (editorWsPathValue) {
      const drained = await waitForSaveQueueToDrain(coreServices.editorEngine, 5000, editorWsPathValue);
      if (!drained) {
        toast.error('Save the current Markdown changes before exporting.');
        return;
      }
    }
    setMarkdownExportBusy(kind);
    try {
      const result = await coreServices.projectExecution.execute(currentWsName, 'EXPORT', {
        activePath: editorProjectPath,
        artifactPaths: ['.learnitive-tmp/document.html', '.learnitive-tmp/document.pdf'],
      });
      if (result.status !== 'success') throw new Error(result.stderr || 'Markdown export failed.');
      const suffix = kind === 'html' ? 'document.html' : 'document.pdf';
      const artifact = result.artifacts.find((item) => item.path.replaceAll('\\', '/').endsWith(suffix));
      if (!artifact?.contentBase64) throw new Error(`${kind.toUpperCase()} export artifact is unavailable.`);
      const binary = atob(artifact.contentBase64);
      const bytes = new Uint8Array(binary.length);
      for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
      const blob = new Blob([bytes], { type: kind === 'html' ? 'text/html;charset=utf-8' : 'application/pdf' });
      const url = URL.createObjectURL(blob);
      const anchor = document.createElement('a');
      anchor.href = url;
      anchor.download = `document.${kind}`;
      document.body.append(anchor);
      anchor.click();
      anchor.remove();
      URL.revokeObjectURL(url);
    } catch (error) {
      toast.error(error instanceof Error ? error.message : String(error));
    } finally {
      setMarkdownExportBusy(undefined);
    }
  }, [coreServices.editorEngine, coreServices.projectExecution, currentWsName, editorProjectPath, editorWsPathValue, isMarkdownProject, markdownExportBusy]);

  const sourceTransitionStatus =
    isLatexSource && routeWsPath && !editorWsPath
      ? latexDocument.requestedPath === routeWsPath.wsPath &&
        latexDocument.status === 'error'
        ? 'error'
        : 'loading'
      : undefined;

  if (currentWsName && currentProjectMode?.id === 'interactive-doc') {
    const failed = legacyDocMigration?.wsName === currentWsName && legacyDocMigration.status === 'failed';
    return (
      <>
        <AppHeader />
        <PageContentContainer applyPadding={false} respectEditorWidthPreference={false} viewportLocked>
          {failed ? (
            <div className="flex h-full min-h-0 flex-1 flex-col">
              <div className="shrink-0 border-b border-destructive/20 bg-destructive/5 px-4 py-3 text-sm text-destructive" role="alert" data-testid="legacy-interactive-doc-migration-error">
                <strong>Markdown migration could not complete.</strong>
                <span className="ml-2">{legacyDocMigration.error}</span>
                <span className="ml-2">The legacy source was left intact and is shown below for recovery.</span>
              </div>
              <InteractiveDocProjectEditor wsName={currentWsName} />
            </div>
          ) : (
            <div className="flex h-full items-center justify-center p-6" data-testid="legacy-interactive-doc-migrating">
              <div className="max-w-md rounded-lg border bg-background p-5 text-center shadow-sm">
                <RefreshCw className="mx-auto mb-3 size-5 animate-spin" />
                <strong className="block">Converting this legacy Interactive Doc to Markdown…</strong>
                <p className="mt-2 text-sm text-muted-foreground">The original structured source is backed up before the project switches to the Markdown editor.</p>
              </div>
            </div>
          )}
        </PageContentContainer>
      </>
    );
  }

  return (
    <>
      <AppHeader />
      <PageContentContainer
        applyPadding={false}
        respectEditorWidthPreference={!isLatexSource && !isProjectCodeSource}
        viewportLocked={isLatexSource || isProjectCodeSource}
      >
        {currentWsName && routeWsPath ? <EditorFileTabs workspace={currentWsName} currentPath={routeWsPath.path} availablePaths={workspaceFilePaths} /> : null}
        {!currentWsName ? (
          <WorkspaceNotFoundView wsName={routeWsName} />
        ) : isLatexSource && routeWsPath ? (
          <div
            ref={splitWorkspaceRef}
            className="flex h-full min-h-0 flex-1 flex-col overflow-hidden lg:grid"
            data-editor-split-percent={editorSplitPercent.toFixed(1)}
            data-testid="interactive-split-workspace"
            style={{ gridTemplateColumns: splitGridColumns(editorSplitPercent) }}
          >
            <div
              className={
                mobileOutputOpen
                  ? 'hidden min-h-0 min-w-0 flex-1 flex-col lg:flex'
                  : 'flex min-h-0 min-w-0 flex-1 flex-col'
              }
              data-testid="interactive-editor-pane"
            >
              <div
                className="interactive-editor-toolbar flex shrink-0 items-center gap-1 border-b bg-background px-2 py-1.5"
                data-testid="interactive-editor-toolbar"
              >
                <Button
                  size="sm"
                  variant={latexMode === 'source' ? 'secondary' : 'ghost'}
                  onClick={() => setLatexMode('source')}
                >
                  <Code2 className="mr-1 size-4" />
                  {t.app.latexVisual.source}
                </Button>
                <Button
                  size="sm"
                  variant={latexMode === 'visual' ? 'secondary' : 'ghost'}
                  disabled={!visualSupported || collaborationActiveHere}
                  title={collaborationActiveHere ? 'Unavailable during live collaboration' : undefined}
                  onClick={() => { if (!collaborationActiveHere) setLatexMode('visual'); }}
                >
                  <Eye className="mr-1 size-4" />
                  {t.app.latexVisual.visual}
                  <span className="ml-1 rounded bg-primary/10 px-1 text-[9px] font-semibold">Beta</span>
                </Button>
                <Button
                  size="sm"
                  variant={outlineOpen ? 'secondary' : 'ghost'}
                  onClick={() => setOutlineOpen((value) => !value)}
                >
                  <ListTree className="mr-1 size-4" />
                  {t.app.projectOutline.title}
                </Button>
                <Button
                  aria-label="Preview compiled PDF"
                  className="ml-auto lg:hidden"
                  disabled={!hasCompiledPdf}
                  size="sm"
                  variant="ghost"
                  onClick={() => requestMobileOutput('pdf')}
                >
                  <PanelRightOpen className="mr-1 size-4" />
                  Preview
                </Button>
                <Button
                  className="lg:ml-auto"
                  size="sm"
                  variant={trackChangesEnabled ? 'secondary' : 'ghost'}
                  data-testid="track-changes-toggle"
                  aria-pressed={trackChangesEnabled}
                  onClick={toggleTrackChanges}
                >
                  <GitCompareArrows className="mr-1 size-4" />
                  Track
                </Button>
                <Button
                  size="sm"
                  variant={currentLivePdf?.enabled ? 'secondary' : 'ghost'}
                  data-testid="live-pdf-toggle"
                  aria-pressed={Boolean(currentLivePdf?.enabled)}
                  disabled={currentLivePdf?.status === 'certifying' || currentLivePdf?.status === 'rendering'}
                  title={(() => {
                    if (currentLivePdf?.error) return currentLivePdf.error;
                    if (currentLivePdf?.backend === 'cloud') {
                      return 'Live PDF is using the remembered Docker TeX Live Full fallback for this project. Subsequent edits compile directly in the cloud fallback until compiler settings are changed.';
                    }
                    const certificate = currentLivePdf?.certificate;
                    if (!certificate) return 'Live PDF uses browser BusyTeX LuaLaTeX after a real runtime certification gate.';
                    return [
                      `LuaLaTeX: ${certificate.compileSucceeded ? 'passed' : 'failed'}`,
                      `directlua: ${certificate.directLuaRecordFound ? 'passed' : 'failed'}`,
                      `MEMFS record: ${certificate.recordArtifactFound ? `${certificate.recordBytes.toLocaleString()} bytes` : 'missing'}`,
                      `shipout records: ${certificate.pageRecords}`,
                      `PDF bytes: ${certificate.pdfBytes > 0 ? certificate.pdfBytes.toLocaleString() : 'missing'}`,
                    ].join('\n');
                  })()}
                  onClick={() => {
                    if (!currentWsName) return;
                    if (currentLivePdf?.enabled) {
                      coreServices.projectLiveDraft.disableLivePdf(currentWsName);
                      return;
                    }
                    requestMobileOutput('pdf');
                    void coreServices.projectLiveDraft.enableLivePdf(currentWsName).then((state) => {
                      if (state.status === 'error') {
                        toast.error(state.error ?? 'Live PDF could not start with the selected project compiler runtime.');
                      }
                    });
                  }}
                >
                  <RefreshCw className={`size-4 sm:mr-1 ${currentLivePdf?.status === 'certifying' || currentLivePdf?.status === 'rendering' ? 'animate-spin' : ''}`} />
                  <span className="hidden sm:inline">Live PDF</span>
                </Button>
                <Button
                  size="sm"
                  variant={tikzEditorOpen ? 'secondary' : 'ghost'}
                  disabled={!editorWsPath || !/\.(?:tex|ltx)$/i.test(editorWsPath.fileName)}
                  onClick={() => setTikzEditorOpen(true)}
                  data-testid="tikz-visual-editor-toggle"
                  aria-label="TikZ Visual Editor"
                  title="Build a TikZ diagram visually and insert its LaTeX at the active cursor"
                >
                  <PenTool className="size-4 sm:mr-1" />
                  <span className="hidden sm:inline">TikZ</span>
                </Button>
                <Button
                  size="sm"
                  variant={terminalOpen ? 'secondary' : 'ghost'}
                  onClick={toggleTerminal}
                  data-testid="browser-terminal-toggle"
                  aria-label="Browser"
                  aria-pressed={terminalOpen}
                  title="Browser terminal — local project capability running against files in your browser workspace"
                >
                  <FlaskConical className="size-4 sm:mr-1" />
                  <span className="hidden sm:inline">Browser</span>
                </Button>
                <Button
                  size="sm"
                  variant={aiReviewOpen ? 'secondary' : 'ghost'}
                  onClick={() => setAiReviewOpen(true)}
                  data-testid="ai-review-toggle"
                  aria-label="AI Review"
                >
                  <Sparkles className="size-4 sm:mr-1" />
                  <span className="hidden sm:inline">AI Review</span>
                </Button>
                <Button
                  size="sm"
                  variant={reviewOpen ? 'secondary' : 'ghost'}
                  onClick={() => setReviewOpen((value) => !value)}
                  data-testid="review-toggle"
                  aria-label={reviewAttentionCount > 0 ? `Review · ${reviewAttentionCount} pending item${reviewAttentionCount === 1 ? '' : 's'}` : 'Review'}
                >
                  <MessageSquare className="mr-1 size-4" />
                  Review
                  {reviewAttentionCount > 0 && (
                    <span className="ml-1 rounded-full bg-amber-200 px-1.5 text-[10px] font-medium text-amber-950 dark:bg-amber-400/25 dark:text-amber-200" data-testid="review-attention-count">{reviewAttentionCount}</span>
                  )}
                </Button>
                <Button
                  size="sm"
                  variant="ghost"
                  onClick={() => setProjectToolsOpen(true)}
                >
                  <Wrench className="mr-1 size-4" />
                  <span className="sm:hidden">{t.app.projectTools.mobileButton}</span>
                  <span className="hidden sm:inline">{t.app.projectTools.button}</span>
                </Button>
              </div>
              {trackChangesEnabled && (
                <div className="shrink-0 border-b bg-amber-50 px-3 py-1 text-xs font-medium text-amber-900 dark:bg-amber-950/30 dark:text-amber-200" data-testid="track-changes-status">
                  Review mode · Changes tracked
                </div>
              )}
              {paperMemorySelection && paperMemorySelection.anchor.filePath === editorProjectPath && (
                <div
                  className="flex shrink-0 flex-wrap items-center gap-1.5 border-b bg-muted/20 px-2 py-1.5 text-xs"
                  data-testid="paper-memory-manuscript-actions"
                >
                  <MessageSquareQuote className="size-3.5 text-muted-foreground" />
                  <span className="max-w-[min(36rem,45vw)] truncate text-muted-foreground" title={paperMemorySelection.text}>
                    Selected: {paperMemorySelection.text.replace(/\s+/gu, ' ').trim()}
                  </span>
                  <div className="ml-auto flex flex-wrap gap-1">
                    <Button size="sm" variant="ghost" onClick={() => {
                      setAiTabRequest((current) => ({ id: (current?.id ?? 0) + 1, tab: 'assistant' }));
                      setAiChatOpen(true);
                    }}>
                      <Sparkles className="mr-1 size-3.5" />AI
                    </Button>
                    <Button size="sm" variant="ghost" onClick={() => requestPaperMemoryAction('evidence')}>
                      Evidence
                    </Button>
                    <Button size="sm" variant="ghost" onClick={() => {
                      setReviewCommentTarget({ scope: 'anchor', anchor: paperMemorySelection.anchor, selectedText: paperMemorySelection.text });
                      setReviewOpen(true);
                    }}>
                      <MessageSquarePlus className="mr-1 size-3.5" />Comment
                    </Button>
                  </div>
                </div>
              )}
              <div className="flex min-h-0 flex-1">
                {outlineOpen && (
                  <ProjectOutlinePanel
                    wsName={currentWsName}
                    onClose={() => setOutlineOpen(false)}
                    onNavigate={() => setLatexMode('source')}
                  />
                )}
                <div className="relative flex min-h-0 min-w-0 flex-1 flex-col">
                  {editorWsPath ? (
                    <>
                      <div
                        className={
                          latexMode === 'source'
                            ? 'flex min-h-0 flex-1 flex-col'
                            : 'hidden'
                        }
                        data-interactive-latex-view="source"
                      >
                        <EditorSurface
                          key={editorKey}
                          name={editorKey}
                          wsPath={editorWsPath.wsPath}
                          className={LATEX_SOURCE_CONTENT_PADDING}
                          active={latexMode === 'source'}
                          restoredState={latexDocumentController.getSourceViewState(
                            editorWsPath.wsPath,
                            latexDocument.revision,
                          )}
                          onPersistState={(codeMirror, revision) =>
                            latexDocumentController.saveSourceViewState(
                              editorWsPath.wsPath,
                              { revision, codeMirror },
                            )
                          }
                          latexDocumentController={latexDocumentController}
                          onManuscriptSelectionChange={setPaperMemorySelection}
                          onRequestReviewComment={(target) => { setReviewCommentTarget(target); setReviewOpen(true); }}
                          onOpenReviewThread={openReviewThread}
                          onOpenPaper={openLocalPaper}
                        />
                      </div>

                      {!collaborationActiveHere && /\.bib$/i.test(editorWsPath.fileName) && (
                        <div
                          className={
                            latexMode === 'visual'
                              ? 'flex min-h-0 flex-1 flex-col'
                              : 'hidden'
                          }
                          data-interactive-latex-view="visual"
                        >
                          <BibliographyVisualEditor
                            key={`visual-bib:${editorKey}`}
                            wsPath={editorWsPath.wsPath}
                          />
                        </div>
                      )}

                      {!collaborationActiveHere && /\.(?:tex|ltx)$/i.test(editorWsPath.fileName) && (
                        <div
                          className={
                            latexMode === 'visual'
                              ? 'flex min-h-0 flex-1 flex-col'
                              : 'hidden'
                          }
                          data-interactive-latex-view="visual"
                        >
                          <LatexVisualEditor
                            key={`visual-tex:${editorKey}`}
                            wsPath={editorWsPath.wsPath}
                            active={latexMode === 'visual'}
                            documentController={latexDocumentController}
                            document={{
                              path: editorWsPath.wsPath,
                              text: latexDocument.content,
                              revision: latexDocument.revision,
                              ...(latexDocument.lastOrigin === undefined
                                ? {}
                                : { lastOrigin: latexDocument.lastOrigin }),
                            }}
                            onRequestSource={(line, range) => {
                              if (range) {
                                coreServices.latexSource.requestSelectRange(
                                  editorWsPath.wsPath,
                                  range.from,
                                  range.to,
                                );
                              } else {
                                coreServices.latexSource.requestSelect(
                                  editorWsPath.wsPath,
                                  line,
                                );
                              }
                              setLatexMode('source');
                            }}
                            onManuscriptSelectionChange={setPaperMemorySelection}
                            onOpenReviewThread={openReviewThread}
                            onRequestReviewComment={(target) => { setReviewCommentTarget(target); setReviewOpen(true); }}
                            onOpenPaper={openLocalPaper}
                          />
                        </div>
                      )}
                    </>
                  ) : sourceTransitionStatus ? (
                    <LatexDocumentTransition
                      wsPath={routeWsPath.wsPath}
                      status={sourceTransitionStatus}
                      onRetry={() =>
                        void latexDocumentController.requestPath(routeWsPath.wsPath)
                      }
                    />
                  ) : null}
                </div>
                {reviewOpen && currentWsName && (
                  <ReviewSidebar
                    wsName={currentWsName}
                    currentFile={editorProjectPath}
                    activeThreadId={activeReviewThreadId}
                    onActiveThreadChange={setActiveReviewThreadId}
                    onClose={() => setReviewOpen(false)}
                    onRequestComment={(target) => setReviewCommentTarget(target)}
                    onAcceptedChange={() => {
                      if (editorWsPath) latexDocumentController.refreshActiveSource(editorWsPath.wsPath);
                    }}
                  />
                )}
              </div>
            </div>
            <EditorPdfResizer
              containerRef={splitWorkspaceRef}
              value={editorSplitPercent}
              onPreview={previewEditorSplit}
              onCommit={commitEditorSplit}
              onReset={resetEditorSplit}
            />
            <div
              className={
                mobileOutputOpen
                  ? 'flex min-h-0 min-w-0 flex-1'
                  : 'hidden min-h-0 min-w-0 flex-1 lg:flex'
              }
              data-testid="interactive-output-pane"
            >
              <PdfPreviewPane
                wsName={currentWsName}
                mobileOutputTabRequest={mobileOutputTabRequest}
                onCloseMobile={() => setMobileOutputOpen(false)}
                onRequestSource={() => setLatexMode('source')}
                livePdf={currentLivePdf?.pdf}
                liveSynctex={currentLivePdf?.synctex}
                liveRootFile={currentLivePdf?.rootFile}
                liveGeneration={currentLivePdf?.generation}
                livePdfActive={Boolean(currentLivePdf?.enabled && currentLivePdf.certified)}
                livePdfStatus={currentLivePdf?.status === 'certifying' || currentLivePdf?.status === 'rendering' || currentLivePdf?.status === 'ready' || currentLivePdf?.status === 'error' ? currentLivePdf.status : undefined}
                liveInstantPatch={currentLivePdf?.instantPatch}
                liveInstantStatus={currentLivePdf?.instantStatus}
              />
            </div>
          </div>
        ) : isMarkdownProject && editorWsPath ? (
          <div
            ref={splitWorkspaceRef}
            className={projectOutputOpen ? 'flex h-full min-h-0 flex-1 flex-col overflow-hidden lg:grid' : 'flex h-full min-h-0 flex-1 overflow-hidden'}
            data-editor-split-percent={editorSplitPercent.toFixed(1)}
            data-testid="markdown-document-workspace"
            style={projectOutputOpen ? { gridTemplateColumns: splitGridColumns(editorSplitPercent) } : undefined}
          >
            <div className={projectOutputOpen ? 'hidden min-h-0 min-w-0 flex-1 flex-col lg:flex' : 'flex min-h-0 min-w-0 flex-1 flex-col'} data-testid="markdown-editor-pane">
              <div className="flex min-h-10 shrink-0 flex-wrap items-center gap-1 border-b bg-background/95 px-2" data-testid="markdown-document-toolbar">
                <Button size="sm" variant="secondary" aria-pressed="true"><Code2 className="mr-1 size-4"/>Edit</Button>
                <Button size="sm" variant={projectOutputOpen ? 'secondary' : 'ghost'} aria-pressed={projectOutputOpen} data-testid="markdown-preview-toggle" onClick={() => {
                  if (projectOutputOpen) coreServices.projectCompileOrchestrator.closeOutput();
                  else void compileMarkdownPreview();
                }}><Eye className="mr-1 size-4"/>Preview</Button>
                <Button size="sm" variant={aiChatOpen ? 'secondary' : 'ghost'} data-testid="markdown-ai-toggle" onClick={toggleAiChat}><Sparkles className="mr-1 size-4"/>AI</Button>
                <Button size="sm" variant="ghost" disabled={Boolean(markdownExportBusy)} data-testid="markdown-html-export" onClick={() => void exportMarkdownArtifact('html')}><Download className="mr-1 size-4"/>HTML</Button>
                <Button size="sm" variant="ghost" disabled={Boolean(markdownExportBusy)} data-testid="markdown-pdf-export" onClick={() => void exportMarkdownArtifact('pdf')}><FileDown className="mr-1 size-4"/>PDF</Button>
                <Button className="lg:ml-auto" size="sm" variant={terminalOpen ? 'secondary' : 'ghost'} onClick={toggleTerminal} data-testid="markdown-browser-toggle" aria-pressed={terminalOpen}><SquareTerminal className="size-4 sm:mr-1"/><span className="hidden sm:inline">Browser</span></Button>
              </div>
              <EditorSurface key={editorKey} name={editorKey} wsPath={editorWsPath.wsPath} className={EDITOR_CONTENT_PADDING} />
            </div>
            {projectOutputOpen && <>
              <EditorPdfResizer containerRef={splitWorkspaceRef} value={editorSplitPercent} onPreview={previewEditorSplit} onCommit={commitEditorSplit} onReset={resetEditorSplit} secondaryLabel="preview" testId="markdown-editor-preview-resizer" />
              <div className="flex min-h-0 min-w-0 flex-1" data-testid="markdown-preview-pane"><ProjectExecutionPane wsName={currentWsName} /></div>
            </>}
          </div>
        ) : isProjectCodeSource && editorWsPath ? (
          <div
            ref={splitWorkspaceRef}
            className={projectOutputOpen
              ? 'flex h-full min-h-0 flex-1 flex-col overflow-hidden lg:grid'
              : 'flex h-full min-h-0 flex-1 overflow-hidden'}
            data-editor-split-percent={editorSplitPercent.toFixed(1)}
            data-testid="project-code-workspace"
            style={projectOutputOpen
              ? { gridTemplateColumns: splitGridColumns(editorSplitPercent) }
              : undefined}
          >
            <div
              className={projectOutputOpen
                ? 'hidden min-h-0 min-w-0 flex-1 flex-col lg:flex'
                : 'flex min-h-0 min-w-0 flex-1 flex-col'}
              data-testid="project-code-editor-pane"
            >
              <div className="flex min-h-10 shrink-0 flex-wrap items-center gap-1 border-b bg-background/95 px-2" data-testid="project-mode-toolbar">
                <Button size="sm" variant="secondary" aria-pressed="true"><Code2 className="mr-1 size-4"/>Editor</Button>
                <Button
                  size="sm"
                  variant={projectOutputOpen ? 'secondary' : 'ghost'}
                  aria-pressed={projectOutputOpen}
                  data-testid={currentProjectMode?.projectType === 'web' ? 'web-preview-toggle' : 'project-console-toggle'}
                  onClick={() => {
                    if (!currentProjectProfile || !currentWsName) return;
                    if (projectOutputOpen) coreServices.projectCompileOrchestrator.closeOutput();
                    else coreServices.projectCompileOrchestrator.openOutput(currentWsName, currentProjectProfile);
                  }}
                >
                  <PanelRightOpen className="mr-1 size-4"/>{projectOutputLabel}
                </Button>
                <Button
                  className="lg:ml-auto"
                  size="sm"
                  variant={terminalOpen ? 'secondary' : 'ghost'}
                  onClick={toggleTerminal}
                  data-testid="code-browser-toggle"
                  aria-label="Browser"
                  aria-pressed={terminalOpen}
                  title="Browser terminal — local project capability running against files in your browser workspace"
                >
                  <SquareTerminal className="size-4 sm:mr-1" />
                  <span className="hidden sm:inline">Browser</span>
                </Button>
                <Button
                  size="sm"
                  variant={trackChangesEnabled ? 'secondary' : 'ghost'}
                  data-testid="code-track-changes-toggle"
                  aria-pressed={trackChangesEnabled}
                  onClick={toggleTrackChanges}
                >
                  <GitCompareArrows className="size-4 sm:mr-1" />
                  <span className="hidden sm:inline">Track</span>
                </Button>
                <Button
                  size="sm"
                  variant={reviewOpen ? 'secondary' : 'ghost'}
                  onClick={() => setReviewOpen((value) => !value)}
                  data-testid="code-review-toggle"
                  aria-label={reviewAttentionCount > 0 ? `Review · ${reviewAttentionCount} pending item${reviewAttentionCount === 1 ? '' : 's'}` : 'Review'}
                >
                  <MessageSquare className="size-4 sm:mr-1" />
                  <span className="hidden sm:inline">Review</span>
                  {reviewAttentionCount > 0 && (
                    <span className="ml-1 rounded-full bg-amber-200 px-1.5 text-[10px] font-medium text-amber-950 dark:bg-amber-400/25 dark:text-amber-200">{reviewAttentionCount}</span>
                  )}
                </Button>
                <Button
                  size="sm"
                  variant={aiReviewOpen ? 'secondary' : 'ghost'}
                  onClick={() => setAiReviewOpen(true)}
                  data-testid="code-ai-review-toggle"
                  aria-label="AI Review"
                >
                  <Sparkles className="size-4 sm:mr-1" />
                  <span className="hidden sm:inline">AI Review</span>
                </Button>
                <Button
                  size="sm"
                  variant={projectToolsOpen ? 'secondary' : 'ghost'}
                  onClick={() => setProjectToolsOpen(true)}
                  data-testid="code-project-tools-toggle"
                  aria-label="Project Tools"
                >
                  <Wrench className="size-4 sm:mr-1" />
                  <span className="hidden sm:inline">Tools</span>
                </Button>
              </div>
              {trackChangesEnabled && (
                <div className="shrink-0 border-b bg-amber-50 px-3 py-1 text-xs font-medium text-amber-900 dark:bg-amber-950/30 dark:text-amber-200" data-testid="code-track-changes-status">
                  Review mode · Changes tracked
                </div>
              )}
              <div className="flex min-h-0 flex-1">
                <EditorSurface
                  key={editorKey}
                  name={editorKey}
                  wsPath={editorWsPath.wsPath}
                  className="min-h-0 flex-1"
                  onOpenReviewThread={openReviewThread}
                />
                {reviewOpen && currentWsName && (
                  <ReviewSidebar
                    wsName={currentWsName}
                    currentFile={editorProjectPath}
                    activeThreadId={activeReviewThreadId}
                    onActiveThreadChange={setActiveReviewThreadId}
                    onClose={() => setReviewOpen(false)}
                    onRequestComment={(target) => setReviewCommentTarget(target)}
                  />
                )}
              </div>
            </div>
            {projectOutputOpen && (
              <>
                <EditorPdfResizer
                  containerRef={splitWorkspaceRef}
                  value={editorSplitPercent}
                  onPreview={previewEditorSplit}
                  onCommit={commitEditorSplit}
                  onReset={resetEditorSplit}
                  secondaryLabel="result"
                  testId="project-editor-result-resizer"
                />
                <div className="flex min-h-0 min-w-0 flex-1" data-testid="project-code-result-pane">
                  <ProjectExecutionPane wsName={currentWsName} />
                </div>
              </>
            )}
          </div>
        ) : editorWsPath ? (
          <>
            <EditorSurface
              key={editorKey}
              name={editorKey}
              wsPath={editorWsPath.wsPath}
              className={EDITOR_CONTENT_PADDING}
            />
            <button
              aria-hidden
              className="min-h-16 flex-1 cursor-text"
              onMouseDown={(event) => {
                event.preventDefault();
                coreServices.editorEngine.focusEditor();
              }}
              tabIndex={-1}
              type="button"
            />
            {currentWsPath && <LinkedMentions currentWsPath={currentWsPath} />}
          </>
        ) : (
          <NoteNotFoundView />
        )}
      </PageContentContainer>
      {editorWsPath && /\.(?:tex|ltx)$/i.test(editorWsPath.fileName) && (
        <TikzVisualEditor
          wsPath={editorWsPath.wsPath}
          open={tikzEditorOpen}
          onOpenChange={setTikzEditorOpen}
          documentController={latexDocumentController}
        />
      )}
      {currentWsName && (isLatexSource || isProjectCodeSource) && (
        <ProjectToolsDialog
          wsName={currentWsName}
          activeFile={routeWsPath?.path ? projectFilePath(routeWsPath.path) : undefined}
          open={projectToolsOpen}
          onOpenChange={setProjectToolsOpen}
          initialTab={projectToolsTab}
          initialTextScope={projectToolsTextScope}
          capabilities={currentProjectCapabilities}
        />
      )}
      {currentWsName && editorProjectPath && isProjectCodeSource ? (
        <CodeAiReviewDialog
          open={aiReviewOpen}
          onOpenChange={setAiReviewOpen}
          wsName={currentWsName}
          currentFile={editorProjectPath}
          diagnostics={universalCompileState.wsName===currentWsName?[universalCompileState.error,universalCompileState.result?.stderr,...(universalCompileState.result?.diagnostics||[]).map((item)=>`${item.severity}${item.file?` ${item.file}${item.line?`:${item.line}`:''}`:''}: ${item.message}`)].filter(Boolean).join('\n'):''}
          onOpenReview={() => setReviewOpen(true)}
        />
      ) : currentWsName && editorProjectPath ? (
        <AiReviewDialog
          open={aiReviewOpen}
          onOpenChange={setAiReviewOpen}
          wsName={currentWsName}
          currentFile={editorProjectPath}
          selection={paperMemorySelection}
          onOpenReview={() => setReviewOpen(true)}
          onFix={(action, fragment) => {
            if (action === 'ignore') return;
            if (fragment) {
              setPaperMemorySelection({
                anchor: fragment.anchor,
                text: fragment.text,
                mode: paperMemorySelection?.mode ?? 'source',
              });
            }
            if (action === 'find-evidence') {
              requestPaperMemoryAction('evidence');
              setAiTabRequest((current) => ({ id: (current?.id ?? 0) + 1, tab: 'papers' }));
              setAiChatOpen(true);
              return;
            }
            if (action === 'find-citation') {
              requestPaperMemoryAction('find-citation');
              setAiTabRequest((current) => ({ id: (current?.id ?? 0) + 1, tab: 'papers' }));
              setAiChatOpen(true);
              return;
            }
            setAiTabRequest((current) => ({ id: (current?.id ?? 0) + 1, tab: 'assistant' }));
            setAiChatOpen(true);
          }}
        />
      ) : null}
      {currentWsName && (
        <ReviewCommentDialog
          open={Boolean(reviewCommentTarget)}
          target={reviewCommentTarget}
          onClose={() => setReviewCommentTarget(undefined)}
          onSubmit={async (body) => {
            const thread = await coreServices.projectReview.createComment(currentWsName, {
              body,
              ...(reviewCommentTarget?.scope ? { scope: reviewCommentTarget.scope } : {}),
              ...(reviewCommentTarget?.anchor ? { anchor: reviewCommentTarget.anchor } : {}),
            });
            setActiveReviewThreadId(thread.id);
            setReviewOpen(true);
          }}
        />
      )}
      {currentWsName && isMarkdownProject && (
        <MarkdownCodeRunDialog wsName={currentWsName} request={markdownCodeRunRequest} onOpenChange={(open) => { if (!open) setMarkdownCodeRunRequest(undefined); }} />
      )}
      {currentWsName && (
        <AiChatShell
          wsName={currentWsName}
          manuscriptSelection={paperMemorySelection}
          paperMemoryActionRequest={paperMemoryActionRequest}
          tabRequest={aiTabRequest}
          paperOpenRequest={paperOpenRequest}
          agentRequest={aiAgentRequest}
          assistantInputRequest={isMarkdownProject ? markdownAiInputRequest : undefined}
          onAgentRequestConsumed={consumeAiAgentRequest}
          open={aiChatOpen}
          onOpenChange={changeAiChatOpen}
          showLauncher={false}
          papersEnabled={currentProjectCapabilities.paperMemory}
          latexAiActions={currentProjectCapabilities.latexAiActions}
        />
      )}
      {currentWsName && (
        <LinuxTerminalPro
          wsName={currentWsName}
          open={linuxTerminalOpen}
          onOpenChange={changeLinuxTerminalOpen}
        />
      )}
      {currentWsName && (
        <ProjectTerminal
          wsName={currentWsName}
          open={terminalOpen}
          onOpenChange={changeTerminalOpen}
          showLauncher={false}
        />
      )}
      {currentWsName && (
        <FloatingResearchDock
          aiOpen={aiChatOpen}
          terminalOpen={useLocalProjectTerminal ? terminalOpen : linuxTerminalOpen}
          terminalKind={useLocalProjectTerminal ? 'local' : 'cloud'}
          onToggleAi={toggleAiChat}
          onToggleTerminal={toggleFloatingTerminal}
        />
      )}
    </>
  );
}
