Files
galaxy/app/src/terminal/wormhole/success_block.rs
T
rkw6086 7c106eecd5 feat: expand Galaxy agent and remote tooling
Add Wormhole remote helpers, provider and agent improvements, filesystem diagnostics, model metadata support, and schema-aware settings IntelliSense.
2026-08-23 13:55:47 -05:00

312 lines
11 KiB
Rust

use std::borrow::Cow;
use std::sync::Arc;
use channel_versions::overrides::TargetOS;
use galaxy_core::semantic_selection::SemanticSelection;
use galaxy_core::ui::theme::GalaxyTheme;
use parking_lot::RwLock;
use warpui::elements::{
Border, Container, CrossAxisAlignment, Flex, Icon, MainAxisAlignment, MainAxisSize,
ParentElement, SelectableArea, SelectionHandle, Text,
};
use warpui::{AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext};
use super::render::HORIZONTAL_TEXT_MARGIN;
use super::settings::WormholeSettings;
use super::{render, subshell_bootstrap_success_block_bytes};
use crate::ai::agent::ProgrammingLanguage;
use crate::ai::blocklist::code_block::{render_runnable_code_snippet, CodeSnippetButtonHandles};
use crate::appearance::Appearance;
use crate::terminal::model::terminal_model::SubshellInitializationInfo;
use crate::terminal::shell::{Shell, ShellType};
use crate::ui_components::blended_colors;
use crate::ui_components::icons::Icon as UiIcon;
use crate::workspace::WorkspaceAction;
const VERTICAL_TEXT_MARGIN: f32 = 16.;
#[derive(Debug, Clone)]
pub enum WormholeSuccessBlockEvent {
OpenWormholeSettings,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum WormholeSuccessBlockAction {
ClearAutoWormholeSnippet,
OpenWormholeSettings,
}
struct AutoWormholeSnippet {
/// On subshell initialization, this will contain the output grid to display,
/// containing info like how to auto-wormhole the subshell.
output_grid: Cow<'static, str>,
/// The output grid needs to be selectable to allow users to copy the command to their clipboard.
selection_handle: SelectionHandle,
selected_text: Arc<RwLock<Option<String>>>,
shell_type: ShellType,
description: Cow<'static, str>,
code_snippet_handles: CodeSnippetButtonHandles,
can_write_to_rc: bool,
}
pub struct WormholeSuccessBlock {
spawning_command: String,
auto_wormhole_snippet: Option<AutoWormholeSnippet>,
}
impl WormholeSuccessBlock {
#[allow(clippy::new_without_default)]
pub fn new(
spawning_command: String,
subshell_info: Option<SubshellInitializationInfo>,
shell: Shell,
ctx: &mut ViewContext<Self>,
) -> Self {
ctx.subscribe_to_model(&WormholeSettings::handle(ctx), move |_, _, _, ctx| {
ctx.notify();
});
// Mac + Linux have the same behavior. We'd need to handle
// getting the OS to write to the correct RC file.
let remote_os = TargetOS::Linux;
let is_auto_wormhole_configured = subshell_info
.as_ref()
.map(|info| info.was_triggered_by_rc_file_snippet)
.unwrap_or_default();
let auto_wormhole_snippet = if is_auto_wormhole_configured {
None
} else {
subshell_info.and_then(|subshell_info| {
// If wormholing wasn't triggered automatically, show a snippet about
// how to automatically wormhole.
(!subshell_info.was_triggered_by_rc_file_snippet).then(|| {
let (command, is_executable) = subshell_bootstrap_success_block_bytes(
&subshell_info,
shell.shell_type(),
remote_os,
);
if command.is_empty() {
return ("".into(), false);
}
(
String::from_utf8(command)
.map(|content| {
// Ensure a blank line between the output grid and the learn more link.
content + "\n"
})
.unwrap_or_default(),
is_executable,
)
})
})
};
let auto_wormhole_snippet = auto_wormhole_snippet.map(|(output_grid, can_write_to_rc)| {
AutoWormholeSnippet {
description: (if !output_grid.is_empty() {
"Run the following to automatically Wormhole in the future:"
} else {
"In remote subshells, Galaxy runs commands in the background to power completions, syntax highlighting, and other features."
}).into(),
output_grid: output_grid.into(),
selection_handle: Default::default(),
selected_text: Default::default(),
code_snippet_handles: Default::default(),
shell_type: shell.shell_type(),
can_write_to_rc,
}
});
Self {
spawning_command,
auto_wormhole_snippet,
}
}
pub fn selected_text(&self) -> Option<String> {
self.auto_wormhole_snippet
.as_ref()
.and_then(|snippet| snippet.selected_text.read().clone())
}
pub fn render_spawning_command(
&self,
theme: &GalaxyTheme,
appearance: &Appearance,
) -> Box<dyn Element> {
let spawning_command = self.spawning_command.clone();
render::build_command_row(spawning_command, theme, appearance, true)
.with_margin_bottom(VERTICAL_TEXT_MARGIN)
.finish()
}
pub fn render_title_ui(
&self,
theme: &GalaxyTheme,
appearance: &Appearance,
) -> Box<dyn Element> {
let header_contents = render::build_header_row(
"Session Wormholed",
Icon::new(UiIcon::GalaxyLogo.into(), theme.active_ui_detail()),
theme,
appearance,
)
.with_margin_right(8.)
.finish();
Container::new(
Flex::row()
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
.with_cross_axis_alignment(CrossAxisAlignment::End)
.with_main_axis_size(MainAxisSize::Max)
.with_child(header_contents)
.finish(),
)
.with_horizontal_margin(HORIZONTAL_TEXT_MARGIN)
.with_margin_top(VERTICAL_TEXT_MARGIN)
.finish()
}
/// Fired when a block ends and we are not in a Wormholed session.
pub fn on_wormholed_session_complete(&mut self, ctx: &mut ViewContext<Self>) {
self.clear_auto_wormhole_snippet(ctx);
}
pub fn clear_auto_wormhole_snippet(&mut self, ctx: &mut ViewContext<Self>) {
self.auto_wormhole_snippet = None;
ctx.notify();
}
/// If there is an output grid to display, render it.
pub fn render_output_grid(
&self,
app: &AppContext,
appearance: &Appearance,
) -> Option<Box<dyn Element>> {
let theme = appearance.theme();
let auto_wormhole_snippet = self.auto_wormhole_snippet.as_ref()?;
if auto_wormhole_snippet.output_grid.is_empty() {
return None;
}
let shell_language = ProgrammingLanguage::Shell(auto_wormhole_snippet.shell_type);
let runnable_command = render_runnable_code_snippet(
&auto_wormhole_snippet.output_grid,
if auto_wormhole_snippet.can_write_to_rc {
Some(&shell_language)
} else {
None
},
Some(Box::new({
move |code_snippet, ctx| {
ctx.dispatch_typed_action(WorkspaceAction::RunCommand(
code_snippet.to_string(),
));
ctx.dispatch_typed_action(WormholeSuccessBlockAction::ClearAutoWormholeSnippet);
}
})),
Some(Box::new({
move |code_snippet, ctx| {
ctx.dispatch_typed_action(WorkspaceAction::CopyTextToClipboard(code_snippet));
}
})),
Some(auto_wormhole_snippet.code_snippet_handles.clone()),
app,
);
let semantic_selection = SemanticSelection::as_ref(app);
let selected_text = auto_wormhole_snippet.selected_text.clone();
// TODO(Simon): Implement full selection and copying functionality for the WormholeSuccessBlock.
// Look to the `EnvVarCollectionBlock` for the existing implementation paradigm. We don't
// yet have a robust way of ensuring that every aspect of text selection is implemented
// properly, so be extra careful not to miss any details!
let output_grid = SelectableArea::new(
auto_wormhole_snippet.selection_handle.clone(),
move |selection_args, _, _| {
*selected_text.write() = selection_args.selection;
},
runnable_command,
)
.with_word_boundaries_policy(semantic_selection.word_boundary_policy())
.with_smart_select_fn(semantic_selection.smart_select_fn())
.finish();
let output_grid = Flex::column()
.with_child(
Container::new(
Text::new(
auto_wormhole_snippet.description.clone(),
appearance.monospace_font_family(),
appearance.monospace_font_size(),
)
.with_color(blended_colors::text_main(theme, theme.background()))
.finish(),
)
.with_horizontal_margin(HORIZONTAL_TEXT_MARGIN)
.with_margin_bottom(VERTICAL_TEXT_MARGIN)
.finish(),
)
.with_child(
Container::new(output_grid)
.with_horizontal_margin(HORIZONTAL_TEXT_MARGIN)
.with_margin_bottom(VERTICAL_TEXT_MARGIN)
.finish(),
)
.finish();
Some(output_grid)
}
}
impl Entity for WormholeSuccessBlock {
type Event = WormholeSuccessBlockEvent;
}
pub const WORMHOLE_SUCCESS_BLOCK_VISIBLE_KEY: &str = "WormholeSuccessBlockVisible";
impl View for WormholeSuccessBlock {
fn ui_name() -> &'static str {
"WormholeSuccessBlock"
}
fn render(&self, app: &AppContext) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
let mut content = Flex::column();
content.add_children([
self.render_title_ui(theme, appearance),
self.render_spawning_command(theme, appearance),
]);
if let Some(output_grid) = self.render_output_grid(app, appearance) {
content.add_child(output_grid);
}
Container::new(content.finish())
.with_background(theme.foreground().with_opacity(10))
.with_border(Border::top(1.).with_border_fill(theme.outline()))
.finish()
}
}
impl TypedActionView for WormholeSuccessBlock {
type Action = WormholeSuccessBlockAction;
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
match action {
WormholeSuccessBlockAction::OpenWormholeSettings => {
ctx.emit(WormholeSuccessBlockEvent::OpenWormholeSettings);
}
WormholeSuccessBlockAction::ClearAutoWormholeSnippet => {
self.clear_auto_wormhole_snippet(ctx);
}
}
}
}