diff --git a/gadget-code/frontend/package.json b/gadget-code/frontend/package.json index 4251916..288c75e 100644 --- a/gadget-code/frontend/package.json +++ b/gadget-code/frontend/package.json @@ -11,7 +11,9 @@ "author": "Robert Colbert ", "license": "Apache-2.0", "dependencies": { + "ace-builds": "^1.36.0", "marked": "^16.0.0", + "react-ace": "^12.0.0", "slug": "^11.0.1" } } \ No newline at end of file diff --git a/gadget-code/frontend/src/components/EditorPanel.tsx b/gadget-code/frontend/src/components/EditorPanel.tsx new file mode 100644 index 0000000..0de3237 --- /dev/null +++ b/gadget-code/frontend/src/components/EditorPanel.tsx @@ -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 = { + '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({ + 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 ( +
+
+ + + +

No file open

+

Select a file from the tree to edit

+
+
+ ); + } + + return ( +
+ {/* Editor Header */} +
+
+ + {filePath} + + {state.isDirty && ( + + + Unsaved changes + + )} + {state.successMessage && ( + + {state.successMessage} + + )} +
+ +
+ {isReadOnly && ( + + Read-only (Agent mode) + + )} + + + + {onCloseFile && ( + + )} +
+
+ + {/* Error State */} + {state.error && ( +
+
+ + + +
+

{state.error}

+ +
+
+
+ )} + + {/* Loading State */} + {state.isLoading && ( +
+
+
+

Loading file...

+
+
+ )} + + {/* ACE Editor */} + {!state.isLoading && ( +
+ +
+ )} +
+ ); +} diff --git a/gadget-code/frontend/src/components/FilesPanel.tsx b/gadget-code/frontend/src/components/FilesPanel.tsx index cddef69..578d695 100644 --- a/gadget-code/frontend/src/components/FilesPanel.tsx +++ b/gadget-code/frontend/src/components/FilesPanel.tsx @@ -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(undefined); const isReadOnly = workspaceMode === WorkspaceMode.Agent; const isReadWrite = workspaceMode === WorkspaceMode.User; + const handleFileSelect = (path: string) => { + setSelectedFilePath(path); + }; + + const handleCloseFile = () => { + setSelectedFilePath(undefined); + }; + return (
@@ -38,15 +49,27 @@ export default function FilesPanel({ workspaceMode }: FilesPanelProps) {
-
- { - console.log('File selected:', path); - // TODO: Open file in editor (next session) - }} - /> + + {/* Split view: File tree on left, editor on right */} +
+ {/* File Tree - 30% width, resizable in future */} +
+ +
+ + {/* Editor Panel - remaining space */} +
+ +
+

{isReadOnly diff --git a/gadget-code/frontend/src/lib/socket.ts b/gadget-code/frontend/src/lib/socket.ts index 1c449e8..88b1862 100644 --- a/gadget-code/frontend/src/lib/socket.ts +++ b/gadget-code/frontend/src/lib/socket.ts @@ -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) => 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, diff --git a/gadget-code/package.json b/gadget-code/package.json index 89e1a95..20288f2 100644 --- a/gadget-code/package.json +++ b/gadget-code/package.json @@ -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", diff --git a/gadget-code/src/lib/code-session.ts b/gadget-code/src/lib/code-session.ts index 74c9c77..3ec55cc 100644 --- a/gadget-code/src/lib/code-session.ts +++ b/gadget-code/src/lib/code-session.ts @@ -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. diff --git a/gadget-drone/src/gadget-drone.ts b/gadget-drone/src/gadget-drone.ts index 47c3fcc..33c426a 100644 --- a/gadget-drone/src/gadget-drone.ts +++ b/gadget-drone/src/gadget-drone.ts @@ -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 { + 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 { + 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, diff --git a/packages/api/src/messages/ide.ts b/packages/api/src/messages/ide.ts index e03e274..6b4c18b 100644 --- a/packages/api/src/messages/ide.ts +++ b/packages/api/src/messages/ide.ts @@ -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; diff --git a/packages/api/src/messages/socket.ts b/packages/api/src/messages/socket.ts index 33c7f21..9bf9df4 100644 --- a/packages/api/src/messages/socket.ts +++ b/packages/api/src/messages/socket.ts @@ -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; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3e580d..b2808ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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):