agentmux_srv\reducer/
window.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4use agentmux_common::ipc::{ErrorCode, Event};
5
6use crate::state::State;
7
8
9use crate::state::WindowRecord;
10
11/// Phase E.5 — record a new window→workspace mapping. Validates
12/// the parent workspace exists; otherwise emits `Event::Error`
13/// (non-fatal). Idempotent on duplicate `window_id`: re-issuing
14/// for the same window updates the workspace pointer if it
15/// changed, or no-ops if identical.
16pub(super) fn handle_create_window(
17    state: &mut State,
18    window_id: String,
19    workspace_id: String,
20) -> Vec<Event> {
21    if !state.workspaces.contains_key(&workspace_id) {
22        let v = state.bump_version();
23        return vec![Event::Error {
24            code: ErrorCode::InvalidCommand,
25            message: format!("CreateWindow: workspace not found: {}", workspace_id),
26            fatal: false,
27            version: v,
28        }];
29    }
30    if let Some(existing) = state.windows.get(&window_id) {
31        if existing.workspace_id == workspace_id {
32            return Vec::new();
33        }
34    }
35    state.windows.insert(
36        window_id.clone(),
37        WindowRecord {
38            window_id: window_id.clone(),
39            workspace_id: workspace_id.clone(),
40        },
41    );
42    let v = state.bump_version();
43    vec![Event::SrvWindowOpened {
44        window_id,
45        workspace_id,
46        version: v,
47    }]
48}
49
50/// Phase E.5 — remove a window's workspace mapping. Idempotent
51/// silent no-op on missing.
52pub(super) fn handle_close_window_internal(state: &mut State, window_id: String) -> Vec<Event> {
53    if state.windows.remove(&window_id).is_none() {
54        return Vec::new();
55    }
56    let v = state.bump_version();
57    vec![Event::SrvWindowClosed {
58        window_id,
59        version: v,
60    }]
61}
62
63/// Phase E.5 — change which workspace a window points at. Errors
64/// (non-fatal) if the window or destination workspace is unknown.
65/// No-op if the window is already pointing at the destination.
66pub(super) fn handle_switch_workspace(
67    state: &mut State,
68    window_id: String,
69    workspace_id: String,
70) -> Vec<Event> {
71    if !state.workspaces.contains_key(&workspace_id) {
72        let v = state.bump_version();
73        return vec![Event::Error {
74            code: ErrorCode::InvalidCommand,
75            message: format!(
76                "SwitchWorkspace: destination workspace not found: {}",
77                workspace_id
78            ),
79            fatal: false,
80            version: v,
81        }];
82    }
83    let Some(window) = state.windows.get_mut(&window_id) else {
84        let v = state.bump_version();
85        return vec![Event::Error {
86            code: ErrorCode::InvalidCommand,
87            message: format!("SwitchWorkspace: window not found: {}", window_id),
88            fatal: false,
89            version: v,
90        }];
91    };
92    if window.workspace_id == workspace_id {
93        return Vec::new();
94    }
95    window.workspace_id = workspace_id.clone();
96    let v = state.bump_version();
97    vec![Event::SrvWindowWorkspaceChanged {
98        window_id,
99        workspace_id,
100        version: v,
101    }]
102}
103
104/// Phase E.5.x (issue #855) — apply a meta-patch to a window. Pass-
105/// through to `Event::WindowMetaUpdated`; the persist subscriber
106/// performs the merge against wstore. Same shape as
107/// `handle_update_workspace_meta` — reducer state does NOT track
108/// window meta, the migration property is "every mutation goes
109/// through the reducer's broadcast bus" so the WaveObjUpdate bridge
110/// can fan out to the frontend.
111///
112/// The validation is best-effort: reducer state's `windows` map only
113/// holds windows for which a workspace mapping has been registered
114/// (via `handle_create_window`). Windows created via wcore-direct
115/// paths (legacy bootstrap) won't appear there but still exist in
116/// wstore. So we don't error on missing — the persist subscriber's
117/// `apply_window_meta_updated` will silently no-op if the window
118/// genuinely doesn't exist in wstore either, matching the
119/// idempotency contract for the bridge to broadcast (or skip).
120pub(super) fn handle_update_window_meta(
121    state: &mut State,
122    window_id: String,
123    meta_patch: serde_json::Value,
124) -> Vec<Event> {
125    let v = state.bump_version();
126    vec![Event::WindowMetaUpdated {
127        window_id,
128        meta_patch,
129        version: v,
130    }]
131}