diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 72fd5c6e..f5e9d12c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -249,7 +249,7 @@ Full agent mode — spawns the selected local CLI agent as a subprocess with too 4. **Agent adapters** — Shared prompts are mode-aware on every turn, including turns with note context snapshots: Vault Safe tells agents not to use or advertise shell, while Power User tells shell-capable agents to keep local commands scoped to the active vault. Claude Code still uses `claude_cli.rs` with `acceptEdits`, strict Tolaria MCP config, and a scoped tool list: Safe enables file/search/edit tools only, while Power User adds Bash to the available tools and pre-approves Bash with `--allowedTools` without using dangerous bypass flags. Codex runtime specifics live in `codex_cli.rs`; Safe runs `codex --sandbox read-only --ask-for-approval untrusted exec --json`, while Power User runs `codex --sandbox workspace-write --ask-for-approval never exec --json` so shell execution stays enabled across repeated turns. OpenCode runs through `opencode run --format json` with transient permissions: Safe denies bash and external directories, while Power User allows bash but still denies external directories. Pi runs through `pi --mode json --no-session` with `npm:pi-mcp-adapter`; both modes currently share the same transient MCP config and the prompt does not promise shell for Pi Power User. Gemini runs through `gemini --output-format stream-json --prompt` so assistant message chunks, tool calls, and final errors are mapped from the CLI event stream instead of relying on a buffered `response` field. Gemini Safe uses `auto_edit` plus `tools.exclude=["run_shell_command"]`; Power User intentionally uses `yolo` against a trusted transient Tolaria MCP entry. Codex, OpenCode, Pi, and Gemini all launch from the active vault cwd with closed stdin and transient MCP config. All app-launched paths use hidden Windows launches and avoid dangerous permission-bypass flags. 5. **MCP Integration** — Claude receives the generated MCP config file path, Codex receives the same Tolaria MCP server via transient `-c mcp_servers.tolaria.*` config overrides using Tolaria's resolved Node path plus `VAULT_PATH` and `WS_UI_PORT`, OpenCode receives it through `OPENCODE_CONFIG_CONTENT`, Pi receives it through a temporary `PI_CODING_AGENT_DIR/mcp.json` consumed by `pi-mcp-adapter`, and Gemini receives it through a temporary settings file pointed at by `GEMINI_CLI_SYSTEM_SETTINGS_PATH` -CLI-agent availability intentionally does not depend only on the desktop app's inherited `PATH`. The detectors check the current process path, the user's login shell, and supported local/toolchain install locations such as native `~/.local/bin`, local `~/.claude/local`, Mise/asdf shims, nvm-managed Node installs, npm-global, Homebrew, Windows `%APPDATA%\npm`/pnpm/Scoop shims, Windows `.exe` launchers, and the macOS Codex app resource path so first-run onboarding works on fresh macOS and Windows installs. App-managed CLI spawns also extend the child process `PATH` with the resolved binary directory plus those common toolchain directories, which lets GUI-launched macOS sessions run Homebrew/npm shims and their `node`-backed MCP subprocesses even when Finder/Dock did not inherit a terminal shell path. +CLI-agent availability intentionally does not depend only on the desktop app's inherited `PATH`. The detectors check the current process path, the user's login shell, and supported local/toolchain install locations such as native `~/.local/bin`, local `~/.claude/local`, Mise/asdf shims, nvm-managed Node installs, npm-global, Homebrew, Windows `%APPDATA%\npm`/pnpm/Scoop shims, Windows `.exe` launchers, and the macOS Codex app resource path so first-run onboarding works on fresh macOS and Windows installs. App-managed CLI spawns also expand the active vault path before using it as the subprocess working directory, then extend the child process `PATH` with the resolved binary directory plus those common toolchain directories, which lets GUI-launched macOS sessions run Homebrew/npm shims and their `node`-backed MCP subprocesses even when Finder/Dock did not inherit a terminal shell path. #### Agent Event Flow diff --git a/src-tauri/src/commands/ai.rs b/src-tauri/src/commands/ai.rs index 83fddbfa..1020f20a 100644 --- a/src-tauri/src/commands/ai.rs +++ b/src-tauri/src/commands/ai.rs @@ -82,12 +82,25 @@ define_desktop_stream_command!( ); #[cfg(desktop)] -define_desktop_stream_command!( - stream_ai_agent, - AiAgentStreamRequest, - "ai-agent-stream", - crate::ai_agents::run_ai_agent_stream -); +fn normalize_agent_request(mut request: AiAgentStreamRequest) -> AiAgentStreamRequest { + request.vault_path = expand_tilde(&request.vault_path).into_owned(); + request +} + +#[cfg(desktop)] +#[tauri::command] +pub async fn stream_ai_agent( + app_handle: tauri::AppHandle, + request: AiAgentStreamRequest, +) -> Result { + run_desktop_stream( + app_handle, + "ai-agent-stream", + normalize_agent_request(request), + crate::ai_agents::run_ai_agent_stream, + ) + .await +} // ── Claude CLI (mobile stubs) ─────────────────────────────────────────────── @@ -150,6 +163,47 @@ mod tests { use super::*; use crate::vault::AiGuidanceFileState; + #[cfg(desktop)] + #[test] + fn normalize_agent_request_expands_tilde_in_vault_path() { + use crate::ai_agents::AiAgentId; + + let home = dirs::home_dir().unwrap(); + let request = AiAgentStreamRequest { + agent: AiAgentId::ClaudeCode, + message: "hi".into(), + system_prompt: None, + vault_path: "~/Vaults/content".into(), + permission_mode: None, + }; + + let normalized = normalize_agent_request(request); + + assert_eq!( + normalized.vault_path, + format!("{}/Vaults/content", home.display()), + "vault_path must be tilde-expanded so spawned agents can chdir into it", + ); + } + + #[cfg(desktop)] + #[test] + fn normalize_agent_request_leaves_absolute_vault_path_untouched() { + use crate::ai_agents::AiAgentId; + + let request = AiAgentStreamRequest { + agent: AiAgentId::Codex, + message: "hi".into(), + system_prompt: None, + vault_path: "/Users/example/vault".into(), + permission_mode: None, + }; + + let normalized = normalize_agent_request(request); + + assert_eq!(normalized.vault_path, "/Users/example/vault"); + } + #[test] fn guidance_commands_report_and_restore_vault_guidance_files() { let dir = tempfile::TempDir::new().unwrap();