Skip to main content

j_cli/command/chat/
ui.rs

1use super::app::{CONFIG_FIELDS, CONFIG_GLOBAL_FIELDS, ChatApp, ChatMode, MsgLinesCache};
2use super::handler::{config_field_label, config_field_value};
3use super::model::agent_config_path;
4use super::render::{build_message_lines_incremental, char_width, display_width, wrap_text};
5use ratatui::{
6    layout::{Constraint, Direction, Layout, Rect},
7    style::{Modifier, Style},
8    text::{Line, Span},
9    widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
10};
11
12pub fn draw_chat_ui(f: &mut ratatui::Frame, app: &mut ChatApp) {
13    let size = f.area();
14
15    // 整体背景
16    let bg = Block::default().style(Style::default().bg(app.theme.bg_primary));
17    f.render_widget(bg, size);
18
19    let chunks = Layout::default()
20        .direction(Direction::Vertical)
21        .constraints([
22            Constraint::Length(3), // 标题栏
23            Constraint::Min(5),    // 消息区
24            Constraint::Length(5), // 输入区
25            Constraint::Length(1), // 操作提示栏(始终可见)
26        ])
27        .split(size);
28
29    // ========== 标题栏 ==========
30    draw_title_bar(f, chunks[0], app);
31
32    // ========== 消息区 ==========
33    if app.mode == ChatMode::Help {
34        draw_help(f, chunks[1], app);
35    } else if app.mode == ChatMode::SelectModel {
36        draw_model_selector(f, chunks[1], app);
37    } else if app.mode == ChatMode::Config {
38        draw_config_screen(f, chunks[1], app);
39    } else if app.mode == ChatMode::ArchiveConfirm {
40        draw_archive_confirm(f, chunks[1], app);
41    } else if app.mode == ChatMode::ArchiveList {
42        draw_archive_list(f, chunks[1], app);
43    } else {
44        draw_messages(f, chunks[1], app);
45    }
46
47    // ========== 输入区 ==========
48    draw_input(f, chunks[2], app);
49
50    // ========== 底部操作提示栏(始终可见)==========
51    draw_hint_bar(f, chunks[3], app);
52
53    // ========== Toast 弹窗覆盖层(右上角)==========
54    draw_toast(f, size, app);
55}
56
57/// 绘制标题栏
58pub fn draw_title_bar(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
59    let t = &app.theme;
60    let model_name = app.active_model_name();
61    let msg_count = app.session.messages.len();
62    let loading = if app.is_loading {
63        " ⏳ 思考中..."
64    } else {
65        ""
66    };
67
68    let title_spans = vec![
69        Span::styled(" 💬 ", Style::default().fg(t.title_icon)),
70        Span::styled(
71            "AI Chat",
72            Style::default()
73                .fg(t.text_white)
74                .add_modifier(Modifier::BOLD),
75        ),
76        Span::styled("  │  ", Style::default().fg(t.title_separator)),
77        Span::styled("🤖 ", Style::default()),
78        Span::styled(
79            model_name,
80            Style::default()
81                .fg(t.title_model)
82                .add_modifier(Modifier::BOLD),
83        ),
84        Span::styled("  │  ", Style::default().fg(t.title_separator)),
85        Span::styled(
86            format!("📨 {} 条消息", msg_count),
87            Style::default().fg(t.title_count),
88        ),
89        Span::styled(
90            loading,
91            Style::default()
92                .fg(t.title_loading)
93                .add_modifier(Modifier::BOLD),
94        ),
95    ];
96
97    let title_block = Paragraph::new(Line::from(title_spans)).block(
98        Block::default()
99            .borders(Borders::ALL)
100            .border_type(ratatui::widgets::BorderType::Rounded)
101            .border_style(Style::default().fg(t.border_title))
102            .style(Style::default().bg(t.bg_title)),
103    );
104    f.render_widget(title_block, area);
105}
106
107/// 绘制消息区
108pub fn draw_messages(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
109    let t = &app.theme;
110    let block = Block::default()
111        .borders(Borders::ALL)
112        .border_type(ratatui::widgets::BorderType::Rounded)
113        .border_style(Style::default().fg(t.border_message))
114        .title(Span::styled(
115            " 对话记录 ",
116            Style::default().fg(t.text_dim).add_modifier(Modifier::BOLD),
117        ))
118        .title_alignment(ratatui::layout::Alignment::Left)
119        .style(Style::default().bg(t.bg_primary));
120
121    // 空消息时显示欢迎界面
122    if app.session.messages.is_empty() && !app.is_loading {
123        let welcome_lines = vec![
124            Line::from(""),
125            Line::from(""),
126            Line::from(Span::styled(
127                "  ╭──────────────────────────────────────╮",
128                Style::default().fg(t.welcome_border),
129            )),
130            Line::from(Span::styled(
131                "  │                                      │",
132                Style::default().fg(t.welcome_border),
133            )),
134            Line::from(vec![
135                Span::styled("  │     ", Style::default().fg(t.welcome_border)),
136                Span::styled(
137                    "Hi! What can I help you?  ",
138                    Style::default().fg(t.welcome_text),
139                ),
140                Span::styled("     │", Style::default().fg(t.welcome_border)),
141            ]),
142            Line::from(Span::styled(
143                "  │                                      │",
144                Style::default().fg(t.welcome_border),
145            )),
146            Line::from(Span::styled(
147                "  │     Type a message, press Enter      │",
148                Style::default().fg(t.welcome_hint),
149            )),
150            Line::from(Span::styled(
151                "  │                                      │",
152                Style::default().fg(t.welcome_border),
153            )),
154            Line::from(Span::styled(
155                "  ╰──────────────────────────────────────╯",
156                Style::default().fg(t.welcome_border),
157            )),
158        ];
159        let empty = Paragraph::new(welcome_lines).block(block);
160        f.render_widget(empty, area);
161        return;
162    }
163
164    // 内部可用宽度(减去边框和左右各1的 padding)
165    let inner_width = area.width.saturating_sub(4) as usize;
166    // 消息内容最大宽度为可用宽度的 75%
167    let bubble_max_width = (inner_width * 75 / 100).max(20);
168
169    // 计算缓存 key:消息数 + 最后一条消息长度 + 流式内容长度 + is_loading + 气泡宽度 + 浏览模式索引
170    let msg_count = app.session.messages.len();
171    let last_msg_len = app
172        .session
173        .messages
174        .last()
175        .map(|m| m.content.len())
176        .unwrap_or(0);
177    let streaming_len = app.streaming_content.lock().unwrap().len();
178    let current_browse_index = if app.mode == ChatMode::Browse {
179        Some(app.browse_msg_index)
180    } else {
181        None
182    };
183    let cache_hit = if let Some(ref cache) = app.msg_lines_cache {
184        cache.msg_count == msg_count
185            && cache.last_msg_len == last_msg_len
186            && cache.streaming_len == streaming_len
187            && cache.is_loading == app.is_loading
188            && cache.bubble_max_width == bubble_max_width
189            && cache.browse_index == current_browse_index
190    } else {
191        false
192    };
193
194    if !cache_hit {
195        // 缓存未命中,增量构建渲染行
196        let old_cache = app.msg_lines_cache.take();
197        let (new_lines, new_msg_start_lines, new_per_msg, new_stable_lines, new_stable_offset) =
198            build_message_lines_incremental(app, inner_width, bubble_max_width, old_cache.as_ref());
199        app.msg_lines_cache = Some(MsgLinesCache {
200            msg_count,
201            last_msg_len,
202            streaming_len,
203            is_loading: app.is_loading,
204            bubble_max_width,
205            browse_index: current_browse_index,
206            lines: new_lines,
207            msg_start_lines: new_msg_start_lines,
208            per_msg_lines: new_per_msg,
209            streaming_stable_lines: new_stable_lines,
210            streaming_stable_offset: new_stable_offset,
211        });
212    }
213
214    // 从缓存中借用 lines(零拷贝)
215    let cached = app.msg_lines_cache.as_ref().unwrap();
216    let all_lines = &cached.lines;
217    let total_lines = all_lines.len() as u16;
218
219    // 渲染边框
220    f.render_widget(block, area);
221
222    // 计算内部区域(去掉边框)
223    let inner = area.inner(ratatui::layout::Margin {
224        vertical: 1,
225        horizontal: 1,
226    });
227    let visible_height = inner.height;
228    let max_scroll = total_lines.saturating_sub(visible_height);
229
230    // 自动滚动到底部(非浏览模式下)
231    if app.mode != ChatMode::Browse {
232        if app.scroll_offset == u16::MAX || app.scroll_offset > max_scroll {
233            app.scroll_offset = max_scroll;
234            // 已经在底部,恢复自动滚动
235            app.auto_scroll = true;
236        }
237    } else {
238        // 浏览模式:scroll_offset 由 msg_start + browse_scroll_offset 决定
239        // browse_scroll_offset 是相对于选中消息起始行的偏移(A/D 键控制)
240        if let Some(msg_start) = cached
241            .msg_start_lines
242            .iter()
243            .find(|(idx, _)| *idx == app.browse_msg_index)
244            .map(|(_, line)| *line as u16)
245        {
246            // 计算当前消息的行数,限制向下滚动的上限
247            let msg_line_count = cached
248                .per_msg_lines
249                .get(app.browse_msg_index)
250                .map(|c| c.lines.len())
251                .unwrap_or(1) as u16;
252            let msg_max_scroll = msg_line_count.saturating_sub(visible_height);
253            if app.browse_scroll_offset > msg_max_scroll {
254                app.browse_scroll_offset = msg_max_scroll;
255            }
256            // 全局滚动位置 = 消息起始行 + 消息内偏移,不超过全局最大值
257            app.scroll_offset = (msg_start + app.browse_scroll_offset).min(max_scroll);
258        }
259    }
260
261    // 填充内部背景色(避免空白行没有背景)
262    let bg_fill = Block::default().style(Style::default().bg(app.theme.bg_primary));
263    f.render_widget(bg_fill, inner);
264
265    // 只渲染可见区域的行(逐行借用缓存,clone 单行开销极小)
266    let start = app.scroll_offset as usize;
267    let end = (start + visible_height as usize).min(all_lines.len());
268    let msg_area_bg = Style::default().bg(app.theme.bg_primary);
269    for (i, line_idx) in (start..end).enumerate() {
270        let line = &all_lines[line_idx];
271        let y = inner.y + i as u16;
272        let line_area = Rect::new(inner.x, y, inner.width, 1);
273        // 使用 Paragraph 渲染单行,设置背景色确保行尾空余区域颜色一致
274        let p = Paragraph::new(line.clone()).style(msg_area_bg);
275        f.render_widget(p, line_area);
276    }
277}
278
279/// 查找流式内容中最后一个安全的段落边界(双换行),
280/// 但要排除代码块内部的双换行(未闭合的 ``` 之后的内容不能拆分)。
281
282pub fn draw_input(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
283    let t = &app.theme;
284    // 输入区可用宽度(减去边框2 + prompt 4)
285    let usable_width = area.width.saturating_sub(2 + 4) as usize;
286
287    let chars: Vec<char> = app.input.chars().collect();
288
289    // 计算光标之前文本的显示宽度,决定是否需要水平滚动
290    let before_all: String = chars[..app.cursor_pos].iter().collect();
291    let before_width = display_width(&before_all);
292
293    // 如果光标超出可视范围,从光标附近开始显示
294    let scroll_offset_chars = if before_width >= usable_width {
295        // 往回找到一个合适的起始字符位置
296        let target_width = before_width.saturating_sub(usable_width / 2);
297        let mut w = 0;
298        let mut skip = 0;
299        for (i, &ch) in chars.iter().enumerate() {
300            if w >= target_width {
301                skip = i;
302                break;
303            }
304            w += char_width(ch);
305        }
306        skip
307    } else {
308        0
309    };
310
311    // 截取可见部分的字符
312    let visible_chars = &chars[scroll_offset_chars..];
313    let cursor_in_visible = app.cursor_pos - scroll_offset_chars;
314
315    let before: String = visible_chars[..cursor_in_visible].iter().collect();
316    let cursor_ch = if cursor_in_visible < visible_chars.len() {
317        visible_chars[cursor_in_visible].to_string()
318    } else {
319        " ".to_string()
320    };
321    let after: String = if cursor_in_visible < visible_chars.len() {
322        visible_chars[cursor_in_visible + 1..].iter().collect()
323    } else {
324        String::new()
325    };
326
327    let prompt_style = if app.is_loading {
328        Style::default().fg(t.input_prompt_loading)
329    } else {
330        Style::default().fg(t.input_prompt)
331    };
332    let prompt_text = if app.is_loading { " .. " } else { " >  " };
333
334    // 构建多行输入显示(手动换行)
335    let full_visible = format!("{}{}{}", before, cursor_ch, after);
336    let inner_height = area.height.saturating_sub(2) as usize; // 减去边框
337    let wrapped_lines = wrap_text(&full_visible, usable_width);
338
339    // 找到光标所在的行索引
340    let before_len = before.chars().count();
341    let cursor_len = cursor_ch.chars().count();
342    let cursor_global_pos = before_len; // 光标在全部可见字符中的位置
343    let mut cursor_line_idx: usize = 0;
344    {
345        let mut cumulative = 0usize;
346        for (li, wl) in wrapped_lines.iter().enumerate() {
347            let line_char_count = wl.chars().count();
348            if cumulative + line_char_count > cursor_global_pos {
349                cursor_line_idx = li;
350                break;
351            }
352            cumulative += line_char_count;
353            cursor_line_idx = li; // 光标恰好在最后一行末尾
354        }
355    }
356
357    // 计算行滚动:确保光标所在行在可见区域内
358    let line_scroll = if wrapped_lines.len() <= inner_height {
359        0
360    } else if cursor_line_idx < inner_height {
361        0
362    } else {
363        // 让光标行显示在可见区域的最后一行
364        cursor_line_idx.saturating_sub(inner_height - 1)
365    };
366
367    // 构建带光标高亮的行
368    let mut display_lines: Vec<Line> = Vec::new();
369    let mut char_offset: usize = 0;
370    // 跳过滚动行的字符数
371    for wl in wrapped_lines.iter().take(line_scroll) {
372        char_offset += wl.chars().count();
373    }
374
375    for (_line_idx, wl) in wrapped_lines
376        .iter()
377        .skip(line_scroll)
378        .enumerate()
379        .take(inner_height.max(1))
380    {
381        let mut spans: Vec<Span> = Vec::new();
382        if _line_idx == 0 && line_scroll == 0 {
383            spans.push(Span::styled(prompt_text, prompt_style));
384        } else {
385            spans.push(Span::styled("    ", Style::default())); // 对齐 prompt
386        }
387
388        // 对该行的每个字符分配样式
389        let line_chars: Vec<char> = wl.chars().collect();
390        let mut seg_start = 0;
391        for (ci, &ch) in line_chars.iter().enumerate() {
392            let global_idx = char_offset + ci;
393            let is_cursor = global_idx >= before_len && global_idx < before_len + cursor_len;
394
395            if is_cursor {
396                // 先把 cursor 前的部分输出
397                if ci > seg_start {
398                    let seg: String = line_chars[seg_start..ci].iter().collect();
399                    spans.push(Span::styled(seg, Style::default().fg(t.text_white)));
400                }
401                spans.push(Span::styled(
402                    ch.to_string(),
403                    Style::default().fg(t.cursor_fg).bg(t.cursor_bg),
404                ));
405                seg_start = ci + 1;
406            }
407        }
408        // 输出剩余部分
409        if seg_start < line_chars.len() {
410            let seg: String = line_chars[seg_start..].iter().collect();
411            spans.push(Span::styled(seg, Style::default().fg(t.text_white)));
412        }
413
414        char_offset += line_chars.len();
415        display_lines.push(Line::from(spans));
416    }
417
418    if display_lines.is_empty() {
419        display_lines.push(Line::from(vec![
420            Span::styled(prompt_text, prompt_style),
421            Span::styled(" ", Style::default().fg(t.cursor_fg).bg(t.cursor_bg)),
422        ]));
423    }
424
425    let input_widget = Paragraph::new(display_lines).block(
426        Block::default()
427            .borders(Borders::ALL)
428            .border_type(ratatui::widgets::BorderType::Rounded)
429            .border_style(if app.is_loading {
430                Style::default().fg(t.border_input_loading)
431            } else {
432                Style::default().fg(t.border_input)
433            })
434            .title(Span::styled(" 输入消息 ", Style::default().fg(t.text_dim)))
435            .style(Style::default().bg(t.bg_input)),
436    );
437
438    f.render_widget(input_widget, area);
439
440    // 设置终端光标位置,确保中文输入法 IME 候选窗口在正确位置
441    // 计算光标在渲染后的坐标
442    if !app.is_loading {
443        let prompt_w: u16 = 4; // prompt 宽度
444        let border_left: u16 = 1; // 左边框
445
446        // 光标在当前显示行中的列偏移
447        let cursor_col_in_line = {
448            let mut col = 0usize;
449            let mut char_count = 0usize;
450            // 跳过 line_scroll 之前的字符
451            let mut skip_chars = 0usize;
452            for wl in wrapped_lines.iter().take(line_scroll) {
453                skip_chars += wl.chars().count();
454            }
455            // 找到光标在当前行的列
456            for wl in wrapped_lines.iter().skip(line_scroll) {
457                let line_len = wl.chars().count();
458                if skip_chars + char_count + line_len > cursor_global_pos {
459                    // 光标在这一行
460                    let pos_in_line = cursor_global_pos - (skip_chars + char_count);
461                    col = wl.chars().take(pos_in_line).map(|c| char_width(c)).sum();
462                    break;
463                }
464                char_count += line_len;
465            }
466            col as u16
467        };
468
469        // 光标在显示行中的行偏移
470        let cursor_row_in_display = (cursor_line_idx - line_scroll) as u16;
471
472        let cursor_x = area.x + border_left + prompt_w + cursor_col_in_line;
473        let cursor_y = area.y + 1 + cursor_row_in_display; // +1 跳过上边框
474
475        // 确保光标在区域内
476        if cursor_x < area.x + area.width && cursor_y < area.y + area.height {
477            f.set_cursor_position((cursor_x, cursor_y));
478        }
479    }
480}
481
482/// 绘制底部操作提示栏(始终可见)
483pub fn draw_hint_bar(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
484    let t = &app.theme;
485    let hints = match app.mode {
486        ChatMode::Chat => {
487            vec![
488                ("Enter", "发送"),
489                ("↑↓", "滚动"),
490                ("Ctrl+T", "切换模型"),
491                ("Ctrl+L", "归档"),
492                ("Ctrl+R", "还原"),
493                ("Ctrl+Y", "复制"),
494                ("Ctrl+B", "浏览"),
495                ("Ctrl+S", "流式切换"),
496                ("Ctrl+E", "配置"),
497                ("?/F1", "帮助"),
498                ("Esc", "退出"),
499            ]
500        }
501        ChatMode::SelectModel => {
502            vec![("↑↓/jk", "移动"), ("Enter", "确认"), ("Esc", "取消")]
503        }
504        ChatMode::Browse => {
505            vec![("↑↓", "选择消息"), ("y/Enter", "复制"), ("Esc", "返回")]
506        }
507        ChatMode::Help => {
508            vec![("任意键", "返回")]
509        }
510        ChatMode::Config => {
511            vec![
512                ("↑↓", "切换字段"),
513                ("Enter", "编辑"),
514                ("Tab", "切换 Provider"),
515                ("a", "新增"),
516                ("d", "删除"),
517                ("Esc", "保存返回"),
518            ]
519        }
520        ChatMode::ArchiveConfirm => {
521            if app.archive_editing_name {
522                vec![("Enter", "确认"), ("Esc", "取消")]
523            } else {
524                vec![
525                    ("Enter", "默认名称归档"),
526                    ("n", "自定义名称"),
527                    ("Esc", "取消"),
528                ]
529            }
530        }
531        ChatMode::ArchiveList => {
532            if app.restore_confirm_needed {
533                vec![("y/Enter", "确认还原"), ("Esc", "取消")]
534            } else {
535                vec![
536                    ("↑↓/jk", "选择"),
537                    ("Enter", "还原"),
538                    ("d", "删除"),
539                    ("Esc", "返回"),
540                ]
541            }
542        }
543    };
544
545    let mut spans: Vec<Span> = Vec::new();
546    spans.push(Span::styled(" ", Style::default()));
547    for (i, (key, desc)) in hints.iter().enumerate() {
548        if i > 0 {
549            spans.push(Span::styled("  │  ", Style::default().fg(t.hint_separator)));
550        }
551        spans.push(Span::styled(
552            format!(" {} ", key),
553            Style::default().fg(t.hint_key_fg).bg(t.hint_key_bg),
554        ));
555        spans.push(Span::styled(
556            format!(" {}", desc),
557            Style::default().fg(t.hint_desc),
558        ));
559    }
560
561    let hint_bar = Paragraph::new(Line::from(spans)).style(Style::default().bg(t.bg_primary));
562    f.render_widget(hint_bar, area);
563}
564
565/// 绘制 Toast 弹窗(右上角浮层)
566pub fn draw_toast(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
567    let t = &app.theme;
568    if let Some((ref msg, is_error, _)) = app.toast {
569        let text_width = display_width(msg);
570        // toast 宽度 = 文字宽度 + 左右 padding(各2) + emoji(2) + border(2)
571        let toast_width = (text_width + 10).min(area.width as usize).max(16) as u16;
572        let toast_height: u16 = 3;
573
574        // 定位到右上角
575        let x = area.width.saturating_sub(toast_width + 1);
576        let y: u16 = 1;
577
578        if x + toast_width <= area.width && y + toast_height <= area.height {
579            let toast_area = Rect::new(x, y, toast_width, toast_height);
580
581            // 先清空区域背景
582            let clear = Block::default().style(Style::default().bg(if is_error {
583                t.toast_error_bg
584            } else {
585                t.toast_success_bg
586            }));
587            f.render_widget(clear, toast_area);
588
589            let (icon, border_color, text_color) = if is_error {
590                ("❌", t.toast_error_border, t.toast_error_text)
591            } else {
592                ("✅", t.toast_success_border, t.toast_success_text)
593            };
594
595            let toast_widget = Paragraph::new(Line::from(vec![
596                Span::styled(format!(" {} ", icon), Style::default()),
597                Span::styled(msg.as_str(), Style::default().fg(text_color)),
598            ]))
599            .block(
600                Block::default()
601                    .borders(Borders::ALL)
602                    .border_type(ratatui::widgets::BorderType::Rounded)
603                    .border_style(Style::default().fg(border_color))
604                    .style(Style::default().bg(if is_error {
605                        t.toast_error_bg
606                    } else {
607                        t.toast_success_bg
608                    })),
609            );
610            f.render_widget(toast_widget, toast_area);
611        }
612    }
613}
614
615/// 绘制模型选择界面
616pub fn draw_model_selector(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
617    let t = &app.theme;
618    let items: Vec<ListItem> = app
619        .agent_config
620        .providers
621        .iter()
622        .enumerate()
623        .map(|(i, p)| {
624            let is_active = i == app.agent_config.active_index;
625            let marker = if is_active { " ● " } else { " ○ " };
626            let style = if is_active {
627                Style::default()
628                    .fg(t.model_sel_active)
629                    .add_modifier(Modifier::BOLD)
630            } else {
631                Style::default().fg(t.model_sel_inactive)
632            };
633            let detail = format!("{}{}  ({})", marker, p.name, p.model);
634            ListItem::new(Line::from(Span::styled(detail, style)))
635        })
636        .collect();
637
638    let list = List::new(items)
639        .block(
640            Block::default()
641                .borders(Borders::ALL)
642                .border_type(ratatui::widgets::BorderType::Rounded)
643                .border_style(Style::default().fg(t.model_sel_border))
644                .title(Span::styled(
645                    " 🔄 选择模型 ",
646                    Style::default()
647                        .fg(t.model_sel_title)
648                        .add_modifier(Modifier::BOLD),
649                ))
650                .style(Style::default().bg(t.bg_title)),
651        )
652        .highlight_style(
653            Style::default()
654                .bg(t.model_sel_highlight_bg)
655                .fg(t.text_white)
656                .add_modifier(Modifier::BOLD),
657        )
658        .highlight_symbol("  ▸ ");
659
660    f.render_stateful_widget(list, area, &mut app.model_list_state);
661}
662
663/// 绘制帮助界面
664pub fn draw_help(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
665    let t = &app.theme;
666    let separator = Line::from(Span::styled(
667        "  ─────────────────────────────────────────",
668        Style::default().fg(t.separator),
669    ));
670
671    let help_lines = vec![
672        Line::from(""),
673        Line::from(Span::styled(
674            "  📖 快捷键帮助",
675            Style::default()
676                .fg(t.help_title)
677                .add_modifier(Modifier::BOLD),
678        )),
679        Line::from(""),
680        separator.clone(),
681        Line::from(""),
682        Line::from(vec![
683            Span::styled(
684                "  Enter        ",
685                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
686            ),
687            Span::styled("发送消息", Style::default().fg(t.help_desc)),
688        ]),
689        Line::from(vec![
690            Span::styled(
691                "  ↑ / ↓        ",
692                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
693            ),
694            Span::styled("滚动对话记录", Style::default().fg(t.help_desc)),
695        ]),
696        Line::from(vec![
697            Span::styled(
698                "  ← / →        ",
699                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
700            ),
701            Span::styled("移动输入光标", Style::default().fg(t.help_desc)),
702        ]),
703        Line::from(vec![
704            Span::styled(
705                "  Ctrl+T       ",
706                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
707            ),
708            Span::styled("切换模型", Style::default().fg(t.help_desc)),
709        ]),
710        Line::from(vec![
711            Span::styled(
712                "  Ctrl+L       ",
713                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
714            ),
715            Span::styled("归档当前对话", Style::default().fg(t.help_desc)),
716        ]),
717        Line::from(vec![
718            Span::styled(
719                "  Ctrl+R       ",
720                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
721            ),
722            Span::styled("还原归档对话", Style::default().fg(t.help_desc)),
723        ]),
724        Line::from(vec![
725            Span::styled(
726                "  Ctrl+Y       ",
727                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
728            ),
729            Span::styled("复制最后一条 AI 回复", Style::default().fg(t.help_desc)),
730        ]),
731        Line::from(vec![
732            Span::styled(
733                "  Ctrl+B       ",
734                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
735            ),
736            Span::styled(
737                "浏览消息 (↑↓选择, y/Enter复制)",
738                Style::default().fg(t.help_desc),
739            ),
740        ]),
741        Line::from(vec![
742            Span::styled(
743                "  Ctrl+S       ",
744                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
745            ),
746            Span::styled("切换流式/整体输出", Style::default().fg(t.help_desc)),
747        ]),
748        Line::from(vec![
749            Span::styled(
750                "  Ctrl+E       ",
751                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
752            ),
753            Span::styled("打开配置界面", Style::default().fg(t.help_desc)),
754        ]),
755        Line::from(vec![
756            Span::styled(
757                "  Esc / Ctrl+C ",
758                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
759            ),
760            Span::styled("退出对话", Style::default().fg(t.help_desc)),
761        ]),
762        Line::from(vec![
763            Span::styled(
764                "  ? / F1       ",
765                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
766            ),
767            Span::styled("显示 / 关闭此帮助", Style::default().fg(t.help_desc)),
768        ]),
769        Line::from(""),
770        separator,
771        Line::from(""),
772        Line::from(Span::styled(
773            "  📁 配置文件:",
774            Style::default()
775                .fg(t.help_title)
776                .add_modifier(Modifier::BOLD),
777        )),
778        Line::from(Span::styled(
779            format!("     {}", agent_config_path().display()),
780            Style::default().fg(t.help_path),
781        )),
782    ];
783
784    let help_block = Block::default()
785        .borders(Borders::ALL)
786        .border_type(ratatui::widgets::BorderType::Rounded)
787        .border_style(Style::default().fg(t.border_title))
788        .title(Span::styled(
789            " 帮助 (按任意键返回) ",
790            Style::default().fg(t.text_dim),
791        ))
792        .style(Style::default().bg(t.help_bg));
793    let help_widget = Paragraph::new(help_lines).block(help_block);
794    f.render_widget(help_widget, area);
795}
796
797/// 对话模式按键处理,返回 true 表示退出
798
799pub fn draw_config_screen(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
800    let t = &app.theme;
801    let bg = t.bg_title;
802    let total_provider_fields = CONFIG_FIELDS.len();
803
804    let mut lines: Vec<Line> = Vec::new();
805    lines.push(Line::from(""));
806
807    // 标题
808    lines.push(Line::from(vec![Span::styled(
809        "  ⚙️  模型配置",
810        Style::default()
811            .fg(t.config_title)
812            .add_modifier(Modifier::BOLD),
813    )]));
814    lines.push(Line::from(""));
815
816    // Provider 标签栏
817    let provider_count = app.agent_config.providers.len();
818    if provider_count > 0 {
819        let mut tab_spans: Vec<Span> = vec![Span::styled("  ", Style::default())];
820        for (i, p) in app.agent_config.providers.iter().enumerate() {
821            let is_current = i == app.config_provider_idx;
822            let is_active = i == app.agent_config.active_index;
823            let marker = if is_active { "● " } else { "○ " };
824            let label = format!(" {}{} ", marker, p.name);
825            if is_current {
826                tab_spans.push(Span::styled(
827                    label,
828                    Style::default()
829                        .fg(t.config_tab_active_fg)
830                        .bg(t.config_tab_active_bg)
831                        .add_modifier(Modifier::BOLD),
832                ));
833            } else {
834                tab_spans.push(Span::styled(
835                    label,
836                    Style::default().fg(t.config_tab_inactive),
837                ));
838            }
839            if i < provider_count - 1 {
840                tab_spans.push(Span::styled(" │ ", Style::default().fg(t.separator)));
841            }
842        }
843        tab_spans.push(Span::styled(
844            "    (● = 活跃模型, Tab 切换, s 设为活跃)",
845            Style::default().fg(t.config_dim),
846        ));
847        lines.push(Line::from(tab_spans));
848    } else {
849        lines.push(Line::from(Span::styled(
850            "  (无 Provider,按 a 新增)",
851            Style::default().fg(t.config_toggle_off),
852        )));
853    }
854    lines.push(Line::from(""));
855
856    // 分隔线
857    lines.push(Line::from(Span::styled(
858        "  ─────────────────────────────────────────",
859        Style::default().fg(t.separator),
860    )));
861    lines.push(Line::from(""));
862
863    // Provider 字段
864    if provider_count > 0 {
865        lines.push(Line::from(Span::styled(
866            "  📦 Provider 配置",
867            Style::default()
868                .fg(t.config_section)
869                .add_modifier(Modifier::BOLD),
870        )));
871        lines.push(Line::from(""));
872
873        for i in 0..total_provider_fields {
874            let is_selected = app.config_field_idx == i;
875            let label = config_field_label(i);
876            let value = if app.config_editing && is_selected {
877                app.config_edit_buf.clone()
878            } else {
879                config_field_value(app, i)
880            };
881
882            let pointer = if is_selected { "  ▸ " } else { "    " };
883            let pointer_style = if is_selected {
884                Style::default().fg(t.config_pointer)
885            } else {
886                Style::default()
887            };
888
889            let label_style = if is_selected {
890                Style::default()
891                    .fg(t.config_label_selected)
892                    .add_modifier(Modifier::BOLD)
893            } else {
894                Style::default().fg(t.config_label)
895            };
896
897            let value_style = if app.config_editing && is_selected {
898                Style::default().fg(t.text_white).bg(t.config_edit_bg)
899            } else if is_selected {
900                Style::default().fg(t.text_white)
901            } else {
902                if CONFIG_FIELDS[i] == "api_key" {
903                    Style::default().fg(t.config_api_key)
904                } else {
905                    Style::default().fg(t.config_value)
906                }
907            };
908
909            let edit_indicator = if app.config_editing && is_selected {
910                " ✏️"
911            } else {
912                ""
913            };
914
915            lines.push(Line::from(vec![
916                Span::styled(pointer, pointer_style),
917                Span::styled(format!("{:<10}", label), label_style),
918                Span::styled("  ", Style::default()),
919                Span::styled(
920                    if value.is_empty() {
921                        "(空)".to_string()
922                    } else {
923                        value
924                    },
925                    value_style,
926                ),
927                Span::styled(edit_indicator, Style::default()),
928            ]));
929        }
930    }
931
932    lines.push(Line::from(""));
933    // 分隔线
934    lines.push(Line::from(Span::styled(
935        "  ─────────────────────────────────────────",
936        Style::default().fg(t.separator),
937    )));
938    lines.push(Line::from(""));
939
940    // 全局配置
941    lines.push(Line::from(Span::styled(
942        "  🌐 全局配置",
943        Style::default()
944            .fg(t.config_section)
945            .add_modifier(Modifier::BOLD),
946    )));
947    lines.push(Line::from(""));
948
949    for i in 0..CONFIG_GLOBAL_FIELDS.len() {
950        let field_idx = total_provider_fields + i;
951        let is_selected = app.config_field_idx == field_idx;
952        let label = config_field_label(field_idx);
953        let value = if app.config_editing && is_selected {
954            app.config_edit_buf.clone()
955        } else {
956            config_field_value(app, field_idx)
957        };
958
959        let pointer = if is_selected { "  ▸ " } else { "    " };
960        let pointer_style = if is_selected {
961            Style::default().fg(t.config_pointer)
962        } else {
963            Style::default()
964        };
965
966        let label_style = if is_selected {
967            Style::default()
968                .fg(t.config_label_selected)
969                .add_modifier(Modifier::BOLD)
970        } else {
971            Style::default().fg(t.config_label)
972        };
973
974        let value_style = if app.config_editing && is_selected {
975            Style::default().fg(t.text_white).bg(t.config_edit_bg)
976        } else if is_selected {
977            Style::default().fg(t.text_white)
978        } else {
979            Style::default().fg(t.config_value)
980        };
981
982        let edit_indicator = if app.config_editing && is_selected {
983            " ✏️"
984        } else {
985            ""
986        };
987
988        // stream_mode 和 theme 用 toggle 样式
989        if CONFIG_GLOBAL_FIELDS[i] == "stream_mode" {
990            let toggle_on = app.agent_config.stream_mode;
991            let toggle_style = if toggle_on {
992                Style::default()
993                    .fg(t.config_toggle_on)
994                    .add_modifier(Modifier::BOLD)
995            } else {
996                Style::default().fg(t.config_toggle_off)
997            };
998            let toggle_text = if toggle_on {
999                "● 开启"
1000            } else {
1001                "○ 关闭"
1002            };
1003
1004            lines.push(Line::from(vec![
1005                Span::styled(pointer, pointer_style),
1006                Span::styled(format!("{:<10}", label), label_style),
1007                Span::styled("  ", Style::default()),
1008                Span::styled(toggle_text, toggle_style),
1009                Span::styled(
1010                    if is_selected { "  (Enter 切换)" } else { "" },
1011                    Style::default().fg(t.config_dim),
1012                ),
1013            ]));
1014        } else if CONFIG_GLOBAL_FIELDS[i] == "theme" {
1015            // 主题字段用特殊 toggle 样式
1016            let theme_name = app.agent_config.theme.display_name();
1017            lines.push(Line::from(vec![
1018                Span::styled(pointer, pointer_style),
1019                Span::styled(format!("{:<10}", label), label_style),
1020                Span::styled("  ", Style::default()),
1021                Span::styled(
1022                    format!("🎨 {}", theme_name),
1023                    Style::default()
1024                        .fg(t.config_toggle_on)
1025                        .add_modifier(Modifier::BOLD),
1026                ),
1027                Span::styled(
1028                    if is_selected { "  (Enter 切换)" } else { "" },
1029                    Style::default().fg(t.config_dim),
1030                ),
1031            ]));
1032        } else {
1033            lines.push(Line::from(vec![
1034                Span::styled(pointer, pointer_style),
1035                Span::styled(format!("{:<10}", label), label_style),
1036                Span::styled("  ", Style::default()),
1037                Span::styled(
1038                    if value.is_empty() {
1039                        "(空)".to_string()
1040                    } else {
1041                        value
1042                    },
1043                    value_style,
1044                ),
1045                Span::styled(edit_indicator, Style::default()),
1046            ]));
1047        }
1048    }
1049
1050    lines.push(Line::from(""));
1051    lines.push(Line::from(""));
1052
1053    // 操作提示
1054    lines.push(Line::from(Span::styled(
1055        "  ─────────────────────────────────────────",
1056        Style::default().fg(t.separator),
1057    )));
1058    lines.push(Line::from(""));
1059    lines.push(Line::from(vec![
1060        Span::styled("    ", Style::default()),
1061        Span::styled(
1062            "↑↓/jk",
1063            Style::default()
1064                .fg(t.config_hint_key)
1065                .add_modifier(Modifier::BOLD),
1066        ),
1067        Span::styled(" 切换字段  ", Style::default().fg(t.config_hint_desc)),
1068        Span::styled(
1069            "Enter",
1070            Style::default()
1071                .fg(t.config_hint_key)
1072                .add_modifier(Modifier::BOLD),
1073        ),
1074        Span::styled(" 编辑  ", Style::default().fg(t.config_hint_desc)),
1075        Span::styled(
1076            "Tab/←→",
1077            Style::default()
1078                .fg(t.config_hint_key)
1079                .add_modifier(Modifier::BOLD),
1080        ),
1081        Span::styled(" 切换 Provider  ", Style::default().fg(t.config_hint_desc)),
1082        Span::styled(
1083            "a",
1084            Style::default()
1085                .fg(t.config_hint_key)
1086                .add_modifier(Modifier::BOLD),
1087        ),
1088        Span::styled(" 新增  ", Style::default().fg(t.config_hint_desc)),
1089        Span::styled(
1090            "d",
1091            Style::default()
1092                .fg(t.config_hint_key)
1093                .add_modifier(Modifier::BOLD),
1094        ),
1095        Span::styled(" 删除  ", Style::default().fg(t.config_hint_desc)),
1096        Span::styled(
1097            "s",
1098            Style::default()
1099                .fg(t.config_hint_key)
1100                .add_modifier(Modifier::BOLD),
1101        ),
1102        Span::styled(" 设为活跃  ", Style::default().fg(t.config_hint_desc)),
1103        Span::styled(
1104            "Esc",
1105            Style::default()
1106                .fg(t.config_hint_key)
1107                .add_modifier(Modifier::BOLD),
1108        ),
1109        Span::styled(" 保存返回", Style::default().fg(t.config_hint_desc)),
1110    ]));
1111
1112    let content = Paragraph::new(lines)
1113        .block(
1114            Block::default()
1115                .borders(Borders::ALL)
1116                .border_type(ratatui::widgets::BorderType::Rounded)
1117                .border_style(Style::default().fg(t.border_config))
1118                .title(Span::styled(
1119                    " ⚙️  模型配置编辑 ",
1120                    Style::default()
1121                        .fg(t.config_label_selected)
1122                        .add_modifier(Modifier::BOLD),
1123                ))
1124                .style(Style::default().bg(bg)),
1125        )
1126        .scroll((0, 0));
1127    f.render_widget(content, area);
1128}
1129
1130/// 绘制归档确认界面
1131pub fn draw_archive_confirm(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
1132    let t = &app.theme;
1133    let mut lines: Vec<Line> = Vec::new();
1134
1135    lines.push(Line::from(""));
1136    lines.push(Line::from(Span::styled(
1137        "  📦 归档当前对话",
1138        Style::default()
1139            .fg(t.help_title)
1140            .add_modifier(Modifier::BOLD),
1141    )));
1142    lines.push(Line::from(""));
1143    lines.push(Line::from(Span::styled(
1144        "  ─────────────────────────────────────────",
1145        Style::default().fg(t.separator),
1146    )));
1147    lines.push(Line::from(""));
1148    lines.push(Line::from(Span::styled(
1149        "  即将归档当前对话,归档后当前会话将被清空。",
1150        Style::default().fg(t.text_dim),
1151    )));
1152    lines.push(Line::from(""));
1153
1154    if app.archive_editing_name {
1155        // 编辑自定义名称模式
1156        lines.push(Line::from(Span::styled(
1157            "  请输入归档名称:",
1158            Style::default().fg(t.text_white),
1159        )));
1160        lines.push(Line::from(""));
1161
1162        let name_with_cursor = if app.archive_custom_name.is_empty() {
1163            vec![Span::styled(
1164                " ",
1165                Style::default().fg(t.cursor_fg).bg(t.cursor_bg),
1166            )]
1167        } else {
1168            let chars: Vec<char> = app.archive_custom_name.chars().collect();
1169            let mut spans: Vec<Span> = Vec::new();
1170            for (i, &ch) in chars.iter().enumerate() {
1171                if i == app.archive_edit_cursor {
1172                    spans.push(Span::styled(
1173                        ch.to_string(),
1174                        Style::default().fg(t.cursor_fg).bg(t.cursor_bg),
1175                    ));
1176                } else {
1177                    spans.push(Span::styled(
1178                        ch.to_string(),
1179                        Style::default().fg(t.text_white),
1180                    ));
1181                }
1182            }
1183            // 光标在末尾
1184            if app.archive_edit_cursor >= chars.len() {
1185                spans.push(Span::styled(
1186                    " ",
1187                    Style::default().fg(t.cursor_fg).bg(t.cursor_bg),
1188                ));
1189            }
1190            spans
1191        };
1192
1193        lines.push(Line::from(vec![
1194            Span::styled("    ", Style::default()),
1195            Span::styled(
1196                format!("archive-{}", chrono::Local::now().format("%Y-%m-%d")),
1197                Style::default().fg(t.text_dim),
1198            ),
1199        ]));
1200        lines.push(Line::from(
1201            std::iter::once(Span::styled("    ", Style::default()))
1202                .chain(name_with_cursor.into_iter())
1203                .collect::<Vec<_>>(),
1204        ));
1205        lines.push(Line::from(""));
1206        lines.push(Line::from(Span::styled(
1207            "  提示:留空则使用默认名称(如 archive-2026-02-25)",
1208            Style::default().fg(t.text_dim),
1209        )));
1210        lines.push(Line::from(""));
1211        lines.push(Line::from(Span::styled(
1212            "  ─────────────────────────────────────────",
1213            Style::default().fg(t.separator),
1214        )));
1215        lines.push(Line::from(""));
1216        lines.push(Line::from(vec![
1217            Span::styled("  ", Style::default()),
1218            Span::styled(
1219                "Enter",
1220                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
1221            ),
1222            Span::styled("  确认归档", Style::default().fg(t.help_desc)),
1223        ]));
1224        lines.push(Line::from(vec![
1225            Span::styled("  ", Style::default()),
1226            Span::styled(
1227                "Esc",
1228                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
1229            ),
1230            Span::styled("    取消", Style::default().fg(t.help_desc)),
1231        ]));
1232    } else {
1233        // 默认确认模式
1234        lines.push(Line::from(vec![
1235            Span::styled("  默认名称:", Style::default().fg(t.text_dim)),
1236            Span::styled(
1237                &app.archive_default_name,
1238                Style::default()
1239                    .fg(t.config_toggle_on)
1240                    .add_modifier(Modifier::BOLD),
1241            ),
1242        ]));
1243        lines.push(Line::from(""));
1244        lines.push(Line::from(Span::styled(
1245            "  ─────────────────────────────────────────",
1246            Style::default().fg(t.separator),
1247        )));
1248        lines.push(Line::from(""));
1249        lines.push(Line::from(vec![
1250            Span::styled("  ", Style::default()),
1251            Span::styled(
1252                "Enter",
1253                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
1254            ),
1255            Span::styled("  使用默认名称归档", Style::default().fg(t.help_desc)),
1256        ]));
1257        lines.push(Line::from(vec![
1258            Span::styled("  ", Style::default()),
1259            Span::styled(
1260                "n",
1261                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
1262            ),
1263            Span::styled("      自定义名称", Style::default().fg(t.help_desc)),
1264        ]));
1265        lines.push(Line::from(vec![
1266            Span::styled("  ", Style::default()),
1267            Span::styled(
1268                "d",
1269                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
1270            ),
1271            Span::styled("      仅清空不归档", Style::default().fg(t.help_desc)),
1272        ]));
1273        lines.push(Line::from(vec![
1274            Span::styled("  ", Style::default()),
1275            Span::styled(
1276                "Esc",
1277                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
1278            ),
1279            Span::styled("    取消", Style::default().fg(t.help_desc)),
1280        ]));
1281    }
1282
1283    let block = Block::default()
1284        .borders(Borders::ALL)
1285        .border_type(ratatui::widgets::BorderType::Rounded)
1286        .border_style(Style::default().fg(t.border_title))
1287        .title(Span::styled(" 归档确认 ", Style::default().fg(t.text_dim)))
1288        .style(Style::default().bg(t.help_bg));
1289    let widget = Paragraph::new(lines).block(block);
1290    f.render_widget(widget, area);
1291}
1292
1293/// 绘制归档列表界面
1294pub fn draw_archive_list(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
1295    let t = &app.theme;
1296
1297    // 如果需要确认还原
1298    if app.restore_confirm_needed {
1299        let mut lines: Vec<Line> = Vec::new();
1300        lines.push(Line::from(""));
1301        lines.push(Line::from(Span::styled(
1302            "  ⚠️  确认还原",
1303            Style::default()
1304                .fg(t.toast_error_text)
1305                .add_modifier(Modifier::BOLD),
1306        )));
1307        lines.push(Line::from(""));
1308        lines.push(Line::from(Span::styled(
1309            "  当前对话未归档,还原将丢失当前对话内容!",
1310            Style::default().fg(t.text_white),
1311        )));
1312        lines.push(Line::from(""));
1313        lines.push(Line::from(Span::styled(
1314            "  ─────────────────────────────────────────",
1315            Style::default().fg(t.separator),
1316        )));
1317        lines.push(Line::from(""));
1318        if let Some(archive) = app.archives.get(app.archive_list_index) {
1319            lines.push(Line::from(vec![
1320                Span::styled("  将还原归档:", Style::default().fg(t.text_dim)),
1321                Span::styled(
1322                    &archive.name,
1323                    Style::default()
1324                        .fg(t.config_toggle_on)
1325                        .add_modifier(Modifier::BOLD),
1326                ),
1327            ]));
1328        }
1329        lines.push(Line::from(""));
1330        lines.push(Line::from(vec![
1331            Span::styled("  ", Style::default()),
1332            Span::styled(
1333                "y/Enter",
1334                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
1335            ),
1336            Span::styled("  确认还原", Style::default().fg(t.help_desc)),
1337        ]));
1338        lines.push(Line::from(vec![
1339            Span::styled("  ", Style::default()),
1340            Span::styled(
1341                "Esc",
1342                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
1343            ),
1344            Span::styled("     取消", Style::default().fg(t.help_desc)),
1345        ]));
1346
1347        let block = Block::default()
1348            .borders(Borders::ALL)
1349            .border_type(ratatui::widgets::BorderType::Rounded)
1350            .border_style(Style::default().fg(t.toast_error_border))
1351            .title(Span::styled(" 还原确认 ", Style::default().fg(t.text_dim)))
1352            .style(Style::default().bg(t.help_bg));
1353        let widget = Paragraph::new(lines).block(block);
1354        f.render_widget(widget, area);
1355        return;
1356    }
1357
1358    // 归档列表
1359    if app.archives.is_empty() {
1360        let lines = vec![
1361            Line::from(""),
1362            Line::from(""),
1363            Line::from(Span::styled(
1364                "  📦 暂无归档对话",
1365                Style::default().fg(t.text_dim).add_modifier(Modifier::BOLD),
1366            )),
1367            Line::from(""),
1368            Line::from(Span::styled(
1369                "  按 Ctrl+L 归档当前对话",
1370                Style::default().fg(t.text_dim),
1371            )),
1372            Line::from(""),
1373            Line::from(Span::styled(
1374                "  按 Esc 返回聊天",
1375                Style::default().fg(t.text_dim),
1376            )),
1377        ];
1378
1379        let block = Block::default()
1380            .borders(Borders::ALL)
1381            .border_type(ratatui::widgets::BorderType::Rounded)
1382            .border_style(Style::default().fg(t.border_title))
1383            .title(Span::styled(" 归档列表 ", Style::default().fg(t.text_dim)))
1384            .style(Style::default().bg(t.help_bg));
1385        let widget = Paragraph::new(lines).block(block);
1386        f.render_widget(widget, area);
1387        return;
1388    }
1389
1390    // 显示归档列表
1391    let items: Vec<ListItem> = app
1392        .archives
1393        .iter()
1394        .enumerate()
1395        .map(|(i, archive)| {
1396            let is_selected = i == app.archive_list_index;
1397            let marker = if is_selected { "  ▸ " } else { "    " };
1398            let msg_count = archive.messages.len();
1399
1400            // 格式化创建时间
1401            let created_at = archive
1402                .created_at
1403                .split('T')
1404                .next()
1405                .unwrap_or(&archive.created_at);
1406
1407            let style = if is_selected {
1408                Style::default()
1409                    .fg(t.model_sel_active)
1410                    .add_modifier(Modifier::BOLD)
1411            } else {
1412                Style::default().fg(t.model_sel_inactive)
1413            };
1414
1415            let detail = format!(
1416                "{}{}  📨 {} 条消息  📅 {}",
1417                marker, archive.name, msg_count, created_at
1418            );
1419            ListItem::new(Line::from(Span::styled(detail, style)))
1420        })
1421        .collect();
1422
1423    let list = List::new(items)
1424        .block(
1425            Block::default()
1426                .borders(Borders::ALL)
1427                .border_type(ratatui::widgets::BorderType::Rounded)
1428                .border_style(Style::default().fg(t.model_sel_border))
1429                .title(Span::styled(
1430                    " 📦 归档列表 (Enter 还原, d 删除, Esc 返回) ",
1431                    Style::default()
1432                        .fg(t.model_sel_title)
1433                        .add_modifier(Modifier::BOLD),
1434                ))
1435                .style(Style::default().bg(t.bg_title)),
1436        )
1437        .highlight_style(
1438            Style::default()
1439                .bg(t.model_sel_highlight_bg)
1440                .fg(t.text_white)
1441                .add_modifier(Modifier::BOLD),
1442        )
1443        .highlight_symbol("");
1444
1445    // 使用 ListState 来管理选中状态
1446    let mut list_state = ListState::default();
1447    list_state.select(Some(app.archive_list_index));
1448    f.render_stateful_widget(list, area, &mut list_state);
1449}