1use super::super::app::{ChatApp, ChatMode, MsgLinesCache};
2use super::super::model::agent_config_path;
3use super::super::render::{build_message_lines_incremental, char_width, display_width, wrap_text};
4use super::archive::{draw_archive_confirm, draw_archive_list};
5use super::config::draw_config_screen;
6use ratatui::{
7 layout::{Constraint, Direction, Layout, Rect},
8 style::{Modifier, Style},
9 text::{Line, Span},
10 widgets::{Block, Borders, List, ListItem, Paragraph},
11};
12
13pub fn draw_chat_ui(f: &mut ratatui::Frame, app: &mut ChatApp) {
14 let size = f.area();
15
16 let bg = Block::default().style(Style::default().bg(app.theme.bg_primary));
18 f.render_widget(bg, size);
19
20 let chunks = Layout::default()
21 .direction(Direction::Vertical)
22 .constraints([
23 Constraint::Length(3), Constraint::Min(5), Constraint::Length(5), Constraint::Length(1), ])
28 .split(size);
29
30 draw_title_bar(f, chunks[0], app);
32
33 if app.mode == ChatMode::Help {
35 draw_help(f, chunks[1], app);
36 } else if app.mode == ChatMode::SelectModel {
37 draw_model_selector(f, chunks[1], app);
38 } else if app.mode == ChatMode::Config {
39 draw_config_screen(f, chunks[1], app);
40 } else if app.mode == ChatMode::ArchiveConfirm {
41 draw_archive_confirm(f, chunks[1], app);
42 } else if app.mode == ChatMode::ArchiveList {
43 draw_archive_list(f, chunks[1], app);
44 } else {
45 draw_messages(f, chunks[1], app);
46 }
47
48 draw_input(f, chunks[2], app);
50
51 draw_hint_bar(f, chunks[3], app);
53
54 draw_toast(f, size, app);
56}
57
58pub fn draw_title_bar(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
60 let t = &app.theme;
61 let model_name = app.active_model_name();
62 let msg_count = app.session.messages.len();
63 let loading = if app.is_loading {
64 " ⏳ 思考中..."
65 } else {
66 ""
67 };
68
69 let title_spans = vec![
70 Span::styled(" 💬 ", Style::default().fg(t.title_icon)),
71 Span::styled(
72 "AI Chat",
73 Style::default()
74 .fg(t.text_white)
75 .add_modifier(Modifier::BOLD),
76 ),
77 Span::styled(" │ ", Style::default().fg(t.title_separator)),
78 Span::styled("🤖 ", Style::default()),
79 Span::styled(
80 model_name,
81 Style::default()
82 .fg(t.title_model)
83 .add_modifier(Modifier::BOLD),
84 ),
85 Span::styled(" │ ", Style::default().fg(t.title_separator)),
86 Span::styled(
87 format!("📨 {} 条消息", msg_count),
88 Style::default().fg(t.title_count),
89 ),
90 Span::styled(
91 loading,
92 Style::default()
93 .fg(t.title_loading)
94 .add_modifier(Modifier::BOLD),
95 ),
96 ];
97
98 let title_block = Paragraph::new(Line::from(title_spans)).block(
99 Block::default()
100 .borders(Borders::ALL)
101 .border_type(ratatui::widgets::BorderType::Rounded)
102 .border_style(Style::default().fg(t.border_title))
103 .style(Style::default().bg(t.bg_title)),
104 );
105 f.render_widget(title_block, area);
106}
107
108pub fn draw_messages(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
110 let t = &app.theme;
111 let block = Block::default()
112 .borders(Borders::ALL)
113 .border_type(ratatui::widgets::BorderType::Rounded)
114 .border_style(Style::default().fg(t.border_message))
115 .title(Span::styled(
116 " 对话记录 ",
117 Style::default().fg(t.text_dim).add_modifier(Modifier::BOLD),
118 ))
119 .title_alignment(ratatui::layout::Alignment::Left)
120 .style(Style::default().bg(t.bg_primary));
121
122 if app.session.messages.is_empty() && !app.is_loading {
124 let welcome_lines = vec![
125 Line::from(""),
126 Line::from(""),
127 Line::from(Span::styled(
128 " ╭──────────────────────────────────────╮",
129 Style::default().fg(t.welcome_border),
130 )),
131 Line::from(Span::styled(
132 " │ │",
133 Style::default().fg(t.welcome_border),
134 )),
135 Line::from(vec![
136 Span::styled(" │ ", Style::default().fg(t.welcome_border)),
137 Span::styled(
138 "Hi! What can I help you? ",
139 Style::default().fg(t.welcome_text),
140 ),
141 Span::styled(" │", Style::default().fg(t.welcome_border)),
142 ]),
143 Line::from(Span::styled(
144 " │ │",
145 Style::default().fg(t.welcome_border),
146 )),
147 Line::from(Span::styled(
148 " │ Type a message, press Enter │",
149 Style::default().fg(t.welcome_hint),
150 )),
151 Line::from(Span::styled(
152 " │ │",
153 Style::default().fg(t.welcome_border),
154 )),
155 Line::from(Span::styled(
156 " ╰──────────────────────────────────────╯",
157 Style::default().fg(t.welcome_border),
158 )),
159 ];
160 let empty = Paragraph::new(welcome_lines).block(block);
161 f.render_widget(empty, area);
162 return;
163 }
164
165 let inner_width = area.width.saturating_sub(4) as usize;
167 let bubble_max_width = (inner_width * 75 / 100).max(20);
169
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 let old_cache = app.msg_lines_cache.take();
196 let (new_lines, new_msg_start_lines, new_per_msg, new_stable_lines, new_stable_offset) =
197 build_message_lines_incremental(app, inner_width, bubble_max_width, old_cache.as_ref());
198 app.msg_lines_cache = Some(MsgLinesCache {
199 msg_count,
200 last_msg_len,
201 streaming_len,
202 is_loading: app.is_loading,
203 bubble_max_width,
204 browse_index: current_browse_index,
205 lines: new_lines,
206 msg_start_lines: new_msg_start_lines,
207 per_msg_lines: new_per_msg,
208 streaming_stable_lines: new_stable_lines,
209 streaming_stable_offset: new_stable_offset,
210 });
211 }
212
213 let cached = app.msg_lines_cache.as_ref().unwrap();
214 let all_lines = &cached.lines;
215 let total_lines = all_lines.len() as u16;
216
217 f.render_widget(block, area);
218
219 let inner = area.inner(ratatui::layout::Margin {
220 vertical: 1,
221 horizontal: 1,
222 });
223 let visible_height = inner.height;
224 let max_scroll = total_lines.saturating_sub(visible_height);
225
226 if app.mode != ChatMode::Browse {
227 if app.scroll_offset == u16::MAX || app.scroll_offset > max_scroll {
228 app.scroll_offset = max_scroll;
229 app.auto_scroll = true;
230 }
231 } else {
232 if let Some(msg_start) = cached
233 .msg_start_lines
234 .iter()
235 .find(|(idx, _)| *idx == app.browse_msg_index)
236 .map(|(_, line)| *line as u16)
237 {
238 let msg_line_count = cached
239 .per_msg_lines
240 .get(app.browse_msg_index)
241 .map(|c| c.lines.len())
242 .unwrap_or(1) as u16;
243 let msg_max_scroll = msg_line_count.saturating_sub(visible_height);
244 if app.browse_scroll_offset > msg_max_scroll {
245 app.browse_scroll_offset = msg_max_scroll;
246 }
247 app.scroll_offset = (msg_start + app.browse_scroll_offset).min(max_scroll);
248 }
249 }
250
251 let bg_fill = Block::default().style(Style::default().bg(app.theme.bg_primary));
252 f.render_widget(bg_fill, inner);
253
254 let start = app.scroll_offset as usize;
255 let end = (start + visible_height as usize).min(all_lines.len());
256 let msg_area_bg = Style::default().bg(app.theme.bg_primary);
257 for (i, line_idx) in (start..end).enumerate() {
258 let line = &all_lines[line_idx];
259 let y = inner.y + i as u16;
260 let line_area = Rect::new(inner.x, y, inner.width, 1);
261 let p = Paragraph::new(line.clone()).style(msg_area_bg);
262 f.render_widget(p, line_area);
263 }
264}
265
266pub fn draw_input(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
267 let t = &app.theme;
268 let usable_width = area.width.saturating_sub(2 + 4) as usize;
269
270 let chars: Vec<char> = app.input.chars().collect();
271
272 let before_all: String = chars[..app.cursor_pos].iter().collect();
273 let before_width = display_width(&before_all);
274
275 let scroll_offset_chars = if before_width >= usable_width {
276 let target_width = before_width.saturating_sub(usable_width / 2);
277 let mut w = 0;
278 let mut skip = 0;
279 for (i, &ch) in chars.iter().enumerate() {
280 if w >= target_width {
281 skip = i;
282 break;
283 }
284 w += char_width(ch);
285 }
286 skip
287 } else {
288 0
289 };
290
291 let visible_chars = &chars[scroll_offset_chars..];
292 let cursor_in_visible = app.cursor_pos - scroll_offset_chars;
293
294 let before: String = visible_chars[..cursor_in_visible].iter().collect();
295 let cursor_ch = if cursor_in_visible < visible_chars.len() {
296 visible_chars[cursor_in_visible].to_string()
297 } else {
298 " ".to_string()
299 };
300 let after: String = if cursor_in_visible < visible_chars.len() {
301 visible_chars[cursor_in_visible + 1..].iter().collect()
302 } else {
303 String::new()
304 };
305
306 let prompt_style = if app.is_loading {
307 Style::default().fg(t.input_prompt_loading)
308 } else {
309 Style::default().fg(t.input_prompt)
310 };
311 let prompt_text = if app.is_loading { " .. " } else { " > " };
312
313 let full_visible = format!("{}{}{}", before, cursor_ch, after);
314 let inner_height = area.height.saturating_sub(2) as usize;
315 let wrapped_lines = wrap_text(&full_visible, usable_width);
316
317 let before_len = before.chars().count();
318 let cursor_len = cursor_ch.chars().count();
319 let cursor_global_pos = before_len;
320 let mut cursor_line_idx: usize = 0;
321 {
322 let mut cumulative = 0usize;
323 for (li, wl) in wrapped_lines.iter().enumerate() {
324 let line_char_count = wl.chars().count();
325 if cumulative + line_char_count > cursor_global_pos {
326 cursor_line_idx = li;
327 break;
328 }
329 cumulative += line_char_count;
330 cursor_line_idx = li;
331 }
332 }
333
334 let line_scroll = if wrapped_lines.len() <= inner_height {
335 0
336 } else if cursor_line_idx < inner_height {
337 0
338 } else {
339 cursor_line_idx.saturating_sub(inner_height - 1)
340 };
341
342 let mut display_lines: Vec<Line> = Vec::new();
343 let mut char_offset: usize = 0;
344 for wl in wrapped_lines.iter().take(line_scroll) {
345 char_offset += wl.chars().count();
346 }
347
348 for (_line_idx, wl) in wrapped_lines
349 .iter()
350 .skip(line_scroll)
351 .enumerate()
352 .take(inner_height.max(1))
353 {
354 let mut spans: Vec<Span> = Vec::new();
355 if _line_idx == 0 && line_scroll == 0 {
356 spans.push(Span::styled(prompt_text, prompt_style));
357 } else {
358 spans.push(Span::styled(" ", Style::default()));
359 }
360
361 let line_chars: Vec<char> = wl.chars().collect();
362 let mut seg_start = 0;
363 for (ci, &ch) in line_chars.iter().enumerate() {
364 let global_idx = char_offset + ci;
365 let is_cursor = global_idx >= before_len && global_idx < before_len + cursor_len;
366
367 if is_cursor {
368 if ci > seg_start {
369 let seg: String = line_chars[seg_start..ci].iter().collect();
370 spans.push(Span::styled(seg, Style::default().fg(t.text_white)));
371 }
372 spans.push(Span::styled(
373 ch.to_string(),
374 Style::default().fg(t.cursor_fg).bg(t.cursor_bg),
375 ));
376 seg_start = ci + 1;
377 }
378 }
379 if seg_start < line_chars.len() {
380 let seg: String = line_chars[seg_start..].iter().collect();
381 spans.push(Span::styled(seg, Style::default().fg(t.text_white)));
382 }
383
384 char_offset += line_chars.len();
385 display_lines.push(Line::from(spans));
386 }
387
388 if display_lines.is_empty() {
389 display_lines.push(Line::from(vec![
390 Span::styled(prompt_text, prompt_style),
391 Span::styled(" ", Style::default().fg(t.cursor_fg).bg(t.cursor_bg)),
392 ]));
393 }
394
395 let input_widget = Paragraph::new(display_lines).block(
396 Block::default()
397 .borders(Borders::ALL)
398 .border_type(ratatui::widgets::BorderType::Rounded)
399 .border_style(if app.is_loading {
400 Style::default().fg(t.border_input_loading)
401 } else {
402 Style::default().fg(t.border_input)
403 })
404 .title(Span::styled(" 输入消息 ", Style::default().fg(t.text_dim)))
405 .style(Style::default().bg(t.bg_input)),
406 );
407
408 f.render_widget(input_widget, area);
409
410 if !app.is_loading {
411 let prompt_w: u16 = 4;
412 let border_left: u16 = 1;
413
414 let cursor_col_in_line = {
415 let mut col = 0usize;
416 let mut char_count = 0usize;
417 let mut skip_chars = 0usize;
418 for wl in wrapped_lines.iter().take(line_scroll) {
419 skip_chars += wl.chars().count();
420 }
421 for wl in wrapped_lines.iter().skip(line_scroll) {
422 let line_len = wl.chars().count();
423 if skip_chars + char_count + line_len > cursor_global_pos {
424 let pos_in_line = cursor_global_pos - (skip_chars + char_count);
425 col = wl.chars().take(pos_in_line).map(|c| char_width(c)).sum();
426 break;
427 }
428 char_count += line_len;
429 }
430 col as u16
431 };
432
433 let cursor_row_in_display = (cursor_line_idx - line_scroll) as u16;
434 let cursor_x = area.x + border_left + prompt_w + cursor_col_in_line;
435 let cursor_y = area.y + 1 + cursor_row_in_display;
436
437 if cursor_x < area.x + area.width && cursor_y < area.y + area.height {
438 f.set_cursor_position((cursor_x, cursor_y));
439 }
440 }
441}
442
443pub fn draw_hint_bar(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
445 let t = &app.theme;
446 let hints = match app.mode {
447 ChatMode::Chat => vec![
448 ("Enter", "发送"),
449 ("↑↓", "滚动"),
450 ("Ctrl+T", "切换模型"),
451 ("Ctrl+L", "归档"),
452 ("Ctrl+R", "还原"),
453 ("Ctrl+Y", "复制"),
454 ("Ctrl+B", "浏览"),
455 ("Ctrl+S", "流式切换"),
456 ("Ctrl+E", "配置"),
457 ("?/F1", "帮助"),
458 ("Esc", "退出"),
459 ],
460 ChatMode::SelectModel => vec![("↑↓/jk", "移动"), ("Enter", "确认"), ("Esc", "取消")],
461 ChatMode::Browse => vec![("↑↓", "选择消息"), ("y/Enter", "复制"), ("Esc", "返回")],
462 ChatMode::Help => vec![("任意键", "返回")],
463 ChatMode::Config => vec![
464 ("↑↓", "切换字段"),
465 ("Enter", "编辑"),
466 ("Tab", "切换 Provider"),
467 ("a", "新增"),
468 ("d", "删除"),
469 ("Esc", "保存返回"),
470 ],
471 ChatMode::ArchiveConfirm => {
472 if app.archive_editing_name {
473 vec![("Enter", "确认"), ("Esc", "取消")]
474 } else {
475 vec![
476 ("Enter", "默认名称归档"),
477 ("n", "自定义名称"),
478 ("Esc", "取消"),
479 ]
480 }
481 }
482 ChatMode::ArchiveList => {
483 if app.restore_confirm_needed {
484 vec![("y/Enter", "确认还原"), ("Esc", "取消")]
485 } else {
486 vec![
487 ("↑↓/jk", "选择"),
488 ("Enter", "还原"),
489 ("d", "删除"),
490 ("Esc", "返回"),
491 ]
492 }
493 }
494 };
495
496 let mut spans: Vec<Span> = Vec::new();
497 spans.push(Span::styled(" ", Style::default()));
498 for (i, (key, desc)) in hints.iter().enumerate() {
499 if i > 0 {
500 spans.push(Span::styled(" │ ", Style::default().fg(t.hint_separator)));
501 }
502 spans.push(Span::styled(
503 format!(" {} ", key),
504 Style::default().fg(t.hint_key_fg).bg(t.hint_key_bg),
505 ));
506 spans.push(Span::styled(
507 format!(" {}", desc),
508 Style::default().fg(t.hint_desc),
509 ));
510 }
511
512 let hint_bar = Paragraph::new(Line::from(spans)).style(Style::default().bg(t.bg_primary));
513 f.render_widget(hint_bar, area);
514}
515
516pub fn draw_toast(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
518 let t = &app.theme;
519 if let Some((ref msg, is_error, _)) = app.toast {
520 let text_width = display_width(msg);
521 let toast_width = (text_width + 10).min(area.width as usize).max(16) as u16;
522 let toast_height: u16 = 3;
523
524 let x = area.width.saturating_sub(toast_width + 1);
525 let y: u16 = 1;
526
527 if x + toast_width <= area.width && y + toast_height <= area.height {
528 let toast_area = Rect::new(x, y, toast_width, toast_height);
529
530 let clear = Block::default().style(Style::default().bg(if is_error {
531 t.toast_error_bg
532 } else {
533 t.toast_success_bg
534 }));
535 f.render_widget(clear, toast_area);
536
537 let (icon, border_color, text_color) = if is_error {
538 ("❌", t.toast_error_border, t.toast_error_text)
539 } else {
540 ("✅", t.toast_success_border, t.toast_success_text)
541 };
542
543 let toast_widget = Paragraph::new(Line::from(vec![
544 Span::styled(format!(" {} ", icon), Style::default()),
545 Span::styled(msg.as_str(), Style::default().fg(text_color)),
546 ]))
547 .block(
548 Block::default()
549 .borders(Borders::ALL)
550 .border_type(ratatui::widgets::BorderType::Rounded)
551 .border_style(Style::default().fg(border_color))
552 .style(Style::default().bg(if is_error {
553 t.toast_error_bg
554 } else {
555 t.toast_success_bg
556 })),
557 );
558 f.render_widget(toast_widget, toast_area);
559 }
560 }
561}
562
563pub fn draw_model_selector(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
565 let t = &app.theme;
566 let items: Vec<ListItem> = app
567 .agent_config
568 .providers
569 .iter()
570 .enumerate()
571 .map(|(i, p)| {
572 let is_active = i == app.agent_config.active_index;
573 let marker = if is_active { " ● " } else { " ○ " };
574 let style = if is_active {
575 Style::default()
576 .fg(t.model_sel_active)
577 .add_modifier(Modifier::BOLD)
578 } else {
579 Style::default().fg(t.model_sel_inactive)
580 };
581 let detail = format!("{}{} ({})", marker, p.name, p.model);
582 ListItem::new(Line::from(Span::styled(detail, style)))
583 })
584 .collect();
585
586 let list = List::new(items)
587 .block(
588 Block::default()
589 .borders(Borders::ALL)
590 .border_type(ratatui::widgets::BorderType::Rounded)
591 .border_style(Style::default().fg(t.model_sel_border))
592 .title(Span::styled(
593 " 🔄 选择模型 ",
594 Style::default()
595 .fg(t.model_sel_title)
596 .add_modifier(Modifier::BOLD),
597 ))
598 .style(Style::default().bg(t.bg_title)),
599 )
600 .highlight_style(
601 Style::default()
602 .bg(t.model_sel_highlight_bg)
603 .fg(t.text_white)
604 .add_modifier(Modifier::BOLD),
605 )
606 .highlight_symbol(" ▸ ");
607
608 f.render_stateful_widget(list, area, &mut app.model_list_state);
609}
610
611pub fn draw_help(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
613 let t = &app.theme;
614 let separator = Line::from(Span::styled(
615 " ─────────────────────────────────────────",
616 Style::default().fg(t.separator),
617 ));
618
619 let help_lines = vec![
620 Line::from(""),
621 Line::from(Span::styled(
622 " 📖 快捷键帮助",
623 Style::default()
624 .fg(t.help_title)
625 .add_modifier(Modifier::BOLD),
626 )),
627 Line::from(""),
628 separator.clone(),
629 Line::from(""),
630 Line::from(vec![
631 Span::styled(
632 " Enter ",
633 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
634 ),
635 Span::styled("发送消息", Style::default().fg(t.help_desc)),
636 ]),
637 Line::from(vec![
638 Span::styled(
639 " ↑ / ↓ ",
640 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
641 ),
642 Span::styled("滚动对话记录", Style::default().fg(t.help_desc)),
643 ]),
644 Line::from(vec![
645 Span::styled(
646 " ← / → ",
647 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
648 ),
649 Span::styled("移动输入光标", Style::default().fg(t.help_desc)),
650 ]),
651 Line::from(vec![
652 Span::styled(
653 " Ctrl+T ",
654 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
655 ),
656 Span::styled("切换模型", Style::default().fg(t.help_desc)),
657 ]),
658 Line::from(vec![
659 Span::styled(
660 " Ctrl+L ",
661 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
662 ),
663 Span::styled("归档当前对话", Style::default().fg(t.help_desc)),
664 ]),
665 Line::from(vec![
666 Span::styled(
667 " Ctrl+R ",
668 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
669 ),
670 Span::styled("还原归档对话", Style::default().fg(t.help_desc)),
671 ]),
672 Line::from(vec![
673 Span::styled(
674 " Ctrl+Y ",
675 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
676 ),
677 Span::styled("复制最后一条 AI 回复", Style::default().fg(t.help_desc)),
678 ]),
679 Line::from(vec![
680 Span::styled(
681 " Ctrl+B ",
682 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
683 ),
684 Span::styled(
685 "浏览消息 (↑↓选择, y/Enter复制)",
686 Style::default().fg(t.help_desc),
687 ),
688 ]),
689 Line::from(vec![
690 Span::styled(
691 " Ctrl+S ",
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 " Ctrl+E ",
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 " Esc / Ctrl+C ",
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 " ? / F1 ",
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(""),
718 separator,
719 Line::from(""),
720 Line::from(Span::styled(
721 " 📁 配置文件:",
722 Style::default()
723 .fg(t.help_title)
724 .add_modifier(Modifier::BOLD),
725 )),
726 Line::from(Span::styled(
727 format!(" {}", agent_config_path().display()),
728 Style::default().fg(t.help_path),
729 )),
730 ];
731
732 let help_block = Block::default()
733 .borders(Borders::ALL)
734 .border_type(ratatui::widgets::BorderType::Rounded)
735 .border_style(Style::default().fg(t.border_title))
736 .title(Span::styled(
737 " 帮助 (按任意键返回) ",
738 Style::default().fg(t.text_dim),
739 ))
740 .style(Style::default().bg(t.help_bg));
741 let help_widget = Paragraph::new(help_lines).block(help_block);
742 f.render_widget(help_widget, area);
743}