fix(tui): scope context menu actions to sidebar
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ContextMenuEntry> {
|
||||
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<Co
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(filtered_cell_index) = transcript_cell_index_from_mouse(app, mouse) {
|
||||
if !on_sidebar && let Some(filtered_cell_index) = transcript_cell_index_from_mouse(app, mouse) {
|
||||
let cell_index = app.original_cell_index_for_rendered(filtered_cell_index);
|
||||
|
||||
let target = detail_target_label(app, cell_index)
|
||||
@@ -789,6 +800,11 @@ pub(crate) fn handle_context_menu_action(app: &mut App, action: ContextMenuActio
|
||||
ContextMenuAction::Paste => {
|
||||
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<String> {
|
||||
|
||||
#[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();
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user