diff --git a/CHANGELOG.md b/CHANGELOG.md index b7d238de..2c2897ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **ACP registry auth metadata (#1447).** The ACP stdio adapter now advertises terminal authentication setup in `initialize.authMethods`, matching the registry's validation requirement. +- **Sidebar context menus (#3065).** Right-clicking the sidebar no longer shows + `Paste`; clickable sidebar rows now offer their row command as the first + context action. - **Cursor-style activity metadata rows (#3146).** Dense successful tool-run summaries now render as a single muted `Explored ...` / `Updated metadata` row, include short command-family labels for successful generic verifier diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index bf04d649..04ccbdd9 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **ACP registry auth metadata (#1447).** The ACP stdio adapter now advertises terminal authentication setup in `initialize.authMethods`, matching the registry's validation requirement. +- **Sidebar context menus (#3065).** Right-clicking the sidebar no longer shows + `Paste`; clickable sidebar rows now offer their row command as the first + context action. - **Cursor-style activity metadata rows (#3146).** Dense successful tool-run summaries now render as a single muted `Explored ...` / `Updated metadata` row, include short command-family labels for successful generic verifier diff --git a/crates/tui/src/tui/mouse_ui.rs b/crates/tui/src/tui/mouse_ui.rs index 0cda7ba9..ea197275 100644 --- a/crates/tui/src/tui/mouse_ui.rs +++ b/crates/tui/src/tui/mouse_ui.rs @@ -660,14 +660,25 @@ pub(crate) fn open_context_menu(app: &mut App, mouse: MouseEvent) { pub(crate) fn build_context_menu_entries(app: &App, mouse: MouseEvent) -> Vec { let mut entries = Vec::new(); + let on_sidebar = mouse_hits_rect(mouse, app.viewport.last_sidebar_area); - // Paste first — the most common action when right-clicking in the - // composer after copying text from the output area. - entries.push(ContextMenuEntry { - label: app.tr(MessageId::CtxMenuPaste).to_string(), - description: app.tr(MessageId::CtxMenuPasteDesc).to_string(), - action: ContextMenuAction::Paste, - }); + if on_sidebar { + if let Some(command) = sidebar_click_action(app, mouse) { + entries.push(ContextMenuEntry { + label: "Run".to_string(), + description: command.clone(), + action: ContextMenuAction::ExecuteCommand { command }, + }); + } + } else { + // Paste first — the most common action when right-clicking in the + // composer or transcript after copying text from the output area. + entries.push(ContextMenuEntry { + label: app.tr(MessageId::CtxMenuPaste).to_string(), + description: app.tr(MessageId::CtxMenuPasteDesc).to_string(), + action: ContextMenuAction::Paste, + }); + } if selection_has_content(app) { entries.push(ContextMenuEntry { @@ -687,7 +698,7 @@ pub(crate) fn build_context_menu_entries(app: &App, mouse: MouseEvent) -> Vec { app.paste_from_clipboard(); } + ContextMenuAction::ExecuteCommand { command } => { + app.input = command; + app.status_message = Some("Command staged in composer".to_string()); + app.needs_redraw = true; + } ContextMenuAction::OpenCommandPalette => { app.view_stack .push(CommandPaletteView::new(build_command_palette_entries( @@ -1026,9 +1042,10 @@ pub(crate) fn selection_to_text(app: &App) -> Option { #[cfg(test)] mod tests { - use super::sidebar_click_action; + use super::{build_context_menu_entries, sidebar_click_action}; use crate::config::Config; use crate::tui::app::{App, SidebarHoverRow, SidebarHoverSection, TuiOptions}; + use crate::tui::views::ContextMenuAction; use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; use ratatui::layout::Rect; use std::path::PathBuf; @@ -1078,6 +1095,75 @@ mod tests { } } + fn right_click(column: u16, row: u16) -> MouseEvent { + MouseEvent { + kind: MouseEventKind::Down(MouseButton::Right), + column, + row, + modifiers: KeyModifiers::NONE, + } + } + + #[test] + fn context_menu_keeps_paste_first_outside_sidebar() { + let mut app = create_test_app(); + app.viewport.last_sidebar_area = Some(Rect::new(60, 4, 20, 6)); + + let entries = build_context_menu_entries(&app, right_click(10, 4)); + + assert!(matches!( + entries.first().map(|entry| &entry.action), + Some(ContextMenuAction::Paste) + )); + } + + #[test] + fn sidebar_context_menu_omits_paste_without_row_action() { + let mut app = create_test_app(); + app.viewport.last_sidebar_area = Some(Rect::new(60, 4, 20, 6)); + app.sidebar_hover.sections.push(SidebarHoverSection { + content_area: Rect::new(60, 4, 20, 6), + lines: vec!["header".to_string()], + rows: vec![hover_row(4, None)], + }); + + let entries = build_context_menu_entries(&app, right_click(65, 4)); + + assert!( + !entries + .iter() + .any(|entry| matches!(entry.action, ContextMenuAction::Paste)), + "sidebar menu should not offer paste: {entries:?}" + ); + } + + #[test] + fn sidebar_context_menu_runs_clickable_row_action() { + let mut app = create_test_app(); + app.viewport.last_sidebar_area = Some(Rect::new(60, 4, 20, 6)); + app.sidebar_hover.sections.push(SidebarHoverSection { + content_area: Rect::new(60, 4, 20, 6), + lines: vec!["job row".to_string()], + rows: vec![hover_row(4, Some("/jobs show shell_x"))], + }); + + let entries = build_context_menu_entries(&app, right_click(65, 4)); + + let first = entries.first().expect("sidebar row should have menu"); + assert_eq!(first.label, "Run"); + assert_eq!(first.description, "/jobs show shell_x"); + assert!(matches!( + &first.action, + ContextMenuAction::ExecuteCommand { command } if command == "/jobs show shell_x" + )); + assert!( + !entries + .iter() + .any(|entry| matches!(entry.action, ContextMenuAction::Paste)), + "clickable sidebar menu should not offer paste: {entries:?}" + ); + } + #[test] fn sidebar_click_resolves_row_actions_inside_section() { let mut app = create_test_app(); diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 3b18eafd..db4250e1 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -135,7 +135,7 @@ use super::slash_menu::{ apply_slash_menu_selection, partial_inline_skill_mention_at_cursor, try_autocomplete_slash_command, visible_slash_menu_entries, }; -use super::views::{ConfigView, HelpView, ModalKind, ViewEvent}; +use super::views::{ConfigView, ContextMenuAction, HelpView, ModalKind, ViewEvent}; use super::widgets::pending_input_preview::{ContextPreviewItem, PendingInputPreview}; use super::widgets::{ChatWidget, ComposerWidget, HeaderData, HeaderWidget, Renderable}; @@ -8146,9 +8146,24 @@ async fn handle_view_events( app.status_message = Some("Backtrack canceled".to_string()); app.needs_redraw = true; } - ViewEvent::ContextMenuSelected { action } => { - handle_context_menu_action(app, action); + ViewEvent::ContextMenuSelected { + action: ContextMenuAction::ExecuteCommand { command }, + } => { + if execute_command_input( + terminal, + app, + engine_handle, + task_manager, + config, + &mut *web_config_session, + &command, + ) + .await? + { + return Ok(true); + } } + ViewEvent::ContextMenuSelected { action } => handle_context_menu_action(app, action), } } diff --git a/crates/tui/src/tui/views/mod.rs b/crates/tui/src/tui/views/mod.rs index b59a56b4..9b513751 100644 --- a/crates/tui/src/tui/views/mod.rs +++ b/crates/tui/src/tui/views/mod.rs @@ -76,6 +76,10 @@ pub enum ContextMenuAction { }, /// Show all currently hidden cells. ShowAllHidden, + /// Execute a slash command associated with a contextual UI row. + ExecuteCommand { + command: String, + }, } #[derive(Debug, Clone)]