import { useCoreServices } from '@learnitive/context';
import { createLatexImageInsertion } from '@learnitive/latex-source';
import type {
  GeneratedResearchArtifact,
  TerminalAgentPlan,
  TerminalAgentStep,
  TerminalAgentStepResult,
  TerminalScientificProposal,
  TerminalWorkflowResultsProposal,
} from '@learnitive/service-core';
import { Button, toast } from '@learnitive/ui-components';
import { WsPath } from '@learnitive/ws-path';
import { ChevronDown, Maximize2, Minimize2, SquareTerminal, X } from 'lucide-react';
import React from 'react';
import { ResearchArtifactCard } from './research-artifact-card';
import { TERMINAL_COMMAND_INPUT_CLASS, TERMINAL_COMMAND_ROW_CLASS, TERMINAL_CONSOLE_CLASS, TERMINAL_HEADER_CLASS, terminalPanelClass } from './terminal-shell';
import { requestPageConfirm, requestPagePrompt } from './page-modal-service';

type TranscriptLine = { id: number; kind: 'command' | 'stdout' | 'stderr' | 'system' | 'ai' | 'artifacts'; text: string; scientificProposal?: TerminalScientificProposal; agentPlan?: TerminalAgentPlan; workflowResultsProposal?: TerminalWorkflowResultsProposal; artifacts?: readonly GeneratedResearchArtifact[] };
type TerminalAiMode = 'off' | 'copilot' | 'assisted' | 'agent';
type AgentStepUiStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
type AgentPlanUiState = {
  phase: 'idle' | 'running' | 'completed' | 'failed' | 'cancelled';
  steps: Record<string, { status: AgentStepUiStatus; output?: string }>;
  results?: readonly TerminalAgentStepResult[];
};

