Files
Ely-GPUI-Components/src/agent/memory.rs
T
ZacharyZhang-NY 37e53a5ecb 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.
2026-09-26 11:39:24 -04:00

250 lines
8.7 KiB
Rust

use std::rc::Rc;
use gpui::{
App, ElementId, Entity, FontWeight, IntoElement, ParentElement, RenderOnce, SharedString,
Styled, Window, div, prelude::*,
};
use jiff::Timestamp;
use super::line::{OnText, send_line};
use crate::{
buttons::{Button, ButtonVariant, IconButton},
data_display::{Timeline, TimelineItem, Tone},
forms::{Pick, TextInput},
primitives::IconName,
theme::{ActiveTheme, ControlSize, TextSize},
typography::{RelativeTime, format::plural},
};
/// Something an agent keeps between conversations: its key and words, where it came from, and when.
#[derive(Clone, Debug, PartialEq)]
pub struct Memory {
pub key: SharedString,
pub text: SharedString,
pub source: Option<SharedString>,
pub at: Timestamp,
}
type OnForget = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
/// What an agent keeps between conversations: a field to add a memory, then each one with where it came from, when, and a way to forget it.
#[derive(IntoElement)]
pub struct MemoryPanel {
id: ElementId,
field: Entity<TextInput>,
memories: Vec<Memory>,
on_add: Option<OnText>,
on_forget: Option<OnForget>,
}
impl MemoryPanel {
/// `field` is the owner's line for a new memory.
pub fn new(
id: impl Into<ElementId>,
field: &Entity<TextInput>,
memories: impl IntoIterator<Item = Memory>,
) -> Self {
Self {
id: id.into(),
field: field.clone(),
memories: memories.into_iter().collect(),
on_add: None,
on_forget: None,
}
}
/// Gets the words to keep; the field empties after.
pub fn on_add(mut self, handler: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
self.on_add = Some(Rc::new(handler));
self
}
/// Gets the key of the memory to forget.
pub fn on_forget(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_forget = Some(Rc::new(handler));
self
}
}
impl RenderOnce for MemoryPanel {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors.clone();
let count = self.memories.len();
let adding = self.on_add.map(|add| {
send_line(
&self.field,
Button::new((self.id.clone(), "add"), "Remember").variant(ButtonVariant::Secondary),
add,
"memory",
cx,
)
});
div()
.w_full()
.flex()
.flex_col()
.gap_3()
.text_size(theme.text_size(TextSize::Sm))
.child(
div()
.flex()
.items_center()
.justify_between()
.child(div().font_weight(FontWeight::MEDIUM).child("Memory"))
.child(
div()
.text_size(theme.text_size(TextSize::Xs))
.text_color(colors.fg_subtle)
.child(plural(count as u64, "memory", "memories")),
),
)
.children(adding)
.when(count == 0, |panel| {
panel.child(div().text_color(colors.fg_subtle).child("Nothing kept yet"))
})
.children(self.memories.into_iter().map(|memory| {
let forget = self.on_forget.clone().map(|forget| {
let key = memory.key.clone();
IconButton::new(
(self.id.clone(), format!("forget-{}", memory.key)),
IconName::Trash2,
)
.variant(ButtonVariant::Ghost)
.size(ControlSize::Sm)
.tooltip("Forget")
.on_click(move |_, window, cx| {
log::info!("memory: forget {key}");
forget(&key, window, cx)
})
});
div()
.flex()
.items_start()
.gap_2()
.py_2()
.border_t_1()
.border_color(colors.border)
.child(
div()
.flex_1()
.min_w_0()
.child(div().text_color(colors.fg).child(memory.text))
.child(
div()
.mt_0p5()
.flex()
.flex_wrap()
.gap_x_1()
.text_size(theme.text_size(TextSize::Xs))
.text_color(colors.fg_subtle)
.children(memory.source.map(|source| format!("{source} ·")))
.child(RelativeTime::new(
(self.id.clone(), format!("when-{}", memory.key)),
memory.at,
)),
),
)
.children(forget.map(|button| div().flex_none().child(button)))
}))
}
}
/// A point an agent can go back to: its key, what it had done, when, and how many files it had touched.
#[derive(Clone, Debug, PartialEq)]
pub struct Checkpoint {
pub key: SharedString,
pub label: SharedString,
pub at: Timestamp,
pub files: usize,
}
/// Points an agent can go back to, newest first: each with what it had done, when and how many files it had touched; the one it stands on marked, Rewind on the rest.
#[derive(IntoElement)]
pub struct CheckpointList {
id: ElementId,
checkpoints: Vec<Checkpoint>,
current: usize,
on_rewind: Option<Pick>,
}
impl CheckpointList {
/// `checkpoints` oldest first; `current` indexes the one the agent stands on.
pub fn new(
id: impl Into<ElementId>,
checkpoints: impl IntoIterator<Item = Checkpoint>,
current: usize,
) -> Self {
let checkpoints: Vec<Checkpoint> = checkpoints.into_iter().collect();
assert!(
current < checkpoints.len(),
"checkpoint {current} of {}",
checkpoints.len()
);
Self {
id: id.into(),
checkpoints,
current,
on_rewind: None,
}
}
/// Gets the index of the checkpoint to go back to.
pub fn on_rewind(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self {
self.on_rewind = Some(Rc::new(handler));
self
}
}
impl RenderOnce for CheckpointList {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let colors = cx.theme().colors.clone();
let current = self.current;
let items: Vec<(usize, Checkpoint)> =
self.checkpoints.into_iter().enumerate().rev().collect();
items
.into_iter()
.fold(Timeline::new(), |timeline, (ix, point)| {
let rewind = self
.on_rewind
.clone()
.filter(|_| ix != current)
.map(|rewind| {
Button::new((self.id.clone(), format!("rewind-{ix}")), "Rewind to here")
.variant(ButtonVariant::Ghost)
.size(ControlSize::Sm)
.icon(IconName::RotateCcw)
.on_click(move |_, window, cx| {
log::info!("checkpoints: rewind to {ix}");
rewind(ix, window, cx)
})
});
let item = TimelineItem::new(point.label)
.time(RelativeTime::new(
(self.id.clone(), format!("when-{ix}")),
point.at,
))
.child(
div()
.flex()
.flex_wrap()
.items_center()
.gap_x_2()
.child(plural(point.files as u64, "file", "files"))
.when(ix == current, |meta| {
meta.child(div().text_color(colors.accent).child("Here now"))
})
.children(rewind),
);
timeline.item(if ix == current {
item.tone(Tone::Accent)
} else {
item
})
})
}
}