Skip to main content

j_cli/command/
chat.rs

1use crate::config::YamlConfig;
2use crate::{error, info};
3use async_openai::{
4    Client,
5    config::OpenAIConfig,
6    types::chat::{
7        ChatCompletionRequestAssistantMessageArgs, ChatCompletionRequestMessage,
8        ChatCompletionRequestSystemMessageArgs, ChatCompletionRequestUserMessageArgs,
9        CreateChatCompletionRequestArgs,
10    },
11};
12use crossterm::{
13    event::{
14        self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers,
15        MouseEventKind,
16    },
17    execute,
18    terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
19};
20use futures::StreamExt;
21use ratatui::{
22    Terminal,
23    backend::CrosstermBackend,
24    layout::{Constraint, Direction, Layout, Rect},
25    style::{Color, Modifier, Style},
26    text::{Line, Span},
27    widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
28};
29use serde::{Deserialize, Serialize};
30use std::fs;
31use std::io::{self, Write};
32use std::path::PathBuf;
33use std::sync::{Arc, Mutex, mpsc};
34
35// ========== 数据结构 ==========
36
37/// 单个模型提供方配置
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ModelProvider {
40    /// 显示名称(如 "GPT-4o", "DeepSeek-V3")
41    pub name: String,
42    /// API Base URL(如 "https://api.openai.com/v1")
43    pub api_base: String,
44    /// API Key
45    pub api_key: String,
46    /// 模型名称(如 "gpt-4o", "deepseek-chat")
47    pub model: String,
48}
49
50/// Agent 配置
51#[derive(Debug, Clone, Serialize, Deserialize, Default)]
52pub struct AgentConfig {
53    /// 模型提供方列表
54    #[serde(default)]
55    pub providers: Vec<ModelProvider>,
56    /// 当前选中的 provider 索引
57    #[serde(default)]
58    pub active_index: usize,
59    /// 系统提示词(可选)
60    #[serde(default)]
61    pub system_prompt: Option<String>,
62    /// 是否使用流式输出(默认 true,设为 false 则等回复完整后再显示)
63    #[serde(default = "default_stream_mode")]
64    pub stream_mode: bool,
65}
66
67/// 默认流式输出
68fn default_stream_mode() -> bool {
69    true
70}
71
72/// 对话消息
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ChatMessage {
75    pub role: String, // "user" | "assistant" | "system"
76    pub content: String,
77}
78
79/// 对话会话
80#[derive(Debug, Clone, Serialize, Deserialize, Default)]
81pub struct ChatSession {
82    pub messages: Vec<ChatMessage>,
83}
84
85// ========== 文件路径 ==========
86
87/// 获取 agent 数据目录: ~/.jdata/agent/data/
88fn agent_data_dir() -> PathBuf {
89    let dir = YamlConfig::data_dir().join("agent").join("data");
90    let _ = fs::create_dir_all(&dir);
91    dir
92}
93
94/// 获取 agent 配置文件路径
95fn agent_config_path() -> PathBuf {
96    agent_data_dir().join("agent_config.json")
97}
98
99/// 获取对话历史文件路径
100fn chat_history_path() -> PathBuf {
101    agent_data_dir().join("chat_history.json")
102}
103
104// ========== 配置读写 ==========
105
106/// 加载 Agent 配置
107fn load_agent_config() -> AgentConfig {
108    let path = agent_config_path();
109    if !path.exists() {
110        return AgentConfig::default();
111    }
112    match fs::read_to_string(&path) {
113        Ok(content) => serde_json::from_str(&content).unwrap_or_else(|e| {
114            error!("❌ 解析 agent_config.json 失败: {}", e);
115            AgentConfig::default()
116        }),
117        Err(e) => {
118            error!("❌ 读取 agent_config.json 失败: {}", e);
119            AgentConfig::default()
120        }
121    }
122}
123
124/// 保存 Agent 配置
125fn save_agent_config(config: &AgentConfig) -> bool {
126    let path = agent_config_path();
127    if let Some(parent) = path.parent() {
128        let _ = fs::create_dir_all(parent);
129    }
130    match serde_json::to_string_pretty(config) {
131        Ok(json) => match fs::write(&path, json) {
132            Ok(_) => true,
133            Err(e) => {
134                error!("❌ 保存 agent_config.json 失败: {}", e);
135                false
136            }
137        },
138        Err(e) => {
139            error!("❌ 序列化 agent 配置失败: {}", e);
140            false
141        }
142    }
143}
144
145/// 加载对话历史
146fn load_chat_session() -> ChatSession {
147    let path = chat_history_path();
148    if !path.exists() {
149        return ChatSession::default();
150    }
151    match fs::read_to_string(&path) {
152        Ok(content) => serde_json::from_str(&content).unwrap_or_else(|_| ChatSession::default()),
153        Err(_) => ChatSession::default(),
154    }
155}
156
157/// 保存对话历史
158fn save_chat_session(session: &ChatSession) -> bool {
159    let path = chat_history_path();
160    if let Some(parent) = path.parent() {
161        let _ = fs::create_dir_all(parent);
162    }
163    match serde_json::to_string_pretty(session) {
164        Ok(json) => fs::write(&path, json).is_ok(),
165        Err(_) => false,
166    }
167}
168
169// ========== async-openai API 调用 ==========
170
171/// 根据 ModelProvider 配置创建 async-openai Client
172fn create_openai_client(provider: &ModelProvider) -> Client<OpenAIConfig> {
173    let config = OpenAIConfig::new()
174        .with_api_key(&provider.api_key)
175        .with_api_base(&provider.api_base);
176    Client::with_config(config)
177}
178
179/// 将内部 ChatMessage 转换为 async-openai 的请求消息格式
180fn to_openai_messages(messages: &[ChatMessage]) -> Vec<ChatCompletionRequestMessage> {
181    messages
182        .iter()
183        .filter_map(|msg| match msg.role.as_str() {
184            "system" => ChatCompletionRequestSystemMessageArgs::default()
185                .content(msg.content.as_str())
186                .build()
187                .ok()
188                .map(ChatCompletionRequestMessage::System),
189            "user" => ChatCompletionRequestUserMessageArgs::default()
190                .content(msg.content.as_str())
191                .build()
192                .ok()
193                .map(ChatCompletionRequestMessage::User),
194            "assistant" => ChatCompletionRequestAssistantMessageArgs::default()
195                .content(msg.content.as_str())
196                .build()
197                .ok()
198                .map(ChatCompletionRequestMessage::Assistant),
199            _ => None,
200        })
201        .collect()
202}
203
204/// 使用 async-openai 流式调用 API,通过回调逐步输出
205/// 返回完整的助手回复内容
206async fn call_openai_stream_async(
207    provider: &ModelProvider,
208    messages: &[ChatMessage],
209    on_chunk: &mut dyn FnMut(&str),
210) -> Result<String, String> {
211    let client = create_openai_client(provider);
212    let openai_messages = to_openai_messages(messages);
213
214    let request = CreateChatCompletionRequestArgs::default()
215        .model(&provider.model)
216        .messages(openai_messages)
217        .build()
218        .map_err(|e| format!("构建请求失败: {}", e))?;
219
220    let mut stream = client
221        .chat()
222        .create_stream(request)
223        .await
224        .map_err(|e| format!("API 请求失败: {}", e))?;
225
226    let mut full_content = String::new();
227
228    while let Some(result) = stream.next().await {
229        match result {
230            Ok(response) => {
231                for choice in &response.choices {
232                    if let Some(ref content) = choice.delta.content {
233                        full_content.push_str(content);
234                        on_chunk(content);
235                    }
236                }
237            }
238            Err(e) => {
239                return Err(format!("流式响应错误: {}", e));
240            }
241        }
242    }
243
244    Ok(full_content)
245}
246
247/// 同步包装:创建 tokio runtime 执行异步流式调用
248fn call_openai_stream(
249    provider: &ModelProvider,
250    messages: &[ChatMessage],
251    on_chunk: &mut dyn FnMut(&str),
252) -> Result<String, String> {
253    let rt = tokio::runtime::Runtime::new().map_err(|e| format!("创建异步运行时失败: {}", e))?;
254    rt.block_on(call_openai_stream_async(provider, messages, on_chunk))
255}
256
257// ========== 命令入口 ==========
258
259/// 处理 chat 命令: j chat [message...]
260pub fn handle_chat(content: &[String], _config: &YamlConfig) {
261    let agent_config = load_agent_config();
262
263    if agent_config.providers.is_empty() {
264        info!("⚠️  尚未配置 LLM 模型提供方。");
265        info!("📁 请编辑配置文件: {}", agent_config_path().display());
266        info!("📝 配置示例:");
267        let example = AgentConfig {
268            providers: vec![ModelProvider {
269                name: "GPT-4o".to_string(),
270                api_base: "https://api.openai.com/v1".to_string(),
271                api_key: "sk-your-api-key".to_string(),
272                model: "gpt-4o".to_string(),
273            }],
274            active_index: 0,
275            system_prompt: Some("你是一个有用的助手。".to_string()),
276            stream_mode: true,
277        };
278        if let Ok(json) = serde_json::to_string_pretty(&example) {
279            println!("{}", json);
280        }
281        // 自动创建示例配置文件
282        if !agent_config_path().exists() {
283            let _ = save_agent_config(&example);
284            info!(
285                "✅ 已自动创建示例配置文件: {}",
286                agent_config_path().display()
287            );
288            info!("📌 请修改其中的 api_key 和其他配置后重新运行 chat 命令");
289        }
290        return;
291    }
292
293    if content.is_empty() {
294        // 无参数:进入 TUI 对话界面
295        run_chat_tui();
296        return;
297    }
298
299    // 有参数:快速发送消息并打印回复
300    let message = content.join(" ");
301    let message = message.trim().to_string();
302    if message.is_empty() {
303        error!("⚠️ 消息内容为空");
304        return;
305    }
306
307    let idx = agent_config
308        .active_index
309        .min(agent_config.providers.len() - 1);
310    let provider = &agent_config.providers[idx];
311
312    info!("🤖 [{}] 思考中...", provider.name);
313
314    let mut messages = Vec::new();
315    if let Some(sys) = &agent_config.system_prompt {
316        messages.push(ChatMessage {
317            role: "system".to_string(),
318            content: sys.clone(),
319        });
320    }
321    messages.push(ChatMessage {
322        role: "user".to_string(),
323        content: message,
324    });
325
326    match call_openai_stream(provider, &messages, &mut |chunk| {
327        print!("{}", chunk);
328        let _ = io::stdout().flush();
329    }) {
330        Ok(_) => {
331            println!(); // 换行
332        }
333        Err(e) => {
334            error!("\n❌ {}", e);
335        }
336    }
337}
338
339// ========== TUI 界面 ==========
340
341/// 后台线程发送给 TUI 的消息类型
342enum StreamMsg {
343    /// 收到一个流式文本块
344    Chunk,
345    /// 流式响应完成
346    Done,
347    /// 发生错误
348    Error(String),
349}
350
351/// TUI 应用状态
352struct ChatApp {
353    /// Agent 配置
354    agent_config: AgentConfig,
355    /// 当前对话会话
356    session: ChatSession,
357    /// 输入缓冲区
358    input: String,
359    /// 光标位置(字符索引)
360    cursor_pos: usize,
361    /// 当前模式
362    mode: ChatMode,
363    /// 消息列表滚动偏移
364    scroll_offset: u16,
365    /// 是否正在等待 AI 回复
366    is_loading: bool,
367    /// 模型选择列表状态
368    model_list_state: ListState,
369    /// Toast 通知消息 (内容, 是否错误, 创建时间)
370    toast: Option<(String, bool, std::time::Instant)>,
371    /// 用于接收后台流式回复的 channel
372    stream_rx: Option<mpsc::Receiver<StreamMsg>>,
373    /// 当前正在流式接收的 AI 回复内容(实时更新)
374    streaming_content: Arc<Mutex<String>>,
375    /// 消息渲染行缓存:(消息数, 最后一条消息内容hash, 气泡宽度) → 渲染好的行
376    /// 避免每帧都重新解析 Markdown
377    msg_lines_cache: Option<MsgLinesCache>,
378    /// 消息浏览模式中选中的消息索引
379    browse_msg_index: usize,
380}
381
382/// 消息渲染行缓存
383struct MsgLinesCache {
384    /// 会话消息数量
385    msg_count: usize,
386    /// 最后一条消息的内容长度(用于检测流式更新)
387    last_msg_len: usize,
388    /// 流式内容长度
389    streaming_len: usize,
390    /// 是否正在加载
391    is_loading: bool,
392    /// 气泡最大宽度(窗口变化时需要重算)
393    bubble_max_width: usize,
394    /// 浏览模式选中索引(None 表示非浏览模式)
395    browse_index: Option<usize>,
396    /// 缓存的渲染行
397    lines: Vec<Line<'static>>,
398    /// 每条消息(按 msg_index)的起始行号(用于浏览模式自动滚动)
399    msg_start_lines: Vec<(usize, usize)>, // (msg_index, start_line)
400}
401
402/// Toast 通知显示时长(秒)
403const TOAST_DURATION_SECS: u64 = 4;
404
405#[derive(PartialEq)]
406enum ChatMode {
407    /// 正常对话模式(焦点在输入框)
408    Chat,
409    /// 模型选择模式
410    SelectModel,
411    /// 消息浏览模式(可选中消息并复制)
412    Browse,
413    /// 帮助
414    Help,
415}
416
417impl ChatApp {
418    fn new() -> Self {
419        let agent_config = load_agent_config();
420        let session = load_chat_session();
421        let mut model_list_state = ListState::default();
422        if !agent_config.providers.is_empty() {
423            model_list_state.select(Some(agent_config.active_index));
424        }
425        Self {
426            agent_config,
427            session,
428            input: String::new(),
429            cursor_pos: 0,
430            mode: ChatMode::Chat,
431            scroll_offset: u16::MAX, // 默认滚动到底部
432            is_loading: false,
433            model_list_state,
434            toast: None,
435            stream_rx: None,
436            streaming_content: Arc::new(Mutex::new(String::new())),
437            msg_lines_cache: None,
438            browse_msg_index: 0,
439        }
440    }
441
442    /// 显示一条 toast 通知
443    fn show_toast(&mut self, msg: impl Into<String>, is_error: bool) {
444        self.toast = Some((msg.into(), is_error, std::time::Instant::now()));
445    }
446
447    /// 清理过期的 toast
448    fn tick_toast(&mut self) {
449        if let Some((_, _, created)) = &self.toast {
450            if created.elapsed().as_secs() >= TOAST_DURATION_SECS {
451                self.toast = None;
452            }
453        }
454    }
455
456    /// 获取当前活跃的 provider
457    fn active_provider(&self) -> Option<&ModelProvider> {
458        if self.agent_config.providers.is_empty() {
459            return None;
460        }
461        let idx = self
462            .agent_config
463            .active_index
464            .min(self.agent_config.providers.len() - 1);
465        Some(&self.agent_config.providers[idx])
466    }
467
468    /// 获取当前模型名称
469    fn active_model_name(&self) -> String {
470        self.active_provider()
471            .map(|p| p.name.clone())
472            .unwrap_or_else(|| "未配置".to_string())
473    }
474
475    /// 构建发送给 API 的消息列表
476    fn build_api_messages(&self) -> Vec<ChatMessage> {
477        let mut messages = Vec::new();
478        if let Some(sys) = &self.agent_config.system_prompt {
479            messages.push(ChatMessage {
480                role: "system".to_string(),
481                content: sys.clone(),
482            });
483        }
484        for msg in &self.session.messages {
485            messages.push(msg.clone());
486        }
487        messages
488    }
489
490    /// 发送消息(非阻塞,启动后台线程流式接收)
491    fn send_message(&mut self) {
492        let text = self.input.trim().to_string();
493        if text.is_empty() {
494            return;
495        }
496
497        // 添加用户消息
498        self.session.messages.push(ChatMessage {
499            role: "user".to_string(),
500            content: text,
501        });
502        self.input.clear();
503        self.cursor_pos = 0;
504        // 自动滚动到底部
505        self.scroll_offset = u16::MAX;
506
507        // 调用 API
508        let provider = match self.active_provider() {
509            Some(p) => p.clone(),
510            None => {
511                self.show_toast("未配置模型提供方,请先编辑配置文件", true);
512                return;
513            }
514        };
515
516        self.is_loading = true;
517
518        let api_messages = self.build_api_messages();
519
520        // 清空流式内容缓冲
521        {
522            let mut sc = self.streaming_content.lock().unwrap();
523            sc.clear();
524        }
525
526        // 创建 channel 用于后台线程 -> TUI 通信
527        let (tx, rx) = mpsc::channel::<StreamMsg>();
528        self.stream_rx = Some(rx);
529
530        let streaming_content = Arc::clone(&self.streaming_content);
531
532        let use_stream = self.agent_config.stream_mode;
533
534        // 启动后台线程执行 API 调用
535        std::thread::spawn(move || {
536            let rt = match tokio::runtime::Runtime::new() {
537                Ok(rt) => rt,
538                Err(e) => {
539                    let _ = tx.send(StreamMsg::Error(format!("创建异步运行时失败: {}", e)));
540                    return;
541                }
542            };
543
544            rt.block_on(async {
545                let client = create_openai_client(&provider);
546                let openai_messages = to_openai_messages(&api_messages);
547
548                let request = match CreateChatCompletionRequestArgs::default()
549                    .model(&provider.model)
550                    .messages(openai_messages)
551                    .build()
552                {
553                    Ok(req) => req,
554                    Err(e) => {
555                        let _ = tx.send(StreamMsg::Error(format!("构建请求失败: {}", e)));
556                        return;
557                    }
558                };
559
560                if use_stream {
561                    // 流式输出模式
562                    let mut stream = match client.chat().create_stream(request).await {
563                        Ok(s) => s,
564                        Err(e) => {
565                            let _ = tx.send(StreamMsg::Error(format!("API 请求失败: {}", e)));
566                            return;
567                        }
568                    };
569
570                    while let Some(result) = stream.next().await {
571                        match result {
572                            Ok(response) => {
573                                for choice in &response.choices {
574                                    if let Some(ref content) = choice.delta.content {
575                                        // 更新共享缓冲
576                                        {
577                                            let mut sc = streaming_content.lock().unwrap();
578                                            sc.push_str(content);
579                                        }
580                                        let _ = tx.send(StreamMsg::Chunk);
581                                    }
582                                }
583                            }
584                            Err(e) => {
585                                let _ = tx.send(StreamMsg::Error(format!("流式响应错误: {}", e)));
586                                return;
587                            }
588                        }
589                    }
590                } else {
591                    // 非流式输出模式:等待完整响应后一次性返回
592                    match client.chat().create(request).await {
593                        Ok(response) => {
594                            if let Some(choice) = response.choices.first() {
595                                if let Some(ref content) = choice.message.content {
596                                    {
597                                        let mut sc = streaming_content.lock().unwrap();
598                                        sc.push_str(content);
599                                    }
600                                    let _ = tx.send(StreamMsg::Chunk);
601                                }
602                            }
603                        }
604                        Err(e) => {
605                            let _ = tx.send(StreamMsg::Error(format!("API 请求失败: {}", e)));
606                            return;
607                        }
608                    }
609                }
610
611                let _ = tx.send(StreamMsg::Done);
612
613                let _ = tx.send(StreamMsg::Done);
614            });
615        });
616    }
617
618    /// 处理后台流式消息(在主循环中每帧调用)
619    fn poll_stream(&mut self) {
620        if self.stream_rx.is_none() {
621            return;
622        }
623
624        let mut finished = false;
625        let mut had_error = false;
626
627        // 非阻塞地取出所有可用的消息
628        if let Some(ref rx) = self.stream_rx {
629            loop {
630                match rx.try_recv() {
631                    Ok(StreamMsg::Chunk) => {
632                        // 内容已经通过 Arc<Mutex<String>> 更新,这里只确保滚到底部
633                        self.scroll_offset = u16::MAX;
634                    }
635                    Ok(StreamMsg::Done) => {
636                        finished = true;
637                        break;
638                    }
639                    Ok(StreamMsg::Error(e)) => {
640                        self.show_toast(format!("请求失败: {}", e), true);
641                        had_error = true;
642                        finished = true;
643                        break;
644                    }
645                    Err(mpsc::TryRecvError::Empty) => break,
646                    Err(mpsc::TryRecvError::Disconnected) => {
647                        finished = true;
648                        break;
649                    }
650                }
651            }
652        }
653
654        if finished {
655            self.stream_rx = None;
656            self.is_loading = false;
657
658            if !had_error {
659                // 将流式内容作为完整回复添加到会话
660                let content = {
661                    let sc = self.streaming_content.lock().unwrap();
662                    sc.clone()
663                };
664                if !content.is_empty() {
665                    self.session.messages.push(ChatMessage {
666                        role: "assistant".to_string(),
667                        content,
668                    });
669                    // 清空流式缓冲
670                    self.streaming_content.lock().unwrap().clear();
671                    self.show_toast("回复完成 ✓", false);
672                }
673                self.scroll_offset = u16::MAX;
674            } else {
675                // 错误时也清空流式缓冲
676                self.streaming_content.lock().unwrap().clear();
677            }
678
679            // 自动保存对话历史
680            let _ = save_chat_session(&self.session);
681        }
682    }
683
684    /// 清空对话
685    fn clear_session(&mut self) {
686        self.session.messages.clear();
687        self.scroll_offset = 0;
688        let _ = save_chat_session(&self.session);
689        self.show_toast("对话已清空", false);
690    }
691
692    /// 切换模型
693    fn switch_model(&mut self) {
694        if let Some(sel) = self.model_list_state.selected() {
695            self.agent_config.active_index = sel;
696            let _ = save_agent_config(&self.agent_config);
697            let name = self.active_model_name();
698            self.show_toast(format!("已切换到: {}", name), false);
699        }
700        self.mode = ChatMode::Chat;
701    }
702
703    /// 向上滚动消息
704    fn scroll_up(&mut self) {
705        self.scroll_offset = self.scroll_offset.saturating_sub(3);
706    }
707
708    /// 向下滚动消息
709    fn scroll_down(&mut self) {
710        self.scroll_offset = self.scroll_offset.saturating_add(3);
711    }
712}
713
714/// 启动 TUI 对话界面
715fn run_chat_tui() {
716    match run_chat_tui_internal() {
717        Ok(_) => {}
718        Err(e) => {
719            error!("❌ Chat TUI 启动失败: {}", e);
720        }
721    }
722}
723
724fn run_chat_tui_internal() -> io::Result<()> {
725    terminal::enable_raw_mode()?;
726    let mut stdout = io::stdout();
727    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
728
729    let backend = CrosstermBackend::new(stdout);
730    let mut terminal = Terminal::new(backend)?;
731
732    let mut app = ChatApp::new();
733
734    if app.agent_config.providers.is_empty() {
735        terminal::disable_raw_mode()?;
736        execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
737        info!("⚠️  尚未配置 LLM 模型提供方,请先运行 j chat 查看配置说明。");
738        return Ok(());
739    }
740
741    let mut needs_redraw = true; // 首次必须绘制
742
743    loop {
744        // 清理过期 toast(如果有 toast 被清理,需要重绘)
745        let had_toast = app.toast.is_some();
746        app.tick_toast();
747        if had_toast && app.toast.is_none() {
748            needs_redraw = true;
749        }
750
751        // 非阻塞地处理后台流式消息
752        let was_loading = app.is_loading;
753        app.poll_stream();
754        // 流式加载中每帧都需要重绘(内容在更新)
755        if app.is_loading || (was_loading && !app.is_loading) {
756            needs_redraw = true;
757        }
758
759        // 只在状态发生变化时才重绘,大幅降低 CPU 占用
760        if needs_redraw {
761            terminal.draw(|f| draw_chat_ui(f, &mut app))?;
762            needs_redraw = false;
763        }
764
765        // 等待事件:加载中用短间隔以刷新流式内容,空闲时用长间隔节省 CPU
766        let poll_timeout = if app.is_loading {
767            std::time::Duration::from_millis(150)
768        } else {
769            std::time::Duration::from_millis(1000)
770        };
771
772        if event::poll(poll_timeout)? {
773            // 批量消费所有待处理事件,避免快速滚动/打字时事件堆积
774            let mut should_break = false;
775            loop {
776                let evt = event::read()?;
777                match evt {
778                    Event::Key(key) => {
779                        needs_redraw = true;
780                        match app.mode {
781                            ChatMode::Chat => {
782                                if handle_chat_mode(&mut app, key) {
783                                    should_break = true;
784                                    break;
785                                }
786                            }
787                            ChatMode::SelectModel => handle_select_model(&mut app, key),
788                            ChatMode::Browse => handle_browse_mode(&mut app, key),
789                            ChatMode::Help => {
790                                app.mode = ChatMode::Chat;
791                            }
792                        }
793                    }
794                    Event::Mouse(mouse) => match mouse.kind {
795                        MouseEventKind::ScrollUp => {
796                            app.scroll_up();
797                            needs_redraw = true;
798                        }
799                        MouseEventKind::ScrollDown => {
800                            app.scroll_down();
801                            needs_redraw = true;
802                        }
803                        _ => {}
804                    },
805                    Event::Resize(_, _) => {
806                        needs_redraw = true;
807                    }
808                    _ => {}
809                }
810                // 继续消费剩余事件(非阻塞,Duration::ZERO)
811                if !event::poll(std::time::Duration::ZERO)? {
812                    break;
813                }
814            }
815            if should_break {
816                break;
817            }
818        }
819    }
820
821    // 保存对话历史
822    let _ = save_chat_session(&app.session);
823
824    terminal::disable_raw_mode()?;
825    execute!(
826        terminal.backend_mut(),
827        LeaveAlternateScreen,
828        DisableMouseCapture
829    )?;
830    Ok(())
831}
832
833/// 绘制 TUI 界面
834fn draw_chat_ui(f: &mut ratatui::Frame, app: &mut ChatApp) {
835    let size = f.area();
836
837    // 整体背景
838    let bg = Block::default().style(Style::default().bg(Color::Rgb(22, 22, 30)));
839    f.render_widget(bg, size);
840
841    let chunks = Layout::default()
842        .direction(Direction::Vertical)
843        .constraints([
844            Constraint::Length(3), // 标题栏
845            Constraint::Min(5),    // 消息区
846            Constraint::Length(5), // 输入区
847            Constraint::Length(1), // 操作提示栏(始终可见)
848        ])
849        .split(size);
850
851    // ========== 标题栏 ==========
852    draw_title_bar(f, chunks[0], app);
853
854    // ========== 消息区 ==========
855    if app.mode == ChatMode::Help {
856        draw_help(f, chunks[1]);
857    } else if app.mode == ChatMode::SelectModel {
858        draw_model_selector(f, chunks[1], app);
859    } else {
860        draw_messages(f, chunks[1], app);
861    }
862
863    // ========== 输入区 ==========
864    draw_input(f, chunks[2], app);
865
866    // ========== 底部操作提示栏(始终可见)==========
867    draw_hint_bar(f, chunks[3], app);
868
869    // ========== Toast 弹窗覆盖层(右上角)==========
870    draw_toast(f, size, app);
871}
872
873/// 绘制标题栏
874fn draw_title_bar(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
875    let model_name = app.active_model_name();
876    let msg_count = app.session.messages.len();
877    let loading = if app.is_loading {
878        " ⏳ 思考中..."
879    } else {
880        ""
881    };
882
883    let title_spans = vec![
884        Span::styled(" 💬 ", Style::default().fg(Color::Rgb(120, 180, 255))),
885        Span::styled(
886            "AI Chat",
887            Style::default()
888                .fg(Color::White)
889                .add_modifier(Modifier::BOLD),
890        ),
891        Span::styled("  │  ", Style::default().fg(Color::Rgb(60, 60, 80))),
892        Span::styled("🤖 ", Style::default()),
893        Span::styled(
894            model_name,
895            Style::default()
896                .fg(Color::Rgb(160, 220, 160))
897                .add_modifier(Modifier::BOLD),
898        ),
899        Span::styled("  │  ", Style::default().fg(Color::Rgb(60, 60, 80))),
900        Span::styled(
901            format!("📨 {} 条消息", msg_count),
902            Style::default().fg(Color::Rgb(180, 180, 200)),
903        ),
904        Span::styled(
905            loading,
906            Style::default()
907                .fg(Color::Rgb(255, 200, 80))
908                .add_modifier(Modifier::BOLD),
909        ),
910    ];
911
912    let title_block = Paragraph::new(Line::from(title_spans)).block(
913        Block::default()
914            .borders(Borders::ALL)
915            .border_type(ratatui::widgets::BorderType::Rounded)
916            .border_style(Style::default().fg(Color::Rgb(80, 100, 140)))
917            .style(Style::default().bg(Color::Rgb(28, 28, 40))),
918    );
919    f.render_widget(title_block, area);
920}
921
922/// 绘制消息区
923fn draw_messages(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
924    let block = Block::default()
925        .borders(Borders::ALL)
926        .border_type(ratatui::widgets::BorderType::Rounded)
927        .border_style(Style::default().fg(Color::Rgb(50, 55, 70)))
928        .title(Span::styled(
929            " 对话记录 ",
930            Style::default()
931                .fg(Color::Rgb(140, 140, 170))
932                .add_modifier(Modifier::BOLD),
933        ))
934        .title_alignment(ratatui::layout::Alignment::Left)
935        .style(Style::default().bg(Color::Rgb(22, 22, 30)));
936
937    // 空消息时显示欢迎界面
938    if app.session.messages.is_empty() && !app.is_loading {
939        let welcome_lines = vec![
940            Line::from(""),
941            Line::from(""),
942            Line::from(Span::styled(
943                "  ╭──────────────────────────────────────╮",
944                Style::default().fg(Color::Rgb(60, 70, 90)),
945            )),
946            Line::from(Span::styled(
947                "  │                                      │",
948                Style::default().fg(Color::Rgb(60, 70, 90)),
949            )),
950            Line::from(vec![
951                Span::styled("  │     ", Style::default().fg(Color::Rgb(60, 70, 90))),
952                Span::styled(
953                    "Hi! What can I help you?  ",
954                    Style::default().fg(Color::Rgb(120, 140, 180)),
955                ),
956                Span::styled("     │", Style::default().fg(Color::Rgb(60, 70, 90))),
957            ]),
958            Line::from(Span::styled(
959                "  │                                      │",
960                Style::default().fg(Color::Rgb(60, 70, 90)),
961            )),
962            Line::from(Span::styled(
963                "  │     Type a message, press Enter      │",
964                Style::default().fg(Color::Rgb(80, 90, 110)),
965            )),
966            Line::from(Span::styled(
967                "  │                                      │",
968                Style::default().fg(Color::Rgb(60, 70, 90)),
969            )),
970            Line::from(Span::styled(
971                "  ╰──────────────────────────────────────╯",
972                Style::default().fg(Color::Rgb(60, 70, 90)),
973            )),
974        ];
975        let empty = Paragraph::new(welcome_lines).block(block);
976        f.render_widget(empty, area);
977        return;
978    }
979
980    // 内部可用宽度(减去边框和左右各1的 padding)
981    let inner_width = area.width.saturating_sub(4) as usize;
982    // 消息内容最大宽度为可用宽度的 75%
983    let bubble_max_width = (inner_width * 75 / 100).max(20);
984
985    // 计算缓存 key:消息数 + 最后一条消息长度 + 流式内容长度 + is_loading + 气泡宽度 + 浏览模式索引
986    let msg_count = app.session.messages.len();
987    let last_msg_len = app
988        .session
989        .messages
990        .last()
991        .map(|m| m.content.len())
992        .unwrap_or(0);
993    let streaming_len = app.streaming_content.lock().unwrap().len();
994    let current_browse_index = if app.mode == ChatMode::Browse {
995        Some(app.browse_msg_index)
996    } else {
997        None
998    };
999    let cache_hit = if let Some(ref cache) = app.msg_lines_cache {
1000        cache.msg_count == msg_count
1001            && cache.last_msg_len == last_msg_len
1002            && cache.streaming_len == streaming_len
1003            && cache.is_loading == app.is_loading
1004            && cache.bubble_max_width == bubble_max_width
1005            && cache.browse_index == current_browse_index
1006    } else {
1007        false
1008    };
1009
1010    if !cache_hit {
1011        // 缓存未命中,重新构建渲染行并存入缓存
1012        let (new_lines, new_msg_start_lines) =
1013            build_message_lines(app, inner_width, bubble_max_width);
1014        app.msg_lines_cache = Some(MsgLinesCache {
1015            msg_count,
1016            last_msg_len,
1017            streaming_len,
1018            is_loading: app.is_loading,
1019            bubble_max_width,
1020            browse_index: current_browse_index,
1021            lines: new_lines,
1022            msg_start_lines: new_msg_start_lines,
1023        });
1024    }
1025
1026    // 从缓存中借用 lines(零拷贝)
1027    let cached = app.msg_lines_cache.as_ref().unwrap();
1028    let all_lines = &cached.lines;
1029    let total_lines = all_lines.len() as u16;
1030
1031    // 渲染边框
1032    f.render_widget(block, area);
1033
1034    // 计算内部区域(去掉边框)
1035    let inner = area.inner(ratatui::layout::Margin {
1036        vertical: 1,
1037        horizontal: 1,
1038    });
1039    let visible_height = inner.height;
1040    let max_scroll = total_lines.saturating_sub(visible_height);
1041
1042    // 自动滚动到底部(非浏览模式下)
1043    if app.mode != ChatMode::Browse {
1044        if app.scroll_offset == u16::MAX || app.scroll_offset > max_scroll {
1045            app.scroll_offset = max_scroll;
1046        }
1047    } else {
1048        // 浏览模式:自动滚动到选中消息的位置
1049        if let Some(target_line) = cached
1050            .msg_start_lines
1051            .iter()
1052            .find(|(idx, _)| *idx == app.browse_msg_index)
1053            .map(|(_, line)| *line as u16)
1054        {
1055            // 确保选中消息在可视区域内
1056            if target_line < app.scroll_offset {
1057                app.scroll_offset = target_line;
1058            } else if target_line >= app.scroll_offset + visible_height {
1059                app.scroll_offset = target_line.saturating_sub(visible_height / 3);
1060            }
1061            // 限制滚动范围
1062            if app.scroll_offset > max_scroll {
1063                app.scroll_offset = max_scroll;
1064            }
1065        }
1066    }
1067
1068    // 填充内部背景色(避免空白行没有背景)
1069    let bg_fill = Block::default().style(Style::default().bg(Color::Rgb(22, 22, 30)));
1070    f.render_widget(bg_fill, inner);
1071
1072    // 只渲染可见区域的行(逐行借用缓存,clone 单行开销极小)
1073    let start = app.scroll_offset as usize;
1074    let end = (start + visible_height as usize).min(all_lines.len());
1075    for (i, line_idx) in (start..end).enumerate() {
1076        let line = &all_lines[line_idx];
1077        let y = inner.y + i as u16;
1078        let line_area = Rect::new(inner.x, y, inner.width, 1);
1079        // 使用 Paragraph 渲染单行(clone 单行开销很小)
1080        let p = Paragraph::new(line.clone());
1081        f.render_widget(p, line_area);
1082    }
1083}
1084
1085/// 构建所有消息的渲染行(独立函数,用于缓存)
1086/// 返回 (渲染行列表, 消息起始行号映射)
1087fn build_message_lines(
1088    app: &ChatApp,
1089    inner_width: usize,
1090    bubble_max_width: usize,
1091) -> (Vec<Line<'static>>, Vec<(usize, usize)>) {
1092    struct RenderMsg {
1093        role: String,
1094        content: String,
1095        msg_index: Option<usize>, // 对应 session.messages 的索引(流式消息为 None)
1096    }
1097    let mut render_msgs: Vec<RenderMsg> = app
1098        .session
1099        .messages
1100        .iter()
1101        .enumerate()
1102        .map(|(i, m)| RenderMsg {
1103            role: m.role.clone(),
1104            content: m.content.clone(),
1105            msg_index: Some(i),
1106        })
1107        .collect();
1108
1109    // 如果正在流式接收,添加一条临时的 assistant 消息
1110    if app.is_loading {
1111        let streaming = app.streaming_content.lock().unwrap().clone();
1112        if !streaming.is_empty() {
1113            render_msgs.push(RenderMsg {
1114                role: "assistant".to_string(),
1115                content: streaming,
1116                msg_index: None,
1117            });
1118        } else {
1119            // 正在等待首个 chunk,显示占位
1120            render_msgs.push(RenderMsg {
1121                role: "assistant".to_string(),
1122                content: "◍".to_string(),
1123                msg_index: None,
1124            });
1125        }
1126    }
1127
1128    // 构建所有消息行
1129    let is_browse_mode = app.mode == ChatMode::Browse;
1130    let mut lines: Vec<Line> = Vec::new();
1131    let mut msg_start_lines: Vec<(usize, usize)> = Vec::new(); // (msg_index, start_line)
1132    for msg in &render_msgs {
1133        // 判断当前消息是否在浏览模式下被选中
1134        let is_selected = is_browse_mode
1135            && msg.msg_index.is_some()
1136            && msg.msg_index.unwrap() == app.browse_msg_index;
1137
1138        // 记录消息起始行号
1139        if let Some(idx) = msg.msg_index {
1140            msg_start_lines.push((idx, lines.len()));
1141        }
1142
1143        match msg.role.as_str() {
1144            "user" => {
1145                // 用户消息:右对齐,蓝色系
1146                lines.push(Line::from(""));
1147                // 用户标签(浏览模式选中时加 ▶ 指示器)
1148                let label = if is_selected { "▶ You " } else { "You " };
1149                let pad = inner_width.saturating_sub(display_width(label) + 2);
1150                lines.push(Line::from(vec![
1151                    Span::raw(" ".repeat(pad)),
1152                    Span::styled(
1153                        label,
1154                        Style::default()
1155                            .fg(if is_selected {
1156                                Color::Rgb(255, 200, 80)
1157                            } else {
1158                                Color::Rgb(100, 160, 255)
1159                            })
1160                            .add_modifier(Modifier::BOLD),
1161                    ),
1162                ]));
1163                // 消息内容(右对齐气泡效果)
1164                let user_bg = if is_selected {
1165                    Color::Rgb(55, 85, 140)
1166                } else {
1167                    Color::Rgb(40, 70, 120)
1168                };
1169                let user_pad_lr = 3usize; // 左右内边距
1170                let user_content_w = bubble_max_width.saturating_sub(user_pad_lr * 2);
1171
1172                // 先预计算所有换行后的内容行,以便确定实际气泡宽度
1173                let mut all_wrapped_lines: Vec<String> = Vec::new();
1174                for content_line in msg.content.lines() {
1175                    let wrapped = wrap_text(content_line, user_content_w);
1176                    all_wrapped_lines.extend(wrapped);
1177                }
1178                // 如果消息为空,至少保留一行空行
1179                if all_wrapped_lines.is_empty() {
1180                    all_wrapped_lines.push(String::new());
1181                }
1182
1183                // 根据实际内容宽度动态计算气泡宽度(不超过 bubble_max_width)
1184                let actual_content_w = all_wrapped_lines
1185                    .iter()
1186                    .map(|l| display_width(l))
1187                    .max()
1188                    .unwrap_or(0);
1189                let actual_bubble_w = (actual_content_w + user_pad_lr * 2)
1190                    .min(bubble_max_width)
1191                    .max(user_pad_lr * 2 + 1);
1192                let actual_inner_content_w = actual_bubble_w.saturating_sub(user_pad_lr * 2);
1193
1194                // 上边距
1195                {
1196                    let bubble_text = " ".repeat(actual_bubble_w);
1197                    let pad = inner_width.saturating_sub(actual_bubble_w);
1198                    lines.push(Line::from(vec![
1199                        Span::raw(" ".repeat(pad)),
1200                        Span::styled(bubble_text, Style::default().bg(user_bg)),
1201                    ]));
1202                }
1203
1204                for wl in &all_wrapped_lines {
1205                    let wl_width = display_width(wl);
1206                    let fill = actual_inner_content_w.saturating_sub(wl_width);
1207                    let text = format!(
1208                        "{}{}{}{}",
1209                        " ".repeat(user_pad_lr),
1210                        wl,
1211                        " ".repeat(fill),
1212                        " ".repeat(user_pad_lr),
1213                    );
1214                    let text_width = display_width(&text);
1215                    let pad = inner_width.saturating_sub(text_width);
1216                    lines.push(Line::from(vec![
1217                        Span::raw(" ".repeat(pad)),
1218                        Span::styled(text, Style::default().fg(Color::White).bg(user_bg)),
1219                    ]));
1220                }
1221
1222                // 下边距
1223                {
1224                    let bubble_text = " ".repeat(actual_bubble_w);
1225                    let pad = inner_width.saturating_sub(actual_bubble_w);
1226                    lines.push(Line::from(vec![
1227                        Span::raw(" ".repeat(pad)),
1228                        Span::styled(bubble_text, Style::default().bg(user_bg)),
1229                    ]));
1230                }
1231            }
1232            "assistant" => {
1233                // AI 消息:左对齐,使用 Markdown 渲染
1234                lines.push(Line::from(""));
1235                let ai_label = if is_selected { "  ▶ AI" } else { "  AI" };
1236                lines.push(Line::from(Span::styled(
1237                    ai_label,
1238                    Style::default()
1239                        .fg(if is_selected {
1240                            Color::Rgb(255, 200, 80)
1241                        } else {
1242                            Color::Rgb(120, 220, 160)
1243                        })
1244                        .add_modifier(Modifier::BOLD),
1245                )));
1246
1247                // 使用 pulldown-cmark 解析 Markdown 内容并渲染
1248                let bubble_bg = if is_selected {
1249                    Color::Rgb(48, 48, 68)
1250                } else {
1251                    Color::Rgb(38, 38, 52)
1252                };
1253                let pad_left = "   "; // 左内边距 3 字符
1254                let pad_right = "   "; // 右内边距 3 字符
1255                let pad_left_w = 3usize;
1256                let pad_right_w = 3usize;
1257                // 内容区最大宽度要减去左右内边距
1258                let md_content_w = bubble_max_width.saturating_sub(pad_left_w + pad_right_w);
1259                let md_lines = markdown_to_lines(&msg.content, md_content_w + 2); // +2 因为 markdown_to_lines 内部还会减2
1260
1261                // 气泡总宽度 = pad_left + 内容填充到统一宽度 + pad_right
1262                let bubble_total_w = bubble_max_width;
1263
1264                // 上边距:一行空白行(带背景色)
1265                {
1266                    let mut top_spans: Vec<Span> = Vec::new();
1267                    top_spans.push(Span::styled(
1268                        " ".repeat(bubble_total_w),
1269                        Style::default().bg(bubble_bg),
1270                    ));
1271                    lines.push(Line::from(top_spans));
1272                }
1273
1274                for md_line in md_lines {
1275                    // 给每行添加左侧内边距,并应用 AI 消息背景色
1276                    let mut styled_spans: Vec<Span> = Vec::new();
1277                    styled_spans.push(Span::styled(pad_left, Style::default().bg(bubble_bg)));
1278                    // 计算内容区宽度
1279                    let mut content_w: usize = 0;
1280                    for span in md_line.spans {
1281                        let sw = display_width(&span.content);
1282                        content_w += sw;
1283                        // 保留 Markdown 渲染的前景色/修饰符,叠加背景色
1284                        let merged_style = span.style.bg(bubble_bg);
1285                        styled_spans.push(Span::styled(span.content.to_string(), merged_style));
1286                    }
1287                    // 用空格填充到统一宽度,再加右内边距
1288                    let target_content_w = bubble_total_w.saturating_sub(pad_left_w + pad_right_w);
1289                    let fill = target_content_w.saturating_sub(content_w);
1290                    if fill > 0 {
1291                        styled_spans.push(Span::styled(
1292                            " ".repeat(fill),
1293                            Style::default().bg(bubble_bg),
1294                        ));
1295                    }
1296                    styled_spans.push(Span::styled(pad_right, Style::default().bg(bubble_bg)));
1297                    lines.push(Line::from(styled_spans));
1298                }
1299
1300                // 下边距:一行空白行(带背景色)
1301                {
1302                    let mut bottom_spans: Vec<Span> = Vec::new();
1303                    bottom_spans.push(Span::styled(
1304                        " ".repeat(bubble_total_w),
1305                        Style::default().bg(bubble_bg),
1306                    ));
1307                    lines.push(Line::from(bottom_spans));
1308                }
1309            }
1310            "system" => {
1311                // 系统消息:居中,淡色
1312                lines.push(Line::from(""));
1313                let wrapped = wrap_text(&msg.content, inner_width.saturating_sub(8));
1314                for wl in wrapped {
1315                    lines.push(Line::from(Span::styled(
1316                        format!("    {}  {}", "sys", wl),
1317                        Style::default().fg(Color::Rgb(100, 100, 120)),
1318                    )));
1319                }
1320            }
1321            _ => {}
1322        }
1323    }
1324    // 末尾留白
1325    lines.push(Line::from(""));
1326
1327    (lines, msg_start_lines)
1328}
1329
1330/// 将 Markdown 文本解析为 ratatui 的 Line 列表
1331/// 支持:标题(去掉 # 标记)、加粗、斜体、行内代码、代码块(语法高亮)、列表、分隔线
1332/// content_width:内容区可用宽度(不含外层 "  " 缩进和右侧填充)
1333fn markdown_to_lines(md: &str, max_width: usize) -> Vec<Line<'static>> {
1334    use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
1335
1336    // 内容区宽度 = max_width - 2(左侧 "  " 缩进由外层负责)
1337    let content_width = max_width.saturating_sub(2);
1338
1339    let options = Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TABLES;
1340    let parser = Parser::new_ext(md, options);
1341
1342    let mut lines: Vec<Line<'static>> = Vec::new();
1343    let mut current_spans: Vec<Span<'static>> = Vec::new();
1344    let mut style_stack: Vec<Style> = vec![Style::default().fg(Color::Rgb(220, 220, 230))];
1345    let mut in_code_block = false;
1346    let mut code_block_content = String::new();
1347    let mut code_block_lang = String::new();
1348    let mut list_depth: usize = 0;
1349    let mut ordered_index: Option<u64> = None;
1350    let mut heading_level: Option<u8> = None;
1351    // 跟踪是否在引用块中
1352    let mut in_blockquote = false;
1353    // 表格相关状态
1354    let mut in_table = false;
1355    let mut table_rows: Vec<Vec<String>> = Vec::new(); // 收集所有行(含表头)
1356    let mut current_row: Vec<String> = Vec::new();
1357    let mut current_cell = String::new();
1358    let mut table_alignments: Vec<pulldown_cmark::Alignment> = Vec::new();
1359
1360    let base_style = Style::default().fg(Color::Rgb(220, 220, 230));
1361
1362    let flush_line = |current_spans: &mut Vec<Span<'static>>, lines: &mut Vec<Line<'static>>| {
1363        if !current_spans.is_empty() {
1364            lines.push(Line::from(current_spans.drain(..).collect::<Vec<_>>()));
1365        }
1366    };
1367
1368    for event in parser {
1369        match event {
1370            Event::Start(Tag::Heading { level, .. }) => {
1371                flush_line(&mut current_spans, &mut lines);
1372                heading_level = Some(level as u8);
1373                if !lines.is_empty() {
1374                    lines.push(Line::from(""));
1375                }
1376                // 根据标题级别使用不同的颜色
1377                let heading_style = match level as u8 {
1378                    1 => Style::default()
1379                        .fg(Color::Rgb(100, 180, 255))
1380                        .add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
1381                    2 => Style::default()
1382                        .fg(Color::Rgb(130, 190, 255))
1383                        .add_modifier(Modifier::BOLD),
1384                    3 => Style::default()
1385                        .fg(Color::Rgb(160, 200, 255))
1386                        .add_modifier(Modifier::BOLD),
1387                    _ => Style::default()
1388                        .fg(Color::Rgb(180, 210, 255))
1389                        .add_modifier(Modifier::BOLD),
1390                };
1391                style_stack.push(heading_style);
1392            }
1393            Event::End(TagEnd::Heading(level)) => {
1394                flush_line(&mut current_spans, &mut lines);
1395                // h1/h2 下方加分隔线(完整填充 content_width)
1396                if (level as u8) <= 2 {
1397                    let sep_char = if (level as u8) == 1 { "━" } else { "─" };
1398                    lines.push(Line::from(Span::styled(
1399                        sep_char.repeat(content_width),
1400                        Style::default().fg(Color::Rgb(60, 70, 100)),
1401                    )));
1402                }
1403                style_stack.pop();
1404                heading_level = None;
1405            }
1406            Event::Start(Tag::Strong) => {
1407                let current = *style_stack.last().unwrap_or(&base_style);
1408                style_stack.push(current.add_modifier(Modifier::BOLD));
1409            }
1410            Event::End(TagEnd::Strong) => {
1411                style_stack.pop();
1412            }
1413            Event::Start(Tag::Emphasis) => {
1414                let current = *style_stack.last().unwrap_or(&base_style);
1415                style_stack.push(current.add_modifier(Modifier::ITALIC));
1416            }
1417            Event::End(TagEnd::Emphasis) => {
1418                style_stack.pop();
1419            }
1420            Event::Start(Tag::Strikethrough) => {
1421                let current = *style_stack.last().unwrap_or(&base_style);
1422                style_stack.push(current.add_modifier(Modifier::CROSSED_OUT));
1423            }
1424            Event::End(TagEnd::Strikethrough) => {
1425                style_stack.pop();
1426            }
1427            Event::Start(Tag::CodeBlock(kind)) => {
1428                flush_line(&mut current_spans, &mut lines);
1429                in_code_block = true;
1430                code_block_content.clear();
1431                code_block_lang = match kind {
1432                    CodeBlockKind::Fenced(lang) => lang.to_string(),
1433                    CodeBlockKind::Indented => String::new(),
1434                };
1435                // 代码块上方边框(自适应宽度)
1436                let label = if code_block_lang.is_empty() {
1437                    " code ".to_string()
1438                } else {
1439                    format!(" {} ", code_block_lang)
1440                };
1441                let label_w = display_width(&label);
1442                let border_fill = content_width.saturating_sub(2 + label_w);
1443                let top_border = format!("┌─{}{}", label, "─".repeat(border_fill));
1444                lines.push(Line::from(Span::styled(
1445                    top_border,
1446                    Style::default().fg(Color::Rgb(80, 90, 110)),
1447                )));
1448            }
1449            Event::End(TagEnd::CodeBlock) => {
1450                // 渲染代码块内容(带语法高亮)
1451                let code_inner_w = content_width.saturating_sub(4); // "│ " 前缀 + 右侧 " │" 后缀占4
1452                for code_line in code_block_content.lines() {
1453                    let wrapped = wrap_text(code_line, code_inner_w);
1454                    for wl in wrapped {
1455                        let highlighted = highlight_code_line(&wl, &code_block_lang);
1456                        let text_w: usize =
1457                            highlighted.iter().map(|s| display_width(&s.content)).sum();
1458                        let fill = code_inner_w.saturating_sub(text_w);
1459                        let mut spans_vec = Vec::new();
1460                        spans_vec.push(Span::styled(
1461                            "│ ",
1462                            Style::default().fg(Color::Rgb(80, 90, 110)),
1463                        ));
1464                        for hs in highlighted {
1465                            spans_vec.push(Span::styled(
1466                                hs.content.to_string(),
1467                                hs.style.bg(Color::Rgb(30, 30, 42)),
1468                            ));
1469                        }
1470                        spans_vec.push(Span::styled(
1471                            format!("{} │", " ".repeat(fill)),
1472                            Style::default()
1473                                .fg(Color::Rgb(80, 90, 110))
1474                                .bg(Color::Rgb(30, 30, 42)),
1475                        ));
1476                        lines.push(Line::from(spans_vec));
1477                    }
1478                }
1479                let bottom_border = format!("└{}", "─".repeat(content_width.saturating_sub(1)));
1480                lines.push(Line::from(Span::styled(
1481                    bottom_border,
1482                    Style::default().fg(Color::Rgb(80, 90, 110)),
1483                )));
1484                in_code_block = false;
1485                code_block_content.clear();
1486                code_block_lang.clear();
1487            }
1488            Event::Code(text) => {
1489                if in_table {
1490                    // 表格中的行内代码也收集到当前单元格
1491                    current_cell.push('`');
1492                    current_cell.push_str(&text);
1493                    current_cell.push('`');
1494                } else {
1495                    // 行内代码
1496                    current_spans.push(Span::styled(
1497                        format!(" {} ", text),
1498                        Style::default()
1499                            .fg(Color::Rgb(230, 190, 120))
1500                            .bg(Color::Rgb(45, 45, 60)),
1501                    ));
1502                }
1503            }
1504            Event::Start(Tag::List(start)) => {
1505                flush_line(&mut current_spans, &mut lines);
1506                list_depth += 1;
1507                ordered_index = start;
1508            }
1509            Event::End(TagEnd::List(_)) => {
1510                flush_line(&mut current_spans, &mut lines);
1511                list_depth = list_depth.saturating_sub(1);
1512                ordered_index = None;
1513            }
1514            Event::Start(Tag::Item) => {
1515                flush_line(&mut current_spans, &mut lines);
1516                let indent = "  ".repeat(list_depth);
1517                let bullet = if let Some(ref mut idx) = ordered_index {
1518                    let s = format!("{}{}. ", indent, idx);
1519                    *idx += 1;
1520                    s
1521                } else {
1522                    format!("{}- ", indent)
1523                };
1524                current_spans.push(Span::styled(
1525                    bullet,
1526                    Style::default().fg(Color::Rgb(160, 180, 220)),
1527                ));
1528            }
1529            Event::End(TagEnd::Item) => {
1530                flush_line(&mut current_spans, &mut lines);
1531            }
1532            Event::Start(Tag::Paragraph) => {
1533                if !lines.is_empty() && !in_code_block && heading_level.is_none() {
1534                    let last_empty = lines.last().map(|l| l.spans.is_empty()).unwrap_or(false);
1535                    if !last_empty {
1536                        lines.push(Line::from(""));
1537                    }
1538                }
1539            }
1540            Event::End(TagEnd::Paragraph) => {
1541                flush_line(&mut current_spans, &mut lines);
1542            }
1543            Event::Start(Tag::BlockQuote(_)) => {
1544                flush_line(&mut current_spans, &mut lines);
1545                in_blockquote = true;
1546                style_stack.push(Style::default().fg(Color::Rgb(150, 160, 180)));
1547            }
1548            Event::End(TagEnd::BlockQuote(_)) => {
1549                flush_line(&mut current_spans, &mut lines);
1550                in_blockquote = false;
1551                style_stack.pop();
1552            }
1553            Event::Text(text) => {
1554                if in_code_block {
1555                    code_block_content.push_str(&text);
1556                } else if in_table {
1557                    // 表格中的文本收集到当前单元格
1558                    current_cell.push_str(&text);
1559                } else {
1560                    let style = *style_stack.last().unwrap_or(&base_style);
1561                    let text_str = text.to_string();
1562
1563                    // 标题:添加可视化符号前缀代替 # 标记
1564                    if let Some(level) = heading_level {
1565                        let (prefix, prefix_style) = match level {
1566                            1 => (
1567                                ">> ",
1568                                Style::default()
1569                                    .fg(Color::Rgb(100, 180, 255))
1570                                    .add_modifier(Modifier::BOLD),
1571                            ),
1572                            2 => (
1573                                ">> ",
1574                                Style::default()
1575                                    .fg(Color::Rgb(130, 190, 255))
1576                                    .add_modifier(Modifier::BOLD),
1577                            ),
1578                            3 => (
1579                                "> ",
1580                                Style::default()
1581                                    .fg(Color::Rgb(160, 200, 255))
1582                                    .add_modifier(Modifier::BOLD),
1583                            ),
1584                            _ => (
1585                                "> ",
1586                                Style::default()
1587                                    .fg(Color::Rgb(180, 210, 255))
1588                                    .add_modifier(Modifier::BOLD),
1589                            ),
1590                        };
1591                        current_spans.push(Span::styled(prefix.to_string(), prefix_style));
1592                        heading_level = None; // 只加一次前缀
1593                    }
1594
1595                    // 计算 current_spans 已有的显示宽度
1596                    let existing_w: usize = current_spans
1597                        .iter()
1598                        .map(|s| display_width(&s.content))
1599                        .sum();
1600
1601                    // 引用块:加左侧竖线
1602                    let effective_prefix_w = if in_blockquote { 2 } else { 0 }; // "| " 宽度
1603                    let wrap_w = content_width.saturating_sub(effective_prefix_w + existing_w);
1604
1605                    for (i, line) in text_str.split('\n').enumerate() {
1606                        if i > 0 {
1607                            flush_line(&mut current_spans, &mut lines);
1608                            if in_blockquote {
1609                                current_spans.push(Span::styled(
1610                                    "| ".to_string(),
1611                                    Style::default().fg(Color::Rgb(80, 100, 140)),
1612                                ));
1613                            }
1614                        }
1615                        if !line.is_empty() {
1616                            // 第一行使用减去已有 span 宽度的 wrap_w,后续行使用完整 content_width
1617                            let effective_wrap = if i == 0 {
1618                                wrap_w
1619                            } else {
1620                                content_width.saturating_sub(effective_prefix_w)
1621                            };
1622                            let wrapped = wrap_text(line, effective_wrap);
1623                            for (j, wl) in wrapped.iter().enumerate() {
1624                                if j > 0 {
1625                                    flush_line(&mut current_spans, &mut lines);
1626                                    if in_blockquote {
1627                                        current_spans.push(Span::styled(
1628                                            "| ".to_string(),
1629                                            Style::default().fg(Color::Rgb(80, 100, 140)),
1630                                        ));
1631                                    }
1632                                }
1633                                current_spans.push(Span::styled(wl.clone(), style));
1634                            }
1635                        }
1636                    }
1637                }
1638            }
1639            Event::SoftBreak => {
1640                if in_table {
1641                    current_cell.push(' ');
1642                } else {
1643                    current_spans.push(Span::raw(" "));
1644                }
1645            }
1646            Event::HardBreak => {
1647                if in_table {
1648                    current_cell.push(' ');
1649                } else {
1650                    flush_line(&mut current_spans, &mut lines);
1651                }
1652            }
1653            Event::Rule => {
1654                flush_line(&mut current_spans, &mut lines);
1655                lines.push(Line::from(Span::styled(
1656                    "─".repeat(content_width),
1657                    Style::default().fg(Color::Rgb(70, 75, 90)),
1658                )));
1659            }
1660            // ===== 表格支持 =====
1661            Event::Start(Tag::Table(alignments)) => {
1662                flush_line(&mut current_spans, &mut lines);
1663                in_table = true;
1664                table_rows.clear();
1665                table_alignments = alignments;
1666            }
1667            Event::End(TagEnd::Table) => {
1668                // 表格结束:计算列宽,渲染完整表格
1669                flush_line(&mut current_spans, &mut lines);
1670                in_table = false;
1671
1672                if !table_rows.is_empty() {
1673                    let num_cols = table_rows.iter().map(|r| r.len()).max().unwrap_or(0);
1674                    if num_cols > 0 {
1675                        // 计算每列最大宽度
1676                        let mut col_widths: Vec<usize> = vec![0; num_cols];
1677                        for row in &table_rows {
1678                            for (i, cell) in row.iter().enumerate() {
1679                                let w = display_width(cell);
1680                                if w > col_widths[i] {
1681                                    col_widths[i] = w;
1682                                }
1683                            }
1684                        }
1685
1686                        // 限制总宽度不超过 content_width,等比缩放
1687                        let sep_w = num_cols + 1; // 竖线占用
1688                        let pad_w = num_cols * 2; // 每列左右各1空格
1689                        let avail = content_width.saturating_sub(sep_w + pad_w);
1690                        // 单列最大宽度限制(避免一列过宽)
1691                        let max_col_w = avail * 2 / 3;
1692                        for cw in col_widths.iter_mut() {
1693                            if *cw > max_col_w {
1694                                *cw = max_col_w;
1695                            }
1696                        }
1697                        let total_col_w: usize = col_widths.iter().sum();
1698                        if total_col_w > avail && total_col_w > 0 {
1699                            // 等比缩放
1700                            let mut remaining = avail;
1701                            for (i, cw) in col_widths.iter_mut().enumerate() {
1702                                if i == num_cols - 1 {
1703                                    // 最后一列取剩余宽度,避免取整误差
1704                                    *cw = remaining.max(1);
1705                                } else {
1706                                    *cw = ((*cw) * avail / total_col_w).max(1);
1707                                    remaining = remaining.saturating_sub(*cw);
1708                                }
1709                            }
1710                        }
1711
1712                        let table_style = Style::default().fg(Color::Rgb(180, 180, 200));
1713                        let header_style = Style::default()
1714                            .fg(Color::Rgb(120, 180, 255))
1715                            .add_modifier(Modifier::BOLD);
1716                        let border_style = Style::default().fg(Color::Rgb(60, 70, 100));
1717
1718                        // 表格行的实际字符宽度(用空格字符计算,不依赖 Box Drawing 字符宽度)
1719                        // table_row_w = 竖线数(num_cols+1) + 每列(cw+2) = sep_w + pad_w + total_col_w
1720                        let total_col_w_final: usize = col_widths.iter().sum();
1721                        let table_row_w = sep_w + pad_w + total_col_w_final;
1722                        // 表格行右侧需要补充的空格数,使整行宽度等于 content_width
1723                        let table_right_pad = content_width.saturating_sub(table_row_w);
1724
1725                        // 渲染顶边框 ┌─┬─┐
1726                        let mut top = String::from("┌");
1727                        for (i, cw) in col_widths.iter().enumerate() {
1728                            top.push_str(&"─".repeat(cw + 2));
1729                            if i < num_cols - 1 {
1730                                top.push('┬');
1731                            }
1732                        }
1733                        top.push('┐');
1734                        // 补充右侧空格,使宽度对齐 content_width
1735                        let mut top_spans = vec![Span::styled(top, border_style)];
1736                        if table_right_pad > 0 {
1737                            top_spans.push(Span::raw(" ".repeat(table_right_pad)));
1738                        }
1739                        lines.push(Line::from(top_spans));
1740
1741                        for (row_idx, row) in table_rows.iter().enumerate() {
1742                            // 数据行 │ cell │ cell │
1743                            let mut row_spans: Vec<Span> = Vec::new();
1744                            row_spans.push(Span::styled("│", border_style));
1745                            for (i, cw) in col_widths.iter().enumerate() {
1746                                let cell_text = row.get(i).map(|s| s.as_str()).unwrap_or("");
1747                                let cell_w = display_width(cell_text);
1748                                let text = if cell_w > *cw {
1749                                    // 截断
1750                                    let mut t = String::new();
1751                                    let mut w = 0;
1752                                    for ch in cell_text.chars() {
1753                                        let chw = char_width(ch);
1754                                        if w + chw > *cw {
1755                                            break;
1756                                        }
1757                                        t.push(ch);
1758                                        w += chw;
1759                                    }
1760                                    let fill = cw.saturating_sub(w);
1761                                    format!(" {}{} ", t, " ".repeat(fill))
1762                                } else {
1763                                    // 根据对齐方式填充
1764                                    let fill = cw.saturating_sub(cell_w);
1765                                    let align = table_alignments
1766                                        .get(i)
1767                                        .copied()
1768                                        .unwrap_or(pulldown_cmark::Alignment::None);
1769                                    match align {
1770                                        pulldown_cmark::Alignment::Center => {
1771                                            let left = fill / 2;
1772                                            let right = fill - left;
1773                                            format!(
1774                                                " {}{}{} ",
1775                                                " ".repeat(left),
1776                                                cell_text,
1777                                                " ".repeat(right)
1778                                            )
1779                                        }
1780                                        pulldown_cmark::Alignment::Right => {
1781                                            format!(" {}{} ", " ".repeat(fill), cell_text)
1782                                        }
1783                                        _ => {
1784                                            format!(" {}{} ", cell_text, " ".repeat(fill))
1785                                        }
1786                                    }
1787                                };
1788                                let style = if row_idx == 0 {
1789                                    header_style
1790                                } else {
1791                                    table_style
1792                                };
1793                                row_spans.push(Span::styled(text, style));
1794                                row_spans.push(Span::styled("│", border_style));
1795                            }
1796                            // 补充右侧空格,使宽度对齐 content_width
1797                            if table_right_pad > 0 {
1798                                row_spans.push(Span::raw(" ".repeat(table_right_pad)));
1799                            }
1800                            lines.push(Line::from(row_spans));
1801
1802                            // 表头行后加分隔线 ├─┼─┤
1803                            if row_idx == 0 {
1804                                let mut sep = String::from("├");
1805                                for (i, cw) in col_widths.iter().enumerate() {
1806                                    sep.push_str(&"─".repeat(cw + 2));
1807                                    if i < num_cols - 1 {
1808                                        sep.push('┼');
1809                                    }
1810                                }
1811                                sep.push('┤');
1812                                let mut sep_spans = vec![Span::styled(sep, border_style)];
1813                                if table_right_pad > 0 {
1814                                    sep_spans.push(Span::raw(" ".repeat(table_right_pad)));
1815                                }
1816                                lines.push(Line::from(sep_spans));
1817                            }
1818                        }
1819
1820                        // 底边框 └─┴─┘
1821                        let mut bottom = String::from("└");
1822                        for (i, cw) in col_widths.iter().enumerate() {
1823                            bottom.push_str(&"─".repeat(cw + 2));
1824                            if i < num_cols - 1 {
1825                                bottom.push('┴');
1826                            }
1827                        }
1828                        bottom.push('┘');
1829                        let mut bottom_spans = vec![Span::styled(bottom, border_style)];
1830                        if table_right_pad > 0 {
1831                            bottom_spans.push(Span::raw(" ".repeat(table_right_pad)));
1832                        }
1833                        lines.push(Line::from(bottom_spans));
1834                    }
1835                }
1836                table_rows.clear();
1837                table_alignments.clear();
1838            }
1839            Event::Start(Tag::TableHead) => {
1840                current_row.clear();
1841            }
1842            Event::End(TagEnd::TableHead) => {
1843                table_rows.push(current_row.clone());
1844                current_row.clear();
1845            }
1846            Event::Start(Tag::TableRow) => {
1847                current_row.clear();
1848            }
1849            Event::End(TagEnd::TableRow) => {
1850                table_rows.push(current_row.clone());
1851                current_row.clear();
1852            }
1853            Event::Start(Tag::TableCell) => {
1854                current_cell.clear();
1855            }
1856            Event::End(TagEnd::TableCell) => {
1857                current_row.push(current_cell.clone());
1858                current_cell.clear();
1859            }
1860            _ => {}
1861        }
1862    }
1863
1864    // 刷新最后一行
1865    if !current_spans.is_empty() {
1866        lines.push(Line::from(current_spans));
1867    }
1868
1869    // 如果解析结果为空,至少返回原始文本
1870    if lines.is_empty() {
1871        let wrapped = wrap_text(md, content_width);
1872        for wl in wrapped {
1873            lines.push(Line::from(Span::styled(wl, base_style)));
1874        }
1875    }
1876
1877    lines
1878}
1879
1880/// 简单的代码语法高亮(无需外部依赖)
1881/// 根据语言类型对常见关键字、字符串、注释、数字进行着色
1882fn highlight_code_line<'a>(line: &'a str, lang: &str) -> Vec<Span<'static>> {
1883    let lang_lower = lang.to_lowercase();
1884    let keywords: &[&str] = match lang_lower.as_str() {
1885        "rust" | "rs" => &[
1886            "fn", "let", "mut", "pub", "use", "mod", "struct", "enum", "impl", "trait", "for",
1887            "while", "loop", "if", "else", "match", "return", "self", "Self", "where", "async",
1888            "await", "move", "ref", "type", "const", "static", "crate", "super", "as", "in",
1889            "true", "false", "Some", "None", "Ok", "Err",
1890        ],
1891        "python" | "py" => &[
1892            "def", "class", "return", "if", "elif", "else", "for", "while", "import", "from", "as",
1893            "with", "try", "except", "finally", "raise", "pass", "break", "continue", "yield",
1894            "lambda", "and", "or", "not", "in", "is", "True", "False", "None", "global",
1895            "nonlocal", "assert", "del", "async", "await", "self", "print",
1896        ],
1897        "javascript" | "js" | "typescript" | "ts" | "jsx" | "tsx" => &[
1898            "function",
1899            "const",
1900            "let",
1901            "var",
1902            "return",
1903            "if",
1904            "else",
1905            "for",
1906            "while",
1907            "class",
1908            "new",
1909            "this",
1910            "import",
1911            "export",
1912            "from",
1913            "default",
1914            "async",
1915            "await",
1916            "try",
1917            "catch",
1918            "finally",
1919            "throw",
1920            "typeof",
1921            "instanceof",
1922            "true",
1923            "false",
1924            "null",
1925            "undefined",
1926            "of",
1927            "in",
1928            "switch",
1929            "case",
1930        ],
1931        "go" | "golang" => &[
1932            "func",
1933            "package",
1934            "import",
1935            "return",
1936            "if",
1937            "else",
1938            "for",
1939            "range",
1940            "struct",
1941            "interface",
1942            "type",
1943            "var",
1944            "const",
1945            "defer",
1946            "go",
1947            "chan",
1948            "select",
1949            "case",
1950            "switch",
1951            "default",
1952            "break",
1953            "continue",
1954            "map",
1955            "true",
1956            "false",
1957            "nil",
1958            "make",
1959            "append",
1960            "len",
1961            "cap",
1962        ],
1963        "java" | "kotlin" | "kt" => &[
1964            "public",
1965            "private",
1966            "protected",
1967            "class",
1968            "interface",
1969            "extends",
1970            "implements",
1971            "return",
1972            "if",
1973            "else",
1974            "for",
1975            "while",
1976            "new",
1977            "this",
1978            "import",
1979            "package",
1980            "static",
1981            "final",
1982            "void",
1983            "int",
1984            "String",
1985            "boolean",
1986            "true",
1987            "false",
1988            "null",
1989            "try",
1990            "catch",
1991            "throw",
1992            "throws",
1993            "fun",
1994            "val",
1995            "var",
1996            "when",
1997            "object",
1998            "companion",
1999        ],
2000        "sh" | "bash" | "zsh" | "shell" => &[
2001            "if",
2002            "then",
2003            "else",
2004            "elif",
2005            "fi",
2006            "for",
2007            "while",
2008            "do",
2009            "done",
2010            "case",
2011            "esac",
2012            "function",
2013            "return",
2014            "exit",
2015            "echo",
2016            "export",
2017            "local",
2018            "readonly",
2019            "set",
2020            "unset",
2021            "shift",
2022            "source",
2023            "in",
2024            "true",
2025            "false",
2026            "read",
2027            "declare",
2028            "typeset",
2029            "trap",
2030            "eval",
2031            "exec",
2032            "test",
2033            "select",
2034            "until",
2035            "break",
2036            "continue",
2037            "printf",
2038            // Go 命令
2039            "go",
2040            "build",
2041            "run",
2042            "test",
2043            "fmt",
2044            "vet",
2045            "mod",
2046            "get",
2047            "install",
2048            "clean",
2049            "doc",
2050            "list",
2051            "version",
2052            "env",
2053            "generate",
2054            "tool",
2055            "proxy",
2056            "GOPATH",
2057            "GOROOT",
2058            "GOBIN",
2059            "GOMODCACHE",
2060            "GOPROXY",
2061            "GOSUMDB",
2062            // Cargo 命令
2063            "cargo",
2064            "new",
2065            "init",
2066            "add",
2067            "remove",
2068            "update",
2069            "check",
2070            "clippy",
2071            "rustfmt",
2072            "rustc",
2073            "rustup",
2074            "publish",
2075            "install",
2076            "uninstall",
2077            "search",
2078            "tree",
2079            "locate_project",
2080            "metadata",
2081            "audit",
2082            "watch",
2083            "expand",
2084        ],
2085        "c" | "cpp" | "c++" | "h" | "hpp" => &[
2086            "int",
2087            "char",
2088            "float",
2089            "double",
2090            "void",
2091            "long",
2092            "short",
2093            "unsigned",
2094            "signed",
2095            "const",
2096            "static",
2097            "extern",
2098            "struct",
2099            "union",
2100            "enum",
2101            "typedef",
2102            "sizeof",
2103            "return",
2104            "if",
2105            "else",
2106            "for",
2107            "while",
2108            "do",
2109            "switch",
2110            "case",
2111            "break",
2112            "continue",
2113            "default",
2114            "goto",
2115            "auto",
2116            "register",
2117            "volatile",
2118            "class",
2119            "public",
2120            "private",
2121            "protected",
2122            "virtual",
2123            "override",
2124            "template",
2125            "namespace",
2126            "using",
2127            "new",
2128            "delete",
2129            "try",
2130            "catch",
2131            "throw",
2132            "nullptr",
2133            "true",
2134            "false",
2135            "this",
2136            "include",
2137            "define",
2138            "ifdef",
2139            "ifndef",
2140            "endif",
2141        ],
2142        "sql" => &[
2143            "SELECT",
2144            "FROM",
2145            "WHERE",
2146            "INSERT",
2147            "UPDATE",
2148            "DELETE",
2149            "CREATE",
2150            "DROP",
2151            "ALTER",
2152            "TABLE",
2153            "INDEX",
2154            "INTO",
2155            "VALUES",
2156            "SET",
2157            "AND",
2158            "OR",
2159            "NOT",
2160            "NULL",
2161            "JOIN",
2162            "LEFT",
2163            "RIGHT",
2164            "INNER",
2165            "OUTER",
2166            "ON",
2167            "GROUP",
2168            "BY",
2169            "ORDER",
2170            "ASC",
2171            "DESC",
2172            "HAVING",
2173            "LIMIT",
2174            "OFFSET",
2175            "UNION",
2176            "AS",
2177            "DISTINCT",
2178            "COUNT",
2179            "SUM",
2180            "AVG",
2181            "MIN",
2182            "MAX",
2183            "LIKE",
2184            "IN",
2185            "BETWEEN",
2186            "EXISTS",
2187            "CASE",
2188            "WHEN",
2189            "THEN",
2190            "ELSE",
2191            "END",
2192            "BEGIN",
2193            "COMMIT",
2194            "ROLLBACK",
2195            "PRIMARY",
2196            "KEY",
2197            "FOREIGN",
2198            "REFERENCES",
2199            "select",
2200            "from",
2201            "where",
2202            "insert",
2203            "update",
2204            "delete",
2205            "create",
2206            "drop",
2207            "alter",
2208            "table",
2209            "index",
2210            "into",
2211            "values",
2212            "set",
2213            "and",
2214            "or",
2215            "not",
2216            "null",
2217            "join",
2218            "left",
2219            "right",
2220            "inner",
2221            "outer",
2222            "on",
2223            "group",
2224            "by",
2225            "order",
2226            "asc",
2227            "desc",
2228            "having",
2229            "limit",
2230            "offset",
2231            "union",
2232            "as",
2233            "distinct",
2234            "count",
2235            "sum",
2236            "avg",
2237            "min",
2238            "max",
2239            "like",
2240            "in",
2241            "between",
2242            "exists",
2243            "case",
2244            "when",
2245            "then",
2246            "else",
2247            "end",
2248            "begin",
2249            "commit",
2250            "rollback",
2251            "primary",
2252            "key",
2253            "foreign",
2254            "references",
2255        ],
2256        "yaml" | "yml" => &["true", "false", "null", "yes", "no", "on", "off"],
2257        "toml" => &[
2258            "true",
2259            "false",
2260            "true",
2261            "false",
2262            // Cargo.toml 常用
2263            "name",
2264            "version",
2265            "edition",
2266            "authors",
2267            "dependencies",
2268            "dev-dependencies",
2269            "build-dependencies",
2270            "features",
2271            "workspace",
2272            "members",
2273            "exclude",
2274            "include",
2275            "path",
2276            "git",
2277            "branch",
2278            "tag",
2279            "rev",
2280            "package",
2281            "lib",
2282            "bin",
2283            "example",
2284            "test",
2285            "bench",
2286            "doc",
2287            "profile",
2288            "release",
2289            "debug",
2290            "opt-level",
2291            "lto",
2292            "codegen-units",
2293            "panic",
2294            "strip",
2295            "default",
2296            "features",
2297            "optional",
2298            // 常见配置项
2299            "repository",
2300            "homepage",
2301            "documentation",
2302            "license",
2303            "license-file",
2304            "keywords",
2305            "categories",
2306            "readme",
2307            "description",
2308            "resolver",
2309        ],
2310        "css" | "scss" | "less" => &[
2311            "color",
2312            "background",
2313            "border",
2314            "margin",
2315            "padding",
2316            "display",
2317            "position",
2318            "width",
2319            "height",
2320            "font",
2321            "text",
2322            "flex",
2323            "grid",
2324            "align",
2325            "justify",
2326            "important",
2327            "none",
2328            "auto",
2329            "inherit",
2330            "initial",
2331            "unset",
2332        ],
2333        "dockerfile" | "docker" => &[
2334            "FROM",
2335            "RUN",
2336            "CMD",
2337            "LABEL",
2338            "EXPOSE",
2339            "ENV",
2340            "ADD",
2341            "COPY",
2342            "ENTRYPOINT",
2343            "VOLUME",
2344            "USER",
2345            "WORKDIR",
2346            "ARG",
2347            "ONBUILD",
2348            "STOPSIGNAL",
2349            "HEALTHCHECK",
2350            "SHELL",
2351            "AS",
2352        ],
2353        "ruby" | "rb" => &[
2354            "def", "end", "class", "module", "if", "elsif", "else", "unless", "while", "until",
2355            "for", "do", "begin", "rescue", "ensure", "raise", "return", "yield", "require",
2356            "include", "attr", "self", "true", "false", "nil", "puts", "print",
2357        ],
2358        _ => &[
2359            "fn", "function", "def", "class", "return", "if", "else", "for", "while", "import",
2360            "export", "const", "let", "var", "true", "false", "null", "nil", "None", "self",
2361            "this",
2362        ],
2363    };
2364
2365    let comment_prefix = match lang_lower.as_str() {
2366        "python" | "py" | "sh" | "bash" | "zsh" | "shell" | "ruby" | "rb" | "yaml" | "yml"
2367        | "toml" | "dockerfile" | "docker" => "#",
2368        "sql" => "--",
2369        "css" | "scss" | "less" => "/*",
2370        _ => "//",
2371    };
2372
2373    // 默认代码颜色
2374    let code_style = Style::default().fg(Color::Rgb(200, 200, 210));
2375    // 关键字颜色
2376    let kw_style = Style::default().fg(Color::Rgb(198, 120, 221));
2377    // 字符串颜色
2378    let str_style = Style::default().fg(Color::Rgb(152, 195, 121));
2379    // 注释颜色
2380    let comment_style = Style::default()
2381        .fg(Color::Rgb(92, 99, 112))
2382        .add_modifier(Modifier::ITALIC);
2383    // 数字颜色
2384    let num_style = Style::default().fg(Color::Rgb(209, 154, 102));
2385    // 类型/大写开头标识符
2386    let type_style = Style::default().fg(Color::Rgb(229, 192, 123));
2387
2388    let trimmed = line.trim_start();
2389
2390    // 注释行
2391    if trimmed.starts_with(comment_prefix) {
2392        return vec![Span::styled(line.to_string(), comment_style)];
2393    }
2394
2395    // 逐词解析
2396    let mut spans = Vec::new();
2397    let mut chars = line.chars().peekable();
2398    let mut buf = String::new();
2399
2400    while let Some(&ch) = chars.peek() {
2401        // 字符串
2402        if ch == '"' || ch == '\'' || ch == '`' {
2403            // 先刷新 buf
2404            if !buf.is_empty() {
2405                spans.extend(colorize_tokens(
2406                    &buf, keywords, code_style, kw_style, num_style, type_style,
2407                ));
2408                buf.clear();
2409            }
2410            let quote = ch;
2411            let mut s = String::new();
2412            s.push(ch);
2413            chars.next();
2414            while let Some(&c) = chars.peek() {
2415                s.push(c);
2416                chars.next();
2417                if c == quote && !s.ends_with("\\\\") {
2418                    break;
2419                }
2420            }
2421            spans.push(Span::styled(s, str_style));
2422            continue;
2423        }
2424        // Shell 变量 ($VAR, ${VAR}, $1 等)
2425        if ch == '$'
2426            && matches!(
2427                lang_lower.as_str(),
2428                "sh" | "bash" | "zsh" | "shell" | "dockerfile" | "docker"
2429            )
2430        {
2431            if !buf.is_empty() {
2432                spans.extend(colorize_tokens(
2433                    &buf, keywords, code_style, kw_style, num_style, type_style,
2434                ));
2435                buf.clear();
2436            }
2437            let var_style = Style::default().fg(Color::Rgb(86, 182, 194));
2438            let mut var = String::new();
2439            var.push(ch);
2440            chars.next();
2441            if let Some(&next_ch) = chars.peek() {
2442                if next_ch == '{' {
2443                    // ${VAR}
2444                    var.push(next_ch);
2445                    chars.next();
2446                    while let Some(&c) = chars.peek() {
2447                        var.push(c);
2448                        chars.next();
2449                        if c == '}' {
2450                            break;
2451                        }
2452                    }
2453                } else if next_ch == '(' {
2454                    // $(cmd)
2455                    var.push(next_ch);
2456                    chars.next();
2457                    let mut depth = 1;
2458                    while let Some(&c) = chars.peek() {
2459                        var.push(c);
2460                        chars.next();
2461                        if c == '(' {
2462                            depth += 1;
2463                        }
2464                        if c == ')' {
2465                            depth -= 1;
2466                            if depth == 0 {
2467                                break;
2468                            }
2469                        }
2470                    }
2471                } else if next_ch.is_alphanumeric()
2472                    || next_ch == '_'
2473                    || next_ch == '@'
2474                    || next_ch == '#'
2475                    || next_ch == '?'
2476                    || next_ch == '!'
2477                {
2478                    // $VAR, $1, $@, $#, $? 等
2479                    while let Some(&c) = chars.peek() {
2480                        if c.is_alphanumeric() || c == '_' {
2481                            var.push(c);
2482                            chars.next();
2483                        } else {
2484                            break;
2485                        }
2486                    }
2487                }
2488            }
2489            spans.push(Span::styled(var, var_style));
2490            continue;
2491        }
2492        // 行内注释
2493        if ch == '/' || ch == '#' {
2494            let rest: String = chars.clone().collect();
2495            if rest.starts_with(comment_prefix) {
2496                if !buf.is_empty() {
2497                    spans.extend(colorize_tokens(
2498                        &buf, keywords, code_style, kw_style, num_style, type_style,
2499                    ));
2500                    buf.clear();
2501                }
2502                spans.push(Span::styled(rest, comment_style));
2503                break;
2504            }
2505        }
2506        buf.push(ch);
2507        chars.next();
2508    }
2509
2510    if !buf.is_empty() {
2511        spans.extend(colorize_tokens(
2512            &buf, keywords, code_style, kw_style, num_style, type_style,
2513        ));
2514    }
2515
2516    if spans.is_empty() {
2517        spans.push(Span::styled(line.to_string(), code_style));
2518    }
2519
2520    spans
2521}
2522
2523/// 将文本按照 word boundary 拆分并对关键字、数字、类型名着色
2524fn colorize_tokens<'a>(
2525    text: &str,
2526    keywords: &[&str],
2527    default_style: Style,
2528    kw_style: Style,
2529    num_style: Style,
2530    type_style: Style,
2531) -> Vec<Span<'static>> {
2532    let mut spans = Vec::new();
2533    let mut current_word = String::new();
2534    let mut current_non_word = String::new();
2535
2536    for ch in text.chars() {
2537        if ch.is_alphanumeric() || ch == '_' {
2538            if !current_non_word.is_empty() {
2539                spans.push(Span::styled(current_non_word.clone(), default_style));
2540                current_non_word.clear();
2541            }
2542            current_word.push(ch);
2543        } else {
2544            if !current_word.is_empty() {
2545                let style = if keywords.contains(&current_word.as_str()) {
2546                    kw_style
2547                } else if current_word
2548                    .chars()
2549                    .next()
2550                    .map(|c| c.is_ascii_digit())
2551                    .unwrap_or(false)
2552                {
2553                    num_style
2554                } else if current_word
2555                    .chars()
2556                    .next()
2557                    .map(|c| c.is_uppercase())
2558                    .unwrap_or(false)
2559                {
2560                    type_style
2561                } else {
2562                    default_style
2563                };
2564                spans.push(Span::styled(current_word.clone(), style));
2565                current_word.clear();
2566            }
2567            current_non_word.push(ch);
2568        }
2569    }
2570
2571    // 刷新剩余
2572    if !current_non_word.is_empty() {
2573        spans.push(Span::styled(current_non_word, default_style));
2574    }
2575    if !current_word.is_empty() {
2576        let style = if keywords.contains(&current_word.as_str()) {
2577            kw_style
2578        } else if current_word
2579            .chars()
2580            .next()
2581            .map(|c| c.is_ascii_digit())
2582            .unwrap_or(false)
2583        {
2584            num_style
2585        } else if current_word
2586            .chars()
2587            .next()
2588            .map(|c| c.is_uppercase())
2589            .unwrap_or(false)
2590        {
2591            type_style
2592        } else {
2593            default_style
2594        };
2595        spans.push(Span::styled(current_word, style));
2596    }
2597
2598    spans
2599}
2600
2601/// 简单文本自动换行
2602fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
2603    if max_width == 0 {
2604        return vec![text.to_string()];
2605    }
2606    let mut result = Vec::new();
2607    let mut current_line = String::new();
2608    let mut current_width = 0;
2609
2610    for ch in text.chars() {
2611        let ch_width = char_width(ch);
2612        if current_width + ch_width > max_width && !current_line.is_empty() {
2613            result.push(current_line.clone());
2614            current_line.clear();
2615            current_width = 0;
2616        }
2617        current_line.push(ch);
2618        current_width += ch_width;
2619    }
2620    if !current_line.is_empty() {
2621        result.push(current_line);
2622    }
2623    if result.is_empty() {
2624        result.push(String::new());
2625    }
2626    result
2627}
2628
2629/// 计算字符串的显示宽度
2630/// 使用 unicode-width 规则:CJK 字符宽度2,其他(含 Box Drawing、符号等)宽度1
2631fn display_width(s: &str) -> usize {
2632    s.chars().map(|c| char_width(c)).sum()
2633}
2634
2635/// 计算单个字符的显示宽度
2636fn char_width(c: char) -> usize {
2637    if c.is_ascii() {
2638        return 1;
2639    }
2640    // CJK Unified Ideographs 及扩展
2641    let cp = c as u32;
2642    if (0x4E00..=0x9FFF).contains(&cp)    // CJK Unified Ideographs
2643        || (0x3400..=0x4DBF).contains(&cp) // CJK Unified Ideographs Extension A
2644        || (0x20000..=0x2A6DF).contains(&cp) // Extension B
2645        || (0x2A700..=0x2B73F).contains(&cp) // Extension C
2646        || (0x2B740..=0x2B81F).contains(&cp) // Extension D
2647        || (0xF900..=0xFAFF).contains(&cp)   // CJK Compatibility Ideographs
2648        || (0x2F800..=0x2FA1F).contains(&cp)  // CJK Compatibility Ideographs Supplement
2649        // CJK 标点和符号
2650        || (0x3000..=0x303F).contains(&cp)    // CJK Symbols and Punctuation
2651        || (0xFF01..=0xFF60).contains(&cp)    // Fullwidth Forms
2652        || (0xFFE0..=0xFFE6).contains(&cp)    // Fullwidth Signs
2653        // 日韩
2654        || (0x3040..=0x309F).contains(&cp)    // Hiragana
2655        || (0x30A0..=0x30FF).contains(&cp)    // Katakana
2656        || (0xAC00..=0xD7AF).contains(&cp)    // Hangul Syllables
2657        // Emoji(常见范围)
2658        || (0x1F300..=0x1F9FF).contains(&cp)
2659        || (0x2600..=0x26FF).contains(&cp)
2660        || (0x2700..=0x27BF).contains(&cp)
2661    {
2662        2
2663    } else {
2664        1
2665    }
2666}
2667
2668/// 绘制输入区
2669fn draw_input(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
2670    // 输入区可用宽度(减去边框2 + prompt 4)
2671    let usable_width = area.width.saturating_sub(2 + 4) as usize;
2672
2673    let chars: Vec<char> = app.input.chars().collect();
2674
2675    // 计算光标之前文本的显示宽度,决定是否需要水平滚动
2676    let before_all: String = chars[..app.cursor_pos].iter().collect();
2677    let before_width = display_width(&before_all);
2678
2679    // 如果光标超出可视范围,从光标附近开始显示
2680    let scroll_offset_chars = if before_width >= usable_width {
2681        // 往回找到一个合适的起始字符位置
2682        let target_width = before_width.saturating_sub(usable_width / 2);
2683        let mut w = 0;
2684        let mut skip = 0;
2685        for (i, &ch) in chars.iter().enumerate() {
2686            if w >= target_width {
2687                skip = i;
2688                break;
2689            }
2690            w += char_width(ch);
2691        }
2692        skip
2693    } else {
2694        0
2695    };
2696
2697    // 截取可见部分的字符
2698    let visible_chars = &chars[scroll_offset_chars..];
2699    let cursor_in_visible = app.cursor_pos - scroll_offset_chars;
2700
2701    let before: String = visible_chars[..cursor_in_visible].iter().collect();
2702    let cursor_ch = if cursor_in_visible < visible_chars.len() {
2703        visible_chars[cursor_in_visible].to_string()
2704    } else {
2705        " ".to_string()
2706    };
2707    let after: String = if cursor_in_visible < visible_chars.len() {
2708        visible_chars[cursor_in_visible + 1..].iter().collect()
2709    } else {
2710        String::new()
2711    };
2712
2713    let prompt_style = if app.is_loading {
2714        Style::default().fg(Color::Rgb(255, 200, 80))
2715    } else {
2716        Style::default().fg(Color::Rgb(100, 200, 130))
2717    };
2718    let prompt_text = if app.is_loading { " .. " } else { " >  " };
2719
2720    // 构建多行输入显示(手动换行)
2721    let full_visible = format!("{}{}{}", before, cursor_ch, after);
2722    let inner_height = area.height.saturating_sub(2) as usize; // 减去边框
2723    let wrapped_lines = wrap_text(&full_visible, usable_width);
2724
2725    // 找到光标所在的行索引
2726    let before_len = before.chars().count();
2727    let cursor_len = cursor_ch.chars().count();
2728    let cursor_global_pos = before_len; // 光标在全部可见字符中的位置
2729    let mut cursor_line_idx: usize = 0;
2730    {
2731        let mut cumulative = 0usize;
2732        for (li, wl) in wrapped_lines.iter().enumerate() {
2733            let line_char_count = wl.chars().count();
2734            if cumulative + line_char_count > cursor_global_pos {
2735                cursor_line_idx = li;
2736                break;
2737            }
2738            cumulative += line_char_count;
2739            cursor_line_idx = li; // 光标恰好在最后一行末尾
2740        }
2741    }
2742
2743    // 计算行滚动:确保光标所在行在可见区域内
2744    let line_scroll = if wrapped_lines.len() <= inner_height {
2745        0
2746    } else if cursor_line_idx < inner_height {
2747        0
2748    } else {
2749        // 让光标行显示在可见区域的最后一行
2750        cursor_line_idx.saturating_sub(inner_height - 1)
2751    };
2752
2753    // 构建带光标高亮的行
2754    let mut display_lines: Vec<Line> = Vec::new();
2755    let mut char_offset: usize = 0;
2756    // 跳过滚动行的字符数
2757    for wl in wrapped_lines.iter().take(line_scroll) {
2758        char_offset += wl.chars().count();
2759    }
2760
2761    for (_line_idx, wl) in wrapped_lines
2762        .iter()
2763        .skip(line_scroll)
2764        .enumerate()
2765        .take(inner_height.max(1))
2766    {
2767        let mut spans: Vec<Span> = Vec::new();
2768        if _line_idx == 0 && line_scroll == 0 {
2769            spans.push(Span::styled(prompt_text, prompt_style));
2770        } else {
2771            spans.push(Span::styled("    ", Style::default())); // 对齐 prompt
2772        }
2773
2774        // 对该行的每个字符分配样式
2775        let line_chars: Vec<char> = wl.chars().collect();
2776        let mut seg_start = 0;
2777        for (ci, &ch) in line_chars.iter().enumerate() {
2778            let global_idx = char_offset + ci;
2779            let is_cursor = global_idx >= before_len && global_idx < before_len + cursor_len;
2780
2781            if is_cursor {
2782                // 先把 cursor 前的部分输出
2783                if ci > seg_start {
2784                    let seg: String = line_chars[seg_start..ci].iter().collect();
2785                    spans.push(Span::styled(seg, Style::default().fg(Color::White)));
2786                }
2787                spans.push(Span::styled(
2788                    ch.to_string(),
2789                    Style::default()
2790                        .fg(Color::Rgb(22, 22, 30))
2791                        .bg(Color::Rgb(200, 210, 240)),
2792                ));
2793                seg_start = ci + 1;
2794            }
2795        }
2796        // 输出剩余部分
2797        if seg_start < line_chars.len() {
2798            let seg: String = line_chars[seg_start..].iter().collect();
2799            spans.push(Span::styled(seg, Style::default().fg(Color::White)));
2800        }
2801
2802        char_offset += line_chars.len();
2803        display_lines.push(Line::from(spans));
2804    }
2805
2806    if display_lines.is_empty() {
2807        display_lines.push(Line::from(vec![
2808            Span::styled(prompt_text, prompt_style),
2809            Span::styled(
2810                " ",
2811                Style::default()
2812                    .fg(Color::Rgb(22, 22, 30))
2813                    .bg(Color::Rgb(200, 210, 240)),
2814            ),
2815        ]));
2816    }
2817
2818    let input_widget = Paragraph::new(display_lines).block(
2819        Block::default()
2820            .borders(Borders::ALL)
2821            .border_type(ratatui::widgets::BorderType::Rounded)
2822            .border_style(if app.is_loading {
2823                Style::default().fg(Color::Rgb(120, 100, 50))
2824            } else {
2825                Style::default().fg(Color::Rgb(60, 100, 80))
2826            })
2827            .title(Span::styled(
2828                " 输入消息 ",
2829                Style::default().fg(Color::Rgb(140, 140, 170)),
2830            ))
2831            .style(Style::default().bg(Color::Rgb(26, 26, 38))),
2832    );
2833
2834    f.render_widget(input_widget, area);
2835
2836    // 设置终端光标位置,确保中文输入法 IME 候选窗口在正确位置
2837    // 计算光标在渲染后的坐标
2838    if !app.is_loading {
2839        let prompt_w: u16 = 4; // prompt 宽度
2840        let border_left: u16 = 1; // 左边框
2841
2842        // 光标在当前显示行中的列偏移
2843        let cursor_col_in_line = {
2844            let mut col = 0usize;
2845            let mut char_count = 0usize;
2846            // 跳过 line_scroll 之前的字符
2847            let mut skip_chars = 0usize;
2848            for wl in wrapped_lines.iter().take(line_scroll) {
2849                skip_chars += wl.chars().count();
2850            }
2851            // 找到光标在当前行的列
2852            for wl in wrapped_lines.iter().skip(line_scroll) {
2853                let line_len = wl.chars().count();
2854                if skip_chars + char_count + line_len > cursor_global_pos {
2855                    // 光标在这一行
2856                    let pos_in_line = cursor_global_pos - (skip_chars + char_count);
2857                    col = wl.chars().take(pos_in_line).map(|c| char_width(c)).sum();
2858                    break;
2859                }
2860                char_count += line_len;
2861            }
2862            col as u16
2863        };
2864
2865        // 光标在显示行中的行偏移
2866        let cursor_row_in_display = (cursor_line_idx - line_scroll) as u16;
2867
2868        let cursor_x = area.x + border_left + prompt_w + cursor_col_in_line;
2869        let cursor_y = area.y + 1 + cursor_row_in_display; // +1 跳过上边框
2870
2871        // 确保光标在区域内
2872        if cursor_x < area.x + area.width && cursor_y < area.y + area.height {
2873            f.set_cursor_position((cursor_x, cursor_y));
2874        }
2875    }
2876}
2877
2878/// 绘制底部操作提示栏(始终可见)
2879fn draw_hint_bar(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
2880    let hints = match app.mode {
2881        ChatMode::Chat => {
2882            vec![
2883                ("Enter", "发送"),
2884                ("↑↓", "滚动"),
2885                ("Ctrl+T", "切换模型"),
2886                ("Ctrl+L", "清空"),
2887                ("Ctrl+Y", "复制"),
2888                ("Ctrl+B", "浏览"),
2889                ("Ctrl+S", "流式切换"),
2890                ("?/F1", "帮助"),
2891                ("Esc", "退出"),
2892            ]
2893        }
2894        ChatMode::SelectModel => {
2895            vec![("↑↓/jk", "移动"), ("Enter", "确认"), ("Esc", "取消")]
2896        }
2897        ChatMode::Browse => {
2898            vec![("↑↓", "选择消息"), ("y/Enter", "复制"), ("Esc", "返回")]
2899        }
2900        ChatMode::Help => {
2901            vec![("任意键", "返回")]
2902        }
2903    };
2904
2905    let mut spans: Vec<Span> = Vec::new();
2906    spans.push(Span::styled(" ", Style::default()));
2907    for (i, (key, desc)) in hints.iter().enumerate() {
2908        if i > 0 {
2909            spans.push(Span::styled(
2910                "  │  ",
2911                Style::default().fg(Color::Rgb(50, 50, 65)),
2912            ));
2913        }
2914        spans.push(Span::styled(
2915            format!(" {} ", key),
2916            Style::default()
2917                .fg(Color::Rgb(22, 22, 30))
2918                .bg(Color::Rgb(100, 110, 140)),
2919        ));
2920        spans.push(Span::styled(
2921            format!(" {}", desc),
2922            Style::default().fg(Color::Rgb(120, 120, 150)),
2923        ));
2924    }
2925
2926    let hint_bar =
2927        Paragraph::new(Line::from(spans)).style(Style::default().bg(Color::Rgb(22, 22, 30)));
2928    f.render_widget(hint_bar, area);
2929}
2930
2931/// 绘制 Toast 弹窗(右上角浮层)
2932fn draw_toast(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
2933    if let Some((ref msg, is_error, _)) = app.toast {
2934        let text_width = display_width(msg);
2935        // toast 宽度 = 文字宽度 + 左右 padding(各2) + emoji(2) + border(2)
2936        let toast_width = (text_width + 10).min(area.width as usize).max(16) as u16;
2937        let toast_height: u16 = 3;
2938
2939        // 定位到右上角
2940        let x = area.width.saturating_sub(toast_width + 1);
2941        let y: u16 = 1;
2942
2943        if x + toast_width <= area.width && y + toast_height <= area.height {
2944            let toast_area = Rect::new(x, y, toast_width, toast_height);
2945
2946            // 先清空区域背景
2947            let clear = Block::default().style(Style::default().bg(if is_error {
2948                Color::Rgb(60, 20, 20)
2949            } else {
2950                Color::Rgb(20, 50, 30)
2951            }));
2952            f.render_widget(clear, toast_area);
2953
2954            let (icon, border_color, text_color) = if is_error {
2955                ("❌", Color::Rgb(200, 70, 70), Color::Rgb(255, 130, 130))
2956            } else {
2957                ("✅", Color::Rgb(60, 160, 80), Color::Rgb(140, 230, 160))
2958            };
2959
2960            let toast_widget = Paragraph::new(Line::from(vec![
2961                Span::styled(format!(" {} ", icon), Style::default()),
2962                Span::styled(msg.as_str(), Style::default().fg(text_color)),
2963            ]))
2964            .block(
2965                Block::default()
2966                    .borders(Borders::ALL)
2967                    .border_type(ratatui::widgets::BorderType::Rounded)
2968                    .border_style(Style::default().fg(border_color))
2969                    .style(Style::default().bg(if is_error {
2970                        Color::Rgb(50, 18, 18)
2971                    } else {
2972                        Color::Rgb(18, 40, 25)
2973                    })),
2974            );
2975            f.render_widget(toast_widget, toast_area);
2976        }
2977    }
2978}
2979
2980/// 绘制模型选择界面
2981fn draw_model_selector(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
2982    let items: Vec<ListItem> = app
2983        .agent_config
2984        .providers
2985        .iter()
2986        .enumerate()
2987        .map(|(i, p)| {
2988            let is_active = i == app.agent_config.active_index;
2989            let marker = if is_active { " ● " } else { " ○ " };
2990            let style = if is_active {
2991                Style::default()
2992                    .fg(Color::Rgb(120, 220, 160))
2993                    .add_modifier(Modifier::BOLD)
2994            } else {
2995                Style::default().fg(Color::Rgb(180, 180, 200))
2996            };
2997            let detail = format!("{}{}  ({})", marker, p.name, p.model);
2998            ListItem::new(Line::from(Span::styled(detail, style)))
2999        })
3000        .collect();
3001
3002    let list = List::new(items)
3003        .block(
3004            Block::default()
3005                .borders(Borders::ALL)
3006                .border_type(ratatui::widgets::BorderType::Rounded)
3007                .border_style(Style::default().fg(Color::Rgb(180, 160, 80)))
3008                .title(Span::styled(
3009                    " 🔄 选择模型 ",
3010                    Style::default()
3011                        .fg(Color::Rgb(230, 210, 120))
3012                        .add_modifier(Modifier::BOLD),
3013                ))
3014                .style(Style::default().bg(Color::Rgb(28, 28, 40))),
3015        )
3016        .highlight_style(
3017            Style::default()
3018                .bg(Color::Rgb(50, 55, 80))
3019                .fg(Color::White)
3020                .add_modifier(Modifier::BOLD),
3021        )
3022        .highlight_symbol("  ▸ ");
3023
3024    f.render_stateful_widget(list, area, &mut app.model_list_state);
3025}
3026
3027/// 绘制帮助界面
3028fn draw_help(f: &mut ratatui::Frame, area: Rect) {
3029    let separator = Line::from(Span::styled(
3030        "  ─────────────────────────────────────────",
3031        Style::default().fg(Color::Rgb(50, 55, 70)),
3032    ));
3033
3034    let help_lines = vec![
3035        Line::from(""),
3036        Line::from(Span::styled(
3037            "  📖 快捷键帮助",
3038            Style::default()
3039                .fg(Color::Rgb(120, 180, 255))
3040                .add_modifier(Modifier::BOLD),
3041        )),
3042        Line::from(""),
3043        separator.clone(),
3044        Line::from(""),
3045        Line::from(vec![
3046            Span::styled(
3047                "  Enter        ",
3048                Style::default()
3049                    .fg(Color::Rgb(230, 210, 120))
3050                    .add_modifier(Modifier::BOLD),
3051            ),
3052            Span::styled("发送消息", Style::default().fg(Color::Rgb(200, 200, 220))),
3053        ]),
3054        Line::from(vec![
3055            Span::styled(
3056                "  ↑ / ↓        ",
3057                Style::default()
3058                    .fg(Color::Rgb(230, 210, 120))
3059                    .add_modifier(Modifier::BOLD),
3060            ),
3061            Span::styled(
3062                "滚动对话记录",
3063                Style::default().fg(Color::Rgb(200, 200, 220)),
3064            ),
3065        ]),
3066        Line::from(vec![
3067            Span::styled(
3068                "  ← / →        ",
3069                Style::default()
3070                    .fg(Color::Rgb(230, 210, 120))
3071                    .add_modifier(Modifier::BOLD),
3072            ),
3073            Span::styled(
3074                "移动输入光标",
3075                Style::default().fg(Color::Rgb(200, 200, 220)),
3076            ),
3077        ]),
3078        Line::from(vec![
3079            Span::styled(
3080                "  Ctrl+T       ",
3081                Style::default()
3082                    .fg(Color::Rgb(230, 210, 120))
3083                    .add_modifier(Modifier::BOLD),
3084            ),
3085            Span::styled("切换模型", Style::default().fg(Color::Rgb(200, 200, 220))),
3086        ]),
3087        Line::from(vec![
3088            Span::styled(
3089                "  Ctrl+L       ",
3090                Style::default()
3091                    .fg(Color::Rgb(230, 210, 120))
3092                    .add_modifier(Modifier::BOLD),
3093            ),
3094            Span::styled(
3095                "清空对话历史",
3096                Style::default().fg(Color::Rgb(200, 200, 220)),
3097            ),
3098        ]),
3099        Line::from(vec![
3100            Span::styled(
3101                "  Ctrl+Y       ",
3102                Style::default()
3103                    .fg(Color::Rgb(230, 210, 120))
3104                    .add_modifier(Modifier::BOLD),
3105            ),
3106            Span::styled(
3107                "复制最后一条 AI 回复",
3108                Style::default().fg(Color::Rgb(200, 200, 220)),
3109            ),
3110        ]),
3111        Line::from(vec![
3112            Span::styled(
3113                "  Ctrl+B       ",
3114                Style::default()
3115                    .fg(Color::Rgb(230, 210, 120))
3116                    .add_modifier(Modifier::BOLD),
3117            ),
3118            Span::styled(
3119                "浏览消息 (↑↓选择, y/Enter复制)",
3120                Style::default().fg(Color::Rgb(200, 200, 220)),
3121            ),
3122        ]),
3123        Line::from(vec![
3124            Span::styled(
3125                "  Ctrl+S       ",
3126                Style::default()
3127                    .fg(Color::Rgb(230, 210, 120))
3128                    .add_modifier(Modifier::BOLD),
3129            ),
3130            Span::styled(
3131                "切换流式/整体输出",
3132                Style::default().fg(Color::Rgb(200, 200, 220)),
3133            ),
3134        ]),
3135        Line::from(vec![
3136            Span::styled(
3137                "  Esc / Ctrl+C ",
3138                Style::default()
3139                    .fg(Color::Rgb(230, 210, 120))
3140                    .add_modifier(Modifier::BOLD),
3141            ),
3142            Span::styled("退出对话", Style::default().fg(Color::Rgb(200, 200, 220))),
3143        ]),
3144        Line::from(vec![
3145            Span::styled(
3146                "  ? / F1       ",
3147                Style::default()
3148                    .fg(Color::Rgb(230, 210, 120))
3149                    .add_modifier(Modifier::BOLD),
3150            ),
3151            Span::styled(
3152                "显示 / 关闭此帮助",
3153                Style::default().fg(Color::Rgb(200, 200, 220)),
3154            ),
3155        ]),
3156        Line::from(""),
3157        separator,
3158        Line::from(""),
3159        Line::from(Span::styled(
3160            "  📁 配置文件:",
3161            Style::default()
3162                .fg(Color::Rgb(120, 180, 255))
3163                .add_modifier(Modifier::BOLD),
3164        )),
3165        Line::from(Span::styled(
3166            format!("     {}", agent_config_path().display()),
3167            Style::default().fg(Color::Rgb(100, 100, 130)),
3168        )),
3169    ];
3170
3171    let help_block = Block::default()
3172        .borders(Borders::ALL)
3173        .border_type(ratatui::widgets::BorderType::Rounded)
3174        .border_style(Style::default().fg(Color::Rgb(80, 100, 140)))
3175        .title(Span::styled(
3176            " 帮助 (按任意键返回) ",
3177            Style::default().fg(Color::Rgb(140, 140, 170)),
3178        ))
3179        .style(Style::default().bg(Color::Rgb(24, 24, 34)));
3180    let help_widget = Paragraph::new(help_lines).block(help_block);
3181    f.render_widget(help_widget, area);
3182}
3183
3184/// 对话模式按键处理,返回 true 表示退出
3185fn handle_chat_mode(app: &mut ChatApp, key: KeyEvent) -> bool {
3186    // Ctrl+C 强制退出
3187    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
3188        return true;
3189    }
3190
3191    // Ctrl+T 切换模型(替代 Ctrl+M,因为 Ctrl+M 在终端中等于 Enter)
3192    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('t') {
3193        if !app.agent_config.providers.is_empty() {
3194            app.mode = ChatMode::SelectModel;
3195            app.model_list_state
3196                .select(Some(app.agent_config.active_index));
3197        }
3198        return false;
3199    }
3200
3201    // Ctrl+L 清空对话
3202    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('l') {
3203        app.clear_session();
3204        return false;
3205    }
3206
3207    // Ctrl+Y 复制最后一条 AI 回复
3208    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('y') {
3209        if let Some(last_ai) = app
3210            .session
3211            .messages
3212            .iter()
3213            .rev()
3214            .find(|m| m.role == "assistant")
3215        {
3216            if copy_to_clipboard(&last_ai.content) {
3217                app.show_toast("已复制最后一条 AI 回复", false);
3218            } else {
3219                app.show_toast("复制到剪切板失败", true);
3220            }
3221        } else {
3222            app.show_toast("暂无 AI 回复可复制", true);
3223        }
3224        return false;
3225    }
3226
3227    // Ctrl+B 进入消息浏览模式(可选中历史消息并复制)
3228    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('b') {
3229        if !app.session.messages.is_empty() {
3230            // 默认选中最后一条消息
3231            app.browse_msg_index = app.session.messages.len() - 1;
3232            app.mode = ChatMode::Browse;
3233            app.msg_lines_cache = None; // 清除缓存以触发高亮重绘
3234        } else {
3235            app.show_toast("暂无消息可浏览", true);
3236        }
3237        return false;
3238    }
3239
3240    // Ctrl+S 切换流式/非流式输出
3241    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('s') {
3242        app.agent_config.stream_mode = !app.agent_config.stream_mode;
3243        let _ = save_agent_config(&app.agent_config);
3244        let mode_str = if app.agent_config.stream_mode {
3245            "流式输出"
3246        } else {
3247            "整体输出"
3248        };
3249        app.show_toast(&format!("已切换为: {}", mode_str), false);
3250        return false;
3251    }
3252
3253    let char_count = app.input.chars().count();
3254
3255    match key.code {
3256        KeyCode::Esc => return true,
3257
3258        KeyCode::Enter => {
3259            if !app.is_loading {
3260                app.send_message();
3261            }
3262        }
3263
3264        // 滚动消息
3265        KeyCode::Up => app.scroll_up(),
3266        KeyCode::Down => app.scroll_down(),
3267        KeyCode::PageUp => {
3268            for _ in 0..10 {
3269                app.scroll_up();
3270            }
3271        }
3272        KeyCode::PageDown => {
3273            for _ in 0..10 {
3274                app.scroll_down();
3275            }
3276        }
3277
3278        // 光标移动
3279        KeyCode::Left => {
3280            if app.cursor_pos > 0 {
3281                app.cursor_pos -= 1;
3282            }
3283        }
3284        KeyCode::Right => {
3285            if app.cursor_pos < char_count {
3286                app.cursor_pos += 1;
3287            }
3288        }
3289        KeyCode::Home => app.cursor_pos = 0,
3290        KeyCode::End => app.cursor_pos = char_count,
3291
3292        // 删除
3293        KeyCode::Backspace => {
3294            if app.cursor_pos > 0 {
3295                let start = app
3296                    .input
3297                    .char_indices()
3298                    .nth(app.cursor_pos - 1)
3299                    .map(|(i, _)| i)
3300                    .unwrap_or(0);
3301                let end = app
3302                    .input
3303                    .char_indices()
3304                    .nth(app.cursor_pos)
3305                    .map(|(i, _)| i)
3306                    .unwrap_or(app.input.len());
3307                app.input.drain(start..end);
3308                app.cursor_pos -= 1;
3309            }
3310        }
3311        KeyCode::Delete => {
3312            if app.cursor_pos < char_count {
3313                let start = app
3314                    .input
3315                    .char_indices()
3316                    .nth(app.cursor_pos)
3317                    .map(|(i, _)| i)
3318                    .unwrap_or(app.input.len());
3319                let end = app
3320                    .input
3321                    .char_indices()
3322                    .nth(app.cursor_pos + 1)
3323                    .map(|(i, _)| i)
3324                    .unwrap_or(app.input.len());
3325                app.input.drain(start..end);
3326            }
3327        }
3328
3329        // F1 任何时候都能唤起帮助
3330        KeyCode::F(1) => {
3331            app.mode = ChatMode::Help;
3332        }
3333        // 输入框为空时,? 也可唤起帮助
3334        KeyCode::Char('?') if app.input.is_empty() => {
3335            app.mode = ChatMode::Help;
3336        }
3337        KeyCode::Char(c) => {
3338            let byte_idx = app
3339                .input
3340                .char_indices()
3341                .nth(app.cursor_pos)
3342                .map(|(i, _)| i)
3343                .unwrap_or(app.input.len());
3344            app.input.insert_str(byte_idx, &c.to_string());
3345            app.cursor_pos += 1;
3346        }
3347
3348        _ => {}
3349    }
3350
3351    false
3352}
3353
3354/// 消息浏览模式按键处理:↑↓ 选择消息,y/Enter 复制选中消息,Esc 退出
3355fn handle_browse_mode(app: &mut ChatApp, key: KeyEvent) {
3356    let msg_count = app.session.messages.len();
3357    if msg_count == 0 {
3358        app.mode = ChatMode::Chat;
3359        app.msg_lines_cache = None;
3360        return;
3361    }
3362
3363    match key.code {
3364        KeyCode::Esc => {
3365            app.mode = ChatMode::Chat;
3366            app.msg_lines_cache = None; // 退出浏览模式时清除缓存,去掉高亮
3367        }
3368        KeyCode::Up | KeyCode::Char('k') => {
3369            if app.browse_msg_index > 0 {
3370                app.browse_msg_index -= 1;
3371                app.msg_lines_cache = None; // 选中变化时清缓存
3372            }
3373        }
3374        KeyCode::Down | KeyCode::Char('j') => {
3375            if app.browse_msg_index < msg_count - 1 {
3376                app.browse_msg_index += 1;
3377                app.msg_lines_cache = None; // 选中变化时清缓存
3378            }
3379        }
3380        KeyCode::Enter | KeyCode::Char('y') => {
3381            // 复制选中消息的原始内容到剪切板
3382            if let Some(msg) = app.session.messages.get(app.browse_msg_index) {
3383                let content = msg.content.clone();
3384                let role_label = if msg.role == "assistant" {
3385                    "AI"
3386                } else if msg.role == "user" {
3387                    "用户"
3388                } else {
3389                    "系统"
3390                };
3391                if copy_to_clipboard(&content) {
3392                    app.show_toast(
3393                        &format!("已复制第 {} 条{}消息", app.browse_msg_index + 1, role_label),
3394                        false,
3395                    );
3396                } else {
3397                    app.show_toast("复制到剪切板失败", true);
3398                }
3399            }
3400        }
3401        _ => {}
3402    }
3403}
3404
3405/// 模型选择模式按键处理
3406fn handle_select_model(app: &mut ChatApp, key: KeyEvent) {
3407    let count = app.agent_config.providers.len();
3408    match key.code {
3409        KeyCode::Esc => {
3410            app.mode = ChatMode::Chat;
3411        }
3412        KeyCode::Up | KeyCode::Char('k') => {
3413            if count > 0 {
3414                let i = app
3415                    .model_list_state
3416                    .selected()
3417                    .map(|i| if i == 0 { count - 1 } else { i - 1 })
3418                    .unwrap_or(0);
3419                app.model_list_state.select(Some(i));
3420            }
3421        }
3422        KeyCode::Down | KeyCode::Char('j') => {
3423            if count > 0 {
3424                let i = app
3425                    .model_list_state
3426                    .selected()
3427                    .map(|i| if i >= count - 1 { 0 } else { i + 1 })
3428                    .unwrap_or(0);
3429                app.model_list_state.select(Some(i));
3430            }
3431        }
3432        KeyCode::Enter => {
3433            app.switch_model();
3434        }
3435        _ => {}
3436    }
3437}
3438
3439/// 复制内容到系统剪切板
3440fn copy_to_clipboard(content: &str) -> bool {
3441    use std::process::{Command, Stdio};
3442
3443    let (cmd, args): (&str, Vec<&str>) = if cfg!(target_os = "macos") {
3444        ("pbcopy", vec![])
3445    } else if cfg!(target_os = "linux") {
3446        if Command::new("which")
3447            .arg("xclip")
3448            .output()
3449            .map(|o| o.status.success())
3450            .unwrap_or(false)
3451        {
3452            ("xclip", vec!["-selection", "clipboard"])
3453        } else {
3454            ("xsel", vec!["--clipboard", "--input"])
3455        }
3456    } else {
3457        return false;
3458    };
3459
3460    let child = Command::new(cmd).args(&args).stdin(Stdio::piped()).spawn();
3461
3462    match child {
3463        Ok(mut child) => {
3464            if let Some(ref mut stdin) = child.stdin {
3465                let _ = stdin.write_all(content.as_bytes());
3466            }
3467            child.wait().map(|s| s.success()).unwrap_or(false)
3468        }
3469        Err(_) => false,
3470    }
3471}