Skip to content

Commit b2195ca

Browse files
committed
Fix inspector CWD handling
Add an editable working directory field, default to ~/, and pass it to createSession instead of hardcoding /.
1 parent 3d9476e commit b2195ca

4 files changed

Lines changed: 150 additions & 3 deletions

File tree

frontend/packages/inspector/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,7 @@ export default function App() {
773773
setSessionError(null);
774774
};
775775

776-
const createNewSession = async (nextAgentId: string, config: { agentMode: string; model: string }) => {
776+
const createNewSession = async (nextAgentId: string, config: { agentMode: string; model: string; cwd: string }) => {
777777
console.log("[createNewSession] Creating session for agent:", nextAgentId, "config:", config);
778778
setSessionError(null);
779779
creatingSessionRef.current = true;
@@ -784,7 +784,7 @@ export default function App() {
784784
const createSessionPromise = getClient().createSession({
785785
agent: nextAgentId,
786786
sessionInit: {
787-
cwd: "/",
787+
cwd: config.cwd,
788788
mcpServers: [],
789789
},
790790
});

frontend/packages/inspector/src/components/SessionCreateMenu.tsx

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,16 @@ type AgentModelInfo = { id: string; name?: string };
88
export type SessionConfig = {
99
agentMode: string;
1010
model: string;
11+
cwd: string;
1112
};
1213

1314
const CUSTOM_MODEL_VALUE = "__custom__";
15+
const DEFAULT_CWD = "/";
16+
const LAST_CWD_KEY = "sandbox-agent-inspector-last-cwd";
17+
18+
type InspectorRuntimeConfig = {
19+
defaultCwd?: string;
20+
};
1421

1522
const agentLabels: Record<string, string> = {
1623
claude: "Claude Code",
@@ -29,6 +36,56 @@ const agentLogos: Record<string, string> = {
2936
pi: `${import.meta.env.BASE_URL}logos/pi.svg`,
3037
};
3138

39+
function normalizeCwd(value: string | null | undefined) {
40+
if (!value) {
41+
return null;
42+
}
43+
44+
const trimmed = value.trim();
45+
return trimmed ? trimmed : null;
46+
}
47+
48+
function getQueryDefaultCwd() {
49+
if (typeof window === "undefined") {
50+
return null;
51+
}
52+
53+
const params = new URLSearchParams(window.location.search);
54+
return normalizeCwd(params.get("cwd")) ?? normalizeCwd(params.get("defaultCwd"));
55+
}
56+
57+
function getRuntimeDefaultCwd() {
58+
if (typeof window === "undefined") {
59+
return null;
60+
}
61+
62+
const runtimeWindow = window as typeof window & {
63+
__SANDBOX_AGENT_INSPECTOR_CONFIG__?: InspectorRuntimeConfig;
64+
};
65+
return normalizeCwd(runtimeWindow.__SANDBOX_AGENT_INSPECTOR_CONFIG__?.defaultCwd);
66+
}
67+
68+
function getStoredCwd() {
69+
if (typeof window === "undefined") {
70+
return null;
71+
}
72+
73+
try {
74+
return normalizeCwd(window.localStorage.getItem(LAST_CWD_KEY));
75+
} catch {}
76+
77+
return null;
78+
}
79+
80+
function getInitialCwd() {
81+
return (
82+
getQueryDefaultCwd() ??
83+
getRuntimeDefaultCwd() ??
84+
getStoredCwd() ??
85+
DEFAULT_CWD
86+
);
87+
}
88+
3289
const SessionCreateMenu = ({
3390
agents,
3491
agentsLoading,
@@ -58,6 +115,7 @@ const SessionCreateMenu = ({
58115
const [selectedModel, setSelectedModel] = useState("");
59116
const [customModel, setCustomModel] = useState("");
60117
const [isCustomModel, setIsCustomModel] = useState(false);
118+
const [cwd, setCwd] = useState(getInitialCwd);
61119
const [creating, setCreating] = useState(false);
62120

63121
// Reset state when menu closes
@@ -69,6 +127,7 @@ const SessionCreateMenu = ({
69127
setSelectedModel("");
70128
setCustomModel("");
71129
setIsCustomModel(false);
130+
setCwd(getInitialCwd());
72131
setCreating(false);
73132
}
74133
}, [open]);
@@ -138,12 +197,17 @@ const SessionCreateMenu = ({
138197
};
139198

140199
const resolvedModel = isCustomModel ? customModel : selectedModel;
200+
const resolvedCwd = cwd.trim() || getInitialCwd();
141201

142202
const handleCreate = async () => {
143203
if (!selectedAgent) return;
144204
setCreating(true);
145205
try {
146-
await onCreateSession(selectedAgent, { agentMode, model: resolvedModel });
206+
try {
207+
window.localStorage.setItem(LAST_CWD_KEY, resolvedCwd);
208+
} catch {}
209+
210+
await onCreateSession(selectedAgent, { agentMode, model: resolvedModel, cwd: resolvedCwd });
147211
onClose();
148212
} catch (error) {
149213
console.error("[SessionCreateMenu] Failed to create session:", error);
@@ -286,6 +350,19 @@ const SessionCreateMenu = ({
286350
</select>
287351
</div>
288352
)}
353+
<div className="setup-field">
354+
<span className="setup-label">Working directory</span>
355+
<input
356+
className="setup-input mono"
357+
type="text"
358+
value={cwd}
359+
onChange={(e) => setCwd(e.target.value)}
360+
placeholder={DEFAULT_CWD}
361+
spellCheck={false}
362+
autoCapitalize="off"
363+
autoCorrect="off"
364+
/>
365+
</div>
289366
</div>
290367

291368
<div className="session-create-actions">

server/packages/sandbox-agent/src/cli.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ pub struct ServerArgs {
9393
#[arg(long, short = 'p', default_value_t = DEFAULT_PORT)]
9494
port: u16,
9595

96+
#[arg(long = "inspector-default-cwd")]
97+
inspector_default_cwd: Option<String>,
98+
9699
#[arg(long = "cors-allow-origin", short = 'O')]
97100
cors_allow_origin: Vec<String>,
98101

@@ -421,6 +424,8 @@ fn run_server(cli: &CliConfig, server: &ServerArgs) -> Result<(), CliError> {
421424

422425
let agent_manager = AgentManager::new(default_install_dir())
423426
.map_err(|err| CliError::Server(err.to_string()))?;
427+
ui::configure_default_cwd(server.inspector_default_cwd.clone());
428+
424429
let state = Arc::new(AppState::with_branding(auth, agent_manager, branding));
425430
let (mut router, state) = build_router_with_state(state);
426431

server/packages/sandbox-agent/src/ui.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::path::Path;
2+
use std::sync::OnceLock;
23

34
use axum::body::Body;
45
use axum::extract::Path as AxumPath;
@@ -9,10 +10,18 @@ use axum::Router;
910

1011
include!(concat!(env!("OUT_DIR"), "/inspector_assets.rs"));
1112

13+
static INSPECTOR_DEFAULT_CWD: OnceLock<String> = OnceLock::new();
14+
1215
pub fn is_enabled() -> bool {
1316
INSPECTOR_ENABLED
1417
}
1518

19+
pub fn configure_default_cwd(value: Option<String>) {
20+
if let Some(value) = normalize_cwd(value) {
21+
let _ = INSPECTOR_DEFAULT_CWD.set(value);
22+
}
23+
}
24+
1625
pub fn router() -> Router {
1726
if !INSPECTOR_ENABLED {
1827
return Router::new()
@@ -72,6 +81,10 @@ fn serve_path(path: &str) -> Response {
7281
}
7382

7483
fn file_response(file: &include_dir::File) -> Response {
84+
if file.path().file_name().and_then(|name| name.to_str()) == Some("index.html") {
85+
return index_response(file);
86+
}
87+
7588
let mut response = Response::new(Body::from(file.contents().to_vec()));
7689
*response.status_mut() = StatusCode::OK;
7790
let content_type = content_type_for(file.path());
@@ -80,6 +93,58 @@ fn file_response(file: &include_dir::File) -> Response {
8093
response
8194
}
8295

96+
fn index_response(file: &include_dir::File) -> Response {
97+
let html = String::from_utf8_lossy(file.contents());
98+
let config_json = serde_json::json!({
99+
"defaultCwd": resolve_default_cwd(),
100+
})
101+
.to_string();
102+
let config_script = format!(
103+
r#"<script>window.__SANDBOX_AGENT_INSPECTOR_CONFIG__={};</script>"#,
104+
config_json
105+
);
106+
107+
let body = if let Some(position) = html.find("</head>") {
108+
let mut injected = String::with_capacity(html.len() + config_script.len());
109+
injected.push_str(&html[..position]);
110+
injected.push_str(&config_script);
111+
injected.push_str(&html[position..]);
112+
injected
113+
} else {
114+
let mut injected = html.into_owned();
115+
injected.push_str(&config_script);
116+
injected
117+
};
118+
119+
let mut response = Response::new(Body::from(body));
120+
*response.status_mut() = StatusCode::OK;
121+
response.headers_mut().insert(
122+
header::CONTENT_TYPE,
123+
HeaderValue::from_static("text/html; charset=utf-8"),
124+
);
125+
response
126+
}
127+
128+
fn resolve_default_cwd() -> String {
129+
INSPECTOR_DEFAULT_CWD
130+
.get()
131+
.cloned()
132+
.or_else(|| normalize_cwd(std::env::var("SANDBOX_AGENT_INSPECTOR_DEFAULT_CWD").ok()))
133+
.or_else(|| normalize_cwd(std::env::var("HOME").ok()))
134+
.unwrap_or_else(|| "/".to_string())
135+
}
136+
137+
fn normalize_cwd(value: Option<String>) -> Option<String> {
138+
value.and_then(|value| {
139+
let trimmed = value.trim();
140+
if trimmed.is_empty() {
141+
None
142+
} else {
143+
Some(trimmed.to_string())
144+
}
145+
})
146+
}
147+
83148
fn content_type_for(path: &Path) -> &'static str {
84149
match path.extension().and_then(|ext| ext.to_str()) {
85150
Some("html") => "text/html; charset=utf-8",

0 commit comments

Comments
 (0)