Port recent Warp terminal and UI improvements

Improve tab-group search, rename focus, history navigation, shell prompt and
completion handling, emoji width promotion, and macOS clipboard support.
Guard unavailable settings/model-selector/undo-close views and forward the
MCP OAuth callback issuer through the existing authentication flow.

Adapt local changes from 14 upstream Warp commits through f571865ca3f2,
reviewed from the latest 200 commits. Preserve Galaxy provider lifecycle,
permissions, storage, and branding; exclude hosted/cloud infrastructure.

Fix existing boolean-assertion lints and use stronger optimization with four
code-generation units for the app test target to fit macOS unwind offsets.
Production and dependency compilation settings remain unchanged.

Validation: two clean builds and a final build; both prescribed Clippy
commands with -D warnings; formatting, shell syntax and completion cleanup;
167 focused regression tests. Galaxy was not launched.
This commit is contained in:
2026-10-06 14:48:09 -05:00
parent d6da089862
commit 75d4965385
23 changed files with 1142 additions and 188 deletions
+77 -24
View File
@@ -1132,6 +1132,10 @@ pub enum InputAction {
CtrlR,
CtrlD,
Up,
/// Deferred so Up does not update InlineHistoryMenuView while it is already checked out.
SelectPreviousInlineHistoryItem,
/// Deferred so Down does not update InlineHistoryMenuView while it is already checked out.
SelectNextInlineHistoryItem,
PageUp,
PageDown,
ClearScreen,
@@ -2741,7 +2745,9 @@ impl Input {
render_decorator_elements: Some(Box::new(
move |app| -> EditorDecoratorElements {
let terminal_model = model_clone.lock();
let active_block = terminal_model.block_list().active_block();
let prompt_block = terminal_model
.prompt_block()
.unwrap_or_else(|| terminal_model.block_list().active_block());
let mut editor_decorator_elements = EditorDecoratorElements::default();
@@ -2771,7 +2777,7 @@ impl Input {
editor_decorator_elements.left_notch = lprompt_bottom;
editor_decorator_elements.right_notch = rprompt;
editor_decorator_elements.right_notch_offset_px = Some(
active_block.rprompt_render_offset(
prompt_block.rprompt_render_offset(
&input_render_state_model_handle_clone
.as_ref(app)
.size_info,
@@ -4963,6 +4969,9 @@ impl Input {
let has_input = !self.editor.as_ref(ctx).buffer_text(ctx).is_empty();
let should_clear_prompt_for_search =
has_input && FeatureFlag::RestorePromptOnInlineModelSelectorSearch.is_enabled();
if !ctx.is_window_open(self.inline_model_selector_view.window_id(ctx)) {
return;
}
self.inline_model_selector_view.update(ctx, |view, ctx| {
if has_input && !should_clear_prompt_for_search {
view.set_filter_results_by_input(false);
@@ -8743,6 +8752,44 @@ impl Input {
ctx.notify();
}
fn select_previous_inline_history_item(&mut self, ctx: &mut ViewContext<Self>) {
if !self
.suggestions_mode_model
.as_ref(ctx)
.is_inline_history_menu()
{
return;
}
if self.is_cloud_mode_input_v2_composing(ctx) {
if let Some(view) = self.cloud_mode_v2_history_menu_view.clone() {
view.update(ctx, |view, ctx| view.select_up(ctx));
}
} else {
self.inline_history_menu_view
.update(ctx, |view, ctx| view.select_up(ctx));
}
}
fn select_next_inline_history_item(&mut self, ctx: &mut ViewContext<Self>) {
if !self
.suggestions_mode_model
.as_ref(ctx)
.is_inline_history_menu()
{
return;
}
if self.is_cloud_mode_input_v2_composing(ctx) {
if let Some(view) = self.cloud_mode_v2_history_menu_view.clone() {
view.update(ctx, |view, ctx| view.select_down(ctx));
}
} else {
self.inline_history_menu_view
.update(ctx, |view, ctx| view.select_down(ctx));
}
}
fn editor_up(&mut self, ctx: &mut ViewContext<Self>) {
if self.should_show_auth_secret_ftux(ctx) {
if let Some(ftux_view) = self.auth_secret_ftux_view().cloned() {
@@ -8848,17 +8895,7 @@ impl Input {
true
}
InputSuggestionsMode::InlineHistoryMenu { .. } => {
if self.is_cloud_mode_input_v2_composing(ctx) {
if let Some(view) = self.cloud_mode_v2_history_menu_view.clone() {
view.update(ctx, |view, ctx| {
view.select_up(ctx);
});
}
} else {
self.inline_history_menu_view.update(ctx, |view, ctx| {
view.select_up(ctx);
});
}
ctx.dispatch_typed_action_deferred(InputAction::SelectPreviousInlineHistoryItem);
true
}
InputSuggestionsMode::IndexedReposMenu => {
@@ -9249,17 +9286,7 @@ impl Input {
.as_ref(ctx)
.is_inline_history_menu()
{
if self.is_cloud_mode_input_v2_composing(ctx) {
if let Some(view) = self.cloud_mode_v2_history_menu_view.clone() {
view.update(ctx, |view, ctx| {
view.select_down(ctx);
});
}
} else {
self.inline_history_menu_view.update(ctx, |view, ctx| {
view.select_down(ctx);
});
}
ctx.dispatch_typed_action_deferred(InputAction::SelectNextInlineHistoryItem);
return;
}
@@ -11985,6 +12012,28 @@ impl Input {
}),
};
let suggestions = match suggestions {
Some(suggestions)
if suggestions.suggestions.is_empty()
&& use_native_shell_completions =>
{
completer::suggestions(
before_cursor_text.as_str(),
cursor_position,
session_env_vars.as_ref(),
CompleterOptions {
match_strategy: matcher,
fallback_strategy: CompletionsFallbackStrategy::FilePaths,
suggest_file_path_completions_only: true,
parse_quotes_as_literals: input_type.is_ai(),
},
&completion_context,
)
.await
}
suggestions => suggestions,
};
(suggestions, completions_trigger, editor_snapshot)
},
|input, (suggestions, completions_trigger, editor_model), ctx| {
@@ -15733,6 +15782,10 @@ impl TypedActionView for Input {
match action {
InputAction::FocusInputBox => self.focus_input_box(ctx),
InputAction::Up => self.editor_up(ctx),
InputAction::SelectPreviousInlineHistoryItem => {
self.select_previous_inline_history_item(ctx)
}
InputAction::SelectNextInlineHistoryItem => self.select_next_inline_history_item(ctx),
InputAction::PageUp => self.editor_page_up(ctx),
InputAction::PageDown => self.editor_page_down(ctx),
InputAction::CtrlD => self.ctrl_d(ctx),
+123
View File
@@ -2269,6 +2269,129 @@ fn slash_compact_still_queues_while_in_progress() {
});
}
fn selected_inline_history_command(
history_menu: &warpui::ViewHandle<super::inline_history::InlineHistoryMenuView>,
app: &App,
) -> Option<String> {
history_menu.read(app, |view, ctx| {
view.model()
.as_ref(ctx)
.selected_item()
.and_then(|item| item.buffer_replacement_text().cloned())
})
}
#[test]
fn history_up_does_not_reenter_inline_history_menu_update() {
let _inline_history_menu = FeatureFlag::InlineHistoryMenu.override_enabled(true);
App::test((), |mut app| async move {
initialize_app(&mut app);
let terminal = add_window_with_bootstrapped_terminal(
&mut app,
Some(vec!["cd ~".to_string(), "ls".to_string()]),
None,
)
.await;
let input = terminal.read(&app, |view, _| view.input().clone());
let session_id = input
.read(&app, |input, _| input.active_block_session_id())
.expect("bootstrapped input should have a session id");
simulate_directory_for_completion(session_id, &terminal, &mut app, "/tmp");
input.update(&mut app, |input, ctx| {
input.open_inline_history_menu(ctx);
});
input.read(&app, |input, ctx| {
assert!(input
.suggestions_mode_model
.as_ref(ctx)
.is_inline_history_menu());
});
let inline_history_menu =
input.read(&app, |input, _| input.inline_history_menu_view.clone());
let result_count = inline_history_menu.read(&app, |view, ctx| view.result_count(ctx));
assert!(
result_count >= 2,
"inline history should list both seeded commands, got {result_count}"
);
let selected_before = selected_inline_history_command(&inline_history_menu, &app);
let buffer_before = input.read(&app, |input, ctx| input.buffer_text(ctx).to_owned());
assert_eq!(selected_before.as_deref(), Some("ls"));
assert_eq!(buffer_before, "ls");
inline_history_menu.update(&mut app, |_, ctx| {
input.update(ctx, |input, ctx| {
input.handle_action(&InputAction::Up, ctx);
});
});
input.read(&app, |input, ctx| {
assert!(
input
.suggestions_mode_model
.as_ref(ctx)
.is_inline_history_menu(),
"History/Up must still show inline history after a nested checkout"
);
assert_eq!(input.buffer_text(ctx), "cd ~");
});
assert_eq!(
selected_inline_history_command(&inline_history_menu, &app).as_deref(),
Some("cd ~")
);
inline_history_menu.update(&mut app, |_, ctx| {
input.update(ctx, |input, ctx| input.editor_down(ctx));
});
input.read(&app, |input, ctx| {
assert!(input
.suggestions_mode_model
.as_ref(ctx)
.is_inline_history_menu());
assert_eq!(input.buffer_text(ctx), "ls");
});
assert_eq!(
selected_inline_history_command(&inline_history_menu, &app).as_deref(),
Some("ls")
);
});
}
#[test]
fn editor_down_does_not_reenter_inline_history_menu_update() {
let _inline_history_menu = FeatureFlag::InlineHistoryMenu.override_enabled(true);
App::test((), |mut app| async move {
initialize_app(&mut app);
let terminal = add_window_with_bootstrapped_terminal(&mut app, None, None).await;
let input = terminal.read(&app, |view, _| view.input().clone());
input.update(&mut app, |input, ctx| {
input.open_inline_history_menu(ctx);
});
input.read(&app, |input, ctx| {
assert!(input
.suggestions_mode_model
.as_ref(ctx)
.is_inline_history_menu());
});
let inline_history_menu =
input.read(&app, |input, _| input.inline_history_menu_view.clone());
inline_history_menu.update(&mut app, |_, ctx| {
input.update(ctx, |input, ctx| input.editor_down(ctx));
});
input.read(&app, |input, ctx| {
assert!(
!input
.suggestions_mode_model
.as_ref(ctx)
.is_inline_history_menu(),
"Down on empty inline history must close the menu after the nested checkout ends"
);
});
});
}
#[test]
fn test_history_up_multiline() {
App::test((), |mut app| async move {
+7 -1
View File
@@ -2007,10 +2007,16 @@ impl Block {
/// Returns the offset, in pixels, at which the rprompt should be rendered
/// relative to the prompt.
pub fn rprompt_render_offset(&self, size: &SizeInfo) -> Vector2F {
let right_margin = match self.shell_host.as_ref().map(|shell| shell.shell_type) {
Some(ShellType::Fish | ShellType::PowerShell) => 0,
// TODO: Honor ZLE_RPROMPT_INDENT instead of assuming zsh's default.
Some(ShellType::Zsh | ShellType::Bash) | None => 1,
};
let rprompt_width_cells = self.rprompt_grid.grid_storage().max_cursor_point.col;
let rprompt_width_px = rprompt_width_cells as f32 * size.cell_width_px.as_f32();
Vector2F::new(
(self.prompt_grid_columns().saturating_sub(1) as f32 * size.cell_width_px().as_f32())
(self.prompt_grid_columns().saturating_sub(right_margin) as f32
* size.cell_width_px().as_f32())
- rprompt_width_px,
self.prompt_number_of_rows().saturating_sub(1) as f32 * size.cell_height_px().as_f32(),
)
+65 -41
View File
@@ -8,9 +8,9 @@ mod tab_stops;
use std::cmp::min;
use std::collections::HashMap;
use std::io;
use std::ops::Range;
use std::sync::Arc;
use std::{io, mem};
use base64::Engine as _;
use bounded_vec_deque::BoundedVecDeque;
@@ -208,7 +208,21 @@ impl ansi::Handler for GridHandler {
col = col.saturating_sub(1);
}
self.grid[row][col].push_zerowidth(c, /* log_long_grapheme_warnings */ true);
let old_cell_content_width = match self.grid[row][col].raw_content() {
CharOrStr::Str(s) => s.width(),
CharOrStr::Char(c) => match c.width() {
Some(width) => width,
None => {
return;
}
},
};
// A rejected append cannot change the cell's width, so there is no structural work to do.
if !self.grid[row][col].push_zerowidth(c, /* log_long_grapheme_warnings */ true) {
return;
}
let cell_content_width = match self.grid[row][col].raw_content() {
CharOrStr::Str(s) => s.width(),
// Note that we should never reach here since we are pushing a zerowidth character,
@@ -225,19 +239,29 @@ impl ansi::Handler for GridHandler {
// Bash and Fish support emoji variation selectors, but Zsh does not in bracketed paste
// mode. Specifically, this references sequences such as \0x2601\0xFE0F (☁️),
// which are commonly used in prompts e.g. GCloud prompt chip in Starship.
if cell_content_width == 2
if old_cell_content_width != cell_content_width
&& cell_content_width == 2
&& self.ansi_handler_state.supports_emoji_presentation_selector
{
// Current cursor cell contains a wide character (double-width).
self.grid[row][col].flags.insert(Flags::WIDE_CHAR);
let mut wide_cell = mem::take(&mut self.grid[row][col]);
if col + 1 == num_cols
&& !self.ansi_handler_state.mode.contains(TermMode::LINE_WRAP)
{
// Remove the selector rather than leave a wide grapheme without room for its spacer.
let popped = wide_cell.pop_zerowidth();
debug_assert_eq!(popped, Some(c));
self.grid[row][col] = wide_cell;
return;
}
wide_cell.flags.remove(
Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER | Flags::WRAPLINE,
);
let base_char = wide_cell.c;
// Insert spacer at the next cell.
self.write_at_cursor(cell::DEFAULT_CHAR)
.flags
.insert(Flags::WIDE_CHAR_SPACER);
// Update cursor appropriately before early-return.
self.advance_cursor_by_one_cell();
self.update_cursor(|cursor| {
cursor.point.col = col;
});
self.write_wide_char(base_char, |cell| *cell = wide_cell);
}
return;
}
@@ -275,37 +299,10 @@ impl ansi::Handler for GridHandler {
if width == 1 {
self.write_at_cursor(c);
self.advance_cursor_by_one_cell();
} else {
if self.grid.cursor().point.col + 1 >= num_cols {
if self.ansi_handler_state.mode.contains(TermMode::LINE_WRAP) {
// Insert placeholder before wide char if glyph does not fit in this row.
self.write_at_cursor(cell::DEFAULT_CHAR)
.flags
.insert(Flags::LEADING_WIDE_CHAR_SPACER);
self.wrapline();
} else {
// Prevent out of bounds crash when linewrapping is disabled.
self.move_cursor_forward(|cursor| {
cursor.input_needs_wrap = true;
});
return;
}
}
// Write full width glyph to current cursor cell.
self.write_at_cursor(c).flags.insert(Flags::WIDE_CHAR);
// Write spacer to cell following the wide glyph.
self.move_cursor_forward(|cursor| {
cursor.point.col += 1;
});
self.write_at_cursor(cell::DEFAULT_CHAR)
.flags
.insert(Flags::WIDE_CHAR_SPACER);
self.write_wide_char(c, |_| {});
}
self.advance_cursor_by_one_cell();
}
fn goto(&mut self, row: VisibleRow, column: usize) {
@@ -1470,6 +1467,33 @@ impl GridHandler {
});
}
fn write_wide_char(&mut self, c: char, update_wide_cell: impl FnOnce(&mut Cell)) {
let num_cols = self.columns();
if self.grid.cursor().point.col + 1 >= num_cols {
if !self.ansi_handler_state.mode.contains(TermMode::LINE_WRAP) {
self.move_cursor_forward(|cursor| {
cursor.input_needs_wrap = true;
});
return;
}
let leading_spacer = self.write_at_cursor(cell::DEFAULT_CHAR);
leading_spacer.flags.insert(Flags::LEADING_WIDE_CHAR_SPACER);
self.wrapline();
}
let wide_cell = self.write_at_cursor(c);
update_wide_cell(wide_cell);
wide_cell.flags.insert(Flags::WIDE_CHAR);
self.move_cursor_forward(|cursor| {
cursor.point.col += 1;
});
let spacer = self.write_at_cursor(cell::DEFAULT_CHAR);
spacer.flags.insert(Flags::WIDE_CHAR_SPACER);
self.advance_cursor_by_one_cell();
}
/// Insert a linebreak at the current cursor position.
#[inline]
pub(crate) fn wrapline(&mut self) {
@@ -2535,3 +2535,76 @@ fn test_full_grid_clear_resize_then_bounds_to_string_does_not_panic() {
);
}
}
#[test]
fn emoji_variation_selector_wraps_width_promotion_at_last_column() {
let mut grid = GridHandler::new_for_test(2, 4);
grid.input('a');
grid.input('b');
grid.input('c');
grid.input('\u{26A0}');
grid.input('\u{FE0F}');
let storage = grid.grid_storage();
assert_eq!(storage[VisibleRow(0)][3].c, '\0');
assert!(storage[VisibleRow(0)][3]
.flags
.contains(Flags::LEADING_WIDE_CHAR_SPACER));
assert!(storage[VisibleRow(0)][3].flags.contains(Flags::WRAPLINE));
assert!(matches!(
storage[VisibleRow(1)][0].content_for_display(),
CharOrStr::Str("⚠️")
));
assert!(storage[VisibleRow(1)][0].flags.contains(Flags::WIDE_CHAR));
assert!(storage[VisibleRow(1)][1]
.flags
.contains(Flags::WIDE_CHAR_SPACER));
assert_eq!(grid.cursor_point(), Point::new(1, 2));
}
#[test]
fn emoji_variation_selector_drops_width_promotion_when_line_wrap_is_disabled() {
let mut grid = GridHandler::new_for_test(2, 4);
grid.unset_mode(ansi::Mode::LineWrap);
grid.input('a');
grid.input('b');
grid.input('c');
grid.input('\u{26A0}');
grid.input('\u{FE0F}');
let storage = grid.grid_storage();
assert!(matches!(
storage[VisibleRow(0)][3].content_for_display(),
CharOrStr::Char('⚠')
));
assert!(!storage[VisibleRow(0)][3]
.flags
.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER));
assert_eq!(grid.cursor_point(), Point::new(0, 3));
}
#[test]
fn emoji_variation_selector_does_not_advance_already_wide_grapheme() {
let mut grid = GridHandler::new_for_test(2, 6);
grid.input('😀');
grid.input('\u{FE0F}');
grid.input('x');
let storage = grid.grid_storage();
assert!(matches!(
storage[VisibleRow(0)][0].content_for_display(),
CharOrStr::Str("😀️")
));
assert!(storage[VisibleRow(0)][0].flags.contains(Flags::WIDE_CHAR));
assert!(storage[VisibleRow(0)][1]
.flags
.contains(Flags::WIDE_CHAR_SPACER));
assert_eq!(storage[VisibleRow(0)][2].c, 'x');
assert!(!storage[VisibleRow(0)][2]
.flags
.contains(Flags::WIDE_CHAR_SPACER));
assert_eq!(grid.cursor_point(), Point::new(0, 3));
}