Appearance
Built-in Editor
The right sidebar's Editor tab opens any text file in the active project's working directory in a CodeMirror 6 buffer, with explicit save and optimistic concurrency against the on-disk file. This is milestone 3 of the right-sidebar feature.
Image, audio, and video files in the same tree open in a read-only media viewer in the same pane - see Media viewer below.
Where files come from
- Click any file in the Files tab → opens in the editor and switches to the Editor tab.
- Click the file-edit icon in the Git diff viewer's header → same.
- Both routes flow through the global
appBus.emit('open-in-editor', …)event so any future trigger (command palette, keyboard shortcut, etc.) hooks into the same path without prop-drilling.
The editor holds a single open buffer at a time. The narrow sidebar column makes a tab strip impractical; switching files prompts the user when the current buffer is dirty.
Editing model
- The buffer's
baselineis what was last loaded or saved;currentis what the user is typing. Dirty state iscurrent !== baseline. - A small amber dot in the header signals unsaved changes.
- Save with
Ctrl/Cmd+Sor the toolbar icon. Save is fire-and-forget on the wire - the daemon answers withproject_file_saved(success) orproject_file_write_conflict(sha256 mismatch). - Revert discards local edits and re-loads from disk.
- Close drops the buffer; with unsaved changes the user is prompted.
There is no autosave. Read-only buffers are not implemented yet - if the file is read-only on disk, the daemon's write fails with a clear error.
Languages and theming
Only the language packs already present in package.json are wired:
@codemirror/lang-cssfor.css,.scss,.less- Everything else opens as plain text (no syntax highlighting).
Adding more languages is a one-line change per pack - see the languageFor helper in CodeEditor.vue. Keeping the list small avoids pulling new dependencies until it's clear which languages users actually edit.
The editor now reads its palette from the app's CSS custom properties, the same way the rest of the UI and the terminal do:
- Surface variables:
--editor-bg,--editor-foreground,--editor-gutter-*,--editor-border,--editor-active-line-*,--editor-selection-bg,--editor-cursor - Search / bracket variables:
--editor-search-match-*,--editor-bracket-match-*
Because the editor theme is CSS-variable driven, theme toggles and custom CSS edits update already-open editors live without recreating them.
Wire protocol
WriteProjectFile
jsonc
// client → daemon
{
"type": "write_project_file",
"project_id": "<uuid>",
"path": "src/lib.rs",
"content": "fn main() {}\n",
"base_sha256": "abc123…" // omit / null to force-write a new file
}The daemon validates the path (canonicalize the parent directory and ensure it stays inside the project's working_dir), enforces a 1 MiB write cap, performs an optimistic concurrency check when base_sha256 is provided, and writes atomically: bytes go to a <name>.<uuid>.tmp file in the same directory, the file's existing mode is preserved on Unix, then a rename swaps it into place. The temp file is removed on rename failure.
ProjectFileSaved
jsonc
{
"type": "project_file_saved",
"project_id": "<uuid>",
"path": "src/lib.rs",
"sha256": "def456…"
}The new sha256 is the digest the editor should treat as its base for the next save.
ProjectFileWriteConflict
jsonc
{
"type": "project_file_write_conflict",
"project_id": "<uuid>",
"path": "src/lib.rs",
"current_sha256": "ff00ff00…"
}Returned when base_sha256 was provided and the on-disk file's current digest differs (or the file no longer exists). The editor shows an inline banner with Reload (re-baseline against disk, lose edits) and Overwrite (force-write, discarding the other side's changes). Genuine errors (path traversal, oversized payload, parent missing) come back as project_file_error instead.
Run-as-user
Writes happen as the daemon's own user, like reads in M1. The path containment check is the security boundary. If your project's run_as_users map points at a different OS user, the file ends up owned by the daemon user; per-user write impersonation can be added later if needed.
Frontend integration
- Pinia stores:
src/stores/files.ts- owns content cache,loadContent,saveContent, plus the newonFileSaved/onFileWriteConflictevent handlers.src/stores/editor.ts- owns the single open buffer, dirty state,save/revert/close, and the surfacedconflictnotice.
- Components:
src/components/sidebar-right/CodeEditor.vue- CodeMirror host, toolbar, conflict banner.
- Tauri command:
src-tauri/src/commands/files.rs::write_project_file - Events:
hive:project-file-saved,hive:project-file-write-conflict(registered insrc/composables/useTauriEvents.ts)
Limitations
- No autosave; explicit
Ctrl/Cmd+S. - No multi-buffer / tab strip; one open file at a time.
- No diff-against-saved view (M2's
DiffViewcovers worktree vs index). - Language pack list is intentionally short - extend as needed.
- 1 MiB write cap, matching the read cap, so the editor's round-trip can't silently grow past what it can load back.
Media viewer
Image, audio, and video files use the same open-file flow as text - clicking one in the Files tab opens it in the editor pane - but render in a read-only viewer instead of CodeMirror.
- Detection. By file extension on the frontend (
src/lib/mediaKind.ts). Image:png,jpg/jpeg,gif,webp,avif,bmp,ico,svg. Audio:mp3,wav,ogg/oga,flac,m4a,aac,opus. Video:mp4/m4v,webm,mov,mkv,ogv. - Rendering.
<img>,<audio controls>, or<video controls>whosesrcis adata:<mime>;base64,<bytes>URL built from the daemon's response. - Toolbar. Save and Revert are hidden; only Close is available.
- Size cap. 25 MiB per file (vs 1 MiB for text). Bytes are base64- encoded into a single WebSocket frame, so larger files are rejected with
project_file_error.
ReadProjectFileBinary
jsonc
// client → daemon
{
"type": "read_project_file_binary",
"project_id": "<uuid>",
"path": "assets/logo.png"
}The daemon validates the path (same containment check as the text reader), enforces the 25 MiB cap, reads the file, derives a MIME type from the extension, and responds with:
ProjectFileBinary
jsonc
{
"type": "project_file_binary",
"project_id": "<uuid>",
"path": "assets/logo.png",
"data": "<base64>", // raw bytes, base64-encoded by serde
"mime": "image/png",
"sha256": "abc123…"
}Failures (path traversal, oversize, missing file) come back as the same project_file_error variant the text reader uses. There is no write counterpart - media files are read-only in the viewer.
Frontend integration (additions)
src/lib/mediaKind.ts- extension → kind detection.src/stores/files.ts- addsloadBinaryContentand theonFileBinaryContentevent handler alongside the text equivalents.src/stores/editor.ts-OpenBufferis a discriminated union overtextandmedia;openFilepicks the transport by extension.src/components/workspace/CodeEditor.vue- renders<img>/<audio>/<video>for media buffers; CodeMirror remains for text.- Tauri command:
src-tauri/src/commands/files.rs::read_project_file_binary - Event:
hive:project-file-binary(registered insrc/composables/useTauriEvents.ts).