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