Skip to content

Commit 44f112c

Browse files
moirahuangoz-agent
andauthored
TUI: Add the first-session zero state (#14580)
## Description <!-- Please remember to add your design buddy onto the PR for review, if it contains any UI changes! --> Add the once-per-account first-session zero state. The first-run variant reuses the existing zero-state layout, animation, starfield, project context, input, and footer while presenting the Figma-aligned "What's different about Warp" guidance. Design: https://www.figma.com/design/yg5nbPZuGoAszHS3Rhvehu/TUI?node-id=1768-18320&m=dev ## Linked Issue - [ ] The linked issue is labeled `ready-to-spec` or `ready-to-implement`. - [ ] Where appropriate, screenshots or a short video of the implementation are included below (especially for user-visible or UI changes). Implementation plan: https://staging.warp.dev/drive/notebook/VEDTJkrbVxEnFZIwM5uC3A ## Testing - `./script/format` - `cargo check -p warp_tui --tests` - Previously passed focused coverage: `first_zero_state_matches_welcome_design_copy` and `first_zero_state_is_provisional_and_reconciles_without_replacing_the_session`. - Full test execution and Clippy were skipped as requested. - [ ] I have manually tested my changes locally with `./script/run` ### Screenshots / Videos Manual verification screenshot and discussion: https://staging.warp.dev/conversation/b543d79e-e3d9-4f48-9516-50c2372eabc8 ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode CHANGELOG-TUI: Added first-run guidance to the Warp Agent CLI welcome screen. Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent a95e6e5 commit 44f112c

9 files changed

Lines changed: 455 additions & 28 deletions

File tree

app/src/tui_onboarding_markers.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ impl TuiOnboardingMarkers {
105105
load_timeout: MARKER_LOAD_TIMEOUT,
106106
}
107107
}
108+
108109
#[cfg(test)]
109110
fn new_loading_for_test(
110111
onboarding_client: Arc<dyn TuiOnboardingClient>,
@@ -140,6 +141,7 @@ impl TuiOnboardingMarkers {
140141
);
141142
ctx.notify();
142143
}
144+
143145
/// Invalidates the previous account snapshot before a signed-out terminal
144146
/// can be created for the next browser authentication flow.
145147
pub fn reset_for_account_transition(&mut self, ctx: &mut ModelContext<Self>) {

crates/warp_tui/src/session.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,10 +325,12 @@ fn ensure_terminal_session(
325325

326326
let resume_token = sessions.update(ctx, |sessions, _| sessions.take_resume_token());
327327
let window_id = root.window_id(ctx);
328+
let handles_first_run_onboarding = resume_token.is_none();
328329
let (_, surface) = TuiSessions::create_local_terminal_session(
329330
sessions,
330331
window_id,
331332
true,
333+
handles_first_run_onboarding,
332334
std::env::current_dir().ok(),
333335
ctx,
334336
);

crates/warp_tui/src/session_registry.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ impl TuiSessions {
151151
sessions: &ModelHandle<Self>,
152152
window_id: WindowId,
153153
focus: bool,
154+
handles_first_run_onboarding: bool,
154155
startup_directory: Option<PathBuf>,
155156
ctx: &mut AppContext,
156157
) -> (TuiSessionId, ViewHandle<TuiTerminalSessionView>) {
@@ -197,6 +198,7 @@ impl TuiSessions {
197198
exit_summary,
198199
keyboard_enhancement_supported,
199200
default_autoexecute_mode,
201+
handles_first_run_onboarding,
200202
initial_settings_file_error,
201203
ctx,
202204
)
@@ -268,8 +270,14 @@ impl TuiSessions {
268270
conversation: AIConversation,
269271
ctx: &mut AppContext,
270272
) -> (TuiSessionId, ViewHandle<TuiTerminalSessionView>) {
271-
let (session_id, surface) =
272-
Self::create_local_terminal_session(sessions, window_id, false, startup_directory, ctx);
273+
let (session_id, surface) = Self::create_local_terminal_session(
274+
sessions,
275+
window_id,
276+
false,
277+
false,
278+
startup_directory,
279+
ctx,
280+
);
273281
surface.update(ctx, |view, ctx| {
274282
view.restore_orchestrated_child_conversation(conversation, ctx);
275283
});
@@ -434,6 +442,7 @@ impl TuiSessions {
434442
&sessions,
435443
window_id,
436444
false,
445+
false,
437446
working_directory.clone(),
438447
ctx,
439448
);

crates/warp_tui/src/terminal_session_view.rs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ use warp::tui_export::{
3636
SlashCommandDataSource as _, SlashCommandKind, SlashCommandSelectionBehavior,
3737
StartAgentExecutorEvent, StartAgentRequest, StaticCommand, TelemetryEvent, TerminalColorList,
3838
TerminalColors, TerminalModel, TerminalSurface, TerminalSurfaceInit, TranscriptScope,
39-
TuiMcpAction, TuiMcpManager, TuiMcpServerId, TuiMcpVariableValue, TuiSlashCommandDataSource,
39+
TuiMcpAction, TuiMcpManager, TuiMcpServerId, TuiMcpVariableValue, TuiOnboardingMarker,
40+
TuiOnboardingMarkers, TuiOnboardingMarkersEvent, TuiSlashCommandDataSource,
4041
TuiSlashCommandDataSourceArgs, TuiUpArrowHistoryItemKind, TuiUserInfoManager,
4142
TuiUserInfoManagerEvent, TuiZeroStateDataSource, UserTakeOverReason, WAKEUP_THROTTLE_PERIOD,
4243
WarpConfig, WarpConfigUpdateEvent, block_context_from_terminal_model,
@@ -1435,6 +1436,7 @@ impl TuiTerminalSessionView {
14351436
exit_summary: TuiExitSummaryHandle,
14361437
keyboard_enhancement_supported: bool,
14371438
default_autoexecute_mode: AIConversationAutoexecuteMode,
1439+
handles_first_run_onboarding: bool,
14381440
initial_settings_file_error: Option<SettingsFileError>,
14391441
ctx: &mut ViewContext<Self>,
14401442
) -> Self {
@@ -1769,6 +1771,17 @@ impl TuiTerminalSessionView {
17691771
let suggestions_mode_for_input = suggestions_mode.clone();
17701772
let terminal_model_for_input = model.clone();
17711773
let orchestration_tab_bar = ctx.add_typed_action_tui_view(|_| TuiTabBarView::empty());
1774+
let onboarding_markers =
1775+
handles_first_run_onboarding.then(|| TuiOnboardingMarkers::handle(ctx));
1776+
let show_first_zero_state = onboarding_markers.as_ref().is_some_and(|markers| {
1777+
markers.update(ctx, |markers, ctx| {
1778+
if markers.is_ready() {
1779+
markers.consume(TuiOnboardingMarker::FirstZeroState, ctx)
1780+
} else {
1781+
true
1782+
}
1783+
})
1784+
});
17721785
let session_state = ctx.add_model(|_| {
17731786
TuiTerminalSessionStateModel::new(
17741787
&model,
@@ -1777,8 +1790,32 @@ impl TuiTerminalSessionView {
17771790
&ai_input_model,
17781791
&suggestions_mode,
17791792
&orchestration_tab_bar,
1793+
show_first_zero_state,
17801794
)
17811795
});
1796+
if let Some(onboarding_markers) = onboarding_markers {
1797+
let session_state_for_markers = session_state.clone();
1798+
ctx.subscribe_to_model(
1799+
&onboarding_markers,
1800+
move |_, markers, event, ctx| match event {
1801+
TuiOnboardingMarkersEvent::Loading => {
1802+
session_state_for_markers.update(ctx, |state, ctx| {
1803+
state.set_show_first_zero_state(true, ctx);
1804+
});
1805+
}
1806+
TuiOnboardingMarkersEvent::Ready => {
1807+
let keep_showing = markers.update(ctx, |markers, ctx| {
1808+
markers.consume(TuiOnboardingMarker::FirstZeroState, ctx)
1809+
});
1810+
session_state_for_markers.update(ctx, |state, ctx| {
1811+
if state.show_first_zero_state() {
1812+
state.set_show_first_zero_state(keep_showing, ctx);
1813+
}
1814+
});
1815+
}
1816+
},
1817+
);
1818+
}
17821819
let input_editor_for_input = input_editor_model.clone();
17831820
let session_state_for_input = session_state.clone();
17841821
let input_view = ctx.add_typed_action_tui_view(move |ctx| {
@@ -3679,6 +3716,9 @@ impl TuiTerminalSessionView {
36793716
linked_workflow_data: Option<LinkedWorkflowData>,
36803717
ctx: &mut ViewContext<Self>,
36813718
) {
3719+
self.session_state.update(ctx, |state, ctx| {
3720+
state.set_show_first_zero_state(false, ctx);
3721+
});
36823722
// A stale editor frame must not submit into a shell that is still
36833723
// bootstrapping or has handed input to a foreground process.
36843724
if !self.input_target().agent_editor_owns_input() {
@@ -5024,7 +5064,9 @@ impl TuiTerminalSessionView {
50245064
let mut content = TuiFlex::column();
50255065
let transcript_is_empty = self.transcript.as_ref(ctx).is_empty();
50265066
self.zero_state_interaction.set_visible(transcript_is_empty);
5027-
if transcript_is_empty {
5067+
if transcript_is_empty && self.session_state.as_ref(ctx).show_first_zero_state() {
5068+
content = content.flex_child(self.zero_state_view.as_ref(ctx).render_first_run(ctx));
5069+
} else if transcript_is_empty {
50285070
content = content.flex_child(TuiChildView::new(&self.zero_state_view).finish());
50295071
} else {
50305072
content = content.flex_child(TuiChildView::new(&self.transcript).finish());

crates/warp_tui/src/terminal_session_view/state.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ use std::{error, fmt};
66
use parking_lot::FairMutex;
77
use warp::tui_export::{BlocklistAIInputModel, CLISubagentController, TerminalModel};
88
use warpui_core::keymap::Context;
9-
use warpui_core::{AppContext, Entity, ModelHandle, ViewHandle, WeakModelHandle, WeakViewHandle};
9+
use warpui_core::{
10+
AppContext, Entity, ModelContext, ModelHandle, ViewHandle, WeakModelHandle, WeakViewHandle,
11+
};
1012

1113
use super::{
1214
AUTO_APPROVE_TOGGLE_BINDING_NAME, BlockingInputSource,
@@ -54,6 +56,7 @@ enum TuiTerminalSessionStateSource {
5456
/// presentation components one shared state source.
5557
pub(crate) struct TuiTerminalSessionStateModel {
5658
source: TuiTerminalSessionStateSource,
59+
show_first_zero_state: bool,
5760
}
5861

5962
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -101,6 +104,7 @@ impl TuiTerminalSessionStateModel {
101104
input_mode: &ModelHandle<BlocklistAIInputModel>,
102105
suggestions_mode: &ModelHandle<TuiInputSuggestionsModeModel>,
103106
orchestration_tab_bar: &ViewHandle<TuiTabBarView>,
107+
show_first_zero_state: bool,
104108
) -> Self {
105109
Self {
106110
source: TuiTerminalSessionStateSource::Session {
@@ -111,6 +115,18 @@ impl TuiTerminalSessionStateModel {
111115
suggestions_mode: suggestions_mode.downgrade(),
112116
orchestration_tab_bar: orchestration_tab_bar.downgrade(),
113117
},
118+
show_first_zero_state,
119+
}
120+
}
121+
122+
pub(crate) fn show_first_zero_state(&self) -> bool {
123+
self.show_first_zero_state
124+
}
125+
126+
pub(crate) fn set_show_first_zero_state(&mut self, show: bool, ctx: &mut ModelContext<Self>) {
127+
if self.show_first_zero_state != show {
128+
self.show_first_zero_state = show;
129+
ctx.notify();
114130
}
115131
}
116132
#[cfg(test)]
@@ -125,6 +141,7 @@ impl TuiTerminalSessionStateModel {
125141
suggestions_mode: suggestions_mode.downgrade(),
126142
orchestration_tabs_available: Rc::new(orchestration_tabs_available),
127143
},
144+
show_first_zero_state: false,
128145
}
129146
}
130147

0 commit comments

Comments
 (0)