From feb414ac79858fcb276ab92bcdf5637dd4f68b51 Mon Sep 17 00:00:00 2001 From: reidliu41 Date: Fri, 8 May 2026 16:59:54 +0800 Subject: [PATCH] Add feedback command for GitHub links Add a /feedback command for opening project feedback links. The command shows a picker when run without arguments and supports direct bug, feature, and security targets. Bug and feature options open the matching GitHub issue templates, while security opens the repository security policy. --- crates/tui/src/commands/feedback.rs | 277 ++++++++++++++++++++++++++ crates/tui/src/commands/mod.rs | 8 + crates/tui/src/localization.rs | 6 + crates/tui/src/tui/app.rs | 7 + crates/tui/src/tui/feedback_picker.rs | 243 ++++++++++++++++++++++ crates/tui/src/tui/mod.rs | 1 + crates/tui/src/tui/ui.rs | 57 +++++- crates/tui/src/tui/views/mod.rs | 1 + 8 files changed, 599 insertions(+), 1 deletion(-) create mode 100644 crates/tui/src/commands/feedback.rs create mode 100644 crates/tui/src/tui/feedback_picker.rs diff --git a/crates/tui/src/commands/feedback.rs b/crates/tui/src/commands/feedback.rs new file mode 100644 index 00000000..ac483e6a --- /dev/null +++ b/crates/tui/src/commands/feedback.rs @@ -0,0 +1,277 @@ +use super::CommandResult; +use crate::tui::app::{App, AppAction}; + +const SECURITY_POLICY_URL: &str = "https://github.com/Hmbown/DeepSeek-TUI/security/policy"; + +pub fn feedback(_app: &mut App, arg: Option<&str>) -> CommandResult { + let raw = arg.map(str::trim).unwrap_or(""); + if raw.is_empty() { + return CommandResult::action(AppAction::OpenFeedbackPicker); + } + if matches!(raw, "help" | "--help" | "-h") { + return CommandResult::message(feedback_help()); + } + + let kind = match parse_feedback_kind(raw) { + Some(parsed) => parsed, + None => { + return CommandResult::error( + "Unknown feedback type. Use `/feedback` to list feedback options.", + ); + } + }; + + if matches!(kind, FeedbackKind::Security) { + return CommandResult::with_message_and_action( + format!( + "Review the project's security policy before reporting a vulnerability.\n\n\ + Trying to open it in your browser. If that fails, open this URL manually:\n\n\ + {SECURITY_POLICY_URL}\n\n\ + Do not include sensitive security details in a public issue.", + ), + AppAction::OpenExternalUrl { + url: SECURITY_POLICY_URL.to_string(), + label: "GitHub security policy".to_string(), + }, + ); + } + + let url = kind.issue_url(); + let message = format!( + "Trying to open GitHub {} template in your browser. If that fails, open this URL manually:\n\n{}", + kind.label().to_ascii_lowercase(), + url, + ); + + CommandResult::with_message_and_action( + message, + AppAction::OpenExternalUrl { + url, + label: format!("GitHub {}", kind.label().to_ascii_lowercase()), + }, + ) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FeedbackKind { + Bug, + Feature, + Security, +} + +impl FeedbackKind { + fn label(self) -> &'static str { + match self { + Self::Bug => "Bug report", + Self::Feature => "Feature request", + Self::Security => "Security vulnerability", + } + } + + fn description(self) -> &'static str { + match self { + Self::Bug => "Report a problem or regression", + Self::Feature => "Suggest an idea or improvement", + Self::Security => "Review the security policy", + } + } + + fn issue_url_base(self) -> &'static str { + match self { + Self::Bug => "https://github.com/Hmbown/DeepSeek-TUI/issues/new?template=bug_report.md", + Self::Feature => { + "https://github.com/Hmbown/DeepSeek-TUI/issues/new?template=feature_request.md" + } + Self::Security => SECURITY_POLICY_URL, + } + } + + fn issue_url(self) -> String { + self.issue_url_base().to_string() + } +} + +fn feedback_help() -> String { + let rows = [ + ("1", FeedbackKind::Bug), + ("2", FeedbackKind::Feature), + ("3", FeedbackKind::Security), + ]; + let mut message = String::from("Choose a feedback type:\n\n"); + for (number, kind) in rows { + message.push_str(&format!( + "{number}. {} {}\n", + kind.label(), + kind.description() + )); + } + message.push_str("\nUsage:\n"); + for (number, kind) in rows { + message.push_str(&format!("/feedback {number} {}\n", kind.label())); + } + message.push_str("/feedback bug\n"); + message.push_str("/feedback feature\n"); + message.push_str("/feedback security\n"); + message +} + +fn parse_feedback_kind(input: &str) -> Option { + Some(match input.to_ascii_lowercase().as_str() { + "1" | "bug" | "bug-report" | "bug_report" => FeedbackKind::Bug, + "2" | "feature" | "feature-request" | "feature_request" | "enhancement" => { + FeedbackKind::Feature + } + "3" | "security" | "vulnerability" | "private" => FeedbackKind::Security, + _ => return None, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Config; + use crate::tui::app::{App, TuiOptions}; + use tempfile::TempDir; + + fn test_app() -> (App, TempDir) { + let tmpdir = TempDir::new().expect("tempdir"); + let workspace = tmpdir.path().to_path_buf(); + let options = TuiOptions { + model: "deepseek-v4-pro".to_string(), + workspace: workspace.clone(), + config_path: None, + config_profile: None, + allow_shell: false, + use_alt_screen: true, + use_mouse_capture: false, + use_bracketed_paste: true, + max_subagents: 1, + skills_dir: workspace.join("skills"), + memory_path: workspace.join("memory.md"), + notes_path: workspace.join("notes.txt"), + mcp_config_path: workspace.join("mcp.json"), + use_memory: false, + start_in_agent_mode: false, + skip_onboarding: true, + yolo: false, + resume_session_id: None, + initial_input: None, + }; + let mut app = App::new(options, &Config::default()); + app.current_session_id = Some("session-123".to_string()); + (app, tmpdir) + } + + fn external_url(result: &CommandResult) -> &str { + match result.action.as_ref() { + Some(AppAction::OpenExternalUrl { url, .. }) => url, + other => panic!("expected external URL action, got {other:?}"), + } + } + + #[test] + fn feedback_without_args_opens_feedback_picker() { + let (mut app, _tmpdir) = test_app(); + let result = feedback(&mut app, None); + assert_eq!(result.action, Some(AppAction::OpenFeedbackPicker)); + assert!(result.message.is_none()); + assert!(!result.is_error); + } + + #[test] + fn feedback_help_lists_feedback_types() { + let (mut app, _tmpdir) = test_app(); + let result = feedback(&mut app, Some("--help")); + let message = result.message.expect("feedback help"); + assert!(message.contains("1. Bug report")); + assert!(message.contains("2. Feature request")); + assert!(message.contains("3. Security vulnerability")); + assert!(!message.contains("Blank issue")); + assert!(message.contains("/feedback bug")); + assert!(!message.contains("")); + } + + #[test] + fn feedback_bug_opens_bug_template_url_without_prefilled_body() { + let (mut app, _tmpdir) = test_app(); + let result = feedback(&mut app, Some("bug")); + assert!(!result.is_error); + let message = result + .message + .as_deref() + .expect("feedback command returns guidance"); + let url = external_url(&result); + + assert!(message.contains("Trying to open GitHub bug report template")); + assert!(message.contains("open this URL manually")); + assert!(message.contains(url)); + assert!(url.contains("template=bug_report.md")); + assert!(!url.contains("title=")); + assert!(!url.contains("body=")); + } + + #[test] + fn feedback_feature_generates_feature_template_url() { + let (mut app, _tmpdir) = test_app(); + let result = feedback(&mut app, Some("2")); + let message = result + .message + .as_deref() + .expect("feedback command returns guidance"); + let url = external_url(&result); + assert!(message.contains("Trying to open GitHub feature request template")); + assert!(message.contains("open this URL manually")); + assert!(message.contains(url)); + assert!(url.contains("template=feature_request.md")); + assert!(!url.contains("title=")); + assert!(!url.contains("body=")); + } + + #[test] + fn feedback_template_urls_do_not_prefill_titles() { + let (mut app, _tmpdir) = test_app(); + let bug = feedback(&mut app, Some("bug")); + let feature = feedback(&mut app, Some("feature")); + + assert!(!external_url(&bug).contains("title=")); + assert!(!external_url(&feature).contains("title=")); + } + + #[test] + fn feedback_urls_use_template_only() { + let bug = FeedbackKind::Bug.issue_url(); + let feature = FeedbackKind::Feature.issue_url(); + + assert_eq!( + bug, + "https://github.com/Hmbown/DeepSeek-TUI/issues/new?template=bug_report.md" + ); + assert_eq!( + feature, + "https://github.com/Hmbown/DeepSeek-TUI/issues/new?template=feature_request.md" + ); + } + + #[test] + fn feedback_security_uses_security_policy() { + let (mut app, _tmpdir) = test_app(); + let result = feedback(&mut app, Some("security")); + let message = result + .message + .as_deref() + .expect("security feedback message"); + assert_eq!(external_url(&result), SECURITY_POLICY_URL); + assert!(message.contains(SECURITY_POLICY_URL)); + assert!(message.contains("Do not include sensitive security details")); + assert!(!message.contains("/issues/new")); + } + + #[test] + fn feedback_unknown_type_returns_error() { + let (mut app, _tmpdir) = test_app(); + let result = feedback(&mut app, Some("other thing")); + assert!(result.is_error); + let message = result.message.expect("error message"); + assert!(message.contains("Unknown feedback type")); + } +} diff --git a/crates/tui/src/commands/mod.rs b/crates/tui/src/commands/mod.rs index f391d593..310c2202 100644 --- a/crates/tui/src/commands/mod.rs +++ b/crates/tui/src/commands/mod.rs @@ -9,6 +9,7 @@ mod config; mod core; mod cycle; mod debug; +mod feedback; mod goal; mod hooks; mod init; @@ -207,6 +208,12 @@ pub const COMMANDS: &[CommandInfo] = &[ usage: "/links", description_id: MessageId::CmdLinksDescription, }, + CommandInfo { + name: "feedback", + aliases: &[], + usage: "/feedback [bug|feature|security]", + description_id: MessageId::CmdFeedbackDescription, + }, CommandInfo { name: "home", aliases: &["stats", "overview"], @@ -514,6 +521,7 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult { "hooks" | "hook" => hooks::hooks(app, arg), "subagents" | "agents" => core::subagents(app), "links" | "dashboard" | "api" => core::deepseek_links(app), + "feedback" => feedback::feedback(app, arg), "home" | "stats" | "overview" => core::home_dashboard(app), "note" => note::note(app, arg), "memory" => memory::memory(app, arg), diff --git a/crates/tui/src/localization.rs b/crates/tui/src/localization.rs index 30799572..88cd076d 100644 --- a/crates/tui/src/localization.rs +++ b/crates/tui/src/localization.rs @@ -229,6 +229,7 @@ pub enum MessageId { CmdEditDescription, CmdExitDescription, CmdExportDescription, + CmdFeedbackDescription, CmdHelpDescription, CmdHomeDescription, CmdHooksDescription, @@ -421,6 +422,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[ MessageId::CmdEditDescription, MessageId::CmdExitDescription, MessageId::CmdExportDescription, + MessageId::CmdFeedbackDescription, MessageId::CmdHelpDescription, MessageId::CmdHomeDescription, MessageId::CmdHooksDescription, @@ -739,6 +741,7 @@ fn english(id: MessageId) -> &'static str { MessageId::CmdEditDescription => "Revise and resubmit the last message", MessageId::CmdExitDescription => "Exit the application", MessageId::CmdExportDescription => "Export conversation to markdown", + MessageId::CmdFeedbackDescription => "Generate a GitHub feedback URL", MessageId::CmdHelpDescription => "Show help information", MessageId::CmdHomeDescription => "Show home dashboard with stats and quick actions", MessageId::CmdHooksDescription => "List configured lifecycle hooks (read-only)", @@ -1025,6 +1028,7 @@ fn japanese(id: MessageId) -> Option<&'static str> { MessageId::CmdEditDescription => "最後のメッセージを編集して再送信", MessageId::CmdExitDescription => "アプリを終了", MessageId::CmdExportDescription => "会話を Markdown にエクスポート", + MessageId::CmdFeedbackDescription => "GitHub フィードバック URL を生成", MessageId::CmdHelpDescription => "ヘルプを表示", MessageId::CmdHomeDescription => "統計とクイックアクション付きのホームダッシュボードを表示", MessageId::CmdHooksDescription => { @@ -1293,6 +1297,7 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> { MessageId::CmdEditDescription => "修改并重新提交最后一条消息", MessageId::CmdExitDescription => "退出应用", MessageId::CmdExportDescription => "将对话导出为 Markdown", + MessageId::CmdFeedbackDescription => "生成 GitHub 反馈链接", MessageId::CmdHelpDescription => "显示帮助信息", MessageId::CmdHomeDescription => "显示主页面板,含统计与快捷操作", MessageId::CmdHooksDescription => "列出已配置的生命周期钩子(只读)", @@ -1547,6 +1552,7 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> { MessageId::CmdEditDescription => "Revisar e reenviar a última mensagem", MessageId::CmdExitDescription => "Sair do aplicativo", MessageId::CmdExportDescription => "Exportar a conversa para markdown", + MessageId::CmdFeedbackDescription => "Gerar uma URL de feedback no GitHub", MessageId::CmdHelpDescription => "Exibir informações de ajuda", MessageId::CmdHomeDescription => "Exibir o painel inicial com estatísticas e ações rápidas", MessageId::CmdHooksDescription => { diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index 5332623b..3ed71b1b 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -3792,6 +3792,13 @@ pub enum AppAction { OpenProviderPicker, /// Open the `/statusline` multi-select picker for footer items. OpenStatusPicker, + /// Open the `/feedback` picker for GitHub issue/security destinations. + OpenFeedbackPicker, + /// Open an external URL in the system browser. + OpenExternalUrl { + url: String, + label: String, + }, /// Send a message to the AI (normal chat mode). SendMessage(String), /// Run a Recursive Language Model (RLM) turn — Algorithm 1 from diff --git a/crates/tui/src/tui/feedback_picker.rs b/crates/tui/src/tui/feedback_picker.rs new file mode 100644 index 00000000..1b467b6c --- /dev/null +++ b/crates/tui/src/tui/feedback_picker.rs @@ -0,0 +1,243 @@ +//! `/feedback` picker for GitHub feedback destinations. + +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use ratatui::{ + buffer::Buffer, + layout::Rect, + style::{Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Clear, Padding, Paragraph, Widget}, +}; + +use crate::palette; +use crate::tui::views::{CommandPaletteAction, ModalKind, ModalView, ViewAction, ViewEvent}; + +#[derive(Debug, Clone, Copy)] +struct FeedbackOption { + number: char, + label: &'static str, + description: &'static str, + command: &'static str, +} + +const OPTIONS: &[FeedbackOption] = &[ + FeedbackOption { + number: '1', + label: "Bug report", + description: "Report a problem or regression", + command: "/feedback bug", + }, + FeedbackOption { + number: '2', + label: "Feature request", + description: "Suggest an idea or improvement", + command: "/feedback feature", + }, + FeedbackOption { + number: '3', + label: "Security vulnerability", + description: "Review the security policy before reporting", + command: "/feedback security", + }, +]; + +pub struct FeedbackPickerView { + selected: usize, +} + +impl FeedbackPickerView { + #[must_use] + pub fn new() -> Self { + Self { selected: 0 } + } + + fn move_up(&mut self) { + if self.selected > 0 { + self.selected -= 1; + } + } + + fn move_down(&mut self) { + let max = OPTIONS.len().saturating_sub(1); + if self.selected < max { + self.selected += 1; + } + } + + fn select_number(&mut self, number: char) -> Option { + let idx = OPTIONS.iter().position(|option| option.number == number)?; + self.selected = idx; + Some(self.selected_action()) + } + + fn selected_action(&self) -> ViewAction { + let command = OPTIONS + .get(self.selected) + .map(|option| option.command) + .unwrap_or(OPTIONS[0].command) + .to_string(); + ViewAction::EmitAndClose(ViewEvent::CommandPaletteSelected { + action: CommandPaletteAction::ExecuteCommand { command }, + }) + } +} + +impl Default for FeedbackPickerView { + fn default() -> Self { + Self::new() + } +} + +impl ModalView for FeedbackPickerView { + fn kind(&self) -> ModalKind { + ModalKind::FeedbackPicker + } + + 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 => ViewAction::Close, + KeyCode::Enter => self.selected_action(), + KeyCode::Up | KeyCode::Char('k') => { + self.move_up(); + ViewAction::None + } + KeyCode::Down | KeyCode::Char('j') => { + self.move_down(); + ViewAction::None + } + KeyCode::Char(number) + if !key.modifiers.contains(KeyModifiers::CONTROL) + && OPTIONS.iter().any(|option| option.number == number) => + { + self.select_number(number).unwrap_or(ViewAction::None) + } + _ => ViewAction::None, + } + } + + fn render(&self, area: Rect, buf: &mut Buffer) { + let popup_width = 78.min(area.width.saturating_sub(4)).max(44); + let needed_height = (OPTIONS.len() as u16).saturating_add(7); + let popup_height = needed_height.min(area.height.saturating_sub(4)).max(8); + + let popup_area = Rect { + x: area.x + (area.width.saturating_sub(popup_width)) / 2, + y: area.y + (area.height.saturating_sub(popup_height)) / 2, + width: popup_width, + height: popup_height, + }; + + Clear.render(popup_area, buf); + + let block = Block::default() + .title(Line::from(Span::styled( + " Feedback ", + Style::default() + .fg(palette::DEEPSEEK_SKY) + .add_modifier(Modifier::BOLD), + ))) + .title_bottom(Line::from(vec![ + Span::styled(" Up/Down ", Style::default().fg(palette::TEXT_MUTED)), + Span::raw("move "), + Span::styled(" Enter ", Style::default().fg(palette::TEXT_MUTED)), + Span::raw("open "), + Span::styled(" Esc ", Style::default().fg(palette::TEXT_MUTED)), + Span::raw("cancel "), + ])) + .borders(Borders::ALL) + .border_style(Style::default().fg(palette::BORDER_COLOR)) + .style(Style::default().bg(palette::DEEPSEEK_INK)) + .padding(Padding::uniform(1)); + + let inner = block.inner(popup_area); + block.render(popup_area, buf); + + let mut lines = Vec::with_capacity(OPTIONS.len() + 2); + lines.push(Line::from(Span::styled( + "Choose where to send feedback:", + Style::default().fg(palette::TEXT_MUTED), + ))); + lines.push(Line::from("")); + + for (idx, option) in OPTIONS.iter().enumerate() { + let is_selected = idx == self.selected; + let row_style = if is_selected { + Style::default() + .fg(palette::SELECTION_TEXT) + .bg(palette::SELECTION_BG) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(palette::TEXT_PRIMARY) + }; + let desc_style = if is_selected { + Style::default() + .fg(palette::SELECTION_TEXT) + .bg(palette::SELECTION_BG) + } else { + Style::default().fg(palette::TEXT_MUTED) + }; + let pointer = if is_selected { ">" } else { " " }; + + lines.push(Line::from(vec![ + Span::styled(format!(" {pointer} {}. ", option.number), row_style), + Span::styled(option.label, row_style), + Span::raw(" "), + Span::styled(option.description, desc_style), + ])); + } + + Paragraph::new(lines).render(inner, buf); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn emitted_command(action: ViewAction) -> String { + match action { + ViewAction::EmitAndClose(ViewEvent::CommandPaletteSelected { + action: CommandPaletteAction::ExecuteCommand { command }, + }) => command, + other => panic!("expected feedback command emit, got {other:?}"), + } + } + + #[test] + fn enter_emits_selected_feedback_command() { + let mut view = FeedbackPickerView::new(); + let command = + emitted_command(view.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))); + assert_eq!(command, "/feedback bug"); + } + + #[test] + fn arrow_down_selects_feature_command() { + let mut view = FeedbackPickerView::new(); + view.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + let command = + emitted_command(view.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))); + assert_eq!(command, "/feedback feature"); + } + + #[test] + fn digit_selects_security_command() { + let mut view = FeedbackPickerView::new(); + let command = + emitted_command(view.handle_key(KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE))); + assert_eq!(command, "/feedback security"); + } + + #[test] + fn esc_closes_picker() { + let mut view = FeedbackPickerView::new(); + assert!(matches!( + view.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)), + ViewAction::Close + )); + } +} diff --git a/crates/tui/src/tui/mod.rs b/crates/tui/src/tui/mod.rs index 88df8ebc..f89ec799 100644 --- a/crates/tui/src/tui/mod.rs +++ b/crates/tui/src/tui/mod.rs @@ -14,6 +14,7 @@ pub mod context_menu; pub mod diff_render; pub mod event_broker; pub mod external_editor; +pub mod feedback_picker; pub mod file_frecency; pub mod file_mention; pub mod file_picker; diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index c7f5b208..0f43ca47 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use std::io::{self, Stdout, Write}; use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; use anyhow::Result; @@ -4651,6 +4651,24 @@ async fn apply_command_result( )); } } + AppAction::OpenFeedbackPicker => { + if app.view_stack.top_kind() != Some(ModalKind::FeedbackPicker) { + app.view_stack + .push(crate::tui::feedback_picker::FeedbackPickerView::new()); + } + } + AppAction::OpenExternalUrl { url, label } => match open_external_url(&url) { + Ok(()) => { + app.status_message = Some(format!("Opened {label} in your browser")); + } + Err(err) => { + app.add_message(HistoryCell::System { + content: format!( + "Could not open {label} automatically: {err}\n\nThe URL is printed above." + ), + }); + } + }, AppAction::OpenContextInspector => { open_context_inspector(app); } @@ -4790,6 +4808,43 @@ async fn apply_command_result( Ok(false) } +fn open_external_url(url: &str) -> Result<()> { + #[cfg(target_os = "macos")] + let mut command = { + let mut command = Command::new("open"); + command.arg(url); + command + }; + #[cfg(target_os = "linux")] + let mut command = { + let mut command = Command::new("xdg-open"); + command.arg(url); + command + }; + #[cfg(target_os = "windows")] + let mut command = { + let mut command = Command::new("cmd"); + command.args(["/C", "start", "", url]); + command + }; + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + return Err(anyhow::anyhow!( + "browser opening is unsupported on this platform" + )); + + let status = command + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map_err(|err| anyhow::anyhow!("failed to launch browser command: {err}"))?; + if !status.success() { + return Err(anyhow::anyhow!( + "browser command exited with status {status}" + )); + } + Ok(()) +} + async fn handle_mcp_ui_action( app: &mut App, config: &Config, diff --git a/crates/tui/src/tui/views/mod.rs b/crates/tui/src/tui/views/mod.rs index c6058948..195eef80 100644 --- a/crates/tui/src/tui/views/mod.rs +++ b/crates/tui/src/tui/views/mod.rs @@ -32,6 +32,7 @@ pub enum ModalKind { ProviderPicker, FilePicker, StatusPicker, + FeedbackPicker, ContextMenu, ShellControl, }