Skip to main content

j_cli/command/chat/
handler.rs

1use super::app::{CONFIG_FIELDS, CONFIG_GLOBAL_FIELDS, ChatApp, ChatMode, config_total_fields};
2use super::model::{ModelProvider, save_agent_config, save_chat_session};
3use super::render::copy_to_clipboard;
4use super::theme::ThemeName;
5use super::ui::draw_chat_ui;
6use crate::{error, info};
7use crossterm::{
8    event::{
9        self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers,
10        MouseEventKind,
11    },
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, EnableMouseCapture)?;
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                        }
116                    }
117                    Event::Mouse(mouse) => match mouse.kind {
118                        MouseEventKind::ScrollUp => {
119                            app.scroll_up();
120                            needs_redraw = true;
121                        }
122                        MouseEventKind::ScrollDown => {
123                            app.scroll_down();
124                            needs_redraw = true;
125                        }
126                        _ => {}
127                    },
128                    Event::Resize(_, _) => {
129                        needs_redraw = true;
130                    }
131                    _ => {}
132                }
133                // 继续消费剩余事件(非阻塞,Duration::ZERO)
134                if !event::poll(std::time::Duration::ZERO)? {
135                    break;
136                }
137            }
138            if should_break {
139                break;
140            }
141        }
142    }
143
144    // 保存对话历史
145    let _ = save_chat_session(&app.session);
146
147    terminal::disable_raw_mode()?;
148    execute!(
149        terminal.backend_mut(),
150        LeaveAlternateScreen,
151        DisableMouseCapture
152    )?;
153    Ok(())
154}
155
156/// 绘制 TUI 界面
157
158pub fn handle_chat_mode(app: &mut ChatApp, key: KeyEvent) -> bool {
159    // Ctrl+C 强制退出
160    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
161        return true;
162    }
163
164    // Ctrl+T 切换模型(替代 Ctrl+M,因为 Ctrl+M 在终端中等于 Enter)
165    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('t') {
166        if !app.agent_config.providers.is_empty() {
167            app.mode = ChatMode::SelectModel;
168            app.model_list_state
169                .select(Some(app.agent_config.active_index));
170        }
171        return false;
172    }
173
174    // Ctrl+L 归档对话
175    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('l') {
176        if app.session.messages.is_empty() {
177            app.show_toast("当前对话为空,无法归档", true);
178        } else {
179            app.start_archive_confirm();
180        }
181        return false;
182    }
183
184    // Ctrl+R 还原归档
185    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('r') {
186        app.start_archive_list();
187        return false;
188    }
189
190    // Ctrl+Y 复制最后一条 AI 回复
191    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('y') {
192        if let Some(last_ai) = app
193            .session
194            .messages
195            .iter()
196            .rev()
197            .find(|m| m.role == "assistant")
198        {
199            if copy_to_clipboard(&last_ai.content) {
200                app.show_toast("已复制最后一条 AI 回复", false);
201            } else {
202                app.show_toast("复制到剪切板失败", true);
203            }
204        } else {
205            app.show_toast("暂无 AI 回复可复制", true);
206        }
207        return false;
208    }
209
210    // Ctrl+B 进入消息浏览模式(可选中历史消息并复制)
211    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('b') {
212        if !app.session.messages.is_empty() {
213            // 默认选中最后一条消息
214            app.browse_msg_index = app.session.messages.len() - 1;
215            app.mode = ChatMode::Browse;
216            app.msg_lines_cache = None; // 清除缓存以触发高亮重绘
217            // 进入浏览模式时关闭鼠标捕获,让终端可以原生选中文本
218            let _ = execute!(io::stdout(), DisableMouseCapture);
219        } else {
220            app.show_toast("暂无消息可浏览", true);
221        }
222        return false;
223    }
224
225    // Ctrl+E 打开配置界面
226    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('e') {
227        // 初始化配置界面状态
228        app.config_provider_idx = app
229            .agent_config
230            .active_index
231            .min(app.agent_config.providers.len().saturating_sub(1));
232        app.config_field_idx = 0;
233        app.config_editing = false;
234        app.config_edit_buf.clear();
235        app.mode = ChatMode::Config;
236        return false;
237    }
238
239    // Ctrl+S 切换流式/非流式输出
240    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('s') {
241        app.agent_config.stream_mode = !app.agent_config.stream_mode;
242        let _ = save_agent_config(&app.agent_config);
243        let mode_str = if app.agent_config.stream_mode {
244            "流式输出"
245        } else {
246            "整体输出"
247        };
248        app.show_toast(&format!("已切换为: {}", mode_str), false);
249        return false;
250    }
251
252    let char_count = app.input.chars().count();
253
254    match key.code {
255        KeyCode::Esc => return true,
256
257        KeyCode::Enter => {
258            if !app.is_loading {
259                app.send_message();
260            }
261        }
262
263        // 滚动消息
264        KeyCode::Up => app.scroll_up(),
265        KeyCode::Down => app.scroll_down(),
266        KeyCode::PageUp => {
267            for _ in 0..10 {
268                app.scroll_up();
269            }
270        }
271        KeyCode::PageDown => {
272            for _ in 0..10 {
273                app.scroll_down();
274            }
275        }
276
277        // 光标移动
278        KeyCode::Left => {
279            if app.cursor_pos > 0 {
280                app.cursor_pos -= 1;
281            }
282        }
283        KeyCode::Right => {
284            if app.cursor_pos < char_count {
285                app.cursor_pos += 1;
286            }
287        }
288        KeyCode::Home => app.cursor_pos = 0,
289        KeyCode::End => app.cursor_pos = char_count,
290
291        // 删除
292        KeyCode::Backspace => {
293            if app.cursor_pos > 0 {
294                let start = app
295                    .input
296                    .char_indices()
297                    .nth(app.cursor_pos - 1)
298                    .map(|(i, _)| i)
299                    .unwrap_or(0);
300                let end = app
301                    .input
302                    .char_indices()
303                    .nth(app.cursor_pos)
304                    .map(|(i, _)| i)
305                    .unwrap_or(app.input.len());
306                app.input.drain(start..end);
307                app.cursor_pos -= 1;
308            }
309        }
310        KeyCode::Delete => {
311            if app.cursor_pos < char_count {
312                let start = app
313                    .input
314                    .char_indices()
315                    .nth(app.cursor_pos)
316                    .map(|(i, _)| i)
317                    .unwrap_or(app.input.len());
318                let end = app
319                    .input
320                    .char_indices()
321                    .nth(app.cursor_pos + 1)
322                    .map(|(i, _)| i)
323                    .unwrap_or(app.input.len());
324                app.input.drain(start..end);
325            }
326        }
327
328        // F1 任何时候都能唤起帮助
329        KeyCode::F(1) => {
330            app.mode = ChatMode::Help;
331        }
332        // 输入框为空时,? 也可唤起帮助
333        KeyCode::Char('?') if app.input.is_empty() => {
334            app.mode = ChatMode::Help;
335        }
336        KeyCode::Char(c) => {
337            let byte_idx = app
338                .input
339                .char_indices()
340                .nth(app.cursor_pos)
341                .map(|(i, _)| i)
342                .unwrap_or(app.input.len());
343            app.input.insert_str(byte_idx, &c.to_string());
344            app.cursor_pos += 1;
345        }
346
347        _ => {}
348    }
349
350    false
351}
352
353/// 消息浏览模式按键处理:↑↓ 选择消息,y/Enter 复制选中消息,Esc 退出
354pub fn handle_browse_mode(app: &mut ChatApp, key: KeyEvent) {
355    let msg_count = app.session.messages.len();
356    if msg_count == 0 {
357        app.mode = ChatMode::Chat;
358        app.msg_lines_cache = None;
359        // 退出浏览模式时重新开启鼠标捕获
360        let _ = execute!(io::stdout(), EnableMouseCapture);
361        return;
362    }
363
364    match key.code {
365        KeyCode::Esc => {
366            app.mode = ChatMode::Chat;
367            app.msg_lines_cache = None; // 退出浏览模式时清除缓存,去掉高亮
368            // 退出浏览模式时重新开启鼠标捕获
369            let _ = execute!(io::stdout(), EnableMouseCapture);
370        }
371        KeyCode::Up | KeyCode::Char('k') => {
372            if app.browse_msg_index > 0 {
373                app.browse_msg_index -= 1;
374                app.msg_lines_cache = None; // 选中变化时清缓存
375            }
376        }
377        KeyCode::Down | KeyCode::Char('j') => {
378            if app.browse_msg_index < msg_count - 1 {
379                app.browse_msg_index += 1;
380                app.msg_lines_cache = None; // 选中变化时清缓存
381            }
382        }
383        KeyCode::Enter | KeyCode::Char('y') => {
384            // 复制选中消息的原始内容到剪切板
385            if let Some(msg) = app.session.messages.get(app.browse_msg_index) {
386                let content = msg.content.clone();
387                let role_label = if msg.role == "assistant" {
388                    "AI"
389                } else if msg.role == "user" {
390                    "用户"
391                } else {
392                    "系统"
393                };
394                if copy_to_clipboard(&content) {
395                    app.show_toast(
396                        &format!("已复制第 {} 条{}消息", app.browse_msg_index + 1, role_label),
397                        false,
398                    );
399                } else {
400                    app.show_toast("复制到剪切板失败", true);
401                }
402            }
403        }
404        _ => {}
405    }
406}
407
408/// 获取配置界面中当前字段的标签
409pub fn config_field_label(idx: usize) -> &'static str {
410    let total_provider = CONFIG_FIELDS.len();
411    if idx < total_provider {
412        match CONFIG_FIELDS[idx] {
413            "name" => "显示名称",
414            "api_base" => "API Base",
415            "api_key" => "API Key",
416            "model" => "模型名称",
417            _ => CONFIG_FIELDS[idx],
418        }
419    } else {
420        let gi = idx - total_provider;
421        match CONFIG_GLOBAL_FIELDS[gi] {
422            "system_prompt" => "系统提示词",
423            "stream_mode" => "流式输出",
424            "max_history_messages" => "历史消息数",
425            "theme" => "主题风格",
426            _ => CONFIG_GLOBAL_FIELDS[gi],
427        }
428    }
429}
430
431/// 获取配置界面中当前字段的值
432pub fn config_field_value(app: &ChatApp, field_idx: usize) -> String {
433    let total_provider = CONFIG_FIELDS.len();
434    if field_idx < total_provider {
435        if app.agent_config.providers.is_empty() {
436            return String::new();
437        }
438        let p = &app.agent_config.providers[app.config_provider_idx];
439        match CONFIG_FIELDS[field_idx] {
440            "name" => p.name.clone(),
441            "api_base" => p.api_base.clone(),
442            "api_key" => {
443                // 显示时隐藏 API Key 中间部分
444                if p.api_key.len() > 8 {
445                    format!(
446                        "{}****{}",
447                        &p.api_key[..4],
448                        &p.api_key[p.api_key.len() - 4..]
449                    )
450                } else {
451                    p.api_key.clone()
452                }
453            }
454            "model" => p.model.clone(),
455            _ => String::new(),
456        }
457    } else {
458        let gi = field_idx - total_provider;
459        match CONFIG_GLOBAL_FIELDS[gi] {
460            "system_prompt" => app.agent_config.system_prompt.clone().unwrap_or_default(),
461            "stream_mode" => {
462                if app.agent_config.stream_mode {
463                    "开启".into()
464                } else {
465                    "关闭".into()
466                }
467            }
468            "max_history_messages" => app.agent_config.max_history_messages.to_string(),
469            "theme" => app.agent_config.theme.display_name().to_string(),
470            _ => String::new(),
471        }
472    }
473}
474
475/// 获取配置字段的原始值(用于编辑时填入输入框)
476pub fn config_field_raw_value(app: &ChatApp, field_idx: usize) -> String {
477    let total_provider = CONFIG_FIELDS.len();
478    if field_idx < total_provider {
479        if app.agent_config.providers.is_empty() {
480            return String::new();
481        }
482        let p = &app.agent_config.providers[app.config_provider_idx];
483        match CONFIG_FIELDS[field_idx] {
484            "name" => p.name.clone(),
485            "api_base" => p.api_base.clone(),
486            "api_key" => p.api_key.clone(),
487            "model" => p.model.clone(),
488            _ => String::new(),
489        }
490    } else {
491        let gi = field_idx - total_provider;
492        match CONFIG_GLOBAL_FIELDS[gi] {
493            "system_prompt" => app.agent_config.system_prompt.clone().unwrap_or_default(),
494            "stream_mode" => {
495                if app.agent_config.stream_mode {
496                    "true".into()
497                } else {
498                    "false".into()
499                }
500            }
501            "theme" => app.agent_config.theme.to_str().to_string(),
502            _ => String::new(),
503        }
504    }
505}
506
507/// 将编辑结果写回配置
508pub fn config_field_set(app: &mut ChatApp, field_idx: usize, value: &str) {
509    let total_provider = CONFIG_FIELDS.len();
510    if field_idx < total_provider {
511        if app.agent_config.providers.is_empty() {
512            return;
513        }
514        let p = &mut app.agent_config.providers[app.config_provider_idx];
515        match CONFIG_FIELDS[field_idx] {
516            "name" => p.name = value.to_string(),
517            "api_base" => p.api_base = value.to_string(),
518            "api_key" => p.api_key = value.to_string(),
519            "model" => p.model = value.to_string(),
520            _ => {}
521        }
522    } else {
523        let gi = field_idx - total_provider;
524        match CONFIG_GLOBAL_FIELDS[gi] {
525            "system_prompt" => {
526                if value.is_empty() {
527                    app.agent_config.system_prompt = None;
528                } else {
529                    app.agent_config.system_prompt = Some(value.to_string());
530                }
531            }
532            "stream_mode" => {
533                app.agent_config.stream_mode = matches!(
534                    value.trim().to_lowercase().as_str(),
535                    "true" | "1" | "开启" | "on" | "yes"
536                );
537            }
538            "max_history_messages" => {
539                if let Ok(num) = value.trim().parse::<usize>() {
540                    app.agent_config.max_history_messages = num;
541                }
542            }
543            "theme" => {
544                app.agent_config.theme = ThemeName::from_str(value.trim());
545                app.theme = super::theme::Theme::from_name(&app.agent_config.theme);
546                app.msg_lines_cache = None;
547            }
548            _ => {}
549        }
550    }
551}
552
553/// 配置模式按键处理
554pub fn handle_config_mode(app: &mut ChatApp, key: KeyEvent) {
555    let total_fields = config_total_fields();
556
557    if app.config_editing {
558        // 正在编辑某个字段
559        match key.code {
560            KeyCode::Esc => {
561                // 取消编辑
562                app.config_editing = false;
563            }
564            KeyCode::Enter => {
565                // 确认编辑
566                let val = app.config_edit_buf.clone();
567                config_field_set(app, app.config_field_idx, &val);
568                app.config_editing = false;
569            }
570            KeyCode::Backspace => {
571                if app.config_edit_cursor > 0 {
572                    let idx = app
573                        .config_edit_buf
574                        .char_indices()
575                        .nth(app.config_edit_cursor - 1)
576                        .map(|(i, _)| i)
577                        .unwrap_or(0);
578                    let end_idx = app
579                        .config_edit_buf
580                        .char_indices()
581                        .nth(app.config_edit_cursor)
582                        .map(|(i, _)| i)
583                        .unwrap_or(app.config_edit_buf.len());
584                    app.config_edit_buf = format!(
585                        "{}{}",
586                        &app.config_edit_buf[..idx],
587                        &app.config_edit_buf[end_idx..]
588                    );
589                    app.config_edit_cursor -= 1;
590                }
591            }
592            KeyCode::Left => {
593                app.config_edit_cursor = app.config_edit_cursor.saturating_sub(1);
594            }
595            KeyCode::Right => {
596                let char_count = app.config_edit_buf.chars().count();
597                if app.config_edit_cursor < char_count {
598                    app.config_edit_cursor += 1;
599                }
600            }
601            KeyCode::Char(c) => {
602                let byte_idx = app
603                    .config_edit_buf
604                    .char_indices()
605                    .nth(app.config_edit_cursor)
606                    .map(|(i, _)| i)
607                    .unwrap_or(app.config_edit_buf.len());
608                app.config_edit_buf.insert(byte_idx, c);
609                app.config_edit_cursor += 1;
610            }
611            _ => {}
612        }
613        return;
614    }
615
616    // 非编辑状态
617    match key.code {
618        KeyCode::Esc => {
619            // 保存并返回
620            let _ = save_agent_config(&app.agent_config);
621            app.show_toast("配置已保存 ✅", false);
622            app.mode = ChatMode::Chat;
623        }
624        KeyCode::Up | KeyCode::Char('k') => {
625            if total_fields > 0 {
626                if app.config_field_idx == 0 {
627                    app.config_field_idx = total_fields - 1;
628                } else {
629                    app.config_field_idx -= 1;
630                }
631            }
632        }
633        KeyCode::Down | KeyCode::Char('j') => {
634            if total_fields > 0 {
635                app.config_field_idx = (app.config_field_idx + 1) % total_fields;
636            }
637        }
638        KeyCode::Tab | KeyCode::Right => {
639            // 切换 provider
640            let count = app.agent_config.providers.len();
641            if count > 1 {
642                app.config_provider_idx = (app.config_provider_idx + 1) % count;
643                // 切换后如果在 provider 字段区域,保持字段位置不变
644            }
645        }
646        KeyCode::BackTab | KeyCode::Left => {
647            // 反向切换 provider
648            let count = app.agent_config.providers.len();
649            if count > 1 {
650                if app.config_provider_idx == 0 {
651                    app.config_provider_idx = count - 1;
652                } else {
653                    app.config_provider_idx -= 1;
654                }
655            }
656        }
657        KeyCode::Enter => {
658            // 进入编辑模式
659            let total_provider = CONFIG_FIELDS.len();
660            if app.config_field_idx < total_provider && app.agent_config.providers.is_empty() {
661                app.show_toast("还没有 Provider,按 a 新增", true);
662                return;
663            }
664            // stream_mode 字段直接切换,不进入编辑模式
665            let gi = app.config_field_idx.checked_sub(total_provider);
666            if let Some(gi) = gi {
667                if CONFIG_GLOBAL_FIELDS[gi] == "stream_mode" {
668                    app.agent_config.stream_mode = !app.agent_config.stream_mode;
669                    return;
670                }
671                // theme 字段直接循环切换,不进入编辑模式
672                if CONFIG_GLOBAL_FIELDS[gi] == "theme" {
673                    app.switch_theme();
674                    return;
675                }
676            }
677            app.config_edit_buf = config_field_raw_value(app, app.config_field_idx);
678            app.config_edit_cursor = app.config_edit_buf.chars().count();
679            app.config_editing = true;
680        }
681        KeyCode::Char('a') => {
682            // 新增 Provider
683            let new_provider = ModelProvider {
684                name: format!("Provider-{}", app.agent_config.providers.len() + 1),
685                api_base: "https://api.openai.com/v1".to_string(),
686                api_key: String::new(),
687                model: String::new(),
688            };
689            app.agent_config.providers.push(new_provider);
690            app.config_provider_idx = app.agent_config.providers.len() - 1;
691            app.config_field_idx = 0; // 跳到 name 字段
692            app.show_toast("已新增 Provider,请填写配置", false);
693        }
694        KeyCode::Char('d') => {
695            // 删除当前 Provider
696            let count = app.agent_config.providers.len();
697            if count == 0 {
698                app.show_toast("没有可删除的 Provider", true);
699            } else {
700                let removed_name = app.agent_config.providers[app.config_provider_idx]
701                    .name
702                    .clone();
703                app.agent_config.providers.remove(app.config_provider_idx);
704                // 调整索引
705                if app.config_provider_idx >= app.agent_config.providers.len()
706                    && app.config_provider_idx > 0
707                {
708                    app.config_provider_idx -= 1;
709                }
710                // 调整 active_index
711                if app.agent_config.active_index >= app.agent_config.providers.len()
712                    && app.agent_config.active_index > 0
713                {
714                    app.agent_config.active_index -= 1;
715                }
716                app.show_toast(format!("已删除 Provider: {}", removed_name), false);
717            }
718        }
719        KeyCode::Char('s') => {
720            // 将当前 provider 设为活跃
721            if !app.agent_config.providers.is_empty() {
722                app.agent_config.active_index = app.config_provider_idx;
723                let name = app.agent_config.providers[app.config_provider_idx]
724                    .name
725                    .clone();
726                app.show_toast(format!("已设为活跃模型: {}", name), false);
727            }
728        }
729        _ => {}
730    }
731}
732
733/// 绘制配置编辑界面
734pub fn handle_select_model(app: &mut ChatApp, key: KeyEvent) {
735    let count = app.agent_config.providers.len();
736    match key.code {
737        KeyCode::Esc => {
738            app.mode = ChatMode::Chat;
739        }
740        KeyCode::Up | KeyCode::Char('k') => {
741            if count > 0 {
742                let i = app
743                    .model_list_state
744                    .selected()
745                    .map(|i| if i == 0 { count - 1 } else { i - 1 })
746                    .unwrap_or(0);
747                app.model_list_state.select(Some(i));
748            }
749        }
750        KeyCode::Down | KeyCode::Char('j') => {
751            if count > 0 {
752                let i = app
753                    .model_list_state
754                    .selected()
755                    .map(|i| if i >= count - 1 { 0 } else { i + 1 })
756                    .unwrap_or(0);
757                app.model_list_state.select(Some(i));
758            }
759        }
760        KeyCode::Enter => {
761            app.switch_model();
762        }
763        _ => {}
764    }
765}
766
767/// 归档确认模式按键处理
768pub fn handle_archive_confirm_mode(app: &mut ChatApp, key: KeyEvent) {
769    if app.archive_editing_name {
770        // 正在编辑自定义名称
771        match key.code {
772            KeyCode::Esc => {
773                app.archive_editing_name = false;
774                app.archive_custom_name.clear();
775                app.archive_edit_cursor = 0;
776            }
777            KeyCode::Enter => {
778                let name = if app.archive_custom_name.is_empty() {
779                    app.archive_default_name.clone()
780                } else {
781                    app.archive_custom_name.clone()
782                };
783                // 验证名称
784                if let Err(e) = super::archive::validate_archive_name(&name) {
785                    app.show_toast(e, true);
786                    return;
787                }
788                // 检查是否重名
789                if super::archive::archive_exists(&name) {
790                    // 直接覆盖
791                    let _ = super::archive::delete_archive(&name);
792                }
793                app.do_archive(&name);
794            }
795            KeyCode::Backspace => {
796                if app.archive_edit_cursor > 0 {
797                    let chars: Vec<char> = app.archive_custom_name.chars().collect();
798                    app.archive_custom_name = chars[..app.archive_edit_cursor - 1]
799                        .iter()
800                        .chain(chars[app.archive_edit_cursor..].iter())
801                        .collect();
802                    app.archive_edit_cursor -= 1;
803                }
804            }
805            KeyCode::Left => {
806                app.archive_edit_cursor = app.archive_edit_cursor.saturating_sub(1);
807            }
808            KeyCode::Right => {
809                let char_count = app.archive_custom_name.chars().count();
810                if app.archive_edit_cursor < char_count {
811                    app.archive_edit_cursor += 1;
812                }
813            }
814            KeyCode::Char(c) => {
815                let chars: Vec<char> = app.archive_custom_name.chars().collect();
816                app.archive_custom_name = chars[..app.archive_edit_cursor]
817                    .iter()
818                    .chain(std::iter::once(&c))
819                    .chain(chars[app.archive_edit_cursor..].iter())
820                    .collect();
821                app.archive_edit_cursor += 1;
822            }
823            _ => {}
824        }
825    } else {
826        // 非编辑状态
827        match key.code {
828            KeyCode::Esc => {
829                app.mode = ChatMode::Chat;
830            }
831            KeyCode::Enter => {
832                // 使用默认名称归档
833                let name = app.archive_default_name.clone();
834                // 检查是否重名(generate_default_archive_name 应该已经处理了重名,但这里可能用户一直在同一个界面)
835                if super::archive::archive_exists(&name) {
836                    let _ = super::archive::delete_archive(&name);
837                }
838                app.do_archive(&name);
839            }
840            KeyCode::Char('n') | KeyCode::Char('N') => {
841                // 进入编辑自定义名称模式
842                app.archive_editing_name = true;
843                app.archive_custom_name.clear();
844                app.archive_edit_cursor = 0;
845            }
846            KeyCode::Char('d') | KeyCode::Char('D') => {
847                // 仅清空对话,不归档
848                app.clear_session();
849                app.mode = ChatMode::Chat;
850            }
851            _ => {}
852        }
853    }
854}
855
856/// 归档列表模式按键处理
857pub fn handle_archive_list_mode(app: &mut ChatApp, key: KeyEvent) {
858    let count = app.archives.len();
859
860    // 如果需要确认还原
861    if app.restore_confirm_needed {
862        match key.code {
863            KeyCode::Esc => {
864                app.restore_confirm_needed = false;
865            }
866            KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
867                app.do_restore();
868            }
869            _ => {}
870        }
871        return;
872    }
873
874    match key.code {
875        KeyCode::Esc => {
876            app.mode = ChatMode::Chat;
877        }
878        KeyCode::Up | KeyCode::Char('k') => {
879            if count > 0 {
880                app.archive_list_index = if app.archive_list_index == 0 {
881                    count - 1
882                } else {
883                    app.archive_list_index - 1
884                };
885            }
886        }
887        KeyCode::Down | KeyCode::Char('j') => {
888            if count > 0 {
889                app.archive_list_index = if app.archive_list_index >= count - 1 {
890                    0
891                } else {
892                    app.archive_list_index + 1
893                };
894            }
895        }
896        KeyCode::Enter => {
897            if count > 0 {
898                // 如果当前会话有消息,需要确认
899                if !app.session.messages.is_empty() {
900                    app.restore_confirm_needed = true;
901                } else {
902                    app.do_restore();
903                }
904            }
905        }
906        KeyCode::Char('d') | KeyCode::Char('D') => {
907            // 删除选中的归档
908            if count > 0 {
909                app.do_delete_archive();
910            }
911        }
912        _ => {}
913    }
914}