Add completions to rate prediction modal for common failure modes (#48199)

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
This commit is contained in:
Ben Kunkle authored and GitHub committed 2026-02-02 13:00:38 -05:00
1 parent c06847e48e
commit a5e15da0fd
1 file changed
+199 -13
@@ -6,12 +6,16 @@ use gpui::{
App, BorderStyle, DismissEvent, EdgesRefinement, Entity, EventEmitter, FocusHandle, Focusable,
Length, StyleRefinement, TextStyleRefinement, Window, actions, prelude::*,
};
use language::{LanguageRegistry, Point, language_settings};
use language::{Buffer, CodeLabel, LanguageRegistry, Point, ToOffset, language_settings};
use markdown::{Markdown, MarkdownStyle};
use project::{Completion, CompletionDisplayOptions, CompletionResponse, CompletionSource};
use settings::Settings as _;
use std::rc::Rc;
use std::{fmt::Write, sync::Arc, time::Duration};
use theme::ThemeSettings;
use ui::{KeyBinding, List, ListItem, ListItemSpacing, Tooltip, prelude::*};
use ui::{
ContextMenu, DropdownMenu, KeyBinding, List, ListItem, ListItemSpacing, Tooltip, prelude::*,
};
use workspace::{ModalView, Workspace};
actions!(
@@ -419,6 +423,7 @@ impl RatePredictionsModal {
editor.set_show_indent_guides(false, cx);
editor.set_show_edit_predictions(Some(false), window, cx);
editor.set_placeholder_text("Add your feedback…", window, cx);
editor.set_completion_provider(Some(Rc::new(FeedbackCompletionProvider)));
if focus {
cx.focus_self(window);
}
@@ -613,6 +618,44 @@ impl RatePredictionsModal {
),
)
.when(!rated, |this| {
let modal = cx.entity().downgrade();
let failure_mode_menu =
ContextMenu::build(window, cx, move |menu, _window, _cx| {
FeedbackCompletionProvider::FAILURE_MODES
.iter()
.fold(menu, |menu, (_key, description)| {
let description: SharedString = (*description).into();
let modal = modal.clone();
menu.entry(
description.clone(),
None,
move |window, cx| {
if let Some(modal) = modal.upgrade() {
modal.update(cx, |this, cx| {
if let Some(active) = &this.active_prediction {
active.feedback_editor.update(
cx,
|editor, cx| {
editor.set_text(
description.clone(),
window,
cx,
);
},
);
}
this.thumbs_down_active(
&ThumbsDownActivePrediction,
window,
cx,
);
});
}
},
)
})
});
this.child(
h_flex()
.p_2()
@@ -620,19 +663,32 @@ impl RatePredictionsModal {
.border_y_1()
.border_color(border_color)
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
.color(Color::Muted),
DropdownMenu::new(
"failure-mode-dropdown",
"Issue",
failure_mode_menu,
)
.style(ui::DropdownStyle::Outlined)
.trigger_size(ButtonSize::Compact),
)
.child(
div().w_full().pr_2().flex_wrap().child(
Label::new(concat!(
"Explain why this completion is good or bad. ",
"If it's negative, describe what you expected instead."
))
.size(LabelSize::Small)
.color(Color::Muted),
),
h_flex()
.gap_2()
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
div().flex_wrap().child(
Label::new(concat!(
"Explain why this completion is good or bad. ",
"If it's negative, describe what you expected instead."
))
.size(LabelSize::Small)
.color(Color::Muted),
),
),
),
)
})
@@ -910,3 +966,133 @@ fn format_time_ago(elapsed: Duration) -> String {
format!("{} days", seconds / 86400)
}
}
struct FeedbackCompletionProvider;
impl FeedbackCompletionProvider {
const FAILURE_MODES: &'static [(&'static str, &'static str)] = &[
(
"bad_location",
"Made a prediction somewhere other than expected",
),
("incomplete", "Prediction was incomplete or cut off"),
(
"deleted",
"Prediction deleted code that should have been kept. Prefer `reverted` if it reverted an edit",
),
(
"bad_style",
"Prediction used wrong coding style or conventions",
),
(
"repetitive",
"Prediction repeated existing code unnecessarily",
),
(
"hallucinated",
"Prediction referenced non-existent variables/functions",
),
("wrong_indent", "Prediction had incorrect indentation"),
("syntax_error", "Introduced a syntax error"),
(
"too_aggressive",
"Prediction made more changes than expected",
),
(
"too_conservative",
"Prediction was overly cautious/conservative",
),
(
"no_context",
"Misunderstood or did not use contextual information",
),
("reverted", "Reverted recent edits"),
(
"bad_cursor_position",
"The prediction moved the cursor to an unhelpful position",
),
];
}
impl editor::CompletionProvider for FeedbackCompletionProvider {
fn completions(
&self,
_excerpt_id: editor::ExcerptId,
buffer: &Entity<Buffer>,
buffer_position: language::Anchor,
_trigger: editor::CompletionContext,
_window: &mut Window,
cx: &mut Context<Editor>,
) -> gpui::Task<anyhow::Result<Vec<CompletionResponse>>> {
let buffer = buffer.read(cx);
let mut count_back = 0;
for char in buffer.reversed_chars_at(buffer_position) {
if char.is_ascii_alphanumeric() || char == '_' {
count_back += 1;
} else {
break;
}
}
let start_anchor = buffer.anchor_before(
buffer_position
.to_offset(&buffer)
.saturating_sub(count_back),
);
let replace_range = start_anchor..buffer_position;
let snapshot = buffer.text_snapshot();
let query: String = snapshot.text_for_range(replace_range.clone()).collect();
if query.len() < 3 {
return gpui::Task::ready(Ok(vec![CompletionResponse {
completions: vec![],
display_options: CompletionDisplayOptions {
dynamic_width: true,
},
is_incomplete: false,
}]));
}
let query_lower = query.to_lowercase();
let completions: Vec<Completion> = Self::FAILURE_MODES
.iter()
.filter(|(key, _description)| key.starts_with(&query_lower))
.map(|(key, description)| Completion {
replace_range: replace_range.clone(),
new_text: description.to_string(),
label: CodeLabel::plain(format!("{}: {}", key, description), None),
documentation: None,
source: CompletionSource::Custom,
icon_path: None,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: None,
})
.collect();
gpui::Task::ready(Ok(vec![CompletionResponse {
completions,
display_options: CompletionDisplayOptions {
dynamic_width: true,
},
is_incomplete: false,
}]))
}
fn is_completion_trigger(
&self,
_buffer: &Entity<Buffer>,
_position: language::Anchor,
text: &str,
_trigger_in_words: bool,
_cx: &mut Context<Editor>,
) -> bool {
text.chars()
.last()
.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_')
}
}