Building a Native AI IDE in 3 Days — Solo at Startup Week Buildathon

February 22, 2026

rust · tauri · hackathon · ai-agent · desktop-app · typescript

TL;DR: Solo IDE is a native desktop IDE (Rust/Tauri 2) where an AI agent works on isolated git worktrees while you keep coding on yours. I pushed it through a 3-day hackathon — rewrote the entire agent layer, built a credential vault, fixed cross-repo session leaks, and shipped voice I/O. 46 commits, ~40K lines of changes, a lot of coffee.


Most Hackathon Projects Start From Scratch. Ours Didn't.

Here's the thing about hackathons — starting from zero is actually easier. You scaffold a project, pick your stack, write clean code from the beginning. No legacy decisions to navigate, no existing abstractions that don't fit, no merge conflicts with yourself from last week.

Solo IDE had been in active development for months when I entered the Startup Week Buildathon. The codebase already had a Rust backend, a React frontend, a panel system, a file explorer, a terminal emulator, and an AI agent layer that... kind of worked. The agent ran on Rust-native HTTP calls, credentials were scattered across separate keychain entries, and switching repos leaked agent sessions everywhere.

The hackathon wasn't "build something." It was "make this thing actually impressive in 72 hours."

What Solo Actually Is

Quick primer if you haven't read my previous deep-dive on the Claude SDK OAuth architecture: Solo is a native desktop IDE built on Tauri 2 with a Rust backend and React 19 frontend. The core idea is that the AI agent isn't a sidebar chatbot — it's a first-class workspace participant. It creates its own git worktrees, works on isolated branches, and operates through a permission system where you approve or deny every tool call.

The stack:

  • Rust backend — file system, git operations, terminal PTY, credential vault, ElevenLabs voice proxy
  • Node.js sidecar — bridges to the Claude Agent SDK (TypeScript-only)
  • React 19 frontend — Zustand stores, Mosaic panel layout, Monaco editor
  • ~15MB binary — 60-100MB idle RAM

The 72-Hour Decisions

Three days, four major decisions. Each one had downstream consequences I didn't fully anticipate.

Replacing the Agent Layer Mid-Hackathon

Day 1, hour 3.

The existing agent layer made direct HTTP calls to Claude's API from Rust. It worked for basic chat, but the Claude Agent SDK — with its tool loop, permission system, streaming, and session management — only exists in TypeScript. I'd been planning to write Rust bindings "eventually," but "eventually" doesn't exist in a hackathon.

The decision: spawn a Node.js child process as a sidecar, communicate over stdin/stdout using newline-delimited JSON.

// apps/desktop/src-tauri/src/agent/bridge.rs
let mut child = Command::new("node")
    .arg(node_script_path)
    .stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .stderr(Stdio::inherit())
    .spawn()?;

let stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();

The Rust side sends JSON requests via stdin, reads JSON responses from stdout on a dedicated thread, and routes async events (agent messages, permission requests) through a crossbeam channel. The Node.js side is a readline loop that dispatches to a session manager.

78 files changed in one commit. The entire agent layer, rewritten. But it gave us the full Claude Agent SDK — tool use, extended thinking, plan mode, accept mode, tool policies — in a single afternoon.

The pattern is genuinely good. stdin/stdout IPC is dead simple, the processes are isolated, and if the sidecar crashes, the main app keeps running. I'd start here if I did it again.

Unified Credential Vault on macOS Keychain

Before the hackathon, credentials were a mess. Anthropic API key in one keychain entry, OpenAI key in another, OAuth tokens in a third, ElevenLabs somewhere else. Each one triggered a separate macOS ACL prompt when the app binary changed — which happens on every compile during development. Six prompts every time I rebuilt. Maddening.

I consolidated everything into a single keychain entry — one JSON blob containing all credentials under namespaced keys like anthropic.apiKey, openai.oauth, github.oauth:

// crates/solo-auth/src/credentials.rs
const VAULT_SERVICE: &str = "com.solo-ide.credentials";
const VAULT_ACCOUNT: &str = "vault";

The interesting part is the rollback pattern. If the keychain write fails after the in-memory cache is already updated, you've got an inconsistency. The vault thinks a credential is saved when it isn't persisted:

async fn vault_set(&self, key: &str, value: &str) -> ProviderResult<()> {
    self.load_vault().await?;
    let mut guard = self.vault.write().await;
    let vault = guard.get_or_insert_with(HashMap::new);
    let old_value = vault.insert(key.to_string(), value.to_string());

    if let Err(e) = Self::persist_vault(vault) {
        // Rollback: restore previous in-memory state
        match old_value {
            Some(v) => { vault.insert(key.to_string(), v); }
            None    => { vault.remove(key); }
        }
        return Err(e);
    }
    Ok(())
}

