agentmux_srv/server/app_api/browser_start_page.rs
1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! `browser_start_page.set` — the browser pane's configured start page.
5//! Write-only: the read side is `GetFullConfig`'s `browserstartpage` field
6//! (`crates/srv/src/backend/wconfig/types.rs`), not a matching `.get`
7//! here — see `docs/specs/SPEC_BROWSER_PANE_START_PAGE_2026_09_16.md` §3.1
8//! for why. Deliberately its own file rather than folded into
9//! `bookmarks.rs`: unlike bookmarks, this handler needs `AppState`'s
10//! `config_watcher`/`event_bus` to update the live config and broadcast
11//! immediately, the same shape `COMMAND_SET_CONFIG` already has
12//! (`server/websocket.rs`).
13
14use super::*;
15use crate::backend::browser_start_page;
16
17pub fn register(engine: &Arc<WshRpcEngine>, state: &AppState) {
18 register_browser_start_page_set(engine, state);
19}
20
21#[derive(serde::Deserialize, ts_rs::TS)]
22#[ts(export, export_to = "../../../frontend/types/rpc/")]
23struct StartPageSetReq {
24 url: String,
25}
26
27/// What `browser_start_page.set` answers: the start page it saved.
28#[derive(serde::Serialize, ts_rs::TS)]
29#[ts(export, export_to = "../../../frontend/types/rpc/")]
30struct StartPageSetResult {
31 url: String,
32}
33
34fn register_browser_start_page_set(engine: &Arc<WshRpcEngine>, state: &AppState) {
35 let config_watcher = state.config_watcher.clone();
36 let event_bus = state.event_bus.clone();
37 engine.register_typed(COMMAND_BROWSER_START_PAGE_SET, move |req: StartPageSetReq, _ctx| {
38 let config_watcher = config_watcher.clone();
39 let event_bus = event_bus.clone();
40 async move {
41 if req.url.trim().is_empty() {
42 return Err("browser_start_page.set: url must not be empty".to_string());
43 }
44
45 // 1. Write to disk. Unlike bookmarks.set, a failure to
46 // resolve the shared dir must fail loudly here too — the
47 // same "don't tell the caller it saved when it didn't"
48 // posture, not silently no-oping.
49 let path = browser_start_page::start_page_file_path()
50 .ok_or_else(|| "browser_start_page.set: could not resolve the shared data directory".to_string())?;
51 browser_start_page::write_start_page(&path, &req.url)?;
52
53 // 2. Update in-memory config immediately — same
54 // write-then-update-then-broadcast-now shape as
55 // COMMAND_SET_CONFIG (server/websocket.rs), so every
56 // window converges on the new value without waiting on
57 // the fs watcher's 300ms debounce (spawn_start_page_watcher
58 // still exists as a backstop for an external file edit).
59 config_watcher.update_browser_start_page(Some(req.url.clone()));
60
61 // 3. Broadcast now.
62 crate::backend::config_watcher_fs::broadcast_full_config(&config_watcher, &event_bus);
63
64 Ok(StartPageSetResult { url: req.url })
65 }
66 });
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn set_req_deserializes_a_url() {
75 let data = json!({ "url": "https://example.com" });
76 let req: StartPageSetReq = serde_json::from_value(data).unwrap();
77 assert_eq!(req.url, "https://example.com");
78 }
79}