App.tsxā¢6.18 kB
import React, { useState, useCallback, useRef, useEffect } from 'react';
import ActivityBar from './components/ActivityBar.js';
import FileExplorer from './components/FileExplorer.js';
import PackageManager from './components/PackageManager.js';
import CodeEditor from './components/CodeEditor.js';
import BottomPanel from './components/BottomPanel.js';
import { VFS } from './lib/vfs.js';
import { useTool } from '@modelcontextprotocol/sdk/react';
import SourceControlPanel from './components/SourceControlPanel.js';
export type View = 'files' | 'packages' | 'source-control';
export type File = { path: string; content: string; isDirty?: boolean };
export type BottomPanelView = 'terminal' | 'preview';
const App = () => {
const [activeView, setActiveView] = useState<View>('files');
const [sidebarWidth, setSidebarWidth] = useState(250);
const [terminalHeight, setTerminalHeight] = useState(200);
const [isResizingSidebar, setIsResizingSidebar] = useState(false);
const [isResizingTerminal, setIsResizingTerminal] = useState(false);
const [openFiles, setOpenFiles] = useState<File[]>([]);
const [activeFile, setActiveFile] = useState<string | null>(null);
const [terminalOutput, setTerminalOutput] = useState<string[]>(['Welcome to PyForge! All command output from the UI will appear here. For an interactive shell, use the Terminal tab below.']);
const [isLoading, setIsLoading] = useState(false);
const [activeBottomView, setActiveBottomView] = useState<BottomPanelView>('terminal');
const [previewContent, setPreviewContent] = useState<string | null>(null);
const [refreshFileTreeKey, setRefreshFileTreeKey] = useState(0);
const runCommandTool = useTool('run_bash_command');
const vfs = React.useMemo(() => new VFS(runCommandTool), [runCommandTool]);
const addToTerminal = useCallback((output: string) => {
// Add a timestamp to distinguish logs
const timestamp = new Date().toLocaleTimeString();
setTerminalOutput(prev => [...prev, `[${timestamp}] ${output}`]);
}, []);
const refreshFileTree = useCallback(() => {
setRefreshFileTreeKey(k => k + 1);
}, []);
const openFile = useCallback(async (path: string) => {
const existingFile = openFiles.find(f => f.path === path);
if (existingFile) {
setActiveFile(path);
return;
}
setIsLoading(true);
addToTerminal(`Opening file: ${path}`);
try {
const content = await vfs.readFile(path);
setOpenFiles(prev => [...prev, { path, content }]);
setActiveFile(path);
addToTerminal(`Successfully opened ${path}`);
} catch (error) {
addToTerminal(`Error opening file ${path}: ${error instanceof Error ? error.message : String(error)}`);
} finally {
setIsLoading(false);
}
}, [openFiles, vfs, addToTerminal]);
const handleMouseMove = useCallback((e: MouseEvent) => {
if (isResizingSidebar) {
setSidebarWidth(e.clientX);
}
if (isResizingTerminal) {
setTerminalHeight(window.innerHeight - e.clientY);
}
}, [isResizingSidebar, isResizingTerminal]);
const handleMouseUp = useCallback(() => {
setIsResizingSidebar(false);
setIsResizingTerminal(false);
}, []);
useEffect(() => {
if (isResizingSidebar || isResizingTerminal) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isResizingSidebar, isResizingTerminal, handleMouseMove, handleMouseUp]);
return (
<div style={styles.appContainer}>
<ActivityBar activeView={activeView} setActiveView={setActiveView} />
<div style={{ ...styles.sidebar, width: sidebarWidth }}>
{activeView === 'files' && <FileExplorer vfs={vfs} onOpenFile={openFile} addToTerminal={addToTerminal} refreshKey={refreshFileTreeKey} />}
{activeView === 'packages' && <PackageManager addToTerminal={addToTerminal} />}
{activeView === 'source-control' && <SourceControlPanel addToTerminal={addToTerminal} refreshFileTree={refreshFileTree} />}
</div>
<div
style={styles.sidebarResizer}
onMouseDown={() => setIsResizingSidebar(true)}
/>
<div style={styles.mainContent}>
<CodeEditor
openFiles={openFiles}
setOpenFiles={setOpenFiles}
activeFile={activeFile}
setActiveFile={setActiveFile}
addToTerminal={addToTerminal}
vfs={vfs}
setPreviewContent={setPreviewContent}
setActiveBottomView={setActiveBottomView}
/>
<div
style={styles.terminalResizer}
onMouseDown={() => setIsResizingTerminal(true)}
/>
<div style={{ ...styles.terminalContainer, height: terminalHeight }}>
<BottomPanel
// FIX: Pass the correct state for the bottom panel view.
activeView={activeBottomView}
setActiveView={setActiveBottomView}
terminalOutput={terminalOutput}
setTerminalOutput={setTerminalOutput}
htmlContent={previewContent}
/>
</div>
</div>
</div>
);
};
const styles: { [key: string]: React.CSSProperties } = {
appContainer: {
display: 'flex',
height: '100vh',
width: '100vw',
backgroundColor: 'var(--background-primary)',
},
sidebar: {
height: '100%',
backgroundColor: 'var(--background-secondary)',
display: 'flex',
flexDirection: 'column',
color: 'var(--text-primary)',
borderRight: '1px solid var(--border-color)',
},
sidebarResizer: {
width: '5px',
cursor: 'col-resize',
backgroundColor: 'var(--border-color)',
zIndex: 10,
},
mainContent: {
flex: 1,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
},
terminalResizer: {
height: '5px',
cursor: 'row-resize',
backgroundColor: 'var(--border-color)',
zIndex: 10,
},
terminalContainer: {
width: '100%',
backgroundColor: 'var(--background-secondary)',
},
};
export default App;