TUIからPauseする実装
This commit is contained in:
+19
-2
@@ -1,9 +1,13 @@
|
||||
use protocol::{Event, Greeting, Method, NotificationLevel, NotificationSource};
|
||||
use protocol::{Event, Greeting, Method, NotificationLevel, NotificationSource, RunResult};
|
||||
|
||||
pub struct App {
|
||||
pub pod_name: String,
|
||||
pub connected: bool,
|
||||
pub running: bool,
|
||||
/// True while the Pod is in `PodStatus::Paused`. Set on
|
||||
/// `RunEnd::Paused` and cleared when a new turn starts (either via
|
||||
/// `Resume` or a fresh `Run`).
|
||||
pub paused: bool,
|
||||
pub run_requests: usize,
|
||||
pub run_input_tokens: u64,
|
||||
pub run_output_tokens: u64,
|
||||
@@ -13,6 +17,10 @@ pub struct App {
|
||||
pub cursor: usize,
|
||||
pub quit: bool,
|
||||
pub shutdown_confirm: Option<std::time::Instant>,
|
||||
/// 2-tap guard for `Ctrl-C` when the Pod is not running. First press
|
||||
/// records the instant; a second press within the timeout exits the
|
||||
/// TUI (the Pod itself stays alive).
|
||||
pub quit_confirm: Option<std::time::Instant>,
|
||||
/// Lines waiting to be flushed to terminal via insert_before.
|
||||
pub output_queue: Vec<OutputItem>,
|
||||
/// Partial streaming text not yet terminated by newline.
|
||||
@@ -48,6 +56,7 @@ impl App {
|
||||
pod_name,
|
||||
connected: false,
|
||||
running: false,
|
||||
paused: false,
|
||||
run_requests: 0,
|
||||
run_input_tokens: 0,
|
||||
run_output_tokens: 0,
|
||||
@@ -57,6 +66,7 @@ impl App {
|
||||
cursor: 0,
|
||||
quit: false,
|
||||
shutdown_confirm: None,
|
||||
quit_confirm: None,
|
||||
output_queue: Vec::new(),
|
||||
pending_text: String::new(),
|
||||
}
|
||||
@@ -65,6 +75,11 @@ impl App {
|
||||
pub fn submit_input(&mut self) -> Option<Method> {
|
||||
let text = self.input.trim().to_owned();
|
||||
if text.is_empty() {
|
||||
// Empty Enter only does something meaningful when the Pod
|
||||
// is paused: resume the interrupted turn. Otherwise no-op.
|
||||
if self.paused {
|
||||
return Some(Method::Resume);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
self.turn_index += 1;
|
||||
@@ -83,6 +98,7 @@ impl App {
|
||||
match event {
|
||||
Event::TurnStart { .. } => {
|
||||
self.running = true;
|
||||
self.paused = false;
|
||||
self.run_requests += 1;
|
||||
self.current_tool = None;
|
||||
}
|
||||
@@ -154,7 +170,7 @@ impl App {
|
||||
format!("[{code:?}] {message}"),
|
||||
));
|
||||
}
|
||||
Event::RunEnd { .. } => {
|
||||
Event::RunEnd { result } => {
|
||||
self.output_queue.push(OutputItem::PaddedRight(
|
||||
MessageKind::TurnStats,
|
||||
format!(
|
||||
@@ -166,6 +182,7 @@ impl App {
|
||||
));
|
||||
self.output_queue.push(OutputItem::Blank);
|
||||
self.running = false;
|
||||
self.paused = matches!(result, RunResult::Paused);
|
||||
self.run_requests = 0;
|
||||
self.run_input_tokens = 0;
|
||||
self.run_output_tokens = 0;
|
||||
|
||||
+40
-15
@@ -141,14 +141,14 @@ async fn run_loop(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_disconnected(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn run_disconnected(_app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
loop {
|
||||
if event::poll(std::time::Duration::from_millis(100))? {
|
||||
if let TermEvent::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Esc => break,
|
||||
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => break,
|
||||
_ => {}
|
||||
if let KeyCode::Char('c') = key.code {
|
||||
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,16 +158,20 @@ fn run_disconnected(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
fn handle_key(app: &mut App, key: KeyEvent) -> Option<Method> {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
app.quit = true;
|
||||
None
|
||||
}
|
||||
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
app.quit = true;
|
||||
None
|
||||
handle_pause_or_quit(app)
|
||||
}
|
||||
KeyCode::Char('x') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
if app.running {
|
||||
Some(Method::Cancel)
|
||||
} else {
|
||||
app.output_queue.push(app::OutputItem::Padded(
|
||||
app::MessageKind::Error,
|
||||
"Nothing to cancel (Pod is not running).".into(),
|
||||
));
|
||||
None
|
||||
}
|
||||
}
|
||||
KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => Some(Method::Resume),
|
||||
KeyCode::Char('x') if key.modifiers.contains(KeyModifiers::CONTROL) => Some(Method::Cancel),
|
||||
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
return handle_shutdown(app);
|
||||
}
|
||||
@@ -204,14 +208,14 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option<Method> {
|
||||
}
|
||||
}
|
||||
|
||||
const SHUTDOWN_CONFIRM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
|
||||
const CONFIRM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
|
||||
|
||||
fn handle_shutdown(app: &mut App) -> Option<Method> {
|
||||
if !app.running {
|
||||
return Some(Method::Shutdown);
|
||||
}
|
||||
if let Some(t) = app.shutdown_confirm {
|
||||
if t.elapsed() < SHUTDOWN_CONFIRM_TIMEOUT {
|
||||
if t.elapsed() < CONFIRM_TIMEOUT {
|
||||
app.shutdown_confirm = None;
|
||||
return Some(Method::Shutdown);
|
||||
}
|
||||
@@ -223,3 +227,24 @@ fn handle_shutdown(app: &mut App) -> Option<Method> {
|
||||
));
|
||||
None
|
||||
}
|
||||
|
||||
/// Running → send `Method::Pause`.
|
||||
/// Idle / Paused → 2-tap to quit the TUI (the Pod keeps running).
|
||||
fn handle_pause_or_quit(app: &mut App) -> Option<Method> {
|
||||
if app.running {
|
||||
return Some(Method::Pause);
|
||||
}
|
||||
if let Some(t) = app.quit_confirm {
|
||||
if t.elapsed() < CONFIRM_TIMEOUT {
|
||||
app.quit_confirm = None;
|
||||
app.quit = true;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
app.quit_confirm = Some(std::time::Instant::now());
|
||||
app.output_queue.push(app::OutputItem::Padded(
|
||||
app::MessageKind::Error,
|
||||
"Press Ctrl-C again within 3 s to exit the TUI (the Pod keeps running).".into(),
|
||||
));
|
||||
None
|
||||
}
|
||||
|
||||
@@ -170,6 +170,18 @@ fn draw_status(frame: &mut Frame, app: &App, area: Rect) {
|
||||
};
|
||||
spans.push(Span::raw(" | "));
|
||||
spans.push(Span::styled(status, Style::default().fg(Color::Yellow)));
|
||||
} else if app.paused {
|
||||
spans.push(Span::raw(" | "));
|
||||
spans.push(Span::styled(
|
||||
"paused",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
spans.push(Span::styled(
|
||||
" — Enter to resume, type to start new turn",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
} else {
|
||||
spans.push(Span::styled(" idle", Style::default().fg(Color::DarkGray)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user