Launchpad / Architecture

Architecture

A map of how Launchpad is put together — stack, layout, and the handful of patterns that show up across the codebase.

Tech stack

  • Tauri v2 — the native shell. Gives us a Swift / Objective-C free macOS window with a system WebView inside.
  • Rust — PTY management (portable-pty), filesystem ops, git via git2 (libgit2 bindings) plus shelled-out system git for network operations. Filesystem watching via notify (FSEvents on macOS).
  • Vanilla JS — all frontend logic, direct DOM manipulation.
  • xterm.js 6 — the terminal emulator, with WebGL renderer and Unicode 11 width tables.
  • CodeMirror 6 — the editor, with syntax highlighting, search, autocompletion, bracket matching, code folding.
  • Vite — frontend bundler.

Project layout

src/                  # Frontend (JS / CSS / HTML)
  main.js             # App entry, tab mgmt, split workspace, panes, shortcuts
  projects.js         # Active project state + project Tauri command wrappers
  projectpicker.js    # Project picker UI (welcome / recents)
  filebrowser.js      # File tree, context menu, DnD, git colors, CRUD
  editor.js           # CodeMirror factory — creates independent editor instances
  git.js              # Git status polling, file status colors
  gitpanel.js         # Full git panel UI
  quickopen.js        # Cmd+P fuzzy file finder
  settings.js         # Persistent settings store (~/.launchpad/config.json)
  settingspanel.js    # Settings form UI
  styles.css          # All styles, organized by section

src-tauri/            # Rust backend
  src/lib.rs          # All Tauri commands (PTY, fs, git, settings, projects)
  Cargo.toml          # Rust deps
  tauri.conf.json     # App config

index.html            # HTML shell — picker-root + workspace-root siblings
specs/                # Design specs (project model, agent model, etc.)

Projects model

A project is just a root directory stored in ~/.launchpad/projects.json. The workspace is gated behind the picker; enterWorkspace(project) initializes the terminal, file browser, and git panel only after a project is chosen.

The active project is held in projects.js as module state (activeProject). It's the single source of truth for:

  • Terminal spawn cwd
  • File browser root
  • Git panel path
  • ⌘P search root
  • Filesystem watcher

All three PTY spawn sites (createTab, splitPane, createTabInRight) pass getActiveProject()?.path directly — no inheritance from the previous tab's cwd, no defaultDirectory setting, no file-browser path.

Window routing

Single-window by default, multi-window when asked. Clicking a project in the picker takes over the current window — the picker view swaps for the workspace in-place. If the project is already open in another window, focus_project_window focuses that window and the current one stays on the picker.

enterWorkspace(project) calls register_project_window(path, current_window_label); the "← Projects" teardown calls unregister_project_window(path) before reloading. A project_windows: HashMap<canonical_path, label> in AppState is the routing table, cleaned lazily when get_webview_window returns None.

Tab system & split workspace

Unified tab bar. Terminal tabs, editor tabs, and the settings tab share one tab bar. Each tab has a type field ("terminal", "editor", or "settings") and tab-type guards protect terminal-specific code (fitAllPanes, PTY writes, split panes).

Split workspace. ⌘\ toggles a vertical split. The right group tracks its own tab IDs in rightGroupTabIds. Each group has its own tab bar. Tabs can be dragged between groups or moved with ⌘⇧M. Closing the last tab in the right group auto-collapses the workspace.

In-tab pane split. ⌘D splits a single terminal tab into two vertical panes (50/50, draggable divider). Each pane gets its own PTY. Independent from the workspace split.

Tab drag-to-move. When the workspace is split, a MutationObserver-based drag system moves tabs between left and right groups. Group markers on tab objects track which group they belong to.

PTY lifecycle

Each terminal tab or pane spawns its own PTY via portable-pty. The reader thread is not started during spawn_pty — instead, the frontend calls start_pty_reader after registering the pane in paneMap. This prevents a race where early output would be dropped before the frontend had anywhere to route it.

