/** * CodeEditorPage * * Main page for the CodeEditor feature. * Three-panel layout: FileList (left) | Chat (center) | DiffPreview (right) * Files are dragged from FileList into the prompt textarea as @fileName references. */ import React, { useState, useRef, useCallback } from 'react'; import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; import { useCodeEditor } from './useCodeEditor'; import { FileListPanel } from './FileListPanel'; import { DiffPreviewPanel } from './DiffPreviewPanel'; import { useResizablePanels } from '../../../hooks/useResizablePanels'; import { Messages } from '../../../components/UiComponents'; import { FaPaperPlane, FaStop, FaRobot, FaEdit } from 'react-icons/fa'; import styles from './CodeEditor.module.css'; export const CodeEditorPage: React.FC = () => { const { instance } = useCurrentInstance(); const instanceId = instance?.id || ''; const inputRef = useRef(null); const [inputValue, setInputValue] = useState(''); const [mode, setMode] = useState<'simple' | 'agent'>('simple'); const [isDragOver, setIsDragOver] = useState(false); const { messages, pendingEdits, acceptEdit, rejectEdit, isProcessing, sendMessage, stopProcessing, files, agentProgress, } = useCodeEditor(instanceId); const { leftWidth: fileListWidth, handleMouseDown: fileListMouseDown, containerRef: outerContainerRef, isDragging: isDraggingLeft, } = useResizablePanels({ storageKey: 'codeeditor-filelist-width', defaultLeftWidth: 20, minLeftWidth: 10, maxLeftWidth: 40, }); const { leftWidth: chatWidth, handleMouseDown: chatMouseDown, containerRef: innerContainerRef, isDragging: isDraggingRight, } = useResizablePanels({ storageKey: 'codeeditor-chat-width', defaultLeftWidth: 60, minLeftWidth: 30, maxLeftWidth: 80, }); const handleSubmit = useCallback(() => { const trimmed = inputValue.trim(); if (!trimmed || isProcessing) return; sendMessage(trimmed, mode); setInputValue(''); }, [inputValue, isProcessing, sendMessage, mode]); const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSubmit(); } }, [handleSubmit]); const handleDragOver = useCallback((e: React.DragEvent) => { if (e.dataTransfer.types.includes('application/x-codeeditor-file')) { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; setIsDragOver(true); } }, []); const handleDragLeave = useCallback(() => { setIsDragOver(false); }, []); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); const fileDataStr = e.dataTransfer.getData('application/x-codeeditor-file'); if (!fileDataStr) return; try { const fileData = JSON.parse(fileDataStr); const tag = `@${fileData.fileName}`; const textarea = inputRef.current; if (textarea) { const pos = textarea.selectionStart || inputValue.length; const before = inputValue.slice(0, pos); const after = inputValue.slice(pos); const spaceBefore = before.length > 0 && !before.endsWith(' ') && !before.endsWith('\n') ? ' ' : ''; const spaceAfter = after.length > 0 && !after.startsWith(' ') && !after.startsWith('\n') ? ' ' : ''; const newValue = `${before}${spaceBefore}${tag}${spaceAfter}${after}`; setInputValue(newValue); requestAnimationFrame(() => { const newPos = pos + spaceBefore.length + tag.length + spaceAfter.length; textarea.focus(); textarea.setSelectionRange(newPos, newPos); }); } else { setInputValue(prev => prev + (prev && !prev.endsWith(' ') ? ' ' : '') + tag + ' '); } } catch { // ignore malformed drop data } }, [inputValue]); return (
{/* Left: File List */}
{/* Center + Right */}
{/* Center: Chat */}
{agentProgress && isProcessing && (
Round {agentProgress.round} | {agentProgress.totalToolCalls} tools |{' '} {agentProgress.costCHF.toFixed(4)} CHF
)}