fix: slim down token usage
This commit is contained in:
299
src/app/runtime/events.rs
Normal file
299
src/app/runtime/events.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::app::{App, AppEvent, WorkspaceRuntime};
|
||||
use crate::model::{
|
||||
group_session_entries, ControllerState, SessionEntry, SessionSource, SessionStream,
|
||||
};
|
||||
use crate::repo;
|
||||
use crate::ui::{self, SessionRenderRow};
|
||||
|
||||
use super::usage;
|
||||
|
||||
pub(super) fn drain_workspace_events(app: &mut App) -> Result<()> {
|
||||
let Some(workspace) = app.workspace.as_mut() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut events = Vec::new();
|
||||
while let Ok(event) = workspace.event_rx.try_recv() {
|
||||
events.push(event);
|
||||
}
|
||||
for event in events {
|
||||
apply_event(app, event);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn push_local_entry(
|
||||
app: &mut App,
|
||||
source: SessionSource,
|
||||
stream: SessionStream,
|
||||
title: &str,
|
||||
tag: Option<String>,
|
||||
body: &str,
|
||||
) {
|
||||
let Some(workspace) = app.workspace.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
workspace.session_follow_output = true;
|
||||
reset_session_interaction(workspace);
|
||||
workspace.session_entries.push(SessionEntry {
|
||||
source,
|
||||
stream,
|
||||
title: title.to_string(),
|
||||
tag,
|
||||
body: body.to_string(),
|
||||
run_id: repo::next_run_id(),
|
||||
});
|
||||
workspace.session_rows = rebuild_session_rows(&workspace.session_entries);
|
||||
workspace.session_view = None;
|
||||
}
|
||||
|
||||
pub(super) fn rebuild_session_rows(entries: &[SessionEntry]) -> Vec<SessionRenderRow> {
|
||||
ui::build_session_render_rows(&group_session_entries(entries))
|
||||
}
|
||||
|
||||
fn apply_event(app: &mut App, event: AppEvent) {
|
||||
let Some(workspace) = app.workspace.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
match event {
|
||||
AppEvent::Session(entry) => {
|
||||
let should_keep_following = workspace.session_follow_output
|
||||
|| workspace.session_scrollbar.position_lines
|
||||
>= workspace.session_scrollbar.scroll_range().saturating_sub(1);
|
||||
reset_session_interaction(workspace);
|
||||
workspace.session_entries.push(entry);
|
||||
workspace.session_rows = rebuild_session_rows(&workspace.session_entries);
|
||||
workspace.session_view = None;
|
||||
workspace.session_follow_output = should_keep_following;
|
||||
}
|
||||
AppEvent::Snapshot {
|
||||
goal_md,
|
||||
standards_md,
|
||||
plan,
|
||||
state,
|
||||
} => apply_snapshot(workspace, goal_md, standards_md, plan, state),
|
||||
AppEvent::CodexUsage {
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
} => {
|
||||
*workspace.session_input_tokens.get_or_insert(0) += input_tokens;
|
||||
*workspace.session_output_tokens.get_or_insert(0) += output_tokens;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_snapshot(
|
||||
workspace: &mut WorkspaceRuntime,
|
||||
goal_md: String,
|
||||
standards_md: String,
|
||||
plan: crate::model::Plan,
|
||||
state: ControllerState,
|
||||
) {
|
||||
let should_keep_following_sidebar = workspace.sidebar_follow_output
|
||||
|| workspace.sidebar_scrollbar.position_lines
|
||||
>= workspace.sidebar_scrollbar.scroll_range().saturating_sub(1);
|
||||
let has_usage = usage::state_has_usage(&state);
|
||||
|
||||
workspace.goal_md = goal_md;
|
||||
workspace.standards_md = standards_md;
|
||||
workspace.plan = plan;
|
||||
workspace.state = state.clone();
|
||||
workspace.sidebar_view = None;
|
||||
workspace.sidebar_follow_output = should_keep_following_sidebar;
|
||||
|
||||
if has_usage {
|
||||
workspace.usage_snapshot = usage::usage_snapshot_from_state(&state);
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_session_interaction(workspace: &mut WorkspaceRuntime) {
|
||||
workspace.session_selection = None;
|
||||
workspace.session_drag_active = false;
|
||||
workspace.session_scrollbar.stop_arrow_hold();
|
||||
workspace.session_scrollbar.stop_drag();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::app::{ControlCommand, WorkspaceRuntime};
|
||||
use crate::cli::DEFAULT_TASK_CONFIG_PATH;
|
||||
use crate::model::{
|
||||
ControllerPhase, ControllerState, Plan, Screen, SessionCursor, SessionSelection,
|
||||
UsageSnapshot,
|
||||
};
|
||||
use crate::ui::{
|
||||
scroll::VerticalScrollState, SessionView, SessionViewMetrics, SidebarView,
|
||||
SidebarViewMetrics,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn sample_app() -> (App, mpsc::Sender<AppEvent>) {
|
||||
let (event_tx, event_rx) = mpsc::channel();
|
||||
let (control_tx, _control_rx) = mpsc::channel::<ControlCommand>();
|
||||
let mut workspace = WorkspaceRuntime {
|
||||
task_config: crate::model::TaskConfig::default_for("alpha"),
|
||||
goal_md: "goal".to_string(),
|
||||
standards_md: "standards".to_string(),
|
||||
plan: Plan::default(),
|
||||
state: ControllerState::default(),
|
||||
input: String::new(),
|
||||
session_entries: Vec::new(),
|
||||
event_rx,
|
||||
control_tx,
|
||||
session_input_tokens: None,
|
||||
session_output_tokens: None,
|
||||
usage_snapshot: UsageSnapshot::unavailable("usage unavailable"),
|
||||
last_usage_refresh: Instant::now(),
|
||||
session_follow_output: false,
|
||||
session_scrollbar: VerticalScrollState::new(false),
|
||||
session_rows: Vec::new(),
|
||||
session_view: Some(SessionView {
|
||||
metrics: SessionViewMetrics {
|
||||
text_rect: ratatui::layout::Rect::default(),
|
||||
scrollbar_rect: ratatui::layout::Rect::default(),
|
||||
total_lines: 0,
|
||||
has_scrollbar: false,
|
||||
},
|
||||
lines: Vec::new(),
|
||||
}),
|
||||
session_view_area: ratatui::layout::Rect::default(),
|
||||
sidebar_follow_output: false,
|
||||
sidebar_scrollbar: VerticalScrollState::new(false),
|
||||
sidebar_view: Some(SidebarView {
|
||||
metrics: SidebarViewMetrics {
|
||||
text_rect: ratatui::layout::Rect::default(),
|
||||
scrollbar_rect: ratatui::layout::Rect::default(),
|
||||
total_lines: 0,
|
||||
has_scrollbar: false,
|
||||
},
|
||||
lines: Vec::new(),
|
||||
}),
|
||||
sidebar_view_area: ratatui::layout::Rect::default(),
|
||||
session_selection: Some(SessionSelection {
|
||||
anchor: SessionCursor { line: 0, column: 0 },
|
||||
focus: SessionCursor { line: 0, column: 4 },
|
||||
}),
|
||||
session_drag_active: true,
|
||||
};
|
||||
workspace.session_scrollbar.set_content_viewport(8, 3);
|
||||
workspace.sidebar_scrollbar.set_content_viewport(8, 3);
|
||||
|
||||
(
|
||||
App {
|
||||
screen: Screen::Workspace,
|
||||
picker_items: Vec::new(),
|
||||
picker_selected: 0,
|
||||
create_input: String::new(),
|
||||
create_error: None,
|
||||
default_task_path: PathBuf::from(DEFAULT_TASK_CONFIG_PATH),
|
||||
frame_tick: 0,
|
||||
workspace: Some(workspace),
|
||||
},
|
||||
event_tx,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_events_clear_selection_and_rebuild_rows() {
|
||||
let (mut app, event_tx) = sample_app();
|
||||
{
|
||||
let workspace = app.workspace.as_mut().expect("workspace");
|
||||
workspace.session_follow_output = false;
|
||||
workspace.session_scrollbar.position_lines = 0;
|
||||
}
|
||||
|
||||
event_tx
|
||||
.send(AppEvent::Session(SessionEntry {
|
||||
source: SessionSource::Planner,
|
||||
stream: SessionStream::Stdout,
|
||||
title: "Plan".to_string(),
|
||||
tag: Some("alpha".to_string()),
|
||||
body: "first line\nsecond line".to_string(),
|
||||
run_id: 7,
|
||||
}))
|
||||
.expect("send session event");
|
||||
|
||||
drain_workspace_events(&mut app).expect("drain events");
|
||||
|
||||
let workspace = app.workspace.as_ref().expect("workspace");
|
||||
assert_eq!(workspace.session_entries.len(), 1);
|
||||
assert!(workspace.session_selection.is_none());
|
||||
assert!(!workspace.session_drag_active);
|
||||
assert!(!workspace.session_follow_output);
|
||||
assert!(workspace.session_view.is_none());
|
||||
assert!(!workspace.session_rows.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_events_refresh_sidebar_and_cached_usage() {
|
||||
let (mut app, event_tx) = sample_app();
|
||||
{
|
||||
let workspace = app.workspace.as_mut().expect("workspace");
|
||||
workspace.sidebar_scrollbar.position_lines =
|
||||
workspace.sidebar_scrollbar.scroll_range().saturating_sub(1);
|
||||
}
|
||||
|
||||
let state = ControllerState {
|
||||
phase: ControllerPhase::Blocked,
|
||||
last_usage_refresh_at: Some("123".to_string()),
|
||||
last_usage_input_tokens: Some(21),
|
||||
last_usage_output_tokens: Some(34),
|
||||
..ControllerState::default()
|
||||
};
|
||||
event_tx
|
||||
.send(AppEvent::Snapshot {
|
||||
goal_md: "updated goal".to_string(),
|
||||
standards_md: "updated standards".to_string(),
|
||||
plan: Plan::default(),
|
||||
state,
|
||||
})
|
||||
.expect("send snapshot event");
|
||||
|
||||
drain_workspace_events(&mut app).expect("drain events");
|
||||
|
||||
let workspace = app.workspace.as_ref().expect("workspace");
|
||||
assert_eq!(workspace.goal_md, "updated goal");
|
||||
assert_eq!(workspace.standards_md, "updated standards");
|
||||
assert!(workspace.sidebar_follow_output);
|
||||
assert!(workspace.sidebar_view.is_none());
|
||||
assert_eq!(workspace.usage_snapshot.input_tokens, Some(21));
|
||||
assert_eq!(workspace.usage_snapshot.output_tokens, Some(34));
|
||||
assert_eq!(
|
||||
workspace.usage_snapshot.refreshed_at.as_deref(),
|
||||
Some("123")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_usage_events_accumulate_session_totals() {
|
||||
let (mut app, event_tx) = sample_app();
|
||||
|
||||
event_tx
|
||||
.send(AppEvent::CodexUsage {
|
||||
input_tokens: 4,
|
||||
output_tokens: 9,
|
||||
})
|
||||
.expect("send first usage");
|
||||
event_tx
|
||||
.send(AppEvent::CodexUsage {
|
||||
input_tokens: 6,
|
||||
output_tokens: 1,
|
||||
})
|
||||
.expect("send second usage");
|
||||
|
||||
drain_workspace_events(&mut app).expect("drain events");
|
||||
|
||||
let workspace = app.workspace.as_ref().expect("workspace");
|
||||
assert_eq!(workspace.session_input_tokens, Some(10));
|
||||
assert_eq!(workspace.session_output_tokens, Some(10));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user