aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/next/website/src/content/docs/ja/keyboard.mdx2
-rw-r--r--docs/next/website/src/content/docs/keyboard.mdx2
-rw-r--r--docs/next/website/src/content/docs/zh-cn/keyboard.mdx2
-rw-r--r--src/client/shell/actions.rs2
-rw-r--r--src/client/shell/copy_mode.rs22
-rw-r--r--src/client/shell/mouse.rs2
-rw-r--r--src/client/shell/state.rs104
-rw-r--r--src/client/shell/tests/copy.rs131
-rw-r--r--src/client/shell/tests/mouse_selection.rs39
-rw-r--r--src/client/shell/tests/popup_focus_projection.rs1
10 files changed, 200 insertions, 107 deletions
diff --git a/docs/next/website/src/content/docs/ja/keyboard.mdx b/docs/next/website/src/content/docs/ja/keyboard.mdx
index 24b554c8..97ce6b5c 100644
--- a/docs/next/website/src/content/docs/ja/keyboard.mdx
+++ b/docs/next/website/src/content/docs/ja/keyboard.mdx
@@ -66,6 +66,8 @@ Herdr はマウスネイティブです。キーバインドをひとつも覚ã�
`prefix+[` を押すと、フォーカス中のペインでコピーモードに入ります。`h/j/k/l`、tmux 形式の `w/b/e`、`{`/`}` で移動します。`/` または `?` で前方または後方のリテラル検索を開始し、`n` または `N` で同じ方向または逆方向に繰り返します。クエリに大文字が含まれる場合だけ大文字と小文字を区別します。`v` または Space で選択を開始し、`y` または Enter でコピーし、`q` または Esc でコピーせずに抜けます。Esc は終了する前に、アクティブな選択または検索を消します。コピーモードはペインのプロセスを停止しません。最下部では出力を追従し、履歴へ移動するとその位置を保ちます。マウスのドラッグ選択なら、コピーモードに入らずにそのままコピーできます。
+マウスのドラッグ選択と、`v`、Space、`V` で開始した選択は、出力や再描画が続いても維持されます。選択範囲の一部が表示領域の外にある場合も同様です。コピーするのは固定されたスナップショットではなく、その範囲に現在あるテキストです。ペインのサイズ変更や通常画面と代替画面の切り替えでは、引き続き選択が解除されます。検索結果とダブルクリックによる単語選択では、内容変更のチェックが維持されます。
+
## 何でも変更できる
プレフィックス自体を含め、すべてのバインドは設定可能です:
diff --git a/docs/next/website/src/content/docs/keyboard.mdx b/docs/next/website/src/content/docs/keyboard.mdx
index 84f78013..d20b34b3 100644
--- a/docs/next/website/src/content/docs/keyboard.mdx
+++ b/docs/next/website/src/content/docs/keyboard.mdx
@@ -66,6 +66,8 @@ The full keymap and the binding syntax live in the [keybinding reference](/docs/
Press `prefix+[` to enter copy mode for the focused pane. Use `h/j/k/l`, tmux-style `w/b/e` and big-word `W/B/E`, `{`/`}`, `PageUp`/`PageDown`, `ctrl+b`/`ctrl+f`, and `ctrl+u`/`ctrl+d` to move. Press `/` or `?` for forward or backward literal search, then `n` or `N` to repeat in the same or opposite direction. Search is case-insensitive unless the query contains an uppercase letter. Use `v` or Space to start a selection, `y` or Enter to copy it, and `q` or Esc to leave without copying. Esc clears an active selection or search before exiting. Copy mode does not pause the pane process: output remains live, follows at the bottom, and stays pinned when you navigate into history. The configured prefix keeps its normal meaning in copy mode; with the default prefix, `ctrl+b` enters prefix mode instead of paging up, so use a different prefix if you want `ctrl+b` for copy-mode page-up. Mouse drag-select copies without entering copy mode at all.
+Mouse drag selections and selections started with `v`, Space, or `V` remain active during output and redraws, including when part of the selected range is outside the viewport. Copy reads the current text in that range, not a frozen snapshot. Resizing the pane or switching between its normal and alternate screens still clears the selection. Search results and double-click word selections retain their content-change checks.
+
## Change anything
Every binding is configurable, including the prefix itself:
diff --git a/docs/next/website/src/content/docs/zh-cn/keyboard.mdx b/docs/next/website/src/content/docs/zh-cn/keyboard.mdx
index d7b0d297..da795378 100644
--- a/docs/next/website/src/content/docs/zh-cn/keyboard.mdx
+++ b/docs/next/website/src/content/docs/zh-cn/keyboard.mdx
@@ -66,6 +66,8 @@ Herdr 是鼠标原生的。你可以点击窗格、标签页、工作区和智èƒ
按 `prefix+[` 让聚焦的窗格进入复制模式。用 `h/j/k/l`、tmux 风格的 `w/b/e` 和 `{`/`}` 移动。按 `/` 或 `?` 向前或向后进行文本搜索,再用 `n` 或 `N` 按相同或相反方向重复搜索。查询包含大写字母时区分大小写,否则不区分。用 `v` 或空格开始选择,用 `y` 或回车复制,用 `q` 或 Esc 不复制直接退出。Esc 会先清除当前选择或搜索,然后才退出。复制模式不会暂停窗格进程:停留在底部时继续跟随输出,进入历史记录后保持当前位置。鼠标拖选可以直接复制,完全不用进入复制模式。
+鼠标拖选以及通过 `v`、空格或 `V` 开始的选择会在输出和重绘期间保持有效,即使部分选中范围位于可视区域之外。复制读取的是该范围内当前的文本,而不是冻结的快照。调整窗格大小或在普通屏幕与备用屏幕之间切换仍会清除选择。搜索结果和双击单词选择仍保留内容变化检查。
+
## 一切都可以改
每个绑定都可以配置,包括前缀键本身:
diff --git a/src/client/shell/actions.rs b/src/client/shell/actions.rs
index 48dab344..12d8cc16 100644
--- a/src/client/shell/actions.rs
+++ b/src/client/shell/actions.rs
@@ -269,7 +269,7 @@ impl ClientShellState {
.as_ref()
.and_then(|surface| surface.panes.iter().find(|pane| pane.pane_id == pane_id))
.map(|pane| pane.content_revision)
- // Read a manual mouse selection atomically from the live terminal. Output
+ // Read an explicit selection atomically from the live terminal. Output
// between the displayed frame and this request must not reject the copy.
.filter(|_| !live);
let (anchor, cursor) = selection.ordered_cells();
diff --git a/src/client/shell/copy_mode.rs b/src/client/shell/copy_mode.rs
index 9750e19e..07dc8d18 100644
--- a/src/client/shell/copy_mode.rs
+++ b/src/client/shell/copy_mode.rs
@@ -70,15 +70,18 @@ impl ClientShellState {
self.stop_selection_autoscroll();
self.selection_highlight_clear_deadline = None;
self.reset_copy_pipeline();
- let content_revision = self
+ let (content_revision, alternate_screen_active) = self
.pane_surface
.as_ref()
.and_then(|surface| surface.panes.iter().find(|pane| pane.pane_id == pane_id))
- .map_or(0, |pane| pane.content_revision);
+ .map_or((0, false), |pane| {
+ (pane.content_revision, pane.alternate_screen_active)
+ });
self.copy_mode = Some(ClientCopyModeState {
pane_id,
content_revision,
geometry: (hit.inner_rect.width, hit.inner_rect.height),
+ alternate_screen_active,
cursor,
offset_from_bottom: metrics.offset_from_bottom,
max_offset_from_bottom: metrics.max_offset_from_bottom,
@@ -814,12 +817,11 @@ impl ClientShellState {
}
pub(super) fn exit_copy_mode(&mut self, copy: bool, outcome: &mut ClientShellInput) {
- if copy
- && !self
- .selection
- .as_ref()
- .is_some_and(crate::selection::Selection::is_visible)
- {
+ let live_selection = self
+ .selection
+ .as_ref()
+ .is_some_and(crate::selection::Selection::is_visible);
+ if copy && !live_selection {
if let Some((pane_id, text_match)) = self.copy_mode.as_ref().and_then(|copy_mode| {
copy_mode
.search_current
@@ -843,7 +845,9 @@ impl ClientShellState {
.as_ref()
.is_some_and(crate::selection::Selection::is_visible)
{
- self.request_selection_copy(outcome, false);
+ // A visible explicit selection is live. If the fallback above supplied
+ // a search match, retain the revision that established its boundaries.
+ self.request_selection_copy(outcome, live_selection);
}
self.selection = None;
self.selection_highlight_clear_deadline = None;
diff --git a/src/client/shell/mouse.rs b/src/client/shell/mouse.rs
index 1828d7f6..0b9326d0 100644
--- a/src/client/shell/mouse.rs
+++ b/src/client/shell/mouse.rs
@@ -1712,7 +1712,7 @@ impl ClientShellState {
.as_mut()
.is_some_and(crate::selection::Selection::finish);
if copied && self.config.copy_on_select {
- self.request_selection_copy(outcome, false);
+ self.request_selection_copy(outcome, true);
self.selection = None;
} else if self
.selection
diff --git a/src/client/shell/state.rs b/src/client/shell/state.rs
index f61295c8..901c5a28 100644
--- a/src/client/shell/state.rs
+++ b/src/client/shell/state.rs
@@ -5,63 +5,6 @@ pub(super) const NEW_TAB_WIDTH: u16 = 3;
pub(super) const WORKSPACE_HEADER_ROWS: u16 = 2;
const ENDPOINT_ERROR_TIMEOUT_SECS: u64 = 5;
-fn pane_surface_row<'a>(
- surface: &'a PaneSurfaceFrame,
- pane: &crate::protocol::PaneSurfacePane,
- absolute_row: u32,
-) -> Option<&'a [crate::protocol::CellData]> {
- let viewport_top = pane
- .scroll
- .map(|scroll| {
- scroll
- .max_offset_from_bottom
- .saturating_sub(scroll.offset_from_bottom) as u32
- })
- .unwrap_or(0);
- let viewport_row = u16::try_from(absolute_row.checked_sub(viewport_top)?).ok()?;
- if viewport_row >= pane.inner_rect.height {
- return None;
- }
- let start = (usize::from(pane.inner_rect.y) + usize::from(viewport_row))
- * usize::from(surface.frame.width)
- + usize::from(pane.inner_rect.x);
- surface
- .frame
- .cells
- .get(start..start + usize::from(pane.inner_rect.width))
-}
-
-fn selection_cells_unchanged(
- selection: &crate::selection::Selection<String>,
- previous_surface: &PaneSurfaceFrame,
- previous_pane: &crate::protocol::PaneSurfacePane,
- next_surface: &PaneSurfaceFrame,
- next_pane: &crate::protocol::PaneSurfacePane,
-) -> bool {
- let ((start_row, start_col), (end_row, end_col)) = selection.ordered_cells();
- (start_row..=end_row).all(|row| {
- let first_col = if row == start_row { start_col } else { 0 };
- let last_col = if row == end_row {
- end_col
- } else {
- previous_pane.inner_rect.width.saturating_sub(1)
- };
- pane_surface_row(previous_surface, previous_pane, row)
- .zip(pane_surface_row(next_surface, next_pane, row))
- .and_then(|(previous, next)| {
- previous
- .get(usize::from(first_col)..=usize::from(last_col))
- .zip(next.get(usize::from(first_col)..=usize::from(last_col)))
- })
- .is_some_and(|(previous, next)| {
- previous
- .iter()
- .zip(next)
- .all(|(previous, next)| previous.symbol == next.symbol)
- })
- })
-}
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ClientShellKeybindingSource {
Local,
@@ -874,6 +817,7 @@ pub(super) struct ClientCopyModeState {
pub(super) pane_id: String,
pub(super) content_revision: u64,
pub(super) geometry: (u16, u16),
+ pub(super) alternate_screen_active: bool,
pub(super) cursor: crate::api::schema::PaneTextPoint,
pub(super) offset_from_bottom: usize,
pub(super) max_offset_from_bottom: usize,
@@ -1729,7 +1673,7 @@ impl ClientShellState {
Some(gesture) => Some(&gesture.pane_id),
None => self.selection.as_ref().map(|selection| &selection.pane_id),
};
- let selection_content_changed = selection_pane.is_some_and(|pane_id| {
+ let selection_invalidated = selection_pane.is_some_and(|pane_id| {
let Some(previous_surface) = self.pane_surface.as_ref() else {
return false;
};
@@ -1741,34 +1685,15 @@ impl ClientShellState {
let (Some(previous), Some(next)) = (previous, next) else {
return false;
};
- if previous.inner_rect.width != next.inner_rect.width
+ previous.inner_rect.width != next.inner_rect.width
|| previous.inner_rect.height != next.inner_rect.height
|| previous.alternate_screen_active != next.alternate_screen_active
- {
- return true;
- }
- if previous.content_revision == next.content_revision {
- return false;
- }
- match (&self.word_selection_gesture, &self.selection) {
- // Word gestures cache boundaries outside the selected cells too.
- (Some(_), _) => true,
- (None, Some(selection)) => {
- self.config.copy_on_select
- && (!previous.content_revision.is_multiple_of(2)
- || !next.content_revision.is_multiple_of(2)
- || !selection_cells_unchanged(
- selection,
- previous_surface,
- previous,
- &surface,
- next,
- ))
- }
- (None, None) => false,
- }
+ // Ordinary selections are live buffer ranges. Only word gestures
+ // cache content-dependent boundaries that output can invalidate.
+ || (self.word_selection_gesture.is_some()
+ && previous.content_revision != next.content_revision)
});
- if selection_content_changed {
+ if selection_invalidated {
self.word_selection_gesture = None;
self.selection = None;
self.stop_selection_autoscroll();
@@ -1795,19 +1720,22 @@ impl ClientShellState {
.find(|pane| pane.pane_id == copy_mode.pane_id)
{
let geometry = (pane.inner_rect.width, pane.inner_rect.height);
- if copy_mode.content_revision != pane.content_revision
- || copy_mode.geometry != geometry
- {
+ let coordinates_changed = copy_mode.geometry != geometry
+ || copy_mode.alternate_screen_active != pane.alternate_screen_active;
+ if copy_mode.content_revision != pane.content_revision || coordinates_changed {
copy_mode.content_revision = pane.content_revision;
copy_mode.geometry = geometry;
- copy_mode.selection = None;
+ copy_mode.alternate_screen_active = pane.alternate_screen_active;
+ if coordinates_changed {
+ copy_mode.selection = None;
+ invalidated_copy_pane = Some(copy_mode.pane_id.clone());
+ }
copy_mode.search_matches.clear();
copy_mode.search_total = 0;
copy_mode.search_current = None;
copy_mode.search_current_global = None;
copy_mode.search_generation = copy_mode.search_generation.saturating_add(1);
copy_mode.copy_after_search = false;
- invalidated_copy_pane = Some(copy_mode.pane_id.clone());
}
if let Some(scroll) = pane.scroll {
let actual_offset =
diff --git a/src/client/shell/tests/copy.rs b/src/client/shell/tests/copy.rs
index 8ee35f45..eda4da89 100644
--- a/src/client/shell/tests/copy.rs
+++ b/src/client/shell/tests/copy.rs
@@ -22,6 +22,7 @@ fn pasted_help_and_copy_queries_normalize_single_line_text() {
pane_id: "pane_1".into(),
content_revision: 0,
geometry: (80, 24),
+ alternate_screen_active: false,
cursor: crate::api::schema::PaneTextPoint { row: 0, col: 0 },
offset_from_bottom: 0,
max_offset_from_bottom: 0,
@@ -194,6 +195,7 @@ fn client_mouse_selection_highlights_and_copies_through_endpoint_extraction() {
if params.pane_id == "pane_1"
&& params.anchor == crate::api::schema::PaneTextPoint { row: 0, col: 0 }
&& params.cursor == crate::api::schema::PaneTextPoint { row: 0, col: 2 }
+ && params.content_revision.is_none()
));
let (repaint, actions) = state.handle_endpoint_result(
@@ -506,7 +508,7 @@ fn keyboard_copy_mode_owns_cursor_selection_copy_and_scroll_restore() {
action,
ClientShellAction::Endpoint { request, .. }
if matches!(&request.method, crate::api::schema::Method::PaneSelectionRead(params)
- if params.content_revision == Some(0))
+ if params.content_revision.is_none())
)));
assert!(copy.actions.iter().any(|action| matches!(
action,
@@ -520,6 +522,133 @@ fn keyboard_copy_mode_owns_cursor_selection_copy_and_scroll_restore() {
}
#[test]
+fn keyboard_selections_survive_output_and_copy_live_ranges() {
+ // Character and linewise selections have distinct anchor/range projections.
+ for selection_key in [b"v", b"V"] {
+ let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default()));
+ state.set_snapshot(Box::new(snapshot()));
+ let mut pane_surface = surface();
+ pane_surface.panes[0].scroll = Some(crate::protocol::PaneSurfaceScrollMetrics {
+ offset_from_bottom: 0,
+ max_offset_from_bottom: 0,
+ viewport_rows: 2,
+ });
+ state.set_pane_surface(pane_surface.clone());
+ state.compose(106, 20).expect("composed frame");
+ state.handle_input_bytes(b"\x02[");
+ state.handle_input_bytes(selection_key);
+ state.handle_input_bytes(b"k");
+ let range = state
+ .selection
+ .as_ref()
+ .expect("selected range")
+ .ordered_cells();
+
+ pane_surface.surface_revision += 1;
+ pane_surface.panes[0].content_revision += 2;
+ pane_surface.frame.cells[0].symbol = "X".into();
+ state.set_pane_surface(pane_surface);
+ assert_eq!(state.mode, ClientShellMode::Copy);
+ assert!(state.copy_mode.as_ref().unwrap().selection.is_some());
+ assert_eq!(
+ state
+ .selection
+ .as_ref()
+ .expect("retained range")
+ .ordered_cells(),
+ range
+ );
+
+ let copied = state.handle_input_bytes(b"y");
+ assert!(copied.actions.iter().any(|action| matches!(
+ action,
+ ClientShellAction::Endpoint { request, .. }
+ if matches!(&request.method, crate::api::schema::Method::PaneSelectionRead(params)
+ if params.content_revision.is_none()
+ && (params.anchor.row, params.anchor.col) == range.0
+ && (params.cursor.row, params.cursor.col) == range.1)
+ )));
+ assert_eq!(state.mode, ClientShellMode::Terminal);
+ assert!(state.selection.is_none());
+ assert!(state.copy_mode.is_none());
+ }
+}
+
+#[test]
+fn empty_keyboard_anchor_keeps_search_fallback_revision_guard() {
+ let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default()));
+ state.set_snapshot(Box::new(snapshot()));
+ let mut pane_surface = surface();
+ pane_surface.panes[0].scroll = Some(crate::protocol::PaneSurfaceScrollMetrics {
+ offset_from_bottom: 0,
+ max_offset_from_bottom: 0,
+ viewport_rows: 2,
+ });
+ state.set_pane_surface(pane_surface);
+ state.compose(106, 20).expect("composed frame");
+ state.handle_input_bytes(b"\x02[");
+ let search = state.handle_input_bytes(b"/LIVE\r");
+ let [ClientShellAction::Endpoint { request, .. }] = &search.actions[..] else {
+ panic!("search request");
+ };
+ let found = crate::api::schema::PaneTextRange {
+ start: crate::api::schema::PaneTextPoint { row: 0, col: 0 },
+ end: crate::api::schema::PaneTextPoint { row: 0, col: 3 },
+ };
+ state.handle_endpoint_result(
+ "boot-1",
+ &request.id,
+ Ok(copy_search_result(vec![found], Some(0))),
+ );
+ state.handle_input_bytes(b"v");
+ assert!(!state.selection.as_ref().unwrap().is_visible());
+ let copy = state.handle_input_bytes(b"y");
+ assert!(copy.actions.iter().any(|action| matches!(
+ action,
+ ClientShellAction::Endpoint { request, .. }
+ if matches!(&request.method, crate::api::schema::Method::PaneSelectionRead(params)
+ if params.anchor == found.start
+ && params.cursor == found.end
+ && params.content_revision == Some(0))
+ )));
+}
+
+#[test]
+fn keyboard_selection_does_not_return_after_resize_or_screen_switch() {
+ for screen_switch in [false, true] {
+ let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default()));
+ state.set_snapshot(Box::new(snapshot()));
+ let mut pane_surface = surface();
+ pane_surface.panes[0].scroll = Some(crate::protocol::PaneSurfaceScrollMetrics {
+ offset_from_bottom: 0,
+ max_offset_from_bottom: 0,
+ viewport_rows: 2,
+ });
+ state.set_pane_surface(pane_surface.clone());
+ state.compose(106, 20).expect("composed frame");
+ state.handle_input_bytes(b"\x02[");
+ state.handle_input_bytes(b"vk");
+ assert!(state.selection.is_some());
+ pane_surface.surface_revision += 1;
+ pane_surface.panes[0].content_revision += 2;
+ if screen_switch {
+ pane_surface.panes[0].alternate_screen_active = true;
+ } else {
+ pane_surface.panes[0].inner_rect.width -= 1;
+ }
+ state.set_pane_surface(pane_surface);
+ assert!(state.selection.is_none());
+ assert!(state.copy_mode.as_ref().unwrap().selection.is_none());
+ state.compose(106, 20).expect("changed frame");
+ state.handle_input_bytes(b"l");
+ assert!(
+ state.selection.is_none(),
+ "movement must not resurrect the old anchor"
+ );
+ }
+}
+
+#[test]
fn keyboard_copy_mode_content_motion_is_endpoint_backed_and_stale_safe() {
let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default()));
state.set_snapshot(Box::new(snapshot()));
diff --git a/src/client/shell/tests/mouse_selection.rs b/src/client/shell/tests/mouse_selection.rs
index ccb9b86d..ae4168c2 100644
--- a/src/client/shell/tests/mouse_selection.rs
+++ b/src/client/shell/tests/mouse_selection.rs
@@ -692,7 +692,7 @@ fn double_click_drag_autoscroll_keeps_absolute_word_anchor() {
}
#[test]
-fn pane_content_updates_preserve_active_selection_only_when_selected_cells_stay_stable() {
+fn pane_content_updates_preserve_live_ranges_until_geometry_or_screen_changes() {
let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default()));
state.set_snapshot(Box::new(snapshot()));
let surface_at = |surface_revision, content_revision, alternate_screen_active| {
@@ -740,17 +740,42 @@ fn pane_content_updates_preserve_active_selection_only_when_selected_cells_stay_
assert!(selection.is_visible());
assert_eq!(selection.ordered_cells(), ((12, 0), (12, 1)));
- state.selection = Some(crate::selection::Selection::absolute_anchor(
- "pane_1".to_owned(),
- (12, 0),
- ));
let mut replaced_surface = surface_at(3, 4, true);
replaced_surface.frame.cells[4].symbol = "X".into();
state.set_pane_surface(replaced_surface);
- assert!(state.selection.is_none());
+ assert_eq!(
+ state.selection.as_ref().unwrap().ordered_cells(),
+ ((12, 0), (12, 1))
+ );
+
+ // The selected row can leave the viewport during a drag. A later patch,
+ // including an in-flight content revision, must keep that absolute range.
+ let mut scrolled = surface_at(4, 5, true);
+ scrolled.panes[0]
+ .scroll
+ .as_mut()
+ .unwrap()
+ .offset_from_bottom = 2;
+ assert!(matches!(
+ state.apply_pane_surface_patch(crate::protocol::PaneSurfacePatch {
+ boot_id: scrolled.boot_id,
+ projection_revision: scrolled.projection_revision,
+ base_surface_revision: 3,
+ surface_revision: 4,
+ panes: scrolled.panes,
+ rows: vec![],
+ cursor: scrolled.frame.cursor,
+ }),
+ super::super::surface_patch::ClientPaneSurfacePatchOutcome::Applied(_)
+ ));
+ assert!(state.selection.as_ref().unwrap().is_in_progress());
+ assert_eq!(
+ state.selection.as_ref().unwrap().ordered_cells(),
+ ((12, 0), (12, 1))
+ );
for (surface_revision, content_revision, width, alternate_screen_active) in
- [(4, 6, 4, false), (5, 8, 3, false), (6, 9, 3, false)]
+ [(5, 6, 4, false), (6, 8, 3, false)]
{
state.selection = Some(crate::selection::Selection::absolute_anchor(
"pane_1".to_owned(),
diff --git a/src/client/shell/tests/popup_focus_projection.rs b/src/client/shell/tests/popup_focus_projection.rs
index aa44bf6d..25d3b334 100644
--- a/src/client/shell/tests/popup_focus_projection.rs
+++ b/src/client/shell/tests/popup_focus_projection.rs
@@ -107,6 +107,7 @@ fn modal_paste_target_requires_a_focused_editable_client_field() {
pane_id: "pane_1".into(),
content_revision: 0,
geometry: (80, 24),
+ alternate_screen_active: false,
cursor: crate::api::schema::PaneTextPoint { row: 0, col: 0 },
offset_from_bottom: 0,
max_offset_from_bottom: 0,