fix: slim down token usage

This commit is contained in:
eric
2026-04-04 12:37:50 +02:00
parent 97f329c825
commit 1240ab946b
55 changed files with 6799 additions and 2333 deletions

299
src/app/runtime/events.rs Normal file
View 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));
}
}