Skip to content

Commit 956ae6b

Browse files
Keep TUI zero state stable during shell startup (#14632)
## Description - Keep the empty TUI zero state vertically stable while the temporary `Starting shell...` hint is visible. - Account for the hint row and its top padding only in the bootstrap zero-state slot; transcript, long-running-command, and GUI paths remain unchanged. - Add a presenter-based 80×40 regression covering bootstrap and ready states. Originating thread: https://warpdotdev.slack.com/archives/C0BDQDW8V5E/p1785652323329829 ## Linked Issue - [x] The request is tracked in [APP-5098](https://linear.app/warpdotdev/issue/APP-5098/tui-zero-state-shifts-when-starting-shell-disappears). - [x] Real live-TUI `tmux capture-pane` proof is included below. ## Testing - [x] `zero_state_position_stays_stable_across_shell_bootstrap` failed before the fix and passes after it. - [x] `bootstrap_renders_starting_shell_above_input` and `long_running_command_keeps_input_hidden` pass. - [x] `./script/format --check` - [x] `cargo clippy -p warp_tui --all-targets --tests -- -D warnings` - [x] `cargo nextest run -p warp_tui --lib` — 949/949 passed. - [x] `cargo build -p warp_tui --bin warp-tui-oss` - [x] `cargo build -p warp_tui --bin warp-tui-dev` Full workspace presubmit completed format, inline-test, clippy, clang-format, and WGSL checks, but its workspace-test phase exceeded the sandbox target filesystem twice (`No space left on device`); the scoped TUI suite and PR CI are fully green. ### Real live TUI proof These frames came directly from `tmux capture-pane` while one authenticated `warp-tui-dev` process ran in an 80×40 PTY. They are terminal output from the real TUI, not replayed presenter buffers. - [Bootstrap frame — full terminal capture](https://staging.warp.dev/api/v1/agent/artifacts/019fc1a8-0abb-76b7-925e-2a8a669a16bf/download) - [Ready frame from the same process — full terminal capture](https://staging.warp.dev/api/v1/agent/artifacts/019fc1a8-0d3b-73db-9ee4-a32f78dc6ef5/download) Relevant lines (one-based `capture-pane` output; equivalent zero-based rows are 13/32/33): ``` bootstrap 14: Warp Agent CLI bootstrap 33: Starting shell... bootstrap 34: ▁▁▁... (input top border) ready 14: Warp Agent CLI ready 34: ▁▁▁... (input top border) ``` ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode CHANGELOG-TUI: Fixed the zero state shifting when shell startup completes. ### Rework changes - ⚠️ **Real TUI proof:** installed the pinned internal channel-config generator, built the dev/dogfood TUI, authenticated with the inherited cloud-runner API key, and captured bootstrap plus ready frames from the same live tmux session. The title and input border remain on identical rows while `Starting shell...` disappears. - No code changes were needed; the review finding concerned verification evidence only. <!-- factory-agent: {"source":"factory-agent","task_id":"APP-5098","task_source":"linear","task_url":"https://linear.app/warpdotdev/issue/APP-5098/tui-zero-state-shifts-when-starting-shell-disappears","linear_issue_id":"APP-5098","oz_run_id":"019fc139-4de5-70c1-a4ef-4a772853163a","repo":"warpdotdev/warp","pr_url":"https://github.com/warpdotdev/warp/pull/14632","review_rework_attempts":1} --> _Conversation: https://staging.warp.dev/conversation/5020fa91-cade-4b17-8d57-af8c5de70f0c_ _Run: https://oz.staging.warp.dev/runs/019fc139-4de5-70c1-a4ef-4a772853163a_ Co-Authored-By: Oz <oz-agent@warp.dev> _This PR was generated with [Oz](https://warp.dev/oz)._ Co-authored-by: Oz <oz-agent@warp.dev>
1 parent 41f91c6 commit 956ae6b

2 files changed

Lines changed: 63 additions & 5 deletions

File tree

crates/warp_tui/src/terminal_session_view.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,9 @@ const RUNNING_COMMAND_DETACH_HINT: &str = "ctrl-c to return to command";
192192
/// Replaces the exit hint when viewing a child agent conversation.
193193
pub(crate) const CTRL_C_KILL_CHILD_HINT: &str = "ctrl-c again to kill child agent";
194194
const STARTING_SHELL_HINT: &str = "Starting shell...";
195+
/// The hint row plus its top padding. The zero state accounts for this temporary
196+
/// chrome so its centered position already matches the post-bootstrap layout.
197+
const STARTING_SHELL_CHROME_ROWS: u16 = 2;
195198
const SETTINGS_PARSE_FAILED_HINT: &str = "Settings failed to load: invalid syntax.";
196199
const SETTINGS_INVALID_VALUES_HINT: &str = "Settings failed to load: invalid values.";
197200

@@ -5145,6 +5148,8 @@ impl TuiTerminalSessionView {
51455148
let builder = TuiUiBuilder::from_app(ctx);
51465149
let orchestration_tabs_available = state.orchestration_available();
51475150
let blocker_active = state.has_blocking_interaction();
5151+
let show_starting_shell_hint =
5152+
!blocker_active && matches!(input_target, TuiInputTarget::Disabled);
51485153

51495154
if state.is_alt_screen() {
51505155
self.zero_state_interaction.set_visible(false);
@@ -5224,10 +5229,20 @@ impl TuiTerminalSessionView {
52245229
let mut content = TuiFlex::column();
52255230
let transcript_is_empty = self.transcript.as_ref(ctx).is_empty();
52265231
self.zero_state_interaction.set_visible(transcript_is_empty);
5227-
if transcript_is_empty && self.session_state.as_ref(ctx).show_first_zero_state() {
5228-
content = content.flex_child(self.zero_state_view.as_ref(ctx).render_first_run(ctx));
5229-
} else if transcript_is_empty {
5230-
content = content.flex_child(TuiChildView::new(&self.zero_state_view).finish());
5232+
if transcript_is_empty {
5233+
let zero_state = if self.session_state.as_ref(ctx).show_first_zero_state() {
5234+
self.zero_state_view.as_ref(ctx).render_first_run(ctx)
5235+
} else {
5236+
TuiChildView::new(&self.zero_state_view).finish()
5237+
};
5238+
let zero_state = if show_starting_shell_hint {
5239+
TuiContainer::new(zero_state)
5240+
.with_padding_top(STARTING_SHELL_CHROME_ROWS)
5241+
.finish()
5242+
} else {
5243+
zero_state
5244+
};
5245+
content = content.flex_child(zero_state);
52315246
} else {
52325247
content = content.flex_child(TuiChildView::new(&self.transcript).finish());
52335248
}
@@ -5239,7 +5254,7 @@ impl TuiTerminalSessionView {
52395254
// fresh each pass — no stored suppression flag — and the hidden
52405255
// input model is never written to, so its draft/cursor/selection/
52415256
// scroll survive untouched.
5242-
if !blocker_active && matches!(input_target, TuiInputTarget::Disabled) {
5257+
if show_starting_shell_hint {
52435258
content = content.child(
52445259
TuiContainer::new(
52455260
TuiText::new(STARTING_SHELL_HINT)

crates/warp_tui/src/terminal_session_view_tests.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2996,6 +2996,49 @@ fn bootstrap_renders_starting_shell_above_input() {
29962996
});
29972997
}
29982998

2999+
#[test]
3000+
fn zero_state_position_stays_stable_across_shell_bootstrap() {
3001+
App::test((), |mut app| async move {
3002+
let fixture = focus_test_fixture(&mut app);
3003+
let (view, _) = add_focus_test_session(&mut app, &fixture, true);
3004+
3005+
let ready_lines = render_session(&mut app, &view, 80, 40);
3006+
assert!(
3007+
ready_lines
3008+
.iter()
3009+
.all(|line| line.trim() != "Starting shell..."),
3010+
"ready state must not render the bootstrap hint:\n{}",
3011+
ready_lines.join("\n")
3012+
);
3013+
3014+
view.update(&mut app, |view, _| {
3015+
view.terminal_model.lock().block_list_mut().reinit_shell();
3016+
});
3017+
let bootstrap_lines = render_session(&mut app, &view, 80, 40);
3018+
assert!(
3019+
bootstrap_lines
3020+
.iter()
3021+
.any(|line| line.trim() == "Starting shell..."),
3022+
"bootstrap state must render the starting-shell hint:\n{}",
3023+
bootstrap_lines.join("\n")
3024+
);
3025+
3026+
let title_row = |lines: &[String]| {
3027+
lines
3028+
.iter()
3029+
.position(|line| line.contains("Warp Agent CLI"))
3030+
.unwrap_or_else(|| panic!("zero-state title should render:\n{}", lines.join("\n")))
3031+
};
3032+
assert_eq!(
3033+
title_row(&bootstrap_lines),
3034+
title_row(&ready_lines),
3035+
"zero state must not shift when the bootstrap hint disappears\nbootstrap:\n{}\nready:\n{}",
3036+
bootstrap_lines.join("\n"),
3037+
ready_lines.join("\n")
3038+
);
3039+
});
3040+
}
3041+
29993042
/// The input child's rendered element is cached by the presenter, and
30003043
/// transcript emptiness can flip without any input-owned event (a terminal
30013044
/// block landing via the PTY wakeup path only invalidates the session view).

0 commit comments

Comments
 (0)