Show and refresh monitoring command approvals
This commit is contained in:
@@ -119,6 +119,17 @@ fn review_summary(action: &AIAgentAction) -> String {
|
|||||||
AIAgentActionType::RequestCommandOutput { command, .. } => {
|
AIAgentActionType::RequestCommandOutput { command, .. } => {
|
||||||
format!("Run: {command}")
|
format!("Run: {command}")
|
||||||
}
|
}
|
||||||
|
AIAgentActionType::WriteToLongRunningShellCommand { input, mode, .. }
|
||||||
|
if mode.is_shell_interrupt(input) =>
|
||||||
|
{
|
||||||
|
"Interrupt the running command (Ctrl+C)".to_string()
|
||||||
|
}
|
||||||
|
AIAgentActionType::WriteToLongRunningShellCommand { input, .. } => {
|
||||||
|
format!(
|
||||||
|
"Send to running command: {}",
|
||||||
|
String::from_utf8_lossy(input)
|
||||||
|
)
|
||||||
|
}
|
||||||
_ => action.action.user_friendly_name(),
|
_ => action.action.user_friendly_name(),
|
||||||
};
|
};
|
||||||
let summary = description.split_whitespace().collect::<Vec<_>>().join(" ");
|
let summary = description.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||||
|
|||||||
@@ -1088,6 +1088,9 @@ pub struct AIBlock {
|
|||||||
/// Map from collapsible block message IDs (reasoning or summarization) to their states.
|
/// Map from collapsible block message IDs (reasoning or summarization) to their states.
|
||||||
collapsible_block_states: HashMap<MessageId, CollapsibleElementState>,
|
collapsible_block_states: HashMap<MessageId, CollapsibleElementState>,
|
||||||
|
|
||||||
|
/// Expansion state for Galaxy's per-action permission review panes.
|
||||||
|
review_pane_states: HashMap<AIAgentActionId, CollapsibleElementState>,
|
||||||
|
|
||||||
/// Map from suggested prompt action ID to its view handle and status.
|
/// Map from suggested prompt action ID to its view handle and status.
|
||||||
unit_tests_suggestions: HashMap<AIAgentActionId, ViewHandle<SuggestedUnitTestsView>>,
|
unit_tests_suggestions: HashMap<AIAgentActionId, ViewHandle<SuggestedUnitTestsView>>,
|
||||||
|
|
||||||
@@ -1658,6 +1661,7 @@ impl AIBlock {
|
|||||||
todo_list_states: Default::default(),
|
todo_list_states: Default::default(),
|
||||||
comment_states,
|
comment_states,
|
||||||
collapsible_block_states: Default::default(),
|
collapsible_block_states: Default::default(),
|
||||||
|
review_pane_states: Default::default(),
|
||||||
unit_tests_suggestions: Default::default(),
|
unit_tests_suggestions: Default::default(),
|
||||||
secret_redaction_state,
|
secret_redaction_state,
|
||||||
find_state: FindState::default(),
|
find_state: FindState::default(),
|
||||||
@@ -2174,6 +2178,10 @@ impl AIBlock {
|
|||||||
}
|
}
|
||||||
self.requested_action_ids = new_action_ids;
|
self.requested_action_ids = new_action_ids;
|
||||||
|
|
||||||
|
self.review_pane_states
|
||||||
|
.entry(action.id.clone())
|
||||||
|
.or_default();
|
||||||
|
|
||||||
if !self.action_buttons.contains_key(&action.id) {
|
if !self.action_buttons.contains_key(&action.id) {
|
||||||
let run_button = CompactibleActionButton::new(
|
let run_button = CompactibleActionButton::new(
|
||||||
"Run".to_string(),
|
"Run".to_string(),
|
||||||
@@ -4847,6 +4855,9 @@ impl AIBlock {
|
|||||||
|
|
||||||
match event {
|
match event {
|
||||||
BlocklistAIActionEvent::ExecutingAction { .. } => {
|
BlocklistAIActionEvent::ExecutingAction { .. } => {
|
||||||
|
if let Some(state) = me.review_pane_states.get_mut(action_id) {
|
||||||
|
state.finish_summarization();
|
||||||
|
}
|
||||||
match &me.autonomy_setting_speedbump {
|
match &me.autonomy_setting_speedbump {
|
||||||
AutonomySettingSpeedbump::ShouldShowForAutoexecutingReadonlyCommands {
|
AutonomySettingSpeedbump::ShouldShowForAutoexecutingReadonlyCommands {
|
||||||
action_id: speedbump_action_id,
|
action_id: speedbump_action_id,
|
||||||
@@ -4917,10 +4928,23 @@ impl AIBlock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
BlocklistAIActionEvent::ActionBlockedOnUserConfirmation { .. } => {
|
BlocklistAIActionEvent::ActionBlockedOnUserConfirmation { .. } => {
|
||||||
|
if let Some(state) = me.review_pane_states.get_mut(action_id) {
|
||||||
|
state.sync_finished_state(false);
|
||||||
|
if !state.user_toggled_while_streaming {
|
||||||
|
state.expand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(buttons) = me.action_buttons.get_mut(action_id) {
|
||||||
|
buttons.run_button.set_label("Approve".to_string(), ctx);
|
||||||
|
buttons.cancel_button.set_label("Deny".to_string(), ctx);
|
||||||
|
}
|
||||||
ctx.emit(AIBlockEvent::ActionBlockedOnUserConfirmation);
|
ctx.emit(AIBlockEvent::ActionBlockedOnUserConfirmation);
|
||||||
ctx.notify();
|
ctx.notify();
|
||||||
}
|
}
|
||||||
BlocklistAIActionEvent::FinishedAction { action_id, .. } => {
|
BlocklistAIActionEvent::FinishedAction { action_id, .. } => {
|
||||||
|
if let Some(state) = me.review_pane_states.get_mut(action_id) {
|
||||||
|
state.finish_summarization();
|
||||||
|
}
|
||||||
me.abort_auto_expand_requested_command_timer();
|
me.abort_auto_expand_requested_command_timer();
|
||||||
|
|
||||||
// Handle auto-collapsing of requested commands that were marked for auto-collapse
|
// Handle auto-collapsing of requested commands that were marked for auto-collapse
|
||||||
@@ -6590,6 +6614,8 @@ pub enum AIBlockAction {
|
|||||||
action_id: AIAgentActionId,
|
action_id: AIAgentActionId,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ToggleReviewPaneExpanded(AIAgentActionId),
|
||||||
|
|
||||||
/// Execute the next pending action, if any.
|
/// Execute the next pending action, if any.
|
||||||
ExecuteNextPendingAction,
|
ExecuteNextPendingAction,
|
||||||
|
|
||||||
@@ -6840,6 +6866,12 @@ impl TypedActionView for AIBlock {
|
|||||||
action_model.execute_action(action_id, self.client_ids.conversation_id, ctx)
|
action_model.execute_action(action_id, self.client_ids.conversation_id, ctx)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
AIBlockAction::ToggleReviewPaneExpanded(action_id) => {
|
||||||
|
if let Some(state) = self.review_pane_states.get_mut(action_id) {
|
||||||
|
state.toggle_expansion();
|
||||||
|
ctx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
AIBlockAction::ChangedHoverOnLink {
|
AIBlockAction::ChangedHoverOnLink {
|
||||||
link_range,
|
link_range,
|
||||||
location,
|
location,
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ struct StateHandles {
|
|||||||
query_scroll_state: ClippedScrollStateHandle,
|
query_scroll_state: ClippedScrollStateHandle,
|
||||||
input_hover_state: MouseStateHandle,
|
input_hover_state: MouseStateHandle,
|
||||||
dismiss_input_mouse_state: MouseStateHandle,
|
dismiss_input_mouse_state: MouseStateHandle,
|
||||||
|
approval_header_mouse_state: MouseStateHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CLISubagentView {
|
pub struct CLISubagentView {
|
||||||
@@ -218,6 +219,7 @@ pub struct CLISubagentView {
|
|||||||
transfer_control_button: CompactibleActionButton,
|
transfer_control_button: CompactibleActionButton,
|
||||||
allow_menu: ViewHandle<Menu<CLISubagentAction>>,
|
allow_menu: ViewHandle<Menu<CLISubagentAction>>,
|
||||||
is_allow_menu_open: bool,
|
is_allow_menu_open: bool,
|
||||||
|
is_approval_expanded: bool,
|
||||||
always_allow_write_to_pty_checked: bool,
|
always_allow_write_to_pty_checked: bool,
|
||||||
always_allow_read_files_checked: bool,
|
always_allow_read_files_checked: bool,
|
||||||
|
|
||||||
@@ -439,6 +441,20 @@ impl CLISubagentView {
|
|||||||
ctx,
|
ctx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Permission changes can arrive after the model output stops streaming.
|
||||||
|
// Redraw immediately so the approval controls appear without window focus.
|
||||||
|
ctx.subscribe_to_model(&action_model, |me, _, event, ctx| {
|
||||||
|
if event.conversation_id() == Some(me.conversation_id) {
|
||||||
|
if matches!(
|
||||||
|
event,
|
||||||
|
crate::ai::blocklist::action_model::BlocklistAIActionEvent::ActionBlockedOnUserConfirmation { .. }
|
||||||
|
) {
|
||||||
|
me.is_approval_expanded = true;
|
||||||
|
}
|
||||||
|
ctx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ctx.subscribe_to_model(&subagent_controller, |me, _, event, ctx| match event {
|
ctx.subscribe_to_model(&subagent_controller, |me, _, event, ctx| match event {
|
||||||
CLISubagentEvent::UpdatedControl { block_id, .. } => {
|
CLISubagentEvent::UpdatedControl { block_id, .. } => {
|
||||||
if *block_id == me.block_id {
|
if *block_id == me.block_id {
|
||||||
@@ -473,6 +489,7 @@ impl CLISubagentView {
|
|||||||
transfer_control_button,
|
transfer_control_button,
|
||||||
allow_menu,
|
allow_menu,
|
||||||
is_allow_menu_open: false,
|
is_allow_menu_open: false,
|
||||||
|
is_approval_expanded: true,
|
||||||
always_allow_write_to_pty_checked,
|
always_allow_write_to_pty_checked,
|
||||||
always_allow_read_files_checked,
|
always_allow_read_files_checked,
|
||||||
is_input_dismissed: false,
|
is_input_dismissed: false,
|
||||||
@@ -1364,6 +1381,13 @@ impl View for CLISubagentView {
|
|||||||
Some(render_blocked_action(
|
Some(render_blocked_action(
|
||||||
BlockedActionProps {
|
BlockedActionProps {
|
||||||
header: header.to_string(),
|
header: header.to_string(),
|
||||||
|
review_detail: self
|
||||||
|
.action_model
|
||||||
|
.as_ref(app)
|
||||||
|
.review_display(self.conversation_id, &action.id)
|
||||||
|
.map(|review| review.detail),
|
||||||
|
is_expanded: self.is_approval_expanded,
|
||||||
|
header_mouse_state: &self.state_handles.approval_header_mouse_state,
|
||||||
description: Some(render_write_to_pty_input(
|
description: Some(render_write_to_pty_input(
|
||||||
WriteToPtyInputProps {
|
WriteToPtyInputProps {
|
||||||
input: input.clone(),
|
input: input.clone(),
|
||||||
@@ -1399,6 +1423,13 @@ impl View for CLISubagentView {
|
|||||||
Some(render_blocked_action(
|
Some(render_blocked_action(
|
||||||
BlockedActionProps {
|
BlockedActionProps {
|
||||||
header: BLOCKED_ACTION_MESSAGE_FOR_TRANSFER_CONTROL.to_string(),
|
header: BLOCKED_ACTION_MESSAGE_FOR_TRANSFER_CONTROL.to_string(),
|
||||||
|
review_detail: self
|
||||||
|
.action_model
|
||||||
|
.as_ref(app)
|
||||||
|
.review_display(self.conversation_id, &action.id)
|
||||||
|
.map(|review| review.detail),
|
||||||
|
is_expanded: self.is_approval_expanded,
|
||||||
|
header_mouse_state: &self.state_handles.approval_header_mouse_state,
|
||||||
description: Some(render_transfer_control_reason(reason, app)),
|
description: Some(render_transfer_control_reason(reason, app)),
|
||||||
is_allow_menu_open: false,
|
is_allow_menu_open: false,
|
||||||
allow_menu: None,
|
allow_menu: None,
|
||||||
@@ -1414,6 +1445,13 @@ impl View for CLISubagentView {
|
|||||||
| AIAgentActionType::FileGlobV2 { .. } => Some(render_blocked_action(
|
| AIAgentActionType::FileGlobV2 { .. } => Some(render_blocked_action(
|
||||||
BlockedActionProps {
|
BlockedActionProps {
|
||||||
header: get_blocked_action_header(action.action.clone()).unwrap_or_default(),
|
header: get_blocked_action_header(action.action.clone()).unwrap_or_default(),
|
||||||
|
review_detail: self
|
||||||
|
.action_model
|
||||||
|
.as_ref(app)
|
||||||
|
.review_display(self.conversation_id, &action.id)
|
||||||
|
.map(|review| review.detail),
|
||||||
|
is_expanded: self.is_approval_expanded,
|
||||||
|
header_mouse_state: &self.state_handles.approval_header_mouse_state,
|
||||||
description: render_search_action_input(action.action.clone(), app),
|
description: render_search_action_input(action.action.clone(), app),
|
||||||
is_allow_menu_open: self.is_allow_menu_open,
|
is_allow_menu_open: self.is_allow_menu_open,
|
||||||
allow_menu: Some(&self.allow_menu),
|
allow_menu: Some(&self.allow_menu),
|
||||||
@@ -1516,6 +1554,7 @@ pub enum CLISubagentAction {
|
|||||||
RejectBlockedAction { should_user_take_over: bool },
|
RejectBlockedAction { should_user_take_over: bool },
|
||||||
TakeControlOfRunningCommand,
|
TakeControlOfRunningCommand,
|
||||||
ToggleAllowMenu,
|
ToggleAllowMenu,
|
||||||
|
ToggleApprovalExpanded,
|
||||||
ToggleAlwaysAllowWriteToPty,
|
ToggleAlwaysAllowWriteToPty,
|
||||||
ToggleAlwaysAllowReadFiles,
|
ToggleAlwaysAllowReadFiles,
|
||||||
DismissInput,
|
DismissInput,
|
||||||
@@ -1562,6 +1601,10 @@ impl TypedActionView for CLISubagentView {
|
|||||||
CLISubagentAction::ToggleAllowMenu => {
|
CLISubagentAction::ToggleAllowMenu => {
|
||||||
self.toggle_allow_menu(ctx);
|
self.toggle_allow_menu(ctx);
|
||||||
}
|
}
|
||||||
|
CLISubagentAction::ToggleApprovalExpanded => {
|
||||||
|
self.is_approval_expanded = !self.is_approval_expanded;
|
||||||
|
ctx.notify();
|
||||||
|
}
|
||||||
CLISubagentAction::ToggleAlwaysAllowWriteToPty => {
|
CLISubagentAction::ToggleAlwaysAllowWriteToPty => {
|
||||||
self.always_allow_write_to_pty_checked = !self.always_allow_write_to_pty_checked;
|
self.always_allow_write_to_pty_checked = !self.always_allow_write_to_pty_checked;
|
||||||
BlocklistAIPermissions::handle(ctx).update(ctx, |model, ctx| {
|
BlocklistAIPermissions::handle(ctx).update(ctx, |model, ctx| {
|
||||||
@@ -1895,14 +1938,14 @@ fn render_action_buttons(
|
|||||||
let regular_wrapped = Container::new(regular_row)
|
let regular_wrapped = Container::new(regular_row)
|
||||||
.with_vertical_padding(8.)
|
.with_vertical_padding(8.)
|
||||||
.with_horizontal_padding(CONTENT_PADDING)
|
.with_horizontal_padding(CONTENT_PADDING)
|
||||||
.with_background_color(internal_colors::neutral_2(theme))
|
.with_background(theme.background())
|
||||||
.with_border(Border::top(1.).with_border_color(internal_colors::neutral_3(theme)))
|
.with_border(Border::top(1.).with_border_color(internal_colors::neutral_3(theme)))
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
let compact_wrapped = Container::new(compact_row)
|
let compact_wrapped = Container::new(compact_row)
|
||||||
.with_vertical_padding(8.)
|
.with_vertical_padding(8.)
|
||||||
.with_horizontal_padding(CONTENT_PADDING)
|
.with_horizontal_padding(CONTENT_PADDING)
|
||||||
.with_background_color(internal_colors::neutral_2(theme))
|
.with_background(theme.background())
|
||||||
.with_border(Border::top(1.).with_border_color(internal_colors::neutral_3(theme)))
|
.with_border(Border::top(1.).with_border_color(internal_colors::neutral_3(theme)))
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
@@ -2002,7 +2045,7 @@ fn render_permissions_speedbump(
|
|||||||
)
|
)
|
||||||
.with_vertical_padding(8.)
|
.with_vertical_padding(8.)
|
||||||
.with_horizontal_padding(CONTENT_PADDING)
|
.with_horizontal_padding(CONTENT_PADDING)
|
||||||
.with_background_color(internal_colors::neutral_2(theme))
|
.with_background(theme.background())
|
||||||
.with_border(Border::top(1.).with_border_color(internal_colors::neutral_3(theme)))
|
.with_border(Border::top(1.).with_border_color(internal_colors::neutral_3(theme)))
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
@@ -2111,9 +2154,10 @@ fn render_write_to_pty_input(props: WriteToPtyInputProps, app: &AppContext) -> B
|
|||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
Container::new(clipped)
|
Container::new(clipped)
|
||||||
.with_background_color(internal_colors::neutral_2(theme))
|
.with_background(theme.background())
|
||||||
.with_horizontal_padding(CONTENT_PADDING)
|
.with_horizontal_padding(CONTENT_PADDING)
|
||||||
.with_vertical_padding(8.)
|
.with_padding_top(8.)
|
||||||
|
.with_padding_bottom(12.)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2191,7 +2235,7 @@ fn render_search_action_input(
|
|||||||
|
|
||||||
Some(
|
Some(
|
||||||
Container::new(text)
|
Container::new(text)
|
||||||
.with_background_color(internal_colors::neutral_2(theme))
|
.with_background(theme.background())
|
||||||
.with_uniform_padding(CONTENT_PADDING)
|
.with_uniform_padding(CONTENT_PADDING)
|
||||||
.finish(),
|
.finish(),
|
||||||
)
|
)
|
||||||
@@ -2199,6 +2243,9 @@ fn render_search_action_input(
|
|||||||
|
|
||||||
struct BlockedActionProps<'a> {
|
struct BlockedActionProps<'a> {
|
||||||
header: String,
|
header: String,
|
||||||
|
review_detail: Option<String>,
|
||||||
|
is_expanded: bool,
|
||||||
|
header_mouse_state: &'a MouseStateHandle,
|
||||||
description: Option<Box<dyn Element>>,
|
description: Option<Box<dyn Element>>,
|
||||||
|
|
||||||
is_allow_menu_open: bool,
|
is_allow_menu_open: bool,
|
||||||
@@ -2217,7 +2264,7 @@ fn render_blocked_action(props: BlockedActionProps<'_>, app: &AppContext) -> Box
|
|||||||
appearance.ui_font_family(),
|
appearance.ui_font_family(),
|
||||||
appearance.monospace_font_size(),
|
appearance.monospace_font_size(),
|
||||||
)
|
)
|
||||||
.with_color(theme.active_ui_text_color().into())
|
.with_color(blended_colors::text_main(theme, theme.surface_2()))
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
let icon = Container::new(
|
let icon = Container::new(
|
||||||
@@ -2229,27 +2276,101 @@ fn render_blocked_action(props: BlockedActionProps<'_>, app: &AppContext) -> Box
|
|||||||
.with_margin_right(AVATAR_RIGHT_MARGIN)
|
.with_margin_right(AVATAR_RIGHT_MARGIN)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
|
let chevron = if props.is_expanded {
|
||||||
|
Icon::ChevronDown
|
||||||
|
} else {
|
||||||
|
Icon::ChevronRight
|
||||||
|
};
|
||||||
let header = Container::new(
|
let header = Container::new(
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||||
.with_children(vec![icon, Shrinkable::new(1.0, text).finish()])
|
.with_children(vec![
|
||||||
|
icon,
|
||||||
|
Shrinkable::new(1.0, text).finish(),
|
||||||
|
chevron
|
||||||
|
.to_galaxyui_icon(theme.sub_text_color(theme.surface_2()))
|
||||||
|
.finish(),
|
||||||
|
])
|
||||||
.finish(),
|
.finish(),
|
||||||
)
|
)
|
||||||
.with_background_color(internal_colors::neutral_3(theme))
|
.with_background(theme.surface_2())
|
||||||
.with_uniform_padding(CONTENT_PADDING)
|
.with_uniform_padding(CONTENT_PADDING)
|
||||||
|
.with_corner_radius(CornerRadius::with_top(Radius::Pixels(4.)))
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
|
let header = Hoverable::new(props.header_mouse_state.clone(), |_| header)
|
||||||
|
.on_click(|ctx, _, _| {
|
||||||
|
ctx.dispatch_typed_action(CLISubagentAction::ToggleApprovalExpanded);
|
||||||
|
})
|
||||||
|
.with_cursor(Cursor::PointingHand)
|
||||||
|
.finish();
|
||||||
let mut body_children = vec![header];
|
let mut body_children = vec![header];
|
||||||
|
|
||||||
if let Some(description) = props.description {
|
if props.is_expanded {
|
||||||
body_children.push(description);
|
if let Some(description) = props.description {
|
||||||
|
body_children.push(
|
||||||
|
Container::new(
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Text::new(
|
||||||
|
"REQUESTED ACTION",
|
||||||
|
appearance.ui_font_family(),
|
||||||
|
appearance.ui_font_size(),
|
||||||
|
)
|
||||||
|
.with_color(theme.accent().into())
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_child(Container::new(description).with_margin_top(4.).finish())
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_background(theme.background())
|
||||||
|
.with_horizontal_padding(CONTENT_PADDING)
|
||||||
|
.with_padding_top(14.)
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(review_detail) = props.review_detail {
|
||||||
|
body_children.push(
|
||||||
|
Container::new(
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Text::new(
|
||||||
|
"RISK REVIEW",
|
||||||
|
appearance.ui_font_family(),
|
||||||
|
appearance.ui_font_size(),
|
||||||
|
)
|
||||||
|
.with_color(theme.accent().into())
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Container::new(
|
||||||
|
Text::new(
|
||||||
|
review_detail,
|
||||||
|
appearance.ai_font_family(),
|
||||||
|
appearance.monospace_font_size(),
|
||||||
|
)
|
||||||
|
.with_color(blended_colors::text_sub(theme, theme.surface_1()))
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_margin_top(4.)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_background(theme.background())
|
||||||
|
.with_horizontal_padding(CONTENT_PADDING)
|
||||||
|
.with_padding_top(12.)
|
||||||
|
.with_padding_bottom(12.)
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(speedbump) = props.speedbump {
|
||||||
|
body_children.push(render_permissions_speedbump(speedbump, app));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let buttons = render_action_buttons(props.buttons, app);
|
let buttons = render_action_buttons(props.buttons, app);
|
||||||
body_children.push(buttons);
|
body_children.push(buttons);
|
||||||
if let Some(speedbump) = props.speedbump {
|
|
||||||
body_children.push(render_permissions_speedbump(speedbump, app));
|
|
||||||
}
|
|
||||||
|
|
||||||
let body = Flex::column()
|
let body = Flex::column()
|
||||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||||
@@ -2281,7 +2402,7 @@ fn render_blocked_action(props: BlockedActionProps<'_>, app: &AppContext) -> Box
|
|||||||
Expanded::new(
|
Expanded::new(
|
||||||
1.0,
|
1.0,
|
||||||
Container::new(stack.finish())
|
Container::new(stack.finish())
|
||||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
|
.with_corner_radius(CornerRadius::with_top(Radius::Pixels(4.)))
|
||||||
.with_border(Border::all(1.).with_border_color(internal_colors::neutral_3(theme)))
|
.with_border(Border::all(1.).with_border_color(internal_colors::neutral_3(theme)))
|
||||||
.finish(),
|
.finish(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1109,6 +1109,7 @@ impl View for AIBlock {
|
|||||||
unit_test_suggestions: &self.unit_tests_suggestions,
|
unit_test_suggestions: &self.unit_tests_suggestions,
|
||||||
todo_list_states: &self.todo_list_states,
|
todo_list_states: &self.todo_list_states,
|
||||||
collapsible_block_states: &self.collapsible_block_states,
|
collapsible_block_states: &self.collapsible_block_states,
|
||||||
|
review_pane_states: &self.review_pane_states,
|
||||||
is_selecting_text: self.state_handles.selection_handle.is_selecting(),
|
is_selecting_text: self.state_handles.selection_handle.is_selecting(),
|
||||||
is_ai_input_enabled: self
|
is_ai_input_enabled: self
|
||||||
.context_model
|
.context_model
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ pub(crate) struct Props<'a> {
|
|||||||
&'a HashMap<AIAgentActionId, ViewHandle<SuggestedUnitTestsView>>,
|
&'a HashMap<AIAgentActionId, ViewHandle<SuggestedUnitTestsView>>,
|
||||||
pub(super) todo_list_states: &'a HashMap<MessageId, TodoListElementState>,
|
pub(super) todo_list_states: &'a HashMap<MessageId, TodoListElementState>,
|
||||||
pub(super) collapsible_block_states: &'a HashMap<MessageId, CollapsibleElementState>,
|
pub(super) collapsible_block_states: &'a HashMap<MessageId, CollapsibleElementState>,
|
||||||
|
pub(super) review_pane_states: &'a HashMap<AIAgentActionId, CollapsibleElementState>,
|
||||||
pub(crate) is_selecting_text: bool,
|
pub(crate) is_selecting_text: bool,
|
||||||
pub(super) is_ai_input_enabled: bool,
|
pub(super) is_ai_input_enabled: bool,
|
||||||
pub(crate) find_context: Option<FindContext<'a>>,
|
pub(crate) find_context: Option<FindContext<'a>>,
|
||||||
@@ -324,7 +325,8 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box<dyn Element> {
|
|||||||
.as_ref(app)
|
.as_ref(app)
|
||||||
.review_display(props.conversation_id, &action.id)
|
.review_display(props.conversation_id, &action.id)
|
||||||
{
|
{
|
||||||
output_items.add_child(render_review_status(review, app));
|
output_items
|
||||||
|
.add_child(render_review_status(&action.id, review, props, app));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match &output_message.message {
|
match &output_message.message {
|
||||||
@@ -1320,8 +1322,22 @@ fn render_streaming_tool_call(
|
|||||||
render_tool_pane_shell(Clipped::new(header).finish(), false, false, false, app)
|
render_tool_pane_shell(Clipped::new(header).finish(), false, false, false, app)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_review_status(review: ReviewDisplay, app: &AppContext) -> Box<dyn Element> {
|
fn render_review_status(
|
||||||
|
action_id: &AIAgentActionId,
|
||||||
|
review: ReviewDisplay,
|
||||||
|
props: Props,
|
||||||
|
app: &AppContext,
|
||||||
|
) -> Box<dyn Element> {
|
||||||
let appearance = Appearance::as_ref(app);
|
let appearance = Appearance::as_ref(app);
|
||||||
|
let theme = appearance.theme();
|
||||||
|
let state = props.review_pane_states.get(action_id);
|
||||||
|
let is_expanded = state.is_some_and(|state| {
|
||||||
|
matches!(
|
||||||
|
state.expansion_state,
|
||||||
|
CollapsibleExpansionState::Expanded { .. }
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let needs_approval = matches!(review.state, ReviewDisplayState::AwaitingApproval);
|
||||||
let icon = match review.state {
|
let icon = match review.state {
|
||||||
ReviewDisplayState::Assessing => Icon::Loading.to_galaxyui_icon(
|
ReviewDisplayState::Assessing => Icon::Loading.to_galaxyui_icon(
|
||||||
galaxy_core::ui::theme::Fill::Solid(internal_colors::neutral_6(appearance.theme())),
|
galaxy_core::ui::theme::Fill::Solid(internal_colors::neutral_6(appearance.theme())),
|
||||||
@@ -1336,37 +1352,117 @@ fn render_review_status(review: ReviewDisplay, app: &AppContext) -> Box<dyn Elem
|
|||||||
ReviewDisplayState::Approved => "Approved",
|
ReviewDisplayState::Approved => "Approved",
|
||||||
ReviewDisplayState::Denied => "Denied",
|
ReviewDisplayState::Denied => "Denied",
|
||||||
};
|
};
|
||||||
let header = HeaderConfig::new(title, app)
|
let mut header = HeaderConfig::new(title, app)
|
||||||
.with_icon(icon)
|
.with_icon(icon)
|
||||||
.with_corner_radius_override(CornerRadius::with_all(Radius::Pixels(8.)))
|
.with_corner_radius_override(CornerRadius::with_top(Radius::Pixels(8.)));
|
||||||
.render(app);
|
if let Some(state) = state {
|
||||||
let detail = format!("{}\n{}", review.summary, review.detail);
|
let action_id = action_id.clone();
|
||||||
let body = Container::new(
|
header = header.with_interaction_mode(InteractionMode::ManuallyExpandable(
|
||||||
Text::new(
|
ExpandedConfig::new(is_expanded, state.expansion_toggle_mouse_state.clone())
|
||||||
detail,
|
.with_toggle_callback(move |ctx| {
|
||||||
appearance.ai_font_family(),
|
ctx.dispatch_typed_action(AIBlockAction::ToggleReviewPaneExpanded(
|
||||||
appearance.monospace_font_size(),
|
action_id.clone(),
|
||||||
)
|
));
|
||||||
.with_color(blended_colors::text_main(
|
}),
|
||||||
appearance.theme(),
|
));
|
||||||
appearance.theme().surface_1(),
|
}
|
||||||
))
|
|
||||||
.finish(),
|
let mut content = Flex::column()
|
||||||
)
|
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||||
.with_margin_left(16.)
|
.with_child(Clipped::new(header.render(app)).finish());
|
||||||
.with_margin_right(16.)
|
if is_expanded {
|
||||||
.with_margin_bottom(12.)
|
let detail = Flex::column()
|
||||||
.finish();
|
.with_child(
|
||||||
render_tool_pane_shell(
|
Text::new(
|
||||||
Flex::column()
|
"ACTION",
|
||||||
.with_child(Clipped::new(header).finish())
|
appearance.ui_font_family(),
|
||||||
.with_child(body)
|
appearance.ui_font_size(),
|
||||||
.finish(),
|
)
|
||||||
matches!(review.state, ReviewDisplayState::AwaitingApproval),
|
.with_color(theme.accent().into())
|
||||||
false,
|
.finish(),
|
||||||
false,
|
)
|
||||||
app,
|
.with_child(
|
||||||
)
|
Container::new(
|
||||||
|
Text::new(
|
||||||
|
review.summary,
|
||||||
|
appearance.monospace_font_family(),
|
||||||
|
appearance.monospace_font_size(),
|
||||||
|
)
|
||||||
|
.with_color(blended_colors::text_main(theme, theme.surface_1()))
|
||||||
|
.with_selectable(true)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_margin_top(4.)
|
||||||
|
.with_margin_bottom(12.)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Text::new(
|
||||||
|
"REVIEW",
|
||||||
|
appearance.ui_font_family(),
|
||||||
|
appearance.ui_font_size(),
|
||||||
|
)
|
||||||
|
.with_color(theme.accent().into())
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Container::new(
|
||||||
|
Text::new(
|
||||||
|
review.detail,
|
||||||
|
appearance.ai_font_family(),
|
||||||
|
appearance.monospace_font_size(),
|
||||||
|
)
|
||||||
|
.with_color(blended_colors::text_sub(theme, theme.surface_1()))
|
||||||
|
.with_selectable(true)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_margin_top(4.)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.finish();
|
||||||
|
content.add_child(
|
||||||
|
Container::new(detail)
|
||||||
|
.with_horizontal_padding(INLINE_ACTION_HORIZONTAL_PADDING)
|
||||||
|
.with_padding_top(14.)
|
||||||
|
.with_padding_bottom(12.)
|
||||||
|
.with_background(theme.background())
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if needs_approval {
|
||||||
|
if let Some(buttons) = props.action_buttons.get(action_id) {
|
||||||
|
content.add_child(
|
||||||
|
Container::new(
|
||||||
|
Flex::row()
|
||||||
|
.with_main_axis_alignment(MainAxisAlignment::End)
|
||||||
|
.with_child(buttons.cancel_button.render_expanded_button())
|
||||||
|
.with_child(
|
||||||
|
Container::new(buttons.run_button.render_expanded_button())
|
||||||
|
.with_margin_left(8.)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_horizontal_padding(INLINE_ACTION_HORIZONTAL_PADDING)
|
||||||
|
.with_padding_top(8.)
|
||||||
|
.with_padding_bottom(12.)
|
||||||
|
.with_background(theme.background())
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let border_color = if needs_approval {
|
||||||
|
theme.accent()
|
||||||
|
} else {
|
||||||
|
theme.surface_2()
|
||||||
|
};
|
||||||
|
Container::new(content.finish())
|
||||||
|
.with_margin_left(CONTENT_HORIZONTAL_PADDING)
|
||||||
|
.with_margin_right(CONTENT_HORIZONTAL_PADDING)
|
||||||
|
.with_margin_bottom(CONTENT_ITEM_VERTICAL_MARGIN)
|
||||||
|
.with_corner_radius(CornerRadius::with_top(Radius::Pixels(8.)))
|
||||||
|
.with_border(Border::all(1.).with_border_fill(border_color))
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_runtime_activity(
|
fn render_runtime_activity(
|
||||||
|
|||||||
@@ -934,7 +934,7 @@ impl RequestedCommandView {
|
|||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
Text::new(
|
Text::new(
|
||||||
"Your profile is set to always ask for permission to execute commands.",
|
"Command auto-execution is disabled in this profile.",
|
||||||
appearance.ui_font_family(),
|
appearance.ui_font_family(),
|
||||||
font_size,
|
font_size,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user