feat: Podのステータスを厳密にし、同期漏れを防ぐ
This commit is contained in:
+43
-9
@@ -1,7 +1,8 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use protocol::{
|
||||
AlertLevel, AlertSource, CompletionEntry, CompletionKind, Event, Method, RunResult, Segment,
|
||||
AlertLevel, AlertSource, CompletionEntry, CompletionKind, Event, Method, PodStatus, RunResult,
|
||||
Segment,
|
||||
};
|
||||
|
||||
use crate::block::{
|
||||
@@ -41,11 +42,16 @@ impl CompletionState {
|
||||
pub struct App {
|
||||
pub pod_name: String,
|
||||
pub connected: bool,
|
||||
/// Last controller status reported by the Pod. Drives the status line
|
||||
/// and Ctrl-key routing; do not infer this solely from replayed history.
|
||||
pub pod_status: PodStatus,
|
||||
/// True while the Pod is in `PodStatus::Running`.
|
||||
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`).
|
||||
/// True while the Pod is in `PodStatus::Paused`.
|
||||
pub paused: bool,
|
||||
/// True after worker `RunEnd` while controller post-run work is still
|
||||
/// blocking the next method.
|
||||
pub busy: bool,
|
||||
pub run_requests: usize,
|
||||
/// Sum of `input_tokens - cache_read_input_tokens` across the
|
||||
/// current turn's LLM requests — i.e. the net tokens this turn
|
||||
@@ -80,8 +86,10 @@ impl App {
|
||||
Self {
|
||||
pod_name,
|
||||
connected: false,
|
||||
pod_status: PodStatus::Idle,
|
||||
running: false,
|
||||
paused: false,
|
||||
busy: false,
|
||||
run_requests: 0,
|
||||
run_upload_tokens: 0,
|
||||
run_output_tokens: 0,
|
||||
@@ -99,6 +107,16 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_pod_status(&mut self, status: PodStatus) {
|
||||
self.pod_status = status;
|
||||
self.running = status == PodStatus::Running;
|
||||
self.paused = status == PodStatus::Paused;
|
||||
self.busy = status == PodStatus::Busy;
|
||||
if self.running || self.busy {
|
||||
self.quit_confirm = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-evaluate the completion popup against the current input.
|
||||
/// Returns a `Method::ListCompletions` to send when the
|
||||
/// `(kind, prefix_start, prefix)` triple changed; otherwise `None`.
|
||||
@@ -278,6 +296,10 @@ impl App {
|
||||
}
|
||||
|
||||
pub fn submit_input(&mut self) -> Option<Method> {
|
||||
if self.busy {
|
||||
self.push_error("Pod is finishing post-run work; wait for idle before submitting.");
|
||||
return None;
|
||||
}
|
||||
let segments = self.input.submit_segments();
|
||||
if segments_are_blank(&segments) {
|
||||
// Empty Enter only does something meaningful when the Pod
|
||||
@@ -450,8 +472,7 @@ impl App {
|
||||
self.assistant_streaming = false;
|
||||
}
|
||||
Event::TurnStart { .. } => {
|
||||
self.running = true;
|
||||
self.paused = false;
|
||||
self.set_pod_status(PodStatus::Running);
|
||||
self.run_requests += 1;
|
||||
self.current_tool = None;
|
||||
self.assistant_streaming = false;
|
||||
@@ -617,8 +638,10 @@ impl App {
|
||||
upload_tokens: self.run_upload_tokens,
|
||||
output_tokens: self.run_output_tokens,
|
||||
});
|
||||
self.running = false;
|
||||
self.paused = matches!(result, RunResult::Paused);
|
||||
self.set_pod_status(match result {
|
||||
RunResult::Paused => PodStatus::Paused,
|
||||
RunResult::Finished | RunResult::LimitReached => PodStatus::Busy,
|
||||
});
|
||||
self.run_requests = 0;
|
||||
self.run_upload_tokens = 0;
|
||||
self.run_output_tokens = 0;
|
||||
@@ -643,8 +666,16 @@ impl App {
|
||||
message: alert.message,
|
||||
});
|
||||
}
|
||||
Event::History { items, greeting } => {
|
||||
Event::History {
|
||||
items,
|
||||
greeting,
|
||||
status,
|
||||
} => {
|
||||
self.restore_history(&items, greeting);
|
||||
self.set_pod_status(status);
|
||||
}
|
||||
Event::Status { status } => {
|
||||
self.set_pod_status(status);
|
||||
}
|
||||
Event::Completions { kind, entries } => {
|
||||
// Apply only if the popup is still on the same
|
||||
@@ -1216,8 +1247,11 @@ mod completion_flow_tests {
|
||||
"text": "[File: src/main.rs]\nfn main() {}",
|
||||
}],
|
||||
})],
|
||||
status: PodStatus::Running,
|
||||
});
|
||||
|
||||
assert!(matches!(app.pod_status, PodStatus::Running));
|
||||
assert!(app.running);
|
||||
assert!(matches!(
|
||||
app.blocks.get(1),
|
||||
Some(Block::SystemMessage { text }) if text == "[File: src/main.rs]\nfn main() {}"
|
||||
|
||||
@@ -35,6 +35,10 @@ impl PodClient {
|
||||
self.writer.write(method).await
|
||||
}
|
||||
|
||||
pub fn try_next_event(&mut self) -> Option<Event> {
|
||||
self.event_rx.try_recv().ok()
|
||||
}
|
||||
|
||||
pub async fn next_event(&mut self) -> Option<Event> {
|
||||
self.event_rx.recv().await
|
||||
}
|
||||
|
||||
+72
-29
@@ -21,7 +21,7 @@ use crossterm::execute;
|
||||
use crossterm::terminal::{
|
||||
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
|
||||
};
|
||||
use protocol::Method;
|
||||
use protocol::{Method, PodStatus};
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use session_store::SessionId;
|
||||
@@ -283,29 +283,44 @@ async fn run_loop(
|
||||
break;
|
||||
}
|
||||
|
||||
// Drain any already-buffered Pod events in a bounded batch before
|
||||
// polling the terminal. This keeps status fresh without letting a
|
||||
// busy event stream starve Ctrl-C / Ctrl-X input.
|
||||
for _ in 0..32 {
|
||||
match client.try_next_event() {
|
||||
Some(ev) => app.handle_pod_event(ev),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Always give the terminal queue a non-blocking pass each frame.
|
||||
// The awaited select below only waits after this pass found nothing.
|
||||
let mut handled_term_event = false;
|
||||
while event::poll(std::time::Duration::ZERO)? {
|
||||
handled_term_event = true;
|
||||
handle_terminal_event(app, &mut client, event::read()?).await?;
|
||||
if app.quit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if app.quit {
|
||||
break;
|
||||
}
|
||||
if handled_term_event {
|
||||
terminal.draw(|f| ui::draw(f, app))?;
|
||||
continue;
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::task::spawn_blocking(|| event::poll(std::time::Duration::from_millis(50))) => {
|
||||
while event::poll(std::time::Duration::ZERO)? {
|
||||
match event::read()? {
|
||||
TermEvent::Key(key) => {
|
||||
if let Some(method) = handle_key(app, key) {
|
||||
client.send(&method).await?;
|
||||
}
|
||||
}
|
||||
TermEvent::Mouse(mouse) => {
|
||||
handle_mouse(app, mouse);
|
||||
}
|
||||
TermEvent::Paste(s) => {
|
||||
app.insert_paste(s);
|
||||
}
|
||||
TermEvent::Resize(_, _) => {
|
||||
// No-op: next draw repaints in full.
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if app.quit {
|
||||
break;
|
||||
}
|
||||
term_event = tokio::task::spawn_blocking(|| {
|
||||
if event::poll(std::time::Duration::from_millis(50))? {
|
||||
event::read().map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}) => {
|
||||
if let Some(term_event) = term_event?? {
|
||||
handle_terminal_event(app, &mut client, term_event).await?;
|
||||
}
|
||||
}
|
||||
event = client.next_event(), if app.connected => {
|
||||
@@ -325,6 +340,31 @@ async fn run_loop(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_terminal_event(
|
||||
app: &mut App,
|
||||
client: &mut PodClient,
|
||||
event: TermEvent,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match event {
|
||||
TermEvent::Key(key) => {
|
||||
if let Some(method) = handle_key(app, key) {
|
||||
client.send(&method).await?;
|
||||
}
|
||||
}
|
||||
TermEvent::Mouse(mouse) => {
|
||||
handle_mouse(app, mouse);
|
||||
}
|
||||
TermEvent::Paste(s) => {
|
||||
app.insert_paste(s);
|
||||
}
|
||||
TermEvent::Resize(_, _) => {
|
||||
// No-op: next draw repaints in full.
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_disconnected(_app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
loop {
|
||||
if event::poll(std::time::Duration::from_millis(100))?
|
||||
@@ -392,10 +432,13 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option<Method> {
|
||||
Some(app.refresh_completion())
|
||||
}
|
||||
KeyCode::Char('c') if ctrl => Some(handle_pause_or_quit(app)),
|
||||
KeyCode::Char('x') if ctrl => Some(if app.running {
|
||||
Some(Method::Cancel)
|
||||
} else {
|
||||
Some(Method::Shutdown)
|
||||
KeyCode::Char('x') if ctrl => Some(match app.pod_status {
|
||||
PodStatus::Running => Some(Method::Cancel),
|
||||
PodStatus::Paused | PodStatus::Idle => Some(Method::Shutdown),
|
||||
PodStatus::Busy => {
|
||||
app.push_error("Pod is finishing post-run work; wait for idle or press Ctrl-C twice to exit the TUI.");
|
||||
None
|
||||
}
|
||||
}),
|
||||
KeyCode::Char('d') if ctrl => {
|
||||
app.quit = true;
|
||||
@@ -534,9 +577,9 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option<Method> {
|
||||
const CONFIRM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
|
||||
|
||||
/// Running → send `Method::Pause`.
|
||||
/// Idle / Paused → 2-tap to quit the TUI (the Pod keeps running).
|
||||
/// Idle / Paused / Busy → 2-tap to quit the TUI (the Pod keeps running).
|
||||
fn handle_pause_or_quit(app: &mut App) -> Option<Method> {
|
||||
if app.running {
|
||||
if app.pod_status == PodStatus::Running {
|
||||
return Some(Method::Pause);
|
||||
}
|
||||
if let Some(t) = app.quit_confirm
|
||||
|
||||
@@ -877,6 +877,18 @@ fn draw_status(frame: &mut Frame, app: &App, area: Rect) {
|
||||
" — Enter to resume, type to start new turn",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
} else if app.busy {
|
||||
spans.push(Span::raw(" | "));
|
||||
spans.push(Span::styled(
|
||||
"busy",
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
spans.push(Span::styled(
|
||||
" — finishing post-run work",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
} else {
|
||||
spans.push(Span::styled(" idle", Style::default().fg(Color::DarkGray)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user