function fileTarget(text: string): { path: string; line?: number; column?: number } | undefined {
  const match = text.match(/(?:^|\s)(?:\/workspace\/)?([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)(?::(\d+))?(?::(\d+))?/u);
  if (!match?.[1]) return undefined;
  return { path: match[1].replace(/^\.\//u, ''), ...(match[2] ? { line: Number(match[2]) } : {}), ...(match[3] ? { column: Number(match[3]) } : {}) };
}

function redactAgentArgs(step: TerminalAgentStep): Record<string, unknown> {
  const copy: Record<string, unknown> = { ...step.args };
  if (typeof copy.content === 'string') {
    const content = copy.content;
    copy.content = `${content.slice(0, 1_500)}${content.length > 1_500 ? '\n…[content truncated for confirmation]' : ''}`;
  }
  if (typeof copy.body === 'string') copy.body = '[request body present]';
  if (copy.headers && typeof copy.headers === 'object' && !Array.isArray(copy.headers)) {
    const headers: Record<string, unknown> = {};
    for (const [name, value] of Object.entries(copy.headers as Record<string, unknown>)) {
      headers[name] = /(?:authorization|api-key|token|secret)/iu.test(name) ? '[credential-bearing value hidden]' : value;
    }
    copy.headers = headers;
  }
  return copy;
}

function agentStepConfirmationMessage(step: TerminalAgentStep): string {
  const action = step.capability === 'network'
    ? 'Approve Agent network step?'
    : step.capability === 'destructive-local'
      ? 'Approve destructive Agent step?'
      : step.capability === 'write-local'
        ? 'Approve Agent project change?'
        : 'Approve Agent computation?';
  const warnings: string[] = [];
  if (step.capability === 'network') warnings.push('This step communicates with an external endpoint.');
  if (step.tool === 'network.http' && step.args.bodyFile) warnings.push(`Project file ${String(step.args.bodyFile)} will be sent outside the browser.`);
  if (step.tool === 'git.push') warnings.push('Committed project history will be sent to the configured Git remote.');
  if (step.tool === 'git.pull') warnings.push('Remote Git data may update the local workspace.');
  if (step.capability === 'destructive-local') warnings.push('This step can remove or replace local project/repository state.');
  return `${action}\n\n${step.title}\n${step.tool} · ${step.capability}\n\n${step.reason}\n\nArguments:\n${JSON.stringify(redactAgentArgs(step), null, 2)}${warnings.length ? `\n\n${warnings.join('\n')}` : ''}`;
}

function agentPlanSummary(plan: TerminalAgentPlan): string {
  return `${plan.title}\n${plan.summary}\n\n${plan.steps.map((step, index) => `${index + 1}. ${step.title} — ${step.tool} (${step.capability})`).join('\n')}`;
}

function terminalConfirmationMessage(command: string, capability: string): string {
  if (capability !== 'network') {
    return `Run destructive local command?\n\n${command}\n\nLearnitive will keep project filesystem access restricted to /workspace.`;
  }
  const warnings = ['This action communicates with an external network endpoint.'];
  if (/^\s*curl\b/iu.test(command)) {
    if (/(?:-d|--data|--data-raw|--data-binary)\b/iu.test(command)) {
      warnings.push('The HTTP request includes a body and may send project or entered data outside the browser.');
    }
    if (/(?:authorization|x-api-key|x-auth-token|x-access-token|api-key)\s*:/iu.test(command)) {
      warnings.push('The request includes a credential-bearing header.');
    }
  } else if (/^\s*wasix\s+install\s+clang\b/iu.test(command)) {
    warnings.push('This downloads the pinned browser Clang/WASIX compiler toolchain from the Wasmer registry. The toolchain is a large external package.');
  } else if (/^\s*git\s+push\b/iu.test(command)) {
    warnings.push('Git push sends committed project history to the configured remote.');
  } else if (/^\s*git\s+(?:pull|clone)\b/iu.test(command)) {
    warnings.push('This Git operation can update the local workspace after downloading remote data.');
  }
  return `Run network command?\n\n${command}\n\n${warnings.join('\n')}\n\nBrowser CORS and Learnitive project safeguards still apply.`;
}

type ProjectTerminalProps = {
  wsName: string;
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
  showLauncher?: boolean;
};

export function ProjectTerminal({
  wsName,
  open: controlledOpen,
  onOpenChange,
  showLauncher = true,
}: ProjectTerminalProps) {
  const { projectTerminal, projectTerminalAi, projectScientificRuntime, projectDataset, projectReview, projectStorage, projectCollaboration, navigation, latexSource } = useCoreServices();
  const [internalOpen, setInternalOpen] = React.useState(false);
  const open = controlledOpen ?? internalOpen;
  const openRef = React.useRef(open);
  const controlledOpenRef = React.useRef(controlledOpen);
  openRef.current = open;
  controlledOpenRef.current = controlledOpen;
  const setOpen = React.useCallback<React.Dispatch<React.SetStateAction<boolean>>>((next) => {
    const value = typeof next === 'function' ? next(openRef.current) : next;
    openRef.current = value;
    if (controlledOpenRef.current === undefined) setInternalOpen(value);
    onOpenChange?.(value);
  }, [onOpenChange]);
  const [maximized, setMaximized] = React.useState(false);
  const [input, setInput] = React.useState('');
  const [running, setRunning] = React.useState(false);
  const [cwd, setCwd] = React.useState(() => projectTerminal.getCwd(wsName));
  const [lines, setLines] = React.useState<TranscriptLine[]>([
    { id: 1, kind: 'system', text: 'Learnitive browser terminal — local project capability: /workspace\nType help for commands or Ctrl/Cmd+I for AI.' },
  ]);
  const [history, setHistory] = React.useState<string[]>([]);
  const [historyIndex, setHistoryIndex] = React.useState(-1);
  const [aiMode, setAiMode] = React.useState<TerminalAiMode>('copilot');
  const [showArtifacts, setShowArtifacts] = React.useState(false);
  const [artifactLibrary, setArtifactLibrary] = React.useState<readonly GeneratedResearchArtifact[]>([]);
  const [agentRuns, setAgentRuns] = React.useState<Record<string, AgentPlanUiState>>({});
  const abortRef = React.useRef<AbortController | undefined>(undefined);
  const scrollRef = React.useRef<HTMLDivElement>(null);
  const inputRef = React.useRef<HTMLInputElement>(null);
  const nextId = React.useRef(2);

  React.useEffect(() => {
    setCwd(projectTerminal.getCwd(wsName));
    setLines([{ id: nextId.current++, kind: 'system', text: 'Workspace changed. Terminal root is /workspace.' }]);
    setAgentRuns({});
  }, [projectTerminal, wsName]);

  const refreshArtifactLibrary = React.useCallback(async () => {
    try {
      const artifacts = await projectTerminal.getGeneratedArtifacts(wsName);
      setArtifactLibrary(artifacts.slice().sort((a, b) => b.createdAt - a.createdAt));
      return artifacts;
    } catch {
      setArtifactLibrary([]);
      return [] as readonly GeneratedResearchArtifact[];
    }
  }, [projectTerminal, wsName]);

  React.useEffect(() => {
    void refreshArtifactLibrary();
  }, [refreshArtifactLibrary]);

  React.useEffect(() => {
    scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });
  }, [lines, running]);

  React.useEffect(() => {
    if (!open) return undefined;
    const onKeyDown = (event: KeyboardEvent) => {
      if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'i') {
        event.preventDefault();
        if (aiMode === 'off') setAiMode('copilot');
        setInput((value) => {
          if (value.trim().startsWith('ai')) return value;
          if (aiMode === 'agent') return value.trim() ? `ai agent ${value}` : 'ai agent ';
          return `ai ${value}`.trimEnd() + ' ';
        });
        queueMicrotask(() => inputRef.current?.focus());
      }
    };
    window.addEventListener('keydown', onKeyDown);
    return () => window.removeEventListener('keydown', onKeyDown);
  }, [aiMode, open]);

  const append = React.useCallback((
    kind: TranscriptLine['kind'],
    text: string,
    scientificProposal?: TerminalScientificProposal,
    agentPlan?: TerminalAgentPlan,
    workflowResultsProposal?: TerminalWorkflowResultsProposal,
  ) => {
    if (!text) return;
    setLines((current) => [...current, {
      id: nextId.current++,
      kind,
      text,
      ...(scientificProposal ? { scientificProposal } : {}),
      ...(agentPlan ? { agentPlan } : {}),
      ...(workflowResultsProposal ? { workflowResultsProposal } : {}),
    }]);
  }, []);

  const appendArtifacts = React.useCallback((artifacts: readonly GeneratedResearchArtifact[]) => {
    if (!artifacts.length) return;
    setLines((current) => [...current, { id: nextId.current++, kind: 'artifacts', text: 'Generated research artifacts', artifacts }]);
  }, []);

  const captureArtifactVersions = React.useCallback(async () => {
    try {
      return new Map((await projectTerminal.getGeneratedArtifacts(wsName)).map((artifact) => [artifact.path, artifact.createdAt]));
    } catch {
      return new Map<string, number>();
    }
  }, [projectTerminal, wsName]);

  const appendChangedArtifacts = React.useCallback(async (before: ReadonlyMap<string, number>) => {
    const after = await refreshArtifactLibrary();
    appendArtifacts(after.filter((artifact) => before.get(artifact.path) !== artifact.createdAt).sort((a, b) => b.createdAt - a.createdAt));
  }, [appendArtifacts, refreshArtifactLibrary]);

  const run = React.useCallback(async () => {
    const command = input.trim();
    if (!command || running) return;
    if (command === 'clear') {
      setLines([]);
      setInput('');
      setHistory((items) => [...items, command]);
      return;
    }
    const isAi = projectTerminalAi.isAiCommand(command);
    if (isAi && aiMode === 'off') {
      append('stderr', 'Local AI is Off. Choose Copilot, Assisted, or Agent in the Local header.');
      return;
    }
    const decision = isAi ? undefined : projectTerminal.policy(command, 'user');
    if (decision?.action === 'deny') {
      append('stderr', decision.reason ?? 'Command denied by terminal policy.');
      return;
    }
    if (decision?.action === 'confirm') {
      if (!(await requestPageConfirm({ title: decision.capability === 'network' ? 'Approve network command?' : 'Approve local command?', description: terminalConfirmationMessage(command, decision.capability), confirmText: 'Run command', tone: decision.capability === 'network' ? 'default' : 'destructive' }))) return;
    }
    append('command', `${cwd.replace('/workspace', '~') || '~'} $ ${command}`);
    setHistory((items) => [...items, command].slice(-200));
    setHistoryIndex(-1);
    setInput('');
    const artifactsBefore = await captureArtifactVersions();
    setRunning(true);
    const controller = new AbortController();
    abortRef.current = controller;
    try {
      if (isAi) {
        const result = await projectTerminalAi.execute(wsName, command, controller.signal, { mode: aiMode });
        append('ai', `AI · ${result.kind}\n${result.text}`, result.scientificProposal, result.agentPlan);
      } else {
        const result = await projectTerminal.execute(wsName, command, controller.signal, 'user');
        setCwd(result.cwd);
        for (const output of result.outputs) append(output.stream, output.text.replace(/\n$/u, ''));
        if (result.exitCode !== 0) append('system', `exit ${result.exitCode}`);
      }
      await appendChangedArtifacts(artifactsBefore);
    } catch (error) {
      if (!controller.signal.aborted) append('stderr', error instanceof Error ? error.message : String(error));
    } finally {
      if (abortRef.current === controller) abortRef.current = undefined;
      setRunning(false);
    }
  }, [aiMode, append, appendChangedArtifacts, captureArtifactVersions, cwd, input, projectTerminal, projectTerminalAi, running, wsName]);

  const applyScientificProposal = React.useCallback(async (proposal: TerminalScientificProposal, runAfterCreate: boolean) => {
    if (running) return;
    const action = runAfterCreate ? 'create and run' : 'create';
    const outputs = proposal.expectedOutputs.length ? `\n\nApproved output files:\n${proposal.expectedOutputs.map((path) => `  ${path}`).join('\n')}` : '\n\nApproved output files: none';
    if (!(await requestPageConfirm({ title: `Approve AI ${action}?`, description: `AI proposes to ${action}:\n\n${proposal.scriptPath}${runAfterCreate ? `\n\nThen run:\n${proposal.runCommand}${outputs}` : ''}\n\nReview the generated source in this card before approving.`, confirmText: runAfterCreate ? 'Create and run' : 'Create script' }))) return;
    const artifactsBefore = await captureArtifactVersions();
    setRunning(true);
    const controller = new AbortController();
    abortRef.current = controller;
    try {
      try {
        await projectTerminalAi.createScientificScript(wsName, proposal, controller.signal, { approved: true });
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        if (!/already exists/iu.test(message) || !(await requestPageConfirm({ title: 'Overwrite existing script?', description: `${message}\n\nOverwrite it with the reviewed AI proposal?`, confirmText: 'Overwrite', tone: 'destructive' }))) throw error;
        await projectTerminalAi.createScientificScript(wsName, proposal, controller.signal, { approved: true, overwrite: true });
      }
      append('system', `Created ${proposal.scriptPath}`);
      if (!runAfterCreate) {
        navigation.goWsPath(WsPath.fromParts(wsName, proposal.scriptPath).wsPath);
        return;
      }
      append('command', `${cwd.replace('/workspace', '~') || '~'} $ ${proposal.runCommand}`);
      const result = await projectTerminalAi.runScientificProposal(wsName, proposal, controller.signal, { approved: true });
      setCwd(result.cwd);
      for (const output of result.outputs) append(output.stream, output.text.replace(/\n$/u, ''));
      if (result.exitCode !== 0) append('system', `exit ${result.exitCode}`);
      else append('system', 'Scientific run completed. Use `ai explain` to interpret the computed output, or inspect generated artifacts below.');
      await appendChangedArtifacts(artifactsBefore);
    } catch (error) {
      if (!controller.signal.aborted) append('stderr', error instanceof Error ? error.message : String(error));
    } finally {
      if (abortRef.current === controller) abortRef.current = undefined;
      setRunning(false);
    }
  }, [append, appendChangedArtifacts, captureArtifactVersions, cwd, navigation, projectTerminalAi, running, wsName]);

  const updateAgentPlanState = React.useCallback((
    planId: string,
    update: (current: AgentPlanUiState) => AgentPlanUiState,
  ) => {
    setAgentRuns((current) => {
      const existing = current[planId] ?? { phase: 'idle', steps: {} };
      return { ...current, [planId]: update(existing) };
    });
  }, []);

  const setAgentStepUi = React.useCallback((
    planId: string,
    stepId: string,
    status: AgentStepUiStatus,
    output?: string,
  ) => {
    updateAgentPlanState(planId, (current) => ({
      ...current,
      steps: { ...current.steps, [stepId]: { status, ...(output ? { output } : {}) } },
    }));
  }, [updateAgentPlanState]);

  const runAgentPlan = React.useCallback(async (plan: TerminalAgentPlan) => {
    if (running) return;
    if (aiMode !== 'agent') {
      append('stderr', 'Executable plans require Local AI Agent mode.');
      return;
    }
    if (!(await requestPageConfirm({ title: 'Run bounded Agent plan?', description: `${agentPlanSummary(plan)}\n\nRead-only and local-compute steps may run automatically. Every write, destructive, or network step will ask again before it runs.`, confirmText: 'Run plan' }))) return;

    const initialSteps = Object.fromEntries(plan.steps.map((step) => [step.id, { status: 'pending' as const }]));
    setAgentRuns((current) => ({ ...current, [plan.id]: { phase: 'running', steps: initialSteps } }));
    const artifactsBefore = await captureArtifactVersions();
    const controller = new AbortController();
    abortRef.current = controller;
    setRunning(true);
    const results: TerminalAgentStepResult[] = [];
    let phase: AgentPlanUiState['phase'] = 'completed';

    try {
      for (const step of plan.steps) {
        controller.signal.throwIfAborted();
        const decision = projectTerminalAi.assessAgentStep(plan, step.id, aiMode, { planApproved: true });
        if (decision.action === 'deny') {
          const message = decision.reason ?? `Agent step denied: ${step.tool}`;
          setAgentStepUi(plan.id, step.id, 'failed', message);
          append('stderr', `Agent stopped at “${step.title}”: ${message}`);
          phase = 'failed';
          break;
        }

        let stepApproved = false;
        if (decision.action === 'confirm') {
          if (!(await requestPageConfirm({ title: 'Approve Agent step?', description: agentStepConfirmationMessage(step), confirmText: 'Approve step', tone: step.capability === 'destructive-local' ? 'destructive' : 'default' }))) {
            setAgentStepUi(plan.id, step.id, 'skipped', 'User did not approve this step.');
            append('system', `Agent plan stopped before “${step.title}” because the required ${step.capability} approval was not granted.`);
            phase = 'cancelled';
            break;
          }
          stepApproved = true;
        }

        setAgentStepUi(plan.id, step.id, 'running');
        try {
          const result = await projectTerminalAi.executeAgentStep(wsName, plan, step.id, controller.signal, {
            mode: aiMode,
            planApproved: true,
            stepApproved,
          });
          results.push(result);
          setAgentStepUi(plan.id, step.id, result.ok ? 'completed' : 'failed', result.output);
          if (!result.ok) {
            append('stderr', `Agent step failed: ${step.title}${result.exitCode === undefined ? '' : ` (exit ${result.exitCode})`}`);
            phase = 'failed';
            break;
          }
        } catch (error) {
          const message = error instanceof Error ? error.message : String(error);
          setAgentStepUi(plan.id, step.id, 'failed', message);
          append('stderr', `Agent step failed: ${step.title}\n${message}`);
          phase = 'failed';
          break;
        }
      }

      await appendChangedArtifacts(artifactsBefore);
      if (results.length && !controller.signal.aborted) {
        try {
          const summary = await projectTerminalAi.summarizeAgentRun(plan, results, controller.signal);
          append('ai', `AI · agent result\n${summary}`);
        } catch (error) {
          append('system', `Agent tools finished, but the AI summary was unavailable: ${error instanceof Error ? error.message : String(error)}`);
        }
      }
    } catch (error) {
      if (controller.signal.aborted) phase = 'cancelled';
      else {
        phase = 'failed';
        append('stderr', error instanceof Error ? error.message : String(error));
      }
    } finally {
      updateAgentPlanState(plan.id, (current) => ({ ...current, phase, results }));
      if (abortRef.current === controller) abortRef.current = undefined;
      setRunning(false);
    }
  }, [aiMode, append, appendChangedArtifacts, captureArtifactVersions, projectTerminalAi, running, setAgentStepUi, updateAgentPlanState, wsName]);

  const draftWorkflowResults = React.useCallback(async (plan: TerminalAgentPlan) => {
    if (running) return;
    const results = agentRuns[plan.id]?.results ?? [];
    if (!results.length) {
      append('stderr', 'Complete a scientific Agent workflow before drafting Results text.');
      return;
    }
    setRunning(true);
    const controller = new AbortController();
    abortRef.current = controller;
    try {
      const proposal = await projectTerminalAi.draftWorkflowResults(wsName, plan, results, controller.signal);
      append('ai', `AI · workflow Results draft\n${proposal.title}\nTarget: ${proposal.targetFile}\n\nReview the full proposed manuscript text below before creating a review change.`, undefined, undefined, proposal);
    } catch (error) {
      if (!controller.signal.aborted) append('stderr', error instanceof Error ? error.message : String(error));
    } finally {
      if (abortRef.current === controller) abortRef.current = undefined;
      setRunning(false);
    }
  }, [agentRuns, append, projectTerminalAi, running, wsName]);

  const applyWorkflowResultsProposal = React.useCallback(async (proposal: TerminalWorkflowResultsProposal) => {
    if (running) return;
    if (!(await requestPageConfirm({ title: 'Create review change?', description: `Create a pending Learnitive review change in ${proposal.targetFile}?\n\nThe manuscript will not be silently modified. Review/Track Changes controls will accept or reject the proposal.`, confirmText: 'Create review change' }))) return;
    setRunning(true);
    const controller = new AbortController();
    abortRef.current = controller;
    try {
      const review = await projectTerminalAi.applyWorkflowResultsProposal(wsName, proposal, controller.signal, { approved: true });
      append('system', `Created pending Results review change in ${review.filePath}${review.changeId ? ` (${review.changeId})` : ''}.`);
      navigation.goWsPath(WsPath.fromParts(wsName, review.filePath).wsPath);
      toast.success('Results text proposed for manuscript review');
    } catch (error) {
      if (!controller.signal.aborted) append('stderr', error instanceof Error ? error.message : String(error));
    } finally {
      if (abortRef.current === controller) abortRef.current = undefined;
      setRunning(false);
    }
  }, [append, navigation, projectTerminalAi, running, wsName]);

  const reviseAgentPlan = React.useCallback(async (plan: TerminalAgentPlan) => {
    if (running) return;
    const instruction = await requestPagePrompt({ title: 'Revise Agent plan', description: 'Describe how Learnitive should revise this bounded Agent plan.', label: 'Revision instruction', confirmText: 'Revise plan' });
    if (!instruction?.trim()) return;
    setRunning(true);
    const controller = new AbortController();
    abortRef.current = controller;
    try {
      const revised = await projectTerminalAi.reviseAgentPlan(plan, instruction, controller.signal);
      setLines((current) => current.map((line) => line.agentPlan?.id === plan.id
        ? { ...line, text: `AI · agent revised\n${agentPlanSummary(revised)}`, agentPlan: revised }
        : line));
      setAgentRuns((current) => ({
        ...current,
        [plan.id]: { ...(current[plan.id] ?? { steps: {} }), phase: 'cancelled' },
        [revised.id]: { phase: 'idle', steps: {} },
      }));
    } catch (error) {
      if (!controller.signal.aborted) append('stderr', error instanceof Error ? error.message : String(error));
    } finally {
      if (abortRef.current === controller) abortRef.current = undefined;
      setRunning(false);
    }
  }, [append, projectTerminalAi, running]);

  const cancelAgentPlan = React.useCallback((plan: TerminalAgentPlan) => {
    const state = agentRuns[plan.id];
    if (state?.phase === 'running') abortRef.current?.abort(new DOMException('Agent plan cancelled', 'AbortError'));
    updateAgentPlanState(plan.id, (current) => {
      const steps: AgentPlanUiState['steps'] = {};
      for (const step of plan.steps) {
        const existing = current.steps[step.id];
        steps[step.id] = existing && (existing.status === 'completed' || existing.status === 'failed')
          ? existing
          : { status: 'skipped', output: 'Plan cancelled.' };
      }
      return { phase: 'cancelled', steps };
    });
  }, [agentRuns, updateAgentPlanState]);

  const insertArtifactIntoManuscript = React.useCallback(async (artifact: GeneratedResearchArtifact) => {
    try {
      await projectReview.refresh(wsName);
      const active = navigation.resolveAtoms().activeWsFilePath;
      const projectConfig = await projectStorage.getProjectConfig(wsName);
      const targetPath = active?.wsName === wsName && /\.(?:tex|ltx)$/iu.test(active.path)
        ? active.path
        : projectConfig.rootFile;
      const targetWsPath = WsPath.fromParts(wsName, targetPath).wsPath;
      await latexSource.load(targetWsPath);
      const canonical = latexSource.getSnapshot(targetWsPath)?.text;
      if (canonical === undefined) throw new Error(`Unable to load manuscript source: ${targetPath}`);
      const trackChanges = projectReview.getTrackChangesEnabled(wsName);
      const current = trackChanges ? projectReview.getProjectedSource(wsName, targetPath, canonical) : canonical;
      const cursor = active?.wsPath === targetWsPath
        ? Math.min(current.length, latexSource.getCursorLocation(targetWsPath)?.offset ?? current.length)
        : current.length;

      let edit: { from: number; to: number; text: string };
      let semanticKind: string;
      let previewText: string;
      if (artifact.kind === 'plot' && /\.(?:pdf|png|jpe?g|svg|eps)$/iu.test(artifact.path)) {
        const insertion = createLatexImageInsertion({ source: current, cursor, projectPath: artifact.path });
        edit = insertion.edit;
        semanticKind = 'figure';
        previewText = insertion.edit.text;
      } else if ((artifact.kind === 'dataset' || artifact.kind === 'table') && /\.(?:csv|tsv)$/iu.test(artifact.path)) {
        const table = await projectDataset.toLatexTable(wsName, artifact.path, { maxRows: 30, maxColumns: 12 });
        if (table.truncatedRows || table.truncatedColumns) {
          const scope = `${table.includedRows}/${table.rowCount} rows and ${table.includedColumns}/${table.columnCount} columns`;
          if (!(await requestPageConfirm({ title: 'Insert truncated table?', description: `This result is larger than an inline manuscript table. Learnitive will insert ${scope}.`, confirmText: 'Insert table' }))) return;
        }
        const before = current.slice(0, cursor);
        const after = current.slice(cursor);
        const prefix = before.length === 0 || before.endsWith('\n\n') ? '' : before.endsWith('\n') ? '\n' : '\n\n';
        const suffix = after.length === 0 || after.startsWith('\n\n') ? '' : after.startsWith('\n') ? '\n' : '\n\n';
        edit = { from: cursor, to: cursor, text: `${prefix}${table.latex}${suffix}` };
        semanticKind = 'table';
        previewText = table.latex;
      } else {
        throw new Error('This generated artifact cannot be inserted directly into a LaTeX manuscript.');
      }

      if (!(await requestPageConfirm({ title: `Insert into ${targetPath}?`, description: `${previewText.slice(0, 1800)}${previewText.length > 1800 ? '\n…' : ''}`, confirmText: 'Insert' }))) return;
      if (trackChanges) {
        projectReview.applyProjectedEdit(wsName, targetPath, canonical, edit, { semanticKind, source: 'terminal' });
        toast.success(`Proposed ${semanticKind} insertion in ${targetPath} for review`);
      } else {
        if (projectCollaboration.isSourcePathCollaborative(wsName, targetPath)) {
          projectCollaboration.applyTextEdits(wsName, targetPath, [edit], { reviewSource: 'terminal', semanticKind: 'terminal-edit' });
          await projectCollaboration.flushToStorage(wsName);
        } else {
          latexSource.applyEdit(targetWsPath, edit, 'asset');
          await latexSource.flush(targetWsPath);
        }
        toast.success(`Inserted ${semanticKind} into ${targetPath}`);
      }
      navigation.goWsPath(targetWsPath);
    } catch (error) {
      toast.error(error instanceof Error ? error.message : 'Unable to insert generated artifact');
    }
  }, [latexSource, navigation, projectCollaboration, projectDataset, projectReview, projectStorage, wsName]);

  const openFile = React.useCallback((line: TranscriptLine) => {
    const target = fileTarget(line.text);
    if (!target) return;
    try {
      const wsPath = WsPath.fromParts(wsName, target.path).wsPath;
      if (/\.(?:tex|ltx|bib|sty|cls)$/iu.test(target.path) && target.line) {
        latexSource.requestSelect(wsPath, target.line, target.column ?? 1);
      }
      navigation.goWsPath(wsPath);
    } catch (error) {
      toast.error(error instanceof Error ? error.message : 'Could not open file');
    }
  }, [latexSource, navigation, wsName]);

  if (!open) {
    if (!showLauncher) return null;
    return (
      <Button
        className="fixed bottom-4 right-[4.5rem] z-40 shadow-lg"
        data-testid="terminal-open"
        onClick={() => setOpen(true)}
      >
        <SquareTerminal className="mr-1 size-4" />Local
      </Button>
    );
  }

  return (
    <section
      className={terminalPanelClass(maximized)}
      data-testid="project-terminal"
      aria-label="Local"
    >
      <header className={TERMINAL_HEADER_CLASS}>
        <SquareTerminal className="size-4 shrink-0" />
        <strong className="shrink-0">LOCAL</strong>
        <span className="hidden min-w-0 truncate text-muted-foreground sm:inline">{cwd}</span>
        <label className="hidden items-center gap-1 text-[10px] text-muted-foreground sm:ml-2 sm:flex">
          AI:
          <select
            className="rounded border bg-background px-1 py-0.5 text-[10px] text-foreground"
            aria-label="Local AI mode"
            value={aiMode}
            disabled={running}
            onChange={(event) => setAiMode(event.target.value as TerminalAiMode)}
          >
            <option value="off">Off</option>
            <option value="copilot">Copilot</option>
            <option value="assisted">Assisted</option>
            <option value="agent">Agent</option>
          </select>
        </label>
        <Button
          size="sm"
          variant={showArtifacts ? 'secondary' : 'ghost'}
          className="ml-auto hidden h-7 px-2 text-[10px] sm:inline-flex"
          onClick={() => {
            const next = !showArtifacts;
            setShowArtifacts(next);
            if (next) void refreshArtifactLibrary();
          }}
        >
          Results {artifactLibrary.length ? `(${artifactLibrary.length})` : ''}
        </Button>
        <div className="ml-auto flex shrink-0 items-center gap-0.5 sm:ml-0">
          <Button className="h-8 w-8 p-0" size="sm" variant="ghost" aria-label={maximized ? 'Restore Local' : 'Maximize Local'} onClick={() => setMaximized((value) => !value)}>
            {maximized ? <Minimize2 className="size-3.5" /> : <Maximize2 className="size-3.5" />}
          </Button>
          <Button className="h-8 w-8 p-0" size="sm" variant="ghost" aria-label="Minimize Local" onClick={() => setOpen(false)}>
            <ChevronDown className="size-3.5" />
          </Button>
          <Button className="h-8 w-8 p-0" size="sm" variant="ghost" aria-label="Close Local" onClick={() => { abortRef.current?.abort(); setOpen(false); }}>
            <X className="size-3.5" />
          </Button>
        </div>
      </header>
      {showArtifacts && (
        <div className="max-h-[45%] shrink-0 overflow-auto border-b bg-muted/10 px-3 py-2" data-testid="research-results-panel">
          {artifactLibrary.length ? (
            <div className="space-y-2">
              {artifactLibrary.slice(0, 100).map((artifact) => (
                <ResearchArtifactCard
                  key={`library:${artifact.path}:${artifact.createdAt}`}
                  wsName={wsName}
                  artifact={artifact}
                  onInsertIntoManuscript={insertArtifactIntoManuscript}
                />
              ))}
            </div>
          ) : (
            <div className="py-2 text-xs text-muted-foreground">No generated research artifacts yet. Run Python/R analysis or C/C++/WASI programs to create figures, tables, binaries, and results.</div>
          )}
        </div>
      )}
      <div ref={scrollRef} className={`${TERMINAL_CONSOLE_CLASS} overflow-auto px-3 py-2 font-mono text-xs leading-5`}>
        {lines.map((line) => {
          if (line.kind === 'artifacts' && line.artifacts?.length) {
            return (
              <div key={line.id} className="my-2 space-y-2" data-testid="terminal-transcript-line" data-terminal-kind={line.kind}>
                <div className="font-sans text-[10px] font-medium uppercase tracking-wide text-muted-foreground">Generated research artifacts</div>
                {line.artifacts.map((artifact) => (
                  <ResearchArtifactCard
                    key={`${artifact.path}:${artifact.createdAt}`}
                    wsName={wsName}
                    artifact={artifact}
                    onInsertIntoManuscript={insertArtifactIntoManuscript}
                  />
                ))}
              </div>
            );
          }
          const target = line.kind === 'ai' ? undefined : fileTarget(line.text);
          const agentState = line.agentPlan ? agentRuns[line.agentPlan.id] ?? { phase: 'idle' as const, steps: {} } : undefined;
          return (
            <div key={line.id} data-testid="terminal-transcript-line" data-terminal-kind={line.kind} className={line.kind === 'stderr' ? 'text-red-300' : line.kind === 'command' ? 'font-semibold text-white' : line.kind === 'system' ? 'text-white/60' : line.kind === 'ai' ? 'rounded border bg-background/95 px-2 py-1.5 font-sans text-foreground' : 'text-white'}>
              <div className="flex items-start gap-2">
                <div className="min-w-0 flex-1">
                  {target ? (
                    <button type="button" className="cursor-pointer whitespace-pre-wrap text-left hover:underline" onClick={() => openFile(line)}>{line.text}</button>
                  ) : <span className="whitespace-pre-wrap">{line.text}</span>}
                  {line.kind === 'ai' && line.scientificProposal && (
                    <div className="mt-2 border-t pt-2 font-sans">
                      <details>
                        <summary className="cursor-pointer text-xs font-medium">Review generated {line.scientificProposal.runtime === 'python' ? 'Python' : 'R'} script</summary>
                        <pre className="mt-2 max-h-64 overflow-auto whitespace-pre-wrap rounded bg-muted p-2 font-mono text-[11px]">{line.scientificProposal.script}</pre>
                      </details>
                      <div className="mt-2 flex flex-wrap gap-1.5">
                        <button type="button" disabled={running} className="rounded border px-2 py-1 text-[10px] hover:bg-muted disabled:opacity-50" onClick={() => void applyScientificProposal(line.scientificProposal!, false)}>Create script</button>
                        <button type="button" disabled={running} className="rounded border px-2 py-1 text-[10px] hover:bg-muted disabled:opacity-50" onClick={() => void applyScientificProposal(line.scientificProposal!, true)}>Create &amp; Run</button>
                      </div>
                    </div>
                  )}
                  {line.kind === 'ai' && line.workflowResultsProposal && (
                    <div className="mt-2 rounded border bg-background/70 p-2 font-sans" data-testid="workflow-results-proposal">
                      <div className="text-xs font-semibold">{line.workflowResultsProposal.title}</div>
                      <div className="mt-0.5 text-[10px] text-muted-foreground">Pending proposal target: {line.workflowResultsProposal.targetFile}</div>
                      {line.workflowResultsProposal.evidencePaths.length > 0 && (
                        <div className="mt-0.5 text-[10px] text-muted-foreground">Evidence: {line.workflowResultsProposal.evidencePaths.join(', ')}</div>
                      )}
                      <details className="mt-2" open>
                        <summary className="cursor-pointer text-xs font-medium">Review full proposed Results text</summary>
                        <pre className="mt-2 max-h-80 overflow-auto whitespace-pre-wrap rounded bg-muted p-2 font-mono text-[11px]">{line.workflowResultsProposal.text}</pre>
                      </details>
                      {line.workflowResultsProposal.notes.length > 0 && (
                        <ul className="mt-2 list-disc space-y-0.5 pl-4 text-[10px] text-muted-foreground">
                          {line.workflowResultsProposal.notes.map((note) => <li key={`${line.workflowResultsProposal!.id}:note:${note}`}>{note}</li>)}
                        </ul>
                      )}
                      <div className="mt-2">
                        <button
                          type="button"
                          disabled={running}
                          className="rounded border px-2 py-1 text-[10px] hover:bg-muted disabled:opacity-50"
                          onClick={() => void applyWorkflowResultsProposal(line.workflowResultsProposal!)}
                        >Create review change</button>
                      </div>
                    </div>
                  )}
                  {line.kind === 'ai' && line.agentPlan && agentState && (
                    <div className="mt-2 rounded border bg-background/70 p-2 font-sans" data-testid="terminal-agent-plan">
                      <div className="flex items-center justify-between gap-2">
                        <div>
                          <div className="text-xs font-semibold">{line.agentPlan.title}</div>
                          <div className="text-[10px] text-muted-foreground">Agent plan · {agentState.phase}</div>
                        </div>
                        <span className="rounded bg-muted px-1.5 py-0.5 text-[10px]">{line.agentPlan.steps.length} step{line.agentPlan.steps.length === 1 ? '' : 's'}</span>
                      </div>
                      <p className="mt-1 text-[11px] text-muted-foreground">{line.agentPlan.summary}</p>
                      <div className="mt-2 space-y-1.5">
                        {line.agentPlan.steps.map((step, index) => {
                          const state = agentState.steps[step.id] ?? { status: 'pending' as const };
                          const marker = state.status === 'completed' ? '✓' : state.status === 'failed' ? '✕' : state.status === 'running' ? '●' : state.status === 'skipped' ? '–' : '○';
                          return (
                            <div key={step.id} className="rounded border px-2 py-1.5 text-[10px]">
                              <div className="flex items-start gap-1.5">
                                <span className="w-3 shrink-0 text-center font-mono">{marker}</span>
                                <div className="min-w-0 flex-1">
                                  <div className="font-medium">{index + 1}. {step.title}</div>
                                  <div className="text-muted-foreground">{step.tool} · {step.capability} · {state.status}</div>
                                  <div className="mt-0.5 text-muted-foreground">{step.reason}</div>
                                  <details className="mt-1">
                                    <summary className="cursor-pointer">Review arguments</summary>
                                    <pre className="mt-1 max-h-48 overflow-auto whitespace-pre-wrap rounded bg-muted p-1.5 font-mono text-[10px]">{JSON.stringify(redactAgentArgs(step), null, 2)}</pre>
                                  </details>
                                  {typeof step.args.content === 'string' && (
                                    <details className="mt-1">
                                      <summary className="cursor-pointer">Review full proposed file content</summary>
                                      <pre className="mt-1 max-h-80 overflow-auto whitespace-pre-wrap rounded bg-muted p-1.5 font-mono text-[10px]">{step.args.content}</pre>
                                    </details>
                                  )}
                                  {state.output && (
                                    <details className="mt-1" open={state.status === 'failed'}>
                                      <summary className="cursor-pointer">{state.status === 'failed' ? 'Error/output' : 'Tool result'}</summary>
                                      <pre className="mt-1 max-h-48 overflow-auto whitespace-pre-wrap rounded bg-muted p-1.5 font-mono text-[10px]">{state.output}</pre>
                                    </details>
                                  )}
                                </div>
                              </div>
                            </div>
                          );
                        })}
                      </div>
                      <div className="mt-2 flex flex-wrap gap-1.5">
                        <button
                          type="button"
                          disabled={running || agentState.phase !== 'idle' || aiMode !== 'agent'}
                          className="rounded border px-2 py-1 text-[10px] hover:bg-muted disabled:opacity-50"
                          onClick={() => void runAgentPlan(line.agentPlan!)}
                        >Run plan</button>
                        <button
                          type="button"
                          disabled={running || agentState.phase === 'completed'}
                          className="rounded border px-2 py-1 text-[10px] hover:bg-muted disabled:opacity-50"
                          onClick={() => void reviseAgentPlan(line.agentPlan!)}
                        >Edit plan</button>
                        <button
                          type="button"
                          disabled={agentState.phase === 'completed' || agentState.phase === 'cancelled'}
                          className="rounded border px-2 py-1 text-[10px] hover:bg-muted disabled:opacity-50"
                          onClick={() => cancelAgentPlan(line.agentPlan!)}
                        >Cancel</button>
                        {agentState.phase === 'completed' && agentState.results?.some((item) => item.ok && (item.tool === 'scientific.run' || item.tool === 'workflow.rerunArtifact' || item.artifactPaths.length > 0)) && (
                          <button
                            type="button"
                            disabled={running}
                            className="rounded border px-2 py-1 text-[10px] hover:bg-muted disabled:opacity-50"
                            onClick={() => void draftWorkflowResults(line.agentPlan!)}
                          >Draft Results section</button>
                        )}
                      </div>
                      {aiMode !== 'agent' && <div className="mt-1 text-[10px] text-muted-foreground">Select Agent mode to execute this plan.</div>}
                    </div>
                  )}
                </div>
                {line.kind === 'stderr' && aiMode !== 'off' && (
                  <div className="flex shrink-0 gap-1 font-sans">
                    <button type="button" className="rounded border px-1.5 text-[10px] hover:bg-muted" onClick={() => { setInput('ai explain'); inputRef.current?.focus(); }}>Explain</button>
                    <button type="button" className="rounded border px-1.5 text-[10px] hover:bg-muted" onClick={() => { setInput('ai fix'); inputRef.current?.focus(); }}>Fix</button>
                  </div>
                )}
              </div>
            </div>
          );
        })}
        {running && <div className="text-muted-foreground">running…</div>}
      </div>
      <div className={TERMINAL_COMMAND_ROW_CLASS}>
        <span className="select-none font-mono text-xs text-white/70">{cwd.replace('/workspace', '~') || '~'} $</span>
        <input
          ref={inputRef}
          autoFocus
          className={TERMINAL_COMMAND_INPUT_CLASS}
          data-testid="terminal-input"
          value={input}
          disabled={running}
          onChange={(event) => setInput(event.target.value)}
          onKeyDown={(event) => {
            if (event.key === 'Enter') { event.preventDefault(); void run(); return; }
            if (event.key === 'c' && event.ctrlKey && running) { event.preventDefault(); abortRef.current?.abort(new DOMException('Interrupted', 'AbortError')); append('system', '^C'); return; }
            if (event.key === 'ArrowUp') {
              event.preventDefault();
              if (!history.length) return;
              const next = historyIndex < 0 ? history.length - 1 : Math.max(0, historyIndex - 1);
              setHistoryIndex(next); setInput(history[next] ?? ''); return;
            }
            if (event.key === 'ArrowDown') {
              event.preventDefault();
              if (historyIndex < 0) return;
              const next = historyIndex + 1;
              if (next >= history.length) { setHistoryIndex(-1); setInput(''); }
              else { setHistoryIndex(next); setInput(history[next] ?? ''); }
            }
          }}
          placeholder={aiMode === 'agent' ? 'Type a command or ai agent "task"…' : 'Type a command…'}
          aria-label="Local command"
        />
      </div>
    </section>
  );
}
