1use super::model::{
2 ModelProvider, save_agent_config, save_chat_session, save_style, save_system_prompt,
3};
4use super::render::copy_to_clipboard;
5use super::theme::ThemeName;
6use super::ui::draw_chat_ui;
7use crate::command::chat::app::{ChatApp, ChatMode, config_total_fields};
8use crate::constants::{CONFIG_FIELDS, CONFIG_GLOBAL_FIELDS};
9use crate::{error, info};
10use crossterm::{
11 event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
12 execute,
13 terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
14};
15use ratatui::{Terminal, backend::CrosstermBackend};
16use std::io;
17
18pub fn run_chat_tui() {
19 match run_chat_tui_internal() {
20 Ok(_) => {}
21 Err(e) => {
22 error!("❌ Chat TUI 启动失败: {}", e);
23 }
24 }
25}
26
27pub fn run_chat_tui_internal() -> io::Result<()> {
28 terminal::enable_raw_mode()?;
29 let mut stdout = io::stdout();
30 execute!(stdout, EnterAlternateScreen)?;
31
32 let backend = CrosstermBackend::new(stdout);
33 let mut terminal = Terminal::new(backend)?;
34
35 let mut app = ChatApp::new();
36
37 if app.agent_config.providers.is_empty() {
38 terminal::disable_raw_mode()?;
39 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
40 info!("⚠️ 尚未配置 LLM 模型提供方,请先运行 j chat 查看配置说明。");
41 return Ok(());
42 }
43
44 let mut needs_redraw = true; loop {
47 let had_toast = app.toast.is_some();
49 app.tick_toast();
50 if had_toast && app.toast.is_none() {
51 needs_redraw = true;
52 }
53
54 let was_loading = app.is_loading;
56 app.poll_stream();
57 if app.is_loading {
59 let current_len = app.streaming_content.lock().unwrap().len();
60 let bytes_delta = current_len.saturating_sub(app.last_rendered_streaming_len);
61 let time_elapsed = app.last_stream_render_time.elapsed();
62 if bytes_delta >= 200
64 || time_elapsed >= std::time::Duration::from_millis(200)
65 || current_len == 0
66 {
67 needs_redraw = true;
68 }
69 } else if was_loading {
70 needs_redraw = true;
72 }
73
74 if needs_redraw {
76 terminal.draw(|f| draw_chat_ui(f, &mut app))?;
77 needs_redraw = false;
78 if app.is_loading {
80 app.last_rendered_streaming_len = app.streaming_content.lock().unwrap().len();
81 app.last_stream_render_time = std::time::Instant::now();
82 }
83 }
84
85 let poll_timeout = if app.is_loading {
87 std::time::Duration::from_millis(150)
88 } else {
89 std::time::Duration::from_millis(1000)
90 };
91
92 if event::poll(poll_timeout)? {
93 let mut should_break = false;
95 loop {
96 let evt = event::read()?;
97 match evt {
98 Event::Key(key) => {
99 needs_redraw = true;
100 match app.mode {
101 ChatMode::Chat => {
102 if handle_chat_mode(&mut app, key) {
103 should_break = true;
104 break;
105 }
106 }
107 ChatMode::SelectModel => handle_select_model(&mut app, key),
108 ChatMode::Browse => handle_browse_mode(&mut app, key),
109 ChatMode::Help => {
110 app.mode = ChatMode::Chat;
111 }
112 ChatMode::Config => handle_config_mode(&mut app, key),
113 ChatMode::ArchiveConfirm => handle_archive_confirm_mode(&mut app, key),
114 ChatMode::ArchiveList => handle_archive_list_mode(&mut app, key),
115 ChatMode::ToolConfirm => handle_tool_confirm_mode(&mut app, key),
116 }
117 }
118 Event::Resize(_, _) => {
119 needs_redraw = true;
120 }
121 _ => {}
122 }
123 if !event::poll(std::time::Duration::ZERO)? {
125 break;
126 }
127 }
128 if should_break {
129 break;
130 }
131
132 if app.pending_system_prompt_edit {
134 app.pending_system_prompt_edit = false;
135 let current_prompt = app.agent_config.system_prompt.clone().unwrap_or_default();
136 match crate::tui::editor::open_editor_on_terminal(
137 &mut terminal,
138 "编辑系统提示词 (System Prompt)",
139 ¤t_prompt,
140 ) {
141 Ok(Some(new_text)) => {
142 if new_text.is_empty() {
143 app.agent_config.system_prompt = None;
144 } else {
145 app.agent_config.system_prompt = Some(new_text);
146 }
147 let prompt_text = app.agent_config.system_prompt.as_deref().unwrap_or("");
148 if save_system_prompt(prompt_text) {
149 app.show_toast("系统提示词已更新", false);
150 } else {
151 app.show_toast("系统提示词保存失败", true);
152 }
153 }
154 Ok(None) => {
155 }
157 Err(e) => {
158 app.show_toast(format!("编辑器错误: {}", e), true);
159 }
160 }
161 needs_redraw = true;
162 }
163
164 if app.pending_style_edit {
166 app.pending_style_edit = false;
167 let current_style = app.agent_config.style.clone().unwrap_or_default();
168 match crate::tui::editor::open_editor_on_terminal(
169 &mut terminal,
170 "编辑回复风格 (Style)",
171 ¤t_style,
172 ) {
173 Ok(Some(new_text)) => {
174 if new_text.is_empty() {
175 app.agent_config.style = None;
176 } else {
177 app.agent_config.style = Some(new_text);
178 }
179 let style_text = app.agent_config.style.as_deref().unwrap_or("");
180 if save_style(style_text) {
181 app.show_toast("回复风格已更新", false);
182 } else {
183 app.show_toast("回复风格保存失败", true);
184 }
185 }
186 Ok(None) => {
187 }
189 Err(e) => {
190 app.show_toast(format!("编辑器错误: {}", e), true);
191 }
192 }
193 needs_redraw = true;
194 }
195 }
196 }
197
198 let _ = save_chat_session(&app.session);
200
201 terminal::disable_raw_mode()?;
202 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
203 Ok(())
204}
205
206pub fn handle_chat_mode(app: &mut ChatApp, key: KeyEvent) -> bool {
209 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
211 return true;
212 }
213
214 if app.at_popup_active {
216 let filtered = get_filtered_skills(app);
217 match key.code {
218 KeyCode::Up => {
219 if !filtered.is_empty() && app.at_popup_selected > 0 {
220 app.at_popup_selected -= 1;
221 }
222 return false;
223 }
224 KeyCode::Down => {
225 if !filtered.is_empty() && app.at_popup_selected < filtered.len().saturating_sub(1)
226 {
227 app.at_popup_selected += 1;
228 }
229 return false;
230 }
231 KeyCode::Tab | KeyCode::Enter => {
232 if !filtered.is_empty() {
233 let sel = app.at_popup_selected.min(filtered.len() - 1);
234 let name = filtered[sel].clone();
235 complete_at_mention(app, &name);
236 }
237 app.at_popup_active = false;
238 return false;
239 }
240 KeyCode::Esc => {
241 app.at_popup_active = false;
242 return false;
243 }
244 KeyCode::Char(' ') => {
245 app.at_popup_active = false;
247 }
249 KeyCode::Backspace => {
250 if app.cursor_pos > 0 {
252 let start = app
253 .input
254 .char_indices()
255 .nth(app.cursor_pos - 1)
256 .map(|(i, _)| i)
257 .unwrap_or(0);
258 let end = app
259 .input
260 .char_indices()
261 .nth(app.cursor_pos)
262 .map(|(i, _)| i)
263 .unwrap_or(app.input.len());
264 app.input.drain(start..end);
265 app.cursor_pos -= 1;
266 }
267 if app.cursor_pos <= app.at_popup_start_pos {
269 app.at_popup_active = false;
270 } else {
271 update_at_filter(app);
272 }
273 return false;
274 }
275 _ => {
276 }
278 }
279 }
280
281 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('t') {
283 if !app.agent_config.providers.is_empty() {
284 app.mode = ChatMode::SelectModel;
285 app.model_list_state
286 .select(Some(app.agent_config.active_index));
287 }
288 return false;
289 }
290
291 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('l') {
293 if app.session.messages.is_empty() {
294 app.show_toast("当前对话为空,无法归档", true);
295 } else {
296 app.start_archive_confirm();
297 }
298 return false;
299 }
300
301 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('r') {
303 app.start_archive_list();
304 return false;
305 }
306
307 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('y') {
309 if let Some(last_ai) = app
310 .session
311 .messages
312 .iter()
313 .rev()
314 .find(|m| m.role == "assistant")
315 {
316 if copy_to_clipboard(&last_ai.content) {
317 app.show_toast("已复制最后一条 AI 回复", false);
318 } else {
319 app.show_toast("复制到剪切板失败", true);
320 }
321 } else {
322 app.show_toast("暂无 AI 回复可复制", true);
323 }
324 return false;
325 }
326
327 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('b') {
329 if !app.session.messages.is_empty() {
330 app.browse_msg_index = app.session.messages.len() - 1;
332 app.browse_scroll_offset = 0; app.mode = ChatMode::Browse;
334 app.msg_lines_cache = None; } else {
336 app.show_toast("暂无消息可浏览", true);
337 }
338 return false;
339 }
340
341 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('e') {
343 app.config_provider_idx = app
345 .agent_config
346 .active_index
347 .min(app.agent_config.providers.len().saturating_sub(1));
348 app.config_field_idx = 0;
349 app.config_editing = false;
350 app.config_edit_buf.clear();
351 app.mode = ChatMode::Config;
352 return false;
353 }
354
355 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('s') {
357 app.agent_config.stream_mode = !app.agent_config.stream_mode;
358 let _ = save_agent_config(&app.agent_config);
359 let mode_str = if app.agent_config.stream_mode {
360 "流式输出"
361 } else {
362 "整体输出"
363 };
364 app.show_toast(&format!("已切换为: {}", mode_str), false);
365 return false;
366 }
367
368 let char_count = app.input.chars().count();
369
370 match key.code {
371 KeyCode::Esc => return true,
372
373 KeyCode::Enter => {
374 if !app.is_loading {
375 app.send_message();
376 }
377 }
378
379 KeyCode::Up => app.scroll_up(),
381 KeyCode::Down => app.scroll_down(),
382 KeyCode::PageUp => {
383 for _ in 0..10 {
384 app.scroll_up();
385 }
386 }
387 KeyCode::PageDown => {
388 for _ in 0..10 {
389 app.scroll_down();
390 }
391 }
392
393 KeyCode::Left => {
395 if app.cursor_pos > 0 {
396 app.cursor_pos -= 1;
397 }
398 }
399 KeyCode::Right => {
400 if app.cursor_pos < char_count {
401 app.cursor_pos += 1;
402 }
403 }
404 KeyCode::Home => app.cursor_pos = 0,
405 KeyCode::End => app.cursor_pos = char_count,
406
407 KeyCode::Backspace => {
409 if app.cursor_pos > 0 {
410 let start = app
411 .input
412 .char_indices()
413 .nth(app.cursor_pos - 1)
414 .map(|(i, _)| i)
415 .unwrap_or(0);
416 let end = app
417 .input
418 .char_indices()
419 .nth(app.cursor_pos)
420 .map(|(i, _)| i)
421 .unwrap_or(app.input.len());
422 app.input.drain(start..end);
423 app.cursor_pos -= 1;
424 }
425 }
426 KeyCode::Delete => {
427 if app.cursor_pos < char_count {
428 let start = app
429 .input
430 .char_indices()
431 .nth(app.cursor_pos)
432 .map(|(i, _)| i)
433 .unwrap_or(app.input.len());
434 let end = app
435 .input
436 .char_indices()
437 .nth(app.cursor_pos + 1)
438 .map(|(i, _)| i)
439 .unwrap_or(app.input.len());
440 app.input.drain(start..end);
441 }
442 }
443
444 KeyCode::F(1) => {
446 app.mode = ChatMode::Help;
447 }
448 KeyCode::Char('?') if app.input.is_empty() => {
450 app.mode = ChatMode::Help;
451 }
452 KeyCode::Char(c) => {
453 let byte_idx = app
454 .input
455 .char_indices()
456 .nth(app.cursor_pos)
457 .map(|(i, _)| i)
458 .unwrap_or(app.input.len());
459 app.input.insert_str(byte_idx, &c.to_string());
460 app.cursor_pos += 1;
461
462 if c == '@' && !app.loaded_skills.is_empty() {
464 let valid = app.cursor_pos <= 1 || {
466 let chars: Vec<char> = app.input.chars().collect();
467 app.cursor_pos >= 2 && chars[app.cursor_pos - 2].is_whitespace()
468 };
469 if valid {
470 app.at_popup_active = true;
471 app.at_popup_start_pos = app.cursor_pos - 1;
472 app.at_popup_filter.clear();
473 app.at_popup_selected = 0;
474 }
475 } else if app.at_popup_active {
476 update_at_filter(app);
477 }
478 }
479
480 _ => {}
481 }
482
483 false
484}
485
486pub fn handle_browse_mode(app: &mut ChatApp, key: KeyEvent) {
488 let msg_count = app.session.messages.len();
489 if msg_count == 0 {
490 app.mode = ChatMode::Chat;
491 app.msg_lines_cache = None;
492 return;
493 }
494
495 match key.code {
496 KeyCode::Esc => {
497 app.mode = ChatMode::Chat;
498 app.msg_lines_cache = None; }
500 KeyCode::Up | KeyCode::Char('k') => {
501 if app.browse_msg_index > 0 {
502 app.browse_msg_index -= 1;
503 app.browse_scroll_offset = 0; app.msg_lines_cache = None; }
506 }
507 KeyCode::Down | KeyCode::Char('j') => {
508 if app.browse_msg_index < msg_count - 1 {
509 app.browse_msg_index += 1;
510 app.browse_scroll_offset = 0; app.msg_lines_cache = None; }
513 }
514 KeyCode::Char('a') | KeyCode::Char('A') => {
516 app.browse_scroll_offset = app.browse_scroll_offset.saturating_sub(3);
517 }
518 KeyCode::Char('d') | KeyCode::Char('D') => {
519 app.browse_scroll_offset = app.browse_scroll_offset.saturating_add(3);
520 }
521 KeyCode::Enter | KeyCode::Char('y') => {
522 if let Some(msg) = app.session.messages.get(app.browse_msg_index) {
524 let content = msg.content.clone();
525 let role_label = if msg.role == "assistant" {
526 "AI"
527 } else if msg.role == "user" {
528 "用户"
529 } else {
530 "系统"
531 };
532 if copy_to_clipboard(&content) {
533 app.show_toast(
534 &format!("已复制第 {} 条{}消息", app.browse_msg_index + 1, role_label),
535 false,
536 );
537 } else {
538 app.show_toast("复制到剪切板失败", true);
539 }
540 }
541 }
542 _ => {}
543 }
544}
545
546pub fn config_field_label(idx: usize) -> &'static str {
548 let total_provider = CONFIG_FIELDS.len();
549 if idx < total_provider {
550 match CONFIG_FIELDS[idx] {
551 "name" => "显示名称",
552 "api_base" => "API Base",
553 "api_key" => "API Key",
554 "model" => "模型名称",
555 _ => CONFIG_FIELDS[idx],
556 }
557 } else {
558 let gi = idx - total_provider;
559 match CONFIG_GLOBAL_FIELDS[gi] {
560 "system_prompt" => "系统提示词",
561 "style" => "回复风格",
562 "stream_mode" => "流式输出",
563 "max_history_messages" => "历史消息数",
564 "theme" => "主题风格",
565 "tools_enabled" => "工具调用",
566 "max_tool_rounds" => "工具轮数上限",
567 _ => CONFIG_GLOBAL_FIELDS[gi],
568 }
569 }
570}
571
572pub fn config_field_value(app: &ChatApp, field_idx: usize) -> String {
574 let total_provider = CONFIG_FIELDS.len();
575 if field_idx < total_provider {
576 if app.agent_config.providers.is_empty() {
577 return String::new();
578 }
579 let p = &app.agent_config.providers[app.config_provider_idx];
580 match CONFIG_FIELDS[field_idx] {
581 "name" => p.name.clone(),
582 "api_base" => p.api_base.clone(),
583 "api_key" => {
584 if p.api_key.len() > 8 {
586 format!(
587 "{}****{}",
588 &p.api_key[..4],
589 &p.api_key[p.api_key.len() - 4..]
590 )
591 } else {
592 p.api_key.clone()
593 }
594 }
595 "model" => p.model.clone(),
596 _ => String::new(),
597 }
598 } else {
599 let gi = field_idx - total_provider;
600 match CONFIG_GLOBAL_FIELDS[gi] {
601 "system_prompt" => app.agent_config.system_prompt.clone().unwrap_or_default(),
602 "style" => app.agent_config.style.clone().unwrap_or_default(),
603 "stream_mode" => {
604 if app.agent_config.stream_mode {
605 "开启".into()
606 } else {
607 "关闭".into()
608 }
609 }
610 "max_history_messages" => app.agent_config.max_history_messages.to_string(),
611 "theme" => app.agent_config.theme.display_name().to_string(),
612 "tools_enabled" => {
613 if app.agent_config.tools_enabled {
614 "开启".into()
615 } else {
616 "关闭".into()
617 }
618 }
619 "max_tool_rounds" => app.agent_config.max_tool_rounds.to_string(),
620 _ => String::new(),
621 }
622 }
623}
624
625pub fn config_field_raw_value(app: &ChatApp, field_idx: usize) -> String {
627 let total_provider = CONFIG_FIELDS.len();
628 if field_idx < total_provider {
629 if app.agent_config.providers.is_empty() {
630 return String::new();
631 }
632 let p = &app.agent_config.providers[app.config_provider_idx];
633 match CONFIG_FIELDS[field_idx] {
634 "name" => p.name.clone(),
635 "api_base" => p.api_base.clone(),
636 "api_key" => p.api_key.clone(),
637 "model" => p.model.clone(),
638 _ => String::new(),
639 }
640 } else {
641 let gi = field_idx - total_provider;
642 match CONFIG_GLOBAL_FIELDS[gi] {
643 "system_prompt" => app.agent_config.system_prompt.clone().unwrap_or_default(),
644 "style" => app.agent_config.style.clone().unwrap_or_default(),
645 "stream_mode" => {
646 if app.agent_config.stream_mode {
647 "true".into()
648 } else {
649 "false".into()
650 }
651 }
652 "theme" => app.agent_config.theme.to_str().to_string(),
653 "tools_enabled" => {
654 if app.agent_config.tools_enabled {
655 "true".into()
656 } else {
657 "false".into()
658 }
659 }
660 "max_tool_rounds" => app.agent_config.max_tool_rounds.to_string(),
661 _ => String::new(),
662 }
663 }
664}
665
666pub fn config_field_set(app: &mut ChatApp, field_idx: usize, value: &str) {
668 let total_provider = CONFIG_FIELDS.len();
669 if field_idx < total_provider {
670 if app.agent_config.providers.is_empty() {
671 return;
672 }
673 let p = &mut app.agent_config.providers[app.config_provider_idx];
674 match CONFIG_FIELDS[field_idx] {
675 "name" => p.name = value.to_string(),
676 "api_base" => p.api_base = value.to_string(),
677 "api_key" => p.api_key = value.to_string(),
678 "model" => p.model = value.to_string(),
679 _ => {}
680 }
681 } else {
682 let gi = field_idx - total_provider;
683 match CONFIG_GLOBAL_FIELDS[gi] {
684 "system_prompt" => {
685 if value.is_empty() {
686 app.agent_config.system_prompt = None;
687 } else {
688 app.agent_config.system_prompt = Some(value.to_string());
689 }
690 }
691 "style" => {
692 if value.is_empty() {
693 app.agent_config.style = None;
694 } else {
695 app.agent_config.style = Some(value.to_string());
696 }
697 }
698 "stream_mode" => {
699 app.agent_config.stream_mode = matches!(
700 value.trim().to_lowercase().as_str(),
701 "true" | "1" | "开启" | "on" | "yes"
702 );
703 }
704 "max_history_messages" => {
705 if let Ok(num) = value.trim().parse::<usize>() {
706 app.agent_config.max_history_messages = num;
707 }
708 }
709 "theme" => {
710 app.agent_config.theme = ThemeName::from_str(value.trim());
711 app.theme = super::theme::Theme::from_name(&app.agent_config.theme);
712 app.msg_lines_cache = None;
713 }
714 "tools_enabled" => {
715 app.agent_config.tools_enabled = matches!(
716 value.trim().to_lowercase().as_str(),
717 "true" | "1" | "开启" | "on" | "yes"
718 );
719 }
720 "max_tool_rounds" => {
721 if let Ok(num) = value.trim().parse::<usize>() {
722 app.agent_config.max_tool_rounds = num;
723 }
724 }
725 _ => {}
726 }
727 }
728}
729
730pub fn handle_config_mode(app: &mut ChatApp, key: KeyEvent) {
732 let total_fields = config_total_fields();
733
734 if app.config_editing {
735 match key.code {
737 KeyCode::Esc => {
738 app.config_editing = false;
740 }
741 KeyCode::Enter => {
742 let val = app.config_edit_buf.clone();
744 config_field_set(app, app.config_field_idx, &val);
745 app.config_editing = false;
746 }
747 KeyCode::Backspace => {
748 if app.config_edit_cursor > 0 {
749 let idx = app
750 .config_edit_buf
751 .char_indices()
752 .nth(app.config_edit_cursor - 1)
753 .map(|(i, _)| i)
754 .unwrap_or(0);
755 let end_idx = app
756 .config_edit_buf
757 .char_indices()
758 .nth(app.config_edit_cursor)
759 .map(|(i, _)| i)
760 .unwrap_or(app.config_edit_buf.len());
761 app.config_edit_buf = format!(
762 "{}{}",
763 &app.config_edit_buf[..idx],
764 &app.config_edit_buf[end_idx..]
765 );
766 app.config_edit_cursor -= 1;
767 }
768 }
769 KeyCode::Left => {
770 app.config_edit_cursor = app.config_edit_cursor.saturating_sub(1);
771 }
772 KeyCode::Right => {
773 let char_count = app.config_edit_buf.chars().count();
774 if app.config_edit_cursor < char_count {
775 app.config_edit_cursor += 1;
776 }
777 }
778 KeyCode::Char(c) => {
779 let byte_idx = app
780 .config_edit_buf
781 .char_indices()
782 .nth(app.config_edit_cursor)
783 .map(|(i, _)| i)
784 .unwrap_or(app.config_edit_buf.len());
785 app.config_edit_buf.insert(byte_idx, c);
786 app.config_edit_cursor += 1;
787 }
788 _ => {}
789 }
790 return;
791 }
792
793 match key.code {
795 KeyCode::Esc => {
796 let prompt_saved =
798 save_system_prompt(app.agent_config.system_prompt.as_deref().unwrap_or(""));
799 let style_saved = save_style(app.agent_config.style.as_deref().unwrap_or(""));
800 let config_saved = save_agent_config(&app.agent_config);
801 if prompt_saved && style_saved && config_saved {
802 app.show_toast("配置已保存 ✅", false);
803 } else if !prompt_saved {
804 app.show_toast("系统提示词保存失败", true);
805 } else if !style_saved {
806 app.show_toast("回复风格保存失败", true);
807 } else {
808 app.show_toast("配置保存失败", true);
809 }
810 app.mode = ChatMode::Chat;
811 }
812 KeyCode::Up | KeyCode::Char('k') => {
813 if total_fields > 0 {
814 if app.config_field_idx == 0 {
815 app.config_field_idx = total_fields - 1;
816 } else {
817 app.config_field_idx -= 1;
818 }
819 }
820 }
821 KeyCode::Down | KeyCode::Char('j') => {
822 if total_fields > 0 {
823 app.config_field_idx = (app.config_field_idx + 1) % total_fields;
824 }
825 }
826 KeyCode::Tab | KeyCode::Right => {
827 let count = app.agent_config.providers.len();
829 if count > 1 {
830 app.config_provider_idx = (app.config_provider_idx + 1) % count;
831 }
833 }
834 KeyCode::BackTab | KeyCode::Left => {
835 let count = app.agent_config.providers.len();
837 if count > 1 {
838 if app.config_provider_idx == 0 {
839 app.config_provider_idx = count - 1;
840 } else {
841 app.config_provider_idx -= 1;
842 }
843 }
844 }
845 KeyCode::Enter => {
846 let total_provider = CONFIG_FIELDS.len();
848 if app.config_field_idx < total_provider && app.agent_config.providers.is_empty() {
849 app.show_toast("还没有 Provider,按 a 新增", true);
850 return;
851 }
852 let gi = app.config_field_idx.checked_sub(total_provider);
854 if let Some(gi) = gi {
855 if CONFIG_GLOBAL_FIELDS[gi] == "stream_mode" {
856 app.agent_config.stream_mode = !app.agent_config.stream_mode;
857 return;
858 }
859 if CONFIG_GLOBAL_FIELDS[gi] == "tools_enabled" {
861 app.agent_config.tools_enabled = !app.agent_config.tools_enabled;
862 return;
863 }
864 if CONFIG_GLOBAL_FIELDS[gi] == "theme" {
866 app.switch_theme();
867 return;
868 }
869 if CONFIG_GLOBAL_FIELDS[gi] == "system_prompt" {
871 app.pending_system_prompt_edit = true;
872 return;
873 }
874 if CONFIG_GLOBAL_FIELDS[gi] == "style" {
876 app.pending_style_edit = true;
877 return;
878 }
879 }
880 app.config_edit_buf = config_field_raw_value(app, app.config_field_idx);
881 app.config_edit_cursor = app.config_edit_buf.chars().count();
882 app.config_editing = true;
883 }
884 KeyCode::Char('a') => {
885 let new_provider = ModelProvider {
887 name: format!("Provider-{}", app.agent_config.providers.len() + 1),
888 api_base: "https://api.openai.com/v1".to_string(),
889 api_key: String::new(),
890 model: String::new(),
891 };
892 app.agent_config.providers.push(new_provider);
893 app.config_provider_idx = app.agent_config.providers.len() - 1;
894 app.config_field_idx = 0; app.show_toast("已新增 Provider,请填写配置", false);
896 }
897 KeyCode::Char('d') => {
898 let count = app.agent_config.providers.len();
900 if count == 0 {
901 app.show_toast("没有可删除的 Provider", true);
902 } else {
903 let removed_name = app.agent_config.providers[app.config_provider_idx]
904 .name
905 .clone();
906 app.agent_config.providers.remove(app.config_provider_idx);
907 if app.config_provider_idx >= app.agent_config.providers.len()
909 && app.config_provider_idx > 0
910 {
911 app.config_provider_idx -= 1;
912 }
913 if app.agent_config.active_index >= app.agent_config.providers.len()
915 && app.agent_config.active_index > 0
916 {
917 app.agent_config.active_index -= 1;
918 }
919 app.show_toast(format!("已删除 Provider: {}", removed_name), false);
920 }
921 }
922 KeyCode::Char('s') => {
923 if !app.agent_config.providers.is_empty() {
925 app.agent_config.active_index = app.config_provider_idx;
926 let name = app.agent_config.providers[app.config_provider_idx]
927 .name
928 .clone();
929 app.show_toast(format!("已设为活跃模型: {}", name), false);
930 }
931 }
932 _ => {}
933 }
934}
935
936pub fn handle_select_model(app: &mut ChatApp, key: KeyEvent) {
938 let count = app.agent_config.providers.len();
939 match key.code {
940 KeyCode::Esc => {
941 app.mode = ChatMode::Chat;
942 }
943 KeyCode::Up | KeyCode::Char('k') => {
944 if count > 0 {
945 let i = app
946 .model_list_state
947 .selected()
948 .map(|i| if i == 0 { count - 1 } else { i - 1 })
949 .unwrap_or(0);
950 app.model_list_state.select(Some(i));
951 }
952 }
953 KeyCode::Down | KeyCode::Char('j') => {
954 if count > 0 {
955 let i = app
956 .model_list_state
957 .selected()
958 .map(|i| if i >= count - 1 { 0 } else { i + 1 })
959 .unwrap_or(0);
960 app.model_list_state.select(Some(i));
961 }
962 }
963 KeyCode::Enter => {
964 app.switch_model();
965 }
966 _ => {}
967 }
968}
969
970pub fn handle_archive_confirm_mode(app: &mut ChatApp, key: KeyEvent) {
972 if app.archive_editing_name {
973 match key.code {
975 KeyCode::Esc => {
976 app.archive_editing_name = false;
977 app.archive_custom_name.clear();
978 app.archive_edit_cursor = 0;
979 }
980 KeyCode::Enter => {
981 let name = if app.archive_custom_name.is_empty() {
982 app.archive_default_name.clone()
983 } else {
984 app.archive_custom_name.clone()
985 };
986 if let Err(e) = super::archive::validate_archive_name(&name) {
988 app.show_toast(e, true);
989 return;
990 }
991 if super::archive::archive_exists(&name) {
993 let _ = super::archive::delete_archive(&name);
995 }
996 app.do_archive(&name);
997 }
998 KeyCode::Backspace => {
999 if app.archive_edit_cursor > 0 {
1000 let chars: Vec<char> = app.archive_custom_name.chars().collect();
1001 app.archive_custom_name = chars[..app.archive_edit_cursor - 1]
1002 .iter()
1003 .chain(chars[app.archive_edit_cursor..].iter())
1004 .collect();
1005 app.archive_edit_cursor -= 1;
1006 }
1007 }
1008 KeyCode::Left => {
1009 app.archive_edit_cursor = app.archive_edit_cursor.saturating_sub(1);
1010 }
1011 KeyCode::Right => {
1012 let char_count = app.archive_custom_name.chars().count();
1013 if app.archive_edit_cursor < char_count {
1014 app.archive_edit_cursor += 1;
1015 }
1016 }
1017 KeyCode::Char(c) => {
1018 let chars: Vec<char> = app.archive_custom_name.chars().collect();
1019 app.archive_custom_name = chars[..app.archive_edit_cursor]
1020 .iter()
1021 .chain(std::iter::once(&c))
1022 .chain(chars[app.archive_edit_cursor..].iter())
1023 .collect();
1024 app.archive_edit_cursor += 1;
1025 }
1026 _ => {}
1027 }
1028 } else {
1029 match key.code {
1031 KeyCode::Esc => {
1032 app.mode = ChatMode::Chat;
1033 }
1034 KeyCode::Enter => {
1035 let name = app.archive_default_name.clone();
1037 if super::archive::archive_exists(&name) {
1039 let _ = super::archive::delete_archive(&name);
1040 }
1041 app.do_archive(&name);
1042 }
1043 KeyCode::Char('n') | KeyCode::Char('N') => {
1044 app.archive_editing_name = true;
1046 app.archive_custom_name.clear();
1047 app.archive_edit_cursor = 0;
1048 }
1049 KeyCode::Char('d') | KeyCode::Char('D') => {
1050 app.clear_session();
1052 app.mode = ChatMode::Chat;
1053 }
1054 _ => {}
1055 }
1056 }
1057}
1058
1059pub fn handle_archive_list_mode(app: &mut ChatApp, key: KeyEvent) {
1061 let count = app.archives.len();
1062
1063 if app.restore_confirm_needed {
1065 match key.code {
1066 KeyCode::Esc => {
1067 app.restore_confirm_needed = false;
1068 }
1069 KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
1070 app.do_restore();
1071 }
1072 _ => {}
1073 }
1074 return;
1075 }
1076
1077 match key.code {
1078 KeyCode::Esc => {
1079 app.mode = ChatMode::Chat;
1080 }
1081 KeyCode::Up | KeyCode::Char('k') => {
1082 if count > 0 {
1083 app.archive_list_index = if app.archive_list_index == 0 {
1084 count - 1
1085 } else {
1086 app.archive_list_index - 1
1087 };
1088 }
1089 }
1090 KeyCode::Down | KeyCode::Char('j') => {
1091 if count > 0 {
1092 app.archive_list_index = if app.archive_list_index >= count - 1 {
1093 0
1094 } else {
1095 app.archive_list_index + 1
1096 };
1097 }
1098 }
1099 KeyCode::Enter => {
1100 if count > 0 {
1101 if !app.session.messages.is_empty() {
1103 app.restore_confirm_needed = true;
1104 } else {
1105 app.do_restore();
1106 }
1107 }
1108 }
1109 KeyCode::Char('d') | KeyCode::Char('D') => {
1110 if count > 0 {
1112 app.do_delete_archive();
1113 }
1114 }
1115 _ => {}
1116 }
1117}
1118
1119pub fn handle_tool_confirm_mode(app: &mut ChatApp, key: KeyEvent) {
1121 match key.code {
1122 KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
1123 app.execute_pending_tool();
1124 }
1125 KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
1126 app.reject_pending_tool();
1127 }
1128 _ => {}
1129 }
1130}
1131
1132fn update_at_filter(app: &mut ChatApp) {
1136 let chars: Vec<char> = app.input.chars().collect();
1137 let start = app.at_popup_start_pos + 1; if start <= app.cursor_pos && app.cursor_pos <= chars.len() {
1139 app.at_popup_filter = chars[start..app.cursor_pos].iter().collect();
1140 } else {
1141 app.at_popup_filter.clear();
1142 }
1143 app.at_popup_selected = 0;
1145}
1146
1147pub fn get_filtered_skills(app: &ChatApp) -> Vec<String> {
1149 let filter = app.at_popup_filter.to_lowercase();
1150 app.loaded_skills
1151 .iter()
1152 .map(|s| s.frontmatter.name.clone())
1153 .filter(|name| filter.is_empty() || name.to_lowercase().contains(&filter))
1154 .collect()
1155}
1156
1157fn complete_at_mention(app: &mut ChatApp, skill_name: &str) {
1159 let chars: Vec<char> = app.input.chars().collect();
1160 let before: String = chars[..app.at_popup_start_pos].iter().collect();
1161 let after: String = if app.cursor_pos < chars.len() {
1162 chars[app.cursor_pos..].iter().collect()
1163 } else {
1164 String::new()
1165 };
1166 let replacement = format!("@{} ", skill_name);
1167 let new_cursor = before.chars().count() + replacement.chars().count();
1168 app.input = format!("{}{}{}", before, replacement, after);
1169 app.cursor_pos = new_cursor;
1170}