@@ -1,360 +0,0 @@
# ByteDraft — Electron Migration Plan
## Current status
```
[✓] Phase 0 – Decision & planning
[✓] Phase 1 – Electron shell + contextBridge
[✓] Phase 2 – Rust subprocess JSON bridge
[✓] Phase 3 – Replace @tauri-apps/api in React
[✓] Phase 4 – Tear-away windows
[✓] Phase 5 – E2E test migration
[✓] Phase 6 – CI pipeline update
```
Update the `[✓]` / `[ ]` markers as each phase completes.
---
## Target architecture
```
React 19 + Vite (ui/)
↓ window.api.* (contextBridge — electron/preload.ts)
Electron main (electron/main.ts)
↓ JSON subprocess
byte_draft_desktop binary (crates/byte_draft_desktop/ — thin CLI wrapper)
↓ Rust crate
byte_draft core (crates/byte_draft/ — encoding, language, format, lint)
```
Key decisions:
- **Rust bridge**: JSON subprocess (stdin/stdout), NOT napi-rs
- **Packaging**: Electron Forge
- **E2E**: Native `@playwright/test` with `_electron` fixture — no bridge plugin
---
## Repo layout
```
crates/byte_draft/ Rust core library — KEEP AS-IS
crates/byte_draft_desktop/ Currently Tauri shell → becomes CLI subprocess target
ui/ React 19 + Vite — KEEP AS-IS except Tauri imports
ui/src/bindings.ts Tauri Specta auto-gen → DELETE, replaced by ui/src/api.ts
e2e/ Playwright tests → migrate fixture, keep specs
electron/ NEW — main.ts, preload.ts (create in Phase 1)
```
---
## Phase 1 — Electron shell + contextBridge
**Goal ** : App launches in Electron with window chrome, vibrancy, and all 14 IPC stubs returning
mock data. React renders without import errors. Vitest passes unchanged.
### Files to create
| File | Purpose |
|------|---------|
| `electron/main.ts` | BrowserWindow config, ipcMain stubs for all 14 commands |
| `electron/preload.ts` | contextBridge exposing `window.api` |
| `electron/tsconfig.json` | CJS target for Electron main process |
| `package.json` (root) | Electron Forge config + `electron:dev` / `electron:build` scripts |
### Files to modify
| File | Change |
|------|--------|
| `ui/package.json` | Remove `@tauri-apps/*` , `@srsholmes/tauri-playwright` ; add `electron` , `@electron-forge/cli` |
| `ui/vite.config.ts` | Add `base: './'` so assets resolve from `file://` in production |
### window.api surface (preload.ts)
``` typescript
window . api = {
// file ops
openFile ( path : string ) : Promise < FileData >
saveFile ( path : string , content : string , encoding : string ) : Promise < void >
saveFileAs ( content : string ) : Promise < string | null >
revertFile ( path : string ) : Promise < FileData >
readFileWithEncoding ( path : string , encoding : string ) : Promise < FileData >
readBinary ( path : string ) : Promise < number [ ] >
// session
loadSession ( ) : Promise < string | null >
saveSession ( json : string ) : Promise < void >
updateTabContent ( tabId : string , content : string ) : Promise < void >
// workspace
openFolder ( ) : Promise < string | null >
readDirectory ( path : string ) : Promise < DirEntry [ ] >
listWorkspaceFiles ( root : string ) : Promise < string [ ] >
// editor
detectLanguage ( path : string , content : string ) : Promise < string >
lintDocument ( content : string , lang : string ) : Promise < Diagnostic [ ] >
formatDocument ( content : string , lang : string ) : Promise < string >
detectEncoding ( path : string ) : Promise < string >
// window controls
minimize ( ) : void
toggleMaximize ( ) : void
close ( ) : void
showMessageBox ( opts : { title : string ; message : string ; kind : 'info' | 'warning' | 'error' } ) : Promise < void >
showOpenDialog ( opts : { title? : string ; multiple? : boolean } ) : Promise < string | null >
// drag-drop
onFilesDropped ( cb : ( paths : string [ ] ) = > void ) : ( ) = > void
}
```
### Phase 1 exit criteria
- `npm run electron:dev` (from repo root) opens ByteDraft in an Electron window
- macOS: vibrancy visible; Windows: custom title bar visible
- All IPC stubs return empty/null without throwing
- `npm test` (Vitest) passes with zero changes to test files
- No `@tauri-apps/*` imports remain in `ui/src/`
---
## Phase 2 — Rust subprocess JSON bridge
**Goal ** : All 14 commands route to the real Rust backend. File open/save/format work end-to-end.
### Files to change
| File | Change |
|------|--------|
| `crates/byte_draft_desktop/src/main.rs` | Add `--cli` flag: read newline-delimited JSON from stdin, dispatch to handler fns, write JSON to stdout |
| `electron/bridge.ts` | NEW — spawn binary, maintain stdin/stdout streams, queue commands, return promises |
### JSON protocol (newline-delimited, one request + one response per line)
``` jsonc
// Request
{ "id" : "uuid" , "cmd" : "open_file" , "args" : { "path" : "/foo/bar.txt" } }
// Response success
{ "id" : "uuid" , "ok" : { "content" : "…" , "encoding" : "UTF-8" , "path" : "/foo/bar.txt" } }
// Response error
{ "id" : "uuid" , "err" : "file not found" }
```
### Phase 2 exit criteria
- Open a real file from disk, edit it, save it — all via Electron + Rust subprocess
- Format document (JSON/YAML) calls Rust and returns formatted text
- Session persists across app restarts
---
## Phase 3 — Replace @tauri-apps/api in React
**Goal ** : All Tauri imports gone from `ui/src/` . App works in browser dev mode and Electron.
### Files to change (exhaustive)
| File | What to change |
|------|---------------|
| `ui/src/bindings.ts` | **Delete ** — replace with `ui/src/api.ts` typed wrapper around `window.api` |
| `ui/src/App.tsx` | Replace `webviewWindow.onDragDropEvent` with `window.api.onFilesDropped` |
| `ui/src/hooks/useFileSystem.ts` | Replace `invoke` + `dialogOpen` /`dialogMessage` with `window.api.*` |
| `ui/src/lib/formatDocument.ts` | Replace `invoke` with `window.api.formatDocument` |
| `ui/src/components/Editor/Editor.tsx` | Replace `invoke` + `dialogMessage` with `window.api.*` |
| `ui/src/state/sessionStore.ts` | Replace `invoke` with `window.api.loadSession` /`saveSession` |
| `ui/src/components/ImageView/ImageView.tsx` | Replace `invoke('read_binary')` with `window.api.readBinary` |
| `ui/src/components/FileTree/FileTree.tsx` | Replace `invoke` with `window.api.readDirectory` |
| `ui/src/components/TitleBar/TitleBar.tsx` | Replace `getCurrentWindow()` with `window.api.minimize/toggleMaximize/close` ; replace `data-tauri-drag-region` with `data-electron-drag-region` |
| `ui/src/components/StatusBar/StatusBar.tsx` | Replace `invoke` with `window.api.*` |
### New file: `ui/src/api.ts`
``` typescript
// Falls back to mock data when window.api is undefined (browser dev / Vitest)
export const api = ( window as unknown as { api : WindowApi } ) . api ? ? mockApi ;
```
### Phase 3 exit criteria
- `grep -r "@tauri-apps" ui/src/` returns zero results
- App fully functional in Electron (open, edit, save, format, workspace tree)
- `npm test` (Vitest) passes
---
## Phase 4 — Tear-away windows
**Goal ** : Dragging a tab below the tab strip creates a new Electron window with that tab's state.
### How it works
1. User drags tab outside threshold → component sends IPC `tear-off-tab` with tab state payload
2. `ipcMain` handles `tear-off-tab` :
- Creates `new BrowserWindow(...)` with same options as main window
- New window loads `index.html?tabId=<id>` ; serialised tab state passed via `loadURL` query param or `webContents.send`
3. React in new window: reads `?tabId` from URL, skips session restore, renders just that tab
4. State sync: changes in detached window send IPC back to main; main broadcasts to all windows
### Phase 4 exit criteria
- Drag a tab 100 px below the title bar → new Electron window appears with that file
- Changes in detached window sync back (save writes to correct path)
- `electronApp.windows()` in Playwright returns 2 after tear-off
---
## Phase 5 — E2E test migration
**Goal ** : Playwright tests run against real Electron binary with full window visibility.
### Files to change
| File | Change |
|------|--------|
| `e2e/fixtures/tauri-test.ts` | Replace with `electron-test.ts` using `_electron` fixture |
| `e2e/playwright.config.ts` | Remove `tauri` project; add `electron` project; remove `webServer` block |
| `e2e/specs/*.tauri.spec.ts` | Rename to `*.electron.spec.ts` ; update fixture import |
### New fixture skeleton (e2e/fixtures/electron-test.ts)
``` typescript
import { test as base , _electron as electron , ElectronApplication , Page } from '@playwright/test' ;
export const test = base . extend < { app : ElectronApplication ; window : Page } > ( {
app : async ( { } , use ) = > {
const app = await electron . launch ( { args : [ 'dist-electron/main.js' ] } ) ;
await use ( app ) ;
await app . close ( ) ;
} ,
window : async ( { app } , use ) = > {
const win = await app . firstWindow ( ) ;
await win . waitForLoadState ( 'domcontentloaded' ) ;
await use ( win ) ;
} ,
} ) ;
export { expect } from '@playwright/test' ;
```
### New specs to add
- `e2e/specs/tear-away.electron.spec.ts` — drag tab, verify `app.windows().length === 2`
- `e2e/specs/window-controls.electron.spec.ts` — minimize, maximize, close buttons
### Phase 5 exit criteria
- All browser-mode specs pass unchanged
- Electron-mode specs pass (editor behavior, tear-away, window controls)
- `npm run test:e2e` green locally
---
## Phase 6 — CI pipeline update
**Goal ** : CI builds Electron app and runs Playwright Electron tests; all Tauri steps removed.
### File: `.gitea/workflows/ci.yml`
| Change | Detail |
|--------|--------|
| Remove `build-tauri` job | Was `cargo tauri build --features e2e-testing` |
| Add `build-electron` job | `npm run make` (Electron Forge) → produces binary artifact |
| Update `e2e` job | Remove `TAURI_APP_PATH` ; set `ELECTRON_APP_PATH` to Forge output path |
| Remove `--features e2e-testing` | From all Rust build steps |
| Remove Linux WebKit2 apt deps | `libwebkit2gtk-4.1-dev` etc. no longer needed |
| Keep all Rust jobs | `fmt` , `clippy` , `test` — `byte_draft` core still tested |
### Phase 6 exit criteria
- CI pipeline green end-to-end on a push to the migration branch
- Electron binary uploaded as CI artifact
- SonarQube gate passes
---
## Feature parity checklist
Check off as verified working in the Electron build.
### Editor Core
- [✓] CodeMirror 6 renders and accepts input
- [✓] Syntax highlighting (Rust, Python, JS/TS, Go, JSON, YAML, TOML, XML, Markdown, Shell, CSS, HTML, C/C++, Java, SQL)
- [✓] Find/Replace with regex
- [✓] Word wrap toggle
- [✓] Format on save
- [✓] Format on paste
- [✓] Line ending detection (CRLF/LF/CR)
- [✓] Cursor position in status bar
- [✓] Language in status bar
- [✓] Encoding in status bar
### File Operations
- [✓] Open file via dialog
- [✓] Open file via workspace tree
- [✓] Save (Ctrl/⌘+S)
- [✓] Save As (Ctrl/⌘+Shift+S)
- [✓] Revert file
- [✓] Encoding detection (encoding_rs via subprocess)
- [✓] Binary read (hex view)
- [✓] Multi-encoding support
### Tab System
- [✓] Multi-tab support
- [✓] Tab reorder (dnd-kit)
- [✓] Close tab (Ctrl/⌘+W)
- [✓] New tab (Ctrl/⌘+T)
- [✓] Cycle tabs (Ctrl/⌘+Tab / Ctrl/⌘+Shift+Tab)
- [✓] Dirty indicator
- [✓] **Tear-away tab → new window **
- [✓] Session restores tabs on relaunch
### Window & Chrome
- [✓] Custom title bar (no native OS bar)
- [✓] Minimize / Maximize / Close buttons
- [✓] Window drag region
- [✓] macOS vibrancy (HUD)
- [✓] Windows: custom chrome
- [✓] Resizable (min 800× 600)
### Workspace / Sidebar
- [✓] Open folder
- [✓] File tree with folder expansion
- [✓] File icons
- [✓] Toggle sidebar (Ctrl/⌘+B)
- [✓] Workspace files in command palette
### Special Viewers
- [✓] Markdown live preview (source / split / preview modes)
- [✓] Hex viewer
- [✓] Image viewer
### UI & Theming
- [✓] 6 built-in themes
- [✓] Tweaks panel (Ctrl+K)
- [✓] Accent color picker
- [✓] 3 layout modes (classic, minimal, pane)
- [✓] Command palette (Ctrl/⌘+P)
- [✓] Preferences modal (Ctrl/⌘+,)
- [✓] App (hamburger) menu
### Session & Persistence
- [✓] Save/load session (tabs, workspace, theme)
- [✓] Tab content auto-saved
- [✓] Workspace root remembered
- [✓] Theme remembered
### Backend IPC (15 Rust commands + 1 Electron-native)
- [✓] open_file
- [✓] save_file
- [✓] save_file_as ← dialog handled in Electron main; write routed to save_file in Rust
- [✓] revert_file
- [✓] read_file_with_encoding
- [✓] read_binary
- [✓] load_session
- [✓] save_session
- [✓] update_tab_content
- [✓] open_folder
- [✓] read_directory
- [✓] list_workspace_files
- [✓] detect_language
- [✓] lint_document ← stub (returns empty array; real linting TBD)
- [✓] format_document
- [✓] detect_encoding
### E2E Test Coverage
- [ ] browser-behavior.spec.ts passes
- [ ] browser-ui.spec.ts passes
- [ ] editor-behavior.electron.spec.ts passes
- [ ] tear-away.electron.spec.ts passes
- [ ] window-controls.electron.spec.ts passes
- [ ] CI pipeline green
---
## How to resume in a new session
1. Read `CLAUDE.md` (project root) for a one-page overview
2. Read this file — check the status block at the top for current phase
3. Run `git branch` — work should be on `electron-migration` branch
4. Find the current phase section above; implement what's listed there
5. Verify exit criteria before marking the phase complete and moving on
6. Update the `[ ]` → `[✓]` marker in the status block when done