Improve local tool execution and shell output panes
This commit is contained in:
@@ -1,17 +1,26 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use galaxyui::elements::{
|
||||
AfterLayoutContext, AppContext, Element, EventContext, LayoutContext, PaintContext, Point,
|
||||
SizeConstraint,
|
||||
};
|
||||
use galaxyui::event::DispatchedEvent;
|
||||
use galaxyui::fonts::Properties;
|
||||
use galaxyui::geometry::rect::RectF;
|
||||
use galaxyui::units::IntoPixels;
|
||||
use galaxyui::EntityId;
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
|
||||
use super::blockgrid_renderer::GridRenderParams;
|
||||
use crate::appearance::Appearance;
|
||||
use crate::settings::EnforceMinimumContrast;
|
||||
use crate::terminal::blockgrid_renderer::BlockGridParams;
|
||||
use crate::terminal::grid_renderer::{self, CellGlyphCache};
|
||||
use crate::terminal::grid_size_util::grid_cell_dimensions;
|
||||
use crate::terminal::model::blockgrid::BlockGrid;
|
||||
use crate::terminal::model::grid::Dimensions;
|
||||
use crate::terminal::model::grid::grid_handler::{GridHandler, TermMode};
|
||||
use crate::terminal::model::grid::{Dimensions, RespectDisplayedOutput};
|
||||
use crate::terminal::model::image_map::StoredImageMetadata;
|
||||
use crate::terminal::model::ObfuscateSecrets;
|
||||
use crate::terminal::{color, SizeInfo};
|
||||
|
||||
@@ -119,3 +128,206 @@ impl Element for BlockGridElement {
|
||||
self.origin
|
||||
}
|
||||
}
|
||||
|
||||
/// A paint-only snapshot of a live terminal grid.
|
||||
///
|
||||
/// Unlike [`BlockGridElement`], this can render both normal command output and an alternate-screen
|
||||
/// grid. It deliberately owns a snapshot so the terminal model lock never spans layout or paint.
|
||||
pub struct TerminalGridSnapshotElement {
|
||||
grid: GridHandler,
|
||||
row_count: usize,
|
||||
colors: color::List,
|
||||
override_colors: color::OverrideList,
|
||||
image_metadata: HashMap<u32, StoredImageMetadata>,
|
||||
grid_render_params: GridRenderParams,
|
||||
terminal_view_id: EntityId,
|
||||
natural_size: Vector2F,
|
||||
size: Vector2F,
|
||||
origin: Option<Point>,
|
||||
bounds: Option<RectF>,
|
||||
}
|
||||
|
||||
impl TerminalGridSnapshotElement {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
grid: &GridHandler,
|
||||
row_count: usize,
|
||||
colors: color::List,
|
||||
override_colors: color::OverrideList,
|
||||
image_metadata: HashMap<u32, StoredImageMetadata>,
|
||||
appearance: &Appearance,
|
||||
enforce_minimum_contrast: EnforceMinimumContrast,
|
||||
obfuscate_secrets: ObfuscateSecrets,
|
||||
terminal_view_id: EntityId,
|
||||
app: &AppContext,
|
||||
) -> Self {
|
||||
let cell_size = grid_cell_dimensions(
|
||||
app.font_cache(),
|
||||
appearance.monospace_font_family(),
|
||||
appearance.monospace_font_size(),
|
||||
appearance.line_height_ratio(),
|
||||
);
|
||||
let size = vec2f(
|
||||
grid.columns() as f32 * cell_size.x(),
|
||||
row_count as f32 * cell_size.y(),
|
||||
);
|
||||
let size_info = SizeInfo::new(
|
||||
size,
|
||||
cell_size.x().into_pixels(),
|
||||
cell_size.y().into_pixels(),
|
||||
0.0.into_pixels(),
|
||||
0.0.into_pixels(),
|
||||
)
|
||||
.with_rows_and_columns(row_count, grid.columns());
|
||||
|
||||
Self {
|
||||
grid: grid.clone(),
|
||||
row_count,
|
||||
colors,
|
||||
override_colors,
|
||||
image_metadata,
|
||||
grid_render_params: GridRenderParams {
|
||||
warp_theme: appearance.theme().clone(),
|
||||
font_family: appearance.monospace_font_family(),
|
||||
font_size: appearance.monospace_font_size(),
|
||||
font_weight: appearance.monospace_font_weight(),
|
||||
line_height_ratio: appearance.line_height_ratio(),
|
||||
enforce_minimum_contrast,
|
||||
obfuscate_secrets,
|
||||
size_info,
|
||||
cell_size,
|
||||
use_ligature_rendering: false,
|
||||
hide_cursor_cell: false,
|
||||
},
|
||||
terminal_view_id,
|
||||
natural_size: size,
|
||||
size,
|
||||
origin: None,
|
||||
bounds: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_ligature_rendering(mut self) -> Self {
|
||||
self.grid_render_params.use_ligature_rendering = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for TerminalGridSnapshotElement {
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
_ctx: &mut LayoutContext,
|
||||
_app: &AppContext,
|
||||
) -> Vector2F {
|
||||
// The parent clips and scrolls this element vertically, so preserve the full terminal
|
||||
// height here. Clamping it to the viewport makes the scroll container believe rows below
|
||||
// the fold do not exist, which also prevents its bottom-follow behavior from engaging.
|
||||
self.size = vec2f(
|
||||
self.natural_size
|
||||
.x()
|
||||
.min(constraint.max.x())
|
||||
.max(constraint.min.x()),
|
||||
self.natural_size.y().max(constraint.min.y()),
|
||||
);
|
||||
self.size
|
||||
}
|
||||
|
||||
fn after_layout(&mut self, _ctx: &mut AfterLayoutContext, _app: &AppContext) {}
|
||||
|
||||
fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) {
|
||||
self.origin = Some(Point::from_vec2f(origin, ctx.scene.z_index()));
|
||||
self.bounds = Some(RectF::new(origin, self.size));
|
||||
|
||||
let Some(visible_bounds) = ctx.scene.visible_rect(
|
||||
self.origin.expect("origin was set immediately above"),
|
||||
self.size,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
let row_height = self.grid_render_params.cell_size.y();
|
||||
let start_row = ((visible_bounds.min_y() - origin.y()) / row_height)
|
||||
.floor()
|
||||
.max(0.) as usize;
|
||||
let end_row = ((visible_bounds.max_y() - origin.y()) / row_height)
|
||||
.ceil()
|
||||
.max(0.) as usize;
|
||||
let start_row = start_row.min(self.row_count);
|
||||
let end_row = end_row.min(self.row_count);
|
||||
|
||||
let mut glyphs = CellGlyphCache::default();
|
||||
let cursor_visible = self.grid.is_mode_set(TermMode::SHOW_CURSOR)
|
||||
&& (start_row..end_row).contains(&self.grid.cursor_render_point().row);
|
||||
let cursor_style = self.grid.cursor_style();
|
||||
let obfuscate_secrets = self
|
||||
.grid_render_params
|
||||
.obfuscate_secrets
|
||||
.and(&self.grid.get_secret_obfuscation());
|
||||
|
||||
grid_renderer::render_grid(
|
||||
&self.grid,
|
||||
start_row,
|
||||
end_row,
|
||||
&self.colors,
|
||||
&self.override_colors,
|
||||
&self.grid_render_params.warp_theme,
|
||||
Properties::default().weight(self.grid_render_params.font_weight),
|
||||
self.grid_render_params.font_family,
|
||||
self.grid_render_params.font_size,
|
||||
self.grid_render_params.line_height_ratio,
|
||||
self.grid_render_params.cell_size,
|
||||
self.grid_render_params.size_info.padding_x_px(),
|
||||
origin,
|
||||
&mut glyphs,
|
||||
255,
|
||||
None,
|
||||
None,
|
||||
None::<std::iter::Empty<&std::ops::RangeInclusive<crate::terminal::model::index::Point>>>,
|
||||
None,
|
||||
self.grid_render_params.enforce_minimum_contrast,
|
||||
obfuscate_secrets,
|
||||
None,
|
||||
self.grid_render_params.use_ligature_rendering,
|
||||
cursor_visible.then_some(cursor_style.shape),
|
||||
RespectDisplayedOutput::Yes,
|
||||
&self.image_metadata,
|
||||
None,
|
||||
false,
|
||||
ctx,
|
||||
app,
|
||||
);
|
||||
|
||||
if cursor_visible {
|
||||
grid_renderer::render_cursor(
|
||||
&self.grid_render_params,
|
||||
self.grid.cursor_render_point(),
|
||||
self.grid.is_cursor_on_wide_char(),
|
||||
cursor_style,
|
||||
self.grid_render_params.size_info.padding_x_px(),
|
||||
origin,
|
||||
self.grid_render_params.warp_theme.cursor().into(),
|
||||
ctx,
|
||||
self.terminal_view_id,
|
||||
None,
|
||||
app,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
_event: &DispatchedEvent,
|
||||
_ctx: &mut EventContext,
|
||||
_app: &AppContext,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn size(&self) -> Option<Vector2F> {
|
||||
Some(self.size)
|
||||
}
|
||||
|
||||
fn origin(&self) -> Option<Point> {
|
||||
self.origin
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,15 +346,20 @@ impl InteractionMode {
|
||||
task_id: &TaskId,
|
||||
conversation_id: AIConversationId,
|
||||
) -> Result<Self, UpdateInteractionModeError> {
|
||||
let requested_command_action_id = match self {
|
||||
InteractionMode::User(_) => None,
|
||||
InteractionMode::Agent(metadata) => {
|
||||
if metadata.conversation_id != conversation_id {
|
||||
return Err(UpdateInteractionModeError::UnexpectedConversationId);
|
||||
let (requested_command_action_id, has_agent_written_to_block, should_hide_block) =
|
||||
match self {
|
||||
InteractionMode::User(_) => (None, false, false),
|
||||
InteractionMode::Agent(metadata) => {
|
||||
if metadata.conversation_id != conversation_id {
|
||||
return Err(UpdateInteractionModeError::UnexpectedConversationId);
|
||||
}
|
||||
(
|
||||
metadata.requested_command_action_id.clone(),
|
||||
metadata.has_agent_written_to_block,
|
||||
metadata.should_hide_block,
|
||||
)
|
||||
}
|
||||
metadata.requested_command_action_id.clone()
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Ok(Self::Agent(AgentInteractionMetadata {
|
||||
requested_command_action_id,
|
||||
@@ -364,8 +369,8 @@ impl InteractionMode {
|
||||
is_blocked: false,
|
||||
should_hide_responses: false,
|
||||
}),
|
||||
has_agent_written_to_block: false,
|
||||
should_hide_block: false,
|
||||
has_agent_written_to_block,
|
||||
should_hide_block,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -6791,7 +6791,18 @@ impl TerminalView {
|
||||
agent_has_control,
|
||||
..
|
||||
} => {
|
||||
self.redetermine_terminal_focus(ctx);
|
||||
if !*agent_has_control && ctx.is_self_or_child_focused() {
|
||||
let block_index = self.model.lock().block_list().block_index_for_id(block_id);
|
||||
if let Some(block_index) = block_index {
|
||||
self.update_scroll_position_locking(
|
||||
ScrollPositionUpdate::ScrollToBottomOfBlock { block_index },
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
self.focus_terminal(ctx);
|
||||
} else {
|
||||
self.redetermine_terminal_focus(ctx);
|
||||
}
|
||||
self.emit_long_running_command_agent_interaction_state_changed(
|
||||
*agent_has_control,
|
||||
block_id.clone(),
|
||||
@@ -9744,6 +9755,7 @@ impl TerminalView {
|
||||
model.block_list_mut().update_active_block_height();
|
||||
}
|
||||
self.maybe_emit_terminal_view_state_changed_for_long_running_block(ctx);
|
||||
self.notify_active_requested_command_output(ctx);
|
||||
self.use_agent_footer.update(ctx, |footer, ctx| {
|
||||
footer.notify_and_notify_children(ctx);
|
||||
});
|
||||
@@ -9752,6 +9764,47 @@ impl TerminalView {
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
/// Invalidates the nested tool pane that owns the active requested-command block.
|
||||
///
|
||||
/// `TerminalView` and `RequestedCommandView` are independently cached views. A PTY wakeup
|
||||
/// invalidates the former, but without this targeted notification the inline pane only sees
|
||||
/// new output when a slower action or conversation event happens to invalidate it.
|
||||
fn notify_active_requested_command_output(&self, ctx: &mut ViewContext<Self>) {
|
||||
let active_command = {
|
||||
let model = self.model.lock();
|
||||
model
|
||||
.block_list()
|
||||
.active_block()
|
||||
.agent_interaction_metadata()
|
||||
.and_then(|metadata| {
|
||||
metadata
|
||||
.requested_command_action_id()
|
||||
.cloned()
|
||||
.map(|action_id| (action_id, *metadata.conversation_id()))
|
||||
})
|
||||
};
|
||||
let Some((action_id, conversation_id)) = active_command else {
|
||||
return;
|
||||
};
|
||||
|
||||
let requested_command_view = self
|
||||
.rich_content_views
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|rich_content| rich_content.ai_block_metadata())
|
||||
.filter(|metadata| metadata.conversation_id == conversation_id)
|
||||
.find_map(|metadata| {
|
||||
metadata
|
||||
.ai_block_handle
|
||||
.as_ref(ctx)
|
||||
.requested_command_view(&action_id)
|
||||
});
|
||||
|
||||
if let Some(requested_command_view) = requested_command_view {
|
||||
requested_command_view.update(ctx, |_, ctx| ctx.notify());
|
||||
}
|
||||
}
|
||||
|
||||
/// This function is invoked whenever we detect an SSH ControlMaster error,
|
||||
/// in which case completions will not work as expected.
|
||||
fn handle_control_master_error(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
@@ -18951,9 +19004,9 @@ impl TerminalView {
|
||||
// Otherwise, if the agent is monitoring this long-running block,
|
||||
// then clear just that block and leave the rest of the blocklist in tact.
|
||||
self.model.lock().clear_screen(ClearMode::ActiveBlock);
|
||||
// In the agent-driving-but-not-monitoring state the terminal block is hidden. Make
|
||||
// it visible and expand the header immediately so the user sees the cleared state
|
||||
// right away, rather than waiting for the auto-expand timer (~3 s).
|
||||
// In the agent-driving-but-not-monitoring state the terminal block is hidden.
|
||||
// Expand its inline tool pane immediately so the user sees the cleared state there,
|
||||
// rather than revealing a duplicate terminal block below the conversation.
|
||||
if is_agent_driving_command && !is_agent_monitoring {
|
||||
let requested_command_action_id = self
|
||||
.model
|
||||
@@ -18963,10 +19016,6 @@ impl TerminalView {
|
||||
.requested_command_action_id()
|
||||
.cloned();
|
||||
if let Some(action_id) = &requested_command_action_id {
|
||||
self.model
|
||||
.lock()
|
||||
.block_list_mut()
|
||||
.set_visibility_of_block_for_ai_action(action_id, true);
|
||||
if let Some(ai_block_handle) = self.active_ai_block(ctx).cloned() {
|
||||
ai_block_handle.update(ctx, |ai_block, ctx| {
|
||||
ai_block.expand_requested_command_view(action_id, ctx);
|
||||
|
||||
Reference in New Issue
Block a user