Skip to content

Commit 49e9e2f

Browse files
authored
Fix TUI zero-state onboarding flicker (#14612)
1 parent fa70ad0 commit 49e9e2f

4 files changed

Lines changed: 75 additions & 35 deletions

File tree

app/src/tui_onboarding_markers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ impl TuiOnboardingMarkers {
121121
}
122122

123123
/// Starts a fresh, account-scoped load. Terminal creation never waits for
124-
/// this request; consumers reconcile provisional one-time UI on
124+
/// this request; consumers keep one-time UI hidden until
125125
/// [`TuiOnboardingMarkersEvent::Ready`].
126126
pub fn load_current_account(&mut self, ctx: &mut ModelContext<Self>) {
127127
self.state = TuiOnboardingMarkersState::Loading;

crates/warp_tui/src/terminal_session_view.rs

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ mod todo_menu;
162162
use self::completions::CompletionRequestState;
163163
use self::input_detection::InputDetectionState;
164164
use self::state::{
165-
TuiTerminalSessionState, TuiTerminalSessionStateModel, TuiTerminalSessionStateResolveError,
165+
TuiFirstZeroStateState, TuiTerminalSessionState, TuiTerminalSessionStateModel,
166+
TuiTerminalSessionStateResolveError,
166167
};
167168

168169
/// Width used before the first layout pass pushes the real terminal width into the editor.
@@ -1779,15 +1780,22 @@ impl TuiTerminalSessionView {
17791780
let orchestration_tab_bar = ctx.add_typed_action_tui_view(|_| TuiTabBarView::empty());
17801781
let onboarding_markers =
17811782
handles_first_run_onboarding.then(|| TuiOnboardingMarkers::handle(ctx));
1782-
let show_first_zero_state = onboarding_markers.as_ref().is_some_and(|markers| {
1783-
markers.update(ctx, |markers, ctx| {
1784-
if markers.is_ready() {
1785-
markers.consume(TuiOnboardingMarker::FirstZeroState, ctx)
1786-
} else {
1787-
true
1788-
}
1789-
})
1790-
});
1783+
let first_zero_state =
1784+
onboarding_markers
1785+
.as_ref()
1786+
.map_or(TuiFirstZeroStateState::Dismissed, |markers| {
1787+
markers.update(ctx, |markers, ctx| {
1788+
if markers.is_ready() {
1789+
if markers.consume(TuiOnboardingMarker::FirstZeroState, ctx) {
1790+
TuiFirstZeroStateState::Visible
1791+
} else {
1792+
TuiFirstZeroStateState::Dismissed
1793+
}
1794+
} else {
1795+
TuiFirstZeroStateState::Pending
1796+
}
1797+
})
1798+
});
17911799
let session_state = ctx.add_model(|_| {
17921800
TuiTerminalSessionStateModel::new(
17931801
&model,
@@ -1796,7 +1804,7 @@ impl TuiTerminalSessionView {
17961804
&ai_input_model,
17971805
&suggestions_mode,
17981806
&orchestration_tab_bar,
1799-
show_first_zero_state,
1807+
first_zero_state,
18001808
)
18011809
});
18021810
if let Some(onboarding_markers) = onboarding_markers {
@@ -1806,17 +1814,15 @@ impl TuiTerminalSessionView {
18061814
move |_, markers, event, ctx| match event {
18071815
TuiOnboardingMarkersEvent::Loading => {
18081816
session_state_for_markers.update(ctx, |state, ctx| {
1809-
state.set_show_first_zero_state(true, ctx);
1817+
state.set_first_zero_state_pending(ctx);
18101818
});
18111819
}
18121820
TuiOnboardingMarkersEvent::Ready => {
18131821
let keep_showing = markers.update(ctx, |markers, ctx| {
18141822
markers.consume(TuiOnboardingMarker::FirstZeroState, ctx)
18151823
});
18161824
session_state_for_markers.update(ctx, |state, ctx| {
1817-
if state.show_first_zero_state() {
1818-
state.set_show_first_zero_state(keep_showing, ctx);
1819-
}
1825+
state.resolve_first_zero_state(keep_showing, ctx);
18201826
});
18211827
}
18221828
},
@@ -3725,7 +3731,7 @@ impl TuiTerminalSessionView {
37253731
ctx: &mut ViewContext<Self>,
37263732
) {
37273733
self.session_state.update(ctx, |state, ctx| {
3728-
state.set_show_first_zero_state(false, ctx);
3734+
state.dismiss_first_zero_state(ctx);
37293735
});
37303736
// A stale editor frame must not submit into a shell that is still
37313737
// bootstrapping or has handed input to a foreground process.

crates/warp_tui/src/terminal_session_view/state.rs

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ enum TuiTerminalSessionStateSource {
4747
orchestration_tabs_available: Rc<dyn Fn(&AppContext) -> bool>,
4848
},
4949
}
50+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51+
pub(super) enum TuiFirstZeroStateState {
52+
Pending,
53+
Visible,
54+
Dismissed,
55+
}
5056

5157
/// Persistent session-owned model that resolves a live state snapshot.
5258
///
@@ -56,7 +62,7 @@ enum TuiTerminalSessionStateSource {
5662
/// presentation components one shared state source.
5763
pub(crate) struct TuiTerminalSessionStateModel {
5864
source: TuiTerminalSessionStateSource,
59-
show_first_zero_state: bool,
65+
first_zero_state: TuiFirstZeroStateState,
6066
}
6167

6268
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -97,14 +103,14 @@ impl Entity for TuiTerminalSessionStateModel {
97103
}
98104

99105
impl TuiTerminalSessionStateModel {
100-
pub(crate) fn new(
106+
pub(super) fn new(
101107
terminal_model: &Arc<FairMutex<TerminalModel>>,
102108
cli_subagent_controller: &ModelHandle<CLISubagentController>,
103109
transcript: &ViewHandle<TuiTranscriptView>,
104110
input_mode: &ModelHandle<BlocklistAIInputModel>,
105111
suggestions_mode: &ModelHandle<TuiInputSuggestionsModeModel>,
106112
orchestration_tab_bar: &ViewHandle<TuiTabBarView>,
107-
show_first_zero_state: bool,
113+
first_zero_state: TuiFirstZeroStateState,
108114
) -> Self {
109115
Self {
110116
source: TuiTerminalSessionStateSource::Session {
@@ -115,17 +121,36 @@ impl TuiTerminalSessionStateModel {
115121
suggestions_mode: suggestions_mode.downgrade(),
116122
orchestration_tab_bar: orchestration_tab_bar.downgrade(),
117123
},
118-
show_first_zero_state,
124+
first_zero_state,
119125
}
120126
}
121127

122128
pub(crate) fn show_first_zero_state(&self) -> bool {
123-
self.show_first_zero_state
129+
self.first_zero_state == TuiFirstZeroStateState::Visible
130+
}
131+
132+
pub(crate) fn set_first_zero_state_pending(&mut self, ctx: &mut ModelContext<Self>) {
133+
if self.first_zero_state != TuiFirstZeroStateState::Pending {
134+
self.first_zero_state = TuiFirstZeroStateState::Pending;
135+
ctx.notify();
136+
}
137+
}
138+
139+
pub(crate) fn resolve_first_zero_state(&mut self, show: bool, ctx: &mut ModelContext<Self>) {
140+
if self.first_zero_state != TuiFirstZeroStateState::Pending {
141+
return;
142+
}
143+
self.first_zero_state = if show {
144+
TuiFirstZeroStateState::Visible
145+
} else {
146+
TuiFirstZeroStateState::Dismissed
147+
};
148+
ctx.notify();
124149
}
125150

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;
151+
pub(crate) fn dismiss_first_zero_state(&mut self, ctx: &mut ModelContext<Self>) {
152+
if self.first_zero_state != TuiFirstZeroStateState::Dismissed {
153+
self.first_zero_state = TuiFirstZeroStateState::Dismissed;
129154
ctx.notify();
130155
}
131156
}
@@ -141,7 +166,7 @@ impl TuiTerminalSessionStateModel {
141166
suggestions_mode: suggestions_mode.downgrade(),
142167
orchestration_tabs_available: Rc::new(orchestration_tabs_available),
143168
},
144-
show_first_zero_state: false,
169+
first_zero_state: TuiFirstZeroStateState::Dismissed,
145170
}
146171
}
147172

crates/warp_tui/src/terminal_session_view_tests.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3863,7 +3863,7 @@ fn zero_state_renders_with_only_zero_height_bootstrap_blocks() {
38633863
}
38643864

38653865
#[test]
3866-
fn first_zero_state_is_provisional_and_reconciles_without_replacing_the_session() {
3866+
fn first_zero_state_stays_hidden_while_markers_load_and_reconciles_without_replacing_session() {
38673867
App::test((), |mut app| async move {
38683868
let fixture = focus_test_fixture(&mut app);
38693869
app.update(|ctx| {
@@ -3875,18 +3875,19 @@ fn first_zero_state_is_provisional_and_reconciles_without_replacing_the_session(
38753875

38763876
app.read(|ctx| {
38773877
assert!(
3878-
view.as_ref(ctx)
3878+
!view
3879+
.as_ref(ctx)
38793880
.session_state
38803881
.as_ref(ctx)
38813882
.show_first_zero_state()
38823883
);
38833884
});
38843885
let lines = render_session(&mut app, &view, 100, 24);
3885-
assert!(lines.iter().any(|line| line.contains("Welcome to Warp")));
3886+
assert!(lines.iter().any(|line| line.contains("Warp Agent CLI")));
38863887
assert!(
38873888
lines
38883889
.iter()
3889-
.any(|line| line.contains("What’s different about Warp"))
3890+
.all(|line| !line.contains("What’s different about Warp"))
38903891
);
38913892
assert!(lines.iter().all(|line| !line.contains("████")));
38923893

@@ -3913,7 +3914,7 @@ fn first_zero_state_is_provisional_and_reconciles_without_replacing_the_session(
39133914
}
39143915

39153916
#[test]
3916-
fn dismissed_provisional_zero_state_stays_hidden_but_consumes_ready_marker() {
3917+
fn dismissed_pending_zero_state_stays_hidden_but_consumes_ready_marker() {
39173918
App::test((), |mut app| async move {
39183919
let fixture = focus_test_fixture(&mut app);
39193920
app.update(|ctx| {
@@ -3925,7 +3926,7 @@ fn dismissed_provisional_zero_state_stays_hidden_but_consumes_ready_marker() {
39253926

39263927
view.update(&mut app, |view, ctx| {
39273928
view.session_state.update(ctx, |state, ctx| {
3928-
state.set_show_first_zero_state(false, ctx);
3929+
state.dismiss_first_zero_state(ctx);
39293930
});
39303931
});
39313932
app.update(|ctx| {
@@ -3966,7 +3967,7 @@ fn background_session_does_not_receive_first_run_onboarding() {
39663967

39673968
app.read(|ctx| {
39683969
assert!(
3969-
onboarding_view
3970+
!onboarding_view
39703971
.as_ref(ctx)
39713972
.session_state
39723973
.as_ref(ctx)
@@ -4001,11 +4002,18 @@ fn background_session_does_not_receive_first_run_onboarding() {
40014002
.show_first_zero_state()
40024003
);
40034004
});
4005+
let lines = render_session(&mut app, &onboarding_view, 100, 24);
4006+
assert!(lines.iter().any(|line| line.contains("Welcome to Warp")));
4007+
assert!(
4008+
lines
4009+
.iter()
4010+
.any(|line| line.contains("What’s different about Warp"))
4011+
);
40044012
});
40054013
}
40064014

40074015
#[test]
4008-
fn account_transition_restores_provisional_zero_state_on_existing_session() {
4016+
fn account_transition_hides_first_zero_state_while_markers_reload() {
40094017
App::test((), |mut app| async move {
40104018
let fixture = focus_test_fixture(&mut app);
40114019
let (view, session_id) = add_first_run_onboarding_test_session(&mut app, &fixture, true);
@@ -4026,7 +4034,8 @@ fn account_transition_restores_provisional_zero_state_on_existing_session() {
40264034
});
40274035
app.read(|ctx| {
40284036
assert!(
4029-
view.as_ref(ctx)
4037+
!view
4038+
.as_ref(ctx)
40304039
.session_state
40314040
.as_ref(ctx)
40324041
.show_first_zero_state()

0 commit comments

Comments
 (0)