Skip to content

Commit 69b6343

Browse files
zachlloydoz-agent
andauthored
Add tab context metadata copy actions (#10120)
## Description Adds copy actions to the attached tab context menu so users can copy tab/session metadata directly from the same UI surface where that metadata is shown. The menu now includes copy actions for available, non-empty metadata when it makes sense for the current tab layout: - Horizontal tabs show `Copy tab title` only, matching the metadata visible in the horizontal tab strip. - Vertical tabs grouped by tabs show copy actions for the tab title plus available focused-session metadata such as branch, working directory, and pull request link. - Vertical tabs grouped by panes show copy actions sourced from the active pane, including `Copy pane title` instead of `Copy tab title`. All new copy actions use sentence casing, and unavailable metadata is omitted from the menu rather than shown disabled. ## Linked Issue - [ ] The linked issue is labeled `ready-to-spec` or `ready-to-implement`. - [x] Where appropriate, screenshots or a short video of the implementation are included below (especially for user-visible or UI changes). ## Screenshots / Videos - Loom demo: https://www.loom.com/share/b57f7bfebea44a15b2703140db88c758 ## Testing Added integration coverage for the main layout variants: - `test_tab_context_menu_copies_metadata` covers horizontal tabs. It verifies that only the tab title copy action is present, and that selecting it copies the tab title. - `test_vertical_tab_context_menu_copies_metadata` covers vertical tabs grouped by tabs. It verifies that the context menu can copy the branch, tab title, and working directory for the tab/focused session. - `test_vertical_pane_context_menu_copies_metadata` covers vertical tabs grouped by panes. It creates a split pane with different metadata and verifies that the menu copies the active pane branch, pane title, and working directory. Validation run: - `./script/run` (bundled and launched WarpLocal.app) - `./script/run` (bundled and launched WarpLocal.app) - `PATH=/tmp/warp-corepack-bin:$PATH /Users/zach/Projects/warp_3/target/debug/integration test_tab_context_menu_copies_metadata` - `PATH=/tmp/warp-corepack-bin:$PATH /Users/zach/Projects/warp_3/target/debug/integration test_vertical_tab_context_menu_copies_metadata` - `PATH=/tmp/warp-corepack-bin:$PATH /Users/zach/Projects/warp_3/target/debug/integration test_vertical_pane_context_menu_copies_metadata` - `cargo fmt --manifest-path /Users/zach/Projects/warp_3/Cargo.toml --all --check` - `PATH=/tmp/warp-corepack-bin:$PATH cargo nextest run --manifest-path /Users/zach/Projects/warp_3/Cargo.toml --no-fail-fast --workspace test_tab_context_menu_copies_metadata test_vertical_tab_context_menu_copies_metadata test_vertical_pane_context_menu_copies_metadata` - `PATH=/tmp/warp-corepack-bin:$PATH cargo clippy --manifest-path /Users/zach/Projects/warp_3/Cargo.toml --workspace --all-targets --all-features --tests -- -D warnings` Note: initial nextest/clippy attempts without temporary Corepack shims failed in `command-signatures-v2` because the global Yarn version is 1.22.22 while that crate requires Corepack/Yarn 4.0.1. Rerunning with temporary Corepack shims passed. ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode CHANGELOG-IMPROVEMENT: Added tab context menu actions to copy visible tab and pane metadata when available. Co-Authored-By: Oz <oz-agent@warp.dev> --------- Co-authored-by: Oz <oz-agent@warp.dev>
1 parent 27d8ef6 commit 69b6343

6 files changed

Lines changed: 533 additions & 7 deletions

File tree

app/src/tab.rs

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::editor::EditorView;
77
use crate::features::FeatureFlag;
88
use crate::launch_configs::launch_config::LaunchConfig;
99
use crate::menu::{MenuAction, MenuItem, MenuItemFields};
10-
use crate::pane_group::PaneGroup;
10+
use crate::pane_group::{PaneGroup, PaneId};
1111
use crate::terminal::model::terminal_model::ConversationTranscriptViewerStatus;
1212
use settings::Setting as _;
1313
use std::sync::Arc;
@@ -25,7 +25,9 @@ use crate::util::truncation::truncate_from_end;
2525

2626
use crate::window_settings::WindowSettings;
2727
use crate::workspace::sync_inputs::SyncedInputState;
28-
use crate::workspace::tab_settings::{TabCloseButtonPosition, TabSettings};
28+
use crate::workspace::tab_settings::{
29+
TabCloseButtonPosition, TabSettings, VerticalTabsDisplayGranularity,
30+
};
2931
use crate::workspace::{
3032
PaneViewLocator, TabBarDropTargetData, TabBarLocation, TabContextMenuAnchor, WorkspaceAction,
3133
};
@@ -192,6 +194,7 @@ impl TabData {
192194

193195
for section_items in [
194196
self.session_sharing_menu_items(index, ctx),
197+
self.copy_metadata_menu_items(pane_name_target, ctx),
195198
self.modify_tab_menu_items(index, tabs_len, pane_name_target, ctx),
196199
self.close_tab_menu_items(index, tabs_len, ctx),
197200
Self::save_config_menu_items(index),
@@ -286,6 +289,111 @@ impl TabData {
286289
menu_items
287290
}
288291

292+
fn copyable_pane_title(
293+
pane_group: &PaneGroup,
294+
pane_id: PaneId,
295+
ctx: &AppContext,
296+
) -> Option<String> {
297+
pane_group.pane_by_id(pane_id).and_then(|pane| {
298+
let configuration = pane.pane_configuration();
299+
let configuration = configuration.as_ref(ctx);
300+
let title = configuration
301+
.custom_vertical_tabs_title()
302+
.unwrap_or_else(|| configuration.title());
303+
Self::copyable_metadata_value(Some(title.to_string()))
304+
})
305+
}
306+
307+
fn copy_metadata_menu_items(
308+
&self,
309+
pane_name_target: Option<PaneNameMenuTarget>,
310+
ctx: &AppContext,
311+
) -> Vec<MenuItem<WorkspaceAction>> {
312+
let pane_group = self.pane_group.as_ref(ctx);
313+
let mut menu_items = vec![];
314+
let tab_title = Self::copyable_metadata_value(Some(pane_group.display_title(ctx)));
315+
if !uses_vertical_tabs(ctx) {
316+
Self::push_copy_metadata_menu_item(&mut menu_items, "Copy tab title", tab_title);
317+
return menu_items;
318+
}
319+
320+
let vertical_tabs_display_granularity = *TabSettings::as_ref(ctx)
321+
.vertical_tabs_display_granularity
322+
.value();
323+
let (title_label, title, terminal_view) = if matches!(
324+
vertical_tabs_display_granularity,
325+
VerticalTabsDisplayGranularity::Panes
326+
) {
327+
let pane_id = pane_name_target
328+
.filter(|target| self.pane_group.id() == target.locator.pane_group_id)
329+
.and_then(|target| {
330+
pane_group
331+
.pane_by_id(target.locator.pane_id)
332+
.map(|_| target.locator.pane_id)
333+
})
334+
.unwrap_or_else(|| pane_group.focused_pane_id(ctx));
335+
(
336+
"Copy pane title",
337+
Self::copyable_pane_title(pane_group, pane_id, ctx),
338+
pane_group.terminal_view_from_pane_id(pane_id, ctx),
339+
)
340+
} else {
341+
let terminal_view = pane_name_target
342+
.filter(|target| self.pane_group.id() == target.locator.pane_group_id)
343+
.and_then(|target| {
344+
pane_group.terminal_view_from_pane_id(target.locator.pane_id, ctx)
345+
})
346+
.or_else(|| pane_group.focused_session_view(ctx));
347+
("Copy tab title", tab_title, terminal_view)
348+
};
349+
350+
if let Some(terminal_view) = terminal_view {
351+
let terminal_view = terminal_view.as_ref(ctx);
352+
Self::push_copy_metadata_menu_item(
353+
&mut menu_items,
354+
"Copy branch",
355+
Self::copyable_metadata_value(terminal_view.current_git_branch(ctx)),
356+
);
357+
Self::push_copy_metadata_menu_item(&mut menu_items, title_label, title);
358+
Self::push_copy_metadata_menu_item(
359+
&mut menu_items,
360+
"Copy working directory",
361+
Self::copyable_metadata_value(
362+
terminal_view
363+
.pwd()
364+
.or_else(|| terminal_view.display_working_directory(ctx)),
365+
),
366+
);
367+
Self::push_copy_metadata_menu_item(
368+
&mut menu_items,
369+
"Copy pull request link",
370+
Self::copyable_metadata_value(terminal_view.current_pull_request_url(ctx)),
371+
);
372+
} else {
373+
Self::push_copy_metadata_menu_item(&mut menu_items, title_label, title);
374+
}
375+
376+
menu_items
377+
}
378+
379+
fn push_copy_metadata_menu_item(
380+
menu_items: &mut Vec<MenuItem<WorkspaceAction>>,
381+
label: &'static str,
382+
value: Option<String>,
383+
) {
384+
if let Some(value) = value {
385+
menu_items.push(
386+
MenuItemFields::new(label)
387+
.with_on_select_action(WorkspaceAction::CopyTextToClipboard(value))
388+
.into_item(),
389+
);
390+
}
391+
}
392+
393+
fn copyable_metadata_value(value: Option<String>) -> Option<String> {
394+
value.filter(|value| !value.trim().is_empty())
395+
}
396+
289397
fn modify_tab_menu_items(
290398
&self,
291399
index: usize,

crates/integration/src/bin/integration.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,9 @@ fn register_tests() -> HashMap<&'static str, BoxedBuilderFn> {
336336
register_test!(test_context_chips_prompt_at_bootstrap);
337337

338338
register_test!(test_active_session_follows_focus);
339+
register_test!(test_tab_context_menu_copies_metadata);
340+
register_test!(test_vertical_tab_context_menu_copies_metadata);
341+
register_test!(test_vertical_pane_context_menu_copies_metadata);
339342

340343
register_test!(test_focus_panes_on_hover);
341344

0 commit comments

Comments
 (0)