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