Pod切断時にTUIがハングする問題

This commit is contained in:
Keisuke Hirata 2026-04-12 04:22:26 +09:00
parent 601d93f8d0
commit 37e6301397
3 changed files with 69 additions and 62 deletions

View File

@ -5,6 +5,7 @@ pub struct App {
pub connected: bool, pub connected: bool,
pub messages: Vec<Message>, pub messages: Vec<Message>,
pub current_text: String, pub current_text: String,
pub status_line: String,
pub input: String, pub input: String,
pub cursor: usize, pub cursor: usize,
pub scroll: u16, pub scroll: u16,
@ -22,7 +23,6 @@ pub enum MessageKind {
Assistant, Assistant,
Tool, Tool,
Error, Error,
Status,
} }
impl App { impl App {
@ -32,6 +32,7 @@ impl App {
connected: false, connected: false,
messages: Vec::new(), messages: Vec::new(),
current_text: String::new(), current_text: String::new(),
status_line: String::new(),
input: String::new(), input: String::new(),
cursor: 0, cursor: 0,
scroll: 0, scroll: 0,
@ -57,7 +58,7 @@ impl App {
pub fn handle_pod_event(&mut self, event: Event) { pub fn handle_pod_event(&mut self, event: Event) {
match event { match event {
Event::TurnStart { turn } => { Event::TurnStart { turn } => {
self.push_status(format!("[turn {turn}] start")); self.status_line = format!("turn {turn}");
} }
Event::TextDelta { text } => { Event::TextDelta { text } => {
self.current_text.push_str(&text); self.current_text.push_str(&text);
@ -73,7 +74,6 @@ impl App {
} }
} }
Event::TurnEnd { turn, result } => { Event::TurnEnd { turn, result } => {
// Flush any remaining text delta
if !self.current_text.is_empty() { if !self.current_text.is_empty() {
let text = std::mem::take(&mut self.current_text); let text = std::mem::take(&mut self.current_text);
self.messages.push(Message { self.messages.push(Message {
@ -81,9 +81,10 @@ impl App {
content: text, content: text,
}); });
} }
self.push_status(format!("[turn {turn}] end ({result:?})")); self.status_line = format!("turn {turn} {result:?}");
} }
Event::ToolCallStart { name, .. } => { Event::ToolCallStart { name, .. } => {
self.status_line = format!("tool: {name}");
self.messages.push(Message { self.messages.push(Message {
kind: MessageKind::Tool, kind: MessageKind::Tool,
content: format!("[tool] {name}"), content: format!("[tool] {name}"),
@ -118,11 +119,10 @@ impl App {
input_tokens, input_tokens,
output_tokens, output_tokens,
} => { } => {
self.push_status(format!( let in_t = input_tokens.unwrap_or(0);
"[usage] in={} out={}", let out_t = output_tokens.unwrap_or(0);
input_tokens.unwrap_or(0), self.status_line
output_tokens.unwrap_or(0), .push_str(&format!(" | in:{in_t} out:{out_t}"));
));
} }
Event::Error { code, message } => { Event::Error { code, message } => {
self.messages.push(Message { self.messages.push(Message {
@ -132,7 +132,7 @@ impl App {
self.scroll_to_bottom(); self.scroll_to_bottom();
} }
Event::RunEnd { result } => { Event::RunEnd { result } => {
self.push_status(format!("[run end] {result:?}")); self.status_line = format!("{result:?}");
} }
Event::ToolCallArgsDelta { .. } => {} Event::ToolCallArgsDelta { .. } => {}
Event::History { items } => { Event::History { items } => {
@ -271,14 +271,6 @@ impl App {
self.scroll_to_bottom(); self.scroll_to_bottom();
} }
fn push_status(&mut self, content: String) {
self.messages.push(Message {
kind: MessageKind::Status,
content,
});
self.scroll_to_bottom();
}
fn scroll_to_bottom(&mut self) { fn scroll_to_bottom(&mut self) {
// Will be clamped during rendering // Will be clamped during rendering
self.scroll = u16::MAX; self.scroll = u16::MAX;

View File

@ -121,8 +121,8 @@ async fn run_loop(
} }
} }
} }
// Pod events // Pod events (disabled after disconnect)
event = client.next_event() => { event = client.next_event(), if app.connected => {
match event { match event {
Some(ev) => app.handle_pod_event(ev), Some(ev) => app.handle_pod_event(ev),
None => { None => {

View File

@ -1,81 +1,97 @@
use ratatui::layout::{Constraint, Layout, Position}; use ratatui::layout::{Constraint, Layout, Position};
use ratatui::style::{Color, Modifier, Style}; use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span}; use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Wrap}; use ratatui::widgets::{Paragraph, Wrap};
use ratatui::Frame; use ratatui::Frame;
use crate::app::{App, MessageKind}; use crate::app::{App, MessageKind};
pub fn draw(frame: &mut Frame, app: &mut App) { pub fn draw(frame: &mut Frame, app: &mut App) {
let chunks = Layout::vertical([ let chunks = Layout::vertical([
Constraint::Length(1), Constraint::Min(1), // messages (scroll area)
Constraint::Min(3), Constraint::Length(1), // separator
Constraint::Length(3), Constraint::Length(1), // status line
Constraint::Length(1), // input
]) ])
.split(frame.area()); .split(frame.area());
draw_status_bar(frame, app, chunks[0]); draw_messages(frame, app, chunks[0]);
draw_output(frame, app, chunks[1]); draw_separator(frame, chunks[1]);
draw_input(frame, app, chunks[2]); draw_status(frame, app, chunks[2]);
draw_input(frame, app, chunks[3]);
} }
fn draw_status_bar(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) { fn draw_messages(frame: &mut Frame, app: &mut App, area: ratatui::layout::Rect) {
let conn_style = if app.connected {
Style::default().fg(Color::Green)
} else {
Style::default().fg(Color::Red)
};
let conn_text = if app.connected {
"connected"
} else {
"disconnected"
};
let line = Line::from(vec![
Span::styled(&app.pod_name, Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" | "),
Span::styled(conn_text, conn_style),
]);
frame.render_widget(Paragraph::new(line), area);
}
fn draw_output(frame: &mut Frame, app: &mut App, area: ratatui::layout::Rect) {
let display = app.display_lines(); let display = app.display_lines();
let lines: Vec<Line> = display let lines: Vec<Line> = display
.iter() .iter()
.flat_map(|(kind, content)| { .flat_map(|(kind, content)| {
let style = kind_style(kind); let style = kind_style(kind);
content.lines().map(move |l| Line::from(Span::styled(l.to_owned(), style))) content
.lines()
.map(move |l| Line::from(Span::styled(l.to_owned(), style)))
}) })
.collect(); .collect();
let total = lines.len() as u16; let total = lines.len() as u16;
let visible = area.height.saturating_sub(2); // block borders let max_scroll = total.saturating_sub(area.height);
let max_scroll = total.saturating_sub(visible);
if app.scroll > max_scroll { if app.scroll > max_scroll {
app.scroll = max_scroll; app.scroll = max_scroll;
} }
let block = Block::default().borders(Borders::ALL);
let paragraph = Paragraph::new(lines) let paragraph = Paragraph::new(lines)
.block(block)
.wrap(Wrap { trim: false }) .wrap(Wrap { trim: false })
.scroll((app.scroll, 0)); .scroll((app.scroll, 0));
frame.render_widget(paragraph, area); frame.render_widget(paragraph, area);
} }
fn draw_input(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) { fn draw_separator(frame: &mut Frame, area: ratatui::layout::Rect) {
let display = format!("> {}", app.input); let line = "".repeat(area.width as usize);
let block = Block::default().borders(Borders::ALL).title("Input"); let paragraph = Paragraph::new(Line::from(Span::styled(
let paragraph = Paragraph::new(display).block(block); line,
Style::default().fg(Color::DarkGray),
)));
frame.render_widget(paragraph, area); frame.render_widget(paragraph, area);
}
// Cursor position: "> " is 2 chars, plus cursor offset in the input fn draw_status(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
let cursor_x = area.x + 1 + 2 + app.input[..app.cursor].chars().count() as u16; let conn = if app.connected {
let cursor_y = area.y + 1; Span::styled("", Style::default().fg(Color::Green))
} else {
Span::styled("", Style::default().fg(Color::Red))
};
let mut spans = vec![
conn,
Span::raw(" "),
Span::styled(
&app.pod_name,
Style::default().add_modifier(Modifier::BOLD),
),
];
if !app.status_line.is_empty() {
spans.push(Span::raw(" | "));
spans.push(Span::styled(
&app.status_line,
Style::default().fg(Color::Yellow),
));
}
frame.render_widget(Paragraph::new(Line::from(spans)), area);
}
fn draw_input(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
let line = Line::from(vec![
Span::styled("> ", Style::default().fg(Color::DarkGray)),
Span::raw(&app.input),
]);
frame.render_widget(Paragraph::new(line), area);
let cursor_x = area.x + 2 + app.input[..app.cursor].chars().count() as u16;
let cursor_y = area.y;
frame.set_cursor_position(Position::new(cursor_x, cursor_y)); frame.set_cursor_position(Position::new(cursor_x, cursor_y));
} }
@ -85,6 +101,5 @@ fn kind_style(kind: &MessageKind) -> Style {
MessageKind::Assistant => Style::default().fg(Color::White), MessageKind::Assistant => Style::default().fg(Color::White),
MessageKind::Tool => Style::default().fg(Color::Cyan), MessageKind::Tool => Style::default().fg(Color::Cyan),
MessageKind::Error => Style::default().fg(Color::Red), MessageKind::Error => Style::default().fg(Color::Red),
MessageKind::Status => Style::default().fg(Color::DarkGray),
} }
} }