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
13 changes: 13 additions & 0 deletions app/src/terminal/shared_session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,19 @@ impl SharedSessionStatus {
!matches!(self, Self::NotShared)
}

/// Returns `true` when the session currently has a usable sharing
/// link the user can copy — i.e. when the share has fully
/// established (sharer side) or been joined (viewer side). Pending
/// and finished states return `false` so the right-click "Copy
/// session sharing link" menu item is not offered when the link
/// would either not exist yet or no longer be valid (GH#9736).
pub fn has_active_share_link(&self) -> bool {
matches!(
self,
Self::ActiveSharer | Self::ActiveViewer { .. }
)
}

pub fn as_keymap_context(&self) -> &'static str {
match self {
Self::NotShared => "SharedSessionStatus_NotShared",
Expand Down
41 changes: 40 additions & 1 deletion app/src/terminal/shared_session/mod_test.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use super::{decode_scrollback, SharedSessionScrollbackType};
use super::{decode_scrollback, SharedSessionScrollbackType, SharedSessionStatus};
use session_sharing_protocol::common::Role;
use session_sharing_protocol::sharer::SessionSourceType;

use crate::ai::blocklist::agent_view::AgentViewState;
use crate::assert_lines_approx_eq;
Expand Down Expand Up @@ -347,3 +349,40 @@ fn test_loading_scrollback_in_alt_screen() {
// Make sure we're in the alt screen.
assert!(model.is_alt_screen_active());
}

/// Regression test for GH#9736: the right-click "Copy session sharing link"
/// menu item must only be offered when a sharing URL actually exists. Pending
/// shares (which can include shares that failed to establish) and finished
/// viewer states leave no link to copy and previously surfaced the menu item
/// anyway, writing a stale or empty value to the clipboard.
#[test]
fn has_active_share_link_only_true_for_active_states() {
// States with a usable link.
assert!(SharedSessionStatus::ActiveSharer.has_active_share_link());
assert!(
SharedSessionStatus::ActiveViewer { role: Role::Reader }.has_active_share_link(),
"ActiveViewer (reader) must offer copy link"
);
assert!(
SharedSessionStatus::ActiveViewer {
role: Role::Executor,
}
.has_active_share_link(),
"ActiveViewer (executor) must offer copy link"
);

// States without a usable link — these are the ones that produced
// the GH#9736 regression.
assert!(!SharedSessionStatus::NotShared.has_active_share_link());
assert!(!SharedSessionStatus::SharePending.has_active_share_link());
assert!(
!SharedSessionStatus::SharePendingPreBootstrap {
source_type: SessionSourceType::default(),
}
.has_active_share_link(),
"SharePendingPreBootstrap must not offer copy link — \
this is the state a failed-to-share session sits in"
);
assert!(!SharedSessionStatus::ViewPending.has_active_share_link());
assert!(!SharedSessionStatus::FinishedViewer.has_active_share_link());
}
9 changes: 8 additions & 1 deletion app/src/terminal/view/pane_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,14 @@ impl BackingView for TerminalView {
let shared_session_status = model.shared_session_status();
let is_ambient_agent = self.is_ambient_agent_session(ctx);
if shared_session_status.is_sharer_or_viewer() {
if !is_ambient_agent {
// "Copy link" is gated on `has_active_share_link()` rather than
// the outer `is_sharer_or_viewer()` so that pending and failed
// states (which leave no usable URL) do not surface a copy
// entry — see GH#9736. The neighbouring "Stop sharing session"
// entry deliberately stays under `is_sharer_or_viewer()` /
// `is_sharer()` so the user can still cancel a stuck pending
// share.
if !is_ambient_agent && shared_session_status.has_active_share_link() {
items.push(
MenuItemFields::new("Copy link")
.with_on_select_action(TerminalAction::CopySharedSessionLink { source })
Expand Down
8 changes: 7 additions & 1 deletion app/src/terminal/view/shared_session/view_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1630,7 +1630,13 @@ impl TerminalView {
);
}

if model.shared_session_status().is_sharer_or_viewer() {
// Only offer "Copy session sharing link" when a link actually
// exists. Pending shares (SharePending / SharePendingPreBootstrap)
// have not yet produced a URL, and a session that finished or
// failed to share leaves no link to copy. Surfacing the menu
// entry in those states writes a stale or empty value to the
// clipboard — see GH#9736.
if model.shared_session_status().has_active_share_link() {
items.push(
MenuItemFields::new("Copy session sharing link")
.with_on_select_action(TerminalAction::CopySharedSessionLink {
Expand Down