739 lines
27 KiB
Rust
739 lines
27 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use galaxy_core::report_error;
|
|
use galaxy_core::ui::theme::{AnsiColorIdentifier, AnsiColors};
|
|
use serde::Serialize;
|
|
use string_offset::CharCounter;
|
|
use warp_completer::signatures::CommandRegistry;
|
|
use warp_completer::util::parse_current_commands_and_tokens;
|
|
use warp_completer::ParsedTokensSnapshot;
|
|
use warpui::clipboard::ClipboardContent;
|
|
use warpui::elements::new_scrollable::{ClippedAxisConfiguration, DualAxisConfig, NewScrollable};
|
|
use warpui::elements::{
|
|
Align, Border, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius,
|
|
CrossAxisAlignment, Element, Empty, Expanded, Flex, Highlight, HighlightedRange,
|
|
MainAxisAlignment, MainAxisSize, MouseStateHandle, ParentElement, Radius, Text,
|
|
};
|
|
use warpui::fonts::{Properties, Weight};
|
|
use warpui::prelude::ChildView;
|
|
use warpui::text_layout::TextStyle;
|
|
use warpui::ui_components::components::{UiComponent, UiComponentStyles};
|
|
use warpui::{AppContext, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle};
|
|
|
|
use crate::ai::agent_management::telemetry::{AgentManagementTelemetryEvent, SetupGuideStep};
|
|
use crate::ai::blocklist::code_block::{
|
|
render_code_block_plain, CodeBlockOptions, CodeSnippetButtonHandles,
|
|
};
|
|
use crate::appearance::Appearance;
|
|
use crate::completer::SessionAgnosticContext;
|
|
use crate::send_telemetry_from_ctx;
|
|
use crate::view_components::action_button::{ActionButton, SecondaryTheme};
|
|
use crate::workflows::workflow::{Argument, ArgumentType, Workflow};
|
|
use crate::workflows::WorkflowType;
|
|
|
|
const DOCS_URL: &str = "https://docs.warp.dev/agent-platform/cloud-agents/overview";
|
|
const ENV_DOCS_URL: &str =
|
|
"https://docs.warp.dev/reference/cli/integration-setup#creating-an-environment";
|
|
const OZ_URL: &str = "https://oz.warp.dev";
|
|
|
|
const CONTENT_MAX_WIDTH: f32 = 720.;
|
|
|
|
const CREATE_ENV_SLASH_CMD: &str = "/create-environment";
|
|
const CREATE_ENV_CLI_CMD: &str =
|
|
"oz environment create [OPTIONS] --name <NAME> --docker-image <DOCKER_IMAGE>";
|
|
const CREATE_SLACK_INTEGRATION_CMD: &str =
|
|
"oz integration create slack --environment {{environment_id}}";
|
|
const CREATE_LINEAR_INTEGRATION_CMD: &str =
|
|
"oz integration create linear --environment {{environment_id}}";
|
|
|
|
pub struct CloudSetupGuideView {
|
|
create_env_code_handles: CodeSnippetButtonHandles,
|
|
create_env_cli_code_handles: CodeSnippetButtonHandles,
|
|
create_slack_integration_code_handles: CodeSnippetButtonHandles,
|
|
create_linear_integration_code_handles: CodeSnippetButtonHandles,
|
|
docs_link_mouse_state: MouseStateHandle,
|
|
env_docs_link_mouse_state: MouseStateHandle,
|
|
integration_docs_link_mouse_state: MouseStateHandle,
|
|
visit_oz_button: ViewHandle<ActionButton>,
|
|
parsed_tokens: HashMap<&'static str, ParsedTokensSnapshot>,
|
|
vertical_scroll_state: ClippedScrollStateHandle,
|
|
horizontal_scroll_state: ClippedScrollStateHandle,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum CloudSetupGuideAction {
|
|
CopyCode {
|
|
code: String,
|
|
step: SetupGuideStep,
|
|
},
|
|
RunWorkflow {
|
|
workflow: Box<WorkflowType>,
|
|
step: SetupGuideStep,
|
|
},
|
|
VisitOz,
|
|
OpenDocs {
|
|
docs: SetupGuideDocs,
|
|
},
|
|
}
|
|
|
|
/// Which URL the user clicked in the setup guide (also used in telemetry)
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
pub enum SetupGuideDocs {
|
|
Main,
|
|
Environment,
|
|
Integration,
|
|
}
|
|
|
|
pub enum CloudSetupGuideEvent {
|
|
OpenNewTabAndInsertWorkflow(WorkflowType),
|
|
}
|
|
|
|
impl CloudSetupGuideView {
|
|
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
|
let code_snippets: Vec<&'static str> = vec![
|
|
CREATE_ENV_SLASH_CMD,
|
|
CREATE_ENV_CLI_CMD,
|
|
CREATE_SLACK_INTEGRATION_CMD,
|
|
CREATE_LINEAR_INTEGRATION_CMD,
|
|
];
|
|
|
|
// We spawn a background task to parse the commands in the code blocks,
|
|
// and re-render the blocks once it's done. Colors are computed at render time
|
|
// so they respond to theme changes.
|
|
let context = SessionAgnosticContext::new(CommandRegistry::global_instance());
|
|
ctx.spawn(
|
|
async move {
|
|
let mut results = HashMap::new();
|
|
for snippet in code_snippets {
|
|
let parsed =
|
|
parse_current_commands_and_tokens(snippet.to_string(), &context).await;
|
|
results.insert(snippet, parsed);
|
|
}
|
|
results
|
|
},
|
|
|view, results, ctx| {
|
|
view.parsed_tokens = results;
|
|
ctx.notify();
|
|
},
|
|
);
|
|
|
|
let visit_oz_button = ctx.add_typed_action_view(|_| {
|
|
ActionButton::new("Visit Oz", SecondaryTheme)
|
|
.on_click(|ctx| ctx.dispatch_typed_action(CloudSetupGuideAction::VisitOz))
|
|
});
|
|
|
|
Self {
|
|
create_env_code_handles: CodeSnippetButtonHandles::default(),
|
|
create_env_cli_code_handles: CodeSnippetButtonHandles::default(),
|
|
create_slack_integration_code_handles: CodeSnippetButtonHandles::default(),
|
|
create_linear_integration_code_handles: CodeSnippetButtonHandles::default(),
|
|
docs_link_mouse_state: MouseStateHandle::default(),
|
|
env_docs_link_mouse_state: MouseStateHandle::default(),
|
|
integration_docs_link_mouse_state: MouseStateHandle::default(),
|
|
visit_oz_button,
|
|
parsed_tokens: HashMap::new(),
|
|
vertical_scroll_state: ClippedScrollStateHandle::default(),
|
|
horizontal_scroll_state: ClippedScrollStateHandle::default(),
|
|
}
|
|
}
|
|
|
|
/// Render the main header for the setup guide.
|
|
fn render_header(&self, appearance: &Appearance) -> Box<dyn Element> {
|
|
let theme = appearance.theme();
|
|
let title_font_size = 24.;
|
|
let subtitle_font_size = 16.;
|
|
|
|
let mut header_container = Flex::column().with_spacing(8.);
|
|
|
|
let title = Text::new(
|
|
"Getting started with Oz cloud agents",
|
|
appearance.ui_font_family(),
|
|
title_font_size,
|
|
)
|
|
.with_style(Properties::default().weight(Weight::Semibold))
|
|
.with_color(theme.active_ui_text_color().into_solid())
|
|
.finish();
|
|
header_container.add_child(title);
|
|
|
|
let subtitle = Text::new(
|
|
"Start Oz cloud agents directly in Warp from an integration (Linear, Slack), with an event (GitHub, built-in schedule), or programmatically with the Oz SDK or CLI.",
|
|
appearance.ui_font_family(),
|
|
subtitle_font_size,
|
|
)
|
|
.with_color(theme.nonactive_ui_text_color().into_solid())
|
|
.finish();
|
|
header_container.add_child(subtitle);
|
|
|
|
// Documentation link line.
|
|
let docs_line = Flex::row()
|
|
.with_child(
|
|
Text::new_inline(
|
|
"Check out the ",
|
|
appearance.ui_font_family(),
|
|
subtitle_font_size,
|
|
)
|
|
.with_color(theme.nonactive_ui_text_color().into_solid())
|
|
.finish(),
|
|
)
|
|
.with_child(
|
|
appearance
|
|
.ui_builder()
|
|
.link(
|
|
"Oz documentation".to_string(),
|
|
None,
|
|
Some(Box::new(|ctx| {
|
|
ctx.dispatch_typed_action(CloudSetupGuideAction::OpenDocs {
|
|
docs: SetupGuideDocs::Main,
|
|
});
|
|
})),
|
|
self.docs_link_mouse_state.clone(),
|
|
)
|
|
.with_style(UiComponentStyles {
|
|
font_size: Some(subtitle_font_size),
|
|
..Default::default()
|
|
})
|
|
.build()
|
|
.finish(),
|
|
)
|
|
.with_child(
|
|
Text::new_inline(
|
|
" to learn more.",
|
|
appearance.ui_font_family(),
|
|
subtitle_font_size,
|
|
)
|
|
.with_color(theme.nonactive_ui_text_color().into_solid())
|
|
.finish(),
|
|
);
|
|
header_container.add_child(docs_line.finish());
|
|
|
|
header_container.finish()
|
|
}
|
|
|
|
/// Render the quick start banner with link to oz.warp.dev.
|
|
fn render_quick_start_banner(&self, appearance: &Appearance) -> Box<dyn Element> {
|
|
let theme = appearance.theme();
|
|
let font_size = 16.;
|
|
|
|
let text = Text::new_inline(
|
|
"Quick start: Visit oz.warp.dev for a UI-based setup experience.",
|
|
appearance.ui_font_family(),
|
|
font_size,
|
|
)
|
|
.with_style(Properties::default().weight(Weight::Semibold))
|
|
.with_color(theme.active_ui_text_color().into_solid())
|
|
.finish();
|
|
|
|
// Use cyan overlay for the blue border per Figma spec.
|
|
let border_color = theme.ansi_overlay_2(theme.terminal_colors().normal.cyan);
|
|
|
|
Container::new(
|
|
Flex::row()
|
|
.with_main_axis_size(MainAxisSize::Max)
|
|
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.with_child(text)
|
|
.with_child(ChildView::new(&self.visit_oz_button).finish())
|
|
.finish(),
|
|
)
|
|
.with_background(theme.surface_overlay_1())
|
|
.with_border(Border::all(1.).with_border_fill(border_color))
|
|
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(8.)))
|
|
.with_horizontal_padding(16.)
|
|
.with_vertical_padding(12.)
|
|
.finish()
|
|
}
|
|
|
|
/// Render the manual setup section header.
|
|
fn render_manual_setup_header(&self, appearance: &Appearance) -> Box<dyn Element> {
|
|
let theme = appearance.theme();
|
|
let font_size = 16.;
|
|
|
|
Text::new(
|
|
"Manual setup: Create a Slack or Linear integration with the Oz CLI",
|
|
appearance.ui_font_family(),
|
|
font_size,
|
|
)
|
|
.with_style(Properties::default().weight(Weight::Semibold))
|
|
.with_color(theme.active_ui_text_color().into_solid())
|
|
.finish()
|
|
}
|
|
|
|
/// Render a styled number to be displayed with each step.
|
|
fn render_step_number(number: u32, appearance: &Appearance) -> Box<dyn Element> {
|
|
let theme = appearance.theme();
|
|
|
|
let number_text = Text::new(
|
|
number.to_string(),
|
|
appearance.ui_font_family(),
|
|
appearance.ui_font_size(),
|
|
)
|
|
.with_style(Properties::default().weight(Weight::Semibold))
|
|
.with_color(theme.active_ui_text_color().into_solid())
|
|
.finish();
|
|
|
|
let centered_number = Flex::row()
|
|
.with_main_axis_size(MainAxisSize::Max)
|
|
.with_main_axis_alignment(MainAxisAlignment::Center)
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.with_child(number_text)
|
|
.finish();
|
|
|
|
Container::new(
|
|
ConstrainedBox::new(centered_number)
|
|
.with_width(28.)
|
|
.with_height(28.)
|
|
.finish(),
|
|
)
|
|
.with_background(theme.surface_1())
|
|
.with_border(Border::all(1.).with_border_fill(theme.outline()))
|
|
.with_corner_radius(CornerRadius::with_all(Radius::Percentage(50.)))
|
|
.finish()
|
|
}
|
|
|
|
/// Render a description that includes a link at the end
|
|
/// (e.g. "Use warp's environment setup command to have an agent help you through it. LINK[Visit docs]")
|
|
fn render_description_with_link(
|
|
prefix: &'static str,
|
|
link_text: &'static str,
|
|
link_mouse_state: MouseStateHandle,
|
|
telemetry_url: SetupGuideDocs,
|
|
appearance: &Appearance,
|
|
) -> Box<dyn Element> {
|
|
let step_desc_font_size = 14.;
|
|
let link = appearance
|
|
.ui_builder()
|
|
.link(
|
|
link_text.to_string(),
|
|
None,
|
|
Some(Box::new(move |ctx| {
|
|
ctx.dispatch_typed_action(CloudSetupGuideAction::OpenDocs {
|
|
docs: telemetry_url,
|
|
});
|
|
})),
|
|
link_mouse_state,
|
|
)
|
|
.with_style(UiComponentStyles {
|
|
font_size: Some(step_desc_font_size),
|
|
..Default::default()
|
|
})
|
|
.build()
|
|
.finish();
|
|
|
|
Flex::row()
|
|
.with_child(
|
|
Text::new_inline(prefix, appearance.ui_font_family(), step_desc_font_size)
|
|
.with_color(appearance.theme().nonactive_ui_text_color().into_solid())
|
|
.finish(),
|
|
)
|
|
.with_child(link)
|
|
.finish()
|
|
}
|
|
|
|
/// Render a code block with buttons to copy and run the code.
|
|
fn render_code_block(
|
|
&self,
|
|
code: &'static str,
|
|
handles: CodeSnippetButtonHandles,
|
|
app: &AppContext,
|
|
) -> Box<dyn Element> {
|
|
let terminal_colors = Appearance::as_ref(app).theme().terminal_colors().normal;
|
|
let highlights = self
|
|
.parsed_tokens
|
|
.get(code)
|
|
.map(|parsed| tokens_to_highlight_ranges(parsed, &terminal_colors))
|
|
.unwrap_or_default();
|
|
|
|
// Match command to formatted workflow with correct args.
|
|
let Some((workflow, setup_step)) = (match code {
|
|
CREATE_ENV_SLASH_CMD => Some((
|
|
WorkflowType::Local(
|
|
Workflow::new("Create Environment", CREATE_ENV_SLASH_CMD).with_arguments(vec![
|
|
Argument::new("github link or local filepath", ArgumentType::Text)
|
|
.with_description("GitHub link or local filepath to the repository"),
|
|
]),
|
|
),
|
|
SetupGuideStep::CreateEnvironment,
|
|
)),
|
|
CREATE_ENV_CLI_CMD => Some((
|
|
WorkflowType::Local(
|
|
Workflow::new("Create Environment (CLI)", CREATE_ENV_CLI_CMD).with_arguments(
|
|
vec![
|
|
Argument::new("NAME", ArgumentType::Text)
|
|
.with_description("Name for the environment"),
|
|
Argument::new("DOCKER_IMAGE", ArgumentType::Text)
|
|
.with_description("Docker image to use for the environment"),
|
|
],
|
|
),
|
|
),
|
|
SetupGuideStep::CreateEnvironmentCli,
|
|
)),
|
|
CREATE_SLACK_INTEGRATION_CMD => Some((
|
|
WorkflowType::Local(
|
|
Workflow::new("Create Slack Integration", CREATE_SLACK_INTEGRATION_CMD)
|
|
.with_arguments(vec![Argument::new("environment_id", ArgumentType::Text)
|
|
.with_description("ID of the environment to integrate with")]),
|
|
),
|
|
SetupGuideStep::CreateSlackIntegration,
|
|
)),
|
|
CREATE_LINEAR_INTEGRATION_CMD => Some((
|
|
WorkflowType::Local(
|
|
Workflow::new("Create Linear Integration", CREATE_LINEAR_INTEGRATION_CMD)
|
|
.with_arguments(vec![Argument::new("environment_id", ArgumentType::Text)
|
|
.with_description("ID of the environment to integrate with")]),
|
|
),
|
|
SetupGuideStep::CreateLinearIntegration,
|
|
)),
|
|
_ => None,
|
|
}) else {
|
|
report_error!(anyhow::anyhow!(
|
|
"Received unknown code in render_code_block: {}",
|
|
code
|
|
));
|
|
return Empty::new().finish();
|
|
};
|
|
|
|
render_code_block_plain(
|
|
code,
|
|
highlights.into_iter(),
|
|
CodeBlockOptions {
|
|
on_open: None,
|
|
on_execute: Some(Box::new(move |_code, ctx| {
|
|
ctx.dispatch_typed_action(CloudSetupGuideAction::RunWorkflow {
|
|
workflow: Box::new(workflow.clone()),
|
|
step: setup_step,
|
|
});
|
|
})),
|
|
on_copy: Some(Box::new(move |_code, ctx| {
|
|
ctx.dispatch_typed_action(CloudSetupGuideAction::CopyCode {
|
|
code: code.to_string().clone(),
|
|
step: setup_step,
|
|
});
|
|
})),
|
|
on_insert: None,
|
|
footer_element: None,
|
|
mouse_handles: Some(handles),
|
|
file_path: None,
|
|
},
|
|
true,
|
|
app,
|
|
None,
|
|
)
|
|
}
|
|
|
|
/// Render step 1: Create an environment.
|
|
fn render_step_1(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
|
|
let theme = appearance.theme();
|
|
let step_title_font_size = 14.;
|
|
let step_desc_font_size = 14.;
|
|
|
|
let title_row = Flex::row()
|
|
.with_spacing(16.)
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.with_child(Self::render_step_number(1, appearance))
|
|
.with_child(
|
|
Text::new(
|
|
"Create an environment",
|
|
appearance.ui_font_family(),
|
|
step_title_font_size,
|
|
)
|
|
.with_style(Properties::default().weight(Weight::Semibold))
|
|
.with_color(theme.active_ui_text_color().into_solid())
|
|
.finish(),
|
|
)
|
|
.finish();
|
|
|
|
let description = Container::new(
|
|
Text::new(
|
|
"First, set up an environment to create an integration.",
|
|
appearance.ui_font_family(),
|
|
step_desc_font_size,
|
|
)
|
|
.with_color(theme.nonactive_ui_text_color().into_solid())
|
|
.finish(),
|
|
)
|
|
.with_padding_left(46.)
|
|
.finish();
|
|
|
|
let sub_description = Container::new(Self::render_description_with_link(
|
|
"Use Warp's environment setup command to have an agent help you through it. ",
|
|
"Visit docs",
|
|
self.env_docs_link_mouse_state.clone(),
|
|
SetupGuideDocs::Environment,
|
|
appearance,
|
|
))
|
|
.with_padding_left(46.)
|
|
.with_padding_bottom(8.)
|
|
.finish();
|
|
|
|
let slash_cmd_code_block = Container::new(self.render_code_block(
|
|
CREATE_ENV_SLASH_CMD,
|
|
self.create_env_code_handles.clone(),
|
|
app,
|
|
))
|
|
.with_padding_left(46.)
|
|
.finish();
|
|
|
|
let or_text = Container::new(
|
|
Text::new(
|
|
"Or, supply your own existing docker image.",
|
|
appearance.ui_font_family(),
|
|
step_desc_font_size,
|
|
)
|
|
.with_color(theme.nonactive_ui_text_color().into_solid())
|
|
.finish(),
|
|
)
|
|
.with_padding_left(46.)
|
|
.with_padding_top(8.)
|
|
.with_padding_bottom(8.)
|
|
.finish();
|
|
|
|
let cli_code_block = Container::new(self.render_code_block(
|
|
CREATE_ENV_CLI_CMD,
|
|
self.create_env_cli_code_handles.clone(),
|
|
app,
|
|
))
|
|
.with_padding_left(46.)
|
|
.finish();
|
|
|
|
Flex::column()
|
|
.with_spacing(8.)
|
|
.with_child(title_row)
|
|
.with_child(description)
|
|
.with_child(sub_description)
|
|
.with_child(slash_cmd_code_block)
|
|
.with_child(or_text)
|
|
.with_child(cli_code_block)
|
|
.finish()
|
|
}
|
|
|
|
/// Render step 2: Create an integration.
|
|
fn render_step_2(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
|
|
let theme = appearance.theme();
|
|
let step_title_font_size = 14.;
|
|
|
|
let title_row = Flex::row()
|
|
.with_spacing(16.)
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.with_child(Self::render_step_number(2, appearance))
|
|
.with_child(
|
|
Text::new(
|
|
"Create an integration",
|
|
appearance.ui_font_family(),
|
|
step_title_font_size,
|
|
)
|
|
.with_style(Properties::default().weight(Weight::Semibold))
|
|
.with_color(theme.active_ui_text_color().into_solid())
|
|
.finish(),
|
|
)
|
|
.finish();
|
|
|
|
let sub_description = Container::new(Self::render_description_with_link(
|
|
"Integrate Slack or Linear to assign Warp's Agent tasks with @Warp. ",
|
|
"Visit docs",
|
|
self.integration_docs_link_mouse_state.clone(),
|
|
SetupGuideDocs::Integration,
|
|
appearance,
|
|
))
|
|
.with_padding_left(46.)
|
|
.with_padding_bottom(8.)
|
|
.finish();
|
|
|
|
let code_block_slack = Container::new(self.render_code_block(
|
|
CREATE_SLACK_INTEGRATION_CMD,
|
|
self.create_slack_integration_code_handles.clone(),
|
|
app,
|
|
))
|
|
.with_padding_left(46.)
|
|
.finish();
|
|
|
|
let code_block_linear = Container::new(self.render_code_block(
|
|
CREATE_LINEAR_INTEGRATION_CMD,
|
|
self.create_linear_integration_code_handles.clone(),
|
|
app,
|
|
))
|
|
.with_padding_left(46.)
|
|
.finish();
|
|
|
|
Flex::column()
|
|
.with_spacing(8.)
|
|
.with_child(title_row)
|
|
.with_child(sub_description)
|
|
.with_child(code_block_slack)
|
|
.with_child(code_block_linear)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl Entity for CloudSetupGuideView {
|
|
type Event = CloudSetupGuideEvent;
|
|
}
|
|
|
|
impl View for CloudSetupGuideView {
|
|
fn ui_name() -> &'static str {
|
|
"AgentManagementHelpPageView"
|
|
}
|
|
|
|
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
|
let appearance = Appearance::as_ref(app);
|
|
let theme = appearance.theme();
|
|
|
|
let steps = Flex::column()
|
|
.with_spacing(24.)
|
|
.with_child(self.render_step_1(appearance, app))
|
|
.with_child(self.render_step_2(appearance, app))
|
|
.finish();
|
|
|
|
let mut content = Flex::column()
|
|
.with_spacing(24.)
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Stretch);
|
|
|
|
content.add_child(self.render_header(appearance));
|
|
content.add_child(self.render_quick_start_banner(appearance));
|
|
content.add_child(self.render_manual_setup_header(appearance));
|
|
content.add_child(steps);
|
|
|
|
let content = content.finish();
|
|
|
|
let scrollable = NewScrollable::horizontal_and_vertical(
|
|
DualAxisConfig::Clipped {
|
|
horizontal: ClippedAxisConfiguration {
|
|
handle: self.horizontal_scroll_state.clone(),
|
|
max_size: None,
|
|
stretch_child: true,
|
|
},
|
|
vertical: ClippedAxisConfiguration {
|
|
handle: self.vertical_scroll_state.clone(),
|
|
max_size: None,
|
|
stretch_child: false,
|
|
},
|
|
child: Align::new(
|
|
Container::new(
|
|
ConstrainedBox::new(content)
|
|
.with_max_width(CONTENT_MAX_WIDTH)
|
|
.finish(),
|
|
)
|
|
.with_uniform_padding(24.)
|
|
.finish(),
|
|
)
|
|
.top_center()
|
|
.finish(),
|
|
},
|
|
theme.nonactive_ui_detail().into(),
|
|
theme.active_ui_detail().into(),
|
|
galaxyui::elements::Fill::None,
|
|
)
|
|
.finish();
|
|
|
|
Flex::column()
|
|
.with_main_axis_size(MainAxisSize::Max)
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
|
.with_child(Expanded::new(1., scrollable).finish())
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl TypedActionView for CloudSetupGuideView {
|
|
type Action = CloudSetupGuideAction;
|
|
|
|
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
|
match action {
|
|
CloudSetupGuideAction::CopyCode { code, step } => {
|
|
send_telemetry_from_ctx!(
|
|
AgentManagementTelemetryEvent::SetupGuideStepCopy { step: *step },
|
|
ctx
|
|
);
|
|
ctx.clipboard()
|
|
.write(ClipboardContent::plain_text(code.clone()));
|
|
}
|
|
CloudSetupGuideAction::RunWorkflow { workflow, step } => {
|
|
send_telemetry_from_ctx!(
|
|
AgentManagementTelemetryEvent::SetupGuideStepRun { step: *step },
|
|
ctx
|
|
);
|
|
ctx.emit(CloudSetupGuideEvent::OpenNewTabAndInsertWorkflow(
|
|
(**workflow).clone(),
|
|
));
|
|
}
|
|
CloudSetupGuideAction::VisitOz => {
|
|
ctx.open_url(OZ_URL);
|
|
send_telemetry_from_ctx!(
|
|
AgentManagementTelemetryEvent::SetupGuideStepRun {
|
|
step: SetupGuideStep::VisitOz
|
|
},
|
|
ctx
|
|
);
|
|
}
|
|
CloudSetupGuideAction::OpenDocs { docs } => {
|
|
let url = match docs {
|
|
SetupGuideDocs::Main => DOCS_URL,
|
|
SetupGuideDocs::Environment => ENV_DOCS_URL,
|
|
SetupGuideDocs::Integration => DOCS_URL,
|
|
};
|
|
ctx.open_url(url);
|
|
send_telemetry_from_ctx!(
|
|
AgentManagementTelemetryEvent::SetupGuideDocsLink { docs: *docs },
|
|
ctx
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Highlight the commands in command blocks correctly (including the command prefix).
|
|
/// It's unfortunate that we have to do this manually, but the alternative is inserting a custom code editor into this component
|
|
/// and that would be a lot of bloat for not much benefit.
|
|
fn tokens_to_highlight_ranges(
|
|
parsed_tokens: &ParsedTokensSnapshot,
|
|
terminal_colors: &AnsiColors,
|
|
) -> Vec<HighlightedRange> {
|
|
let code = &parsed_tokens.buffer_text;
|
|
let mut highlights = Vec::new();
|
|
|
|
// Handle slash commands: if code starts with '/', highlight the command prefix in magenta
|
|
if code.starts_with('/') {
|
|
if let Some(space_idx) = code.find(' ') {
|
|
let color = AnsiColorIdentifier::Magenta.to_ansi_color(terminal_colors);
|
|
highlights.push(HighlightedRange {
|
|
highlight: Highlight::new()
|
|
.with_text_style(TextStyle::new().with_foreground_color(color.into())),
|
|
highlight_indices: (0..space_idx).collect(),
|
|
});
|
|
return highlights;
|
|
}
|
|
}
|
|
|
|
// Highlight commands in the code block (converting bytes to char indexes as we go).
|
|
let mut char_counter = CharCounter::new(code);
|
|
for token_data in &parsed_tokens.parsed_tokens {
|
|
let Some(description) = &token_data.token_description else {
|
|
continue;
|
|
};
|
|
|
|
let byte_start = token_data.token.span.start();
|
|
let byte_end = token_data.token.span.end();
|
|
|
|
let Some(char_start) = char_counter.char_offset(byte_start) else {
|
|
continue;
|
|
};
|
|
let Some(char_end) = char_counter.char_offset(byte_end) else {
|
|
continue;
|
|
};
|
|
|
|
let char_indices: Vec<usize> = (char_start.as_usize()..char_end.as_usize()).collect();
|
|
if char_indices.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let color_id: AnsiColorIdentifier = description.suggestion_type.to_name().into();
|
|
let color = color_id.to_ansi_color(terminal_colors);
|
|
|
|
highlights.push(HighlightedRange {
|
|
highlight: Highlight::new()
|
|
.with_text_style(TextStyle::new().with_foreground_color(color.into())),
|
|
highlight_indices: char_indices,
|
|
});
|
|
}
|
|
|
|
highlights
|
|
}
|