Skip to main content

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}