Backpressure

pause_pty_reader and resume_pty_reader let the frontend signal when xterm.js's write queue exceeds its high-water mark. The reader thread sleeps until cleared; data stays buffered in the OS PTY buffer. No queue explosion, no memory bloat.

Why not stream directly? A naïve implementation that pipes PTY output straight into xterm.js as fast as it arrives can overwhelm the renderer on a cat /very/large/file, peg the CPU, and make the terminal feel stuck. Flow control fixes that.

File browser

Root-locked. setRoot() guards against any path outside projectRoot. Nav-up caps at the project root (no-op when already there). Go-home jumps back to the root.

No terminal disruption. The file browser holds no concept of the terminal's cwd. Browsing subfolders never writes anything to a PTY — CLI agents (Claude, Aider, etc.) are safe from accidental cd.

Off-DOM tree building. The file tree is built in a detached DOM fragment and swapped in a single operation to prevent flicker when expanding folders.

Live filesystem watcher. Uses the notify crate (FSEvents on macOS) with a 300ms debounce to watch the project root recursively. Started once in enterWorkspace(). Emits fs-changed Tauri events that trigger refreshFileBrowser() and git status updates. Frontend throttles to 500ms.

Git backend

libgit2 for local, system git for network. Local operations (stage, unstage, stash, branch) use the git2 crate — fast, in-process, no fork overhead. Network operations (push, pull, fetch, merge) shell out to system git via std::process::Command so they respect the user's SSH keys, credential helpers, and .gitconfig. Network ops are cancellable via cancel_git_op, which kills the spawned process by PID.

Dual status entries. get_git_status emits separate entries for staged (index_new, index_modified, index_deleted) and unstaged (new, modified, deleted) changes. A file can appear in both lists.

Panel re-rendering. The git panel rebuilds its innerHTML on every refresh but uses a snapshot comparison (JSON.stringify) to skip redundant re-renders during the 3-second poll. Module-level state (expandedCommitOid) persists across re-renders.

Tauri commands (Rust → JS IPC)

All commands live in src-tauri/src/lib.rs. A partial index:

PTY

  • spawn_pty(cwd?, rows?, cols?) — spawn a new PTY, returns { tab_id }. Does not start reading output yet.
  • start_pty_reader(tab_id) — start the output reader thread (call after registering the pane)
  • write_to_pty, resize_pty, pause_pty_reader, resume_pty_reader, close_pty

Filesystem

  • read_directory, search_files, read_file_preview
  • write_file, create_file, create_directory, delete_path, rename_path
  • watch_directory / unwatch_directory — emits fs-changed events
  • pick_directory, reveal_in_finder, get_home_dir

Git

  • get_git_status, list_branches, list_remote_branches, get_commits, get_remote_url
  • checkout_branch, create_branch, git_delete_branch
  • git_stage_file, git_unstage_file, git_stage_all, git_unstage_all, git_discard_file
  • git_commit, git_push, git_pull, git_fetch, git_merge_branch, cancel_git_op
  • git_stash_save / pop / list / apply / drop
  • get_file_diff, get_commit_details, git_resolve_ours, git_resolve_theirs

Projects & windows

  • load_projects, add_project, remove_project, rename_project, touch_project
  • focus_project_window, register_project_window, unregister_project_window
  • open_new_window(path?) — creates a new Tauri window (with path: URL ?folder=<path>; without: boots into picker)

Contributing

PRs welcome. A few conventions that keep things consistent:

  • Vanilla JS, direct DOM manipulation. Keep it that way.
  • Project-scoped features rule. When wiring a new feature that cares about "the current directory", use getActiveProject().pathnot getCurrentPath() from the file browser. getCurrentPath() tracks sub-folder navigation; only the file browser itself should read it.
  • Prefer small. If 30 lines handle it, don't reach for a library or an abstraction.

Read CLAUDE.md in the repo for a more exhaustive architecture rundown, and specs/project-model-spec.md for the projects design doc.


Done exploring? Get started →