Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+9
View File
@@ -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);
}
+487
View File
@@ -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;
}
+519
View File
@@ -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 {}