Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 76 additions & 13 deletions apps/maple-agent/app/src/ui/chat/sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,9 @@ pub(super) struct SidebarRow {
pub(super) menu_archive_id: SharedString,
pub(super) menu_delete_id: SharedString,
pub(super) menu_reopen_id: SharedString,
/// The title on one line (see [`one_line`]).
pub(super) title: SharedString,
/// Display name of the task's project, shown on every row.
/// Display name of the task's project, shown on every row, on one line.
pub(super) project_name: SharedString,
/// Lower-cased title, matched against the sidebar filter.
pub(super) search: String,
Expand All @@ -204,16 +205,28 @@ impl SidebarRow {
menu_archive_id: SharedString::from(format!("archive-task-{id}")),
menu_delete_id: SharedString::from(format!("delete-task-{id}")),
menu_reopen_id: SharedString::from(format!("reopen-task-{id}")),
title: SharedString::from(session.title.clone()),
project_name: SharedString::from(project_name.to_string()),
title: one_line(&session.title),
project_name: one_line(project_name),
search: session.title.to_lowercase(),
}
}

/// Preserve raw-title identity when line breaks are flattened.
fn built_from(&self, title: &str) -> bool {
if title.contains(['\r', '\n']) {
self.search == title.to_lowercase() && *self.title == *one_line(title)
} else {
!self.search.contains(['\r', '\n']) && *self.title == *title
}
}
}

/// The switcher's "All projects" row.
const ALL_PROJECTS_ID: &str = "switcher-all-projects";

/// How far a cut-off task title fades into its row.
const ROW_FADE: gpui::Pixels = px(20.);