Pre-warm on startup (one async keychain read before the frontend mounts), double-checked locking to prevent concurrent reads, and a partial recovery path that tries to salvage string values from corrupted JSON. One keychain prompt instead of six.

Cross-Repo Session Isolation

This was a proper bug hunt. Opening two different repos in Solo and switching between them caused agent sessions from repo A to appear in repo B. Worse, sending a message in a stale session would confuse the agent about which codebase it was working in.

The root cause was simple — sessions had no workspace scoping. The fix was a two-level filter applied everywhere sessions appear:

// apps/desktop/src/components/agent/SessionList.tsx
const scopedSessions = useMemo(() => {
  return sessions.filter((session) => {
    // Level 1: filter by workspace path (prevents cross-repo bleed)
    if (currentRootPath && session.workspacePath) {
      if (!session.workspacePath.startsWith(currentRootPath)) {
        return false;
      }
    }
    // Level 2: filter by worktree context
    if (activeWorktreeId) {
      return session.worktreeId === activeWorktreeId || !session.worktreeId;
    }
    return !session.worktreeId;
  });
}, [sessions, activeWorktreeId, currentRootPath]);

The startsWith check is deliberate — worktree paths are children of the repo root (/project/.git/worktrees/feat-xyz), so prefix matching catches both the main workspace and all its worktrees.

This same filter had to be applied in SessionList.tsx, useAgentSession.ts, and the workspace store. Two competing PRs tried to fix this simultaneously — one for accordion-style repo switching, another for worktree state management — and I had to do a hybrid merge to reconcile them. That was not fun at 2am.

Voice I/O in 44 Files

The final push was ElevenLabs integration — speech-to-text for input, text-to-speech for agent responses, with live waveform visualization. 44 files touched in one commit across both Rust and TypeScript.

I wrote a separate post on the STT debugging journey (spoiler: macOS was silently giving me 48kHz audio when I asked for 16kHz), so I won't repeat it here. But the TTS side had its own surprise — the stop button would revert the UI before the audio actually finished playing because AudioContext.close() is async and I wasn't waiting for it.

Making an IDE talk back to you is weird. But for the solopreneur use case — someone who thinks in spoken language, not typed commands — it's the right interaction model.

What I'd Do Differently

Start with the sidecar pattern from day one. I spent weeks building a Rust-native agent layer that got thrown away in an afternoon. The stdin/stdout JSON protocol with a Node.js sidecar is genuinely better for wrapping TypeScript-only SDKs, and it's more maintainable than trying to reimplement an SDK in another language.

Design the credential vault before adding the second provider. Adding providers one at a time meant each got its own keychain entry, and consolidating later required migration logic and sentinel tracking. If I'd started with the unified vault, that's code I never would've needed to write.

Write session scoping tests before multi-repo support. The cross-repo session leak was subtle — everything worked fine with one repo open. The bug only manifested when switching workspaces. An integration test that opens two repos and verifies session isolation would have caught this immediately, not during a hackathon at 2am.

Key Takeaways

  1. Brownfield hackathons are harder than greenfield — but you ship more. Starting from an existing codebase means inheriting every shortcut and tech debt from previous development. But we had a working IDE with terminal, file explorer, and editor before the clock started.

  2. Tauri 2's sidecar pattern is underrated. Spawning a Node.js process from Rust with stdin/stdout IPC gives you access to any JavaScript SDK while keeping the main app native. Process isolation means one can crash without taking the other down.

  3. Typed IPC is insurance. ts-rs auto-generates TypeScript bindings from Rust structs. Every time I changed a protocol type, the frontend compiler caught the breakage immediately. In a hackathon, that speed of feedback is everything.

  4. One keychain entry beats many. On macOS, every separate keychain entry triggers an ACL prompt when the binary changes. For development (where the binary changes on every compile), this is maddening. One JSON blob, one entry, one prompt.

  5. Session isolation is a cross-cutting concern. If your app supports multiple workspaces, session scoping needs to be designed as a system-wide filter from the start, not patched into individual components after the bugs show up.


Solo IDE was built for the 2026 Startup Week Buildathon. The codebase is going private — if you want early download access, sign up for the waitlist at solo-build.com. If you're building Tauri apps with AI integration, the sidecar pattern is worth stealing.