T24e: Agent, environment

- SandboxStatus: a dot and a word (a ring while it starts or works), what it
  uses of what it has as meters, and how long it has run; the header wraps
- McpServerList over lists::ListItem: a dot per state, what it runs or why it
  failed, its tools once connected, and a switch
- ToolRegistry: tools grouped by source, found by every word of the owner's
  query, each Allow, Ask or Deny; rows wrap at 280px
- MemoryPanel: a line to keep a memory, Enter or Remember, blank words kept
  out; each memory with its source, when, and Forget
- CheckpointList over data_display::Timeline, newest first, the current point
  marked, Rewind on the rest
- gallery: every control wired to state
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 11:00:15 -04:00
1 parent 8550c086b0
commit cbdcf85cec
9 files changed
+1245 -5

No files matched your search

+293
View File
@@ -0,0 +1,293 @@
use std::{rc::Rc, time::Duration};
use gpui::{
AnyElement, App, ElementId, FontWeight, Hsla, IntoElement, ParentElement, RenderOnce,
SharedString, Styled, Window, div, prelude::*,
};
use crate::{
data_display::Meter,
forms::Switch,
lists::{List, ListItem},
motion::Pulse,
theme::{ActiveTheme, IconSize, Palette, Radius, TextSize},
typography::{
Ellipsis,
format::{DurationStyle, duration, file_size, plural},
},
};
/// A dot in `color`, ringing while `live`.
fn dot(id: ElementId, color: Hsla, live: bool, cx: &App) -> AnyElement {
let mark = div()
.flex_none()
.size(cx.theme().status_dot())
.rounded_full()
.bg(color);
if live {
Pulse::new(id).child(mark).into_any_element()
} else {
mark.into_any_element()
}
}
/// Where a sandbox stands.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SandboxState {
Starting,
Ready,
Busy,
Stopped,
Failed,
}
impl SandboxState {
fn word(self) -> &'static str {
match self {
Self::Starting => "Starting",
Self::Ready => "Ready",
Self::Busy => "Busy",
Self::Stopped => "Stopped",
Self::Failed => "Failed",
}
}
fn color(self, colors: &Palette) -> Hsla {
match self {
Self::Starting => colors.warning,
Self::Busy => colors.info,
Self::Ready => colors.success,
Self::Stopped => colors.fg_subtle,
Self::Failed => colors.danger,
}
}
}
/// Where an agent's sandbox stands: its name, a dot and a word, what it uses of what it has, and how long it has run.
#[derive(IntoElement)]
pub struct SandboxStatus {
id: ElementId,
name: SharedString,
state: SandboxState,
cpu: Option<f32>,
memory: Option<(u64, u64)>,
uptime: Option<Duration>,
}
impl SandboxStatus {
pub fn new(
id: impl Into<ElementId>,
name: impl Into<SharedString>,
state: SandboxState,
) -> Self {
Self {
id: id.into(),
name: name.into(),
state,
cpu: None,
memory: None,
uptime: None,
}
}
/// The share of its processors in use, 0 to 1.
pub fn cpu(mut self, share: f32) -> Self {
self.cpu = Some(share);
self
}
/// Memory in use and its limit, in bytes.
pub fn memory(mut self, used: u64, limit: u64) -> Self {
assert!(limit > 0 && used <= limit, "memory {used} of {limit}");
self.memory = Some((used, limit));
self
}
pub fn uptime(mut self, uptime: Duration) -> Self {
self.uptime = Some(uptime);
self
}
}
impl RenderOnce for SandboxStatus {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors.clone();
let live = matches!(self.state, SandboxState::Starting | SandboxState::Busy);
let color = self.state.color(&colors);
div()
.w_full()
.flex()
.flex_col()
.gap_3()
.p_4()
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(colors.border)
.bg(colors.surface)
.text_size(theme.text_size(TextSize::Sm))
.child(
div()
.flex()
.flex_wrap()
.items_center()
.gap_x_2()
.gap_y_1()
.child(
div()
.flex_1()
.min_w(theme.label_width())
.flex()
.items_center()
.gap_2()
.child(dot((self.id.clone(), "state").into(), color, live, cx))
.child(
div()
.flex_1()
.min_w_0()
.font_weight(FontWeight::MEDIUM)
.child(Ellipsis::new(self.name)),
),
)
.child(
div()
.flex_none()
.flex()
.items_center()
.gap_2()
.text_color(colors.fg_muted)
.child(self.state.word())
.children(self.uptime.map(|up| {
div().text_color(colors.fg_subtle).child(format!(
"up {}",
duration(up.as_secs(), DurationStyle::Compact)
))
})),
),
)
.children(self.cpu.map(|share| {
Meter::new(
(self.id.clone(), "cpu"),
"Processors",
share.clamp(0.0, 1.0),
)
}))
.children(self.memory.map(|(used, limit)| {
Meter::new(
(self.id.clone(), "memory"),
"Memory",
used as f32 / limit as f32,
)
.detail(format!(
"{} of {}",
file_size(used, true),
file_size(limit, true)
))
}))
}
}
/// Where a connected server stands.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ServerState {
Connected,
Connecting,
Failed,
Off,
}
/// A server or connector the agent can reach: its key and name, where it runs or why it failed, where it stands and how many tools it brings.
#[derive(Clone, Debug, PartialEq)]
pub struct McpServer {
pub key: SharedString,
pub name: SharedString,
pub detail: SharedString,
pub state: ServerState,
pub tools: usize,
}
type OnToggle = Rc<dyn Fn(&SharedString, bool, &mut Window, &mut App)>;
/// Servers and connectors the agent can reach: each with a dot for where it stands, what it runs or why it failed, how many tools it brings, and a switch.
#[derive(IntoElement)]
pub struct McpServerList {
id: ElementId,
servers: Vec<McpServer>,
on_toggle: Option<OnToggle>,
}
impl McpServerList {
pub fn new(id: impl Into<ElementId>, servers: impl IntoIterator<Item = McpServer>) -> Self {
Self {
id: id.into(),
servers: servers.into_iter().collect(),
on_toggle: None,
}
}
/// Gets a server's key and whether it is now on.
pub fn on_toggle(
mut self,
handler: impl Fn(&SharedString, bool, &mut Window, &mut App) + 'static,
) -> Self {
self.on_toggle = Some(Rc::new(handler));
self
}
}
impl RenderOnce for McpServerList {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let colors = cx.theme().colors.clone();
List::new()
.divided()
.children(self.servers.into_iter().map(|server| {
let (color, live) = match server.state {
ServerState::Connected => (colors.success, false),
ServerState::Connecting => (colors.warning, true),
ServerState::Failed => (colors.danger, false),
ServerState::Off => (colors.fg_subtle, false),
};
let toggle = self.on_toggle.clone().map(|toggle| {
let key = server.key.clone();
Switch::new(
(self.id.clone(), format!("switch-{}", server.key)),
server.state != ServerState::Off,
)
.on_change(move |on, window, cx| {
log::info!("mcp server {key}: {}", if on { "on" } else { "off" });
toggle(&key, on, window, cx)
})
});
ListItem::new(
(self.id.clone(), format!("server-{}", server.key)),
server.name,
)
.description(server.detail)
.leading(
div()
.size(cx.theme().icon_size(IconSize::Sm))
.flex()
.items_center()
.justify_center()
.child(dot(
(self.id.clone(), format!("dot-{}", server.key)).into(),
color,
live,
cx,
)),
)
.trailing(
div()
.flex()
.items_center()
.gap_3()
.text_size(cx.theme().text_size(TextSize::Xs))
.text_color(colors.fg_subtle)
.when(server.state == ServerState::Connected, |end| {
end.child(plural(server.tools as u64, "tool", "tools"))
})
.children(toggle),
)
}))
}
}
+276
View File
@@ -0,0 +1,276 @@
use std::rc::Rc;
use gpui::{
App, ElementId, Entity, FontWeight, InteractiveElement, IntoElement, ParentElement, RenderOnce,
SharedString, Styled, Window, div, prelude::*,
};
use jiff::Timestamp;
use crate::{
buttons::{Button, ButtonVariant, IconButton},
data_display::{Timeline, TimelineItem, Tone},
forms::{Enter, Input, 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 OnAdd = Rc<dyn Fn(&str, &mut Window, &mut App)>;
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<OnAdd>,
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
}
}
/// Hands the field's words to `add` and empties it; blank words are kept out.
fn keep(field: &Entity<TextInput>, add: &OnAdd, window: &mut Window, cx: &mut App) {
let text = field.read(cx).text().trim().to_string();
if text.is_empty() {
return;
}
log::info!("memory: kept {} characters", text.len());
add(&text, window, cx);
field.update(cx, |input, cx| input.set_text("", cx));
}
impl RenderOnce for MemoryPanel {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors.clone();
let ready = !self.field.read(cx).text().trim().is_empty();
let count = self.memories.len();
let adding = self.on_add.map(|add| {
let (enter, press, typed, pressed) =
(add.clone(), add, self.field.clone(), self.field.clone());
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.flex_1()
.min_w_0()
.capture_action(move |_: &Enter, window, cx| {
cx.stop_propagation();
keep(&typed, &enter, window, cx)
})
.child(Input::new(&self.field)),
)
.child(
Button::new((self.id.clone(), "add"), "Remember")
.variant(ButtonVariant::Secondary)
.disabled(!ready)
.on_click(move |_, window, cx| keep(&pressed, &press, window, 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
})
})
}
}
+6
View File
@@ -1,15 +1,21 @@
mod artifact;
mod changes;
mod environment;
mod memory;
mod permission;
mod previews;
mod progress;
mod registry;
#[cfg(all(test, feature = "test-support"))]
mod tests;
mod tools;
pub use artifact::ArtifactPanel;
pub use changes::{ChangeState, FileChange, FileChangeCard, MultiFileDiffReview};
pub use environment::{McpServer, McpServerList, SandboxState, SandboxStatus, ServerState};
pub use memory::{Checkpoint, CheckpointList, Memory, MemoryPanel};
pub use permission::{Permission, PermissionPrompt, ToolApprovalDialog};
pub use previews::{BrowserPreview, ComputerUseViewer, LivePreview};
pub use progress::{AgentPlan, AgentProgress, AgentState, AgentStatus, AgentStep, AgentStepList};
pub use registry::{RegisteredTool, ToolAccess, ToolRegistry};
pub use tools::{ToolCallCard, ToolCallGroup};
+209
View File
@@ -0,0 +1,209 @@
use std::rc::Rc;
use gpui::{
App, ElementId, FontWeight, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
Window, div, prelude::*,
};
use crate::{
buttons::SegmentedControl,
theme::{ActiveTheme, ControlSize, TextSize},
typography::Ellipsis,
};
/// What an agent may do with a tool.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ToolAccess {
Allow,
Ask,
Deny,
}
impl ToolAccess {
const ALL: [(ToolAccess, &'static str, &'static str); 3] = [
(ToolAccess::Allow, "allow", "Allow"),
(ToolAccess::Ask, "ask", "Ask"),
(ToolAccess::Deny, "deny", "Deny"),
];
fn key(self) -> &'static str {
Self::ALL
.iter()
.find(|(access, _, _)| *access == self)
.expect("every access has a key")
.1
}
}
/// A tool the agent can call: its name, where it comes from, what it does, and what the agent may do with it.
#[derive(Clone, Debug, PartialEq)]
pub struct RegisteredTool {
pub name: SharedString,
pub source: SharedString,
pub description: SharedString,
pub access: ToolAccess,
}
/// The tools whose name or description holds every word of `query`, ignoring case, by index.
pub(crate) fn found(tools: &[RegisteredTool], query: &str) -> Vec<usize> {
let words: Vec<String> = query.split_whitespace().map(str::to_lowercase).collect();
(0..tools.len())
.filter(|ix| {
let text = format!("{} {}", tools[*ix].name, tools[*ix].description).to_lowercase();
words.iter().all(|word| text.contains(word))
})
.collect()
}
type OnAccess = Rc<dyn Fn(usize, ToolAccess, &mut Window, &mut App)>;
/// Every tool the agent may call, grouped by where it comes from and found by the owner's query: each allowed, asked for first, or denied.
#[derive(IntoElement)]
pub struct ToolRegistry {
id: ElementId,
tools: Vec<RegisteredTool>,
query: SharedString,
on_access: Option<OnAccess>,
}
impl ToolRegistry {
pub fn new(id: impl Into<ElementId>, tools: impl IntoIterator<Item = RegisteredTool>) -> Self {
Self {
id: id.into(),
tools: tools.into_iter().collect(),
query: SharedString::default(),
on_access: None,
}
}
/// Keeps the tools that hold every word, such as a `forms::SearchInput`'s.
pub fn query(mut self, query: impl Into<SharedString>) -> Self {
self.query = query.into();
self
}
/// Gets a tool's index and its new access.
pub fn on_access(
mut self,
handler: impl Fn(usize, ToolAccess, &mut Window, &mut App) + 'static,
) -> Self {
self.on_access = Some(Rc::new(handler));
self
}
}
impl RenderOnce for ToolRegistry {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors.clone();
let kept = found(&self.tools, &self.query);
let mut groups: Vec<(SharedString, Vec<usize>)> = Vec::new();
for ix in kept {
let source = &self.tools[ix].source;
match groups.iter_mut().find(|(name, _)| name == source) {
Some((_, items)) => items.push(ix),
None => groups.push((source.clone(), vec![ix])),
}
}
let empty = groups.is_empty();
div()
.w_full()
.flex()
.flex_col()
.gap_4()
.text_size(theme.text_size(TextSize::Sm))
.when(empty, |list| {
list.child(div().text_color(colors.fg_subtle).child("No tools match"))
})
.children(groups.into_iter().map(|(source, items)| {
div()
.flex()
.flex_col()
.child(
div()
.pb_1()
.text_size(theme.text_size(TextSize::Xs))
.font_weight(FontWeight::MEDIUM)
.text_color(colors.fg_subtle)
.child(format!("{source} · {}", items.len())),
)
.children(items.into_iter().map(|ix| {
let tool = &self.tools[ix];
let access = self.on_access.clone();
let control = ToolAccess::ALL.iter().fold(
SegmentedControl::new(
(self.id.clone(), format!("access-{ix}")),
tool.access.key(),
)
.size(ControlSize::Sm),
|control, (_, key, label)| control.segment(*key, *label, None),
);
let control = match access {
Some(access) => control.on_change(move |key, window, cx| {
let next = ToolAccess::ALL
.iter()
.find(|(_, name, _)| *name == key.as_ref())
.expect("a segment names an access")
.0;
log::info!("tool registry: tool {ix} {next:?}");
access(ix, next, window, cx)
}),
None => control,
};
div()
.flex()
.flex_wrap()
.items_center()
.gap_x_3()
.gap_y_1()
.py_2()
.border_t_1()
.border_color(colors.border)
.child(
div()
.flex_1()
.min_w(theme.label_width())
.child(
div()
.font_family(theme.mono_family.clone())
.child(Ellipsis::new(tool.name.clone())),
)
.child(
div()
.text_size(theme.text_size(TextSize::Xs))
.text_color(colors.fg_muted)
.child(Ellipsis::new(tool.description.clone())),
),
)
.child(div().flex_none().child(control))
}))
}))
}
}
#[cfg(test)]
mod tests {
use super::{RegisteredTool, ToolAccess, found};
fn tool(name: &str, description: &str) -> RegisteredTool {
RegisteredTool {
name: name.to_string().into(),
source: "files".into(),
description: description.to_string().into(),
access: ToolAccess::Ask,
}
}
#[test]
fn a_query_keeps_tools_that_hold_every_word() {
let tools = [
tool("read_file", "Reads a file"),
tool("write_file", "Writes a file"),
tool("web_search", "Searches the web"),
];
assert_eq!(found(&tools, ""), [0, 1, 2]);
assert_eq!(found(&tools, "FILE"), [0, 1]);
assert_eq!(found(&tools, "writes file"), [1]);
assert_eq!(found(&tools, "web file"), Vec::<usize>::new());
}
}
+170
View File
@@ -0,0 +1,170 @@
use gpui::{
AppContext as _, Context, Entity, IntoElement, KeyBinding, ParentElement, Render, SharedString,
Styled, TestAppContext, VisualTestContext, Window, div, px,
};
use jiff::Timestamp;
use super::{press, settle};
use crate::{
agent::{
Checkpoint, CheckpointList, McpServer, McpServerList, Memory, MemoryPanel, SandboxState,
SandboxStatus, ServerState,
},
forms::{self, TextInput},
primitives::FocusNext,
theme::Theme,
};
fn setup(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::init(cx);
forms::bind_keys(cx);
cx.bind_keys([KeyBinding::new("tab", FocusNext, None)]);
});
}
/// A memory panel over a field, and the words it kept.
struct Memories {
field: Entity<TextInput>,
kept: Vec<String>,
}
impl Render for Memories {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let memory = Memory {
key: "tone".into(),
text: "Prefers plain words".into(),
source: None,
at: Timestamp::UNIX_EPOCH,
};
div().w(px(360.0)).child(
MemoryPanel::new("memory", &self.field, [memory])
.on_add(move |text, _, cx| {
view.update(cx, |memories, _| memories.kept.push(text.to_string()))
})
.on_forget(|_, _, _| {}),
)
}
}
fn memories(cx: &mut TestAppContext) -> (Entity<Memories>, &mut VisualTestContext) {
setup(cx);
let (view, cx) = cx.add_window_view(|window, cx| Memories {
field: cx.new(|cx| TextInput::new(window, cx)),
kept: Vec::new(),
});
cx.update(|window, _| window.activate_window());
settle(cx);
(view, cx)
}
#[gpui::test]
fn enter_keeps_a_memory_and_empties_the_field(cx: &mut TestAppContext) {
let (view, cx) = memories(cx);
cx.update(|window, _| window.focus_next());
cx.simulate_input(" ");
press("enter", cx);
cx.simulate_input("Lifts stay under 20%");
press("enter", cx);
view.read_with(cx, |memories, cx| {
assert_eq!(
memories.kept,
["Lifts stay under 20%"],
"blank words are kept out"
);
assert_eq!(memories.field.read(cx).text(), "");
});
}
/// Two servers, and the switches thrown.
struct Servers {
thrown: Vec<(SharedString, bool)>,
}
impl Render for Servers {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let server = |key: &str, state| McpServer {
key: key.to_string().into(),
name: key.to_string().into(),
detail: "npx server".into(),
state,
tools: 3,
};
div().w(px(360.0)).child(
McpServerList::new(
"servers",
[
server("files", ServerState::Connected),
server("web", ServerState::Off),
],
)
.on_toggle(move |key, on, _, cx| {
view.update(cx, |servers, _| servers.thrown.push((key.clone(), on)))
}),
)
}
}
#[gpui::test]
fn a_servers_switch_turns_it_off_and_on(cx: &mut TestAppContext) {
setup(cx);
let (view, cx) = cx.add_window_view(|_, _| Servers { thrown: Vec::new() });
cx.update(|window, _| window.activate_window());
settle(cx);
cx.update(|window, _| window.focus_next());
press("space", cx);
cx.update(|window, _| window.focus_next());
press("space", cx);
let thrown = view.read_with(cx, |servers, _| servers.thrown.clone());
assert_eq!(thrown, [("files".into(), false), ("web".into(), true)]);
}
/// Three checkpoints standing on the middle one, and the rewinds asked for.
struct Points {
asked: Vec<usize>,
}
impl Render for Points {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let point = |key: &str| Checkpoint {
key: key.to_string().into(),
label: key.to_string().into(),
at: Timestamp::UNIX_EPOCH,
files: 2,
};
div().w(px(360.0)).child(
CheckpointList::new(
"points",
[point("first"), point("second"), point("third")],
1,
)
.on_rewind(move |ix, _, cx| view.update(cx, |points, _| points.asked.push(ix))),
)
}
}
#[gpui::test]
fn every_checkpoint_but_the_current_rewinds(cx: &mut TestAppContext) {
setup(cx);
let (view, cx) = cx.add_window_view(|_, _| Points { asked: Vec::new() });
cx.update(|window, _| window.activate_window());
settle(cx);
cx.update(|window, _| window.focus_next());
press("enter", cx);
cx.update(|window, _| window.focus_next());
press("enter", cx);
assert_eq!(
view.read_with(cx, |points, _| points.asked.clone()),
[2, 0],
"newest first, the current skipped"
);
}
#[test]
#[should_panic(expected = "memory 5 of 4")]
fn memory_past_its_limit_fails_loud() {
let _ = SandboxStatus::new("box", "python", SandboxState::Ready).memory(5, 4);
}
+1
View File
@@ -4,6 +4,7 @@ use gpui::{
};
mod changes;
mod environment;
mod previews;
use super::{AgentProgress, Permission, PermissionPrompt, ToolApprovalDialog};