/// The sidebar's popup menus. One is open at a time; a project's own menu
/// opens inside the switcher (see `Sidebar::project_popup`).
#[derive(Clone, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -1435,7 +1448,7 @@ impl Sidebar {
let fresh = self.rows.len() == self.sessions.len()
&& self.rows.iter().zip(&self.sessions).all(|(row, session)| {
*row.id == *session.id
&& row.title.as_ref() == session.title
&& row.built_from(&session.title)
&& self.root_name_matches(&session.project_root, &row.project_name)
});
if fresh {
Expand Down Expand Up @@ -1619,7 +1632,7 @@ impl Sidebar {

/// Whether a cached row still has the current owner name or pending preview.
fn root_name_matches(&self, root: &str, name: &str) -> bool {
self.root_name(root) == name
*one_line(&self.root_name(root)) == *name
}

// ---- Rename ------------------------------------------------------------
Expand Down Expand Up @@ -2762,6 +2775,12 @@ impl Sidebar {
let pin_id = row.pin_id.clone();
let rename_field = self.task_rename_field(&row.id, cx);
let renaming = rename_field.is_some();
let row_bg = if is_selected {
theme::bg_sidebar_row_selected()
} else {
theme::bg_sidebar()
};
let hover_group = (!renaming).then_some(&row.group);
div()
.relative()
.w_full()
Expand Down Expand Up @@ -2809,21 +2828,17 @@ impl Sidebar {
.flex_col()
.map(|column| match rename_field {
Some(field) => column.child(field),
None => column
.child(div().line_clamp(1).text_ellipsis().child(row.title.clone())),
None => column.child(row_line(row.title.clone(), hover_group, row_bg)),
})
.when(self.computers.len() <= 1, |column| {
column.child(
div()
row_line(row.project_name.clone(), hover_group, row_bg)
.text_xs()
.text_color(gpui::rgb(if is_selected {
theme::text_secondary()
} else {
theme::text_muted()
}))
.line_clamp(1)
.text_ellipsis()
.child(row.project_name.clone()),
})),
)
}),
)
Expand Down Expand Up @@ -2865,11 +2880,21 @@ impl Sidebar {
.flex()
.items_center()
.gap_1p5()
.pl_2()
.pr_6()
.group_hover(row.group.clone(), |style| {
style.bg(gpui::rgb(theme::bg_sidebar_row_hover()))
})
.child(
div()
.absolute()
.top_0()
.bottom_0()
.right_full()
.w(ROW_FADE)
.group_hover(row.group.clone(), |style| {
style.bg(row_fade_background(theme::bg_sidebar_row_hover()))
}),
)
.child(self.task_menu_trigger(row, task, window, cx))
.child(row_action(
row.rename_id.clone(),
Expand Down Expand Up @@ -3093,6 +3118,44 @@ impl Render for Sidebar {
}
}

/// Replace each line break, including CRLF, with one space.
fn one_line(text: &str) -> SharedString {
text.replace("\r\n", " ").replace(['\r', '\n'], " ").into()
}

/// Fade a line into its row background; renames omit the hover group.
fn row_line(text: SharedString, hover_group: Option<&SharedString>, row_bg: u32) -> Div {
div()
.relative()
.overflow_hidden()
.whitespace_nowrap()
.child(text)
.child(
div()
.absolute()
.top_0()
.bottom_0()
.right_0()
.w(ROW_FADE)
.bg(row_fade_background(row_bg))
.when_some(hover_group, |fade, group| {
fade.group_hover(group.clone(), |style| {
style.bg(row_fade_background(theme::bg_sidebar_row_hover()))
})
}),
)
}

/// Left to right from clear to `color`.
fn row_fade_background(color: u32) -> gpui::Background {
let color = gpui::rgb(color);
gpui::linear_gradient(
90.,
gpui::linear_color_stop(color.alpha(0.), 0.),
gpui::linear_color_stop(color, 1.),
)
}

/// A spinner while the task runs, a dot once it completed unseen. The
/// spinner keeps the sidebar repainting while any task runs; `spinner_id`
/// is prebuilt so render allocates nothing.
Expand Down
25 changes: 17 additions & 8 deletions apps/maple-agent/app/src/ui/chat/sidebar_computers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1109,9 +1109,9 @@ impl Sidebar {
destination: Destination::Task(task.uuid.clone()),
},
id: format!("computer-{computer_index}-task-{}", task.uuid).into(),
title: latest
.map_or_else(|| task.title.clone(), |session| session.title.clone())
.into(),
title: one_line(
latest.map_or(task.title.as_str(), |session| session.title.as_str()),
),
pinned: task.pinned_ms.is_some(),
activity: if computer.running.contains(&task.uuid) {
Some(SessionActivity::Running)
Expand All @@ -1125,6 +1125,12 @@ impl Sidebar {
}
}

/// The title on another computer's task row.
#[cfg(test)]
pub(in crate::ui::chat) fn other_task_title(&self, index: usize) -> Option<&str> {
Some(self.other_tasks.get(index)?.title.as_ref())
}

pub(in crate::ui::chat) fn computer_target(
&self,
entry: SidebarEntry,
Expand Down Expand Up @@ -1253,8 +1259,14 @@ impl Sidebar {
SidebarEntry::OtherTask(index) => {
let task = &self.other_tasks[index];
let request = task.request.clone();
let row_bg = if selected {
theme::bg_sidebar_row_hover()
} else {
theme::bg_sidebar()
};
div()
.id(task.id.clone())
.group(task.id.clone())
.role(gpui::Role::ListBoxOption)
.aria_label(task.title.clone())
.aria_selected(false)
Expand All @@ -1279,12 +1291,9 @@ impl Sidebar {
cx.emit(SidebarEvent::Navigate(request.clone()))
}))
.child(
div()
row_line(task.title.clone(), Some(&task.id), row_bg)
.flex_1()
.min_w_0()
.line_clamp(1)
.text_ellipsis()
.child(task.title.clone()),
.min_w_0(),
)
.when_some(task.activity, |row, activity| {
row.child(activity_indicator(&task.id, activity))
Expand Down
26 changes: 25 additions & 1 deletion apps/maple-agent/app/src/ui/chat/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ mod state_tests {
path: "/tmp/proj".into(),
label: "proj".into(),
}],
sessions: vec![summary("same-id", "Remote task")],
sessions: vec![summary("same-id", "Remote\r\ntask")],
running: HashSet::from(["same-id".into()]),
unread: HashSet::new(),
pinned: Vec::new(),
Expand Down Expand Up @@ -213,6 +213,10 @@ mod state_tests {
.iter()
.find(|e| matches!(e, SidebarEntry::OtherTask(_)))
.unwrap();
let SidebarEntry::OtherTask(index) = entry else {
unreachable!()
};
assert_eq!(sidebar.other_task_title(index), Some("Remote task"));
assert_eq!(
sidebar.computer_target(entry),
Some(navigation::SidebarTarget::Computer(Navigate {
Expand Down Expand Up @@ -7167,13 +7171,33 @@ mod state_tests {
// A rename moves the row strings.
this.upsert_session(summary("s1", "Renamed"), cx);
assert_eq!(this.sidebar.read(cx).rows()[0].title.as_ref(), "Renamed");
// Search retains raw line breaks.
for title in ["Two\nlines", "Two\r\nlines", "Two\rlines", "Two lines"] {
this.upsert_session(summary("s1", title), cx);
let sidebar = this.sidebar.read(cx);
assert_eq!(sidebar.rows()[0].title.as_ref(), "Two lines");
assert_eq!(sidebar.rows()[0].search, title.to_lowercase());
}
// Filtering reuses the normalized row cache.
this.upsert_session(summary("s1", "Two\nlines"), cx);
let built = this.sidebar.read(cx).rows()[0].title.as_ptr();
this.set_sidebar_filter("two", cx);
assert_eq!(this.sidebar.read(cx).rows()[0].title.as_ptr(), built);
this.set_sidebar_filter("", cx);
this.sidebar.update(cx, |sidebar, _| {
sidebar.set_project_name_for_test("/tmp/proj", "Nice");
});
assert_eq!(
this.sidebar.read(cx).rows()[0].project_name.as_ref(),
"Nice"
);
this.sidebar.update(cx, |sidebar, _| {
sidebar.set_project_name_for_test("/tmp/proj", "Nice\nname");
});
assert_eq!(
this.sidebar.read(cx).rows()[0].project_name.as_ref(),
"Nice name"
);
});
}

Expand Down
Loading