1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// SPDX-License-Identifier: MIT OR Apache-2.0
//! 2026-09-26: What `q` costs, and when it asks for a second press.
//!
//! `q` stops the server: it sets `should_quit`, the event loop exits with
//! `Exit::Quit` and calls `shutdown::request`, as Ctrl+C does. The prompt is
//! shown only when work is in flight, because a prompt that is usually
//! pointless trains the user to dismiss it without reading.
//!
//! Owner: server tui.
//! Invariants:
//! - While `confirm_quit` is set, `App::on_key` hands every key but Ctrl+C to
//! the prompt, and only `q`, `y` or `Y` there sets `should_quit`.
use crossterm::event::{KeyCode, KeyEvent};
use super::app::App;
impl App {
/// 2026-09-26: What `q` would throw away, named for the prompt; `None` when nothing is in flight.
/// A download the user has already asked to cancel does not count.
pub fn work_in_flight(&self) -> Option<&'static str> {
if self.bench.is_running() {
return Some("a benchmark is running");
}
if self.download.job.as_ref().is_some_and(|j| !j.cancelling) {
return Some("a model download is in progress");
}
if self.chat.streaming {
return Some("a chat reply is still streaming");
}
// 2026-09-26: A model load, from the Library or at boot. The boot arm also needs no live
// model: a failed swap resets `progress` (so `ready` is false) while the old model keeps serving.
if self.lib.launch_in_flight()
|| (!self.progress.ready
&& !self.awaiting_model
&& self.host.as_ref().and_then(|h| h.live_model()).is_none())
{
return Some("a model is still loading");
}
// 2026-09-26: An issue report mid-authorisation or mid-submit, or a draft with text, ends with the process.
if self.help.report_in_flight() {
return Some("an issue report is being submitted");
}
if self.help.has_draft() {
return Some("an unsubmitted issue report has text in it");
}
None
}
/// 2026-09-26: `q` pressed with no text field claiming it.
pub(super) fn on_quit_key(&mut self) {
if self.work_in_flight().is_some() {
self.confirm_quit = true;
} else {
self.should_quit = true;
}
}
/// 2026-09-26: Answer an open prompt. Always returns `true`: the prompt consumes every key, so
/// dismissing it never also acts on the section underneath.
///
/// Only `q`, `y` or `Y` confirms. Every other key cancels, because the
/// safe reading of an ambiguous keystroke is "do not stop the server".
pub(super) fn answer_quit_prompt(&mut self, key: KeyEvent) -> bool {
self.confirm_quit = false;
if matches!(
key.code,
KeyCode::Char('q') | KeyCode::Char('y') | KeyCode::Char('Y')
) {
self.should_quit = true;
}
true
}
}
#[cfg(test)]
#[path = "app_quit_tests.rs"]
mod tests;