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:
Rob Colbert 2026-05-12 19:32:58 -04:00
parent ec7b83d610
commit 1e13f95808
10 changed files with 743 additions and 8 deletions

View File

@ -11,7 +11,9 @@
"author": "Robert Colbert <rob.colbert@openplatform.us>",
"license": "Apache-2.0",
"dependencies": {
"ace-builds": "^1.36.0",
"marked": "^16.0.0",
"react-ace": "^12.0.0",
"slug": "^11.0.1"
}
}

View 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>
);
}

View File

@ -1,14 +1,25 @@
import { useState } from "react";
import { WorkspaceMode } from "../lib/types";
import FileTree from "./FileTree";
import EditorPanel from "./EditorPanel";
interface FilesPanelProps {
workspaceMode: WorkspaceMode;
}
export default function FilesPanel({ workspaceMode }: FilesPanelProps) {
const [selectedFilePath, setSelectedFilePath] = useState<string | undefined>(undefined);
const isReadOnly = workspaceMode === WorkspaceMode.Agent;
const isReadWrite = workspaceMode === WorkspaceMode.User;
const handleFileSelect = (path: string) => {
setSelectedFilePath(path);
};
const handleCloseFile = () => {
setSelectedFilePath(undefined);
};
return (
<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">
@ -38,15 +49,27 @@ export default function FilesPanel({ workspaceMode }: FilesPanelProps) {
</span>
</div>
</div>
<div className="flex-1 overflow-auto">
<FileTree
workspaceMode={workspaceMode}
onFileSelect={(path) => {
console.log('File selected:', path);
// TODO: Open file in editor (next session)
}}
/>
{/* Split view: File tree on left, editor on right */}
<div className="flex-1 flex overflow-hidden min-h-0">
{/* File Tree - 30% width, resizable in future */}
<div className="w-1/3 min-w-[200px] border-r border-border-subtle overflow-auto flex-shrink-0">
<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 className="px-4 py-2 bg-bg-tertiary border-t border-border-subtle">
<p className="text-xs text-text-muted">
{isReadOnly

View File

@ -67,6 +67,14 @@ export interface ClientToServerEvents {
args: { path?: string; recursive?: boolean; maxDepth?: number },
cb: (success: boolean, data: { entries?: any[]; error?: string }) => 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 {
@ -124,6 +132,8 @@ export interface SocketEvents {
sessionUpdated: (updates: Partial<ChatSession>) => void;
tabLockDenied: (data: { message: 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;
reconnect_attempt: (attempt: number) => void;
reconnect_failed: () => void;
@ -224,6 +234,14 @@ class SocketClient {
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(
"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(
registration: any,
project: any,

View File

@ -27,6 +27,8 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
"@gadget/ai": "workspace:*",
"ace-builds": "^1.36.0",
"react-ace": "^12.0.0",
"@gadget/api": "workspace:*",
"@gadget/config": "workspace:*",
"@react-three/drei": "^10.7.7",

View File

@ -62,6 +62,8 @@ export class CodeSession extends SocketSession {
this.socket.on("releaseSessionLock", this.onReleaseSessionLock.bind(this));
this.socket.on("sessionHeartbeat", this.onSessionHeartbeat.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
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
* previously-acquired session lock on a gadget-drone instance.

View File

@ -264,6 +264,14 @@ class GadgetDrone extends GadgetProcess {
"fileTreeRequest",
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
@ -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(
dir: string,
showHidden: boolean,

View File

@ -133,3 +133,43 @@ export type FileTreeResponseMessage = (
entries: FileTreeEntry[],
error?: string,
) => 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;

View File

@ -25,6 +25,10 @@ import {
SubmitPromptMessage,
FileTreeRequestMessage,
FileTreeResponseMessage,
FileReadRequestMessage,
FileReadResponseMessage,
FileWriteRequestMessage,
FileWriteResponseMessage,
} from "./ide.ts";
/*
@ -57,6 +61,8 @@ export interface ClientToServerEvents {
releaseSessionLock: ReleaseSessionLockMessage;
sessionHeartbeat: SessionHeartbeatMessage;
fileTreeRequest: FileTreeRequestMessage;
fileReadRequest: FileReadRequestMessage;
fileWriteRequest: FileWriteRequestMessage;
/*
* gadget-drone => gadget-code:web
@ -121,6 +127,8 @@ export interface ServerToClientEvents {
crashRecoveryResponse: CrashRecoveryResponseMessage;
requestTermination: RequestTerminationMessage;
fileTreeRequest: FileTreeRequestMessage;
fileReadRequest: FileReadRequestMessage;
fileWriteRequest: FileWriteRequestMessage;
/*
* gadget-code:web => gadget-code:ide
@ -136,6 +144,8 @@ export interface ServerToClientEvents {
sessionUpdated: SessionUpdatedMessage;
tabLockDenied: (data: { message: string }) => void;
fileTreeResponse: FileTreeResponseMessage;
fileReadResponse: FileReadResponseMessage;
fileWriteResponse: FileWriteResponseMessage;
"agent:thinking": AgentThinkingMessage;
"agent:response": AgentResponseMessage;
"agent:tool-call": AgentToolCallMessage;

View File

@ -28,6 +28,9 @@ importers:
'@react-three/fiber':
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)
ace-builds:
specifier: ^1.36.0
version: 1.44.0
ansicolor:
specifier: ^2.0.3
version: 2.0.3
@ -109,6 +112,9 @@ importers:
react:
specifier: ^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:
specifier: ^19.2.5
version: 19.2.5(react@19.2.5)
@ -1555,6 +1561,9 @@ packages:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'}
ace-builds@1.44.0:
resolution: {integrity: sha512-PFNMSYqFdEUkul2Ntud0HvA09AgY+F1ag0UYdpMH60wNI/qOA8cB8tlTgoALMEwIdUPJK2CjrIQ7OnbiSS/ugQ==}
acorn@7.4.1:
resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==}
engines: {node: '>=0.4.0'}
@ -1975,6 +1984,9 @@ packages:
diacritics@1.3.0:
resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==}
diff-match-patch@1.0.5:
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
@ -2617,6 +2629,10 @@ packages:
lodash.defaults@4.2.0:
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:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
@ -2626,6 +2642,10 @@ packages:
lodash.isboolean@3.0.3:
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:
resolution: {integrity: sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==}
@ -2647,6 +2667,10 @@ packages:
lodash@4.18.1:
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:
resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==}
engines: {node: 20 || >=22}
@ -3028,6 +3052,9 @@ packages:
promise@7.3.1:
resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
@ -3110,11 +3137,20 @@ packages:
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
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:
resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
peerDependencies:
react: ^19.2.5
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
@ -4801,6 +4837,8 @@ snapshots:
mime-types: 3.0.2
negotiator: 1.0.0
ace-builds@1.44.0: {}
acorn@7.4.1: {}
agent-base@7.1.4: {}
@ -5224,6 +5262,8 @@ snapshots:
diacritics@1.3.0: {}
diff-match-patch@1.0.5: {}
dir-glob@3.0.1:
dependencies:
path-type: 4.0.0
@ -6014,12 +6054,16 @@ snapshots:
lodash.defaults@4.2.0: {}
lodash.get@4.4.2: {}
lodash.includes@4.3.0: {}
lodash.isarguments@3.1.0: {}
lodash.isboolean@3.0.3: {}
lodash.isequal@4.5.0: {}
lodash.isfinite@3.3.2: {}
lodash.isinteger@4.0.4: {}
@ -6034,6 +6078,10 @@ snapshots:
lodash@4.18.1: {}
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
lru-cache@11.3.5: {}
luxon@3.7.2: {}
@ -6360,6 +6408,12 @@ snapshots:
dependencies:
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:
dependencies:
forwarded: 0.2.0
@ -6475,11 +6529,23 @@ snapshots:
iconv-lite: 0.7.2
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):
dependencies:
react: 19.2.5
scheduler: 0.27.0
react-is@16.13.1: {}
react-is@17.0.2: {}
react-router-dom@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5):