Skip to content
Code Editor Panel

Code Editor Panel

The editor panel provides a full-featured code editor with a file tree sidebar, multi-tab interface, and syntax highlighting. It uses CodeMirror 6 with a Darcula (dark) theme and communicates with the Go backend API for all file operations.

Related docs: Layouts | Desktop App


Architecture

The EditorPanel component (src/components/panels/EditorPanel.tsx) combines:

  • A resizable file tree sidebar on the left
  • A tab bar for open files
  • A CodeMirror 6 editor for the active file
  • An optional markdown preview pane for .md/.mdx files

All file I/O goes through the backend REST API (/api/channels/{id}/files, /api/channels/{id}/file), not direct filesystem access. This ensures path traversal protection, Docker container support, and compatibility with browser development mode.


CodeMirror 6 Configuration

Theme

The editor uses a custom Darcula theme inspired by JetBrains IDEs:

  • Font: JetBrains Mono (loaded via @fontsource/jetbrains-mono), 13px
  • Background: colors.sidebar (#171717)
  • Text: #a9b7c6
  • Caret: #bbbbbb
  • Selection: #214283
  • Active line: rgba(255,255,255,0.04)
  • Matching bracket: #3b514d background, #ffef28 text
  • Gutter: same background as editor, #606366 text
  • Active gutter line: colors.selectedBg background

The highlight style covers keywords (#cc7832), strings (#6a8759), numbers (#6897bb), comments (#808080 italic), doc comments (#629755 italic), function names (#ffc66d), types (#ffc66d), properties (#9876aa), and more.

Extensions

ExtensionPurpose
lineNumbers()Line number gutter
highlightActiveLine()Highlights the current line
highlightActiveLineGutter()Highlights the gutter for the current line
drawSelection()Custom selection rendering
EditorView.lineWrappingSoft line wrapping
bracketMatching()Matching bracket highlighting
foldGutter()Code folding controls in gutter
history()Undo/redo
search({ top: true })Search panel at the top
Language extensionSyntax-specific highlighting (see below)

Language Support

Language extensions are selected based on file extension:

ExtensionsLanguage
.js, .jsx, .mjs, .cjsJavaScript
.ts, .tsxTypeScript (with JSX for .tsx)
.goGo
.pyPython
.json, .jsonlJSON
.md, .mdxMarkdown
.css, .scssCSS
.html, .htm, .svgHTML
.yaml, .ymlYAML

Files with unrecognized extensions are opened without syntax highlighting.


File Tree Sidebar

Layout

The sidebar occupies the left side of the editor panel with:

PropertyValue
Default width280px
Minimum width120px
Maximum width500px
Persisted inlocalStorage key loop-editor-tree-width

Header

The tree header contains:

  • “FILES” label – uppercase, dimmed
  • Refresh button – reloads the root and all expanded directories, and re-reads the currently open file from disk
  • New file button (+) – opens an inline input for creating a new file

Directory Listing

Files are fetched from the API (GET /api/channels/{id}/files?path=<dir>). The root directory (.) is loaded on mount.

  • Directories show a chevron icon that rotates on expand/collapse
  • Clicking a directory toggles its expanded state and lazy-loads its contents if not yet fetched
  • Files display with their name; clicking opens them in a tab
  • The tree tracks expanded directories in a Set<string> and directory contents in a Map<string, FileEntry[]>

Right-Click Context Menu

Right-clicking a file or directory opens a context menu with:

ItemAvailable OnAction
Copy relative pathFile, DirectoryCopies relative path to clipboard
Copy absolute pathFile, DirectoryCopies dirPath + "/" + relativePath to clipboard
New file hereDirectory onlyOpens inline input prefilled with directory path
New directory hereDirectory onlyOpens inline input prefilled with directory path; creates via POST /api/channels/{id}/dir which supports nested creation (MkdirAll)
DeleteFile, DirectoryDeletes the file or directory via API. For directories, uses recursive removal (RemoveAll). Closes any open tabs whose paths fall within a deleted directory.

Multi-Root File Tree

When a project has extra directories configured (via Settings or .loop/config.jsonextra_dirs), the file tree displays multiple roots. Each root appears as a top-level entry labeled with its directory name. File operations within each root use the corresponding root query parameter index (0 for the primary directory, 1+ for extras).

This is useful for multi-root projects where related code lives in separate directories — for example, a backend service and a shared library, or a monorepo with multiple packages.

Resize Handle

A 4px-wide handle on the right edge of the tree allows horizontal resizing:

  • Cursor changes to col-resize on hover
  • Drag updates width live (clamped to min/max)
  • Final width is saved to localStorage
  • user-select: none is applied during resize to prevent text selection

Tab System

Opening Tabs

  • Clicking a file in the tree opens it in a new tab (or switches to it if already open)
  • Tabs are displayed horizontally above the editor
  • The active tab is highlighted

Dirty Tracking

  • Any edit marks the tab as dirty (tracked in dirtyTabs Set)
  • Dirty tabs show a dot indicator next to the filename
  • The dirtyContentRef Map caches unsaved content in memory so it survives tab switches without triggering auto-save

Closing Tabs

  • Each tab has a close button
  • Closing the active tab auto-selects the adjacent tab
  • If auto-save is enabled, the file is saved before closing
  • Dirty content cache is cleared for the closed tab

Persistence

Tab state (open tabs and selected tab) is persisted in localStorage:

  • Key: loop-editor-tabs (standalone) or loop-layout-editor-tabs (embedded in layout)
  • Format: Record<channelId, { tabs: string[]; selected: string | null }>
  • Saved on every tab open, close, or switch

Auto-Save on Blur

Auto-save is controlled by the autoSaveOnBlur setting (default: true, configurable in Settings ).

Behavior

TriggerAction
Window blurIf auto-save enabled, saves all dirty tabs
Tab switchIf auto-save enabled, saves the current tab; otherwise, caches content in dirtyContentRef
Tab closeIf auto-save enabled, saves before closing
Component unmountIf auto-save enabled, saves all dirty tabs

The autoSaveOnBlurRef Pattern

The auto-save setting can change at any time. To avoid stale closures in cleanup functions and callbacks, the setting is stored in a useRef (autoSaveOnBlurRef) that is updated on every render. Cleanup functions and event handlers read from the ref instead of the state value.

Window Focus Reload

When the window regains focus, the editor reloads the currently open file from disk. If the content differs from the editor state (indicating an external edit), the editor is updated and the dirty flag is cleared.


Dirty Content Cache

The dirtyContentRef is a Map<string, string> that stores unsaved editor content in memory.

Purpose: When switching tabs, if a file has been edited but not saved (and auto-save is disabled), the unsaved content must be preserved. Since CodeMirror creates a new editor state on tab switch, the dirty content is snapshot into this map before switching away and restored when switching back.

Lifecycle:

  1. On tab switch away: if dirty, snapshot view.state.doc.toString() into the map
  2. On tab switch to: if the path exists in the map, use cached content instead of fetching from disk
  3. On save: remove the path from the map
  4. On tab close: remove the path from the map

File Operations

All operations go through the Go backend API for security.

OperationAPINotes
List directoryGET /api/channels/{id}/files?path=<dir>Returns FileEntry[] with name, type, size
Read fileGET /api/channels/{id}/file?path=<path>Returns text content; X-File-Binary: true header for binary files
Save filePUT /api/channels/{id}/file?path=<path>Body is raw file content. Appends trailing newline if missing.
Delete fileDELETE /api/channels/{id}/file?path=<path>Removes file from disk
Create filePUT with empty bodySame as save with empty content

Binary File Detection

The backend checks the first 512 bytes for null bytes. If detected, the response includes X-File-Binary: true header and the editor shows a “Binary file” message instead of attempting to render.

Image Files

For files whose extension is .png, .jpg/.jpeg, .gif, or .webp, the backend skips the binary header and returns the raw bytes with a real image/* Content-Type (see API: GET /api/channels/{id}/file ). In the editor, useEditorState recognises the extension at every fetch site — initial mount, tab switch, refresh tree, window focus, and agent-driven refresh — and skips the text fetch entirely. Instead it exposes an imageURL pointing back at the same endpoint; CodeEditor renders it as <img src={imageURL}> (centered, object-fit: contain, scrollable container) in place of the CodeMirror surface.

The URL carries an imageVersionRef cache-buster (?t=<n>). The counter bumps when the agent overwrites the file (tool.use for Write/Edit/MultiEdit), when the window regains focus, and on the manual refresh button — forcing the browser to re-fetch even though the URL would otherwise be byte-identical. imageURL is cleared on close or when switching to a non-image tab so the placeholder slot doesn’t leak across tabs.

Pairs with Chat: Paste Images — pasted images land under .loop/pastes/ and their path renders as a clickable file link ; clicking opens this image tab.

Maximum File Size

The backend enforces a 5MB maximum file size (maxFileSize = 5 * 1024 * 1024). Files exceeding this limit return an error. The editor displays file sizes formatted as B, KB, or MB.

Path Validation

The Go backend’s validateFilePath() function rejects:

  • Empty paths
  • Absolute paths (starting with /)
  • Path traversal (.. segments)
  • Symlinks that escape the project root directory

Markdown Preview

For .md and .mdx files, the editor shows a split view with:

  • CodeMirror editor on the left
  • Rendered HTML preview on the right (using the marked library)

The preview updates with a 300ms debounce after each edit. A toggle button lets the user show/hide the preview pane.


Keyboard Shortcuts

ShortcutContextAction
Cmd+S / Ctrl+SEditor focused or window-levelSave the active file immediately
Cmd+F / Ctrl+FEditor focusedOpen the search panel
Cmd+H / Ctrl+HEditor focused (via search keymap)Open search and replace
Cmd+G / Ctrl+GSearch panel openFind next match
Fold/unfold shortcutsEditor focused (via fold keymap)Collapse/expand code blocks
TabEditor focusedInsert indentation (indentWithTab)
Cmd+Z / Ctrl+ZEditor focusedUndo
Cmd+Shift+Z / Ctrl+YEditor focusedRedo

Editor Context Menu

Right-clicking inside the CodeMirror editor area opens a custom context menu with:

ItemConditionAction
CopyText selectedCopy selection to clipboard
CutText selectedCopy and remove selection
Select AllAlwaysSelect entire document
Find…AlwaysOpen the search panel