feat: Archon Desktop — Tauri v2 cross-platform desktop app#6
Conversation
Implements US-001 from PRD. Changes: - New packages/desktop/ with Tauri v2 + React + TypeScript + Vite - package.json registers @archon/desktop as workspace package - tsconfig.json extends root with strict mode, DOM libs, JSX support - src-tauri/ with Cargo.toml, tauri.conf.json, and Rust entry points - Minimal React app (App.tsx + main.tsx) for renderer - Vite config with Tauri-compatible settings - Added desktop vite.config.ts to eslint ignores
Implements US-002 from PRD.
Changes:
- New packages/server/src/routes/schemas/desktop.schemas.ts with Zod schemas for all desktop endpoints
- New packages/server/src/routes/desktop.ts with setupDesktopRoutes() containing loopback guard middleware and registerOpenApiRoute-based route registration
- GET /api/desktop/health returns 200 with { ok, version }
- All other desktop routes (fs/tree, fs/file, pty, tmux/list, tmux/kill, lsp) return 501
- Loopback guard middleware rejects non-127.0.0.1/::1 requests with 403
- Desktop routes registered from api.ts via setupDesktopRoutes(app) call
- Unit tests (18 tests) covering loopback guard, health endpoint, and all placeholder routes
Implements US-003 from PRD.
Changes:
- Add ssh_tunnel.rs with ssh_connect/ssh_disconnect Tauri commands
- Deterministic port allocation: hash('archon-desktop:' + alias) % 900 + 4200
- TCP port wait with 15s timeout and stderr capture for diagnostics
- Error classifier for SSH failures (host key, permission, refused, DNS, timeout, port in use)
- TunnelManager state with cleanup on disconnect and app exit (kill_on_drop)
- 10 Rust unit tests covering port-hash determinism + error classification
- Add tokio dependency for async networking and process management
Implements US-004 from PRD. Changes: - Add react-resizable-panels for horizontally resizable layout - Create App.tsx with [sidebar | editor column | grid] + status bar - Add collapsible Host Sessions right drawer with toggle from status bar - Dark theme via CSS variables (no theme picker, no light mode) - All regions render as empty shells with visible dashed borders and labels - Resizable panels with min/max size constraints for snap behavior
Implements US-005 from PRD. Changes: - Add GET /api/desktop/preflight endpoint that checks tmux, aichat, typescript-language-server, and archon availability - Add Zod schemas for preflight response shape in desktop.schemas.ts - Add PreflightBanner React component with per-dependency copy-command buttons and dismissal persistence - tmux version < 3.0 triggers a warning about missing -A flag support - Banner dismissal persisted via localStorage keyed by missing deps hash - 33 tests: 25 server-side (endpoint + runPreflightChecks) + 8 banner state logic tests
Implements US-006 from PRD.
Changes:
- New local_pty.rs Rust module with portable-pty shell spawning
- Platform-specific default shells: pwsh (Windows), zsh (macOS), bash (Linux)
- Tauri commands: pty_spawn, pty_write, pty_resize, pty_kill
- Base64-encoded output streamed via pty:output:{ptyId} events
- PtyManager state with cleanup on Drop
- Added portable-pty, base64, uuid dependencies to Cargo.toml
- Registered module and commands in lib.rs
Implements US-007 from PRD.
Changes:
- Add WS /api/desktop/pty endpoint with tmux session create/attach
- Session name validated against /^archon-desktop:[a-z0-9:-]+$/
- Bidirectional binary-safe byte relay via script -qfc PTY wrapper
- Resize messages ({ type: 'resize', cols, rows }) forward to tmux
- Shared Bun WebSocket setup module (ws.ts) for Hono integration
- 27 unit tests for session validation, argv construction, spawnProcess
Implements US-008 from PRD. Changes: - Add TerminalPane React component wrapping xterm.js Terminal with WebGL addon and FitAddon - Add TerminalBackend interface abstracting local (Tauri IPC) and remote (WebSocket) PTY backends - Add createLocalBackend and createRemoteBackend factory functions - 10 unit tests covering backend mock (write, resize, onData, dispose, bidirectional flow) - Install @xterm/xterm, @xterm/addon-webgl, @xterm/addon-fit dependencies
Implements US-009 from PRD. Changes: - New Osc133Addon xterm.js addon that parses OSC 133;A/B/C/D sequences into CommandBlock structs - Gutter collapse/expand toggles positioned at prompt start lines - Right-click context menu with Copy Command and Copy Output actions - Integrated into TerminalPane via enableOsc133 prop (default true) - 20 unit tests covering parser, block state machine, and no-op behavior
Implements US-010 from PRD. Changes: - New GridEngine component with react-grid-layout v2 (6x3 grid, max 18 panes) - Grid state reducer with ADD/REMOVE/MOVE/RESIZE/RENAME/MAXIMIZE/LAYOUT_CHANGE actions - PaneHeader component with inline rename, context menu (Close and Kill), double-click maximize - findFreeSlot helper for slot allocation - Integrated GridEngine into App.tsx replacing the placeholder TerminalGrid - Dark theme CSS for grid panes, headers, context menus, and resize handles - 25 unit tests covering all reducer actions, findFreeSlot, and grid constants
Implements US-011 from PRD. Changes: - Add openAdHocTerminal helper with archon-desktop:adhoc:<uuid> session naming - Place new panes in first free grid slot via findFreeSlot - Show toast when grid is full (18 panes) - Wire Ctrl+Shift+` keyboard shortcut for ad-hoc terminal in App.tsx - Add toast UI component with auto-dismiss and fade animation - Add 7 unit tests covering slot allocation, full-grid toast, session naming
Implements US-012 from PRD. Changes: - Replace 501 placeholder with real fs/tree handler using fs/promises readdir - Add listDirectory helper (sorted entries with kind, size, mtime) - Add containsTraversal and isPathWithinRoot path safety helpers - Add notFoundResponseSchema to desktop schemas - Add 13 new tests: path traversal helpers (8) + endpoint behavior (5)
Implements US-013 from PRD. Changes: - New FileTree.tsx with tree state reducer, lazy-loading via GET /api/desktop/fs/tree, host badges, context menu (New File/Folder, Copy Path, Copy Relative Path, Remove from Workspace) - Confirm modal for Remove from Workspace (no files deleted) - Name prompt modal for New File/Folder with API integration - 26 unit tests covering pure helpers, tree reducer, and context menu actions - Integrated into App.tsx replacing placeholder Sidebar - Dark theme CSS for file tree, context menu, and modals
Implements US-014 from PRD. Changes: - New AddFolderModal component with host picker dropdown, path browser with breadcrumb navigation, and OK/Cancel actions - Workspace persistence helpers (loadWorkspace, saveWorkspace, addRootToWorkspace, removeRootFromWorkspace) using localStorage - Wired modal to FileTree's + button via onAddRoot prop in App.tsx - App.tsx now loads workspace roots from persistence on mount and persists on remove - 18 unit tests covering workspace round-trip, path helpers, and CRUD operations - CSS styles for modal (host picker, breadcrumb, directory browser, path display)
Implements US-015 from PRD. Changes: - Add matchesCodebasePath helper for trailing-slash-normalized path comparison - Fetch /api/codebases on tree mount, cache codebase paths in memory - Render Archon badge (styled "A" icon) next to host badge for matched roots - Add reload button to refresh codebase badges - Add 8 unit tests for path matching and badge visibility logic - Add CSS for archon badge and reload button
Implements US-016 from PRD. Changes: - Add Reveal in OS context menu item with OS-specific command branching (explorer.exe on Windows, open -R on macOS) - Remote paths show tooltip and trigger no-op toast instead of revealing - Add Open Archon Web UI context menu item, shown only on roots matching a registered codebase - Uses @tauri-apps/plugin-shell for opening URLs/paths in the native OS - Add shell plugin to tauri.conf.json allowlist - Add unit tests for getRevealCommand, canRevealInOs, and Web UI visibility logic - Pass onToast callback from App to FileTree for remote path feedback
Implements US-017 from PRD. Changes: - Replace 501 placeholders for GET /api/desktop/tmux/list and POST /api/desktop/tmux/kill with real handlers - Add POST /api/desktop/tmux/rename endpoint for session renaming - Add parseTmuxListSessions helper to parse tmux list-sessions pipe-delimited output - Add buildTmuxListSessionsArgs, buildTmuxKillSessionArgs, buildTmuxRenameSessionArgs helpers - Add tmuxRenameQuerySchema to desktop schemas - Session name validated against injection-safety regex for kill and rename - Returns 404 for session not found on kill/rename - Add 20+ unit tests for parse logic, argv construction, and endpoint behavior
Implements US-018 from PRD. Changes: - New HostSessionsPanel component with auto-refresh (15s), session list grouped by host - Session actions: Attach (to first free grid slot), Kill (with confirm), Rename (inline edit) - Drag-and-drop support via HTML5 drag API with application/x-archon-session data format - Replaced placeholder HostSessionsDrawer in App.tsx with real HostSessionsPanel - 25 unit tests covering formatAge, buildAttachPane, buildAttachPaneAtSlot, fetch/kill/rename helpers, drag data format - CSS styles for session rows, host groups, action buttons, status badges
Implements US-025 from PRD. Changes: - New EditorColumn.tsx with snap-to-grid resize (17%, 33%, 50%), collapse toggle, and rail view - Panel uses react-resizable-panels v4 collapsible API with panelRef for programmatic control - Group onLayoutChanged callback snaps editor width to nearest grid-column-width after resize - EditorColumnContent renders thin clickable rail when collapsed, full editor placeholder when expanded - Editor column state (width, collapsed) persisted to workspace JSON via loadWorkspace/saveWorkspace - Extended WorkspaceData type with editorColumn field; updated loadWorkspace to preserve it - 14 unit tests covering snapWidth, persistence round-trip, and collapse/expand state
Implements US-026 from PRD. Changes: - New EditorTabs.ts with pure tab state machine (preview/pinned, dirty, close flow) - EditorColumn.tsx updated with TabBar, CodeMirror 6 editor (lazy-loaded), close-dirty modal, tab context menu - FileTree.tsx extended with onFileClick/onFileDoubleClick callbacks - App.tsx wires file clicks to tab state and fetches file content - CodeMirror 6 with lang-javascript, lang-python, lang-markdown, lang-json, lang-css, lang-html - Dark theme editor styling matching app theme - 20 unit tests covering tab state machine
Implements US-027 from PRD. Changes: - Replace 501 placeholders for GET/PUT /api/desktop/fs/file with real handlers - GET reads file content with 10 MB size limit (413), path traversal rejection, 404 - PUT writes atomically via tempfile + rename with mtime conflict detection (409) - Optional createParents query flag creates parent directories - Add Zod schemas: fsFileWriteQuerySchema, fsFileWriteResponseSchema, conflictResponseSchema, tooLargeResponseSchema - Add readFileContent and writeFileAtomically exported helpers - 22 new tests covering read, write, conflict detection, traversal rejection, createParents
Implements US-028 from PRD. Changes: - Add SaveFlow.ts with saveFile(), isSaveShortcut(), getDirtyFileNames(), hasDirtyTabs() - Wire Ctrl+S/Cmd+S keyboard shortcut to save active editor tab - Save calls PUT /api/desktop/fs/file with expectedMtime for conflict detection - Add ConflictBanner component showing Reload/Overwrite on 409 response - Wire CloseDirtyModal Save button to actually save before closing - Add WindowCloseDirtyModal for window close with multiple dirty files - Add beforeunload guard preventing accidental close with dirty tabs - Track file mtimes from server responses for conflict detection - Add getEditorContent/replaceEditorContent for CM6 document access - 17 unit tests covering save, conflict, shortcuts, and dirty helpers
Implements US-029 from PRD. Changes: - Add SplitState, SplitAction, splitReducer to EditorTabs.ts for multi-split management - Refactor EditorColumnContent to render multiple side-by-side SplitPanes - Each split has independent tab bar, active tab, and context menu - Empty splits auto-remove; last empty split stays for rail collapse - Update App.tsx to use splitReducer instead of tabReducer - Add 16 new tests covering split creation, focus, collapse, and helpers - Add CSS for split pane layout with active indicator
Implements US-030 from PRD. Changes: - Replace WS /api/desktop/lsp 501 placeholder with real LSP proxy endpoint - Add language server spawning with per-project connection reuse (refcounted) - Support TypeScript, Python, Go, Rust, Markdown language servers - Integrate codemirror-languageserver into CM6 editor for hover, completion, diagnostics - Add LspClient.ts helper for language detection and WS URI building - Document CM6 spike outcome in decisions/editor-backend.md (CM6 selected over Monaco) - Add 35 new tests (14 server LSP helpers + 21 desktop LspClient)
Implements US-031 from PRD. Changes: - New SshReconnectBanner.ts with pure state machine for reconnection logic - Exponential backoff intervals: 1s, 2s, 4s, 8s, 16s (~31s total) - Manual Reconnect button resets retry counter at any point - Banner fades out 2s after successful reconnection - Integrated into App.tsx listening for archon:tunnel-drop events - Dark-themed banner CSS with fade-in/fade-out animations - 30 unit tests covering backoff intervals, state transitions, and full lifecycle
Implements US-032 from PRD. Changes: - New packages/desktop/src/lib/errors.ts with classifyDesktopError(error, category) mapping - Categories: ssh, tmux, lsp, file, port with pattern-based error classification - SSH: maps host key verification, permission denied, connection refused, no such host, timeout - Tmux: version check, binary missing, session name validation, protocol mismatch - LSP: command not found, connection refused/reset - File: ENOENT, EACCES, EISDIR, ENOSPC, conflict, too large - Port: EADDRINUSE + collision detection for worktree (3190-4089) and desktop (4200-5099) ranges - 54 unit tests covering all error branches with 100% coverage
Implements US-033 from PRD. Changes: - New packages/desktop/src/lib/logger.ts with DesktopLogger class, structured JSON lines, 10 MB rotation (.1 through .5), secret masking, per-OS path resolution - New packages/desktop/src/lib/logger.test.ts with 28 tests covering rotation, file naming, masking, all log levels - New packages/desktop/src-tauri/src/log_path.rs Rust module with get_log_path Tauri command for Settings → About → Open Logs - Updated lib.rs to register log_path module and command
…ation Implements US-034 from PRD. Changes: - Add placeholder icon files (32x32, 128x128, 128x128@2x PNG, ICO, ICNS) for Tauri bundler - Configure tauri.conf.json bundle settings for Windows WiX MSI and macOS DMG signing - Add signingIdentity and minimumSystemVersion for macOS bundle config - Create README.md with build steps for both platforms, signing/notarization workflow, remote host dependencies, and troubleshooting guide
Implements US-035 from PRD. Changes: - Created packages/desktop/docs/ga-validation.md with full test matrix - Verified all platform-specific code branches (shell defaults, Reveal-in-OS, app-data paths, log paths) - Documented all 5 Primary Success Metrics with evidence - Documented pending manual smoke tests for Windows + macOS hardware - Documented Aqua Voice and G9 ultrawide validation plans
Post-Ralph Code Review — Gap ReportReviewed by code-reviewer subagent against Validation: BLOCKER — workspace + agent presets not stored at PRD-specified pathPRD §10.6 / §13 Decision 9 / §10.8 require workspace roots and agent presets at per-OS app-data JSON:
Actual implementation uses
WebView-scoped MAJOR — macOS notarization unconfiguredPRD §12 Phase 7 requires macOS code-signing with Developer ID + Gatekeeper notarization so the DMG launches without user-mode workarounds.
MAJOR — WS loopback guard lacks test coverageLoopback middleware in MINOR — OSC 133 collapse is visual-onlyPRD §10.4 says collapsible command blocks. MINOR — Rust-side events bypass the rotated log
MINOR — Bundler targets
|
Post-Ralph review found no coverage for the loopback guard on WebSocket upgrade paths. Adds three tests verifying that non-loopback and unknown-IP WS upgrade requests to /api/desktop/pty and /api/desktop/lsp are rejected with 403 before the upgrade completes. Fail-closed on unknown IP is now verified rather than only inferred. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ion, explicit bundler targets Addresses three post-Ralph review findings: 1. OSC 133 collapse now actually hides block output. Previously toggleBlock() only flipped the gutter icon — the lines stayed rendered. Added an absolutely positioned overlay div that covers the output region (commandExecutedLine → commandFinishedLine), repositioned on xterm scroll/resize events. Works with both the DOM and WebGL renderers since the overlay is a sibling of xterm's render layers rather than a style on xterm-row divs. Extracted computeOutputRange() and collapseLabel() as pure helpers for testability. 2. Rust sidecar now writes rotated logs at the per-OS app-data path. New logger.rs module matches the TS frontend's 10 MB rotation cap: rotates archon-desktop.log → archon-desktop.log.1 on overflow. Wired into lib.rs (panic hook + lifecycle event), ssh_tunnel.rs (tunnel failures), and local_pty.rs (PTY read errors). Uses std::fs only — no new crates. 3. Bundler targets changed from "all" to explicit ["msi", "dmg"] so builds don't emit NSIS/AppImage/.deb formats that aren't part of the GA plan (PRD §12 Phase 7). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the localStorage-only storage for workspace roots and agent
presets with a hydrate-before-render + write-through pattern backed by
Tauri's fs plugin. Files land at the paths the PRD requires (§10.6 /
§13 Decision 9 / §10.8):
Windows: %APPDATA%\ArchonDesktop\{workspace.json,agents.json}
macOS: ~/Library/Application Support/ArchonDesktop/{workspace.json,agents.json}
Design:
- New src/lib/appDataStorage.ts exposes readAppData/writeAppData (sync-
facing) plus hydrateAppData (async, called once at bootstrap).
- Hydrate reads each registered JSON file from AppData into localStorage
so existing useState initializers keep working as sync reads.
- Writes update localStorage immediately and fire-and-forget a
writeTextFile to AppData — AppData is canonical on next hydrate.
- Outside Tauri (vitest/jsdom, non-packaged dev) the plugin import is
skipped; writes stay local to the session. No migration needed —
existing localStorage data surfaces on next write.
Rust side:
- Cargo.toml: add tauri-plugin-fs 2
- lib.rs: register tauri_plugin_fs::init()
- capabilities/default.json: new capability granting fs scoped to the
AppData directory only (allow-app-*, mkdir, exists). shell.open kept
for the Reveal-in-OS flow from US-016.
Frontend wiring:
- AddFolderModal.tsx: loadWorkspace/saveWorkspace now go through the
storage module. Stale "would use Tauri fs in a real app" comments
removed.
- AgentPresets.ts: listPresets/savePreset/deletePreset/seedDefaultPresets
moved off the raw PRESETS_STORAGE_KEY. The one-shot SEEDED_KEY stays
localStorage-only since it's a per-WebView flag, not user data.
- main.tsx: awaits hydrateAppData(ALL_APP_DATA_SPECS) before createRoot
so state initializers see persisted data on first paint.
Tests: 11 new cases in src/lib/appDataStorage.test.ts cover the
sync-facing API, spec constants (PRD-mandated filenames), non-Tauri
fall-through, and round-trip semantics. Total suite goes from 506 to
517 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up fixes pushedAll four open issues from the review comment above are now addressed in three new commits on this branch: 2d526dd — de6146f —
7c7d565 —
Validation: Deferred / out of scope for this PR:
|
Summary
@archon/desktop(Tauri v2 + React + TypeScript) with 35 user stories: SSH tunnel sidecar, 3×6 terminal grid, file tree, CodeMirror 6 editor with LSP-over-the-wire, launch profiles, agent presets, and server-side desktop API endpoints@archon/web, workflow engine, isolation system, or any existing functionality. All new code is additive.UX Journey
Before
After
Architecture Diagram
After
Connection inventory:
@archon/desktoprenderer@archon/serverdesktop routesdesktop.tsroutesdesktop.tsroutesdesktop.tsroutesapi.tsdesktop.tssetupDesktopRoutes(app)callindex.tsws.tsLabel Snapshot
risk: mediumsize: XLserver,desktopserver:desktop-routes,desktop:*Change Metadata
featuremultiLinked Issue
.archon/ralph/archon-desktop/prd.md)Validation Evidence (required)
src-tauri/) cannot becargo checked in CI (no Rust toolchain in worktree) — verified via code inspection only.Security Impact (required)
Yes— new/api/desktop/*endpoints expose filesystem and PTY accessNoNoYes— fs/tree and fs/file endpoints read/write files on the server host/api/desktop/*routes are behind a loopback-only guard (rejects non-127.0.0.1/::1). Only reachable via SSH port-forward tunnel. Path traversal (..) rejected in all file endpoints. tmux session names validated against strict regex.Compatibility / Migration
Yes— all changes are additiveNoNoHuman Verification (required)
packages/desktop/docs/ga-validation.md).Side Effects / Blast Radius (required)
@archon/servergains new route registration;packages/server/src/index.tsgains WebSocket handlerBun.serve()could theoretically affect existing SSE streaming (isolated via separate upgrade path)/api/desktop/Rollback Plan (required)
desktop.tsimport breaks; existing tests would catch thisRisks and Mitigations
src-tauri/code is not compiled/tested in CI