feat: add built-in file editor with syntax highlighting (#79)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m4s
CI / Build Linux (push) Has been cancelled
CI / Lint & Test (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled

## Summary
- Add CodeMirror 6 editor with syntax highlighting for 40+ languages
- Add file browser sidebar with collapsible directory tree navigation
- Add multi-tab support with dirty state indicators and close buttons
- Add keyboard shortcuts (Ctrl+E toggle, Ctrl+B file browser, Ctrl+S save, Ctrl+W close tab)
- Add editor toggle button to status bar (disabled when not connected)
- Editor automatically uses current session's working directory
- Add Tauri backend commands for file operations (list_directory, read_file_content, write_file_content)

## Test Plan
- [ ] Connect to a session and verify the editor toggle button becomes enabled
- [ ] Press Ctrl+E to open the editor and verify file tree shows the session's CWD
- [ ] Navigate directories and open files to verify syntax highlighting works
- [ ] Edit a file and verify the dirty indicator (*) appears
- [ ] Save with Ctrl+S and verify the dirty indicator disappears
- [ ] Open multiple files and verify tab switching works
- [ ] Close tabs with Ctrl+W or the X button
- [ ] Disconnect and verify the editor automatically closes
- [ ] Verify keyboard shortcuts are documented in the shortcuts modal

Closes #72

 This PR was created with help from Hikari~ 🌸

Reviewed-on: #79
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #79.
This commit is contained in:
2026-01-28 18:20:02 -08:00
committed by Naomi Carrigan
parent edc863e020
commit e45a1a1c98
21 changed files with 3803 additions and 4 deletions
+9 -1
View File
@@ -1,9 +1,17 @@
<script>
<script lang="ts">
import "../app.css";
let { children } = $props();
// Prevent the default context menu globally
// Individual components can show their own context menus by calling event.stopPropagation()
function handleContextMenu(event: MouseEvent) {
event.preventDefault();
}
</script>
<svelte:window oncontextmenu={handleContextMenu} />
<div id="app">
{@render children()}
</div>
+78 -3
View File
@@ -7,6 +7,7 @@
import { initNotificationSync, cleanupNotificationSync } from "$lib/stores/notifications";
import { conversationsStore } from "$lib/stores/conversations";
import { claudeStore, isClaudeProcessing } from "$lib/stores/claude";
import { editorStore } from "$lib/stores/editor";
import { getCurrentWindow, LogicalSize } from "@tauri-apps/api/window";
import "$lib/notifications/testNotifications";
import Terminal from "$lib/components/Terminal.svelte";
@@ -14,6 +15,7 @@
import StatusBar from "$lib/components/StatusBar.svelte";
import AnimeGirl from "$lib/components/AnimeGirl.svelte";
import CompactMode from "$lib/components/CompactMode.svelte";
import EditorPanel from "$lib/components/editor/EditorPanel.svelte";
import { characterState } from "$lib/stores/character";
import type { CharacterState } from "$lib/types/states";
import PermissionModal from "$lib/components/PermissionModal.svelte";
@@ -29,6 +31,32 @@
let currentCharacterState: CharacterState = $state("idle");
let compactModeActive = $state(false);
// Editor state
const isEditorVisible = editorStore.isEditorVisible;
let lastInitializedCwd = "";
// Track connection status and CWD for the editor
const connectionStatus = claudeStore.connectionStatus;
const currentWorkingDirectory = claudeStore.currentWorkingDirectory;
// Initialize/update editor file tree when CWD changes while editor is visible
$effect(() => {
const visible = $isEditorVisible;
const cwd = $currentWorkingDirectory;
const connected = $connectionStatus === "connected";
// Only initialize when editor is visible, connected, and CWD is set
if (visible && connected && cwd && cwd !== lastInitializedCwd) {
lastInitializedCwd = cwd;
editorStore.initializeFileTree(cwd);
}
// Hide editor if disconnected
if (!connected && visible) {
editorStore.hideEditor();
}
});
// Window size constants
const COMPACT_WIDTH = 280;
const COMPACT_HEIGHT = 400;
@@ -176,6 +204,49 @@
toggleCompactMode();
return;
}
// Ctrl+E - Toggle editor panel (only when connected)
if (event.ctrlKey && event.key === "e") {
event.preventDefault();
// Only allow opening the editor when connected
if (get(claudeStore.connectionStatus) === "connected") {
editorStore.toggleEditor();
}
return;
}
// Ctrl+B - Toggle file browser (when editor is visible)
if (event.ctrlKey && event.key === "b" && get(editorStore.isEditorVisible)) {
event.preventDefault();
editorStore.toggleFileBrowser();
return;
}
// Ctrl+S - Save current file (when editor is visible)
if (event.ctrlKey && event.key === "s" && get(editorStore.isEditorVisible)) {
event.preventDefault();
editorStore.saveFile();
return;
}
// Ctrl+W - Close current tab (when editor is visible)
if (event.ctrlKey && event.key === "w" && get(editorStore.isEditorVisible)) {
event.preventDefault();
const activeTabId = get(editorStore.activeTabId);
if (activeTabId) {
editorStore.closeTab(activeTabId);
}
return;
}
// Ctrl+N - New file (when editor is visible)
// Note: This just emits an event that FileBrowser listens to
if (event.ctrlKey && event.key === "n" && get(editorStore.isEditorVisible)) {
event.preventDefault();
// Dispatch a custom event that FileBrowser will listen to
window.dispatchEvent(new CustomEvent("editor-new-file"));
return;
}
}
async function handleInterrupt() {
@@ -330,10 +401,14 @@
onmousedown={startResize}
></div>
<!-- Right panel: Terminal and input -->
<!-- Right panel: Terminal/Editor and input -->
<div class="terminal-panel flex-1 flex flex-col min-w-0">
<Terminal />
<InputBar />
{#if $isEditorVisible}
<EditorPanel />
{:else}
<Terminal />
<InputBar />
{/if}
</div>
</main>