diff --git a/frontend/packages/inspector/src/App.tsx b/frontend/packages/inspector/src/App.tsx index 2dfe24dc..94d41304 100644 --- a/frontend/packages/inspector/src/App.tsx +++ b/frontend/packages/inspector/src/App.tsx @@ -773,7 +773,7 @@ export default function App() { setSessionError(null); }; - const createNewSession = async (nextAgentId: string, config: { agentMode: string; model: string }) => { + const createNewSession = async (nextAgentId: string, config: { agentMode: string; model: string; cwd: string }) => { console.log("[createNewSession] Creating session for agent:", nextAgentId, "config:", config); setSessionError(null); creatingSessionRef.current = true; @@ -784,7 +784,7 @@ export default function App() { const createSessionPromise = getClient().createSession({ agent: nextAgentId, sessionInit: { - cwd: "/", + cwd: config.cwd, mcpServers: [], }, }); diff --git a/frontend/packages/inspector/src/components/SessionCreateMenu.tsx b/frontend/packages/inspector/src/components/SessionCreateMenu.tsx index e952890f..fd1a4e7b 100644 --- a/frontend/packages/inspector/src/components/SessionCreateMenu.tsx +++ b/frontend/packages/inspector/src/components/SessionCreateMenu.tsx @@ -8,9 +8,16 @@ type AgentModelInfo = { id: string; name?: string }; export type SessionConfig = { agentMode: string; model: string; + cwd: string; }; const CUSTOM_MODEL_VALUE = "__custom__"; +const DEFAULT_CWD = "/"; +const LAST_CWD_KEY = "sandbox-agent-inspector-last-cwd"; + +type InspectorRuntimeConfig = { + defaultCwd?: string; +}; const agentLabels: Record = { claude: "Claude Code", @@ -29,6 +36,56 @@ const agentLogos: Record = { pi: `${import.meta.env.BASE_URL}logos/pi.svg`, }; +function normalizeCwd(value: string | null | undefined) { + if (!value) { + return null; + } + + const trimmed = value.trim(); + return trimmed ? trimmed : null; +} + +function getQueryDefaultCwd() { + if (typeof window === "undefined") { + return null; + } + + const params = new URLSearchParams(window.location.search); + return normalizeCwd(params.get("cwd")) ?? normalizeCwd(params.get("defaultCwd")); +} + +function getRuntimeDefaultCwd() { + if (typeof window === "undefined") { + return null; + } + + const runtimeWindow = window as typeof window & { + __SANDBOX_AGENT_INSPECTOR_CONFIG__?: InspectorRuntimeConfig; + }; + return normalizeCwd(runtimeWindow.__SANDBOX_AGENT_INSPECTOR_CONFIG__?.defaultCwd); +} + +function getStoredCwd() { + if (typeof window === "undefined") { + return null; + } + + try { + return normalizeCwd(window.localStorage.getItem(LAST_CWD_KEY)); + } catch {} + + return null; +} + +function getInitialCwd() { + return ( + getQueryDefaultCwd() ?? + getRuntimeDefaultCwd() ?? + getStoredCwd() ?? + DEFAULT_CWD + ); +} + const SessionCreateMenu = ({ agents, agentsLoading, @@ -58,6 +115,7 @@ const SessionCreateMenu = ({ const [selectedModel, setSelectedModel] = useState(""); const [customModel, setCustomModel] = useState(""); const [isCustomModel, setIsCustomModel] = useState(false); + const [cwd, setCwd] = useState(getInitialCwd); const [creating, setCreating] = useState(false); // Reset state when menu closes @@ -69,6 +127,7 @@ const SessionCreateMenu = ({ setSelectedModel(""); setCustomModel(""); setIsCustomModel(false); + setCwd(getInitialCwd()); setCreating(false); } }, [open]); @@ -138,12 +197,17 @@ const SessionCreateMenu = ({ }; const resolvedModel = isCustomModel ? customModel : selectedModel; + const resolvedCwd = cwd.trim() || getInitialCwd(); const handleCreate = async () => { if (!selectedAgent) return; setCreating(true); try { - await onCreateSession(selectedAgent, { agentMode, model: resolvedModel }); + try { + window.localStorage.setItem(LAST_CWD_KEY, resolvedCwd); + } catch {} + + await onCreateSession(selectedAgent, { agentMode, model: resolvedModel, cwd: resolvedCwd }); onClose(); } catch (error) { console.error("[SessionCreateMenu] Failed to create session:", error); @@ -286,6 +350,19 @@ const SessionCreateMenu = ({ )} +
+ Working directory + setCwd(e.target.value)} + placeholder={DEFAULT_CWD} + spellCheck={false} + autoCapitalize="off" + autoCorrect="off" + /> +
diff --git a/server/packages/sandbox-agent/src/cli.rs b/server/packages/sandbox-agent/src/cli.rs index b1fc6bb5..b5507e8e 100644 --- a/server/packages/sandbox-agent/src/cli.rs +++ b/server/packages/sandbox-agent/src/cli.rs @@ -93,6 +93,9 @@ pub struct ServerArgs { #[arg(long, short = 'p', default_value_t = DEFAULT_PORT)] port: u16, + #[arg(long = "inspector-default-cwd")] + inspector_default_cwd: Option, + #[arg(long = "cors-allow-origin", short = 'O')] cors_allow_origin: Vec, @@ -421,6 +424,8 @@ fn run_server(cli: &CliConfig, server: &ServerArgs) -> Result<(), CliError> { let agent_manager = AgentManager::new(default_install_dir()) .map_err(|err| CliError::Server(err.to_string()))?; + ui::configure_default_cwd(server.inspector_default_cwd.clone()); + let state = Arc::new(AppState::with_branding(auth, agent_manager, branding)); let (mut router, state) = build_router_with_state(state); diff --git a/server/packages/sandbox-agent/src/ui.rs b/server/packages/sandbox-agent/src/ui.rs index c2c27efe..0d88e3b2 100644 --- a/server/packages/sandbox-agent/src/ui.rs +++ b/server/packages/sandbox-agent/src/ui.rs @@ -1,4 +1,5 @@ use std::path::Path; +use std::sync::OnceLock; use axum::body::Body; use axum::extract::Path as AxumPath; @@ -9,10 +10,18 @@ use axum::Router; include!(concat!(env!("OUT_DIR"), "/inspector_assets.rs")); +static INSPECTOR_DEFAULT_CWD: OnceLock = OnceLock::new(); + pub fn is_enabled() -> bool { INSPECTOR_ENABLED } +pub fn configure_default_cwd(value: Option) { + if let Some(value) = normalize_cwd(value) { + let _ = INSPECTOR_DEFAULT_CWD.set(value); + } +} + pub fn router() -> Router { if !INSPECTOR_ENABLED { return Router::new() @@ -72,6 +81,10 @@ fn serve_path(path: &str) -> Response { } fn file_response(file: &include_dir::File) -> Response { + if file.path().file_name().and_then(|name| name.to_str()) == Some("index.html") { + return index_response(file); + } + let mut response = Response::new(Body::from(file.contents().to_vec())); *response.status_mut() = StatusCode::OK; let content_type = content_type_for(file.path()); @@ -80,6 +93,58 @@ fn file_response(file: &include_dir::File) -> Response { response } +fn index_response(file: &include_dir::File) -> Response { + let html = String::from_utf8_lossy(file.contents()); + let config_json = serde_json::json!({ + "defaultCwd": resolve_default_cwd(), + }) + .to_string(); + let config_script = format!( + r#""#, + config_json + ); + + let body = if let Some(position) = html.find("") { + let mut injected = String::with_capacity(html.len() + config_script.len()); + injected.push_str(&html[..position]); + injected.push_str(&config_script); + injected.push_str(&html[position..]); + injected + } else { + let mut injected = html.into_owned(); + injected.push_str(&config_script); + injected + }; + + let mut response = Response::new(Body::from(body)); + *response.status_mut() = StatusCode::OK; + response.headers_mut().insert( + header::CONTENT_TYPE, + HeaderValue::from_static("text/html; charset=utf-8"), + ); + response +} + +fn resolve_default_cwd() -> String { + INSPECTOR_DEFAULT_CWD + .get() + .cloned() + .or_else(|| normalize_cwd(std::env::var("SANDBOX_AGENT_INSPECTOR_DEFAULT_CWD").ok())) + .or_else(|| normalize_cwd(std::env::var("HOME").ok())) + .unwrap_or_else(|| "/".to_string()) +} + +fn normalize_cwd(value: Option) -> Option { + value.and_then(|value| { + let trimmed = value.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } + }) +} + fn content_type_for(path: &Path) -> &'static str { match path.extension().and_then(|ext| ext.to_str()) { Some("html") => "text/html; charset=utf-8",