diff --git a/crates/tui/src/localization.rs b/crates/tui/src/localization.rs index 3dfec9a9..97ffc950 100644 --- a/crates/tui/src/localization.rs +++ b/crates/tui/src/localization.rs @@ -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 => { diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 62df492b..be17a2d9 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -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()); diff --git a/crates/tui/src/tui/views/mod.rs b/crates/tui/src/tui/views/mod.rs index d69ccf3f..ea8fafd5 100644 --- a/crates/tui/src/tui/views/mod.rs +++ b/crates/tui/src/tui/views/mod.rs @@ -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 {