Skip to main content

agentmux_srv/server/
http_layout.rs

1// Copyright 2025-2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Layout, window, workspace and tab queries and actions.
5//! Split out of server/mod.rs unchanged (SPEC_LARGE_FILE_MODULE_ANALYSIS_2026_09_30.md §4.1).
6
7use super::*;
8
9/// `GET /api/v1/layout` — read-only window → workspace → tab → pane tree.
10pub(super) async fn handle_layout(State(state): State<AppState>) -> impl IntoResponse {
11    Json(service::agent_layout(&state.mstore))
12}
13
14/// `GET /api/v1/windows` — flat list of windows.
15pub(super) async fn handle_list_windows(State(state): State<AppState>) -> impl IntoResponse {
16    Json(service::agent_windows(&state.mstore))
17}
18
19/// `GET /api/v1/workspaces` — flat list of workspaces.
20pub(super) async fn handle_list_workspaces(State(state): State<AppState>) -> impl IntoResponse {
21    Json(service::agent_workspaces(&state.mstore))
22}
23
24#[derive(serde::Deserialize)]
25pub(super) struct ListTabsQuery {
26    /// Limit to this workspace's tabs. Omit (or pass `block_id`) for all tabs.
27    #[serde(default)]
28    pub(super) workspace_id: Option<String>,
29    /// Calling agent's block id — scopes to the caller's own workspace when
30    /// `workspace_id` is omitted.
31    #[serde(default)]
32    pub(super) block_id: Option<String>,
33}
34
35/// `GET /api/v1/tabs` — flat list of tabs, optionally scoped to a workspace
36/// (explicit `workspace_id`, or the caller's own via `block_id`).
37pub(super) async fn handle_list_tabs(
38    State(state): State<AppState>,
39    Query(q): Query<ListTabsQuery>,
40) -> impl IntoResponse {
41    let ws_id = q.workspace_id.filter(|w| !w.is_empty()).or_else(|| {
42        q.block_id
43            .filter(|b| !b.is_empty())
44            .and_then(|b| service::resolve_agent_context(&state.mstore, &b).ok())
45            .and_then(|ctx| ctx.workspace_id)
46    });
47    Json(service::agent_tabs(&state.mstore, ws_id.as_deref()))
48}
49
50/// `POST /api/v1/tab/activate` — make `tab_id` the active tab in its
51/// workspace. Routes through `workspace.SetActiveTab`.
52pub(super) async fn handle_tab_activate(
53    State(state): State<AppState>,
54    Json(req): Json<TabActivateRequest>,
55) -> impl IntoResponse {
56    let tab_id = req.tab_id.trim().to_string();
57    if tab_id.is_empty() {
58        return (StatusCode::BAD_REQUEST, Json(json!({ "error": "missing tab_id" }))).into_response();
59    }
60    let ws_id = match service::workspace_id_for_tab(&state.mstore, &tab_id) {
61        Some(w) => w,
62        None => return (StatusCode::NOT_FOUND, Json(json!({ "error": format!("no workspace owns tab {tab_id}") }))).into_response(),
63    };
64    let call = crate::backend::service::WebCallType {
65        service: "workspace".to_string(),
66        method: "SetActiveTab".to_string(),
67        uicontext: None,
68        args: vec![json!(ws_id), json!(tab_id)],
69    };
70    finish_name_call(&state, call, json!({ "success": true, "tab_id": tab_id })).await
71}
72
73/// `POST /api/v1/tab/new` — create (and activate) a new tab in the caller's
74/// workspace (or an explicit `workspace_id`). Routes through
75/// `workspace.CreateTab`.
76pub(super) async fn handle_tab_new(
77    State(state): State<AppState>,
78    Json(req): Json<TabNewRequest>,
79) -> impl IntoResponse {
80    let name = req.name.map(|n| n.trim().chars().take(128).collect::<String>()).unwrap_or_default();
81    let workspace_id = match req.workspace_id.filter(|w| !w.is_empty()) {
82        Some(w) => w,
83        None => match resolve_own(&state, req.block_id, |c| c.workspace_id.clone()) {
84            Ok(w) => w,
85            Err(resp) => return resp,
86        },
87    };
88    let call = crate::backend::service::WebCallType {
89        service: "workspace".to_string(),
90        method: "CreateTab".to_string(),
91        uicontext: None,
92        // [ws_id, name, activate]; empty name → backend auto-names tab{N}.
93        args: vec![json!(workspace_id), json!(name), json!(true)],
94    };
95    finish_name_call(&state, call, json!({ "success": true, "workspace_id": workspace_id })).await
96}
97
98/// `POST /api/v1/window/focus` — bring a window to the foreground. Defaults to
99/// the caller's own window. Routes through `client.FocusWindow`.
100pub(super) async fn handle_window_focus(
101    State(state): State<AppState>,
102    Json(req): Json<WindowFocusRequest>,
103) -> impl IntoResponse {
104    let window_id = match req.window_id.filter(|w| !w.is_empty()) {
105        Some(w) => w,
106        None => match resolve_own(&state, req.block_id, |c| c.window_id.clone()) {
107            Ok(w) => w,
108            Err(resp) => return resp,
109        },
110    };
111    let call = crate::backend::service::WebCallType {
112        service: "client".to_string(),
113        method: "FocusWindow".to_string(),
114        uicontext: None,
115        args: vec![json!(window_id)],
116    };
117    finish_name_call(&state, call, json!({ "success": true, "window_id": window_id })).await
118}