返回 DeepSeek-TUI-2026
queue.rs
根目录 / crates / tui / src / commands / queue.rs
1 //! Queue commands: queue list/edit/drop/clear
2
3 use crate::tui::app::App;
4
5 use super::CommandResult;
6
7 const PREVIEW_LIMIT: usize = 120;
8
9 pub fn queue(app: &mut App, args: Option<&str>) -> CommandResult {
10 let arg = args.unwrap_or("").trim();
11 if arg.is_empty() || arg.eq_ignore_ascii_case("list") {
12 return list_queue(app);
13 }
14
15 let mut parts = arg.split_whitespace();
16 let action = parts.next().unwrap_or("").to_lowercase();
17
18 match action.as_str() {
19 "edit" => edit_queue(app, parts.next()),
20 "drop" | "remove" | "rm" => drop_queue(app, parts.next()),
21 "clear" => clear_queue(app),
22 _ => CommandResult::error("Usage: /queue [list|edit <n>|drop <n>|clear]"),
23 }
24 }
25
26 fn list_queue(app: &mut App) -> CommandResult {
27 let mut lines = Vec::new();
28 let queued = app.queued_message_count();
29
30 if let Some(draft) = app.queued_draft.as_ref() {
31 lines.push("Editing queued message:".to_string());
32 lines.push(format!("- {}", truncate_preview(&draft.display)));
33 }
34
35 if queued == 0 {
36 if lines.is_empty() {
37 return CommandResult::message("No queued messages");
38 }
39 return CommandResult::message(lines.join("\n"));
40 }
41
42 lines.push(format!("Queued messages ({queued}):"));
43 for (idx, message) in app.queued_messages.iter().enumerate() {
44 lines.push(format!(
45 "{}. {}",
46 idx + 1,
47 truncate_preview(&message.display)
48 ));
49 }
50
51 lines.push("Tip: /queue edit <n> to edit, /queue drop <n> to remove".to_string());
52
53 CommandResult::message(lines.join("\n"))
54 }
55
56 fn edit_queue(app: &mut App, index: Option<&str>) -> CommandResult {
57 if app.queued_draft.is_some() {
58 return CommandResult::error(
59 "Already editing a queued message. Send it or /queue clear to discard.",
60 );
61 }
62 let index = match parse_index(index) {
63 Ok(index) => index,
64 Err(err) => return CommandResult::error(err),
65 };
66
67 let Some(message) = app.remove_queued_message(index) else {
68 return CommandResult::error("Queued message not found");
69 };
70
71 app.input = message.display.clone();
72 app.cursor_position = app.input.len();
73 app.queued_draft = Some(message);
74 app.status_message = Some(format!("Editing queued message {}", index + 1));
75
76 CommandResult::message(format!(
77 "Editing queued message {} (press Enter to re-queue/send)",
78 index + 1
79 ))
80 }
81
82 fn drop_queue(app: &mut App, index: Option<&str>) -> CommandResult {
83 let index = match parse_index(index) {
84 Ok(index) => index,
85 Err(err) => return CommandResult::error(err),
86 };
87
88 if app.remove_queued_message(index).is_none() {
89 return CommandResult::error("Queued message not found");
90 }
91
92 CommandResult::message(format!("Dropped queued message {}", index + 1))
93 }
94
95 fn clear_queue(app: &mut App) -> CommandResult {
96 let queued = app.queued_message_count();
97 let had_draft = app.queued_draft.take().is_some();
98 app.queued_messages.clear();
99 if queued == 0 && !had_draft {
100 return CommandResult::message("Queue already empty");
101 }
102
103 CommandResult::message("Queue cleared")
104 }
105
106 fn parse_index(input: Option<&str>) -> Result<usize, &'static str> {
107 let Some(input) = input else {
108 return Err("Missing index. Usage: /queue edit <n> or /queue drop <n>");
109 };
110 let raw = input
111 .parse::<usize>()
112 .map_err(|_| "Index must be a positive number")?;
113 if raw == 0 {
114 return Err("Index must be >= 1");
115 }
116 Ok(raw - 1)
117 }
118
119 fn truncate_preview(text: &str) -> String {
120 if text.chars().count() <= PREVIEW_LIMIT {
121 return text.to_string();
122 }
123 let mut out = String::new();
124 for ch in text.chars().take(PREVIEW_LIMIT.saturating_sub(3)) {
125 out.push(ch);
126 }
127 out.push_str("...");
128 out
129 }
130
131 #[cfg(test)]
132 mod tests {
133 use super::*;
134 use crate::config::Config;
135 use crate::tui::app::{App, QueuedMessage, TuiOptions};
136 use tempfile::TempDir;
137
138 fn create_test_app_with_tmpdir(tmpdir: &TempDir) -> App {
139 let options = TuiOptions {
140 model: "deepseek-v4-pro".to_string(),
141 workspace: tmpdir.path().to_path_buf(),
142 config_path: None,
143 config_profile: None,
144 allow_shell: false,
145 use_alt_screen: true,
146 use_mouse_capture: false,
147 use_bracketed_paste: true,
148 max_subagents: 1,
149 skills_dir: tmpdir.path().join("skills"),
150 memory_path: tmpdir.path().join("memory.md"),
151 notes_path: tmpdir.path().join("notes.txt"),
152 mcp_config_path: tmpdir.path().join("mcp.json"),
153 use_memory: false,
154 start_in_agent_mode: false,
155 skip_onboarding: true,
156 yolo: false,
157 resume_session_id: None,
158 initial_input: None,
159 };
160 App::new(options, &Config::default())
161 }
162
163 #[test]
164 fn test_queue_list_empty() {
165 let tmpdir = TempDir::new().unwrap();
166 let mut app = create_test_app_with_tmpdir(&tmpdir);
167 let result = queue(&mut app, None);
168 assert!(result.message.is_some());
169 let msg = result.message.unwrap();
170 assert!(msg.contains("No queued messages"));
171 }
172
173 #[test]
174 fn test_queue_list_with_messages() {
175 let tmpdir = TempDir::new().unwrap();
176 let mut app = create_test_app_with_tmpdir(&tmpdir);
177 app.queued_messages
178 .push_back(QueuedMessage::new("First message".to_string(), None));
179 app.queued_messages
180 .push_back(QueuedMessage::new("Second message".to_string(), None));
181 let result = queue(&mut app, Some("list"));
182 assert!(result.message.is_some());
183 let msg = result.message.unwrap();
184 assert!(msg.contains("Queued messages (2)"));
185 assert!(msg.contains("1. First message"));
186 assert!(msg.contains("2. Second message"));
187 }
188
189 #[test]
190 fn test_queue_edit_missing_index() {
191 let tmpdir = TempDir::new().unwrap();
192 let mut app = create_test_app_with_tmpdir(&tmpdir);
193 app.queued_messages
194 .push_back(QueuedMessage::new("Test".to_string(), None));
195 let result = queue(&mut app, Some("edit"));
196 assert!(result.message.is_some());
197 assert!(result.message.unwrap().contains("Missing index"));
198 }
199
200 #[test]
201 fn test_queue_edit_invalid_index() {
202 let tmpdir = TempDir::new().unwrap();
203 let mut app = create_test_app_with_tmpdir(&tmpdir);
204 let result = queue(&mut app, Some("edit abc"));
205 assert!(result.message.is_some());
206 assert!(
207 result
208 .message
209 .unwrap()
210 .contains("must be a positive number")
211 );
212 }
213
214 #[test]
215 fn test_queue_edit_not_found() {
216 let tmpdir = TempDir::new().unwrap();
217 let mut app = create_test_app_with_tmpdir(&tmpdir);
218 let result = queue(&mut app, Some("edit 1"));
219 assert!(result.message.is_some());
220 assert!(result.message.unwrap().contains("not found"));
221 }
222
223 #[test]
224 fn test_queue_edit_already_editing() {
225 let tmpdir = TempDir::new().unwrap();
226 let mut app = create_test_app_with_tmpdir(&tmpdir);
227 app.queued_messages
228 .push_back(QueuedMessage::new("First".to_string(), None));
229 app.queued_messages
230 .push_back(QueuedMessage::new("Second".to_string(), None));
231 // Start editing
232 queue(&mut app, Some("edit 1"));
233 // Try to edit another
234 let result = queue(&mut app, Some("edit 2"));
235 assert!(result.message.is_some());
236 assert!(result.message.unwrap().contains("Already editing"));
237 }
238
239 #[test]
240 fn test_queue_edit_success() {
241 let tmpdir = TempDir::new().unwrap();
242 let mut app = create_test_app_with_tmpdir(&tmpdir);
243 app.queued_messages
244 .push_back(QueuedMessage::new("Original message".to_string(), None));
245 let result = queue(&mut app, Some("edit 1"));
246 assert!(result.message.is_some());
247 assert_eq!(app.input, "Original message");
248 assert_eq!(app.cursor_position, app.input.len());
249 assert!(app.queued_draft.is_some());
250 }
251
252 #[test]
253 fn test_queue_drop_success() {
254 let tmpdir = TempDir::new().unwrap();
255 let mut app = create_test_app_with_tmpdir(&tmpdir);
256 app.queued_messages
257 .push_back(QueuedMessage::new("To drop".to_string(), None));
258 let initial_count = app.queued_messages.len();
259 let result = queue(&mut app, Some("drop 1"));
260 assert!(result.message.is_some());
261 assert!(result.message.unwrap().contains("Dropped queued message"));
262 assert_eq!(app.queued_messages.len(), initial_count - 1);
263 }
264
265 #[test]
266 fn test_queue_clear() {
267 let tmpdir = TempDir::new().unwrap();
268 let mut app = create_test_app_with_tmpdir(&tmpdir);
269 app.queued_messages
270 .push_back(QueuedMessage::new("Message 1".to_string(), None));
271 app.queued_messages
272 .push_back(QueuedMessage::new("Message 2".to_string(), None));
273 let result = queue(&mut app, Some("clear"));
274 assert!(result.message.is_some());
275 assert!(result.message.unwrap().contains("Queue cleared"));
276 assert!(app.queued_messages.is_empty());
277 }
278
279 #[test]
280 fn test_queue_clear_already_empty() {
281 let tmpdir = TempDir::new().unwrap();
282 let mut app = create_test_app_with_tmpdir(&tmpdir);
283 let result = queue(&mut app, Some("clear"));
284 assert!(result.message.is_some());
285 assert!(result.message.unwrap().contains("Queue already empty"));
286 }
287
288 #[test]
289 fn test_truncate_preview_short_text() {
290 let result = truncate_preview("Short text");
291 assert_eq!(result, "Short text");
292 }
293
294 #[test]
295 fn test_truncate_preview_long_text() {
296 let long_text = "x".repeat(200);
297 let result = truncate_preview(&long_text);
298 assert!(result.len() <= PREVIEW_LIMIT + 3);
299 assert!(result.ends_with("..."));
300 }
301
302 #[test]
303 fn test_truncate_preview_unicode() {
304 let text = "Hello 世界 🌍";
305 let result = truncate_preview(text);
306 assert_eq!(result, text);
307 }
308 }
309
309 lines RUST