Skip to content

Commit 775616a

Browse files
authored
fix: resolve vp hanging on launch in Warp terminal (#762)
## Background The global `vp` CLI appeared to hang in Warp terminal, requiring a keypress before any output was displayed. This affected both the bare `vp` command (interactive picker) and subcommands like `vp create` that delegate to Node.js. The issue did not occur in other terminals such as iTerm2 or Terminal.app. ## Root Cause `vite_shared::header::vite_plus_header()` calls `query_terminal_colors()` which sends OSC escape sequences (`\x1b]10;?\x1b\\` for foreground color and `\x1b]4;N;?\x1b\\` for palette colors) to `/dev/tty` in raw mode to read the terminal's color values for the header gradient. Most terminals respond to these queries, but Warp's block-mode renderer does not. The function then blocks on `poll()`/`read()` waiting for a response that never comes, consuming the next keypress as a fake "response" instead. Since the header is rendered on nearly every command, this caused the CLI to appear stuck on every invocation in Warp. ## Fix 1. **Skip OSC color queries in Warp** (`header.rs`): Early-return from `query_terminal_colors()` when `TERM_PROGRAM=WarpTerminal`, falling back to default blue/magenta colors. This matches the existing CI environment skip pattern. 2. **Use alternate screen for the picker** (`command_picker.rs`): Switch the interactive command picker to use `EnterAlternateScreen` / `LeaveAlternateScreen` instead of `Clear(ClearType::All)` + manual cursor reset. This is standard TUI practice and avoids a large blank area that Warp's block-mode renderer created with the previous clear-based approach. 3. **Conditional padding for Warp** (`command_picker.rs`): Warp renders alternate screen content flush against the edges. Add a 1-line top margin and 1-space left margin only when running in Warp to improve readability, without affecting other terminals. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches terminal I/O and TUI screen-management paths, which can regress rendering or leave terminals in a bad state if edge cases are missed, but changes are scoped to UI behavior and add explicit cleanup/error handling. > > **Overview** > Fixes `vp` appearing to hang in Warp by detecting Warp (`TERM_PROGRAM=WarpTerminal`) and **skipping OSC color queries** in `vite_shared::header::query_terminal_colors`, falling back to default header colors. > > Updates the interactive command picker to use the terminal **alternate screen** (`EnterAlternateScreen`/`LeaveAlternateScreen`) with safer event/error handling, and adds Warp-specific padding plus a viewport-size adjustment (`compute_viewport_size` now accounts for header overhead; tests updated accordingly). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5bebc95. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent e48cb0c commit 775616a

2 files changed

Lines changed: 82 additions & 47 deletions

File tree

crates/vite_global_cli/src/command_picker.rs

Lines changed: 62 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,11 @@ fn run_picker(command_order: &[usize]) -> io::Result<Option<PickedCommand>> {
160160
let mut viewport_start = 0usize;
161161
let mut query = String::new();
162162

163+
let is_warp = vite_shared::header::is_warp_terminal();
164+
let header_overhead = if is_warp { 10 } else { 9 };
165+
163166
terminal::enable_raw_mode()?;
164-
execute!(stdout, cursor::Hide)?;
167+
execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?;
165168

166169
let pick_result = loop {
167170
let filtered_indices = filtered_command_indices(&query, command_order);
@@ -177,37 +180,45 @@ fn run_picker(command_order: &[usize]) -> io::Result<Option<PickedCommand>> {
177180

178181
let (_, rows) = terminal::size().unwrap_or((80, 24));
179182
let rows = if rows == 0 { 24 } else { rows };
180-
let viewport_size = compute_viewport_size(rows.into(), filtered_indices.len());
183+
let viewport_size =
184+
compute_viewport_size(rows.into(), filtered_indices.len(), header_overhead);
181185
viewport_start = align_viewport(viewport_start, selected_position, viewport_size);
182-
render_picker(
186+
match render_picker(
183187
&mut stdout,
184188
&query,
185189
&filtered_indices,
186190
selected_position,
187191
viewport_start,
188192
viewport_size,
189-
)?;
193+
) {
194+
Ok(()) => {}
195+
Err(err) => break Err(err),
196+
}
190197

191-
if let Event::Key(KeyEvent { code, modifiers, .. }) = event::read()? {
192-
match handle_key_event(
193-
code,
194-
modifiers,
195-
&mut query,
196-
&mut selected_position,
197-
filtered_indices.len(),
198-
) {
199-
ControlFlow::Continue(()) => continue,
200-
ControlFlow::Break(Some(())) => {
201-
let Some(index) = filtered_indices.get(selected_position).copied() else {
202-
continue;
203-
};
204-
break Ok(Some(PickedCommand {
205-
command: COMMANDS[index].command,
206-
append_help: COMMANDS[index].append_help,
207-
}));
198+
match event::read() {
199+
Ok(Event::Key(KeyEvent { code, modifiers, .. })) => {
200+
match handle_key_event(
201+
code,
202+
modifiers,
203+
&mut query,
204+
&mut selected_position,
205+
filtered_indices.len(),
206+
) {
207+
ControlFlow::Continue(()) => continue,
208+
ControlFlow::Break(Some(())) => {
209+
let Some(index) = filtered_indices.get(selected_position).copied() else {
210+
continue;
211+
};
212+
break Ok(Some(PickedCommand {
213+
command: COMMANDS[index].command,
214+
append_help: COMMANDS[index].append_help,
215+
}));
216+
}
217+
ControlFlow::Break(None) => break Ok(None),
208218
}
209-
ControlFlow::Break(None) => break Ok(None),
210219
}
220+
Ok(_) => continue,
221+
Err(err) => break Err(err),
211222
}
212223
};
213224

@@ -221,13 +232,7 @@ fn run_picker(command_order: &[usize]) -> io::Result<Option<PickedCommand>> {
221232

222233
fn cleanup_picker(stdout: &mut io::Stdout) -> io::Result<()> {
223234
terminal::disable_raw_mode()?;
224-
execute!(
225-
stdout,
226-
cursor::Show,
227-
terminal::Clear(ClearType::All),
228-
cursor::MoveTo(0, 0),
229-
ResetColor
230-
)?;
235+
execute!(stdout, cursor::Show, terminal::LeaveAlternateScreen, ResetColor)?;
231236
Ok(())
232237
}
233238

@@ -294,21 +299,26 @@ fn render_picker(
294299
) -> io::Result<()> {
295300
let (columns, _) = terminal::size().unwrap_or((80, 24));
296301
let columns = if columns == 0 { 80 } else { columns };
297-
let max_width = usize::from(columns).saturating_sub(4);
302+
// Warp terminal needs extra padding since it renders alternate screen
303+
// content flush against the edges of its block-mode renderer.
304+
let pad = if vite_shared::header::is_warp_terminal() { " " } else { "" };
305+
let max_width = usize::from(columns).saturating_sub(4 + pad.len());
298306
let viewport_end = (viewport_start + viewport_size).min(filtered_indices.len());
299307
let instruction = truncate_line(
300308
&format!("Select a command (↑/↓, Enter to run, Esc to cancel): {query}"),
301309
max_width,
302310
);
303311

312+
execute!(stdout, cursor::MoveTo(0, 0), terminal::Clear(ClearType::All),)?;
313+
if vite_shared::header::is_warp_terminal() {
314+
execute!(stdout, Print(NEWLINE))?;
315+
}
304316
execute!(
305317
stdout,
306-
cursor::MoveTo(0, 0),
307-
terminal::Clear(ClearType::All),
308-
Print(vite_shared::header::vite_plus_header()),
318+
Print(format!("{pad}{}", vite_shared::header::vite_plus_header())),
309319
Print(NEWLINE),
310320
Print(NEWLINE),
311-
Print(instruction),
321+
Print(format!("{pad}{instruction}")),
312322
Print(NEWLINE),
313323
Print(NEWLINE)
314324
)?;
@@ -317,7 +327,7 @@ fn render_picker(
317327
execute!(
318328
stdout,
319329
SetForegroundColor(crossterm::style::Color::DarkGrey),
320-
Print(" ↑ more"),
330+
Print(format!("{pad} ↑ more")),
321331
Print(NEWLINE),
322332
ResetColor
323333
)?;
@@ -337,7 +347,7 @@ fn render_picker(
337347
execute!(
338348
stdout,
339349
SetForegroundColor(crossterm::style::Color::DarkGrey),
340-
Print(format!(" {marker} ")),
350+
Print(format!("{pad} {marker} ")),
341351
ResetColor
342352
)?;
343353
if is_selected {
@@ -371,7 +381,7 @@ fn render_picker(
371381
execute!(
372382
stdout,
373383
SetForegroundColor(crossterm::style::Color::DarkGrey),
374-
Print(format!(" {marker} ")),
384+
Print(format!("{pad} {marker} ")),
375385
ResetColor
376386
)?;
377387
execute!(stdout, SetForegroundColor(SELECTED_COLOR), SetAttribute(Attribute::Bold),)?;
@@ -391,7 +401,7 @@ fn render_picker(
391401
execute!(
392402
stdout,
393403
SetForegroundColor(crossterm::style::Color::DarkGrey),
394-
Print(format!(" {marker} ")),
404+
Print(format!("{pad} {marker} ")),
395405
ResetColor,
396406
Print(label),
397407
)?;
@@ -403,7 +413,7 @@ fn render_picker(
403413
execute!(
404414
stdout,
405415
SetForegroundColor(crossterm::style::Color::DarkGrey),
406-
Print(" ↓ more"),
416+
Print(format!("{pad} ↓ more")),
407417
Print(NEWLINE),
408418
ResetColor
409419
)?;
@@ -420,7 +430,7 @@ fn render_picker(
420430
stdout,
421431
Print(NEWLINE),
422432
SetForegroundColor(crossterm::style::Color::DarkGrey),
423-
Print(" "),
433+
Print(format!("{pad} ")),
424434
Print(no_match),
425435
Print(NEWLINE),
426436
ResetColor
@@ -430,9 +440,12 @@ fn render_picker(
430440
stdout.flush()
431441
}
432442

433-
fn compute_viewport_size(terminal_rows: usize, total_commands: usize) -> usize {
434-
// Header + instructions + query + spacing takes ~9 rows.
435-
terminal_rows.saturating_sub(9).clamp(6, total_commands.max(6))
443+
fn compute_viewport_size(
444+
terminal_rows: usize,
445+
total_commands: usize,
446+
header_overhead: usize,
447+
) -> usize {
448+
terminal_rows.saturating_sub(header_overhead).clamp(6, total_commands.max(6))
436449
}
437450

438451
fn align_viewport(current_start: usize, selected_index: usize, viewport_size: usize) -> usize {
@@ -572,9 +585,12 @@ mod tests {
572585

573586
#[test]
574587
fn viewport_size_is_clamped() {
575-
assert_eq!(compute_viewport_size(12, 30), 6);
576-
assert_eq!(compute_viewport_size(24, 30), 15);
577-
assert_eq!(compute_viewport_size(100, 8), 8);
588+
assert_eq!(compute_viewport_size(12, 30, 9), 6);
589+
assert_eq!(compute_viewport_size(24, 30, 9), 15);
590+
assert_eq!(compute_viewport_size(100, 8, 9), 8);
591+
// Warp adds 1 extra row of overhead
592+
assert_eq!(compute_viewport_size(12, 30, 10), 6);
593+
assert_eq!(compute_viewport_size(24, 30, 10), 14);
578594
}
579595

580596
#[test]

crates/vite_shared/src/header.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
//! - ANSI palette queries for blue/magenta with timeout
77
//! - Gradient/fade generation and RGB ANSI coloring
88
9-
use std::{io::IsTerminal, sync::OnceLock};
9+
use std::{
10+
io::IsTerminal,
11+
sync::{LazyLock, OnceLock},
12+
};
1013
#[cfg(unix)]
1114
use std::{
1215
io::Write,
@@ -31,6 +34,15 @@ const HEADER_SUFFIX_FADE_GAMMA: f64 = 1.35;
3134

3235
static HEADER_COLORS: OnceLock<HeaderColors> = OnceLock::new();
3336

37+
/// Whether the terminal is Warp, which does not respond to OSC color queries
38+
/// and renders alternate screen content flush against block edges.
39+
#[must_use]
40+
pub fn is_warp_terminal() -> bool {
41+
static IS_WARP: LazyLock<bool> =
42+
LazyLock::new(|| std::env::var("TERM_PROGRAM").as_deref() == Ok("WarpTerminal"));
43+
*IS_WARP
44+
}
45+
3446
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3547
struct Rgb(u8, u8, u8);
3648

@@ -184,6 +196,13 @@ fn query_terminal_colors(palette_indices: &[u8]) -> (Option<Rgb>, Vec<(u8, Rgb)>
184196
return (None, vec![]);
185197
}
186198

199+
// Warp terminal does not respond to OSC color queries in its block-mode
200+
// renderer. Sending the queries causes the process to appear stuck until
201+
// the user presses a key (which is consumed as a fake "response").
202+
if is_warp_terminal() {
203+
return (None, vec![]);
204+
}
205+
187206
let mut tty = match OpenOptions::new().read(true).write(true).open("/dev/tty") {
188207
Ok(file) => file,
189208
Err(_) => return (None, vec![]),

0 commit comments

Comments
 (0)