mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(tui/review): add picker dialog and suppress raw review stream
This commit is contained in:
+65
-1
@@ -1341,7 +1341,11 @@ impl App<'_> {
|
||||
match command {
|
||||
SlashCommand::Review => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.handle_review_command(command_args);
|
||||
if command_args.is_empty() {
|
||||
widget.open_review_dialog();
|
||||
} else {
|
||||
widget.handle_review_command(command_args);
|
||||
}
|
||||
}
|
||||
}
|
||||
SlashCommand::Branch => {
|
||||
@@ -1690,6 +1694,66 @@ impl App<'_> {
|
||||
widget.submit_text_message_with_preface(visible, preface);
|
||||
}
|
||||
}
|
||||
AppEvent::RunReviewCommand(args) => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.handle_review_command(args);
|
||||
}
|
||||
}
|
||||
AppEvent::RunReviewWithScope {
|
||||
prompt,
|
||||
hint,
|
||||
preparation_label,
|
||||
} => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.start_review_with_scope(prompt, hint, preparation_label);
|
||||
}
|
||||
}
|
||||
AppEvent::OpenReviewCustomPrompt => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.show_review_custom_prompt();
|
||||
}
|
||||
}
|
||||
AppEvent::StartReviewCommitPicker => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.show_review_commit_loading();
|
||||
}
|
||||
let cwd = self.config.cwd.clone();
|
||||
let tx = self.app_event_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let commits = codex_core::git_info::recent_commits(&cwd, 60).await;
|
||||
tx.send(AppEvent::PresentReviewCommitPicker { commits });
|
||||
});
|
||||
}
|
||||
AppEvent::PresentReviewCommitPicker { commits } => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.present_review_commit_picker(commits);
|
||||
}
|
||||
}
|
||||
AppEvent::StartReviewBranchPicker => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.show_review_branch_loading();
|
||||
}
|
||||
let cwd = self.config.cwd.clone();
|
||||
let tx = self.app_event_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let (branches, current_branch) = tokio::join!(
|
||||
codex_core::git_info::local_git_branches(&cwd),
|
||||
codex_core::git_info::current_branch_name(&cwd),
|
||||
);
|
||||
tx.send(AppEvent::PresentReviewBranchPicker {
|
||||
current_branch,
|
||||
branches,
|
||||
});
|
||||
});
|
||||
}
|
||||
AppEvent::PresentReviewBranchPicker {
|
||||
current_branch,
|
||||
branches,
|
||||
} => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.present_review_branch_picker(current_branch, branches);
|
||||
}
|
||||
}
|
||||
AppEvent::DiffResult(text) => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.add_diff_output(text);
|
||||
|
||||
@@ -3,6 +3,7 @@ use codex_core::config_types::TextVerbosity;
|
||||
use codex_core::config_types::ThemeName;
|
||||
use codex_core::protocol::Event;
|
||||
use codex_core::protocol::ApprovedCommandMatchKind;
|
||||
use codex_core::git_info::CommitLogEntry;
|
||||
use codex_file_search::FileMatch;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::MouseEvent;
|
||||
@@ -135,6 +136,31 @@ pub(crate) enum AppEvent {
|
||||
/// Submit a message with hidden preface instructions
|
||||
SubmitTextWithPreface { visible: String, preface: String },
|
||||
|
||||
/// Run a review with an explicit prompt/hint pair (used by TUI selections)
|
||||
RunReviewWithScope {
|
||||
prompt: String,
|
||||
hint: String,
|
||||
preparation_label: Option<String>,
|
||||
},
|
||||
|
||||
/// Run the review command with the given argument string (mirrors `/review <args>`)
|
||||
RunReviewCommand(String),
|
||||
|
||||
/// Open a bottom-pane form that lets the user select a commit to review.
|
||||
StartReviewCommitPicker,
|
||||
/// Populate the commit picker with retrieved commit entries.
|
||||
PresentReviewCommitPicker { commits: Vec<CommitLogEntry> },
|
||||
/// Open a bottom-pane form that lets the user select a base branch to diff against.
|
||||
StartReviewBranchPicker,
|
||||
/// Populate the branch picker with branch metadata once loaded asynchronously.
|
||||
PresentReviewBranchPicker {
|
||||
current_branch: Option<String>,
|
||||
branches: Vec<String>,
|
||||
},
|
||||
|
||||
/// Show the multi-line prompt input to collect custom review instructions.
|
||||
OpenReviewCustomPrompt,
|
||||
|
||||
/// Update the theme (with history event)
|
||||
UpdateTheme(ThemeName),
|
||||
/// Add or update a subagent command in memory (UI already persisted to config.toml)
|
||||
|
||||
@@ -62,13 +62,13 @@ impl CustomPromptView {
|
||||
}
|
||||
}
|
||||
|
||||
impl BottomPaneView for CustomPromptView {
|
||||
fn handle_key_event(&mut self, _pane: &mut super::BottomPane, key_event: KeyEvent) {
|
||||
impl BottomPaneView<'_> for CustomPromptView {
|
||||
fn handle_key_event(&mut self, pane: &mut super::BottomPane<'_>, key_event: KeyEvent) {
|
||||
match key_event {
|
||||
KeyEvent {
|
||||
code: KeyCode::Esc, ..
|
||||
} => {
|
||||
self.on_ctrl_c(_pane);
|
||||
self.on_ctrl_c(pane);
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
@@ -93,7 +93,7 @@ impl BottomPaneView for CustomPromptView {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ctrl_c(&mut self, _pane: &mut super::BottomPane) -> CancellationEvent {
|
||||
fn on_ctrl_c(&mut self, _pane: &mut super::BottomPane<'_>) -> CancellationEvent {
|
||||
self.complete = true;
|
||||
if let Some(cb) = &self.on_escape {
|
||||
cb(&self.app_event_tx);
|
||||
@@ -212,32 +212,12 @@ impl BottomPaneView for CustomPromptView {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_paste(&mut self, _pane: &mut super::BottomPane, pasted: String) -> bool {
|
||||
fn handle_paste(&mut self, pasted: String) -> super::bottom_pane_view::ConditionalUpdate {
|
||||
if pasted.is_empty() {
|
||||
return false;
|
||||
return super::bottom_pane_view::ConditionalUpdate::NoRedraw;
|
||||
}
|
||||
self.textarea.insert_str(&pasted);
|
||||
true
|
||||
}
|
||||
|
||||
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
||||
if area.height < 2 || area.width <= 2 {
|
||||
return None;
|
||||
}
|
||||
let text_area_height = self.input_height(area.width).saturating_sub(1);
|
||||
if text_area_height == 0 {
|
||||
return None;
|
||||
}
|
||||
let extra_offset: u16 = if self.context_label.is_some() { 1 } else { 0 };
|
||||
let top_line_count = 1u16 + extra_offset;
|
||||
let textarea_rect = Rect {
|
||||
x: area.x.saturating_add(2),
|
||||
y: area.y.saturating_add(top_line_count).saturating_add(1),
|
||||
width: area.width.saturating_sub(2),
|
||||
height: text_area_height,
|
||||
};
|
||||
let state = self.textarea_state.borrow();
|
||||
self.textarea.cursor_pos_with_state(textarea_rect, &state)
|
||||
super::bottom_pane_view::ConditionalUpdate::NeedsRedraw
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ mod chat_composer;
|
||||
mod chat_composer_history;
|
||||
pub mod chrome_selection_view;
|
||||
mod diff_popup;
|
||||
mod custom_prompt_view;
|
||||
mod command_popup;
|
||||
mod file_search_popup;
|
||||
mod paste_burst;
|
||||
@@ -30,6 +31,8 @@ mod model_selection_view;
|
||||
mod scroll_state;
|
||||
mod selection_popup_common;
|
||||
pub mod list_selection_view;
|
||||
pub(crate) use list_selection_view::SelectionAction;
|
||||
pub(crate) use custom_prompt_view::CustomPromptView;
|
||||
pub mod resume_selection_view;
|
||||
pub mod agents_settings_view;
|
||||
mod github_settings_view;
|
||||
@@ -523,6 +526,13 @@ impl BottomPane<'_> {
|
||||
self.request_redraw()
|
||||
}
|
||||
|
||||
/// Show a multi-line prompt input view (used for custom review instructions)
|
||||
pub fn show_custom_prompt(&mut self, view: CustomPromptView) {
|
||||
self.active_view = Some(Box::new(view));
|
||||
self.status_view_active = false;
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// Show a generic list selection popup with items and actions.
|
||||
pub fn show_list_selection(
|
||||
&mut self,
|
||||
|
||||
+324
-10
@@ -20,6 +20,7 @@ use codex_common::model_presets::ModelPreset;
|
||||
use codex_common::model_presets::builtin_model_presets;
|
||||
use codex_core::ConversationManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::git_info::CommitLogEntry;
|
||||
use codex_core::config_types::AgentConfig;
|
||||
use codex_core::config_types::ReasoningEffort;
|
||||
use codex_core::config_types::TextVerbosity;
|
||||
@@ -125,6 +126,8 @@ use crate::app_event::{
|
||||
TerminalRunController,
|
||||
};
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::bottom_pane::CustomPromptView;
|
||||
use crate::bottom_pane::list_selection_view::{ListSelectionView, SelectionItem};
|
||||
use crate::bottom_pane::validation_settings_view;
|
||||
use crate::bottom_pane::validation_settings_view::ToolStatus;
|
||||
use crate::bottom_pane::BottomPane;
|
||||
@@ -4079,6 +4082,16 @@ impl ChatWidget<'_> {
|
||||
// Close out any running tool/exec indicators before inserting final answer.
|
||||
self.finalize_all_running_due_to_answer();
|
||||
|
||||
if self.is_review_flow_active() {
|
||||
tracing::debug!("Suppressing review final message id={} to avoid duplicate output", id);
|
||||
self.stream_state
|
||||
.closed_answer_ids
|
||||
.insert(StreamId(id.clone()));
|
||||
self.active_task_ids.remove(&id);
|
||||
self.maybe_hide_spinner();
|
||||
return;
|
||||
}
|
||||
|
||||
// Route final message through streaming controller so AppEvent::InsertFinalAnswer
|
||||
// is the single source of truth for assistant content.
|
||||
let sink = AppEventHistorySink(self.app_event_tx.clone());
|
||||
@@ -4145,6 +4158,10 @@ impl ChatWidget<'_> {
|
||||
tracing::debug!("Ignoring Answer delta after interrupt");
|
||||
return;
|
||||
}
|
||||
if self.is_review_flow_active() {
|
||||
tracing::debug!("Suppressing review streaming delta id={}", id);
|
||||
return;
|
||||
}
|
||||
// Ignore late deltas for ids that have already finalized in this turn
|
||||
if self
|
||||
.stream_state
|
||||
@@ -11632,6 +11649,286 @@ fn update_rate_limit_resets(
|
||||
}
|
||||
|
||||
impl ChatWidget<'_> {
|
||||
pub(crate) fn open_review_dialog(&mut self) {
|
||||
if self.is_task_running() {
|
||||
self.history_push(crate::history_cell::new_error_event(
|
||||
"`/review` — complete or cancel the current task before starting a new review.".to_string(),
|
||||
));
|
||||
self.request_redraw();
|
||||
return;
|
||||
}
|
||||
|
||||
let mut items: Vec<SelectionItem> = Vec::new();
|
||||
|
||||
items.push(SelectionItem {
|
||||
name: "Review current workspace changes".to_string(),
|
||||
description: Some("Include staged, unstaged, and untracked files".to_string()),
|
||||
is_current: false,
|
||||
actions: vec![Box::new(|tx: &crate::app_event_sender::AppEventSender| {
|
||||
tx.send(crate::app_event::AppEvent::RunReviewCommand(String::new()));
|
||||
})],
|
||||
});
|
||||
|
||||
items.push(SelectionItem {
|
||||
name: "Review a specific commit".to_string(),
|
||||
description: Some("Pick from recent commits".to_string()),
|
||||
is_current: false,
|
||||
actions: vec![Box::new(|tx: &crate::app_event_sender::AppEventSender| {
|
||||
tx.send(crate::app_event::AppEvent::StartReviewCommitPicker);
|
||||
})],
|
||||
});
|
||||
|
||||
items.push(SelectionItem {
|
||||
name: "Review against a base branch".to_string(),
|
||||
description: Some("Diff current branch against another".to_string()),
|
||||
is_current: false,
|
||||
actions: vec![Box::new(|tx: &crate::app_event_sender::AppEventSender| {
|
||||
tx.send(crate::app_event::AppEvent::StartReviewBranchPicker);
|
||||
})],
|
||||
});
|
||||
|
||||
items.push(SelectionItem {
|
||||
name: "Custom review instructions".to_string(),
|
||||
description: Some("Describe exactly what to audit".to_string()),
|
||||
is_current: false,
|
||||
actions: vec![Box::new(|tx: &crate::app_event_sender::AppEventSender| {
|
||||
tx.send(crate::app_event::AppEvent::OpenReviewCustomPrompt);
|
||||
})],
|
||||
});
|
||||
|
||||
let view: ListSelectionView = ListSelectionView::new(
|
||||
" Review options ".to_string(),
|
||||
Some("Choose what scope to review".to_string()),
|
||||
Some("Enter select · Esc cancel".to_string()),
|
||||
items,
|
||||
self.app_event_tx.clone(),
|
||||
6,
|
||||
);
|
||||
|
||||
self.bottom_pane.show_list_selection(
|
||||
"Review options".to_string(),
|
||||
None,
|
||||
None,
|
||||
view,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn show_review_custom_prompt(&mut self) {
|
||||
let submit_tx = self.app_event_tx.clone();
|
||||
let on_submit: Box<dyn Fn(String) + Send + Sync> = Box::new(move |text: String| {
|
||||
submit_tx.send(crate::app_event::AppEvent::RunReviewCommand(text));
|
||||
});
|
||||
let view = CustomPromptView::new(
|
||||
"Custom review instructions".to_string(),
|
||||
"Describe the files or changes you want reviewed".to_string(),
|
||||
Some("Press Enter to submit · Esc cancel".to_string()),
|
||||
self.app_event_tx.clone(),
|
||||
None,
|
||||
on_submit,
|
||||
);
|
||||
self.bottom_pane.show_custom_prompt(view);
|
||||
}
|
||||
|
||||
pub(crate) fn show_review_commit_loading(&mut self) {
|
||||
let loading_item = SelectionItem {
|
||||
name: "Loading recent commits…".to_string(),
|
||||
description: None,
|
||||
is_current: true,
|
||||
actions: Vec::new(),
|
||||
};
|
||||
let view = ListSelectionView::new(
|
||||
" Select a commit ".to_string(),
|
||||
Some("Fetching recent commits from git".to_string()),
|
||||
Some("Esc cancel".to_string()),
|
||||
vec![loading_item],
|
||||
self.app_event_tx.clone(),
|
||||
6,
|
||||
);
|
||||
self.bottom_pane.show_list_selection(
|
||||
"Select a commit".to_string(),
|
||||
None,
|
||||
None,
|
||||
view,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn present_review_commit_picker(&mut self, commits: Vec<CommitLogEntry>) {
|
||||
if commits.is_empty() {
|
||||
self.bottom_pane
|
||||
.flash_footer_notice("No recent commits found for review".to_string());
|
||||
self.request_redraw();
|
||||
return;
|
||||
}
|
||||
|
||||
let mut items: Vec<SelectionItem> = Vec::with_capacity(commits.len());
|
||||
for entry in commits {
|
||||
let subject = entry.subject.trim().to_string();
|
||||
let sha = entry.sha.trim().to_string();
|
||||
if sha.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let short_sha: String = sha.chars().take(7).collect();
|
||||
let title = if subject.is_empty() {
|
||||
short_sha.clone()
|
||||
} else {
|
||||
format!("{short_sha} — {subject}")
|
||||
};
|
||||
let prompt = if subject.is_empty() {
|
||||
format!(
|
||||
"Review the code changes introduced by commit {sha}. Provide prioritized, actionable findings."
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Review the code changes introduced by commit {sha} (\"{subject}\"). Provide prioritized, actionable findings."
|
||||
)
|
||||
};
|
||||
let hint = format!("commit {short_sha}");
|
||||
let preparation = format!("Preparing code review for commit {short_sha}");
|
||||
let prompt_closure = prompt.clone();
|
||||
let hint_closure = hint.clone();
|
||||
let prep_closure = preparation.clone();
|
||||
items.push(SelectionItem {
|
||||
name: title,
|
||||
description: None,
|
||||
is_current: false,
|
||||
actions: vec![Box::new(move |tx: &crate::app_event_sender::AppEventSender| {
|
||||
tx.send(crate::app_event::AppEvent::RunReviewWithScope {
|
||||
prompt: prompt_closure.clone(),
|
||||
hint: hint_closure.clone(),
|
||||
preparation_label: Some(prep_closure.clone()),
|
||||
});
|
||||
})],
|
||||
});
|
||||
}
|
||||
|
||||
if items.is_empty() {
|
||||
self.bottom_pane
|
||||
.flash_footer_notice("No recent commits found for review".to_string());
|
||||
self.request_redraw();
|
||||
return;
|
||||
}
|
||||
|
||||
let view = ListSelectionView::new(
|
||||
" Select a commit ".to_string(),
|
||||
Some("Choose a commit to review".to_string()),
|
||||
Some("Enter select · Esc cancel".to_string()),
|
||||
items,
|
||||
self.app_event_tx.clone(),
|
||||
10,
|
||||
);
|
||||
|
||||
self.bottom_pane.show_list_selection(
|
||||
"Select a commit to review".to_string(),
|
||||
None,
|
||||
None,
|
||||
view,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn show_review_branch_loading(&mut self) {
|
||||
let loading_item = SelectionItem {
|
||||
name: "Loading local branches…".to_string(),
|
||||
description: None,
|
||||
is_current: true,
|
||||
actions: Vec::new(),
|
||||
};
|
||||
let view = ListSelectionView::new(
|
||||
" Select a base branch ".to_string(),
|
||||
Some("Fetching local branches".to_string()),
|
||||
Some("Esc cancel".to_string()),
|
||||
vec![loading_item],
|
||||
self.app_event_tx.clone(),
|
||||
6,
|
||||
);
|
||||
self.bottom_pane.show_list_selection(
|
||||
"Select a base branch".to_string(),
|
||||
None,
|
||||
None,
|
||||
view,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn present_review_branch_picker(
|
||||
&mut self,
|
||||
current_branch: Option<String>,
|
||||
branches: Vec<String>,
|
||||
) {
|
||||
let current_trimmed = current_branch.as_ref().map(|s| s.trim().to_string());
|
||||
let mut items: Vec<SelectionItem> = Vec::new();
|
||||
for branch in branches {
|
||||
let branch_trimmed = branch.trim();
|
||||
if branch_trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if current_trimmed
|
||||
.as_ref()
|
||||
.is_some_and(|current| current == branch_trimmed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let title = if let Some(current) = current_trimmed.as_ref() {
|
||||
format!("{current} → {branch_trimmed}")
|
||||
} else {
|
||||
format!("Compare against {branch_trimmed}")
|
||||
};
|
||||
|
||||
let prompt = if let Some(current) = current_trimmed.as_ref() {
|
||||
format!(
|
||||
"Review the code changes between the current branch '{current}' and '{branch_trimmed}'. Identify bugs, regressions, risky patterns, and missing tests before merging."
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Review the code changes that would merge into '{branch_trimmed}'. Identify bugs, regressions, risky patterns, and missing tests before merge."
|
||||
)
|
||||
};
|
||||
let hint = format!("against {branch_trimmed}");
|
||||
let preparation = format!("Preparing code review against {branch_trimmed}");
|
||||
let prompt_closure = prompt.clone();
|
||||
let hint_closure = hint.clone();
|
||||
let prep_closure = preparation.clone();
|
||||
items.push(SelectionItem {
|
||||
name: title,
|
||||
description: None,
|
||||
is_current: false,
|
||||
actions: vec![Box::new(move |tx: &crate::app_event_sender::AppEventSender| {
|
||||
tx.send(crate::app_event::AppEvent::RunReviewWithScope {
|
||||
prompt: prompt_closure.clone(),
|
||||
hint: hint_closure.clone(),
|
||||
preparation_label: Some(prep_closure.clone()),
|
||||
});
|
||||
})],
|
||||
});
|
||||
}
|
||||
|
||||
if items.is_empty() {
|
||||
self.bottom_pane
|
||||
.flash_footer_notice("No alternative branches found for review".to_string());
|
||||
self.request_redraw();
|
||||
return;
|
||||
}
|
||||
|
||||
let subtitle = current_trimmed
|
||||
.as_ref()
|
||||
.map(|current| format!("Current branch: {current}"));
|
||||
|
||||
let view = ListSelectionView::new(
|
||||
" Select a base branch ".to_string(),
|
||||
subtitle,
|
||||
Some("Enter select · Esc cancel".to_string()),
|
||||
items,
|
||||
self.app_event_tx.clone(),
|
||||
10,
|
||||
);
|
||||
|
||||
self.bottom_pane.show_list_selection(
|
||||
"Compare against a branch".to_string(),
|
||||
None,
|
||||
None,
|
||||
view,
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle `/review [focus]` command by starting a dedicated review session.
|
||||
pub(crate) fn handle_review_command(&mut self, args: String) {
|
||||
if self.is_task_running() {
|
||||
@@ -11643,24 +11940,37 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
|
||||
let trimmed = args.trim();
|
||||
let (prompt, hint) = if trimmed.is_empty() {
|
||||
(
|
||||
if trimmed.is_empty() {
|
||||
self.start_review_with_scope(
|
||||
"Review the current workspace changes and highlight bugs, regressions, risky patterns, and missing tests before merge.".to_string(),
|
||||
"current workspace changes".to_string(),
|
||||
)
|
||||
Some("Preparing code review request...".to_string()),
|
||||
);
|
||||
} else {
|
||||
let value = trimmed.to_string();
|
||||
(value.clone(), value)
|
||||
};
|
||||
let preparation = format!("Preparing code review for {value}");
|
||||
self.start_review_with_scope(value.clone(), value, Some(preparation));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn start_review_with_scope(
|
||||
&mut self,
|
||||
prompt: String,
|
||||
hint: String,
|
||||
preparation_label: Option<String>,
|
||||
) {
|
||||
self.active_review_hint = None;
|
||||
self.active_review_prompt = None;
|
||||
|
||||
let preparation_notice = if trimmed.is_empty() {
|
||||
"Preparing code review request...".to_string()
|
||||
} else {
|
||||
format!("Preparing code review for {hint}")
|
||||
};
|
||||
let trimmed_hint = hint.trim();
|
||||
let preparation_notice = preparation_label.unwrap_or_else(|| {
|
||||
if trimmed_hint.is_empty() {
|
||||
"Preparing code review request...".to_string()
|
||||
} else {
|
||||
format!("Preparing code review for {trimmed_hint}")
|
||||
}
|
||||
});
|
||||
|
||||
self.insert_background_event_early(preparation_notice);
|
||||
self.request_redraw();
|
||||
|
||||
@@ -11672,6 +11982,10 @@ impl ChatWidget<'_> {
|
||||
self.submit_op(Op::Review { review_request });
|
||||
}
|
||||
|
||||
fn is_review_flow_active(&self) -> bool {
|
||||
self.active_review_hint.is_some() || self.active_review_prompt.is_some()
|
||||
}
|
||||
|
||||
fn build_review_summary_cell(
|
||||
&self,
|
||||
hint: Option<&str>,
|
||||
|
||||
@@ -29,8 +29,10 @@ Notes
|
||||
from the repository root (not inside another branch worktree).
|
||||
- `/merge`: merge the current worktree branch back into the default branch and
|
||||
remove the worktree. Run this from inside the worktree created by `/branch`.
|
||||
- `/review [focus]`: start a code review using the configured review model. Add
|
||||
optional guidance to steer what the reviewer should focus on.
|
||||
- `/review [focus]`: without arguments, opens a review picker so you can audit
|
||||
the workspace, a specific commit, compare against another branch, or enter
|
||||
custom instructions. With a focus argument, skips the picker and uses your
|
||||
text directly.
|
||||
- `/cmd <name>`: run a project command defined for the current workspace.
|
||||
|
||||
## UX & Display
|
||||
|
||||
Reference in New Issue
Block a user