Architecture Overview
Tempest’s desktop app is built with Tauri 2, a lightweight framework that wraps a Rust backend with a React frontend. The invoke handler at the end oflib.rs routes all IPC (Inter-Process Communication) commands from the frontend to their corresponding Rust implementations.
Key characteristics:
- Stateless Rust backend: No persistent in-memory state; all app data flows through React
- Async/blocking operations: Heavy I/O (git, PTY, file operations) runs on dedicated threads to avoid blocking the IPC worker pool
- Cross-platform: Commands handle Windows/Unix differences (paths, shell, process termination)
- Error handling: All commands return
Result<T, String>with descriptive error messages
Window Configuration
The main window is created frameless with these properties:- Frameless:
decorations: falseremoves OS chrome, allowing custom titlebar drawn in React - No drag-drop:
dragDropEnabled: falseprevents default behavior; file drops handled by React handlers instead - Asset protocol: CSP allows
asset://protocol with**scope for full resource access - Updater plugin: Configured with GitHub releases endpoint for auto-updates signed with minisign public key
Tauri Plugins
Three plugins provide native OS functionality:| Plugin | Version | Purpose | Example Usage |
|---|---|---|---|
tauri-plugin-opener | Latest | Open URLs and files with native handlers | open("https://...", target) |
tauri-plugin-dialog | Latest | Native file/folder dialogs | open({ directory: true }) |
tauri-plugin-clipboard-manager | Latest | Read/write system clipboard | readText(), writeText(text) |
State Management
PtyState (Concurrent PTY Registry)
ZenState (Secondary Window Config)
"zen-1234567890") to (project_path, project_name).
Runtime State and Persistence
read_runtime_state
Reads the persisted runtime state from disk.| Param | Type | Description |
|---|---|---|
app | tauri::AppHandle | App context (provided by Tauri, provides access to app data dir) |
Result<String, String>
- Ok: JSON string of RuntimeState (may be
{}if file doesn’t exist on first launch) - Err: Filesystem error message
- Reads from
<app-data-dir>/runtime-state.json - Returns empty object
{}if file missing (first launch case) - Parsed by frontend, falls back to localStorage migration
- Invalid JSON in file: Frontend falls back to localStorage
- Permission denied on app data dir: Returns error string
write_runtime_state
Writes runtime state to disk atomically.| Param | Type | Description |
|---|---|---|
app | tauri::AppHandle | App context for app data dir path |
data | String | Serialized JSON (already stringified by frontend) |
Result<(), String>
- Ok: File written successfully
- Err: Filesystem error
- Creates app data directory if missing
- Writes to temp file
runtime-state.json.tmp - Atomically renames temp to
runtime-state.json(prevents corruption on crash) - No validation of JSON content (frontend is responsible)
- Insufficient disk space
- Permission denied
- Temp file creation fails
Workspace and File Operations
create_workspace
Creates a new workspace directory.| Param | Type | Description |
|---|---|---|
location | String | Parent directory path (e.g., /home/user/projects) |
name | String | Workspace name (becomes directory name) |
Result<String, String>
- Ok: Full path to created directory (e.g.,
/home/user/projects/my-app) - Err: Filesystem error (path invalid, permission denied, etc)
- Calls
fs::create_dir_all(creates parent dirs if needed) - Returns path as lossy UTF-8 string
- Invalid
locationpath (doesn’t exist, not a directory) - Permission denied on parent
- Disk full
list_directory
Lists files and directories in a folder.| Param | Type | Description |
|---|---|---|
path | String | Directory path to list |
Result<Vec<DirEntry>, String>
- Ok: Sorted vector of directory entries
- Err: Path doesn’t exist, permission denied, etc
- Windows: Converts forward slashes to backslashes for consistency
- Unix: Uses paths as-is
- Path doesn’t exist
- Permission denied
- Path is not a directory
read_file
Reads entire file contents.| Param | Type | Description |
|---|---|---|
path | String | File path to read |
Result<String, String>
- Ok: File contents as UTF-8 string
- Err: File doesn’t exist, permission denied, invalid UTF-8, etc
- File doesn’t exist
- Permission denied
- File is invalid UTF-8
- Is a directory (not a file)
write_file
Writes string content to a file.| Param | Type | Description |
|---|---|---|
path | String | File path to write (created if missing) |
content | String | UTF-8 content to write |
Result<(), String>
- Ok: File written successfully
- Err: Permission denied, disk full, etc
- Creates file if it doesn’t exist
- Overwrites entire file (not append)
- Creates parent directories: NO (must exist)
- Permission denied
- Disk full
- Parent directory doesn’t exist
- Path is a directory
Git Repository Setup
git_init
Initializes a new git repository.| Param | Type | Description |
|---|---|---|
project_path | String | Directory to initialize as git repo |
Result<(), String>
- Ok: Repository initialized
- Err: git command failed (not in PATH, permission denied, etc)
- Runs
git init - Sets default branch to
mainviagit symbolic-ref HEAD refs/heads/main - Writes
.tempestand.tempest-pidto.gitignore - Stages all files with
git add -A(respects existing.gitignore) - Creates initial commit with Tempest author
gitnot in PATH- Directory doesn’t exist
- Already a git repository (should be idempotent in future)
- Permission denied
check_git_initialized
Checks if a directory is a git repository.| Param | Type | Description |
|---|---|---|
path | String | Directory to check |
bool
trueif directory is a git repository (has.git/)falseotherwise
- Runs
git rev-parse --git-dirand checks exit code - If true, ensures
.gitignorecontains.tempestentries
gitnot in PATH: Returns false- Path doesn’t exist: Returns false
git_add_remote
Adds a new git remote.| Param | Type | Description |
|---|---|---|
repo_path | String | Path to git repository |
remote_url | String | URL of remote (https or SSH) |
Result<(), String>
- Ok: Remote added successfully
- Err: Repository error, remote already exists, invalid URL, etc
- Runs
git remote add origin <url> - Fails if remote named “origin” already exists
- Repository is not a git repo
- Remote “origin” already exists
- URL is invalid
Git Worktrees (Multi-workspace)
create_terminal_worktree
Creates a new git worktree for isolated sessions.| Param | Type | Description |
|---|---|---|
project_path | String | Root project path |
name | String | Worktree name (branch name and directory name) |
Result<String, String>
- Ok: Full path to worktree (e.g.,
/project/.tempest/my-workspace) - Err: Git error, worktree already exists, etc
- Ensures at least one commit exists in repository (auto-commits empty repo if needed)
- Prunes stale worktree metadata from failed previous runs
- Creates worktree via
git worktree add .tempest/<name> -b <name> - Handles pre-existing path (removes orphan dir or errors if registered)
- Copies gitignored files (
.env,.env.local,.env.development,.env.production) from project root - Creates junctions (Windows) or symlinks (Unix) for large dependency dirs (
node_modules,.venv) - Adds
.tempest-pidto.git/worktrees/<name>/info/exclude(local-only, not committed) - Validates worktree is not empty
- Writes to
.gitignorein project root
- Repository has no commits and auto-commit fails
- Worktree name already exists
- Permission denied in project directory
- Disk full
- Worktree created but ends up empty (all files .gitignored)
- Link creation fails (continue without link, log warning)
git_worktree_remove
Removes a git worktree directory.| Param | Type | Description |
|---|---|---|
repo_path | String | Path to root project repository |
worktree_path | String | Path to worktree to remove |
Result<(), String>
- Ok: Worktree removed
- Err: Directory still in use (process holding handle), permission denied, etc
- Removes directory junctions/symlinks (Windows) or symlinks (Unix) first to avoid following them into the real directories
- Attempts
git worktree remove --force <path> - If git fails, falls back to direct
fs::remove_dir_allwith retry loop (6 attempts, 500ms intervals) - On Windows, last resort uses
cmd /c rmdir /s /qif Rust’s remove fails - Prunes dangling
.git/worktrees/<name>metadata
- PTY process still alive (CWD handle held on Windows): Fails after retries
- Permission denied
- Worktree path doesn’t exist: Succeeds (idempotent)
close_and_remove_worktree
Atomically kills a PTY and removes its worktree.| Param | Type | Description |
|---|---|---|
session_id | String | PTY session ID to close |
repo_path | String | Root project repository path |
worktree_path | String | Worktree path to remove |
state | tauri::State<PtyState> | Global PTY registry |
Result<(), String>
Implementation:
- Removes session from PtyState DashMap (if present)
- Kills child process tree (via portable-pty job object on Windows, SIGTERM then SIGKILL on Unix)
- Waits up to 3 seconds for process to exit (polling every 50ms)
- Reads and force-kills any PID from
.tempest-pidsidecar (handles app restart case) - Waits 500ms for OS to release directory handle
- Removes directory junctions/symlinks
- Attempts
git worktree remove --force - Falls back to direct directory removal with retry loop (6 attempts, 500ms)
- On Windows, uses
cmd /c rmdir /s /qas last resort - Prunes
.git/worktrees/<name>metadata
- PTY not found in state: Continues with directory removal
- Directory still in use after all retries: Returns error
.tempest-pidcorrupted: Ignores and continues
Git Branches
get_git_branch
Returns the current branch name.| Param | Type | Description |
|---|---|---|
path | String | Path to git repository or worktree |
Result<String, String>
- Ok: Branch name (e.g., “main”, “feature/x”)
- Err: Not a git repo, detached HEAD, etc
- Uses
git symbolic-ref --short HEAD - Works even on repos with zero commits (no HEAD yet)
- Not a git repository
- HEAD doesn’t exist (no commits yet)
- Detached HEAD (error message includes current commit)
git_list_branches
Lists all branches in a repository.| Param | Type | Description |
|---|---|---|
repo_path | String | Path to git repository |
Result<Vec<BranchInfo>, String>
- Ok: List of all branches with current status
- Err: Not a git repo, git command failed, etc
- Runs
git branchand parses output - Current branch marked with
*prefix - Detached HEAD marker
+filtered out
- Not a git repository
- No branches exist (repo with zero commits)
git_switch_branch
Switches to a branch, with auto-stash of uncommitted changes.| Param | Type | Description |
|---|---|---|
repo_path | String | Path to git repository |
branch | String | Target branch name |
Result<(), String>
- Ok: Switched successfully
- Err: Branch doesn’t exist, checkout failed, etc
- Gets current branch name
- Checks if working tree is dirty (
git status --porcelain) - If dirty, stashes changes with label
tempest-autostash-from-<branch> - Switches branch via
git checkout <branch> - If checkout fails, restores stash so changes aren’t lost
- If checkout succeeds, checks stash list for a stash from a previous visit to the new branch
- If found, auto-applies stash (restores changes from previous session on this branch)
- Branch doesn’t exist
- Stash operation fails
- Working tree has merge conflicts
git_delete_branch
Deletes a branch locally and optionally remotely.| Param | Type | Description |
|---|---|---|
repo_path | String | Path to git repository |
branch | String | Branch name to delete |
force | bool | Use -D (force) instead of -d (safe) |
delete_remote | bool | Also delete on remote origin |
Result<(), String>
Implementation:
- Deletes locally with
git branch -d <branch>(or-Dif force) - If
delete_remote, attemptsgit push origin --delete <branch>(best-effort, doesn’t fail if remote branch doesn’t exist)
- Branch doesn’t exist
- Branch not fully merged (unless force=true)
- Permission denied on remote
git_branch_delete
Force deletes a branch (internal use).| Param | Type | Description |
|---|---|---|
repo_path | String | Path to git repository |
branch_name | String | Branch to delete |
Result<(), String>
Implementation:
- Equivalent to
git branch -D <branch>(always force) - Used internally for worktree branch cleanup
check_branch_merged
Checks if a branch has been merged into a base branch.| Param | Type | Description |
|---|---|---|
repo_path | String | Path to git repository |
branch | String | Branch to check |
Result<bool, String>
trueif merged (or remote branch deleted)falseif not mergedErrif git command fails
- Refreshes remote state with
git fetch --prune origin(offline is OK, falls back to local refs) - Checks if remote branch exists with
git ls-remote --heads origin <branch> - If remote doesn’t exist, returns
true(deleted after merge) - Checks if branch tip is ancestor of base branches:
git merge-base --is-ancestor <branch> <base> - Tries base branches in order:
origin/main,origin/master,origin/develop - Returns
trueif ancestor of any base branch
- Not a git repository
- No origin remote configured
- Network error during fetch (falls back to local refs)
Git Staging and Commits
git_stage
Stages a file for commit.| Param | Type | Description |
|---|---|---|
repo_path | String | Path to git repository |
file_path | String | Relative path to file within repo |
Result<(), String>
Implementation:
- Runs
git add -- <file_path> --prevents file_path from being interpreted as a flag
git_unstage
Unstages a file.| Param | Type | Description |
|---|---|---|
repo_path | String | Path to git repository |
file_path | String | Relative path to file within repo |
Result<(), String>
Implementation:
- Runs
git restore --staged -- <file_path>
git_discard
Discards changes to a file or deletes untracked file.| Param | Type | Description |
|---|---|---|
repo_path | String | Path to git repository |
file_path | String | Relative path to file within repo |
untracked | bool | If true, delete file; if false, restore from HEAD |
Result<(), String>
Implementation:
- If
untracked: true: Attemptsfs::remove_file, falls back tofs::remove_dir_allfor directories - If
untracked: false: Runsgit restore -- <file_path>
- File doesn’t exist
- Permission denied
- File is directory (for untracked deletion)
git_commit_staged
Commits staged changes.| Param | Type | Description |
|---|---|---|
repo_path | String | Path to git repository |
message | String | Commit message |
Result<(), String>
Validation: Fails if message is empty after trimming.
Implementation:
- Runs
git commit -m <message> - No author configuration (uses git config defaults)
- Empty message
- Nothing staged for commit
- Not a git repository
- User identity not configured in git
Git Status and Diff
git_status
Returns working tree status.| Param | Type | Description |
|---|---|---|
path | String | Path to git repository or worktree |
Result<Vec<GitStatusEntry>, String>
Implementation:
- Runs
git status --short - Parses unified two-character status codes
- Filters empty lines
- First char: index status (staging area)
- Second char: worktree status
| Code | Meaning |
|---|---|
M | Modified (unstaged) |
M | Modified (staged) |
A | Added (staged) |
D | Deleted (unstaged) |
?? | Untracked |
UU | Unmerged |
git_diff
Returns all uncommitted changes.| Param | Type | Description |
|---|---|---|
path | String | Path to git repository or worktree |
Result<Vec<FileDiff>, String>
Implementation:
- Runs
git diff HEAD(shows uncommitted changes vs HEAD) - Parses unified diff format
- Extracts hunk headers, line numbers, and change type
- Includes both staged and unstaged changes
git_diff_file
Returns diff for a single file.| Param | Type | Description |
|---|---|---|
path | String | Path to git repository |
file_path | String | Relative path to file within repo |
staged | bool | If true, show staged changes; else unstaged working tree |
untracked | bool | If true, render entire file as added lines (no git needed) |
Result<Vec<DiffLine>, String>
Implementation:
- If
untracked: true: Reads file and renders each line as “added” without running git - If
staged: true: Runsgit diff --cached -- <file> - If
staged: false: Runsgit diff -- <file> - Parses unified diff output
Git Push
git_push_branch
Commits pending changes and pushes branch.| Param | Type | Description |
|---|---|---|
repo_path | String | Path to git repository |
commit_message | Option<String> | Custom commit message; defaults to "Agent work on <branch>" |
Result<String, String> (JSON string)
Response:
- Gets current branch name
- Checks if working tree has changes (
git status --porcelain) - If changes exist:
- Checks if anything is staged (
git diff --cached --quiet) - If nothing staged, auto-stages all changes (
git add -A) - Commits with provided message or default
- Checks if anything is staged (
- Pushes with
git push -u origin <branch> - Queries remote URL with
git remote get-url origin - Returns JSON with remoteUrl and branch
- Branch doesn’t exist
- No remote configured
- Network error during push
- Commit fails (e.g., user identity not configured)
git_push_current_branch
Pushes current branch without committing.| Param | Type | Description |
|---|---|---|
repo_path | String | Path to git repository |
Result<String, String> (JSON string)
Response format: Same as git_push_branch
Implementation:
- Runs
git push -u origin <current-branch> - Fails if working tree is dirty (no auto-commit like
git_push_branch)
git_create_push_branch
Creates a new branch and pushes to remote.| Param | Type | Description |
|---|---|---|
repo_path | String | Path to git repository |
branch_name | String | New branch name to create and push |
Result<String, String> (JSON string)
Response format: Same as git_push_branch
Implementation:
- Creates branch locally with
git checkout -b <branch> - Pushes with
git push -u origin <branch> - On push failure, switches back to original branch so repo isn’t left detached
- Returns remote URL and branch name
- Branch already exists
- Push fails (no network, auth error)
Terminal and PTY Sessions
create_pty_session
Creates an interactive pseudo-terminal session.| Param | Type | Description |
|---|---|---|
session_id | String | Unique session ID (UUID from frontend) |
cwd | String | Working directory for PTY |
rows | u16 | Terminal height in rows |
cols | u16 | Terminal width in columns |
command | Option<String> | Agent executable (e.g., “claude”) or None for bare shell |
args | Option<Vec<String>> | Agent arguments (pre-assembled by frontend with resume flags, etc) |
on_event | Channel<PtyOutputPayload> | Tauri channel for streaming output |
state | tauri::State<'_, PtyState> | Global PTY registry |
Result<(), String>
Payload type:
-
Shell resolution: Probes platform for preferred shell (cached in static
SHELL):- Windows: Checks if PowerShell Core (
pwsh) available, falls back topowershell.exe - Unix: Honors
$SHELLenv var, falls back to/bin/bash
- Windows: Checks if PowerShell Core (
- PTY creation: Opens PTY pair with given terminal size
-
Command construction:
- If
commandis None: Bare shell session - If
commandis Some: Agent session- Shell-quotes args (handles spaces, single quotes)
- On Windows:
<shell> -NoLogo -NoExit -Command <agent> <args> - On Unix:
<shell> -c "<agent> <args>; exec $SHELL -i"
- If
- Spawn child: Spawns shell process with full agent command
-
PID persistence: Writes child’s OS PID to
<cwd>/.tempest-pidfor recovery after app restart -
Output streaming: Starts background thread that reads PTY master in 4KB chunks and sends via
on_eventchannel - Registry: Inserts session into PtyState DashMap
- Working directory doesn’t exist
- Shell executable not found
- PTY creation fails (system resource limit)
- Child process spawn fails
write_to_pty
Writes data to PTY stdin.| Param | Type | Description |
|---|---|---|
session_id | String | Session ID to write to |
data | Vec<u8> | Bytes to write (terminal input) |
state | tauri::State<PtyState> | Global PTY registry |
Result<(), String>
Implementation:
- Looks up session in DashMap by session_id
- Writes bytes to PTY master’s writer (stdin)
- No buffering; write is synchronous
- Session not found
- PTY closed or broken pipe
- I/O error
resize_pty
Resizes terminal.| Param | Type | Description |
|---|---|---|
session_id | String | Session ID to resize |
rows | u16 | New terminal height |
cols | u16 | New terminal width |
state | tauri::State<PtyState> | Global PTY registry |
Result<(), String>
Implementation:
- Looks up session in DashMap
- Calls
master.resize(PtySize { rows, cols, pixel_width: 0, pixel_height: 0 }) - Sends SIGWINCH signal to shell process (shell updates internal state)
- Session not found
- Resize fails (closed PTY)
close_pty_session
Closes a PTY session.| Param | Type | Description |
|---|---|---|
session_id | String | Session ID to close |
state | tauri::State<PtyState> | Global PTY registry |
Result<(), String>
Implementation:
- Removes session from DashMap (no new data can arrive after this)
- Kills child process (entire job object on Windows)
- Waits up to 1 second for process to exit (polling every 25ms, 40 attempts)
- Removes
.tempest-pidsidecar file - Does nothing if session already closed
close_and_remove_worktree for that.
Git Hooks (Co-author Attribution)
write_coauthor_hook
Installs prepare-commit-msg hook for co-author attribution.| Param | Type | Description |
|---|---|---|
repo_path | String | Path to git repository |
coauthor_line | String | Co-author trailer (e.g., "Co-Authored-By: Name <email>") |
Result<(), String>
Idempotency: Multiple calls have no extra effect (wrapped in markers).
Implementation:
- Creates
.git/hooks/directory if missing - Builds hook script block wrapped in
# Tempest-attribution-beginand# Tempest-attribution-end - If hook exists but has no Tempest block: Appends block
- If hook exists with block: Overwrites block (idempotent)
- If hook doesn’t exist: Creates with shebang
- On Unix/macOS: Sets executable bit (0o755)
remove_coauthor_hook
Removes Tempest co-author hook block.| Param | Type | Description |
|---|---|---|
repo_path | String | Path to git repository |
Result<(), String>
Implementation:
- Reads
.git/hooks/prepare-commit-msg - Strips lines between Tempest markers
- If nothing remains (or only shebang): Deletes hook file
- Otherwise: Rewrites hook without Tempest block
- No-op if file doesn’t exist or has no Tempest block
IDE Panel Integration
embed_ide_panel
Creates an embedded child webview for live preview or IDE panels.| Param | Type | Description |
|---|---|---|
window | tauri::Window | Main window context |
panel_id | String | Unique identifier for panel (e.g., “preview-abc123”) |
url | String | URL to load (external or app-relative) |
x | f64 | Horizontal position in logical pixels |
y | f64 | Vertical position in logical pixels |
width | f64 | Panel width in logical pixels |
height | f64 | Panel height in logical pixels |
Result<(), String>
TypeScript usage:
resize_ide_panel
Resizes and repositions an IDE panel.embed_ide_panel. No-op if panel doesn’t exist.
destroy_ide_panel
Closes and removes an IDE panel.get_ide_panel_url
Returns the current URL of an IDE panel.Some(url) if panel exists, None otherwise.
Secondary Windows (Zen Mode)
open_zen_window
Opens a secondary frameless window for distraction-free editing.| Param | Type | Description |
|---|---|---|
app | tauri::AppHandle | App context |
state | tauri::State<ZenState> | Zen window metadata storage |
path | String | Project path |
name | String | Project name |
Result<(), String>
Window properties:
- Label:
zen-<timestamp-ms>(unique, based on Unix epoch milliseconds) - Title: “Tempest”
- Decorations: false (frameless)
- Size: 1280x800 (logical pixels)
- Centered on screen
- Drag-drop disabled
- Generates unique label based on current timestamp
- Stores
(path, name)in ZenState under label - Creates new WebviewWindow with
index.htmlentry point - The React component reads label and calls
get_zen_configto retrieve project info
get_zen_config
Retrieves path and name for a secondary window.| Param | Type | Description |
|---|---|---|
state | tauri::State<ZenState> | Zen window metadata storage |
label | String | Window label (from getCurrentWindow().label) |
Option<(String, String)>
Some((path, name))if label existsNoneif label not found
Code Indexing (Atlas)
start_atlas_index
Spawns Node.js process to index project codebase.| Param | Type | Description |
|---|---|---|
app | tauri::AppHandle | App context (for resource paths) |
project_path | String | Path to project to index |
Result<(), String>
Execution: Fire-and-forget. Returns immediately; indexing happens in background.
Implementation:
-
Resolves Atlas entry point path:
- Dev builds:
<cargo-manifest-dir>/resources/atlas/dist/mcp/server-entry.js - Release builds:
<exe-dir>/resources/atlas/dist/mcp/server-entry.js
- Dev builds:
- Validates entry point exists
-
Spawns
node --liftoff-only <entry> --init --path <project>with:- Stdin: null
- Stdout: piped to separate reader thread
- Stderr: piped to separate reader thread
-
Each thread emits
atlas:logevents to frontend with log lines as they arrive - Waits for child process to exit
-
Writes Atlas MCP server config to:
.mcp.json(Claude Code, Cline, Zed, Windsurf).cursor/mcp.json(Cursor).gemini/settings.json(Gemini CLI).kiro/settings/mcp.json(Kiro/AWS)opencode.jsonc(OpenCode)
-
Updates
.gitignorewith config file paths (local-only, not committed)
- Atlas entry point not bundled
- Node.js not in PATH
- Permission denied on project directory
check_atlas_db
Checks if Atlas has indexed a project.| Param | Type | Description |
|---|---|---|
project_path | String | Path to project |
bool
trueif.tempest/atlas/atlas.dbexistsfalseotherwise
Dependencies and Technologies
Core Tauri
- tauri 2 with unstable features and asset protocol support
- url for URL parsing and validation
Terminal Emulation
- portable-pty 0.8 for cross-platform PTY support
- libc 0.2 (Unix) for SIGKILL in process tree cleanup
- junction 1 (Windows) for directory junction creation
State Management
- dashmap 6 for lock-free concurrent HashMap storage
Serialization
- serde 1 with derive macros
- serde_json 1 for JSON serialization