feat(coder-lite): move usage to a bottom status bar

7ad52a0c8dd0 · AtlantisPleb · · parent 341e7f9838d0

feat(coder-lite): move usage to a bottom status bar

Per-turn token notices are no longer pushed as transcript entries.
Instead, usage is accumulated in CoderUi.total_usage and rendered as a
right-aligned status bar one row below the input box (the input box is
now one row above the bottom of the screen). Updated frame tests to
match the new behavior.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified crates/coder-lite/src/interactive.rs
  • modified crates/coder-lite/src/tui.rs
  • modified crates/coder-lite/tests/frame.rs
  • modified crates/openagents-cli/src/runtime.rs

Diff

4 files changed, +35 -9

crates/coder-lite/src/interactive.rs modified +1 -1

@@ -397,7 +397,7 @@ pub fn apply(ui: &mut CoderUi, control: Control) {

397 397
        Control::Model(model) => ui.model = model,
398 398
        Control::Usage(usage) => {
399 399
            if usage.reported() {
400
                ui.entries.push(Entry::new(Role::Notice, usage.line()));
400
                ui.add_usage(usage);
401 401
            }
402 402
        }
403 403
        Control::Notice(text) => {
crates/coder-lite/src/tui.rs modified +28 -4

@@ -15,6 +15,7 @@ use openagents_cli::composer::Composer;

15 15
use crate::markdown::theme::{BACKGROUND_COLOR, DIM_TEXT_COLOR, TEXT_COLOR};
16 16
use crate::osc8::PlacedLink;
17 17
use crate::transcript::MarkdownContent;
18
use openagents_cli::runtime::TurnUsage;
18 19
19 20
const SPINNER_FRAMES: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
20 21

@@ -172,6 +173,7 @@ pub struct CoderUi {

172 173
    pub transcript_height: u16,
173 174
    pub loading: bool,
174 175
    pub tick: u64,
176
    pub total_usage: TurnUsage,
175 177
    pub agents: Vec<crate::acp::Agent>,
176 178
    /// Hyperlinks on the last rendered frame, in absolute screen coordinates.
177 179
    ///

@@ -230,11 +232,17 @@ impl CoderUi {

230 232
            transcript_height: 0,
231 233
            loading: false,
232 234
            tick: 0,
235
            total_usage: TurnUsage::default(),
233 236
            agents: Vec::new(),
234 237
            links: Vec::new(),
235 238
        }
236 239
    }
237 240
241
    /// Add a turn's reported usage to the running conversation total.
242
    pub fn add_usage(&mut self, usage: TurnUsage) {
243
        self.total_usage.add(usage);
244
    }
245
238 246
    pub fn render(&mut self, frame: &mut Frame, area: Rect) {
239 247
        self.tick = self.tick.wrapping_add(1);
240 248
        let style = Style::default().fg(TEXT_COLOR).bg(BACKGROUND_COLOR);

@@ -262,12 +270,16 @@ impl CoderUi {

262 270
        let input_scroll = total_input_lines.saturating_sub(visible_input_lines);
263 271
        let input_box_height = visible_input_lines + 2;
264 272
265
        let main_bottom = Layout::default()
273
        let main = Layout::default()
266 274
            .direction(Direction::Vertical)
267
            .constraints([Constraint::Min(0), Constraint::Length(input_box_height)])
275
            .constraints([
276
                Constraint::Min(0),
277
                Constraint::Length(input_box_height),
278
                Constraint::Length(1),
279
            ])
268 280
            .split(area);
269 281
270
        let transcript_area = main_bottom[0];
282
        let transcript_area = main[0];
271 283
        let width = transcript_area.width as usize;
272 284
273 285
        let mut all_lines: Vec<Line<'static>> = Vec::new();

@@ -300,7 +312,7 @@ impl CoderUi {

300 312
            .style(style);
301 313
        frame.render_widget(transcript, transcript_area);
302 314
303
        let input_area = main_bottom[1];
315
        let input_area = main[1];
304 316
305 317
        let mut input_lines = Vec::new();
306 318
        for (i, chunk) in input_chunks.iter().enumerate().skip(input_scroll as usize) {

@@ -326,6 +338,18 @@ impl CoderUi {

326 338
        let cursor_x = input_area.x + 1 + 3 + caret_col as u16;
327 339
        let cursor_y = input_area.y + 1 + caret_screen_row.min(visible_input_lines.saturating_sub(1));
328 340
        frame.set_cursor_position(Position::new(cursor_x, cursor_y));
341
342
        let status_area = main[2];
343
        let status = format!(
344
            "{} prompt + {} completion = {} tokens",
345
            self.total_usage.prompt_tokens,
346
            self.total_usage.completion_tokens,
347
            self.total_usage.total_tokens
348
        );
349
        let status_widget = Paragraph::new(status)
350
            .style(style)
351
            .alignment(ratatui::layout::Alignment::Right);
352
        frame.render_widget(status_widget, status_area);
329 353
        // A block cursor, as grok-build's textarea draws one: the hardware
330 354
        // cursor alone is easy to lose in the alternate screen, and a trailing
331 355
        // space with nothing over it looks like a line that ends earlier than
crates/coder-lite/tests/frame.rs modified +5 -3

@@ -121,12 +121,13 @@ fn output_for_an_unknown_call_lands_on_nothing() {

121 121
    );
122 122
}
123 123
124
/// Usage is shown only when the server reported some.
124
/// Usage is accumulated and never added to the transcript.
125 125
#[test]
126 126
fn an_unreported_usage_is_not_printed_as_zero() {
127 127
    let mut ui = CoderUi::new();
128 128
    apply(&mut ui, Control::Usage(TurnUsage::default()));
129 129
    assert!(ui.entries.is_empty(), "a zero was reported as a figure");
130
    assert!(!ui.total_usage.reported());
130 131
131 132
    apply(
132 133
        &mut ui,

@@ -136,8 +137,9 @@ fn an_unreported_usage_is_not_printed_as_zero() {

136 137
            total_tokens: 15,
137 138
        }),
138 139
    );
139
    assert_eq!(ui.entries.len(), 1);
140
    assert!(ui.entries[0].text.contains("15 tokens"), "{:?}", ui.entries[0].text);
140
    assert!(ui.entries.is_empty(), "usage was pushed as a transcript entry");
141
    assert!(ui.total_usage.reported());
142
    assert_eq!(ui.total_usage.total_tokens, 15);
141 143
}
142 144
143 145
/// What `/diff` and `/help` print is still coder-lite.
crates/openagents-cli/src/runtime.rs modified +1 -1

@@ -291,7 +291,7 @@ pub struct TurnUsage {

291 291
}
292 292
293 293
impl TurnUsage {
294
    fn add(&mut self, other: TurnUsage) {
294
    pub fn add(&mut self, other: TurnUsage) {
295 295
        self.prompt_tokens += other.prompt_tokens;
296 296
        self.completion_tokens += other.completion_tokens;
297 297
        self.total_tokens += other.total_tokens;

This page updates live while a promote is in flight · changelog