agentmux_cef\reducer/
panes.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Browser pane lifecycle (Phase H.1) reducer handlers. Extracted from reducer/mod.rs in
5//! task #182 PR-F-2 for navigability.
6
7use std::time::Instant;
8
9use crate::state::*;
10
11use super::{DispatchOutput, HostEvent, HostLifecyclePhase, HostState, RegisterResult, emit_error};
12
13// ── H.1 — pane lifecycle ─────────────────────────────────────────────────
14
15pub(super) fn handle_enqueue_browser_pane_create(
16    state: &mut HostState,
17    block_id: String,
18    label: String,
19) -> DispatchOutput {
20    if state.lifecycle == HostLifecyclePhase::ShuttingDown {
21        return emit_error(state, format!("enqueue_browser_pane_create: shutting down (block_id={})", block_id));
22    }
23    if state.browser_panes.contains_key(&block_id) {
24        return emit_error(state, format!("enqueue_browser_pane_create: block_id {} already has a pane", block_id));
25    }
26    state.browser_panes.insert(
27        block_id.clone(),
28        BrowserPaneEntry {
29            block_id: block_id.clone(),
30            label: label.clone(),
31            lifecycle: BrowserPaneLifecycle::Live,
32        },
33    );
34    let v = state.bump_version();
35    DispatchOutput {
36        events: vec![HostEvent::BrowserPaneCreateRequested { block_id, label, version: v }],
37        ..Default::default()
38    }
39}
40
41pub(super) fn handle_complete_browser_pane_create(state: &mut HostState, block_id: String) -> DispatchOutput {
42    let entry = match state.browser_panes.get(&block_id) {
43        Some(e) => e.clone(),
44        None => return DispatchOutput::default(), // late callback for already-removed pane; idempotent no-op
45    };
46    // Already Live by EnqueueBrowserPaneCreate's invariant; this is a no-op
47    // confirmation event for observers.
48    let v = state.bump_version();
49    DispatchOutput {
50        events: vec![HostEvent::BrowserPaneLive { block_id, label: entry.label, version: v }],
51        ..Default::default()
52    }
53}
54
55pub(super) fn handle_enqueue_browser_pane_close(state: &mut HostState, block_id: String) -> DispatchOutput {
56    let entry = match state.browser_panes.get_mut(&block_id) {
57        Some(e) => e,
58        None => return DispatchOutput::default(), // close request for already-gone pane; idempotent
59    };
60    if matches!(entry.lifecycle, BrowserPaneLifecycle::Closing { .. }) {
61        return DispatchOutput::default(); // already Closing; idempotent
62    }
63    entry.lifecycle = BrowserPaneLifecycle::Closing { since: Instant::now() };
64    let label = entry.label.clone();
65    let v = state.bump_version();
66    DispatchOutput {
67        events: vec![HostEvent::BrowserPaneClosing { block_id, version: v }],
68        closed_browser_pane_label: Some(label),
69        ..Default::default()
70    }
71}
72
73/// PR #5 — sole pane registration entry point. Replaces
74/// `pane::lifecycle::PaneStateMachine::try_register_live`.
75///
76/// - Live entry exists → `AlreadyLive(label)`
77/// - Closing entry exists → `Closing`
78/// - No entry → generate label, insert Live, `Fresh(label)` + emit
79///   `BrowserPaneCreateRequested`
80pub(super) fn handle_try_register_browser_pane_live(state: &mut HostState, block_id: String) -> DispatchOutput {
81    if state.lifecycle == HostLifecyclePhase::ShuttingDown {
82        return emit_error(
83            state,
84            format!("try_register_browser_pane_live: shutting down (block_id={})", block_id),
85        );
86    }
87    if let Some(entry) = state.browser_panes.get(&block_id) {
88        let result = match entry.lifecycle {
89            BrowserPaneLifecycle::Live => RegisterResult::AlreadyLive(entry.label.clone()),
90            BrowserPaneLifecycle::Closing { .. } => RegisterResult::Closing,
91        };
92        return DispatchOutput {
93            browser_pane_register_result: Some(result),
94            ..Default::default()
95        };
96    }
97    let label = super::next_browser_pane_label(&block_id);
98    state.browser_panes.insert(
99        block_id.clone(),
100        BrowserPaneEntry {
101            block_id: block_id.clone(),
102            label: label.clone(),
103            lifecycle: BrowserPaneLifecycle::Live,
104        },
105    );
106    let v = state.bump_version();
107    DispatchOutput {
108        events: vec![HostEvent::BrowserPaneCreateRequested {
109            block_id,
110            label: label.clone(),
111            version: v,
112        }],
113        browser_pane_register_result: Some(RegisterResult::Fresh(label)),
114        ..Default::default()
115    }
116}
117
118/// PR #5 — sole label-keyed drain entry point. Replaces
119/// `pane::lifecycle::PaneStateMachine::drain_by_label`.
120///
121/// Removes whichever pane entry has `label`. Returns the drained block_id
122/// in `drained_browser_pane_block_id`. Idempotent — `None` if no entry has that label
123/// (e.g., explicit `close()` already cleared it; `on_before_close` arrives
124/// later).
125pub(super) fn handle_drain_browser_pane_by_label(state: &mut HostState, label: String) -> DispatchOutput {
126    let victim = state
127        .browser_panes
128        .iter()
129        .find(|(_, e)| e.label == label)
130        .map(|(k, _)| k.clone());
131    let block_id = match victim {
132        Some(b) => b,
133        None => return DispatchOutput::default(),
134    };
135    state.browser_panes.remove(&block_id);
136    let v = state.bump_version();
137    DispatchOutput {
138        events: vec![HostEvent::BrowserPaneClosed {
139            block_id: block_id.clone(),
140            version: v,
141        }],
142        drained_browser_pane_block_id: Some(block_id),
143        ..Default::default()
144    }
145}
146
147pub(super) fn handle_complete_browser_pane_close(state: &mut HostState, block_id: String) -> DispatchOutput {
148    if state.browser_panes.remove(&block_id).is_none() {
149        return DispatchOutput::default(); // idempotent
150    }
151    let v = state.bump_version();
152    DispatchOutput {
153        events: vec![HostEvent::BrowserPaneClosed { block_id, version: v }],
154        ..Default::default()
155    }
156}
157
158pub(super) fn handle_abort_browser_pane_create(
159    state: &mut HostState,
160    block_id: String,
161    reason: String,
162) -> DispatchOutput {
163    state.browser_panes.remove(&block_id);
164    let v = state.bump_version();
165    DispatchOutput {
166        events: vec![HostEvent::BrowserPaneCreationFailed { block_id, reason, version: v }],
167        ..Default::default()
168    }
169}
170