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