TUI上のターンカウンタ・ターン統計の実装
This commit is contained in:
parent
0c9551eef0
commit
afabd3d7fd
2
TODO.md
2
TODO.md
|
|
@ -17,3 +17,5 @@
|
||||||
- [ ] コンテキスト圧縮 (Prune + Compact) → [tickets/context-compaction.md](tickets/context-compaction.md)
|
- [ ] コンテキスト圧縮 (Prune + Compact) → [tickets/context-compaction.md](tickets/context-compaction.md)
|
||||||
- [x] Protocol: request-response パターン (GetHistory等) → [tickets/request-response-protocol.md](tickets/request-response-protocol.md)
|
- [x] Protocol: request-response パターン (GetHistory等) → [tickets/request-response-protocol.md](tickets/request-response-protocol.md)
|
||||||
- [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md)
|
- [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md)
|
||||||
|
- [ ] session-store: persistence クレートの再構成(wrap廃止、リネーム) → [tickets/session-store-extraction.md](tickets/session-store-extraction.md)
|
||||||
|
- [ ] UI用トークン情報の記録(run stats の永続化、session-store 後)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,12 @@ 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 running: bool,
|
||||||
|
pub run_requests: usize,
|
||||||
|
pub run_input_tokens: u64,
|
||||||
|
pub run_output_tokens: u64,
|
||||||
|
pub turn_index: usize,
|
||||||
|
pub current_tool: Option<String>,
|
||||||
pub input: String,
|
pub input: String,
|
||||||
pub cursor: usize,
|
pub cursor: usize,
|
||||||
pub scroll: u16,
|
pub scroll: u16,
|
||||||
|
|
@ -19,10 +24,12 @@ pub struct Message {
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum MessageKind {
|
pub enum MessageKind {
|
||||||
|
TurnHeader,
|
||||||
User,
|
User,
|
||||||
Assistant,
|
Assistant,
|
||||||
Tool,
|
Tool,
|
||||||
Error,
|
Error,
|
||||||
|
TurnStats,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
|
@ -32,7 +39,12 @@ impl App {
|
||||||
connected: false,
|
connected: false,
|
||||||
messages: Vec::new(),
|
messages: Vec::new(),
|
||||||
current_text: String::new(),
|
current_text: String::new(),
|
||||||
status_line: String::new(),
|
running: false,
|
||||||
|
run_requests: 0,
|
||||||
|
run_input_tokens: 0,
|
||||||
|
run_output_tokens: 0,
|
||||||
|
turn_index: 0,
|
||||||
|
current_tool: None,
|
||||||
input: String::new(),
|
input: String::new(),
|
||||||
cursor: 0,
|
cursor: 0,
|
||||||
scroll: 0,
|
scroll: 0,
|
||||||
|
|
@ -45,6 +57,11 @@ impl App {
|
||||||
if text.is_empty() {
|
if text.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
self.turn_index += 1;
|
||||||
|
self.messages.push(Message {
|
||||||
|
kind: MessageKind::TurnHeader,
|
||||||
|
content: format!("#{}", self.turn_index),
|
||||||
|
});
|
||||||
self.messages.push(Message {
|
self.messages.push(Message {
|
||||||
kind: MessageKind::User,
|
kind: MessageKind::User,
|
||||||
content: text.clone(),
|
content: text.clone(),
|
||||||
|
|
@ -57,8 +74,10 @@ 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 { .. } => {
|
||||||
self.status_line = format!("turn {turn}");
|
self.running = true;
|
||||||
|
self.run_requests += 1;
|
||||||
|
self.current_tool = None;
|
||||||
}
|
}
|
||||||
Event::TextDelta { text } => {
|
Event::TextDelta { text } => {
|
||||||
self.current_text.push_str(&text);
|
self.current_text.push_str(&text);
|
||||||
|
|
@ -73,7 +92,7 @@ impl App {
|
||||||
self.scroll_to_bottom();
|
self.scroll_to_bottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::TurnEnd { turn, result } => {
|
Event::TurnEnd { .. } => {
|
||||||
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,10 +100,10 @@ impl App {
|
||||||
content: text,
|
content: text,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
self.status_line = format!("turn {turn} {result:?}");
|
self.current_tool = None;
|
||||||
}
|
}
|
||||||
Event::ToolCallStart { name, .. } => {
|
Event::ToolCallStart { name, .. } => {
|
||||||
self.status_line = format!("tool: {name}");
|
self.current_tool = Some(name.clone());
|
||||||
self.messages.push(Message {
|
self.messages.push(Message {
|
||||||
kind: MessageKind::Tool,
|
kind: MessageKind::Tool,
|
||||||
content: format!("[tool] {name}"),
|
content: format!("[tool] {name}"),
|
||||||
|
|
@ -94,6 +113,7 @@ impl App {
|
||||||
Event::ToolCallDone {
|
Event::ToolCallDone {
|
||||||
name, arguments, ..
|
name, arguments, ..
|
||||||
} => {
|
} => {
|
||||||
|
self.current_tool = None;
|
||||||
self.messages.push(Message {
|
self.messages.push(Message {
|
||||||
kind: MessageKind::Tool,
|
kind: MessageKind::Tool,
|
||||||
content: format!("[tool] {name} done ({} bytes)", arguments.len()),
|
content: format!("[tool] {name} done ({} bytes)", arguments.len()),
|
||||||
|
|
@ -119,10 +139,8 @@ impl App {
|
||||||
input_tokens,
|
input_tokens,
|
||||||
output_tokens,
|
output_tokens,
|
||||||
} => {
|
} => {
|
||||||
let in_t = input_tokens.unwrap_or(0);
|
self.run_input_tokens += input_tokens.unwrap_or(0);
|
||||||
let out_t = output_tokens.unwrap_or(0);
|
self.run_output_tokens += output_tokens.unwrap_or(0);
|
||||||
self.status_line
|
|
||||||
.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 {
|
||||||
|
|
@ -131,8 +149,22 @@ impl App {
|
||||||
});
|
});
|
||||||
self.scroll_to_bottom();
|
self.scroll_to_bottom();
|
||||||
}
|
}
|
||||||
Event::RunEnd { result } => {
|
Event::RunEnd { .. } => {
|
||||||
self.status_line = format!("{result:?}");
|
self.messages.push(Message {
|
||||||
|
kind: MessageKind::TurnStats,
|
||||||
|
content: format!(
|
||||||
|
"{} reqs ↑{}/↓{}",
|
||||||
|
self.run_requests,
|
||||||
|
fmt_tokens(self.run_input_tokens),
|
||||||
|
fmt_tokens(self.run_output_tokens),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
self.running = false;
|
||||||
|
self.run_requests = 0;
|
||||||
|
self.run_input_tokens = 0;
|
||||||
|
self.run_output_tokens = 0;
|
||||||
|
self.current_tool = None;
|
||||||
|
self.scroll_to_bottom();
|
||||||
}
|
}
|
||||||
Event::ToolCallArgsDelta { .. } => {}
|
Event::ToolCallArgsDelta { .. } => {}
|
||||||
Event::History { items } => {
|
Event::History { items } => {
|
||||||
|
|
@ -220,13 +252,21 @@ impl App {
|
||||||
|
|
||||||
fn restore_history(&mut self, items: &[serde_json::Value]) {
|
fn restore_history(&mut self, items: &[serde_json::Value]) {
|
||||||
self.messages.clear();
|
self.messages.clear();
|
||||||
|
self.turn_index = 0;
|
||||||
for item in items {
|
for item in items {
|
||||||
let item_type = item["type"].as_str().unwrap_or("");
|
let item_type = item["type"].as_str().unwrap_or("");
|
||||||
match item_type {
|
match item_type {
|
||||||
"message" => {
|
"message" => {
|
||||||
let role = item["role"].as_str().unwrap_or("");
|
let role = item["role"].as_str().unwrap_or("");
|
||||||
let kind = match role {
|
let kind = match role {
|
||||||
"user" => MessageKind::User,
|
"user" => {
|
||||||
|
self.turn_index += 1;
|
||||||
|
self.messages.push(Message {
|
||||||
|
kind: MessageKind::TurnHeader,
|
||||||
|
content: format!("#{}", self.turn_index),
|
||||||
|
});
|
||||||
|
MessageKind::User
|
||||||
|
}
|
||||||
"assistant" => MessageKind::Assistant,
|
"assistant" => MessageKind::Assistant,
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
|
|
@ -276,3 +316,13 @@ impl App {
|
||||||
self.scroll = u16::MAX;
|
self.scroll = u16::MAX;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn fmt_tokens(n: u64) -> String {
|
||||||
|
if n >= 1_000_000 {
|
||||||
|
format!("{:.1}M", n as f64 / 1_000_000.0)
|
||||||
|
} else if n >= 1_000 {
|
||||||
|
format!("{:.1}k", n as f64 / 1_000.0)
|
||||||
|
} else {
|
||||||
|
n.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
use ratatui::layout::{Constraint, Layout, Position};
|
use ratatui::layout::{Alignment, 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::{Paragraph, Wrap};
|
use ratatui::widgets::{Paragraph, Wrap};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
use crate::app::{App, MessageKind};
|
use crate::app::{fmt_tokens, 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([
|
||||||
|
|
@ -28,9 +28,14 @@ fn draw_messages(frame: &mut Frame, app: &mut App, area: ratatui::layout::Rect)
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|(kind, content)| {
|
.flat_map(|(kind, content)| {
|
||||||
let style = kind_style(kind);
|
let style = kind_style(kind);
|
||||||
|
let align = if matches!(kind, MessageKind::TurnStats) {
|
||||||
|
Alignment::Right
|
||||||
|
} else {
|
||||||
|
Alignment::Left
|
||||||
|
};
|
||||||
content
|
content
|
||||||
.lines()
|
.lines()
|
||||||
.map(move |l| Line::from(Span::styled(l.to_owned(), style)))
|
.map(move |l| Line::from(Span::styled(l.to_owned(), style)).alignment(align))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
@ -72,17 +77,32 @@ fn draw_status(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
if !app.status_line.is_empty() {
|
if app.running {
|
||||||
|
let status = if let Some(tool) = &app.current_tool {
|
||||||
|
format!(
|
||||||
|
"request: {} | ↑{}/↓{} | tool: {tool}",
|
||||||
|
app.run_requests,
|
||||||
|
fmt_tokens(app.run_input_tokens),
|
||||||
|
fmt_tokens(app.run_output_tokens),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"request: {} | ↑{}/↓{}",
|
||||||
|
app.run_requests,
|
||||||
|
fmt_tokens(app.run_input_tokens),
|
||||||
|
fmt_tokens(app.run_output_tokens),
|
||||||
|
)
|
||||||
|
};
|
||||||
spans.push(Span::raw(" | "));
|
spans.push(Span::raw(" | "));
|
||||||
spans.push(Span::styled(
|
spans.push(Span::styled(status, Style::default().fg(Color::Yellow)));
|
||||||
&app.status_line,
|
} else {
|
||||||
Style::default().fg(Color::Yellow),
|
spans.push(Span::styled(" idle", Style::default().fg(Color::DarkGray)));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.render_widget(Paragraph::new(Line::from(spans)), area);
|
frame.render_widget(Paragraph::new(Line::from(spans)), area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn draw_input(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
fn draw_input(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
||||||
let line = Line::from(vec![
|
let line = Line::from(vec![
|
||||||
Span::styled("> ", Style::default().fg(Color::DarkGray)),
|
Span::styled("> ", Style::default().fg(Color::DarkGray)),
|
||||||
|
|
@ -97,9 +117,11 @@ fn draw_input(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
||||||
|
|
||||||
fn kind_style(kind: &MessageKind) -> Style {
|
fn kind_style(kind: &MessageKind) -> Style {
|
||||||
match kind {
|
match kind {
|
||||||
|
MessageKind::TurnHeader => Style::default().fg(Color::DarkGray),
|
||||||
MessageKind::User => Style::default().fg(Color::Green),
|
MessageKind::User => Style::default().fg(Color::Green),
|
||||||
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::TurnStats => Style::default().fg(Color::DarkGray),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user