agentmux_cef\reducer/
top_level.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Top-level window creation runner (Phase H.6) 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, HostState, emit_error, TOP_LEVEL_CREATION_HISTORY_CAP};
12
13// ── H.6 — top-level window creation runner ───────────────────────────────
14
15pub(super) fn handle_enqueue_top_level_window(
16    state: &mut HostState,
17    request: TopLevelCreationRequest,
18) -> DispatchOutput {
19    if state.quit_state != QuitState::Running {
20        return emit_error(state, format!("enqueue_top_level_window: not Running (label={})", request.label));
21    }
22
23    // Fail-fast for User-initiated requests when in-flight is occupied.
24    // Background (pool refill) requests queue silently.
25    if state.top_level_creation.in_flight.is_some()
26        && request.source == TopLevelSource::User
27    {
28        return emit_error(state, format!("enqueue_top_level_window: busy in-flight (label={})", request.label));
29    }
30
31    state.top_level_creation.queue.push_back(request);
32    let queue_len = state.top_level_creation.queue.len();
33    let v = state.bump_version();
34    let mut out = DispatchOutput {
35        events: vec![HostEvent::TopLevelQueueLengthChanged { len: queue_len, version: v }],
36        ..Default::default()
37    };
38    // If idle, start immediately; chain the start arm's events.
39    if state.top_level_creation.in_flight.is_none() {
40        let started = start_next_top_level_if_idle(state);
41        out.events.extend(started.events);
42    }
43    out
44}
45
46/// Internal helper: if in_flight is None and queue has work, pop the front
47/// and start it. Emits `TopLevelCreationRequested`, `TopLevelCreationStarted`,
48/// `Effect::PostCreateWindow`, and updated queue length.
49///
50/// **Quit gating** (codex P1 PR #654 round 1): if `quit_state != Running`,
51/// don't start anything — queued background requests stay queued but will
52/// never fire (host is exiting; in-memory queue dies with the process).
53/// Without this guard, an in-flight completion during `Draining` would pop
54/// a queued background pool refill and emit `Effect::PostCreateWindow`,
55/// creating a new window mid-shutdown and preventing drain completion.
56pub(super) fn start_next_top_level_if_idle(state: &mut HostState) -> DispatchOutput {
57    if state.top_level_creation.in_flight.is_some() {
58        return DispatchOutput::default();
59    }
60    if state.quit_state != QuitState::Running {
61        return DispatchOutput::default();
62    }
63    let request = match state.top_level_creation.queue.pop_front() {
64        Some(r) => r,
65        None => return DispatchOutput::default(),
66    };
67    state.top_level_creation.next_creation_id =
68        state.top_level_creation.next_creation_id.wrapping_add(1);
69    let creation_id = state.top_level_creation.next_creation_id;
70    let now = Instant::now();
71    state.top_level_creation.in_flight = Some(InFlightCreation {
72        creation_id,
73        label: request.label.clone(),
74        started_at: now,
75        phase: CreationPhase::Started,
76    });
77    let label = request.label.clone();
78    let source = request.source.clone();
79    let queue_len = state.top_level_creation.queue.len();
80    let v_req = state.bump_version();
81    let v_started = state.bump_version();
82    let v_eff = state.bump_version();
83    let v_qlen = state.bump_version();
84    DispatchOutput {
85        events: vec![
86            HostEvent::TopLevelCreationRequested {
87                creation_id,
88                source,
89                label: label.clone(),
90                version: v_req,
91            },
92            HostEvent::TopLevelCreationStarted {
93                creation_id,
94                label: label.clone(),
95                version: v_started,
96            },
97            HostEvent::Effect {
98                effect: EffectKind::PostCreateWindow { request, creation_id },
99                version: v_eff,
100            },
101            HostEvent::TopLevelQueueLengthChanged { len: queue_len, version: v_qlen },
102        ],
103        ..Default::default()
104    }
105}
106
107pub(super) fn handle_top_level_callback_fired(state: &mut HostState, label: String) -> DispatchOutput {
108    let matches_in_flight = state
109        .top_level_creation
110        .in_flight
111        .as_ref()
112        .map(|c| c.label == label)
113        .unwrap_or(false);
114    if !matches_in_flight {
115        // Orphan callback: a CEF browser fired on_after_created with a
116        // label we don't have in flight. Could be from a previously-evicted
117        // creation (won't happen in PR #1 since we don't evict) or a stale
118        // label. Emit an effect to close the orphan, preventing collision.
119        let orphan_browser = state.browsers.get(&label).map(|h| h.browser.clone());
120        if let Some(browser) = orphan_browser {
121            let v = state.bump_version();
122            return DispatchOutput {
123                events: vec![HostEvent::Effect {
124                    effect: EffectKind::CloseOrphanBrowser { browser },
125                    version: v,
126                }],
127                ..Default::default()
128            };
129        }
130        return DispatchOutput::default();
131    }
132    let inflight = state.top_level_creation.in_flight.take().unwrap();
133    let now = Instant::now();
134    let latency_ms = now.duration_since(inflight.started_at).as_millis() as u64;
135    push_top_level_history(
136        state,
137        CompletedCreation {
138            creation_id: inflight.creation_id,
139            label: inflight.label.clone(),
140            outcome: TopLevelCreationOutcome::Completed,
141            started_at: inflight.started_at,
142            finished_at: now,
143            last_phase: CreationPhase::BrowserCallbackFired,
144        },
145    );
146    let v_done = state.bump_version();
147    let mut out = DispatchOutput {
148        events: vec![HostEvent::TopLevelCreationCompleted {
149            creation_id: inflight.creation_id,
150            label: inflight.label,
151            latency_ms,
152            version: v_done,
153        }],
154        ..Default::default()
155    };
156    let next = start_next_top_level_if_idle(state);
157    out.events.extend(next.events);
158    out
159}
160
161pub(super) fn handle_top_level_renderer_terminated(
162    state: &mut HostState,
163    label: String,
164    status: String,
165) -> DispatchOutput {
166    let matches = state
167        .top_level_creation
168        .in_flight
169        .as_ref()
170        .map(|c| c.label == label)
171        .unwrap_or(false);
172    if !matches {
173        return DispatchOutput::default();
174    }
175    let inflight = state.top_level_creation.in_flight.take().unwrap();
176    let now = Instant::now();
177    let outcome = TopLevelCreationOutcome::RendererTerminated { status };
178    push_top_level_history(
179        state,
180        CompletedCreation {
181            creation_id: inflight.creation_id,
182            label: inflight.label.clone(),
183            outcome: outcome.clone(),
184            started_at: inflight.started_at,
185            finished_at: now,
186            last_phase: inflight.phase,
187        },
188    );
189    let v = state.bump_version();
190    let mut out = DispatchOutput {
191        events: vec![HostEvent::TopLevelCreationFailed {
192            creation_id: inflight.creation_id,
193            label: inflight.label,
194            outcome,
195            version: v,
196        }],
197        ..Default::default()
198    };
199    let next = start_next_top_level_if_idle(state);
200    out.events.extend(next.events);
201    out
202}
203
204pub(super) fn handle_top_level_externally_closed(state: &mut HostState, label: String) -> DispatchOutput {
205    let matches = state
206        .top_level_creation
207        .in_flight
208        .as_ref()
209        .map(|c| c.label == label)
210        .unwrap_or(false);
211    if !matches {
212        return DispatchOutput::default();
213    }
214    let inflight = state.top_level_creation.in_flight.take().unwrap();
215    let now = Instant::now();
216    let outcome = TopLevelCreationOutcome::ExternallyClosed;
217    push_top_level_history(
218        state,
219        CompletedCreation {
220            creation_id: inflight.creation_id,
221            label: inflight.label.clone(),
222            outcome: outcome.clone(),
223            started_at: inflight.started_at,
224            finished_at: now,
225            last_phase: inflight.phase,
226        },
227    );
228    let v = state.bump_version();
229    let mut out = DispatchOutput {
230        events: vec![HostEvent::TopLevelCreationFailed {
231            creation_id: inflight.creation_id,
232            label: inflight.label,
233            outcome,
234            version: v,
235        }],
236        ..Default::default()
237    };
238    let next = start_next_top_level_if_idle(state);
239    out.events.extend(next.events);
240    out
241}
242
243pub(super) fn push_top_level_history(state: &mut HostState, entry: CompletedCreation) {
244    if state.top_level_creation.history.len() >= TOP_LEVEL_CREATION_HISTORY_CAP {
245        state.top_level_creation.history.pop_front();
246    }
247    state.top_level_creation.history.push_back(entry);
248}
249