# aishell A single-stream shell where commands and AI coexist. ## Architecture ``` ┌──────────────────────────────────────────────┐ │ aishell │ │ │ │ ┌──────────┐ judge ┌───────────────┐ │ │ │ rustyline │───────────→│ sh -c command │ │ │ │ (input) │ is_command └───────────────┘ │ │ └──────────┘ │ │ │ !is_command │ │ ▼ │ │ ┌──────────┐ stdin(JSON) ┌──────────────┐ │ │ │ send() │─────────────→│ claude │ │ │ │ (async) │ │ (persistent) │ │ │ └──────────┘ │ stream-json │ │ │ └──────────────┘ │ │ ┌──────────┐ stdout(JSON) │ │ │ │ reader │←────────────────────┘ │ │ │ thread │ mpsc channel │ │ └──────────┘ │ │ │ │ │ ▼ │ │ ┌──────────┐ │ │ │ drain() │──→ println! (unified stream) │ │ └──────────┘ │ │ │ │ ┌───────────────────────────────────────┐ │ │ │ ● [1] responding... | [2] thinking... │ │ │ └───────────────────────────────────────┘ │ └──────────────────────────────────────────────┘ ``` ## Claude Process (ai.rs) ### Startup One persistent process for the entire session: ``` claude --input-format stream-json --output-format stream-json \ --verbose --dangerously-skip-permissions ``` - `--input-format stream-json`: accepts JSON lines on stdin - `--output-format stream-json`: emits JSON events on stdout - Process stays alive until aishell exits. No restart per message. ### Input Format ```json {"type":"user","message":{"role":"user","content":"user input here"}} ``` Written to claude's stdin via `Arc>`. ### Output Events ``` type: "system" → init event (tools list, MCP servers, model info) type: "assistant" → response content content[].type: "text" → AI text (accumulated) content[].type: "tool_use" → tool execution (name shown in status) type: "result" → turn complete (final text, cost, usage) ``` ### Thread Model ``` Main Thread Background Thread │ │ ├─ readline() ├─ BufReader::lines() on claude stdout ├─ judge::is_command() ├─ serde_json::from_str() each line ├─ command → executor::execute() ├─ "assistant" → accumulate text, update status ├─ AI → claude.send() (non-blocking) ├─ "tool_use" → update status ├─ drain_responses() before prompt ├─ "result" → mpsc::send() completed text └─ status.set(status_line()) └─ remove session from status vec Polling Thread (200ms interval) ├─ try_recv() completed responses → print immediately └─ update status bar ``` ### Shared State ```rust stdin: Arc> // main → claude stdin writes status: Arc>> // both threads read/write status output_tx: mpsc::Sender // background → main completed responses output_rx: mpsc::Receiver // main drains with try_recv() id_tx: mpsc::Sender // main → background session ID assignment ``` ### Non-blocking Send ```rust pub fn send(&mut self, input: &str) -> usize { let id = self.next_id; // assign session ID self.next_id += 1; status.push(SessionStatus { id, state: "thinking..." }); self.id_tx.send(id); // notify reader thread writeln!(stdin, "{}", json); // write JSON to claude stdin stdin.flush(); // returns immediately — does NOT wait for response id } ``` ## Input Detection (judge.rs) Priority order: 1. Shell operators (`|`, `>`, `<`, `;`, `&`) outside quotes → shell 2. Variable assignment (`FOO=bar`) → shell 3. Shell builtins (cd, echo, export, etc. — 50 builtins) → shell 4. Absolute/relative path to existing file → shell 5. Command found in PATH → shell 6. **None of the above → AI** Quote-aware: operators inside `'...'` or `"..."` are ignored. ## Command Execution (executor.rs) - `cd` → `env::set_current_dir()` (changes process directory) - `cd -` → OLDPWD support, `~` → HOME expansion - Everything else → `sh -c "input"` (pipes, redirects, globs all work) ## Status Bar (status.rs) Terminal last line reserved for Claude status: ``` ● idle ← waiting ● [1] thinking... ← processing ● [1] responding... ← generating text ● [1] running: Bash... ← executing tool ● [1] responding... | [2] thinking... ← multiple sessions ``` Implementation: - ANSI escape `\x1b[1;{rows-1}r` sets scroll region excluding last line - `\x1b7` save cursor → draw status on last row → `\x1b8` restore cursor - Auto-cleanup on Drop (reset scroll region) ## Files ``` src/ ├── main.rs Main loop: input → judge → execute/AI → drain responses ├── lib.rs Module declarations ├── ai.rs ClaudeManager: persistent process, async send/receive ├── judge.rs is_command(): input classification (6 tests) ├── executor.rs execute(): sh -c + cd handling └── status.rs StatusBar: ANSI last-line status display ``` ## Dependencies ```toml rustyline = "14.0" # line input + history serde = "1" # JSON serialization serde_json = "1" # stream-json protocol parsing terminal_size = "0.4" # terminal dimensions for status bar ```