T24f Agent: control and cost

HumanInputRequest asks a question with answers to pick and a line to
write one; CostBreakdown totals a session and splits it by part.
InterruptButton, TokenUsageChart, TraceViewer and EvalResultTable
point to chat::stop_button, a stacked BarChart, TimelineProfiler and
DataTable.

UsageBar shows amounts as its owner writes them. MemoryPanel and
HumanInputRequest share agent::line::send_line; the chapter's bool
callbacks take forms::OnFlag.

DataTable scrolls columns wider than its box sideways, pinned or not,
and leaves a plain wheel to the page; sortable headers are Tab stops.
TimelineProfiler fits its ruler to its lane, its spans are Tab stops,
and a picked span takes accent.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 11:39:24 -04:00
1 parent 96c4fe01a6
commit a29cc8bc45
20 files changed
+934 -180

No files matched your search

+46 -3
View File
@@ -1,11 +1,11 @@
use std::{cell::RefCell, rc::Rc};
use gpui::{
Context, IntoElement, Modifiers, ParentElement, Render, Styled, TestAppContext,
VisualTestContext, Window, div, point, px,
Context, IntoElement, KeyUpEvent, Keystroke, Modifiers, ParentElement, Render, Styled,
TestAppContext, VisualTestContext, Window, div, point, px,
};
use super::{Breakpoint, BreakpointList, Disassembly, Instruction};
use super::{Breakpoint, BreakpointList, Disassembly, Instruction, Span, TimelineProfiler};
use crate::theme::Theme;
fn settle(cx: &mut VisualTestContext) {
@@ -95,3 +95,46 @@ fn a_breakpoints_box_toggles_without_opening_it(cx: &mut TestAppContext) {
settle(cx);
assert_eq!(*opens.borrow(), [0], "the row still opens");
}
/// Two spans on one track; it keeps the spans picked.
struct Spans(Rc<RefCell<Vec<usize>>>);
impl Render for Spans {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let store = self.0.clone();
let span = |name: &str, start, end| Span {
track: 0,
name: name.to_string().into(),
start,
end,
};
let spans = vec![span("a", 0.0, 4.0), span("b", 5.0, 9.0)];
div().w(px(400.0)).child(
TimelineProfiler::new("spans", ["Main"], spans, (0.0, 10.0))
.on_select(move |ix, _, _| store.borrow_mut().push(ix)),
)
}
}
#[gpui::test]
fn tab_reaches_each_span_and_enter_picks_it(cx: &mut TestAppContext) {
cx.update(Theme::init);
let picked = Rc::new(RefCell::new(Vec::new()));
let store = picked.clone();
let (_, cx) = cx.add_window_view(|_, _| Spans(store));
cx.update(|window, _| window.activate_window());
settle(cx);
cx.update(|window, _| window.focus_next());
cx.update(|window, _| window.focus_next());
cx.simulate_keystrokes("enter");
settle(cx);
cx.simulate_event(KeyUpEvent {
keystroke: Keystroke::parse("enter").expect("a key"),
});
settle(cx);
assert_eq!(
*picked.borrow(),
[1],
"the second Tab stands on the second span"
);
}
+53 -29
View File
@@ -1,14 +1,15 @@
use std::{cell::Cell, rc::Rc};
use std::rc::Rc;
use gpui::{
App, Bounds, ElementId, FontWeight, Hsla, InteractiveElement, IntoElement, MouseButton,
App, Bounds, ElementId, Entity, FontWeight, Hsla, InteractiveElement, IntoElement, MouseButton,
ParentElement, Pixels, RenderOnce, ScrollWheelEvent, SharedString, StatefulInteractiveElement,
Styled, Window, canvas, div, prelude::*, relative,
Styled, Window, div, prelude::*, relative,
};
use super::OnIndex;
use crate::{
charts::nice_step,
charts::{measure, nice_step},
primitives::FocusRing,
theme::{ActiveTheme, ControlSize, Radius, TextSize},
typography::{Ellipsis, LEADING, format::decimals},
};
@@ -30,9 +31,26 @@ fn tick_label(time: f64, step: f64) -> String {
format!("{time:.*} ms", decimals(step))
}
/// Ticks along the ruler, about.
/// Ticks along the ruler, at most.
const TICKS: usize = 6;
/// The ruler's step and ticks: as many as leave `room` for each label across `width`, each with room before the end.
fn ruler(range: (f64, f64), width: f32, room: f32) -> (f64, Vec<f64>) {
let length = range.1 - range.0;
let across = move |span: f64| (span / length) as f32 * width;
let step = (2..=TICKS)
.rev()
.map(|count| nice_step(length, count))
.find(|step| across(*step) >= room)
.unwrap_or_else(|| nice_step(length, 1));
let first = (range.0 / step).ceil() as i64;
let ticks = (first..)
.map(|n| n as f64 * step)
.take_while(|time| across(range.1 - time) >= room)
.collect();
(step, ticks)
}
/// `range` scaled by `factor` around the point at `at` across it, from 0 to 1; above 1 widens, below narrows.
pub fn zoomed(range: (f64, f64), at: f64, factor: f64) -> (f64, f64) {
assert!(
@@ -108,6 +126,8 @@ impl TimelineProfiler {
impl RenderOnce for TimelineProfiler {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let lane: Entity<Bounds<Pixels>> =
window.use_keyed_state((self.id.clone(), "lane"), cx, |_, _| Bounds::default());
let theme = cx.theme();
let colors = theme.colors.clone();
let line = theme
@@ -116,14 +136,10 @@ impl RenderOnce for TimelineProfiler {
let (from, to) = self.range;
let length = to - from;
let at = move |time: f64| ((time - from) / length) as f32;
let step = nice_step(length, TICKS);
let first = (from / step).ceil() as i64;
let ticks: Vec<f64> = (first..)
.map(|n| n as f64 * step)
.take_while(|time| *time < to)
.collect();
let lane = Rc::new(Cell::new(Bounds::<Pixels>::default()));
let (measure, wheel_lane) = (lane.clone(), lane);
let room = (theme.label_width() * 0.4).to_pixels(window.rem_size());
let width = lane.read(cx).size.width;
let (step, ticks) = ruler(self.range, f32::from(width), f32::from(room));
let wheel_lane = lane.clone();
let on_range = self.on_range.clone();
let label = theme.label_width() * 0.6;
let row = theme.control_height(ControlSize::Md);
@@ -166,7 +182,7 @@ impl RenderOnce for TimelineProfiler {
let Some(on_range) = &on_range else {
return;
};
let bounds = wheel_lane.get();
let bounds = *wheel_lane.read(cx);
let delta = event.delta.pixel_delta(line);
let (x, y) = (f64::from(f32::from(delta.x)), f64::from(f32::from(delta.y)));
cx.stop_propagation();
@@ -208,17 +224,19 @@ impl RenderOnce for TimelineProfiler {
.bg(if lit { color } else { color.opacity(0.55) })
.border_1()
.border_color(if lit {
colors.focus
colors.accent
} else {
color.opacity(0.0)
})
.text_color(colors.fg)
.cursor_pointer()
.on_mouse_down(MouseButton::Left, |_, window, _| {
window.prevent_default()
})
.when_some(pick, |bar, pick| {
bar.on_click(move |_, window, cx| pick(ix, window, cx))
bar.tab_index(0)
.focus_ring(cx)
.cursor_pointer()
.on_click(move |_, window, cx| pick(ix, window, cx))
})
.child(Ellipsis::new(span.name.clone()))
});
@@ -241,18 +259,8 @@ impl RenderOnce for TimelineProfiler {
.h_full()
.overflow_hidden()
.bg(colors.hover.opacity(0.5))
.when(track == 0, |lane| {
let measure = measure.clone();
lane.child(
canvas(
move |bounds, _, _| measure.set(bounds),
|_, _, _, _| {},
)
.absolute()
.top_0()
.left_0()
.size_full(),
)
.when(track == 0, |row| {
row.child(measure(lane.clone(), |bounds| bounds))
})
.children(bars),
)
@@ -290,4 +298,20 @@ mod tests {
);
assert_eq!(zoomed((10.0, 20.0), 1.0, 2.0), (0.0, 20.0));
}
#[test]
fn the_ruler_keeps_a_label_of_room_between_ticks() {
let narrow = ruler((0.0, 4_400.0), 184.0, 64.0);
assert_eq!(
narrow,
(2_000.0, vec![0.0, 2_000.0]),
"4000 has no room before the end"
);
let wide = ruler((0.0, 4_600.0), 664.0, 64.0).1;
assert_eq!(wide, [0.0, 1_000.0, 2_000.0, 3_000.0, 4_000.0]);
assert!(
ruler((0.0, 4_400.0), 0.0, 64.0).1.is_empty(),
"an unmeasured lane holds none"
);
}
}