agent2: Port rules UI (#36429)

Release Notes:

- N/A
This commit is contained in:
Bennet Bo Fenner 2025-08-19 11:12:57 +02:00 committed by GitHub
parent ed14ab8c02
commit b8ddb0141c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 197 additions and 32 deletions

View file

@ -30,7 +30,7 @@ use language::Buffer;
use language_model::LanguageModelRegistry;
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
use project::Project;
use project::{Project, ProjectEntryId};
use prompt_store::PromptId;
use rope::Point;
use settings::{Settings as _, SettingsStore};
@ -703,6 +703,38 @@ impl AcpThreadView {
})
}
fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
let Some(thread) = self.as_native_thread(cx) else {
return;
};
let project_context = thread.read(cx).project_context().read(cx);
let project_entry_ids = project_context
.worktrees
.iter()
.flat_map(|worktree| worktree.rules_file.as_ref())
.map(|rules_file| ProjectEntryId::from_usize(rules_file.project_entry_id))
.collect::<Vec<_>>();
self.workspace
.update(cx, move |workspace, cx| {
// TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
// files clear. For example, if rules file 1 is already open but rules file 2 is not,
// this would open and focus rules file 2 in a tab that is not next to rules file 1.
let project = workspace.project().read(cx);
let project_paths = project_entry_ids
.into_iter()
.flat_map(|entry_id| project.path_for_entry(entry_id, cx))
.collect::<Vec<_>>();
for project_path in project_paths {
workspace
.open_path(project_path, None, true, window, cx)
.detach_and_log_err(cx);
}
})
.ok();
}
fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context<Self>) {
self.thread_error = Some(ThreadError::from_err(error));
cx.notify();
@ -858,6 +890,12 @@ impl AcpThreadView {
let editor_focus = editor.focus_handle(cx).is_focused(window);
let focus_border = cx.theme().colors().border_focused;
let rules_item = if entry_ix == 0 {
self.render_rules_item(cx)
} else {
None
};
div()
.id(("user_message", entry_ix))
.py_4()
@ -874,6 +912,7 @@ impl AcpThreadView {
}))
})
}))
.children(rules_item)
.child(
div()
.relative()
@ -1862,6 +1901,125 @@ impl AcpThreadView {
.into_any_element()
}
fn render_rules_item(&self, cx: &Context<Self>) -> Option<AnyElement> {
let project_context = self
.as_native_thread(cx)?
.read(cx)
.project_context()
.read(cx);
let user_rules_text = if project_context.user_rules.is_empty() {
None
} else if project_context.user_rules.len() == 1 {
let user_rules = &project_context.user_rules[0];
match user_rules.title.as_ref() {
Some(title) => Some(format!("Using \"{title}\" user rule")),
None => Some("Using user rule".into()),
}
} else {
Some(format!(
"Using {} user rules",
project_context.user_rules.len()
))
};
let first_user_rules_id = project_context
.user_rules
.first()
.map(|user_rules| user_rules.uuid.0);
let rules_files = project_context
.worktrees
.iter()
.filter_map(|worktree| worktree.rules_file.as_ref())
.collect::<Vec<_>>();
let rules_file_text = match rules_files.as_slice() {
&[] => None,
&[rules_file] => Some(format!(
"Using project {:?} file",
rules_file.path_in_worktree
)),
rules_files => Some(format!("Using {} project rules files", rules_files.len())),
};
if user_rules_text.is_none() && rules_file_text.is_none() {
return None;
}
Some(
v_flex()
.pt_2()
.px_2p5()
.gap_1()
.when_some(user_rules_text, |parent, user_rules_text| {
parent.child(
h_flex()
.w_full()
.child(
Icon::new(IconName::Reader)
.size(IconSize::XSmall)
.color(Color::Disabled),
)
.child(
Label::new(user_rules_text)
.size(LabelSize::XSmall)
.color(Color::Muted)
.truncate()
.buffer_font(cx)
.ml_1p5()
.mr_0p5(),
)
.child(
IconButton::new("open-prompt-library", IconName::ArrowUpRight)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Ignored)
// TODO: Figure out a way to pass focus handle here so we can display the `OpenRulesLibrary` keybinding
.tooltip(Tooltip::text("View User Rules"))
.on_click(move |_event, window, cx| {
window.dispatch_action(
Box::new(OpenRulesLibrary {
prompt_to_select: first_user_rules_id,
}),
cx,
)
}),
),
)
})
.when_some(rules_file_text, |parent, rules_file_text| {
parent.child(
h_flex()
.w_full()
.child(
Icon::new(IconName::File)
.size(IconSize::XSmall)
.color(Color::Disabled),
)
.child(
Label::new(rules_file_text)
.size(LabelSize::XSmall)
.color(Color::Muted)
.buffer_font(cx)
.ml_1p5()
.mr_0p5(),
)
.child(
IconButton::new("open-rule", IconName::ArrowUpRight)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Ignored)
.on_click(cx.listener(Self::handle_open_rules))
.tooltip(Tooltip::text("View Rules")),
),
)
})
.into_any(),
)
}
fn render_empty_state(&self, cx: &App) -> AnyElement {
let loading = matches!(&self.thread_state, ThreadState::Loading { .. });