Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions crates/warp_tui/src/terminal_session_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ const RUNNING_COMMAND_DETACH_HINT: &str = "ctrl-c to return to command";
/// Replaces the exit hint when viewing a child agent conversation.
pub(crate) const CTRL_C_KILL_CHILD_HINT: &str = "ctrl-c again to kill child agent";
const STARTING_SHELL_HINT: &str = "Starting shell...";
/// The hint row plus its top padding. The zero state accounts for this temporary
/// chrome so its centered position already matches the post-bootstrap layout.
const STARTING_SHELL_CHROME_ROWS: u16 = 2;
const SETTINGS_PARSE_FAILED_HINT: &str = "Settings failed to load: invalid syntax.";
const SETTINGS_INVALID_VALUES_HINT: &str = "Settings failed to load: invalid values.";

Expand Down Expand Up @@ -5145,6 +5148,8 @@ impl TuiTerminalSessionView {
let builder = TuiUiBuilder::from_app(ctx);
let orchestration_tabs_available = state.orchestration_available();
let blocker_active = state.has_blocking_interaction();
let show_starting_shell_hint =
!blocker_active && matches!(input_target, TuiInputTarget::Disabled);

if state.is_alt_screen() {
self.zero_state_interaction.set_visible(false);
Expand Down Expand Up @@ -5224,10 +5229,20 @@ impl TuiTerminalSessionView {
let mut content = TuiFlex::column();
let transcript_is_empty = self.transcript.as_ref(ctx).is_empty();
self.zero_state_interaction.set_visible(transcript_is_empty);
if transcript_is_empty && self.session_state.as_ref(ctx).show_first_zero_state() {
content = content.flex_child(self.zero_state_view.as_ref(ctx).render_first_run(ctx));
} else if transcript_is_empty {
content = content.flex_child(TuiChildView::new(&self.zero_state_view).finish());
if transcript_is_empty {
let zero_state = if self.session_state.as_ref(ctx).show_first_zero_state() {
self.zero_state_view.as_ref(ctx).render_first_run(ctx)
} else {
TuiChildView::new(&self.zero_state_view).finish()
};
let zero_state = if show_starting_shell_hint {
TuiContainer::new(zero_state)
.with_padding_top(STARTING_SHELL_CHROME_ROWS)
.finish()
} else {
zero_state
};
content = content.flex_child(zero_state);
} else {
content = content.flex_child(TuiChildView::new(&self.transcript).finish());
}
Expand All @@ -5239,7 +5254,7 @@ impl TuiTerminalSessionView {
// fresh each pass — no stored suppression flag — and the hidden
// input model is never written to, so its draft/cursor/selection/
// scroll survive untouched.
if !blocker_active && matches!(input_target, TuiInputTarget::Disabled) {
if show_starting_shell_hint {
content = content.child(
TuiContainer::new(
TuiText::new(STARTING_SHELL_HINT)
Expand Down
43 changes: 43 additions & 0 deletions crates/warp_tui/src/terminal_session_view_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2996,6 +2996,49 @@ fn bootstrap_renders_starting_shell_above_input() {
});
}

#[test]
fn zero_state_position_stays_stable_across_shell_bootstrap() {
App::test((), |mut app| async move {
let fixture = focus_test_fixture(&mut app);
let (view, _) = add_focus_test_session(&mut app, &fixture, true);

let ready_lines = render_session(&mut app, &view, 80, 40);
assert!(
ready_lines
.iter()
.all(|line| line.trim() != "Starting shell..."),
"ready state must not render the bootstrap hint:\n{}",
ready_lines.join("\n")
);

view.update(&mut app, |view, _| {
view.terminal_model.lock().block_list_mut().reinit_shell();
});
let bootstrap_lines = render_session(&mut app, &view, 80, 40);
assert!(
bootstrap_lines
.iter()
.any(|line| line.trim() == "Starting shell..."),
"bootstrap state must render the starting-shell hint:\n{}",
bootstrap_lines.join("\n")
);

let title_row = |lines: &[String]| {
lines
.iter()
.position(|line| line.contains("Warp Agent CLI"))
.unwrap_or_else(|| panic!("zero-state title should render:\n{}", lines.join("\n")))
};
assert_eq!(
title_row(&bootstrap_lines),
title_row(&ready_lines),
"zero state must not shift when the bootstrap hint disappears\nbootstrap:\n{}\nready:\n{}",
bootstrap_lines.join("\n"),
ready_lines.join("\n")
);
});
}

/// The input child's rendered element is cached by the presenter, and
/// transcript emptiness can flip without any input-owned event (a terminal
/// block landing via the PTY wakeup path only invalidates the session view).
Expand Down
Loading