Pod切断時にTUIがハングする問題
This commit is contained in:
parent
601d93f8d0
commit
37e6301397
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user