Phase 2: ACE Editor integration and file operations
- Added react-ace and ace-builds dependencies - Created EditorPanel component with ACE editor integration - Implemented file read/write socket protocol - Added backend handlers for fileReadRequest and fileWriteRequest - Implemented file loading from tree click - Implemented file saving with Ctrl+S shortcut - Added dirty state tracking and unsaved changes indicator - Enforced workspace mode (read-only in Agent mode) - Added security: path traversal prevention, binary file detection, file size limits - Updated FilesPanel with split view (tree + editor) Enables Users to edit files for the first time in Gadget Code.
This commit is contained in:
parent
ec7b83d610
commit
1e13f95808
@ -11,7 +11,9 @@
|
|||||||
"author": "Robert Colbert <rob.colbert@openplatform.us>",
|
"author": "Robert Colbert <rob.colbert@openplatform.us>",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ace-builds": "^1.36.0",
|
||||||
"marked": "^16.0.0",
|
"marked": "^16.0.0",
|
||||||
|
"react-ace": "^12.0.0",
|
||||||
"slug": "^11.0.1"
|
"slug": "^11.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
330
gadget-code/frontend/src/components/EditorPanel.tsx
Normal file
330
gadget-code/frontend/src/components/EditorPanel.tsx
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
import AceEditor from 'react-ace';
|
||||||
|
import 'ace-builds/src-noconflict/mode-text';
|
||||||
|
import 'ace-builds/src-noconflict/theme-tomorrow';
|
||||||
|
import 'ace-builds/src-noconflict/ext-language_tools';
|
||||||
|
import { WorkspaceMode } from '../lib/types';
|
||||||
|
import { socketClient } from '../lib/socket';
|
||||||
|
|
||||||
|
interface EditorPanelProps {
|
||||||
|
workspaceMode: WorkspaceMode;
|
||||||
|
filePath?: string;
|
||||||
|
onCloseFile?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditorState {
|
||||||
|
content: string;
|
||||||
|
originalContent: string;
|
||||||
|
isDirty: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
isSaving: boolean;
|
||||||
|
error?: string;
|
||||||
|
language: string;
|
||||||
|
successMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map file extensions to ACE language modes
|
||||||
|
function detectLanguage(filePath: string): string {
|
||||||
|
const ext = filePath.split('.').pop()?.toLowerCase();
|
||||||
|
const languageMap: Record<string, string> = {
|
||||||
|
'js': 'javascript',
|
||||||
|
'jsx': 'javascript',
|
||||||
|
'ts': 'typescript',
|
||||||
|
'tsx': 'typescript',
|
||||||
|
'py': 'python',
|
||||||
|
'rb': 'ruby',
|
||||||
|
'java': 'java',
|
||||||
|
'c': 'c_cpp',
|
||||||
|
'cpp': 'c_cpp',
|
||||||
|
'h': 'c_cpp',
|
||||||
|
'hpp': 'c_cpp',
|
||||||
|
'cs': 'csharp',
|
||||||
|
'go': 'golang',
|
||||||
|
'rs': 'rust',
|
||||||
|
'php': 'php',
|
||||||
|
'html': 'html',
|
||||||
|
'htm': 'html',
|
||||||
|
'css': 'css',
|
||||||
|
'scss': 'scss',
|
||||||
|
'sass': 'sass',
|
||||||
|
'less': 'less',
|
||||||
|
'json': 'json',
|
||||||
|
'xml': 'xml',
|
||||||
|
'yaml': 'yaml',
|
||||||
|
'yml': 'yaml',
|
||||||
|
'md': 'markdown',
|
||||||
|
'sql': 'sql',
|
||||||
|
'sh': 'sh',
|
||||||
|
'bash': 'sh',
|
||||||
|
'dockerfile': 'dockerfile',
|
||||||
|
'makefile': 'makefile',
|
||||||
|
};
|
||||||
|
return languageMap[ext || ''] || 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EditorPanel({ workspaceMode, filePath, onCloseFile }: EditorPanelProps) {
|
||||||
|
const [state, setState] = useState<EditorState>({
|
||||||
|
content: '',
|
||||||
|
originalContent: '',
|
||||||
|
isDirty: false,
|
||||||
|
isLoading: false,
|
||||||
|
isSaving: false,
|
||||||
|
language: 'text',
|
||||||
|
});
|
||||||
|
|
||||||
|
const isReadOnly = workspaceMode === WorkspaceMode.Agent;
|
||||||
|
const isReadWrite = workspaceMode === WorkspaceMode.User;
|
||||||
|
|
||||||
|
// Load file when filePath changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (filePath) {
|
||||||
|
loadFile(filePath);
|
||||||
|
} else {
|
||||||
|
// Clear editor when no file is selected
|
||||||
|
setState({
|
||||||
|
content: '',
|
||||||
|
originalContent: '',
|
||||||
|
isDirty: false,
|
||||||
|
isLoading: false,
|
||||||
|
isSaving: false,
|
||||||
|
language: 'text',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [filePath]);
|
||||||
|
|
||||||
|
const loadFile = useCallback(async (path: string) => {
|
||||||
|
setState(prev => ({ ...prev, isLoading: true, error: undefined }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await socketClient.requestFileRead({ path });
|
||||||
|
|
||||||
|
if (result.success && result.content !== undefined) {
|
||||||
|
const language = detectLanguage(path);
|
||||||
|
|
||||||
|
// Dynamically import the language mode
|
||||||
|
try {
|
||||||
|
await import(`ace-builds/src-noconflict/mode-${language}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Could not load ACE mode for ${language}, falling back to text`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState({
|
||||||
|
content: result.content,
|
||||||
|
originalContent: result.content,
|
||||||
|
isDirty: false,
|
||||||
|
isLoading: false,
|
||||||
|
isSaving: false,
|
||||||
|
language,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
isLoading: false,
|
||||||
|
error: result.error || 'Failed to load file',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to load file',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveFile = useCallback(async () => {
|
||||||
|
if (!filePath || isReadOnly) return;
|
||||||
|
|
||||||
|
setState(prev => ({ ...prev, isSaving: true, error: undefined }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await socketClient.requestFileWrite({
|
||||||
|
path: filePath,
|
||||||
|
content: state.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
isSaving: false,
|
||||||
|
originalContent: prev.content,
|
||||||
|
isDirty: false,
|
||||||
|
successMessage: 'File saved successfully',
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Clear success message after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
setState(prev => ({ ...prev, successMessage: undefined }));
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
isSaving: false,
|
||||||
|
error: result.error || 'Failed to save file',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
isSaving: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to save file',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [filePath, state.content, isReadOnly]);
|
||||||
|
|
||||||
|
const handleContentChange = useCallback((newValue: string) => {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
content: newValue,
|
||||||
|
isDirty: newValue !== prev.originalContent,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||||
|
// Ctrl+S or Cmd+S to save
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isReadOnly && state.isDirty) {
|
||||||
|
saveFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [saveFile, isReadOnly, state.isDirty]);
|
||||||
|
|
||||||
|
// Add keyboard listener
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [handleKeyDown]);
|
||||||
|
|
||||||
|
// If no file is selected, show placeholder
|
||||||
|
if (!filePath) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex items-center justify-center bg-bg-secondary">
|
||||||
|
<div className="text-center text-text-muted">
|
||||||
|
<svg className="w-16 h-16 mx-auto mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
<p className="text-lg font-medium">No file open</p>
|
||||||
|
<p className="text-sm mt-2">Select a file from the tree to edit</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col bg-bg-secondary min-h-0">
|
||||||
|
{/* Editor Header */}
|
||||||
|
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary border-b border-border-subtle">
|
||||||
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
|
<span className="text-sm font-medium text-text-primary truncate">
|
||||||
|
{filePath}
|
||||||
|
</span>
|
||||||
|
{state.isDirty && (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-yellow-500">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-yellow-500"></span>
|
||||||
|
Unsaved changes
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{state.successMessage && (
|
||||||
|
<span className="text-xs text-green-500">
|
||||||
|
{state.successMessage}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isReadOnly && (
|
||||||
|
<span className="text-xs text-text-muted px-2 py-1 bg-bg-secondary rounded border border-border-default">
|
||||||
|
Read-only (Agent mode)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={saveFile}
|
||||||
|
disabled={isReadOnly || !state.isDirty || state.isSaving}
|
||||||
|
className={`px-3 py-1.5 text-xs font-medium rounded transition-colors ${
|
||||||
|
isReadOnly || !state.isDirty || state.isSaving
|
||||||
|
? 'bg-bg-secondary text-text-muted cursor-not-allowed'
|
||||||
|
: 'bg-brand text-white hover:bg-brand/80'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{state.isSaving ? 'Saving...' : 'Save'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{onCloseFile && (
|
||||||
|
<button
|
||||||
|
onClick={onCloseFile}
|
||||||
|
className="p-1.5 text-text-muted hover:text-text-primary hover:bg-bg-secondary rounded transition-colors"
|
||||||
|
title="Close file"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error State */}
|
||||||
|
{state.error && (
|
||||||
|
<div className="px-4 py-3 bg-red-500/10 border-b border-red-500/20">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<svg className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm text-red-500">{state.error}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => loadFile(filePath)}
|
||||||
|
className="mt-2 text-xs text-red-400 hover:text-red-300 underline"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Loading State */}
|
||||||
|
{state.isLoading && (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand mx-auto"></div>
|
||||||
|
<p className="text-sm text-text-muted mt-4">Loading file...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ACE Editor */}
|
||||||
|
{!state.isLoading && (
|
||||||
|
<div className="flex-1 min-h-0">
|
||||||
|
<AceEditor
|
||||||
|
mode={state.language}
|
||||||
|
theme="tomorrow"
|
||||||
|
name="editor"
|
||||||
|
value={state.content}
|
||||||
|
onChange={handleContentChange}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
fontSize={14}
|
||||||
|
showPrintMargin={false}
|
||||||
|
showGutter={true}
|
||||||
|
highlightActiveLine={true}
|
||||||
|
readOnly={isReadOnly}
|
||||||
|
setOptions={{
|
||||||
|
useWorker: false,
|
||||||
|
enableBasicAutocompletion: true,
|
||||||
|
enableLiveAutocompletion: false,
|
||||||
|
enableSnippets: false,
|
||||||
|
tabSize: 2,
|
||||||
|
indentedAutoWrap: true,
|
||||||
|
showLineNumbers: true,
|
||||||
|
wrap: false,
|
||||||
|
}}
|
||||||
|
editorProps={{ $blockScrolling: true }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,14 +1,25 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { WorkspaceMode } from "../lib/types";
|
import { WorkspaceMode } from "../lib/types";
|
||||||
import FileTree from "./FileTree";
|
import FileTree from "./FileTree";
|
||||||
|
import EditorPanel from "./EditorPanel";
|
||||||
|
|
||||||
interface FilesPanelProps {
|
interface FilesPanelProps {
|
||||||
workspaceMode: WorkspaceMode;
|
workspaceMode: WorkspaceMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FilesPanel({ workspaceMode }: FilesPanelProps) {
|
export default function FilesPanel({ workspaceMode }: FilesPanelProps) {
|
||||||
|
const [selectedFilePath, setSelectedFilePath] = useState<string | undefined>(undefined);
|
||||||
const isReadOnly = workspaceMode === WorkspaceMode.Agent;
|
const isReadOnly = workspaceMode === WorkspaceMode.Agent;
|
||||||
const isReadWrite = workspaceMode === WorkspaceMode.User;
|
const isReadWrite = workspaceMode === WorkspaceMode.User;
|
||||||
|
|
||||||
|
const handleFileSelect = (path: string) => {
|
||||||
|
setSelectedFilePath(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseFile = () => {
|
||||||
|
setSelectedFilePath(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-border-subtle flex flex-col flex-1 min-h-0">
|
<div className="border-t border-border-subtle flex flex-col flex-1 min-h-0">
|
||||||
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary">
|
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary">
|
||||||
@ -38,15 +49,27 @@ export default function FilesPanel({ workspaceMode }: FilesPanelProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto">
|
|
||||||
<FileTree
|
{/* Split view: File tree on left, editor on right */}
|
||||||
workspaceMode={workspaceMode}
|
<div className="flex-1 flex overflow-hidden min-h-0">
|
||||||
onFileSelect={(path) => {
|
{/* File Tree - 30% width, resizable in future */}
|
||||||
console.log('File selected:', path);
|
<div className="w-1/3 min-w-[200px] border-r border-border-subtle overflow-auto flex-shrink-0">
|
||||||
// TODO: Open file in editor (next session)
|
<FileTree
|
||||||
}}
|
workspaceMode={workspaceMode}
|
||||||
/>
|
onFileSelect={handleFileSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Editor Panel - remaining space */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<EditorPanel
|
||||||
|
workspaceMode={workspaceMode}
|
||||||
|
filePath={selectedFilePath}
|
||||||
|
onCloseFile={handleCloseFile}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-4 py-2 bg-bg-tertiary border-t border-border-subtle">
|
<div className="px-4 py-2 bg-bg-tertiary border-t border-border-subtle">
|
||||||
<p className="text-xs text-text-muted">
|
<p className="text-xs text-text-muted">
|
||||||
{isReadOnly
|
{isReadOnly
|
||||||
|
|||||||
@ -67,6 +67,14 @@ export interface ClientToServerEvents {
|
|||||||
args: { path?: string; recursive?: boolean; maxDepth?: number },
|
args: { path?: string; recursive?: boolean; maxDepth?: number },
|
||||||
cb: (success: boolean, data: { entries?: any[]; error?: string }) => void,
|
cb: (success: boolean, data: { entries?: any[]; error?: string }) => void,
|
||||||
) => void;
|
) => void;
|
||||||
|
fileReadRequest: (
|
||||||
|
args: { path: string },
|
||||||
|
cb: (success: boolean, data: { content?: string; error?: string }) => void,
|
||||||
|
) => void;
|
||||||
|
fileWriteRequest: (
|
||||||
|
args: { path: string; content: string },
|
||||||
|
cb: (success: boolean, data: { success?: boolean; error?: string }) => void,
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SocketEvents {
|
export interface SocketEvents {
|
||||||
@ -124,6 +132,8 @@ export interface SocketEvents {
|
|||||||
sessionUpdated: (updates: Partial<ChatSession>) => void;
|
sessionUpdated: (updates: Partial<ChatSession>) => void;
|
||||||
tabLockDenied: (data: { message: string }) => void;
|
tabLockDenied: (data: { message: string }) => void;
|
||||||
fileTreeResponse: (path: string, entries: any[], error?: string) => void;
|
fileTreeResponse: (path: string, entries: any[], error?: string) => void;
|
||||||
|
fileReadResponse: (path: string, content: string | null, error?: string) => void;
|
||||||
|
fileWriteResponse: (path: string, success: boolean, error?: string) => void;
|
||||||
status: (content: string) => void;
|
status: (content: string) => void;
|
||||||
reconnect_attempt: (attempt: number) => void;
|
reconnect_attempt: (attempt: number) => void;
|
||||||
reconnect_failed: () => void;
|
reconnect_failed: () => void;
|
||||||
@ -224,6 +234,14 @@ class SocketClient {
|
|||||||
this.emit("fileTreeResponse", path, entries as any[], error);
|
this.emit("fileTreeResponse", path, entries as any[], error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.socket.on("fileReadResponse", (path: string, content: string | null, error?: string) => {
|
||||||
|
this.emit("fileReadResponse", path, content, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("fileWriteResponse", (path: string, success: boolean, error?: string) => {
|
||||||
|
this.emit("fileWriteResponse", path, success, error);
|
||||||
|
});
|
||||||
|
|
||||||
this.socket.on(
|
this.socket.on(
|
||||||
"log",
|
"log",
|
||||||
(
|
(
|
||||||
@ -433,6 +451,40 @@ class SocketClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestFileRead(
|
||||||
|
args: { path: string },
|
||||||
|
): Promise<{ success: boolean; content?: string; error?: string }> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (this._socket?.connected) {
|
||||||
|
this._socket.emit(
|
||||||
|
"fileReadRequest",
|
||||||
|
args,
|
||||||
|
(success: boolean, data: { content?: string; error?: string }) =>
|
||||||
|
resolve({ success, ...data }),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: "Socket not connected" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
requestFileWrite(
|
||||||
|
args: { path: string; content: string },
|
||||||
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (this._socket?.connected) {
|
||||||
|
this._socket.emit(
|
||||||
|
"fileWriteRequest",
|
||||||
|
args,
|
||||||
|
(success: boolean, data: { success?: boolean; error?: string }) =>
|
||||||
|
resolve({ success: success, ...data }),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: "Socket not connected" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
releaseSessionLock(
|
releaseSessionLock(
|
||||||
registration: any,
|
registration: any,
|
||||||
project: any,
|
project: any,
|
||||||
|
|||||||
@ -27,6 +27,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
"@gadget/ai": "workspace:*",
|
"@gadget/ai": "workspace:*",
|
||||||
|
"ace-builds": "^1.36.0",
|
||||||
|
"react-ace": "^12.0.0",
|
||||||
"@gadget/api": "workspace:*",
|
"@gadget/api": "workspace:*",
|
||||||
"@gadget/config": "workspace:*",
|
"@gadget/config": "workspace:*",
|
||||||
"@react-three/drei": "^10.7.7",
|
"@react-three/drei": "^10.7.7",
|
||||||
|
|||||||
@ -62,6 +62,8 @@ export class CodeSession extends SocketSession {
|
|||||||
this.socket.on("releaseSessionLock", this.onReleaseSessionLock.bind(this));
|
this.socket.on("releaseSessionLock", this.onReleaseSessionLock.bind(this));
|
||||||
this.socket.on("sessionHeartbeat", this.onSessionHeartbeat.bind(this));
|
this.socket.on("sessionHeartbeat", this.onSessionHeartbeat.bind(this));
|
||||||
this.socket.on("fileTreeRequest", this.onFileTreeRequest.bind(this));
|
this.socket.on("fileTreeRequest", this.onFileTreeRequest.bind(this));
|
||||||
|
this.socket.on("fileReadRequest", this.onFileReadRequest.bind(this));
|
||||||
|
this.socket.on("fileWriteRequest", this.onFileWriteRequest.bind(this));
|
||||||
|
|
||||||
// Check for active session on connect
|
// Check for active session on connect
|
||||||
this.checkAndReestablishActiveSession();
|
this.checkAndReestablishActiveSession();
|
||||||
@ -451,6 +453,54 @@ export class CodeSession extends SocketSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onFileReadRequest(
|
||||||
|
args: { path: string },
|
||||||
|
cb: (success: boolean, data: { content?: string; error?: string }) => void,
|
||||||
|
): void {
|
||||||
|
if (!this.selectedDrone) {
|
||||||
|
return cb(false, { error: "No drone selected" });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const droneSession = SocketService.getDroneSession(this.selectedDrone);
|
||||||
|
droneSession.socket.emit("fileReadRequest", args, (success: boolean, data: { content?: string; error?: string }) => {
|
||||||
|
// Forward response to IDE
|
||||||
|
if (success && data?.content !== undefined) {
|
||||||
|
this.socket.emit("fileReadResponse", args.path, data.content);
|
||||||
|
} else {
|
||||||
|
this.socket.emit("fileReadResponse", args.path, null, data?.error);
|
||||||
|
}
|
||||||
|
cb(success, data);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error("failed to forward fileReadRequest to drone", { error });
|
||||||
|
cb(false, { error: "Failed to reach drone" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileWriteRequest(
|
||||||
|
args: { path: string; content: string },
|
||||||
|
cb: (success: boolean, data: { success?: boolean; error?: string }) => void,
|
||||||
|
): void {
|
||||||
|
if (!this.selectedDrone) {
|
||||||
|
return cb(false, { error: "No drone selected" });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const droneSession = SocketService.getDroneSession(this.selectedDrone);
|
||||||
|
droneSession.socket.emit("fileWriteRequest", args, (success: boolean, data: { success?: boolean; error?: string }) => {
|
||||||
|
// Forward response to IDE
|
||||||
|
if (success) {
|
||||||
|
this.socket.emit("fileWriteResponse", args.path, true);
|
||||||
|
} else {
|
||||||
|
this.socket.emit("fileWriteResponse", args.path, false, data?.error);
|
||||||
|
}
|
||||||
|
cb(success, data);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error("failed to forward fileWriteRequest to drone", { error });
|
||||||
|
cb(false, { error: "Failed to reach drone" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the IDE sends a releaseSessionLock event to release a
|
* Called when the IDE sends a releaseSessionLock event to release a
|
||||||
* previously-acquired session lock on a gadget-drone instance.
|
* previously-acquired session lock on a gadget-drone instance.
|
||||||
|
|||||||
@ -264,6 +264,14 @@ class GadgetDrone extends GadgetProcess {
|
|||||||
"fileTreeRequest",
|
"fileTreeRequest",
|
||||||
this.onFileTreeRequest.bind(this),
|
this.onFileTreeRequest.bind(this),
|
||||||
);
|
);
|
||||||
|
this.socket.on(
|
||||||
|
"fileReadRequest",
|
||||||
|
this.onFileReadRequest.bind(this),
|
||||||
|
);
|
||||||
|
this.socket.on(
|
||||||
|
"fileWriteRequest",
|
||||||
|
this.onFileWriteRequest.bind(this),
|
||||||
|
);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Handle socket disconnect: clear the heartbeat timer to prevent
|
* Handle socket disconnect: clear the heartbeat timer to prevent
|
||||||
@ -792,6 +800,158 @@ class GadgetDrone extends GadgetProcess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onFileReadRequest(
|
||||||
|
args: { path: string },
|
||||||
|
cb: (success: boolean, data: { content?: string; error?: string }) => void,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!this.sessionLock) {
|
||||||
|
return cb(false, { error: "No session lock active" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the project directory for this session
|
||||||
|
const projectSlug = this.sessionLock.project.slug;
|
||||||
|
const projectRoot = WorkspaceService.getProjectDirectory(projectSlug);
|
||||||
|
|
||||||
|
this.log.debug("fileReadRequest received", {
|
||||||
|
projectSlug,
|
||||||
|
projectRoot,
|
||||||
|
requestedPath: args.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!projectRoot) {
|
||||||
|
return cb(false, { error: `Project directory not found for slug: ${projectSlug}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve path relative to project root
|
||||||
|
const targetPath = path.resolve(projectRoot, args.path);
|
||||||
|
|
||||||
|
// Security: Ensure resolved path is within project root
|
||||||
|
const normalizedTarget = path.normalize(targetPath);
|
||||||
|
const normalizedRoot = path.normalize(projectRoot);
|
||||||
|
if (!normalizedTarget.startsWith(normalizedRoot + path.sep) && normalizedTarget !== normalizedRoot) {
|
||||||
|
this.log.warn("fileReadRequest path traversal attempt", {
|
||||||
|
targetPath: normalizedTarget,
|
||||||
|
projectRoot: normalizedRoot,
|
||||||
|
});
|
||||||
|
return cb(false, { error: "Access denied: path outside project root" });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log.debug("fileReadRequest resolved paths", {
|
||||||
|
projectRoot,
|
||||||
|
targetPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stat = await fs.stat(targetPath);
|
||||||
|
if (!stat.isFile()) {
|
||||||
|
return cb(false, { error: "Path is not a file" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file size (limit to 1MB)
|
||||||
|
const maxSize = 1 * 1024 * 1024; // 1MB
|
||||||
|
if (stat.size > maxSize) {
|
||||||
|
return cb(false, { error: `File too large (${(stat.size / 1024 / 1024).toFixed(2)}MB). Maximum size is 1MB.` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
const content = await fs.readFile(targetPath, "utf-8");
|
||||||
|
|
||||||
|
// Check for binary file (null bytes)
|
||||||
|
if (content.includes('\0')) {
|
||||||
|
return cb(false, { error: "Cannot edit binary files" });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log.debug("fileReadRequest completed", {
|
||||||
|
path: args.path,
|
||||||
|
size: stat.size,
|
||||||
|
});
|
||||||
|
|
||||||
|
cb(true, { content });
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
this.log.error("failed to read file", {
|
||||||
|
path: args.path,
|
||||||
|
targetPath,
|
||||||
|
error: errorMessage,
|
||||||
|
});
|
||||||
|
cb(false, { error: `Failed to read file: ${errorMessage}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onFileWriteRequest(
|
||||||
|
args: { path: string; content: string },
|
||||||
|
cb: (success: boolean, data: { success?: boolean; error?: string }) => void,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!this.sessionLock) {
|
||||||
|
return cb(false, { error: "No session lock active" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate workspace mode - only allow writes in User mode
|
||||||
|
if (this.workspaceMode !== WorkspaceMode.User) {
|
||||||
|
this.log.warn("fileWriteRequest rejected: not in User mode", {
|
||||||
|
workspaceMode: this.workspaceMode,
|
||||||
|
});
|
||||||
|
return cb(false, { error: "File writing is only allowed in User mode" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the project directory for this session
|
||||||
|
const projectSlug = this.sessionLock.project.slug;
|
||||||
|
const projectRoot = WorkspaceService.getProjectDirectory(projectSlug);
|
||||||
|
|
||||||
|
this.log.debug("fileWriteRequest received", {
|
||||||
|
projectSlug,
|
||||||
|
projectRoot,
|
||||||
|
requestedPath: args.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!projectRoot) {
|
||||||
|
return cb(false, { error: `Project directory not found for slug: ${projectSlug}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve path relative to project root
|
||||||
|
const targetPath = path.resolve(projectRoot, args.path);
|
||||||
|
|
||||||
|
// Security: Ensure resolved path is within project root
|
||||||
|
const normalizedTarget = path.normalize(targetPath);
|
||||||
|
const normalizedRoot = path.normalize(projectRoot);
|
||||||
|
if (!normalizedTarget.startsWith(normalizedRoot + path.sep) && normalizedTarget !== normalizedRoot) {
|
||||||
|
this.log.warn("fileWriteRequest path traversal attempt", {
|
||||||
|
targetPath: normalizedTarget,
|
||||||
|
projectRoot: normalizedRoot,
|
||||||
|
});
|
||||||
|
return cb(false, { error: "Access denied: path outside project root" });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log.debug("fileWriteRequest resolved paths", {
|
||||||
|
projectRoot,
|
||||||
|
targetPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ensure parent directory exists
|
||||||
|
const parentDir = path.dirname(targetPath);
|
||||||
|
await fs.mkdir(parentDir, { recursive: true });
|
||||||
|
|
||||||
|
// Write file content
|
||||||
|
await fs.writeFile(targetPath, args.content, "utf-8");
|
||||||
|
|
||||||
|
this.log.info("fileWriteRequest completed", {
|
||||||
|
path: args.path,
|
||||||
|
size: args.content.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
cb(true, { success: true });
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
this.log.error("failed to write file", {
|
||||||
|
path: args.path,
|
||||||
|
targetPath,
|
||||||
|
error: errorMessage,
|
||||||
|
});
|
||||||
|
cb(false, { error: `Failed to write file: ${errorMessage}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async listDirectoryForTree(
|
private async listDirectoryForTree(
|
||||||
dir: string,
|
dir: string,
|
||||||
showHidden: boolean,
|
showHidden: boolean,
|
||||||
|
|||||||
@ -133,3 +133,43 @@ export type FileTreeResponseMessage = (
|
|||||||
entries: FileTreeEntry[],
|
entries: FileTreeEntry[],
|
||||||
error?: string,
|
error?: string,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* fileReadRequest / fileReadResponse
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type FileReadRequestCallback = (
|
||||||
|
success: boolean,
|
||||||
|
data: { content?: string; error?: string },
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export type FileReadRequestMessage = (
|
||||||
|
args: { path: string },
|
||||||
|
cb: FileReadRequestCallback,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export type FileReadResponseMessage = (
|
||||||
|
path: string,
|
||||||
|
content: string | null,
|
||||||
|
error?: string,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* fileWriteRequest / fileWriteResponse
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type FileWriteRequestCallback = (
|
||||||
|
success: boolean,
|
||||||
|
data: { success?: boolean; error?: string },
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export type FileWriteRequestMessage = (
|
||||||
|
args: { path: string; content: string },
|
||||||
|
cb: FileWriteRequestCallback,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export type FileWriteResponseMessage = (
|
||||||
|
path: string,
|
||||||
|
success: boolean,
|
||||||
|
error?: string,
|
||||||
|
) => void;
|
||||||
|
|||||||
@ -25,6 +25,10 @@ import {
|
|||||||
SubmitPromptMessage,
|
SubmitPromptMessage,
|
||||||
FileTreeRequestMessage,
|
FileTreeRequestMessage,
|
||||||
FileTreeResponseMessage,
|
FileTreeResponseMessage,
|
||||||
|
FileReadRequestMessage,
|
||||||
|
FileReadResponseMessage,
|
||||||
|
FileWriteRequestMessage,
|
||||||
|
FileWriteResponseMessage,
|
||||||
} from "./ide.ts";
|
} from "./ide.ts";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -57,6 +61,8 @@ export interface ClientToServerEvents {
|
|||||||
releaseSessionLock: ReleaseSessionLockMessage;
|
releaseSessionLock: ReleaseSessionLockMessage;
|
||||||
sessionHeartbeat: SessionHeartbeatMessage;
|
sessionHeartbeat: SessionHeartbeatMessage;
|
||||||
fileTreeRequest: FileTreeRequestMessage;
|
fileTreeRequest: FileTreeRequestMessage;
|
||||||
|
fileReadRequest: FileReadRequestMessage;
|
||||||
|
fileWriteRequest: FileWriteRequestMessage;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* gadget-drone => gadget-code:web
|
* gadget-drone => gadget-code:web
|
||||||
@ -121,6 +127,8 @@ export interface ServerToClientEvents {
|
|||||||
crashRecoveryResponse: CrashRecoveryResponseMessage;
|
crashRecoveryResponse: CrashRecoveryResponseMessage;
|
||||||
requestTermination: RequestTerminationMessage;
|
requestTermination: RequestTerminationMessage;
|
||||||
fileTreeRequest: FileTreeRequestMessage;
|
fileTreeRequest: FileTreeRequestMessage;
|
||||||
|
fileReadRequest: FileReadRequestMessage;
|
||||||
|
fileWriteRequest: FileWriteRequestMessage;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* gadget-code:web => gadget-code:ide
|
* gadget-code:web => gadget-code:ide
|
||||||
@ -136,6 +144,8 @@ export interface ServerToClientEvents {
|
|||||||
sessionUpdated: SessionUpdatedMessage;
|
sessionUpdated: SessionUpdatedMessage;
|
||||||
tabLockDenied: (data: { message: string }) => void;
|
tabLockDenied: (data: { message: string }) => void;
|
||||||
fileTreeResponse: FileTreeResponseMessage;
|
fileTreeResponse: FileTreeResponseMessage;
|
||||||
|
fileReadResponse: FileReadResponseMessage;
|
||||||
|
fileWriteResponse: FileWriteResponseMessage;
|
||||||
"agent:thinking": AgentThinkingMessage;
|
"agent:thinking": AgentThinkingMessage;
|
||||||
"agent:response": AgentResponseMessage;
|
"agent:response": AgentResponseMessage;
|
||||||
"agent:tool-call": AgentToolCallMessage;
|
"agent:tool-call": AgentToolCallMessage;
|
||||||
|
|||||||
@ -28,6 +28,9 @@ importers:
|
|||||||
'@react-three/fiber':
|
'@react-three/fiber':
|
||||||
specifier: ^9.6.1
|
specifier: ^9.6.1
|
||||||
version: 9.6.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(three@0.184.0)
|
version: 9.6.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(three@0.184.0)
|
||||||
|
ace-builds:
|
||||||
|
specifier: ^1.36.0
|
||||||
|
version: 1.44.0
|
||||||
ansicolor:
|
ansicolor:
|
||||||
specifier: ^2.0.3
|
specifier: ^2.0.3
|
||||||
version: 2.0.3
|
version: 2.0.3
|
||||||
@ -109,6 +112,9 @@ importers:
|
|||||||
react:
|
react:
|
||||||
specifier: ^19.2.5
|
specifier: ^19.2.5
|
||||||
version: 19.2.5
|
version: 19.2.5
|
||||||
|
react-ace:
|
||||||
|
specifier: ^12.0.0
|
||||||
|
version: 12.0.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.2.5
|
specifier: ^19.2.5
|
||||||
version: 19.2.5(react@19.2.5)
|
version: 19.2.5(react@19.2.5)
|
||||||
@ -1555,6 +1561,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
ace-builds@1.44.0:
|
||||||
|
resolution: {integrity: sha512-PFNMSYqFdEUkul2Ntud0HvA09AgY+F1ag0UYdpMH60wNI/qOA8cB8tlTgoALMEwIdUPJK2CjrIQ7OnbiSS/ugQ==}
|
||||||
|
|
||||||
acorn@7.4.1:
|
acorn@7.4.1:
|
||||||
resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==}
|
resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
@ -1975,6 +1984,9 @@ packages:
|
|||||||
diacritics@1.3.0:
|
diacritics@1.3.0:
|
||||||
resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==}
|
resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==}
|
||||||
|
|
||||||
|
diff-match-patch@1.0.5:
|
||||||
|
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
|
||||||
|
|
||||||
dir-glob@3.0.1:
|
dir-glob@3.0.1:
|
||||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -2617,6 +2629,10 @@ packages:
|
|||||||
lodash.defaults@4.2.0:
|
lodash.defaults@4.2.0:
|
||||||
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
|
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
|
||||||
|
|
||||||
|
lodash.get@4.4.2:
|
||||||
|
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
|
||||||
|
deprecated: This package is deprecated. Use the optional chaining (?.) operator instead.
|
||||||
|
|
||||||
lodash.includes@4.3.0:
|
lodash.includes@4.3.0:
|
||||||
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||||
|
|
||||||
@ -2626,6 +2642,10 @@ packages:
|
|||||||
lodash.isboolean@3.0.3:
|
lodash.isboolean@3.0.3:
|
||||||
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||||
|
|
||||||
|
lodash.isequal@4.5.0:
|
||||||
|
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
|
||||||
|
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
|
||||||
|
|
||||||
lodash.isfinite@3.3.2:
|
lodash.isfinite@3.3.2:
|
||||||
resolution: {integrity: sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==}
|
resolution: {integrity: sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==}
|
||||||
|
|
||||||
@ -2647,6 +2667,10 @@ packages:
|
|||||||
lodash@4.18.1:
|
lodash@4.18.1:
|
||||||
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
||||||
|
|
||||||
|
loose-envify@1.4.0:
|
||||||
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
lru-cache@11.3.5:
|
lru-cache@11.3.5:
|
||||||
resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==}
|
resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@ -3028,6 +3052,9 @@ packages:
|
|||||||
promise@7.3.1:
|
promise@7.3.1:
|
||||||
resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
|
resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
|
||||||
|
|
||||||
|
prop-types@15.8.1:
|
||||||
|
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@ -3110,11 +3137,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
|
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
react-ace@12.0.0:
|
||||||
|
resolution: {integrity: sha512-PstU6CSMfYIJknb4su2Fa0WgLXzq2ufQgR6fjcSWuGT1hGTHkBzuKw+SncV8PuLCdSJBJc1VehPhyeXlWByG/g==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||||
|
|
||||||
react-dom@19.2.5:
|
react-dom@19.2.5:
|
||||||
resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
|
resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.2.5
|
react: ^19.2.5
|
||||||
|
|
||||||
|
react-is@16.13.1:
|
||||||
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
react-is@17.0.2:
|
react-is@17.0.2:
|
||||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||||
|
|
||||||
@ -4801,6 +4837,8 @@ snapshots:
|
|||||||
mime-types: 3.0.2
|
mime-types: 3.0.2
|
||||||
negotiator: 1.0.0
|
negotiator: 1.0.0
|
||||||
|
|
||||||
|
ace-builds@1.44.0: {}
|
||||||
|
|
||||||
acorn@7.4.1: {}
|
acorn@7.4.1: {}
|
||||||
|
|
||||||
agent-base@7.1.4: {}
|
agent-base@7.1.4: {}
|
||||||
@ -5224,6 +5262,8 @@ snapshots:
|
|||||||
|
|
||||||
diacritics@1.3.0: {}
|
diacritics@1.3.0: {}
|
||||||
|
|
||||||
|
diff-match-patch@1.0.5: {}
|
||||||
|
|
||||||
dir-glob@3.0.1:
|
dir-glob@3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-type: 4.0.0
|
path-type: 4.0.0
|
||||||
@ -6014,12 +6054,16 @@ snapshots:
|
|||||||
|
|
||||||
lodash.defaults@4.2.0: {}
|
lodash.defaults@4.2.0: {}
|
||||||
|
|
||||||
|
lodash.get@4.4.2: {}
|
||||||
|
|
||||||
lodash.includes@4.3.0: {}
|
lodash.includes@4.3.0: {}
|
||||||
|
|
||||||
lodash.isarguments@3.1.0: {}
|
lodash.isarguments@3.1.0: {}
|
||||||
|
|
||||||
lodash.isboolean@3.0.3: {}
|
lodash.isboolean@3.0.3: {}
|
||||||
|
|
||||||
|
lodash.isequal@4.5.0: {}
|
||||||
|
|
||||||
lodash.isfinite@3.3.2: {}
|
lodash.isfinite@3.3.2: {}
|
||||||
|
|
||||||
lodash.isinteger@4.0.4: {}
|
lodash.isinteger@4.0.4: {}
|
||||||
@ -6034,6 +6078,10 @@ snapshots:
|
|||||||
|
|
||||||
lodash@4.18.1: {}
|
lodash@4.18.1: {}
|
||||||
|
|
||||||
|
loose-envify@1.4.0:
|
||||||
|
dependencies:
|
||||||
|
js-tokens: 4.0.0
|
||||||
|
|
||||||
lru-cache@11.3.5: {}
|
lru-cache@11.3.5: {}
|
||||||
|
|
||||||
luxon@3.7.2: {}
|
luxon@3.7.2: {}
|
||||||
@ -6360,6 +6408,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
asap: 2.0.6
|
asap: 2.0.6
|
||||||
|
|
||||||
|
prop-types@15.8.1:
|
||||||
|
dependencies:
|
||||||
|
loose-envify: 1.4.0
|
||||||
|
object-assign: 4.1.1
|
||||||
|
react-is: 16.13.1
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
forwarded: 0.2.0
|
forwarded: 0.2.0
|
||||||
@ -6475,11 +6529,23 @@ snapshots:
|
|||||||
iconv-lite: 0.7.2
|
iconv-lite: 0.7.2
|
||||||
unpipe: 1.0.0
|
unpipe: 1.0.0
|
||||||
|
|
||||||
|
react-ace@12.0.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
|
||||||
|
dependencies:
|
||||||
|
ace-builds: 1.44.0
|
||||||
|
diff-match-patch: 1.0.5
|
||||||
|
lodash.get: 4.4.2
|
||||||
|
lodash.isequal: 4.5.0
|
||||||
|
prop-types: 15.8.1
|
||||||
|
react: 19.2.5
|
||||||
|
react-dom: 19.2.5(react@19.2.5)
|
||||||
|
|
||||||
react-dom@19.2.5(react@19.2.5):
|
react-dom@19.2.5(react@19.2.5):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.5
|
react: 19.2.5
|
||||||
scheduler: 0.27.0
|
scheduler: 0.27.0
|
||||||
|
|
||||||
|
react-is@16.13.1: {}
|
||||||
|
|
||||||
react-is@17.0.2: {}
|
react-is@17.0.2: {}
|
||||||
|
|
||||||
react-router-dom@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
|
react-router-dom@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user