fix(tui): remove ShellControlView menu now unreachable after direct Ctrl+B
Ctrl+B backgrounds the foreground shell directly (#3032), leaving the two-step shell-control modal dead code that fails clippy -Dwarnings. Delete ShellControlView/ShellControlChoice, the ModalKind and ViewEvent variants, and open_shell_control; repoint the default-paste regression test at HelpView; update the Ctrl+B keybinding description in all locales to describe the new direct-background behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1373,7 +1373,7 @@ fn english(id: MessageId) -> &'static str {
|
||||
MessageId::KbSendDraft => "Send the current draft",
|
||||
MessageId::KbCloseMenu => "Close menu, cancel request, discard draft, or clear input",
|
||||
MessageId::KbCancelOrExit => "Cancel request, or exit when idle",
|
||||
MessageId::KbShellControls => "Open shell controls for a running foreground command",
|
||||
MessageId::KbShellControls => "Background the running foreground shell command",
|
||||
MessageId::KbExitEmpty => "Exit when input is empty",
|
||||
MessageId::KbCommandPalette => "Open the command palette",
|
||||
MessageId::KbFuzzyFilePicker => "Open the fuzzy file picker (insert @path on Enter)",
|
||||
@@ -1894,7 +1894,7 @@ fn vietnamese(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::KbSendDraft => "Gửi bản nháp hiện tại",
|
||||
MessageId::KbCloseMenu => "Đóng menu, hủy yêu cầu, hủy bản nháp hoặc xóa sạch đầu vào",
|
||||
MessageId::KbCancelOrExit => "Hủy yêu cầu, hoặc thoát khi rảnh",
|
||||
MessageId::KbShellControls => "Mở các điều khiển shell cho một lệnh đang chạy ở tiền cảnh",
|
||||
MessageId::KbShellControls => "Chuyển lệnh shell đang chạy ở tiền cảnh xuống nền",
|
||||
MessageId::KbExitEmpty => "Thoát khi khung nhập trống",
|
||||
MessageId::KbCommandPalette => "Mở bảng lệnh (command palette)",
|
||||
MessageId::KbFuzzyFilePicker => {
|
||||
@@ -2495,7 +2495,7 @@ fn japanese(id: MessageId) -> Option<&'static str> {
|
||||
"メニューを閉じる、リクエストをキャンセル、下書きを破棄、または入力をクリア"
|
||||
}
|
||||
MessageId::KbCancelOrExit => "リクエストをキャンセル、またはアイドル時に終了",
|
||||
MessageId::KbShellControls => "実行中のフォアグラウンドコマンドのシェル制御を開く",
|
||||
MessageId::KbShellControls => "実行中のフォアグラウンドコマンドをバックグラウンドへ移す",
|
||||
MessageId::KbExitEmpty => "入力が空の時に終了",
|
||||
MessageId::KbCommandPalette => "コマンドパレットを開く",
|
||||
MessageId::KbFuzzyFilePicker => "ファジーファイルピッカーを開く(Enter で @path を挿入)",
|
||||
@@ -2955,7 +2955,7 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::KbSendDraft => "发送当前草稿",
|
||||
MessageId::KbCloseMenu => "关闭菜单、取消请求、丢弃草稿或清空输入",
|
||||
MessageId::KbCancelOrExit => "取消请求,或空闲时退出",
|
||||
MessageId::KbShellControls => "打开正在运行的前台命令的 shell 控制",
|
||||
MessageId::KbShellControls => "将正在运行的前台命令转入后台",
|
||||
MessageId::KbExitEmpty => "输入框为空时退出",
|
||||
MessageId::KbCommandPalette => "打开命令面板",
|
||||
MessageId::KbFuzzyFilePicker => "打开模糊文件选择器(按 Enter 插入 @path)",
|
||||
@@ -3437,7 +3437,7 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> {
|
||||
"Fechar menu, cancelar requisição, descartar rascunho ou limpar entrada"
|
||||
}
|
||||
MessageId::KbCancelOrExit => "Cancelar requisição ou sair quando ocioso",
|
||||
MessageId::KbShellControls => "Abrir controles de shell para comando em primeiro plano",
|
||||
MessageId::KbShellControls => "Enviar o comando em primeiro plano para segundo plano",
|
||||
MessageId::KbExitEmpty => "Sair quando entrada vazia",
|
||||
MessageId::KbCommandPalette => "Abrir paleta de comandos",
|
||||
MessageId::KbFuzzyFilePicker => {
|
||||
@@ -3967,7 +3967,7 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> {
|
||||
"Cerrar menú, cancelar solicitud, descartar borrador o limpiar entrada"
|
||||
}
|
||||
MessageId::KbCancelOrExit => "Cancelar solicitud o salir cuando está inactivo",
|
||||
MessageId::KbShellControls => "Abrir controles de shell para comando en primer plano",
|
||||
MessageId::KbShellControls => "Enviar el comando en primer plano a segundo plano",
|
||||
MessageId::KbExitEmpty => "Salir cuando la entrada está vacía",
|
||||
MessageId::KbCommandPalette => "Abrir paleta de comandos",
|
||||
MessageId::KbFuzzyFilePicker => {
|
||||
|
||||
@@ -134,7 +134,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, ShellControlView, ViewEvent};
|
||||
use super::views::{ConfigView, HelpView, ModalKind, ViewEvent};
|
||||
use super::widgets::pending_input_preview::{ContextPreviewItem, PendingInputPreview};
|
||||
use super::widgets::{ChatWidget, ComposerWidget, HeaderData, HeaderWidget, Renderable};
|
||||
|
||||
@@ -7924,15 +7924,6 @@ async fn handle_view_events(
|
||||
ViewEvent::ContextMenuSelected { action } => {
|
||||
handle_context_menu_action(app, action);
|
||||
}
|
||||
ViewEvent::ShellControlBackground => {
|
||||
request_foreground_shell_background(app);
|
||||
}
|
||||
ViewEvent::ShellControlCancel => {
|
||||
app.backtrack.reset();
|
||||
engine_handle.cancel();
|
||||
mark_active_turn_cancelled_locally(app);
|
||||
app.status_message = Some("Request cancelled".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8779,16 +8770,6 @@ fn render_toast_stack_overlay(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn open_shell_control(app: &mut App) {
|
||||
if !app.is_loading || !active_foreground_shell_running(app) {
|
||||
app.status_message = Some("No foreground shell command to control".to_string());
|
||||
return;
|
||||
}
|
||||
|
||||
app.view_stack.push(ShellControlView::new());
|
||||
app.status_message = Some("Shell control opened".to_string());
|
||||
}
|
||||
|
||||
pub(crate) fn request_foreground_shell_background(app: &mut App) {
|
||||
if !app.is_loading || !active_foreground_shell_running(app) {
|
||||
app.status_message = Some("No foreground shell command to background".to_string());
|
||||
|
||||
@@ -38,7 +38,6 @@ pub enum ModalKind {
|
||||
FeedbackPicker,
|
||||
ThemePicker,
|
||||
ContextMenu,
|
||||
ShellControl,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -195,8 +194,6 @@ pub enum ViewEvent {
|
||||
ContextMenuSelected {
|
||||
action: ContextMenuAction,
|
||||
},
|
||||
ShellControlBackground,
|
||||
ShellControlCancel,
|
||||
/// Emitted by the pager (`c` / `y`) to copy its body to the system
|
||||
/// clipboard. The host handler writes via `app.clipboard` and surfaces a
|
||||
/// status message — modal views cannot reach `app` directly. `label` is
|
||||
@@ -363,142 +360,6 @@ impl fmt::Debug for ViewStack {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ShellControlChoice {
|
||||
Background,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
impl ShellControlChoice {
|
||||
fn event(self) -> ViewEvent {
|
||||
match self {
|
||||
ShellControlChoice::Background => ViewEvent::ShellControlBackground,
|
||||
ShellControlChoice::Cancel => ViewEvent::ShellControlCancel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ShellControlView {
|
||||
selected: ShellControlChoice,
|
||||
}
|
||||
|
||||
impl ShellControlView {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
selected: ShellControlChoice::Background,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle(&mut self) {
|
||||
self.selected = match self.selected {
|
||||
ShellControlChoice::Background => ShellControlChoice::Cancel,
|
||||
ShellControlChoice::Cancel => ShellControlChoice::Background,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for ShellControlView {
|
||||
fn kind(&self) -> ModalKind {
|
||||
ModalKind::ShellControl
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: KeyEvent) -> ViewAction {
|
||||
match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => ViewAction::Close,
|
||||
KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right | KeyCode::Tab => {
|
||||
self.toggle();
|
||||
ViewAction::None
|
||||
}
|
||||
KeyCode::Char('b') | KeyCode::Char('B') => {
|
||||
ViewAction::EmitAndClose(ViewEvent::ShellControlBackground)
|
||||
}
|
||||
KeyCode::Char('c') | KeyCode::Char('C') => {
|
||||
ViewAction::EmitAndClose(ViewEvent::ShellControlCancel)
|
||||
}
|
||||
KeyCode::Enter => ViewAction::EmitAndClose(self.selected.event()),
|
||||
_ => ViewAction::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
use ratatui::{
|
||||
style::Style,
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Clear, Padding, Paragraph, Widget},
|
||||
};
|
||||
|
||||
let popup_width = 62.min(area.width.saturating_sub(4));
|
||||
let popup_height = 11.min(area.height.saturating_sub(2));
|
||||
|
||||
let popup_area = Rect {
|
||||
x: (area.width - popup_width) / 2,
|
||||
y: (area.height - popup_height) / 2,
|
||||
width: popup_width,
|
||||
height: popup_height,
|
||||
};
|
||||
|
||||
Clear.render(popup_area, buf);
|
||||
|
||||
let option_line = |choice: ShellControlChoice, key: &'static str, label: &'static str| {
|
||||
let selected = self.selected == choice;
|
||||
let style = if selected {
|
||||
Style::default()
|
||||
.fg(palette::SELECTION_TEXT)
|
||||
.bg(palette::SELECTION_BG)
|
||||
} else {
|
||||
Style::default().fg(palette::TEXT_PRIMARY)
|
||||
};
|
||||
Line::from(vec![
|
||||
Span::styled(if selected { "> " } else { " " }, style),
|
||||
Span::styled(format!("{key:<3}"), style.bold()),
|
||||
Span::styled(label, style),
|
||||
])
|
||||
};
|
||||
|
||||
let lines = vec![
|
||||
Line::from(Span::styled(
|
||||
"Foreground shell command is still running.",
|
||||
Style::default().fg(palette::TEXT_PRIMARY),
|
||||
)),
|
||||
Line::from(""),
|
||||
option_line(
|
||||
ShellControlChoice::Background,
|
||||
"B",
|
||||
"Background - detach and keep the command running",
|
||||
),
|
||||
option_line(
|
||||
ShellControlChoice::Cancel,
|
||||
"C",
|
||||
"Cancel - stop the command and interrupt this turn",
|
||||
),
|
||||
];
|
||||
|
||||
let view = Paragraph::new(lines)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Line::from(vec![Span::styled(
|
||||
" Shell command ",
|
||||
Style::default().fg(palette::DEEPSEEK_BLUE).bold(),
|
||||
)]))
|
||||
.title_bottom(Line::from(Span::styled(
|
||||
" Enter select | Esc close ",
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
)))
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(palette::BORDER_COLOR))
|
||||
.style(Style::default().bg(palette::DEEPSEEK_INK))
|
||||
.padding(Padding::uniform(1)),
|
||||
)
|
||||
.style(Style::default().fg(palette::TEXT_PRIMARY));
|
||||
|
||||
view.render(popup_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ConfigScope {
|
||||
Session,
|
||||
@@ -2174,8 +2035,8 @@ fn truncate_view_text(text: &str, max_chars: usize) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
ConfigListItem, ConfigSection, ConfigView, ModalKind, ModalView, ShellControlView,
|
||||
ViewAction, ViewEvent, ViewStack, subagent_view_agents, truncate_view_text,
|
||||
ConfigListItem, ConfigSection, ConfigView, HelpView, ModalKind, ModalView, ViewAction,
|
||||
ViewEvent, ViewStack, subagent_view_agents, truncate_view_text,
|
||||
};
|
||||
use crate::config::Config;
|
||||
use crate::localization::Locale;
|
||||
@@ -2811,30 +2672,6 @@ base_url = "https://api.xiaomimimo.com/v1"
|
||||
assert_eq!(view.status.as_deref(), Some("Edit cancelled"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_control_view_defaults_to_background() {
|
||||
let mut view = ShellControlView::new();
|
||||
|
||||
let action = view.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert!(matches!(
|
||||
action,
|
||||
ViewAction::EmitAndClose(ViewEvent::ShellControlBackground)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_control_view_can_select_cancel() {
|
||||
let mut view = ShellControlView::new();
|
||||
|
||||
let action = view.handle_key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
|
||||
|
||||
assert!(matches!(
|
||||
action,
|
||||
ViewAction::EmitAndClose(ViewEvent::ShellControlCancel)
|
||||
));
|
||||
}
|
||||
|
||||
/// A modal that doesn't override `handle_paste` must report
|
||||
/// "not consumed" so the host can fall through to the composer.
|
||||
/// Regression: views/mod.rs previously inverted the boolean, swallowing
|
||||
@@ -2842,9 +2679,9 @@ base_url = "https://api.xiaomimimo.com/v1"
|
||||
#[test]
|
||||
fn default_modal_does_not_consume_paste() {
|
||||
let mut stack = ViewStack::new();
|
||||
stack.push(ShellControlView::new());
|
||||
stack.push(HelpView::new_for_locale(crate::localization::Locale::En));
|
||||
assert!(!stack.handle_paste("hello"));
|
||||
assert_eq!(stack.top_kind(), Some(ModalKind::ShellControl));
|
||||
assert_eq!(stack.top_kind(), Some(ModalKind::Help));
|
||||
}
|
||||
|
||||
fn buffer_text(buf: &Buffer, area: Rect) -> String {
|
||||
|
||||
Reference in New Issue
Block a user