Skip to main content
Glama
CodeEditor.tsx•6.58 kB
import React, { useCallback } from 'react'; import { useTool } from '@modelcontextprotocol/sdk/react'; import { PlayIcon, CloseIcon, PreviewIcon } from './icons.js'; import type { File, BottomPanelView } from '../App.js'; import type { VFS } from '../lib/vfs.js'; interface CodeEditorProps { openFiles: File[]; setOpenFiles: React.Dispatch<React.SetStateAction<File[]>>; activeFile: string | null; setActiveFile: (path: string | null) => void; addToTerminal: (output: string) => void; vfs: VFS; setPreviewContent: (content: string) => void; setActiveBottomView: (view: BottomPanelView) => void; } const CodeEditor: React.FC<CodeEditorProps> = ({ openFiles, setOpenFiles, activeFile, setActiveFile, addToTerminal, vfs, setPreviewContent, setActiveBottomView }) => { const { call: runCommand, isPending } = useTool('run_bash_command'); const currentFile = openFiles.find(f => f.path === activeFile); const handleCodeChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { if (activeFile) { const updatedFiles = openFiles.map(f => f.path === activeFile ? { ...f, content: e.target.value, isDirty: true } : f ); setOpenFiles(updatedFiles); } }; const saveFile = useCallback(async (file: File) => { if (!file.isDirty) return; try { await vfs.writeFile(file.path, file.content); setOpenFiles(files => files.map(f => f.path === file.path ? {...f, isDirty: false} : f)); addToTerminal(`Saved ${file.path}`); } catch(e) { addToTerminal(`Error saving ${file.path}: ${e instanceof Error ? e.message : String(e)}`); } }, [vfs, addToTerminal, setOpenFiles]); const handleTabClick = (path: string) => { const current = openFiles.find(f => f.path === activeFile); if(current?.isDirty) { saveFile(current); } setActiveFile(path); }; const closeFile = (path: string) => { const fileToClose = openFiles.find(f => f.path === path); if (fileToClose?.isDirty) { saveFile(fileToClose); } const updatedFiles = openFiles.filter(f => f.path !== path); setOpenFiles(updatedFiles); if (activeFile === path) { setActiveFile(updatedFiles.length > 0 ? updatedFiles[updatedFiles.length-1].path : null); } }; const handleRunCode = async () => { if (currentFile) { if(currentFile.isDirty) { await saveFile(currentFile); } addToTerminal(`Executing ${currentFile.path}...`); // Simple logic to choose interpreter based on file extension const command = currentFile.path.endsWith('.py') ? `python "${currentFile.path}"` : `sh "${currentFile.path}"`; const result = await runCommand({ command }); const output = result?.content?.[0]?.type === 'text' ? result.content[0].text.split('\nCWD_MARKER:')[0].trim() : 'No output'; addToTerminal(output); } }; const handlePreview = async () => { const filePath = prompt("Enter the path to the HTML file to preview:", "/workspace/index.html"); if (!filePath) return; addToTerminal(`Attempting to preview: ${filePath}`); try { const content = await vfs.readFile(filePath); setPreviewContent(content); setActiveBottomView('preview'); addToTerminal(`Successfully loaded ${filePath} into web preview.`); } catch (e) { const errorMessage = e instanceof Error ? e.message : String(e); addToTerminal(`Error reading file for preview: ${errorMessage}`); alert(`Could not read file '${filePath}'. Make sure it exists.`); } }; return ( <div style={styles.editorContainer}> <div style={styles.tabsContainer}> {openFiles.map(file => ( <div key={file.path} style={{ ...styles.tab, ...(file.path === activeFile ? styles.activeTab : {}) }} onClick={() => handleTabClick(file.path)} > <span>{file.path.split('/').pop()}{file.isDirty ? '*' : ''}</span> <button style={styles.closeButton} onClick={(e) => { e.stopPropagation(); closeFile(file.path); }}> <CloseIcon /> </button> </div> ))} </div> <div style={styles.editorActions}> <button style={styles.previewButton} onClick={handlePreview} title="Preview HTML file"> <PreviewIcon /> Preview </button> <button style={styles.runButton} onClick={handleRunCode} disabled={!currentFile || isPending}> <PlayIcon /> {isPending ? 'Running...' : 'Run'} </button> </div> <textarea style={styles.textArea} value={currentFile?.content || ''} onChange={handleCodeChange} onBlur={() => currentFile && saveFile(currentFile)} placeholder={openFiles.length === 0 ? "Open a file from the explorer to start editing." : ""} disabled={!currentFile} aria-label="Code Editor" /> </div> ); }; const styles: { [key: string]: React.CSSProperties } = { editorContainer: { flex: 1, display: 'flex', flexDirection: 'column', backgroundColor: 'var(--background-primary)' }, tabsContainer: { display: 'flex', backgroundColor: 'var(--background-secondary)', flexShrink: 0 }, tab: { padding: '8px 12px', cursor: 'pointer', borderRight: '1px solid var(--border-color)', color: 'var(--text-secondary)', display: 'flex', alignItems: 'center', gap: '8px' }, activeTab: { backgroundColor: 'var(--background-primary)', color: 'var(--text-primary)' }, closeButton: { background: 'none', border: 'none', color: 'inherit', cursor: 'pointer', padding: '0', display: 'flex' }, editorActions: { padding: '5px', backgroundColor: 'var(--background-secondary)', borderBottom: '1px solid var(--border-color)', display: 'flex', justifyContent: 'flex-end', flexShrink: 0, gap: '10px' }, runButton: { background: 'var(--accent-primary)', color: 'white', border: 'none', padding: '5px 10px', borderRadius: '3px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '5px' }, previewButton: { background: 'var(--background-tertiary)', color: 'var(--text-primary)', border: '1px solid var(--border-color)', padding: '5px 10px', borderRadius: '3px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '5px' }, textArea: { flex: 1, backgroundColor: 'var(--background-primary)', color: 'var(--text-primary)', border: 'none', outline: 'none', padding: '10px', fontFamily: 'monospace', fontSize: '14px', resize: 'none' }, }; export default CodeEditor;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pythondev-pro/egw_writings_mcp_server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server