From 2d92ff806c18c5216ab621fa8e93b9eefb6bd5e0 Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Tue, 24 Mar 2026 20:34:52 -0400 Subject: [PATCH] docs: Local PTY Copilot Panel design spec Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-24-local-pty-copilot-design.md | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-24-local-pty-copilot-design.md diff --git a/docs/superpowers/specs/2026-03-24-local-pty-copilot-design.md b/docs/superpowers/specs/2026-03-24-local-pty-copilot-design.md new file mode 100644 index 0000000..e205d95 --- /dev/null +++ b/docs/superpowers/specs/2026-03-24-local-pty-copilot-design.md @@ -0,0 +1,186 @@ +# Local PTY Copilot Panel — Design Spec + +**Date:** 2026-03-24 +**Status:** Approved +**Author:** Claude Opus 4.6 (XO) + +## Problem + +The AI panel is a Gemini API stub (~130 lines backend, ~124 lines frontend) with no OAuth, no conversation history, no tool use. The Commander pays $200/mo Claude Max, $20/mo Gemini, $20/mo ChatGPT — all of which include CLI tool access (Claude Code, Gemini CLI, Codex CLI). These CLIs are designed for terminals. Wraith has a terminal. Ship a local PTY in the sidebar and let the user run whichever CLI they want. + +## Solution + +Replace the Gemini stub with a local PTY terminal in the sidebar panel. Reuse the existing xterm.js infrastructure. User picks a shell (bash, sh, zsh, PowerShell, Git Bash), the panel spawns it locally, and they run `claude`, `gemini`, `codex`, or anything else. + +## Architecture + +### Backend — `src-tauri/src/pty/mod.rs` + +New module following the same patterns as `SshService`: + +``` +PtyService + sessions: DashMap> + +PtySession + id: String + writer: Mutex> // from master.take_writer() + master: Mutex> // kept for resize() + child: Mutex> // for kill/wait + shell_path: String + +PtyService methods: + spawn(shell_path, cols, rows, app_handle) -> Result + write(session_id, data) -> Result<(), String> + resize(session_id, cols, rows) -> Result<(), String> + disconnect(session_id) -> Result<(), String> + list_shells() -> Vec +``` + +Note: `writer` and `master` require `Mutex` wrappers because `portable-pty` trait objects are `Send` but not `Sync`, and the `DashMap` requires `Sync` on stored values. + +**PTY crate:** `portable-pty` — cross-platform (Unix PTY, Windows ConPTY). MIT licensed. Part of the wezterm project. + +**Shell detection** (`list_shells`): +- Unix: check existence of `/bin/bash`, `/bin/sh`, `/bin/zsh`, `$SHELL` +- Windows: `powershell.exe`, `cmd.exe`, plus scan for Git Bash at common paths (`C:\Program Files\Git\bin\bash.exe`, `C:\Program Files (x86)\Git\bin\bash.exe`) +- Return `Vec` with `{ name, path }` pairs + +**Output loop** — spawned per session via `spawn_blocking` (not async — `portable-pty` reader is synchronous `std::io::Read`): +```rust +tokio::task::spawn_blocking(move || { + let mut reader = BufReader::new(pty_reader); + let mut buf = [0u8; 4096]; + loop { + match reader.read(&mut buf) { + Ok(0) => { app.emit("pty:close:{id}", ()); break; } + Ok(n) => { app.emit("pty:data:{id}", base64(&buf[..n])); } + Err(_) => break; + } + } +}); +``` + +`AppHandle::emit()` is synchronous in Tauri v2, so it works from a blocking thread context without issues. + +**Environment:** `CommandBuilder` inherits the parent process environment by default. This is required so that `PATH` includes the user's CLI tools (`claude`, `gemini`, `codex`). No env filtering should be applied. + +### Backend — Tauri Commands (`src-tauri/src/commands/pty_commands.rs`) + +```rust +spawn_local_shell(shell_path: String, cols: u32, rows: u32) -> Result +pty_write(session_id: String, data: String) -> Result<(), String> +pty_resize(session_id: String, cols: u32, rows: u32) -> Result<(), String> +disconnect_pty(session_id: String) -> Result<(), String> +list_available_shells() -> Result, String> +``` + +All registered in `lib.rs` invoke handler. All added to `capabilities/default.json`. + +### Backend — AppState Changes + +```rust +pub struct AppState { + // ... existing fields ... + pub pty: PtyService, // ADD + // pub gemini: Mutex<...>, // DELETE +} +``` + +### Frontend — `src/components/ai/CopilotPanel.vue` + +Replaces `GeminiPanel.vue`. Structure: + +1. **Header bar:** "AI Copilot" title + shell selector dropdown + spawn/kill buttons +2. **Terminal area:** xterm.js instance via `useTerminal` composable (adapted for PTY events) +3. **State:** shell list (from `list_available_shells`), active session ID, connected flag +4. **Close handling:** Listen for `pty:close:{session_id}` events to update `connected` state and show "Session ended — Relaunch?" UI. This differs from the SSH path where tab closure handles cleanup. + +Shell selector is a `