Skip to main content

bob_rs/
run.rs

1//! Build the bob CLI argv and spawn it via the shared streaming engine.
2//!
3//! Both `bob-api` (browser preview HTTP) and `src-tauri` (desktop IPC)
4//! consume this. The generic subprocess engine (`spawn_streaming`, the
5//! process-event type, the run handle) lives in `agent-harness`; this
6//! module is the bob-specific layer on top — the chat-mode / approval
7//! flags, `RunBobOptions`, and injecting bob's `BOBSHELL_API_KEY`.
8
9use crate::error::BobError;
10use crate::keychain::resolve_api_key;
11use cli_stream::{spawn_streaming, ProcessEvent, ProcessHandle};
12use serde::{Deserialize, Serialize};
13use std::path::PathBuf;
14
15// --- Wire shapes (bob-specific) -------------------------------------
16
17/// Bob chat mode CLI flag. `--chat-mode <value>` accepts the snake_case
18/// forms below.
19#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
20#[serde(rename_all = "snake_case")]
21pub enum BobChatMode {
22    Plan,
23    Code,
24    Advanced,
25    Ask,
26}
27
28impl BobChatMode {
29    pub fn as_cli_value(self) -> &'static str {
30        match self {
31            Self::Plan => "plan",
32            Self::Code => "code",
33            Self::Advanced => "advanced",
34            Self::Ask => "ask",
35        }
36    }
37}
38
39/// Bob's approval flow. `default` prompts the user via bob's UI; `yolo`
40/// skips prompts. We only use `default` and `yolo` today (the legacy
41/// `auto_edit` mode kept for back-compat with the existing Tauri command
42/// surface).
43#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
44#[serde(rename_all = "snake_case")]
45pub enum BobApprovalMode {
46    Default,
47    AutoEdit,
48    Yolo,
49}
50
51impl BobApprovalMode {
52    pub fn as_cli_value(self) -> &'static str {
53        match self {
54            Self::Default => "default",
55            Self::AutoEdit => "auto_edit",
56            Self::Yolo => "yolo",
57        }
58    }
59}
60
61/// Options for a single bob run. Built by both the axum endpoint (from
62/// JSON body) and the Tauri command (from invoke args).
63#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
64#[serde(rename_all = "camelCase")]
65pub struct RunBobOptions {
66    pub prompt: String,
67    #[serde(default = "default_chat_mode")]
68    pub chat_mode: BobChatMode,
69    #[serde(default = "default_approval_mode")]
70    pub approval_mode: BobApprovalMode,
71    #[serde(default = "default_max_coins")]
72    pub max_coins: u32,
73    /// Working directory the bob process runs in. Defaults to the
74    /// caller's cwd. For workspace-scoped runs, pass the workspace path
75    /// so bob's tool calls land inside that workspace.
76    pub cwd: Option<PathBuf>,
77    /// Override the bob executable path. Mainly for tests + when the
78    /// caller has already resolved bob (e.g. Tauri's locator). Defaults
79    /// to `bob` on PATH.
80    #[serde(default)]
81    pub bob_executable: Option<PathBuf>,
82}
83
84fn default_chat_mode() -> BobChatMode { BobChatMode::Ask }
85fn default_approval_mode() -> BobApprovalMode { BobApprovalMode::Default }
86fn default_max_coins() -> u32 { 30 }
87
88// --- Spawn ----------------------------------------------------------
89
90/// Spawn bob and stream output through `callback` until the child exits.
91/// Returns a [`ProcessHandle`] immediately — the reader + wait threads
92/// continue in the background.
93///
94/// `run_id` is opaque to bob-rs; the caller chooses the identifier and
95/// uses it to correlate events with the handle.
96pub fn spawn_bob<F>(
97    opts: RunBobOptions,
98    run_id: String,
99    callback: F,
100) -> Result<ProcessHandle, BobError>
101where
102    F: FnMut(ProcessEvent) + Send + Sync + Clone + 'static,
103{
104    let args = build_args(&opts);
105    let api_key = resolve_api_key().map(|(value, _)| value).unwrap_or_default();
106    let program: PathBuf = opts.bob_executable.clone().unwrap_or_else(|| PathBuf::from("bob"));
107    let cwd = opts.cwd.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
108    spawn_bob_raw(program, args, api_key, cwd, run_id, callback)
109}
110
111/// Lower-level spawn for callers that have already built the argv,
112/// resolved the bob executable path, and loaded the API key themselves
113/// (the Tauri runner, which carries its own locator + workspace-aware
114/// argv builder). Thin bob-specific wrapper over
115/// [`cli_stream::spawn_streaming`]: sets bob's `BOBSHELL_API_KEY` env
116/// var, otherwise identical.
117pub fn spawn_bob_raw<F>(
118    program: PathBuf,
119    args: Vec<String>,
120    api_key: String,
121    cwd: PathBuf,
122    run_id: String,
123    callback: F,
124) -> Result<ProcessHandle, BobError>
125where
126    F: FnMut(ProcessEvent) + Send + Sync + Clone + 'static,
127{
128    let handle = spawn_streaming(
129        program,
130        args,
131        vec![("BOBSHELL_API_KEY".to_owned(), api_key)],
132        cwd,
133        run_id,
134        callback,
135    )?; // cli_stream::StreamError → BobError::Stream
136    Ok(handle)
137}
138
139/// Build the bob CLI argv. Mirrors the structure used by both the Vite
140/// `bobRunPlugin` and the Tauri `build_bob_command`.
141fn build_args(opts: &RunBobOptions) -> Vec<String> {
142    vec![
143        opts.prompt.clone(),
144        "--chat-mode".to_owned(),
145        opts.chat_mode.as_cli_value().to_owned(),
146        "--output-format".to_owned(),
147        "stream-json".to_owned(),
148        "--approval-mode".to_owned(),
149        opts.approval_mode.as_cli_value().to_owned(),
150        "--accept-license".to_owned(),
151        "--max-coins".to_owned(),
152        opts.max_coins.to_string(),
153    ]
154}