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