Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
mod model;
|
||||
mod view;
|
||||
|
||||
pub use model::{FinalState, OnboardingQuery};
|
||||
pub use view::{OnboardingCalloutView, OnboardingCalloutViewEvent, OnboardingKeybindings};
|
||||
|
||||
pub fn init(app: &mut warpui::AppContext) {
|
||||
view::init(app);
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
use crate::telemetry::OnboardingEvent;
|
||||
use crate::OnboardingIntention;
|
||||
use warp_core::send_telemetry_from_ctx;
|
||||
use warpui::{Entity, ModelContext};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FinalState {
|
||||
/// User submitted the agent query (legacy flow)
|
||||
Submit,
|
||||
/// User skipped the callout (legacy flow or skip initialization)
|
||||
Skip,
|
||||
/// User finished the callout without submitting
|
||||
Finish,
|
||||
/// User chose to initialize the project (AgentModality with project)
|
||||
Initialize,
|
||||
/// User chose to go back to terminal (AgentModality without project)
|
||||
BackToTerminal,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FinalState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
FinalState::Submit => write!(f, "submitted"),
|
||||
FinalState::Skip => write!(f, "skipped"),
|
||||
FinalState::Finish => write!(f, "finished"),
|
||||
FinalState::Initialize => write!(f, "initialize"),
|
||||
FinalState::BackToTerminal => write!(f, "back_to_terminal"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompt information for the onboarding callout
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum OnboardingQuery {
|
||||
/// A terminal command that should be executed in shell mode
|
||||
TerminalCommand(String),
|
||||
/// An agent prompt that should be executed in agent mode
|
||||
AgentPrompt(String),
|
||||
/// No prompt (empty state)
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(super) enum OnboardingCalloutModelEvent {
|
||||
StateUpdated,
|
||||
Completed(FinalState),
|
||||
EnterAgentModality,
|
||||
/// Emitted when the user toggles the natural language detection checkbox.
|
||||
NaturalLanguageDetectionToggled(bool),
|
||||
}
|
||||
|
||||
/// State for the UniversalInput onboarding flow (non-AgentModality).
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum UniversalInputCalloutState {
|
||||
#[default]
|
||||
Off,
|
||||
MeetInput,
|
||||
TalkToAgent,
|
||||
Complete(FinalState),
|
||||
}
|
||||
|
||||
/// State for the AgentModality onboarding flow.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum AgentModalityCalloutState {
|
||||
#[default]
|
||||
Off,
|
||||
/// Step 1: "Meet your terminal input" / "Meet your updated terminal input"
|
||||
MeetTerminalInput,
|
||||
/// Step 2: "Natural language support" with checkbox
|
||||
NaturalLanguageSupport,
|
||||
/// Step 3: "Introducing Warp's new agent experience" (Agent intention only)
|
||||
IntroducingAgentExperience,
|
||||
/// Step 4: "Updated agent input" (Agent intention only)
|
||||
UpdatedAgentInput,
|
||||
/// Terminal state
|
||||
Complete(FinalState),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum OnboardingCalloutState {
|
||||
UniversalInput(UniversalInputCalloutState),
|
||||
AgentModality(AgentModalityCalloutState),
|
||||
}
|
||||
|
||||
pub(super) struct OnboardingCalloutModel {
|
||||
state: OnboardingCalloutState,
|
||||
intention: OnboardingIntention,
|
||||
has_project: bool,
|
||||
/// The initial value of natural language detection when onboarding started.
|
||||
/// Used to determine which callout variant to show.
|
||||
initial_natural_language_detection_enabled: bool,
|
||||
/// The current value of natural language detection (may change via checkbox toggle).
|
||||
natural_language_detection_enabled: bool,
|
||||
}
|
||||
|
||||
impl OnboardingCalloutModel {
|
||||
/// Create a new model for UniversalInput onboarding flow.
|
||||
pub fn new_universal_input(
|
||||
has_project: bool,
|
||||
initial_natural_language_detection_enabled: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
state: OnboardingCalloutState::UniversalInput(UniversalInputCalloutState::default()),
|
||||
intention: OnboardingIntention::AgentDrivenDevelopment,
|
||||
has_project,
|
||||
initial_natural_language_detection_enabled,
|
||||
natural_language_detection_enabled: initial_natural_language_detection_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new model for AgentModality onboarding flow.
|
||||
pub fn new_agent_modality(
|
||||
has_project: bool,
|
||||
intention: OnboardingIntention,
|
||||
initial_natural_language_detection_enabled: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
state: OnboardingCalloutState::AgentModality(AgentModalityCalloutState::default()),
|
||||
intention,
|
||||
has_project,
|
||||
initial_natural_language_detection_enabled,
|
||||
natural_language_detection_enabled: initial_natural_language_detection_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_project(&self) -> bool {
|
||||
self.has_project
|
||||
}
|
||||
|
||||
pub fn intention(&self) -> OnboardingIntention {
|
||||
self.intention
|
||||
}
|
||||
|
||||
pub fn initial_natural_language_detection_enabled(&self) -> bool {
|
||||
self.initial_natural_language_detection_enabled
|
||||
}
|
||||
|
||||
pub fn natural_language_detection_enabled(&self) -> bool {
|
||||
self.natural_language_detection_enabled
|
||||
}
|
||||
|
||||
pub fn toggle_natural_language_detection(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
self.natural_language_detection_enabled = !self.natural_language_detection_enabled;
|
||||
ctx.emit(
|
||||
OnboardingCalloutModelEvent::NaturalLanguageDetectionToggled(
|
||||
self.natural_language_detection_enabled,
|
||||
),
|
||||
);
|
||||
ctx.emit(OnboardingCalloutModelEvent::StateUpdated);
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub fn next(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
send_telemetry_from_ctx!(OnboardingEvent::CalloutNext, ctx);
|
||||
match &self.state {
|
||||
OnboardingCalloutState::UniversalInput(universal_input_state) => {
|
||||
self.next_universal_input(*universal_input_state, ctx);
|
||||
}
|
||||
OnboardingCalloutState::AgentModality(modality_state) => {
|
||||
self.next_agent_modality(*modality_state, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_universal_input(
|
||||
&mut self,
|
||||
state: UniversalInputCalloutState,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let next_state = match state {
|
||||
UniversalInputCalloutState::Off => Some(UniversalInputCalloutState::MeetInput),
|
||||
UniversalInputCalloutState::MeetInput => Some(UniversalInputCalloutState::TalkToAgent),
|
||||
UniversalInputCalloutState::TalkToAgent => {
|
||||
Some(UniversalInputCalloutState::Complete(FinalState::Submit))
|
||||
}
|
||||
UniversalInputCalloutState::Complete(_) => None,
|
||||
};
|
||||
if let Some(next_state) = next_state {
|
||||
self.set_state(OnboardingCalloutState::UniversalInput(next_state), ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn next_agent_modality(
|
||||
&mut self,
|
||||
state: AgentModalityCalloutState,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let (next_state, emit_enter_agent_modality) = match state {
|
||||
AgentModalityCalloutState::Off => {
|
||||
(Some(AgentModalityCalloutState::MeetTerminalInput), false)
|
||||
}
|
||||
AgentModalityCalloutState::MeetTerminalInput => (
|
||||
Some(AgentModalityCalloutState::NaturalLanguageSupport),
|
||||
false,
|
||||
),
|
||||
AgentModalityCalloutState::NaturalLanguageSupport => {
|
||||
// For Terminal intention, finish here
|
||||
// For Agent intention, continue to IntroducingAgentExperience
|
||||
match self.intention {
|
||||
OnboardingIntention::Terminal => (
|
||||
Some(AgentModalityCalloutState::Complete(FinalState::Finish)),
|
||||
false,
|
||||
),
|
||||
OnboardingIntention::AgentDrivenDevelopment => {
|
||||
// Signal to enter agent modality when showing the agent experience slide
|
||||
(
|
||||
Some(AgentModalityCalloutState::IntroducingAgentExperience),
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
AgentModalityCalloutState::IntroducingAgentExperience => {
|
||||
(Some(AgentModalityCalloutState::UpdatedAgentInput), false)
|
||||
}
|
||||
AgentModalityCalloutState::UpdatedAgentInput => {
|
||||
// For Agent with project: Initialize
|
||||
// For Agent without project: Finish
|
||||
let final_state = if self.has_project {
|
||||
FinalState::Initialize
|
||||
} else {
|
||||
FinalState::Finish
|
||||
};
|
||||
(
|
||||
Some(AgentModalityCalloutState::Complete(final_state)),
|
||||
false,
|
||||
)
|
||||
}
|
||||
AgentModalityCalloutState::Complete(_) => (None, false),
|
||||
};
|
||||
if let Some(next_state) = next_state {
|
||||
self.set_state(OnboardingCalloutState::AgentModality(next_state), ctx);
|
||||
}
|
||||
if emit_enter_agent_modality {
|
||||
ctx.emit(OnboardingCalloutModelEvent::EnterAgentModality);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn skip(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
match &self.state {
|
||||
OnboardingCalloutState::UniversalInput(UniversalInputCalloutState::TalkToAgent) => {
|
||||
self.set_state(
|
||||
OnboardingCalloutState::UniversalInput(UniversalInputCalloutState::Complete(
|
||||
FinalState::Skip,
|
||||
)),
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::UpdatedAgentInput) => {
|
||||
// Skip initialization
|
||||
self.set_state(
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::Complete(
|
||||
FinalState::Skip,
|
||||
)),
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
_ => log::error!(
|
||||
"Skip action called in an unskippable state: {:?}",
|
||||
self.state
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
match &self.state {
|
||||
OnboardingCalloutState::UniversalInput(UniversalInputCalloutState::TalkToAgent) => {
|
||||
self.set_state(
|
||||
OnboardingCalloutState::UniversalInput(UniversalInputCalloutState::Complete(
|
||||
FinalState::Finish,
|
||||
)),
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
OnboardingCalloutState::AgentModality(
|
||||
AgentModalityCalloutState::NaturalLanguageSupport,
|
||||
) => {
|
||||
// Terminal intention finishes here
|
||||
self.set_state(
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::Complete(
|
||||
FinalState::Finish,
|
||||
)),
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::UpdatedAgentInput) => {
|
||||
// Agent without project finishes here
|
||||
self.set_state(
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::Complete(
|
||||
FinalState::Finish,
|
||||
)),
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
_ => log::error!("Finish action called in an invalid state: {:?}", self.state),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle "Back to terminal" action (ESC in UpdatedAgentInput without project)
|
||||
pub fn back_to_terminal(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
match &self.state {
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::UpdatedAgentInput) => {
|
||||
self.set_state(
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::Complete(
|
||||
FinalState::BackToTerminal,
|
||||
)),
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
_ => log::error!(
|
||||
"BackToTerminal action called in an invalid state: {:?}",
|
||||
self.state
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_onboarding_active(&self) -> bool {
|
||||
match &self.state {
|
||||
OnboardingCalloutState::UniversalInput(state) => !matches!(
|
||||
state,
|
||||
UniversalInputCalloutState::Off | UniversalInputCalloutState::Complete(_)
|
||||
),
|
||||
OnboardingCalloutState::AgentModality(state) => !matches!(
|
||||
state,
|
||||
AgentModalityCalloutState::Off | AgentModalityCalloutState::Complete(_)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&self) -> OnboardingCalloutState {
|
||||
self.state
|
||||
}
|
||||
|
||||
fn send_callout_displayed_telemetry(
|
||||
new_state: OnboardingCalloutState,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let callout_name = match new_state {
|
||||
OnboardingCalloutState::UniversalInput(UniversalInputCalloutState::MeetInput) => {
|
||||
Some("meet_input")
|
||||
}
|
||||
OnboardingCalloutState::UniversalInput(UniversalInputCalloutState::TalkToAgent) => {
|
||||
Some("talk_to_agent")
|
||||
}
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::MeetTerminalInput) => {
|
||||
Some("meet_terminal_input")
|
||||
}
|
||||
OnboardingCalloutState::AgentModality(
|
||||
AgentModalityCalloutState::NaturalLanguageSupport,
|
||||
) => Some("natural_language_support"),
|
||||
OnboardingCalloutState::AgentModality(
|
||||
AgentModalityCalloutState::IntroducingAgentExperience,
|
||||
) => Some("introducing_agent_experience"),
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::UpdatedAgentInput) => {
|
||||
Some("updated_agent_input")
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(callout) = callout_name {
|
||||
send_telemetry_from_ctx!(
|
||||
OnboardingEvent::CalloutDisplayed {
|
||||
callout: callout.to_string(),
|
||||
},
|
||||
ctx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_state(&mut self, new_state: OnboardingCalloutState, ctx: &mut ModelContext<Self>) {
|
||||
if self.state != new_state {
|
||||
self.state = new_state;
|
||||
Self::send_callout_displayed_telemetry(new_state, ctx);
|
||||
ctx.emit(OnboardingCalloutModelEvent::StateUpdated);
|
||||
|
||||
// Check for completion
|
||||
let final_state = match new_state {
|
||||
OnboardingCalloutState::UniversalInput(UniversalInputCalloutState::Complete(
|
||||
fs,
|
||||
)) => Some(fs),
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::Complete(fs)) => {
|
||||
Some(fs)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(final_state) = final_state {
|
||||
send_telemetry_from_ctx!(
|
||||
OnboardingEvent::CalloutCompleted {
|
||||
completion_type: final_state.to_string(),
|
||||
},
|
||||
ctx
|
||||
);
|
||||
ctx.emit(OnboardingCalloutModelEvent::Completed(final_state));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a prompt string to populate a command based on current state
|
||||
pub fn prompt_string(&self) -> String {
|
||||
match self.prompt() {
|
||||
OnboardingQuery::TerminalCommand(text) | OnboardingQuery::AgentPrompt(text) => text,
|
||||
OnboardingQuery::None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the prompt information including type for the current state
|
||||
pub fn prompt(&self) -> OnboardingQuery {
|
||||
match &self.state {
|
||||
OnboardingCalloutState::UniversalInput(state) => {
|
||||
self.prompt_for_universal_input(*state)
|
||||
}
|
||||
OnboardingCalloutState::AgentModality(state) => self.prompt_for_agent_modality(*state),
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_for_universal_input(&self, state: UniversalInputCalloutState) -> OnboardingQuery {
|
||||
match state {
|
||||
UniversalInputCalloutState::Off
|
||||
| UniversalInputCalloutState::Complete(FinalState::Skip)
|
||||
| UniversalInputCalloutState::Complete(FinalState::Finish) => OnboardingQuery::None,
|
||||
UniversalInputCalloutState::MeetInput => {
|
||||
OnboardingQuery::TerminalCommand("git status".to_string())
|
||||
}
|
||||
UniversalInputCalloutState::TalkToAgent
|
||||
| UniversalInputCalloutState::Complete(FinalState::Submit) => {
|
||||
OnboardingQuery::AgentPrompt(
|
||||
"What tests exist in this repo, how are they structured, and what do they cover?"
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
UniversalInputCalloutState::Complete(_) => OnboardingQuery::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_for_agent_modality(&self, state: AgentModalityCalloutState) -> OnboardingQuery {
|
||||
match state {
|
||||
AgentModalityCalloutState::Off => OnboardingQuery::None,
|
||||
AgentModalityCalloutState::MeetTerminalInput => {
|
||||
OnboardingQuery::TerminalCommand("Run a command...".to_string())
|
||||
}
|
||||
AgentModalityCalloutState::NaturalLanguageSupport => {
|
||||
OnboardingQuery::AgentPrompt("help me terraform my Gcloud setup".to_string())
|
||||
}
|
||||
AgentModalityCalloutState::IntroducingAgentExperience => {
|
||||
OnboardingQuery::AgentPrompt("Tell the agent what to build...".to_string())
|
||||
}
|
||||
AgentModalityCalloutState::UpdatedAgentInput => {
|
||||
if self.has_project {
|
||||
OnboardingQuery::AgentPrompt("/init".to_string())
|
||||
} else {
|
||||
OnboardingQuery::AgentPrompt("Tell the agent what to build...".to_string())
|
||||
}
|
||||
}
|
||||
// All completion states should return None so the input gets cleared
|
||||
AgentModalityCalloutState::Complete(_) => OnboardingQuery::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_onboarding(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
log::info!(
|
||||
"start_onboarding called with current state: {:?}",
|
||||
self.state
|
||||
);
|
||||
match &self.state {
|
||||
OnboardingCalloutState::UniversalInput(_) => {
|
||||
log::info!("Transitioning to UniversalInput::MeetInput");
|
||||
self.set_state(
|
||||
OnboardingCalloutState::UniversalInput(UniversalInputCalloutState::MeetInput),
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
OnboardingCalloutState::AgentModality(_) => {
|
||||
log::info!("Transitioning to AgentModality::MeetTerminalInput");
|
||||
self.set_state(
|
||||
OnboardingCalloutState::AgentModality(
|
||||
AgentModalityCalloutState::MeetTerminalInput,
|
||||
),
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for OnboardingCalloutModel {
|
||||
type Event = OnboardingCalloutModelEvent;
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
use ui_components::Component;
|
||||
use warp_core::ui::appearance::Appearance;
|
||||
use warpui::{
|
||||
elements::Empty,
|
||||
keymap::{macros::*, FixedBinding, Keystroke},
|
||||
AppContext, Element, Entity, EventContext, ModelHandle, SingletonEntity, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
/// Display strings for keybindings shown in the onboarding callout.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OnboardingKeybindings {
|
||||
/// Display string for toggling between agent/terminal mode (e.g., "⌘I")
|
||||
pub toggle_input_mode: String,
|
||||
/// Display string for submitting to local agent (e.g., "⌘⏎")
|
||||
pub submit_to_local_agent: String,
|
||||
/// Display string for submitting to cloud agent (e.g., "⌘⌥⏎")
|
||||
pub submit_to_cloud_agent: String,
|
||||
}
|
||||
|
||||
use crate::{
|
||||
callout::model::{
|
||||
AgentModalityCalloutState, FinalState, OnboardingCalloutModel, OnboardingCalloutModelEvent,
|
||||
OnboardingCalloutState, OnboardingQuery, UniversalInputCalloutState,
|
||||
},
|
||||
components::onboarding_callout::{self, Button, StepStatus},
|
||||
OnboardingIntention,
|
||||
};
|
||||
|
||||
/// Options for rendering a callout.
|
||||
struct CalloutOptions {
|
||||
title: &'static str,
|
||||
/// Pre-built text with keybindings already embedded
|
||||
text: String,
|
||||
step: StepStatus,
|
||||
right_button: ButtonOptions,
|
||||
/// Optional left button (e.g., "Skip", "Back to terminal")
|
||||
left_button: Option<ButtonOptions>,
|
||||
/// Optional checkbox for natural language detection
|
||||
checkbox: Option<CheckboxOptions>,
|
||||
}
|
||||
|
||||
struct ButtonOptions {
|
||||
text: &'static str,
|
||||
action: OnboardingCalloutViewAction,
|
||||
keystroke: Option<Keystroke>,
|
||||
}
|
||||
|
||||
struct CheckboxOptions {
|
||||
label: &'static str,
|
||||
checked: bool,
|
||||
}
|
||||
|
||||
fn get_universal_input_callout_options(
|
||||
state: UniversalInputCalloutState,
|
||||
has_project: bool,
|
||||
keybindings: &OnboardingKeybindings,
|
||||
) -> Option<CalloutOptions> {
|
||||
match state {
|
||||
UniversalInputCalloutState::MeetInput => Some(CalloutOptions {
|
||||
title: "Meet the Warp input",
|
||||
text: format!(
|
||||
"Your terminal input accepts both terminal commands and agent prompts and automatically detects which you're using. Use {} to lock the input to Agent mode (natural language) or Terminal mode (commands).",
|
||||
keybindings.toggle_input_mode
|
||||
),
|
||||
step: StepStatus::new(0, 2),
|
||||
left_button: None,
|
||||
right_button: ButtonOptions {
|
||||
text: "Next",
|
||||
action: OnboardingCalloutViewAction::NextClicked,
|
||||
keystroke: Some(Keystroke::parse("enter").unwrap_or_default()),
|
||||
},
|
||||
checkbox: None,
|
||||
}),
|
||||
UniversalInputCalloutState::TalkToAgent => Some(CalloutOptions {
|
||||
title: "Talk to the agent",
|
||||
text: "You can type in natural language to engage the agent. Submit the query below to start: What tests exist in this repo, how are they structured, and what do they cover?".to_string(),
|
||||
step: StepStatus::new(1, 2),
|
||||
left_button: if has_project {
|
||||
Some(ButtonOptions {
|
||||
text: "Skip",
|
||||
action: OnboardingCalloutViewAction::SkipClicked,
|
||||
keystroke: Some(Keystroke::parse("delete").unwrap_or_default()),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
right_button: ButtonOptions {
|
||||
text: if has_project { "Submit" } else { "Finish" },
|
||||
action: OnboardingCalloutViewAction::NextClicked,
|
||||
keystroke: Some(Keystroke::parse("enter").unwrap_or_default()),
|
||||
},
|
||||
checkbox: None,
|
||||
}),
|
||||
UniversalInputCalloutState::Off | UniversalInputCalloutState::Complete(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_agent_modality_callout_options(
|
||||
state: AgentModalityCalloutState,
|
||||
intention: OnboardingIntention,
|
||||
has_project: bool,
|
||||
initial_natural_language_detection_enabled: bool,
|
||||
natural_language_detection_enabled: bool,
|
||||
keybindings: &OnboardingKeybindings,
|
||||
) -> Option<CalloutOptions> {
|
||||
let total_steps = match intention {
|
||||
OnboardingIntention::Terminal => 2,
|
||||
OnboardingIntention::AgentDrivenDevelopment => 4,
|
||||
};
|
||||
|
||||
match state {
|
||||
AgentModalityCalloutState::MeetTerminalInput => {
|
||||
let title: &'static str = if has_project || intention == OnboardingIntention::Terminal {
|
||||
"Meet your terminal input"
|
||||
} else {
|
||||
"Meet your updated terminal input"
|
||||
};
|
||||
Some(CalloutOptions {
|
||||
title,
|
||||
text: format!(
|
||||
"Run commands from the terminal, or use {} or {} to start or send to a local or cloud agent respectively.",
|
||||
keybindings.submit_to_local_agent,
|
||||
keybindings.submit_to_cloud_agent
|
||||
),
|
||||
step: StepStatus::new(0, total_steps),
|
||||
left_button: None,
|
||||
right_button: ButtonOptions {
|
||||
text: "Next",
|
||||
action: OnboardingCalloutViewAction::NextClicked,
|
||||
keystroke: Some(Keystroke::parse("enter").unwrap_or_default()),
|
||||
},
|
||||
checkbox: None,
|
||||
})
|
||||
}
|
||||
AgentModalityCalloutState::NaturalLanguageSupport => {
|
||||
let is_final_step = intention == OnboardingIntention::Terminal;
|
||||
// Show different callout content based on initial NL detection state
|
||||
if initial_natural_language_detection_enabled {
|
||||
// NL detection was already enabled - show simpler "overrides" callout without checkbox
|
||||
Some(CalloutOptions {
|
||||
title: "Natural language overrides",
|
||||
text: format!(
|
||||
"You can always override any auto-detection using {}.",
|
||||
keybindings.toggle_input_mode
|
||||
),
|
||||
step: StepStatus::new(1, total_steps),
|
||||
left_button: None,
|
||||
right_button: ButtonOptions {
|
||||
text: if is_final_step { "Finish" } else { "Next" },
|
||||
action: OnboardingCalloutViewAction::NextClicked,
|
||||
keystroke: Some(Keystroke::parse("enter").unwrap_or_default()),
|
||||
},
|
||||
checkbox: None,
|
||||
})
|
||||
} else {
|
||||
// NL detection was disabled - show full explanation with checkbox to enable
|
||||
Some(CalloutOptions {
|
||||
title: "Natural language support",
|
||||
text: format!(
|
||||
"Natural language input is off by default. If enabled, you can type requests in plain English and Warp will autodetect queries for the agent. You can always override them using {}.",
|
||||
keybindings.toggle_input_mode
|
||||
),
|
||||
step: StepStatus::new(1, total_steps),
|
||||
left_button: None,
|
||||
right_button: ButtonOptions {
|
||||
text: if is_final_step { "Finish" } else { "Next" },
|
||||
action: OnboardingCalloutViewAction::NextClicked,
|
||||
keystroke: Some(Keystroke::parse("enter").unwrap_or_default()),
|
||||
},
|
||||
checkbox: Some(CheckboxOptions {
|
||||
label: "Enable Natural Language Detection",
|
||||
checked: natural_language_detection_enabled,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
AgentModalityCalloutState::IntroducingAgentExperience => Some(CalloutOptions {
|
||||
title: "Introducing Warp's new agent experience",
|
||||
text: "Agent conversations are now their own scoped view outside of your terminal. Simply hit ESC to return to the terminal at any point.".to_string(),
|
||||
step: StepStatus::new(2, total_steps),
|
||||
left_button: None,
|
||||
right_button: ButtonOptions {
|
||||
text: "Next",
|
||||
action: OnboardingCalloutViewAction::NextClicked,
|
||||
keystroke: Some(Keystroke::parse("enter").unwrap_or_default()),
|
||||
},
|
||||
checkbox: None,
|
||||
}),
|
||||
AgentModalityCalloutState::UpdatedAgentInput => {
|
||||
if has_project {
|
||||
Some(CalloutOptions {
|
||||
title: "Updated agent input",
|
||||
text: "Your agent input will detect natural language as well as commands by default. Use ! to lock the input in bash mode to write commands.\n\nSubmit the query below to have the agent initialize this project, or ⊗ to clear the input and start your own!".to_string(),
|
||||
step: StepStatus::new(3, total_steps),
|
||||
left_button: Some(ButtonOptions {
|
||||
text: "Skip initialization",
|
||||
action: OnboardingCalloutViewAction::SkipClicked,
|
||||
keystroke: Some(Keystroke::parse("delete").unwrap_or_default()),
|
||||
}),
|
||||
right_button: ButtonOptions {
|
||||
text: "Initialize",
|
||||
action: OnboardingCalloutViewAction::NextClicked,
|
||||
keystroke: Some(Keystroke::parse("enter").unwrap_or_default()),
|
||||
},
|
||||
checkbox: None,
|
||||
})
|
||||
} else {
|
||||
Some(CalloutOptions {
|
||||
title: "Updated agent input",
|
||||
text: "Your agent input will detect natural language as well as commands by default. Use ! to lock the input in bash mode to write commands.".to_string(),
|
||||
step: StepStatus::new(3, total_steps),
|
||||
left_button: Some(ButtonOptions {
|
||||
text: "Back to terminal",
|
||||
action: OnboardingCalloutViewAction::BackToTerminalClicked,
|
||||
keystroke: Some(Keystroke::parse("escape").unwrap_or_default()),
|
||||
}),
|
||||
right_button: ButtonOptions {
|
||||
text: "Finish",
|
||||
action: OnboardingCalloutViewAction::NextClicked,
|
||||
keystroke: Some(Keystroke::parse("enter").unwrap_or_default()),
|
||||
},
|
||||
checkbox: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
AgentModalityCalloutState::Off | AgentModalityCalloutState::Complete(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum OnboardingCalloutViewAction {
|
||||
NextClicked,
|
||||
SkipClicked,
|
||||
BackToTerminalClicked,
|
||||
ToggleCheckbox,
|
||||
}
|
||||
|
||||
pub fn init(app: &mut AppContext) {
|
||||
app.register_fixed_bindings([
|
||||
FixedBinding::new(
|
||||
"enter",
|
||||
OnboardingCalloutViewAction::NextClicked,
|
||||
id!(OnboardingCalloutView::ui_name()),
|
||||
),
|
||||
FixedBinding::new(
|
||||
"numpadenter",
|
||||
OnboardingCalloutViewAction::NextClicked,
|
||||
id!(OnboardingCalloutView::ui_name()),
|
||||
),
|
||||
FixedBinding::new(
|
||||
"backspace",
|
||||
OnboardingCalloutViewAction::SkipClicked,
|
||||
id!(OnboardingCalloutView::ui_name()),
|
||||
),
|
||||
FixedBinding::new(
|
||||
"escape",
|
||||
OnboardingCalloutViewAction::BackToTerminalClicked,
|
||||
id!(OnboardingCalloutView::ui_name()),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum OnboardingCalloutViewEvent {
|
||||
StateUpdated,
|
||||
Completed {
|
||||
final_state: FinalState,
|
||||
},
|
||||
/// Signals that the terminal should enter agent modality (agent view).
|
||||
EnterAgentModality,
|
||||
/// Emitted when the user toggles the natural language detection checkbox.
|
||||
NaturalLanguageDetectionToggled(bool),
|
||||
}
|
||||
|
||||
/// A view that renders the onboarding callout UI component based on the current model state
|
||||
pub struct OnboardingCalloutView {
|
||||
/// Reference to the model that manages onboarding state
|
||||
model: ModelHandle<OnboardingCalloutModel>,
|
||||
/// The UI component that renders the actual callout
|
||||
callout_component: onboarding_callout::OnboardingCallout,
|
||||
/// Display strings for keybindings shown in the callout
|
||||
keybindings: OnboardingKeybindings,
|
||||
}
|
||||
|
||||
impl OnboardingCalloutView {
|
||||
/// Create a new view for the UniversalInput onboarding flow.
|
||||
pub fn new_universal_input(
|
||||
has_project: bool,
|
||||
initial_natural_language_detection_enabled: bool,
|
||||
keybindings: OnboardingKeybindings,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let model = ctx.add_model(|_ctx| {
|
||||
OnboardingCalloutModel::new_universal_input(
|
||||
has_project,
|
||||
initial_natural_language_detection_enabled,
|
||||
)
|
||||
});
|
||||
Self::with_model(model, keybindings, ctx)
|
||||
}
|
||||
|
||||
/// Create a new view for the AgentModality onboarding flow.
|
||||
pub fn new_agent_modality(
|
||||
has_project: bool,
|
||||
intention: OnboardingIntention,
|
||||
initial_natural_language_detection_enabled: bool,
|
||||
keybindings: OnboardingKeybindings,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let model = ctx.add_model(|_ctx| {
|
||||
OnboardingCalloutModel::new_agent_modality(
|
||||
has_project,
|
||||
intention,
|
||||
initial_natural_language_detection_enabled,
|
||||
)
|
||||
});
|
||||
Self::with_model(model, keybindings, ctx)
|
||||
}
|
||||
|
||||
fn with_model(
|
||||
model: ModelHandle<OnboardingCalloutModel>,
|
||||
keybindings: OnboardingKeybindings,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
// Re-emit model updates as view events so parents can subscribe to the view.
|
||||
ctx.subscribe_to_model(&model, |_me, _model, event, ctx| match event {
|
||||
OnboardingCalloutModelEvent::StateUpdated => {
|
||||
ctx.emit(OnboardingCalloutViewEvent::StateUpdated);
|
||||
ctx.notify();
|
||||
}
|
||||
OnboardingCalloutModelEvent::Completed(final_state) => {
|
||||
ctx.emit(OnboardingCalloutViewEvent::Completed {
|
||||
final_state: *final_state,
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
OnboardingCalloutModelEvent::EnterAgentModality => {
|
||||
ctx.emit(OnboardingCalloutViewEvent::EnterAgentModality);
|
||||
ctx.notify();
|
||||
}
|
||||
OnboardingCalloutModelEvent::NaturalLanguageDetectionToggled(enabled) => {
|
||||
ctx.emit(OnboardingCalloutViewEvent::NaturalLanguageDetectionToggled(
|
||||
*enabled,
|
||||
));
|
||||
ctx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
model,
|
||||
callout_component: onboarding_callout::OnboardingCallout::default(),
|
||||
keybindings,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_project(&self, app: &AppContext) -> bool {
|
||||
self.model.as_ref(app).has_project()
|
||||
}
|
||||
|
||||
pub fn start_onboarding(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.model.update(ctx, |model, ctx| {
|
||||
model.start_onboarding(ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub fn is_onboarding_active(&self, app: &AppContext) -> bool {
|
||||
self.model.as_ref(app).is_onboarding_active()
|
||||
}
|
||||
|
||||
pub fn prompt_string(&self, app: &AppContext) -> String {
|
||||
self.model.as_ref(app).prompt_string()
|
||||
}
|
||||
|
||||
pub fn prompt(&self, app: &AppContext) -> OnboardingQuery {
|
||||
self.model.as_ref(app).prompt()
|
||||
}
|
||||
|
||||
/// Returns true if the callout should be positioned above the zero state.
|
||||
/// For UpdatedAgentInput state, always position relative to the input box instead.
|
||||
pub fn should_position_above_zero_state(&self, app: &AppContext) -> bool {
|
||||
!matches!(
|
||||
self.model.as_ref(app).state(),
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::UpdatedAgentInput)
|
||||
)
|
||||
}
|
||||
|
||||
fn get_callout_options(&self, app: &AppContext) -> Option<CalloutOptions> {
|
||||
let model = self.model.as_ref(app);
|
||||
match model.state() {
|
||||
OnboardingCalloutState::UniversalInput(state) => {
|
||||
get_universal_input_callout_options(state, model.has_project(), &self.keybindings)
|
||||
}
|
||||
OnboardingCalloutState::AgentModality(state) => get_agent_modality_callout_options(
|
||||
state,
|
||||
model.intention(),
|
||||
model.has_project(),
|
||||
model.initial_natural_language_detection_enabled(),
|
||||
model.natural_language_detection_enabled(),
|
||||
&self.keybindings,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for OnboardingCalloutView {
|
||||
type Event = OnboardingCalloutViewEvent;
|
||||
}
|
||||
|
||||
impl View for OnboardingCalloutView {
|
||||
fn ui_name() -> &'static str {
|
||||
"OnboardingCalloutView"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let model = self.model.as_ref(app);
|
||||
|
||||
// Check if onboarding is active and render appropriate callout based on state
|
||||
if !model.is_onboarding_active() {
|
||||
return Empty::new().finish();
|
||||
}
|
||||
|
||||
let Some(options) = self.get_callout_options(app) else {
|
||||
log::warn!(
|
||||
"Onboarding callout view: onboarding is active but state has no callout options: {:?}",
|
||||
model.state()
|
||||
);
|
||||
return Empty::new().finish();
|
||||
};
|
||||
|
||||
let right_button = Button {
|
||||
text: options.right_button.text.into(),
|
||||
keystroke: options.right_button.keystroke,
|
||||
handler: Box::new(move |ctx: &mut EventContext, _app_ctx: &AppContext, _pos| {
|
||||
ctx.dispatch_typed_action(options.right_button.action.clone());
|
||||
}),
|
||||
};
|
||||
|
||||
let left_button = options.left_button.map(|left_opts| Button {
|
||||
text: left_opts.text.into(),
|
||||
keystroke: left_opts.keystroke,
|
||||
handler: Box::new(move |ctx: &mut EventContext, _app_ctx: &AppContext, _pos| {
|
||||
ctx.dispatch_typed_action(left_opts.action.clone());
|
||||
}),
|
||||
});
|
||||
|
||||
let checkbox = options
|
||||
.checkbox
|
||||
.map(|checkbox_opts| onboarding_callout::Checkbox {
|
||||
label: checkbox_opts.label.into(),
|
||||
checked: checkbox_opts.checked,
|
||||
handler: Box::new(|ctx: &mut EventContext, _app_ctx: &AppContext, _pos| {
|
||||
ctx.dispatch_typed_action(OnboardingCalloutViewAction::ToggleCheckbox);
|
||||
}),
|
||||
});
|
||||
|
||||
// Render the callout component with data from the model state
|
||||
self.callout_component.render(
|
||||
appearance,
|
||||
onboarding_callout::Params {
|
||||
title: options.title.to_string().into(),
|
||||
text: options.text.into(),
|
||||
step: options.step,
|
||||
right_button,
|
||||
options: onboarding_callout::Options {
|
||||
left_button,
|
||||
checkbox,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for OnboardingCalloutView {
|
||||
type Action = OnboardingCalloutViewAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
OnboardingCalloutViewAction::NextClicked => {
|
||||
self.model.update(ctx, |model, ctx| {
|
||||
// Handle special cases for UniversalInput flow
|
||||
if let OnboardingCalloutState::UniversalInput(
|
||||
UniversalInputCalloutState::TalkToAgent,
|
||||
) = model.state()
|
||||
{
|
||||
if !model.has_project() {
|
||||
model.finish(ctx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
model.next(ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
OnboardingCalloutViewAction::SkipClicked => {
|
||||
self.model.update(ctx, |model, ctx| {
|
||||
model.skip(ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
OnboardingCalloutViewAction::BackToTerminalClicked => {
|
||||
self.model.update(ctx, |model, ctx| {
|
||||
model.back_to_terminal(ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
OnboardingCalloutViewAction::ToggleCheckbox => {
|
||||
self.model.update(ctx, |model, ctx| {
|
||||
model.toggle_natural_language_detection(ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SingletonEntity for OnboardingCalloutView {}
|
||||
Reference in New Issue
Block a user