1use std::sync::Arc;
20
21use axum::{
22 extract::State,
23 http::{HeaderMap, StatusCode},
24 routing::{get, post},
25 Json, Router,
26};
27use tower_http::cors::CorsLayer;
28use tower_http::services::ServeDir;
29
30use crate::commands;
31use crate::state::AppState;
32
33#[derive(Debug, serde::Deserialize)]
35pub struct IpcRequest {
36 pub cmd: String,
38 #[serde(default)]
40 pub args: serde_json::Value,
41}
42
43#[derive(Debug, serde::Serialize)]
45pub struct IpcResponse {
46 pub success: bool,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub data: Option<serde_json::Value>,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub error: Option<String>,
54}
55
56#[derive(Debug, serde::Serialize)]
58struct HealthResponse {
59 status: String,
60 version: String,
61}
62
63pub async fn start_ipc_server(state: Arc<AppState>) -> u16 {
66 let exe_dir = std::env::current_exe()
68 .ok()
69 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
70 .unwrap_or_else(|| std::env::current_dir().unwrap());
71 let runtime_dir = exe_dir.join("runtime");
73 let frontend_dir = if runtime_dir.join("frontend").join("index.html").exists() {
74 runtime_dir.join("frontend")
75 } else {
76 exe_dir.join("frontend")
77 };
78 let has_frontend = frontend_dir.join("index.html").exists();
79 if has_frontend {
80 tracing::info!("Serving static frontend from: {}", frontend_dir.display());
81 }
82
83 let mut app = Router::new()
84 .route("/ipc", post(handle_ipc))
85 .route("/health", get(health));
86 app = crate::browser_api::register_routes(app);
89 let mut app = app
90 .layer(CorsLayer::permissive())
91 .with_state(state);
92
93 if has_frontend {
95 app = app.fallback_service(ServeDir::new(&frontend_dir));
96 }
97
98 let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
99 .await
100 .expect("Failed to bind IPC server");
101 let port = listener
102 .local_addr()
103 .expect("Failed to get local address")
104 .port();
105
106 tracing::info!("IPC HTTP server started on 127.0.0.1:{}", port);
107
108 tokio::spawn(async move {
109 axum::serve(listener, app)
110 .await
111 .expect("IPC server error");
112 });
113
114 port
115}
116
117async fn health() -> Json<HealthResponse> {
119 Json(HealthResponse {
120 status: "ok".to_string(),
121 version: env!("CARGO_PKG_VERSION").to_string(),
122 })
123}
124
125async fn handle_ipc(
130 State(state): State<Arc<AppState>>,
131 headers: HeaderMap,
132 Json(req): Json<IpcRequest>,
133) -> (StatusCode, Json<IpcResponse>) {
134 let authorized = headers
136 .get("authorization")
137 .and_then(|v| v.to_str().ok())
138 .and_then(|v| v.strip_prefix("Bearer "))
139 .map(|token| token == state.ipc_token)
140 .unwrap_or(false);
141
142 if !authorized {
143 return (
144 StatusCode::UNAUTHORIZED,
145 Json(IpcResponse {
146 success: false,
147 data: None,
148 error: Some("Unauthorized: invalid or missing IPC token".to_string()),
149 }),
150 );
151 }
152
153 tracing::debug!("IPC request: cmd={} args={}", req.cmd, req.args);
154
155 let result = route_command(&state, &req.cmd, &req.args).await;
156
157 match result {
158 Ok(data) => (
159 StatusCode::OK,
160 Json(IpcResponse {
161 success: true,
162 data: Some(data),
163 error: None,
164 }),
165 ),
166 Err(error) => (
167 StatusCode::OK, Json(IpcResponse {
169 success: false,
170 data: None,
171 error: Some(error),
172 }),
173 ),
174 }
175}
176
177async fn route_command(
182 state: &Arc<AppState>,
183 cmd: &str,
184 args: &serde_json::Value,
185) -> Result<serde_json::Value, String> {
186 if commands::stubs::is_stub_command(cmd) {
188 return Ok(commands::stubs::handle_stub(cmd, args));
189 }
190
191 match cmd {
192 "get_platform" => Ok(commands::platform::get_platform()),
194 "get_auth_key" => {
195 let key = state.auth_key.lock().clone();
196 tracing::debug!("Frontend requested auth key: {}...", &key[..8.min(key.len())]);
197 Ok(serde_json::json!(key))
198 }
199 "get_is_dev" => Ok(commands::platform::get_is_dev()),
200 "get_user_name" => Ok(commands::platform::get_user_name()),
201 "get_host_name" => Ok(commands::platform::get_host_name()),
202 "get_data_dir" => commands::platform::get_data_dir(state),
203 "get_config_dir" => commands::platform::get_config_dir(state),
204 "get_user_home_dir" => commands::platform::get_user_home_dir(state),
205 "get_docsite_url" => Ok(commands::platform::get_docsite_url(state)),
206 "get_zoom_factor" => Ok(commands::window::get_zoom_factor(state)),
207 "get_about_modal_details" => Ok(commands::platform::get_about_modal_details(state)),
208 "get_host_info" => Ok(commands::platform::get_host_info(state)),
209 "get_backend_endpoints" => commands::backend::get_backend_endpoints(state),
210 "get_wave_init_opts" => commands::backend::get_wave_init_opts(state),
211 "set_window_init_status" => Ok(commands::backend::set_window_init_status(state, args)),
212 "fe_log" => Ok(commands::backend::fe_log(args)),
213 "fe_log_structured" => Ok(commands::backend::fe_log_structured(args)),
214
215 "get_backend_info" => Ok(commands::backend::get_backend_info(state)),
217 "restart_backend" => commands::backend::restart_backend(state.clone()).await,
218 "close_window" => commands::window::close_window(state, args),
219 "minimize_window" => commands::window::minimize_window(state, args),
220 "maximize_window" => commands::window::maximize_window(state, args),
221 "set_zoom_factor" => commands::window::set_zoom_factor(state, args),
222 "is_main_window" => Ok(commands::window::is_main_window(args)),
223 "get_window_label" => Ok(commands::window::get_window_label(args)),
224 "open_new_window" => commands::window::open_new_window(state),
225 "open_subwindow" => {
226 let parent = args
229 .get("parent_instance_id")
230 .and_then(|v| v.as_str())
231 .ok_or_else(|| "open_subwindow: parent_instance_id required".to_string())?
232 .to_string();
233 commands::window::open_subwindow(state, parent)
234 }
235 "open_floating_pane_window" => {
236 commands::floating_pane::open_floating_pane_window(state, args)
242 }
243 "get_instance_number" => Ok(commands::window::get_instance_number(state, args)),
244 "register_backend_window" => Ok(commands::window::register_backend_window(state, args)),
245 "get_env" => Ok(commands::platform::get_env(args)),
246 "open_external" => commands::platform::open_external(args),
247 "set_window_transparency" => commands::window::set_window_transparency(state, args),
248 "set_window_opacity" => commands::window::set_window_opacity(state, args),
249 "get_window_opacity" => commands::window::get_window_opacity(state, args),
250 "start_window_drag" => commands::window::start_window_drag(state, args),
251 "get_window_position" => commands::window::get_window_position(state),
252 "move_window_by" => commands::window::move_window_by(state, args),
253 "set_window_position" => commands::window::set_window_position(state, args),
254 "toggle_devtools" => commands::window::toggle_devtools(state, args),
255 "show_context_menu" => {
256 tracing::debug!("show_context_menu: handled in JS overlay");
257 Ok(serde_json::Value::Null)
258 }
259
260 "start_cross_drag" => commands::drag::start_cross_drag(state, args),
262 "update_cross_drag" => commands::drag::update_cross_drag(state, args),
263 "complete_cross_drag" => commands::drag::complete_cross_drag(state, args),
264 "cancel_cross_drag" => commands::drag::cancel_cross_drag(state, args),
265 "get_cursor_point" => commands::drag::get_cursor_point(),
266 "get_mouse_button_state" => commands::drag::get_mouse_button_state(),
267 "set_drag_cursor" => commands::drag::set_drag_cursor(),
268 "restore_drag_cursor" => commands::drag::restore_drag_cursor(),
269 "release_drag_capture" => commands::drag::release_drag_capture(state),
270 "set_js_drag_active" => commands::drag::set_js_drag_active(args),
271 "open_window_at_position" => commands::drag::open_window_at_position(state, args),
272 "tear_off_pool_promote" => commands::drag::tear_off_pool_promote(state, args),
273 "pool_window_ready" => commands::drag::pool_window_ready(state, args),
274 "tear_off_sc_move_handshake" => {
275 let state_clone = state.clone();
280 let args_clone = args.clone();
281 tokio::task::spawn_blocking(move || {
282 commands::drag::tear_off_sc_move_handshake(&state_clone, &args_clone)
283 })
284 .await
285 .map_err(|e| format!("tear_off_sc_move_handshake join error: {}", e))?
286 }
287 "list_windows" => Ok(commands::window::list_windows(state)),
288 "list_window_instances" => Ok(commands::window::list_window_instances(state)),
289 "get_double_click_time" => Ok(commands::window::get_double_click_time()),
290 "focus_window" => commands::window::focus_window(state, args),
291 "close_window_by_label" => commands::window::close_window_by_label(state, args),
292
293 "read_clipboard" => commands::clipboard::read_clipboard(),
295 "write_clipboard" => commands::clipboard::write_clipboard(args),
296
297 "detect_installed_clis" => commands::providers::detect_installed_clis().await,
299 "get_provider_config" => commands::providers::get_provider_config(state),
300 "save_provider_config" => commands::providers::save_provider_config(state, args),
301 "get_provider_install_info" => commands::providers::get_provider_install_info(args),
302 "set_provider_auth" => commands::providers::set_provider_auth(state, args).await,
303 "clear_provider_auth" => commands::providers::clear_provider_auth(state, args),
304 "get_provider_auth_status" => commands::providers::get_provider_auth_status(state, args),
305 "check_cli_auth_status" => commands::providers::check_cli_auth_status(args).await,
306 "install_cli" => commands::providers::install_cli(state, args).await,
307 "get_cli_path" => commands::providers::get_cli_path(state, args),
308 "check_nodejs_available" => commands::providers::check_nodejs_available().await,
309 "ensure_auth_dir" => commands::platform::ensure_auth_dir(state, args),
310 "run_cli_login" => commands::platform::run_cli_login(state.clone(), args).await,
311 "cancel_cli_login" => commands::platform::cancel_cli_login(state),
312 "ensure_settings_file" => commands::platform::ensure_settings_file(state),
313 "open_in_editor" => commands::platform::open_in_editor(args),
314 "copy_file_to_dir" => commands::providers::copy_file_to_dir(args),
315
316 "run_command" => commands::palette::run_command(state, args),
318
319 "open_agent" => commands::palette::open_agent(state, args),
321
322 "browser_pane_create" => {
324 let block_id = args.get("block_id").and_then(|v| v.as_str()).unwrap_or("");
325 let url = args.get("url").and_then(|v| v.as_str()).unwrap_or("about:blank");
326 let x = args.get("x").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
327 let y = args.get("y").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
328 let w = args.get("width").and_then(|v| v.as_i64()).unwrap_or(800) as i32;
329 let h = args.get("height").and_then(|v| v.as_i64()).unwrap_or(600) as i32;
330 let rect = cef::Rect { x, y, width: w, height: h };
331 let window_label = args
337 .get("window_label")
338 .and_then(|v| v.as_str())
339 .unwrap_or("main");
340 state.browser_panes.create(state, block_id, url, rect, window_label)?;
341 Ok(serde_json::json!(true))
342 }
343 "browser_pane_navigate" => {
344 let block_id = args.get("block_id").and_then(|v| v.as_str()).unwrap_or("");
345 let url = args.get("url").and_then(|v| v.as_str()).unwrap_or("");
346 state.browser_panes.navigate(block_id, url, state)?;
347 Ok(serde_json::json!(true))
348 }
349 "browser_pane_resize" => {
350 let block_id = args.get("block_id").and_then(|v| v.as_str()).unwrap_or("");
351 let x = args.get("x").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
352 let y = args.get("y").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
353 let w = args.get("width").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
354 let h = args.get("height").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
355 tracing::debug!(
359 "[ipc] browser_pane_resize block_id={} rect=({},{},{},{})",
360 block_id, x, y, w, h
361 );
362 state.browser_panes.resize(block_id, cef::Rect { x, y, width: w, height: h }, state);
363 Ok(serde_json::json!(true))
364 }
365 "browser_pane_close" => {
366 let block_id = args.get("block_id").and_then(|v| v.as_str()).unwrap_or("");
367 crate::browser_pane::auth::cancel_for_block(block_id);
372 state.browser_panes.close(block_id, state);
373 Ok(serde_json::json!(true))
374 }
375 "browser_pane_go_back" => {
376 let block_id = args.get("block_id").and_then(|v| v.as_str()).unwrap_or("");
377 state.browser_panes.go_back(block_id, state);
378 Ok(serde_json::json!(true))
379 }
380 "browser_pane_go_forward" => {
381 let block_id = args.get("block_id").and_then(|v| v.as_str()).unwrap_or("");
382 state.browser_panes.go_forward(block_id, state);
383 Ok(serde_json::json!(true))
384 }
385 "browser_pane_reload" => {
386 let block_id = args.get("block_id").and_then(|v| v.as_str()).unwrap_or("");
387 state.browser_panes.reload(block_id, state);
388 Ok(serde_json::json!(true))
389 }
390 "browser_pane_focus" => {
391 let block_id = args.get("block_id").and_then(|v| v.as_str()).unwrap_or("");
392 tracing::info!("[ipc] browser_pane_focus block_id={}", block_id);
393 state.browser_panes.focus(block_id, state);
394 Ok(serde_json::json!(true))
395 }
396 "browser_pane_auth_submit" => {
397 let request_id = args.get("request_id").and_then(|v| v.as_str()).unwrap_or("");
401 let username = args.get("username").and_then(|v| v.as_str()).unwrap_or("");
402 let password = args.get("password").and_then(|v| v.as_str()).unwrap_or("");
403 tracing::info!(
408 "[browser-pane-auth] submit request_id={}",
409 request_id,
410 );
411 if let Some(cb) = crate::browser_pane::auth::take(request_id) {
412 use cef::ImplAuthCallback;
413 let u = cef::CefString::from(username);
414 let p = cef::CefString::from(password);
415 cb.cont(Some(&u), Some(&p));
416 Ok(serde_json::json!(true))
417 } else {
418 tracing::warn!(
419 "[browser-pane-auth] submit for unknown request_id {} — already resolved?",
420 request_id
421 );
422 Ok(serde_json::json!(false))
423 }
424 }
425 "browser_pane_auth_cancel" => {
426 let request_id = args.get("request_id").and_then(|v| v.as_str()).unwrap_or("");
427 tracing::info!("[browser-pane-auth] cancel request_id={}", request_id);
428 if let Some(cb) = crate::browser_pane::auth::take(request_id) {
429 use cef::ImplAuthCallback;
430 cb.cancel();
431 Ok(serde_json::json!(true))
432 } else {
433 Ok(serde_json::json!(false))
434 }
435 }
436 "browser_panes_set_overlay_clip" => {
437 let window_label = args
450 .get("window_label")
451 .and_then(|v| v.as_str())
452 .unwrap_or("main")
453 .to_string();
454 let rects: Vec<(i32, i32, i32, i32)> = args
455 .get("rects")
456 .and_then(|v| v.as_array())
457 .map(|arr| {
458 arr.iter()
459 .filter_map(|item| {
460 let x = item.get("x")?.as_i64()? as i32;
461 let y = item.get("y")?.as_i64()? as i32;
462 let w = item.get("w")?.as_i64()? as i32;
463 let h = item.get("h")?.as_i64()? as i32;
464 Some((x, y, w, h))
465 })
466 .collect()
467 })
468 .unwrap_or_default();
469 state
470 .browser_panes
471 .set_pane_overlay_clip(state, &window_label, &rects);
472 Ok(serde_json::json!(true))
473 }
474 "main_window_focus" => {
475 let window_label = args
492 .get("window_label")
493 .and_then(|v| v.as_str())
494 .unwrap_or("main")
495 .to_string();
496 tracing::info!("[ipc] main_window_focus window_label={}", window_label);
497
498 let target_browser = state
504 .get_browser(&window_label)
505 .map(|b| (window_label.clone(), b))
506 .or_else(|| {
507 state
508 .list_browsers()
509 .into_iter()
510 .find(|(k, _)| !k.starts_with("browser-pane-"))
511 });
512
513 if let Some((label, _browser)) = target_browser {
514 let mut task = crate::ui_tasks::MainFocusReclaimTask::new(
519 state.clone(),
520 label.clone(),
521 );
522 cef::post_task(cef::ThreadId::UI, Some(&mut task));
523 tracing::info!("[ipc] main_window_focus: posted MainFocusReclaimTask for label={}", label);
524 } else {
525 tracing::warn!("[ipc] main_window_focus: no browser found for label={}", window_label);
526 }
527
528 Ok(serde_json::json!(true))
529 }
530
531 _ => Err(format!("Unknown command: {}", cmd)),
533 }
534}
535