Skip to main content

j_cli/command/chat/
handler.rs

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; // 首次必须绘制
45
46    loop {
47        // 清理过期 toast(如果有 toast 被清理,需要重绘)
48        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        // 非阻塞地处理后台流式消息
55        let was_loading = app.is_loading;
56        app.poll_stream();
57        // 流式加载中使用节流策略:只在内容增长超过阈值或超时才重绘
58        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            // 每增加 200 字节或距离上次渲染超过 200ms 才重绘
63            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            // 加载刚结束时必须重绘一次
71            needs_redraw = true;
72        }
73
74        // 只在状态发生变化时才重绘,大幅降低 CPU 占用
75        if needs_redraw {
76            terminal.draw(|f| draw_chat_ui(f, &mut app))?;
77            needs_redraw = false;
78            // 更新流式节流状态
79            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        // 等待事件:加载中用短间隔以刷新流式内容,空闲时用长间隔节省 CPU
86        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            // 批量消费所有待处理事件,避免快速滚动/打字时事件堆积
94            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                // 继续消费剩余事件(非阻塞,Duration::ZERO)
124                if !event::poll(std::time::Duration::ZERO)? {
125                    break;
126                }
127            }
128            if should_break {
129                break;
130            }
131
132            // 检查 system_prompt 全屏编辑器标志
133            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                    &current_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                        // 用户取消编辑
156                    }
157                    Err(e) => {
158                        app.show_toast(format!("编辑器错误: {}", e), true);
159                    }
160                }
161                needs_redraw = true;
162            }
163
164            // 检查 style 全屏编辑器标志
165            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                    &current_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                        // 用户取消编辑
188                    }
189                    Err(e) => {
190                        app.show_toast(format!("编辑器错误: {}", e), true);
191                    }
192                }
193                needs_redraw = true;
194            }
195        }
196    }
197
198    // 保存对话历史
199    let _ = save_chat_session(&app.session);
200
201    terminal::disable_raw_mode()?;
202    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
203    Ok(())
204}
205
206/// 绘制 TUI 界面
207
208pub fn handle_chat_mode(app: &mut ChatApp, key: KeyEvent) -> bool {
209    // Ctrl+C 强制退出
210    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
211        return true;
212    }
213
214    // ===== @ 补全弹窗拦截 =====
215    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                // 空格关闭弹窗,正常处理字符
246                app.at_popup_active = false;
247                // fall through to normal char handling below
248            }
249            KeyCode::Backspace => {
250                // 先执行删除,然后检查弹窗状态
251                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                // 如果光标退回到 @ 之前,关闭弹窗
268                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                // 其他按键不拦截,落入正常处理
277            }
278        }
279    }
280
281    // Ctrl+T 切换模型(替代 Ctrl+M,因为 Ctrl+M 在终端中等于 Enter)
282    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    // Ctrl+L 归档对话
292    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    // Ctrl+R 还原归档
302    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('r') {
303        app.start_archive_list();
304        return false;
305    }
306
307    // Ctrl+Y 复制最后一条 AI 回复
308    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    // Ctrl+B 进入消息浏览模式(可选中历史消息并复制)
328    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('b') {
329        if !app.session.messages.is_empty() {
330            // 默认选中最后一条消息
331            app.browse_msg_index = app.session.messages.len() - 1;
332            app.browse_scroll_offset = 0; // 重置消息内偏移
333            app.mode = ChatMode::Browse;
334            app.msg_lines_cache = None; // 清除缓存以触发高亮重绘
335        } else {
336            app.show_toast("暂无消息可浏览", true);
337        }
338        return false;
339    }
340
341    // Ctrl+E 打开配置界面
342    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('e') {
343        // 初始化配置界面状态
344        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    // Ctrl+S 切换流式/非流式输出
356    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        // 滚动消息
380        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        // 光标移动
394        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        // 删除
408        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        // F1 任何时候都能唤起帮助
445        KeyCode::F(1) => {
446            app.mode = ChatMode::Help;
447        }
448        // 输入框为空时,? 也可唤起帮助
449        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            // @ 补全弹窗触发逻辑
463            if c == '@' && !app.loaded_skills.is_empty() {
464                // @ 在行首或前一个字符是空白
465                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
486/// 消息浏览模式按键处理:↑↓ 选择消息,y/Enter 复制选中消息,Esc 退出
487pub 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; // 退出浏览模式时清除缓存,去掉高亮
499        }
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; // 切换消息时从头显示
504                app.msg_lines_cache = None; // 选中变化时清缓存
505            }
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; // 切换消息时从头显示
511                app.msg_lines_cache = None; // 选中变化时清缓存
512            }
513        }
514        // A/D 细粒度滚动当前消息内容(每次 3 行)
515        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            // 复制选中消息的原始内容到剪切板
523            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
546/// 获取配置界面中当前字段的标签
547pub 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
572/// 获取配置界面中当前字段的值
573pub 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                // 显示时隐藏 API Key 中间部分
585                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
625/// 获取配置字段的原始值(用于编辑时填入输入框)
626pub 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
666/// 将编辑结果写回配置
667pub 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
730/// 配置模式按键处理
731pub fn handle_config_mode(app: &mut ChatApp, key: KeyEvent) {
732    let total_fields = config_total_fields();
733
734    if app.config_editing {
735        // 正在编辑某个字段
736        match key.code {
737            KeyCode::Esc => {
738                // 取消编辑
739                app.config_editing = false;
740            }
741            KeyCode::Enter => {
742                // 确认编辑
743                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    // 非编辑状态
794    match key.code {
795        KeyCode::Esc => {
796            // 保存并返回
797            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            // 切换 provider
828            let count = app.agent_config.providers.len();
829            if count > 1 {
830                app.config_provider_idx = (app.config_provider_idx + 1) % count;
831                // 切换后如果在 provider 字段区域,保持字段位置不变
832            }
833        }
834        KeyCode::BackTab | KeyCode::Left => {
835            // 反向切换 provider
836            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            // 进入编辑模式
847            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            // stream_mode 字段直接切换,不进入编辑模式
853            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                // tools_enabled 字段直接切换
860                if CONFIG_GLOBAL_FIELDS[gi] == "tools_enabled" {
861                    app.agent_config.tools_enabled = !app.agent_config.tools_enabled;
862                    return;
863                }
864                // theme 字段直接循环切换,不进入编辑模式
865                if CONFIG_GLOBAL_FIELDS[gi] == "theme" {
866                    app.switch_theme();
867                    return;
868                }
869                // system_prompt 字段使用全屏编辑器
870                if CONFIG_GLOBAL_FIELDS[gi] == "system_prompt" {
871                    app.pending_system_prompt_edit = true;
872                    return;
873                }
874                // style 字段使用全屏编辑器
875                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            // 新增 Provider
886            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; // 跳到 name 字段
895            app.show_toast("已新增 Provider,请填写配置", false);
896        }
897        KeyCode::Char('d') => {
898            // 删除当前 Provider
899            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                // 调整索引
908                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                // 调整 active_index
914                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            // 将当前 provider 设为活跃
924            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
936/// 绘制配置编辑界面
937pub 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
970/// 归档确认模式按键处理
971pub fn handle_archive_confirm_mode(app: &mut ChatApp, key: KeyEvent) {
972    if app.archive_editing_name {
973        // 正在编辑自定义名称
974        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                // 验证名称
987                if let Err(e) = super::archive::validate_archive_name(&name) {
988                    app.show_toast(e, true);
989                    return;
990                }
991                // 检查是否重名
992                if super::archive::archive_exists(&name) {
993                    // 直接覆盖
994                    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        // 非编辑状态
1030        match key.code {
1031            KeyCode::Esc => {
1032                app.mode = ChatMode::Chat;
1033            }
1034            KeyCode::Enter => {
1035                // 使用默认名称归档
1036                let name = app.archive_default_name.clone();
1037                // 检查是否重名(generate_default_archive_name 应该已经处理了重名,但这里可能用户一直在同一个界面)
1038                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                // 进入编辑自定义名称模式
1045                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                // 仅清空对话,不归档
1051                app.clear_session();
1052                app.mode = ChatMode::Chat;
1053            }
1054            _ => {}
1055        }
1056    }
1057}
1058
1059/// 归档列表模式按键处理
1060pub fn handle_archive_list_mode(app: &mut ChatApp, key: KeyEvent) {
1061    let count = app.archives.len();
1062
1063    // 如果需要确认还原
1064    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                // 如果当前会话有消息,需要确认
1102                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            // 删除选中的归档
1111            if count > 0 {
1112                app.do_delete_archive();
1113            }
1114        }
1115        _ => {}
1116    }
1117}
1118
1119/// 工具确认模式按键处理:Y/Enter 执行,N/Esc 拒绝
1120pub 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
1132// ========== @ 补全辅助函数 ==========
1133
1134/// 从 input 中提取 @ 之后的过滤文本
1135fn update_at_filter(app: &mut ChatApp) {
1136    let chars: Vec<char> = app.input.chars().collect();
1137    let start = app.at_popup_start_pos + 1; // @ 之后
1138    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    // 重置选中索引
1144    app.at_popup_selected = 0;
1145}
1146
1147/// 根据 filter 过滤 loaded_skills 的 name 列表
1148pub 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
1157/// 替换 input 中 @... 为 @skill_name 并加空格
1158fn 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}