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                {
1174                    let bubble_text = " ".repeat(bubble_max_width);
1175                    let pad = inner_width.saturating_sub(bubble_max_width);
1176                    lines.push(Line::from(vec![
1177                        Span::raw(" ".repeat(pad)),
1178                        Span::styled(bubble_text, Style::default().bg(user_bg)),
1179                    ]));
1180                }
1181
1182                for content_line in msg.content.lines() {
1183                    let wrapped = wrap_text(content_line, user_content_w);
1184                    for wl in wrapped {
1185                        let wl_width = display_width(&wl);
1186                        let fill = user_content_w.saturating_sub(wl_width);
1187                        let text = format!(
1188                            "{}{}{}{}",
1189                            " ".repeat(user_pad_lr),
1190                            wl,
1191                            " ".repeat(fill),
1192                            " ".repeat(user_pad_lr),
1193                        );
1194                        let text_width = display_width(&text);
1195                        let pad = inner_width.saturating_sub(text_width);
1196                        lines.push(Line::from(vec![
1197                            Span::raw(" ".repeat(pad)),
1198                            Span::styled(text, Style::default().fg(Color::White).bg(user_bg)),
1199                        ]));
1200                    }
1201                }
1202
1203                // 下边距
1204                {
1205                    let bubble_text = " ".repeat(bubble_max_width);
1206                    let pad = inner_width.saturating_sub(bubble_max_width);
1207                    lines.push(Line::from(vec![
1208                        Span::raw(" ".repeat(pad)),
1209                        Span::styled(bubble_text, Style::default().bg(user_bg)),
1210                    ]));
1211                }
1212            }
1213            "assistant" => {
1214                // AI 消息:左对齐,使用 Markdown 渲染
1215                lines.push(Line::from(""));
1216                let ai_label = if is_selected { "  ▶ AI" } else { "  AI" };
1217                lines.push(Line::from(Span::styled(
1218                    ai_label,
1219                    Style::default()
1220                        .fg(if is_selected {
1221                            Color::Rgb(255, 200, 80)
1222                        } else {
1223                            Color::Rgb(120, 220, 160)
1224                        })
1225                        .add_modifier(Modifier::BOLD),
1226                )));
1227
1228                // 使用 pulldown-cmark 解析 Markdown 内容并渲染
1229                let bubble_bg = if is_selected {
1230                    Color::Rgb(48, 48, 68)
1231                } else {
1232                    Color::Rgb(38, 38, 52)
1233                };
1234                let pad_left = "   "; // 左内边距 3 字符
1235                let pad_right = "   "; // 右内边距 3 字符
1236                let pad_left_w = 3usize;
1237                let pad_right_w = 3usize;
1238                // 内容区最大宽度要减去左右内边距
1239                let md_content_w = bubble_max_width.saturating_sub(pad_left_w + pad_right_w);
1240                let md_lines = markdown_to_lines(&msg.content, md_content_w + 2); // +2 因为 markdown_to_lines 内部还会减2
1241
1242                // 气泡总宽度 = pad_left + 内容填充到统一宽度 + pad_right
1243                let bubble_total_w = bubble_max_width;
1244
1245                // 上边距:一行空白行(带背景色)
1246                {
1247                    let mut top_spans: Vec<Span> = Vec::new();
1248                    top_spans.push(Span::styled(
1249                        " ".repeat(bubble_total_w),
1250                        Style::default().bg(bubble_bg),
1251                    ));
1252                    lines.push(Line::from(top_spans));
1253                }
1254
1255                for md_line in md_lines {
1256                    // 给每行添加左侧内边距,并应用 AI 消息背景色
1257                    let mut styled_spans: Vec<Span> = Vec::new();
1258                    styled_spans.push(Span::styled(pad_left, Style::default().bg(bubble_bg)));
1259                    // 计算内容区宽度
1260                    let mut content_w: usize = 0;
1261                    for span in md_line.spans {
1262                        let sw = display_width(&span.content);
1263                        content_w += sw;
1264                        // 保留 Markdown 渲染的前景色/修饰符,叠加背景色
1265                        let merged_style = span.style.bg(bubble_bg);
1266                        styled_spans.push(Span::styled(span.content.to_string(), merged_style));
1267                    }
1268                    // 用空格填充到统一宽度,再加右内边距
1269                    let target_content_w = bubble_total_w.saturating_sub(pad_left_w + pad_right_w);
1270                    let fill = target_content_w.saturating_sub(content_w);
1271                    if fill > 0 {
1272                        styled_spans.push(Span::styled(
1273                            " ".repeat(fill),
1274                            Style::default().bg(bubble_bg),
1275                        ));
1276                    }
1277                    styled_spans.push(Span::styled(pad_right, Style::default().bg(bubble_bg)));
1278                    lines.push(Line::from(styled_spans));
1279                }
1280
1281                // 下边距:一行空白行(带背景色)
1282                {
1283                    let mut bottom_spans: Vec<Span> = Vec::new();
1284                    bottom_spans.push(Span::styled(
1285                        " ".repeat(bubble_total_w),
1286                        Style::default().bg(bubble_bg),
1287                    ));
1288                    lines.push(Line::from(bottom_spans));
1289                }
1290            }
1291            "system" => {
1292                // 系统消息:居中,淡色
1293                lines.push(Line::from(""));
1294                let wrapped = wrap_text(&msg.content, inner_width.saturating_sub(8));
1295                for wl in wrapped {
1296                    lines.push(Line::from(Span::styled(
1297                        format!("    {}  {}", "sys", wl),
1298                        Style::default().fg(Color::Rgb(100, 100, 120)),
1299                    )));
1300                }
1301            }
1302            _ => {}
1303        }
1304    }
1305    // 末尾留白
1306    lines.push(Line::from(""));
1307
1308    (lines, msg_start_lines)
1309}
1310
1311/// 将 Markdown 文本解析为 ratatui 的 Line 列表
1312/// 支持:标题(去掉 # 标记)、加粗、斜体、行内代码、代码块(语法高亮)、列表、分隔线
1313/// content_width:内容区可用宽度(不含外层 "  " 缩进和右侧填充)
1314fn markdown_to_lines(md: &str, max_width: usize) -> Vec<Line<'static>> {
1315    use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
1316
1317    // 内容区宽度 = max_width - 2(左侧 "  " 缩进由外层负责)
1318    let content_width = max_width.saturating_sub(2);
1319
1320    let options = Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TABLES;
1321    let parser = Parser::new_ext(md, options);
1322
1323    let mut lines: Vec<Line<'static>> = Vec::new();
1324    let mut current_spans: Vec<Span<'static>> = Vec::new();
1325    let mut style_stack: Vec<Style> = vec![Style::default().fg(Color::Rgb(220, 220, 230))];
1326    let mut in_code_block = false;
1327    let mut code_block_content = String::new();
1328    let mut code_block_lang = String::new();
1329    let mut list_depth: usize = 0;
1330    let mut ordered_index: Option<u64> = None;
1331    let mut heading_level: Option<u8> = None;
1332    // 跟踪是否在引用块中
1333    let mut in_blockquote = false;
1334    // 表格相关状态
1335    let mut in_table = false;
1336    let mut table_rows: Vec<Vec<String>> = Vec::new(); // 收集所有行(含表头)
1337    let mut current_row: Vec<String> = Vec::new();
1338    let mut current_cell = String::new();
1339    let mut table_alignments: Vec<pulldown_cmark::Alignment> = Vec::new();
1340
1341    let base_style = Style::default().fg(Color::Rgb(220, 220, 230));
1342
1343    let flush_line = |current_spans: &mut Vec<Span<'static>>, lines: &mut Vec<Line<'static>>| {
1344        if !current_spans.is_empty() {
1345            lines.push(Line::from(current_spans.drain(..).collect::<Vec<_>>()));
1346        }
1347    };
1348
1349    for event in parser {
1350        match event {
1351            Event::Start(Tag::Heading { level, .. }) => {
1352                flush_line(&mut current_spans, &mut lines);
1353                heading_level = Some(level as u8);
1354                if !lines.is_empty() {
1355                    lines.push(Line::from(""));
1356                }
1357                // 根据标题级别使用不同的颜色
1358                let heading_style = match level as u8 {
1359                    1 => Style::default()
1360                        .fg(Color::Rgb(100, 180, 255))
1361                        .add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
1362                    2 => Style::default()
1363                        .fg(Color::Rgb(130, 190, 255))
1364                        .add_modifier(Modifier::BOLD),
1365                    3 => Style::default()
1366                        .fg(Color::Rgb(160, 200, 255))
1367                        .add_modifier(Modifier::BOLD),
1368                    _ => Style::default()
1369                        .fg(Color::Rgb(180, 210, 255))
1370                        .add_modifier(Modifier::BOLD),
1371                };
1372                style_stack.push(heading_style);
1373            }
1374            Event::End(TagEnd::Heading(level)) => {
1375                flush_line(&mut current_spans, &mut lines);
1376                // h1/h2 下方加分隔线(完整填充 content_width)
1377                if (level as u8) <= 2 {
1378                    let sep_char = if (level as u8) == 1 { "━" } else { "─" };
1379                    lines.push(Line::from(Span::styled(
1380                        sep_char.repeat(content_width),
1381                        Style::default().fg(Color::Rgb(60, 70, 100)),
1382                    )));
1383                }
1384                style_stack.pop();
1385                heading_level = None;
1386            }
1387            Event::Start(Tag::Strong) => {
1388                let current = *style_stack.last().unwrap_or(&base_style);
1389                style_stack.push(current.add_modifier(Modifier::BOLD));
1390            }
1391            Event::End(TagEnd::Strong) => {
1392                style_stack.pop();
1393            }
1394            Event::Start(Tag::Emphasis) => {
1395                let current = *style_stack.last().unwrap_or(&base_style);
1396                style_stack.push(current.add_modifier(Modifier::ITALIC));
1397            }
1398            Event::End(TagEnd::Emphasis) => {
1399                style_stack.pop();
1400            }
1401            Event::Start(Tag::Strikethrough) => {
1402                let current = *style_stack.last().unwrap_or(&base_style);
1403                style_stack.push(current.add_modifier(Modifier::CROSSED_OUT));
1404            }
1405            Event::End(TagEnd::Strikethrough) => {
1406                style_stack.pop();
1407            }
1408            Event::Start(Tag::CodeBlock(kind)) => {
1409                flush_line(&mut current_spans, &mut lines);
1410                in_code_block = true;
1411                code_block_content.clear();
1412                code_block_lang = match kind {
1413                    CodeBlockKind::Fenced(lang) => lang.to_string(),
1414                    CodeBlockKind::Indented => String::new(),
1415                };
1416                // 代码块上方边框(自适应宽度)
1417                let label = if code_block_lang.is_empty() {
1418                    " code ".to_string()
1419                } else {
1420                    format!(" {} ", code_block_lang)
1421                };
1422                let label_w = display_width(&label);
1423                let border_fill = content_width.saturating_sub(2 + label_w);
1424                let top_border = format!("┌─{}{}", label, "─".repeat(border_fill));
1425                lines.push(Line::from(Span::styled(
1426                    top_border,
1427                    Style::default().fg(Color::Rgb(80, 90, 110)),
1428                )));
1429            }
1430            Event::End(TagEnd::CodeBlock) => {
1431                // 渲染代码块内容(带语法高亮)
1432                let code_inner_w = content_width.saturating_sub(4); // "│ " 前缀 + 右侧 " │" 后缀占4
1433                for code_line in code_block_content.lines() {
1434                    let wrapped = wrap_text(code_line, code_inner_w);
1435                    for wl in wrapped {
1436                        let highlighted = highlight_code_line(&wl, &code_block_lang);
1437                        let text_w: usize =
1438                            highlighted.iter().map(|s| display_width(&s.content)).sum();
1439                        let fill = code_inner_w.saturating_sub(text_w);
1440                        let mut spans_vec = Vec::new();
1441                        spans_vec.push(Span::styled(
1442                            "│ ",
1443                            Style::default().fg(Color::Rgb(80, 90, 110)),
1444                        ));
1445                        for hs in highlighted {
1446                            spans_vec.push(Span::styled(
1447                                hs.content.to_string(),
1448                                hs.style.bg(Color::Rgb(30, 30, 42)),
1449                            ));
1450                        }
1451                        spans_vec.push(Span::styled(
1452                            format!("{} │", " ".repeat(fill)),
1453                            Style::default()
1454                                .fg(Color::Rgb(80, 90, 110))
1455                                .bg(Color::Rgb(30, 30, 42)),
1456                        ));
1457                        lines.push(Line::from(spans_vec));
1458                    }
1459                }
1460                let bottom_border = format!("└{}", "─".repeat(content_width.saturating_sub(1)));
1461                lines.push(Line::from(Span::styled(
1462                    bottom_border,
1463                    Style::default().fg(Color::Rgb(80, 90, 110)),
1464                )));
1465                in_code_block = false;
1466                code_block_content.clear();
1467                code_block_lang.clear();
1468            }
1469            Event::Code(text) => {
1470                if in_table {
1471                    // 表格中的行内代码也收集到当前单元格
1472                    current_cell.push('`');
1473                    current_cell.push_str(&text);
1474                    current_cell.push('`');
1475                } else {
1476                    // 行内代码
1477                    current_spans.push(Span::styled(
1478                        format!(" {} ", text),
1479                        Style::default()
1480                            .fg(Color::Rgb(230, 190, 120))
1481                            .bg(Color::Rgb(45, 45, 60)),
1482                    ));
1483                }
1484            }
1485            Event::Start(Tag::List(start)) => {
1486                flush_line(&mut current_spans, &mut lines);
1487                list_depth += 1;
1488                ordered_index = start;
1489            }
1490            Event::End(TagEnd::List(_)) => {
1491                flush_line(&mut current_spans, &mut lines);
1492                list_depth = list_depth.saturating_sub(1);
1493                ordered_index = None;
1494            }
1495            Event::Start(Tag::Item) => {
1496                flush_line(&mut current_spans, &mut lines);
1497                let indent = "  ".repeat(list_depth);
1498                let bullet = if let Some(ref mut idx) = ordered_index {
1499                    let s = format!("{}{}. ", indent, idx);
1500                    *idx += 1;
1501                    s
1502                } else {
1503                    format!("{}- ", indent)
1504                };
1505                current_spans.push(Span::styled(
1506                    bullet,
1507                    Style::default().fg(Color::Rgb(160, 180, 220)),
1508                ));
1509            }
1510            Event::End(TagEnd::Item) => {
1511                flush_line(&mut current_spans, &mut lines);
1512            }
1513            Event::Start(Tag::Paragraph) => {
1514                if !lines.is_empty() && !in_code_block && heading_level.is_none() {
1515                    let last_empty = lines.last().map(|l| l.spans.is_empty()).unwrap_or(false);
1516                    if !last_empty {
1517                        lines.push(Line::from(""));
1518                    }
1519                }
1520            }
1521            Event::End(TagEnd::Paragraph) => {
1522                flush_line(&mut current_spans, &mut lines);
1523            }
1524            Event::Start(Tag::BlockQuote(_)) => {
1525                flush_line(&mut current_spans, &mut lines);
1526                in_blockquote = true;
1527                style_stack.push(Style::default().fg(Color::Rgb(150, 160, 180)));
1528            }
1529            Event::End(TagEnd::BlockQuote(_)) => {
1530                flush_line(&mut current_spans, &mut lines);
1531                in_blockquote = false;
1532                style_stack.pop();
1533            }
1534            Event::Text(text) => {
1535                if in_code_block {
1536                    code_block_content.push_str(&text);
1537                } else if in_table {
1538                    // 表格中的文本收集到当前单元格
1539                    current_cell.push_str(&text);
1540                } else {
1541                    let style = *style_stack.last().unwrap_or(&base_style);
1542                    let text_str = text.to_string();
1543
1544                    // 标题:添加可视化符号前缀代替 # 标记
1545                    if let Some(level) = heading_level {
1546                        let (prefix, prefix_style) = match level {
1547                            1 => (
1548                                ">> ",
1549                                Style::default()
1550                                    .fg(Color::Rgb(100, 180, 255))
1551                                    .add_modifier(Modifier::BOLD),
1552                            ),
1553                            2 => (
1554                                ">> ",
1555                                Style::default()
1556                                    .fg(Color::Rgb(130, 190, 255))
1557                                    .add_modifier(Modifier::BOLD),
1558                            ),
1559                            3 => (
1560                                "> ",
1561                                Style::default()
1562                                    .fg(Color::Rgb(160, 200, 255))
1563                                    .add_modifier(Modifier::BOLD),
1564                            ),
1565                            _ => (
1566                                "> ",
1567                                Style::default()
1568                                    .fg(Color::Rgb(180, 210, 255))
1569                                    .add_modifier(Modifier::BOLD),
1570                            ),
1571                        };
1572                        current_spans.push(Span::styled(prefix.to_string(), prefix_style));
1573                        heading_level = None; // 只加一次前缀
1574                    }
1575
1576                    // 计算 current_spans 已有的显示宽度
1577                    let existing_w: usize = current_spans
1578                        .iter()
1579                        .map(|s| display_width(&s.content))
1580                        .sum();
1581
1582                    // 引用块:加左侧竖线
1583                    let effective_prefix_w = if in_blockquote { 2 } else { 0 }; // "| " 宽度
1584                    let wrap_w = content_width.saturating_sub(effective_prefix_w + existing_w);
1585
1586                    for (i, line) in text_str.split('\n').enumerate() {
1587                        if i > 0 {
1588                            flush_line(&mut current_spans, &mut lines);
1589                            if in_blockquote {
1590                                current_spans.push(Span::styled(
1591                                    "| ".to_string(),
1592                                    Style::default().fg(Color::Rgb(80, 100, 140)),
1593                                ));
1594                            }
1595                        }
1596                        if !line.is_empty() {
1597                            // 第一行使用减去已有 span 宽度的 wrap_w,后续行使用完整 content_width
1598                            let effective_wrap = if i == 0 {
1599                                wrap_w
1600                            } else {
1601                                content_width.saturating_sub(effective_prefix_w)
1602                            };
1603                            let wrapped = wrap_text(line, effective_wrap);
1604                            for (j, wl) in wrapped.iter().enumerate() {
1605                                if j > 0 {
1606                                    flush_line(&mut current_spans, &mut lines);
1607                                    if in_blockquote {
1608                                        current_spans.push(Span::styled(
1609                                            "| ".to_string(),
1610                                            Style::default().fg(Color::Rgb(80, 100, 140)),
1611                                        ));
1612                                    }
1613                                }
1614                                current_spans.push(Span::styled(wl.clone(), style));
1615                            }
1616                        }
1617                    }
1618                }
1619            }
1620            Event::SoftBreak => {
1621                if in_table {
1622                    current_cell.push(' ');
1623                } else {
1624                    current_spans.push(Span::raw(" "));
1625                }
1626            }
1627            Event::HardBreak => {
1628                if in_table {
1629                    current_cell.push(' ');
1630                } else {
1631                    flush_line(&mut current_spans, &mut lines);
1632                }
1633            }
1634            Event::Rule => {
1635                flush_line(&mut current_spans, &mut lines);
1636                lines.push(Line::from(Span::styled(
1637                    "─".repeat(content_width),
1638                    Style::default().fg(Color::Rgb(70, 75, 90)),
1639                )));
1640            }
1641            // ===== 表格支持 =====
1642            Event::Start(Tag::Table(alignments)) => {
1643                flush_line(&mut current_spans, &mut lines);
1644                in_table = true;
1645                table_rows.clear();
1646                table_alignments = alignments;
1647            }
1648            Event::End(TagEnd::Table) => {
1649                // 表格结束:计算列宽,渲染完整表格
1650                flush_line(&mut current_spans, &mut lines);
1651                in_table = false;
1652
1653                if !table_rows.is_empty() {
1654                    let num_cols = table_rows.iter().map(|r| r.len()).max().unwrap_or(0);
1655                    if num_cols > 0 {
1656                        // 计算每列最大宽度
1657                        let mut col_widths: Vec<usize> = vec![0; num_cols];
1658                        for row in &table_rows {
1659                            for (i, cell) in row.iter().enumerate() {
1660                                let w = display_width(cell);
1661                                if w > col_widths[i] {
1662                                    col_widths[i] = w;
1663                                }
1664                            }
1665                        }
1666
1667                        // 限制总宽度不超过 content_width,等比缩放
1668                        let sep_w = num_cols + 1; // 竖线占用
1669                        let pad_w = num_cols * 2; // 每列左右各1空格
1670                        let avail = content_width.saturating_sub(sep_w + pad_w);
1671                        // 单列最大宽度限制(避免一列过宽)
1672                        let max_col_w = avail * 2 / 3;
1673                        for cw in col_widths.iter_mut() {
1674                            if *cw > max_col_w {
1675                                *cw = max_col_w;
1676                            }
1677                        }
1678                        let total_col_w: usize = col_widths.iter().sum();
1679                        if total_col_w > avail && total_col_w > 0 {
1680                            // 等比缩放
1681                            let mut remaining = avail;
1682                            for (i, cw) in col_widths.iter_mut().enumerate() {
1683                                if i == num_cols - 1 {
1684                                    // 最后一列取剩余宽度,避免取整误差
1685                                    *cw = remaining.max(1);
1686                                } else {
1687                                    *cw = ((*cw) * avail / total_col_w).max(1);
1688                                    remaining = remaining.saturating_sub(*cw);
1689                                }
1690                            }
1691                        }
1692
1693                        let table_style = Style::default().fg(Color::Rgb(180, 180, 200));
1694                        let header_style = Style::default()
1695                            .fg(Color::Rgb(120, 180, 255))
1696                            .add_modifier(Modifier::BOLD);
1697                        let border_style = Style::default().fg(Color::Rgb(60, 70, 100));
1698
1699                        // 渲染顶边框 ┌─┬─┐
1700                        let mut top = String::from("┌");
1701                        for (i, cw) in col_widths.iter().enumerate() {
1702                            top.push_str(&"─".repeat(cw + 2));
1703                            if i < num_cols - 1 {
1704                                top.push('┬');
1705                            }
1706                        }
1707                        top.push('┐');
1708                        lines.push(Line::from(Span::styled(top, border_style)));
1709
1710                        for (row_idx, row) in table_rows.iter().enumerate() {
1711                            // 数据行 │ cell │ cell │
1712                            let mut row_spans: Vec<Span> = Vec::new();
1713                            row_spans.push(Span::styled("│", border_style));
1714                            for (i, cw) in col_widths.iter().enumerate() {
1715                                let cell_text = row.get(i).map(|s| s.as_str()).unwrap_or("");
1716                                let cell_w = display_width(cell_text);
1717                                let text = if cell_w > *cw {
1718                                    // 截断
1719                                    let mut t = String::new();
1720                                    let mut w = 0;
1721                                    for ch in cell_text.chars() {
1722                                        let chw = char_width(ch);
1723                                        if w + chw > *cw {
1724                                            break;
1725                                        }
1726                                        t.push(ch);
1727                                        w += chw;
1728                                    }
1729                                    let fill = cw.saturating_sub(w);
1730                                    format!(" {}{} ", t, " ".repeat(fill))
1731                                } else {
1732                                    // 根据对齐方式填充
1733                                    let fill = cw.saturating_sub(cell_w);
1734                                    let align = table_alignments
1735                                        .get(i)
1736                                        .copied()
1737                                        .unwrap_or(pulldown_cmark::Alignment::None);
1738                                    match align {
1739                                        pulldown_cmark::Alignment::Center => {
1740                                            let left = fill / 2;
1741                                            let right = fill - left;
1742                                            format!(
1743                                                " {}{}{} ",
1744                                                " ".repeat(left),
1745                                                cell_text,
1746                                                " ".repeat(right)
1747                                            )
1748                                        }
1749                                        pulldown_cmark::Alignment::Right => {
1750                                            format!(" {}{} ", " ".repeat(fill), cell_text)
1751                                        }
1752                                        _ => {
1753                                            format!(" {}{} ", cell_text, " ".repeat(fill))
1754                                        }
1755                                    }
1756                                };
1757                                let style = if row_idx == 0 {
1758                                    header_style
1759                                } else {
1760                                    table_style
1761                                };
1762                                row_spans.push(Span::styled(text, style));
1763                                row_spans.push(Span::styled("│", border_style));
1764                            }
1765                            lines.push(Line::from(row_spans));
1766
1767                            // 表头行后加分隔线 ├─┼─┤
1768                            if row_idx == 0 {
1769                                let mut sep = String::from("├");
1770                                for (i, cw) in col_widths.iter().enumerate() {
1771                                    sep.push_str(&"─".repeat(cw + 2));
1772                                    if i < num_cols - 1 {
1773                                        sep.push('┼');
1774                                    }
1775                                }
1776                                sep.push('┤');
1777                                lines.push(Line::from(Span::styled(sep, border_style)));
1778                            }
1779                        }
1780
1781                        // 底边框 └─┴─┘
1782                        let mut bottom = String::from("└");
1783                        for (i, cw) in col_widths.iter().enumerate() {
1784                            bottom.push_str(&"─".repeat(cw + 2));
1785                            if i < num_cols - 1 {
1786                                bottom.push('┴');
1787                            }
1788                        }
1789                        bottom.push('┘');
1790                        lines.push(Line::from(Span::styled(bottom, border_style)));
1791                    }
1792                }
1793                table_rows.clear();
1794                table_alignments.clear();
1795            }
1796            Event::Start(Tag::TableHead) => {
1797                current_row.clear();
1798            }
1799            Event::End(TagEnd::TableHead) => {
1800                table_rows.push(current_row.clone());
1801                current_row.clear();
1802            }
1803            Event::Start(Tag::TableRow) => {
1804                current_row.clear();
1805            }
1806            Event::End(TagEnd::TableRow) => {
1807                table_rows.push(current_row.clone());
1808                current_row.clear();
1809            }
1810            Event::Start(Tag::TableCell) => {
1811                current_cell.clear();
1812            }
1813            Event::End(TagEnd::TableCell) => {
1814                current_row.push(current_cell.clone());
1815                current_cell.clear();
1816            }
1817            _ => {}
1818        }
1819    }
1820
1821    // 刷新最后一行
1822    if !current_spans.is_empty() {
1823        lines.push(Line::from(current_spans));
1824    }
1825
1826    // 如果解析结果为空,至少返回原始文本
1827    if lines.is_empty() {
1828        let wrapped = wrap_text(md, content_width);
1829        for wl in wrapped {
1830            lines.push(Line::from(Span::styled(wl, base_style)));
1831        }
1832    }
1833
1834    lines
1835}
1836
1837/// 简单的代码语法高亮(无需外部依赖)
1838/// 根据语言类型对常见关键字、字符串、注释、数字进行着色
1839fn highlight_code_line<'a>(line: &'a str, lang: &str) -> Vec<Span<'static>> {
1840    let lang_lower = lang.to_lowercase();
1841    let keywords: &[&str] = match lang_lower.as_str() {
1842        "rust" | "rs" => &[
1843            "fn", "let", "mut", "pub", "use", "mod", "struct", "enum", "impl", "trait", "for",
1844            "while", "loop", "if", "else", "match", "return", "self", "Self", "where", "async",
1845            "await", "move", "ref", "type", "const", "static", "crate", "super", "as", "in",
1846            "true", "false", "Some", "None", "Ok", "Err",
1847        ],
1848        "python" | "py" => &[
1849            "def", "class", "return", "if", "elif", "else", "for", "while", "import", "from", "as",
1850            "with", "try", "except", "finally", "raise", "pass", "break", "continue", "yield",
1851            "lambda", "and", "or", "not", "in", "is", "True", "False", "None", "global",
1852            "nonlocal", "assert", "del", "async", "await", "self", "print",
1853        ],
1854        "javascript" | "js" | "typescript" | "ts" | "jsx" | "tsx" => &[
1855            "function",
1856            "const",
1857            "let",
1858            "var",
1859            "return",
1860            "if",
1861            "else",
1862            "for",
1863            "while",
1864            "class",
1865            "new",
1866            "this",
1867            "import",
1868            "export",
1869            "from",
1870            "default",
1871            "async",
1872            "await",
1873            "try",
1874            "catch",
1875            "finally",
1876            "throw",
1877            "typeof",
1878            "instanceof",
1879            "true",
1880            "false",
1881            "null",
1882            "undefined",
1883            "of",
1884            "in",
1885            "switch",
1886            "case",
1887        ],
1888        "go" | "golang" => &[
1889            "func",
1890            "package",
1891            "import",
1892            "return",
1893            "if",
1894            "else",
1895            "for",
1896            "range",
1897            "struct",
1898            "interface",
1899            "type",
1900            "var",
1901            "const",
1902            "defer",
1903            "go",
1904            "chan",
1905            "select",
1906            "case",
1907            "switch",
1908            "default",
1909            "break",
1910            "continue",
1911            "map",
1912            "true",
1913            "false",
1914            "nil",
1915            "make",
1916            "append",
1917            "len",
1918            "cap",
1919        ],
1920        "java" | "kotlin" | "kt" => &[
1921            "public",
1922            "private",
1923            "protected",
1924            "class",
1925            "interface",
1926            "extends",
1927            "implements",
1928            "return",
1929            "if",
1930            "else",
1931            "for",
1932            "while",
1933            "new",
1934            "this",
1935            "import",
1936            "package",
1937            "static",
1938            "final",
1939            "void",
1940            "int",
1941            "String",
1942            "boolean",
1943            "true",
1944            "false",
1945            "null",
1946            "try",
1947            "catch",
1948            "throw",
1949            "throws",
1950            "fun",
1951            "val",
1952            "var",
1953            "when",
1954            "object",
1955            "companion",
1956        ],
1957        "sh" | "bash" | "zsh" | "shell" => &[
1958            "if",
1959            "then",
1960            "else",
1961            "elif",
1962            "fi",
1963            "for",
1964            "while",
1965            "do",
1966            "done",
1967            "case",
1968            "esac",
1969            "function",
1970            "return",
1971            "exit",
1972            "echo",
1973            "export",
1974            "local",
1975            "readonly",
1976            "set",
1977            "unset",
1978            "shift",
1979            "source",
1980            "in",
1981            "true",
1982            "false",
1983            "read",
1984            "declare",
1985            "typeset",
1986            "trap",
1987            "eval",
1988            "exec",
1989            "test",
1990            "select",
1991            "until",
1992            "break",
1993            "continue",
1994            "printf",
1995            // Go 命令
1996            "go",
1997            "build",
1998            "run",
1999            "test",
2000            "fmt",
2001            "vet",
2002            "mod",
2003            "get",
2004            "install",
2005            "clean",
2006            "doc",
2007            "list",
2008            "version",
2009            "env",
2010            "generate",
2011            "tool",
2012            "proxy",
2013            "GOPATH",
2014            "GOROOT",
2015            "GOBIN",
2016            "GOMODCACHE",
2017            "GOPROXY",
2018            "GOSUMDB",
2019            // Cargo 命令
2020            "cargo",
2021            "new",
2022            "init",
2023            "add",
2024            "remove",
2025            "update",
2026            "check",
2027            "clippy",
2028            "rustfmt",
2029            "rustc",
2030            "rustup",
2031            "publish",
2032            "install",
2033            "uninstall",
2034            "search",
2035            "tree",
2036            "locate_project",
2037            "metadata",
2038            "audit",
2039            "watch",
2040            "expand",
2041        ],
2042        "c" | "cpp" | "c++" | "h" | "hpp" => &[
2043            "int",
2044            "char",
2045            "float",
2046            "double",
2047            "void",
2048            "long",
2049            "short",
2050            "unsigned",
2051            "signed",
2052            "const",
2053            "static",
2054            "extern",
2055            "struct",
2056            "union",
2057            "enum",
2058            "typedef",
2059            "sizeof",
2060            "return",
2061            "if",
2062            "else",
2063            "for",
2064            "while",
2065            "do",
2066            "switch",
2067            "case",
2068            "break",
2069            "continue",
2070            "default",
2071            "goto",
2072            "auto",
2073            "register",
2074            "volatile",
2075            "class",
2076            "public",
2077            "private",
2078            "protected",
2079            "virtual",
2080            "override",
2081            "template",
2082            "namespace",
2083            "using",
2084            "new",
2085            "delete",
2086            "try",
2087            "catch",
2088            "throw",
2089            "nullptr",
2090            "true",
2091            "false",
2092            "this",
2093            "include",
2094            "define",
2095            "ifdef",
2096            "ifndef",
2097            "endif",
2098        ],
2099        "sql" => &[
2100            "SELECT",
2101            "FROM",
2102            "WHERE",
2103            "INSERT",
2104            "UPDATE",
2105            "DELETE",
2106            "CREATE",
2107            "DROP",
2108            "ALTER",
2109            "TABLE",
2110            "INDEX",
2111            "INTO",
2112            "VALUES",
2113            "SET",
2114            "AND",
2115            "OR",
2116            "NOT",
2117            "NULL",
2118            "JOIN",
2119            "LEFT",
2120            "RIGHT",
2121            "INNER",
2122            "OUTER",
2123            "ON",
2124            "GROUP",
2125            "BY",
2126            "ORDER",
2127            "ASC",
2128            "DESC",
2129            "HAVING",
2130            "LIMIT",
2131            "OFFSET",
2132            "UNION",
2133            "AS",
2134            "DISTINCT",
2135            "COUNT",
2136            "SUM",
2137            "AVG",
2138            "MIN",
2139            "MAX",
2140            "LIKE",
2141            "IN",
2142            "BETWEEN",
2143            "EXISTS",
2144            "CASE",
2145            "WHEN",
2146            "THEN",
2147            "ELSE",
2148            "END",
2149            "BEGIN",
2150            "COMMIT",
2151            "ROLLBACK",
2152            "PRIMARY",
2153            "KEY",
2154            "FOREIGN",
2155            "REFERENCES",
2156            "select",
2157            "from",
2158            "where",
2159            "insert",
2160            "update",
2161            "delete",
2162            "create",
2163            "drop",
2164            "alter",
2165            "table",
2166            "index",
2167            "into",
2168            "values",
2169            "set",
2170            "and",
2171            "or",
2172            "not",
2173            "null",
2174            "join",
2175            "left",
2176            "right",
2177            "inner",
2178            "outer",
2179            "on",
2180            "group",
2181            "by",
2182            "order",
2183            "asc",
2184            "desc",
2185            "having",
2186            "limit",
2187            "offset",
2188            "union",
2189            "as",
2190            "distinct",
2191            "count",
2192            "sum",
2193            "avg",
2194            "min",
2195            "max",
2196            "like",
2197            "in",
2198            "between",
2199            "exists",
2200            "case",
2201            "when",
2202            "then",
2203            "else",
2204            "end",
2205            "begin",
2206            "commit",
2207            "rollback",
2208            "primary",
2209            "key",
2210            "foreign",
2211            "references",
2212        ],
2213        "yaml" | "yml" => &["true", "false", "null", "yes", "no", "on", "off"],
2214        "toml" => &[
2215            "true",
2216            "false",
2217            "true",
2218            "false",
2219            // Cargo.toml 常用
2220            "name",
2221            "version",
2222            "edition",
2223            "authors",
2224            "dependencies",
2225            "dev-dependencies",
2226            "build-dependencies",
2227            "features",
2228            "workspace",
2229            "members",
2230            "exclude",
2231            "include",
2232            "path",
2233            "git",
2234            "branch",
2235            "tag",
2236            "rev",
2237            "package",
2238            "lib",
2239            "bin",
2240            "example",
2241            "test",
2242            "bench",
2243            "doc",
2244            "profile",
2245            "release",
2246            "debug",
2247            "opt-level",
2248            "lto",
2249            "codegen-units",
2250            "panic",
2251            "strip",
2252            "default",
2253            "features",
2254            "optional",
2255            // 常见配置项
2256            "repository",
2257            "homepage",
2258            "documentation",
2259            "license",
2260            "license-file",
2261            "keywords",
2262            "categories",
2263            "readme",
2264            "description",
2265            "resolver",
2266        ],
2267        "css" | "scss" | "less" => &[
2268            "color",
2269            "background",
2270            "border",
2271            "margin",
2272            "padding",
2273            "display",
2274            "position",
2275            "width",
2276            "height",
2277            "font",
2278            "text",
2279            "flex",
2280            "grid",
2281            "align",
2282            "justify",
2283            "important",
2284            "none",
2285            "auto",
2286            "inherit",
2287            "initial",
2288            "unset",
2289        ],
2290        "dockerfile" | "docker" => &[
2291            "FROM",
2292            "RUN",
2293            "CMD",
2294            "LABEL",
2295            "EXPOSE",
2296            "ENV",
2297            "ADD",
2298            "COPY",
2299            "ENTRYPOINT",
2300            "VOLUME",
2301            "USER",
2302            "WORKDIR",
2303            "ARG",
2304            "ONBUILD",
2305            "STOPSIGNAL",
2306            "HEALTHCHECK",
2307            "SHELL",
2308            "AS",
2309        ],
2310        "ruby" | "rb" => &[
2311            "def", "end", "class", "module", "if", "elsif", "else", "unless", "while", "until",
2312            "for", "do", "begin", "rescue", "ensure", "raise", "return", "yield", "require",
2313            "include", "attr", "self", "true", "false", "nil", "puts", "print",
2314        ],
2315        _ => &[
2316            "fn", "function", "def", "class", "return", "if", "else", "for", "while", "import",
2317            "export", "const", "let", "var", "true", "false", "null", "nil", "None", "self",
2318            "this",
2319        ],
2320    };
2321
2322    let comment_prefix = match lang_lower.as_str() {
2323        "python" | "py" | "sh" | "bash" | "zsh" | "shell" | "ruby" | "rb" | "yaml" | "yml"
2324        | "toml" | "dockerfile" | "docker" => "#",
2325        "sql" => "--",
2326        "css" | "scss" | "less" => "/*",
2327        _ => "//",
2328    };
2329
2330    // 默认代码颜色
2331    let code_style = Style::default().fg(Color::Rgb(200, 200, 210));
2332    // 关键字颜色
2333    let kw_style = Style::default().fg(Color::Rgb(198, 120, 221));
2334    // 字符串颜色
2335    let str_style = Style::default().fg(Color::Rgb(152, 195, 121));
2336    // 注释颜色
2337    let comment_style = Style::default()
2338        .fg(Color::Rgb(92, 99, 112))
2339        .add_modifier(Modifier::ITALIC);
2340    // 数字颜色
2341    let num_style = Style::default().fg(Color::Rgb(209, 154, 102));
2342    // 类型/大写开头标识符
2343    let type_style = Style::default().fg(Color::Rgb(229, 192, 123));
2344
2345    let trimmed = line.trim_start();
2346
2347    // 注释行
2348    if trimmed.starts_with(comment_prefix) {
2349        return vec![Span::styled(line.to_string(), comment_style)];
2350    }
2351
2352    // 逐词解析
2353    let mut spans = Vec::new();
2354    let mut chars = line.chars().peekable();
2355    let mut buf = String::new();
2356
2357    while let Some(&ch) = chars.peek() {
2358        // 字符串
2359        if ch == '"' || ch == '\'' || ch == '`' {
2360            // 先刷新 buf
2361            if !buf.is_empty() {
2362                spans.extend(colorize_tokens(
2363                    &buf, keywords, code_style, kw_style, num_style, type_style,
2364                ));
2365                buf.clear();
2366            }
2367            let quote = ch;
2368            let mut s = String::new();
2369            s.push(ch);
2370            chars.next();
2371            while let Some(&c) = chars.peek() {
2372                s.push(c);
2373                chars.next();
2374                if c == quote && !s.ends_with("\\\\") {
2375                    break;
2376                }
2377            }
2378            spans.push(Span::styled(s, str_style));
2379            continue;
2380        }
2381        // Shell 变量 ($VAR, ${VAR}, $1 等)
2382        if ch == '$'
2383            && matches!(
2384                lang_lower.as_str(),
2385                "sh" | "bash" | "zsh" | "shell" | "dockerfile" | "docker"
2386            )
2387        {
2388            if !buf.is_empty() {
2389                spans.extend(colorize_tokens(
2390                    &buf, keywords, code_style, kw_style, num_style, type_style,
2391                ));
2392                buf.clear();
2393            }
2394            let var_style = Style::default().fg(Color::Rgb(86, 182, 194));
2395            let mut var = String::new();
2396            var.push(ch);
2397            chars.next();
2398            if let Some(&next_ch) = chars.peek() {
2399                if next_ch == '{' {
2400                    // ${VAR}
2401                    var.push(next_ch);
2402                    chars.next();
2403                    while let Some(&c) = chars.peek() {
2404                        var.push(c);
2405                        chars.next();
2406                        if c == '}' {
2407                            break;
2408                        }
2409                    }
2410                } else if next_ch == '(' {
2411                    // $(cmd)
2412                    var.push(next_ch);
2413                    chars.next();
2414                    let mut depth = 1;
2415                    while let Some(&c) = chars.peek() {
2416                        var.push(c);
2417                        chars.next();
2418                        if c == '(' {
2419                            depth += 1;
2420                        }
2421                        if c == ')' {
2422                            depth -= 1;
2423                            if depth == 0 {
2424                                break;
2425                            }
2426                        }
2427                    }
2428                } else if next_ch.is_alphanumeric()
2429                    || next_ch == '_'
2430                    || next_ch == '@'
2431                    || next_ch == '#'
2432                    || next_ch == '?'
2433                    || next_ch == '!'
2434                {
2435                    // $VAR, $1, $@, $#, $? 等
2436                    while let Some(&c) = chars.peek() {
2437                        if c.is_alphanumeric() || c == '_' {
2438                            var.push(c);
2439                            chars.next();
2440                        } else {
2441                            break;
2442                        }
2443                    }
2444                }
2445            }
2446            spans.push(Span::styled(var, var_style));
2447            continue;
2448        }
2449        // 行内注释
2450        if ch == '/' || ch == '#' {
2451            let rest: String = chars.clone().collect();
2452            if rest.starts_with(comment_prefix) {
2453                if !buf.is_empty() {
2454                    spans.extend(colorize_tokens(
2455                        &buf, keywords, code_style, kw_style, num_style, type_style,
2456                    ));
2457                    buf.clear();
2458                }
2459                spans.push(Span::styled(rest, comment_style));
2460                break;
2461            }
2462        }
2463        buf.push(ch);
2464        chars.next();
2465    }
2466
2467    if !buf.is_empty() {
2468        spans.extend(colorize_tokens(
2469            &buf, keywords, code_style, kw_style, num_style, type_style,
2470        ));
2471    }
2472
2473    if spans.is_empty() {
2474        spans.push(Span::styled(line.to_string(), code_style));
2475    }
2476
2477    spans
2478}
2479
2480/// 将文本按照 word boundary 拆分并对关键字、数字、类型名着色
2481fn colorize_tokens<'a>(
2482    text: &str,
2483    keywords: &[&str],
2484    default_style: Style,
2485    kw_style: Style,
2486    num_style: Style,
2487    type_style: Style,
2488) -> Vec<Span<'static>> {
2489    let mut spans = Vec::new();
2490    let mut current_word = String::new();
2491    let mut current_non_word = String::new();
2492
2493    for ch in text.chars() {
2494        if ch.is_alphanumeric() || ch == '_' {
2495            if !current_non_word.is_empty() {
2496                spans.push(Span::styled(current_non_word.clone(), default_style));
2497                current_non_word.clear();
2498            }
2499            current_word.push(ch);
2500        } else {
2501            if !current_word.is_empty() {
2502                let style = if keywords.contains(&current_word.as_str()) {
2503                    kw_style
2504                } else if current_word
2505                    .chars()
2506                    .next()
2507                    .map(|c| c.is_ascii_digit())
2508                    .unwrap_or(false)
2509                {
2510                    num_style
2511                } else if current_word
2512                    .chars()
2513                    .next()
2514                    .map(|c| c.is_uppercase())
2515                    .unwrap_or(false)
2516                {
2517                    type_style
2518                } else {
2519                    default_style
2520                };
2521                spans.push(Span::styled(current_word.clone(), style));
2522                current_word.clear();
2523            }
2524            current_non_word.push(ch);
2525        }
2526    }
2527
2528    // 刷新剩余
2529    if !current_non_word.is_empty() {
2530        spans.push(Span::styled(current_non_word, default_style));
2531    }
2532    if !current_word.is_empty() {
2533        let style = if keywords.contains(&current_word.as_str()) {
2534            kw_style
2535        } else if current_word
2536            .chars()
2537            .next()
2538            .map(|c| c.is_ascii_digit())
2539            .unwrap_or(false)
2540        {
2541            num_style
2542        } else if current_word
2543            .chars()
2544            .next()
2545            .map(|c| c.is_uppercase())
2546            .unwrap_or(false)
2547        {
2548            type_style
2549        } else {
2550            default_style
2551        };
2552        spans.push(Span::styled(current_word, style));
2553    }
2554
2555    spans
2556}
2557
2558/// 简单文本自动换行
2559fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
2560    if max_width == 0 {
2561        return vec![text.to_string()];
2562    }
2563    let mut result = Vec::new();
2564    let mut current_line = String::new();
2565    let mut current_width = 0;
2566
2567    for ch in text.chars() {
2568        let ch_width = char_width(ch);
2569        if current_width + ch_width > max_width && !current_line.is_empty() {
2570            result.push(current_line.clone());
2571            current_line.clear();
2572            current_width = 0;
2573        }
2574        current_line.push(ch);
2575        current_width += ch_width;
2576    }
2577    if !current_line.is_empty() {
2578        result.push(current_line);
2579    }
2580    if result.is_empty() {
2581        result.push(String::new());
2582    }
2583    result
2584}
2585
2586/// 计算字符串的显示宽度
2587/// 使用 unicode-width 规则:CJK 字符宽度2,其他(含 Box Drawing、符号等)宽度1
2588fn display_width(s: &str) -> usize {
2589    s.chars().map(|c| char_width(c)).sum()
2590}
2591
2592/// 计算单个字符的显示宽度
2593fn char_width(c: char) -> usize {
2594    if c.is_ascii() {
2595        return 1;
2596    }
2597    // CJK Unified Ideographs 及扩展
2598    let cp = c as u32;
2599    if (0x4E00..=0x9FFF).contains(&cp)    // CJK Unified Ideographs
2600        || (0x3400..=0x4DBF).contains(&cp) // CJK Unified Ideographs Extension A
2601        || (0x20000..=0x2A6DF).contains(&cp) // Extension B
2602        || (0x2A700..=0x2B73F).contains(&cp) // Extension C
2603        || (0x2B740..=0x2B81F).contains(&cp) // Extension D
2604        || (0xF900..=0xFAFF).contains(&cp)   // CJK Compatibility Ideographs
2605        || (0x2F800..=0x2FA1F).contains(&cp)  // CJK Compatibility Ideographs Supplement
2606        // CJK 标点和符号
2607        || (0x3000..=0x303F).contains(&cp)    // CJK Symbols and Punctuation
2608        || (0xFF01..=0xFF60).contains(&cp)    // Fullwidth Forms
2609        || (0xFFE0..=0xFFE6).contains(&cp)    // Fullwidth Signs
2610        // 日韩
2611        || (0x3040..=0x309F).contains(&cp)    // Hiragana
2612        || (0x30A0..=0x30FF).contains(&cp)    // Katakana
2613        || (0xAC00..=0xD7AF).contains(&cp)    // Hangul Syllables
2614        // Emoji(常见范围)
2615        || (0x1F300..=0x1F9FF).contains(&cp)
2616        || (0x2600..=0x26FF).contains(&cp)
2617        || (0x2700..=0x27BF).contains(&cp)
2618    {
2619        2
2620    } else {
2621        1
2622    }
2623}
2624
2625/// 绘制输入区
2626fn draw_input(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
2627    // 输入区可用宽度(减去边框2 + prompt 4)
2628    let usable_width = area.width.saturating_sub(2 + 4) as usize;
2629
2630    let chars: Vec<char> = app.input.chars().collect();
2631
2632    // 计算光标之前文本的显示宽度,决定是否需要水平滚动
2633    let before_all: String = chars[..app.cursor_pos].iter().collect();
2634    let before_width = display_width(&before_all);
2635
2636    // 如果光标超出可视范围,从光标附近开始显示
2637    let scroll_offset_chars = if before_width >= usable_width {
2638        // 往回找到一个合适的起始字符位置
2639        let target_width = before_width.saturating_sub(usable_width / 2);
2640        let mut w = 0;
2641        let mut skip = 0;
2642        for (i, &ch) in chars.iter().enumerate() {
2643            if w >= target_width {
2644                skip = i;
2645                break;
2646            }
2647            w += char_width(ch);
2648        }
2649        skip
2650    } else {
2651        0
2652    };
2653
2654    // 截取可见部分的字符
2655    let visible_chars = &chars[scroll_offset_chars..];
2656    let cursor_in_visible = app.cursor_pos - scroll_offset_chars;
2657
2658    let before: String = visible_chars[..cursor_in_visible].iter().collect();
2659    let cursor_ch = if cursor_in_visible < visible_chars.len() {
2660        visible_chars[cursor_in_visible].to_string()
2661    } else {
2662        " ".to_string()
2663    };
2664    let after: String = if cursor_in_visible < visible_chars.len() {
2665        visible_chars[cursor_in_visible + 1..].iter().collect()
2666    } else {
2667        String::new()
2668    };
2669
2670    let prompt_style = if app.is_loading {
2671        Style::default().fg(Color::Rgb(255, 200, 80))
2672    } else {
2673        Style::default().fg(Color::Rgb(100, 200, 130))
2674    };
2675    let prompt_text = if app.is_loading { " .. " } else { " >  " };
2676
2677    // 构建多行输入显示(手动换行)
2678    let full_visible = format!("{}{}{}", before, cursor_ch, after);
2679    let inner_height = area.height.saturating_sub(2) as usize; // 减去边框
2680    let wrapped_lines = wrap_text(&full_visible, usable_width);
2681
2682    // 找到光标所在的行索引
2683    let before_len = before.chars().count();
2684    let cursor_len = cursor_ch.chars().count();
2685    let cursor_global_pos = before_len; // 光标在全部可见字符中的位置
2686    let mut cursor_line_idx: usize = 0;
2687    {
2688        let mut cumulative = 0usize;
2689        for (li, wl) in wrapped_lines.iter().enumerate() {
2690            let line_char_count = wl.chars().count();
2691            if cumulative + line_char_count > cursor_global_pos {
2692                cursor_line_idx = li;
2693                break;
2694            }
2695            cumulative += line_char_count;
2696            cursor_line_idx = li; // 光标恰好在最后一行末尾
2697        }
2698    }
2699
2700    // 计算行滚动:确保光标所在行在可见区域内
2701    let line_scroll = if wrapped_lines.len() <= inner_height {
2702        0
2703    } else if cursor_line_idx < inner_height {
2704        0
2705    } else {
2706        // 让光标行显示在可见区域的最后一行
2707        cursor_line_idx.saturating_sub(inner_height - 1)
2708    };
2709
2710    // 构建带光标高亮的行
2711    let mut display_lines: Vec<Line> = Vec::new();
2712    let mut char_offset: usize = 0;
2713    // 跳过滚动行的字符数
2714    for wl in wrapped_lines.iter().take(line_scroll) {
2715        char_offset += wl.chars().count();
2716    }
2717
2718    for (_line_idx, wl) in wrapped_lines
2719        .iter()
2720        .skip(line_scroll)
2721        .enumerate()
2722        .take(inner_height.max(1))
2723    {
2724        let mut spans: Vec<Span> = Vec::new();
2725        if _line_idx == 0 && line_scroll == 0 {
2726            spans.push(Span::styled(prompt_text, prompt_style));
2727        } else {
2728            spans.push(Span::styled("    ", Style::default())); // 对齐 prompt
2729        }
2730
2731        // 对该行的每个字符分配样式
2732        let line_chars: Vec<char> = wl.chars().collect();
2733        let mut seg_start = 0;
2734        for (ci, &ch) in line_chars.iter().enumerate() {
2735            let global_idx = char_offset + ci;
2736            let is_cursor = global_idx >= before_len && global_idx < before_len + cursor_len;
2737
2738            if is_cursor {
2739                // 先把 cursor 前的部分输出
2740                if ci > seg_start {
2741                    let seg: String = line_chars[seg_start..ci].iter().collect();
2742                    spans.push(Span::styled(seg, Style::default().fg(Color::White)));
2743                }
2744                spans.push(Span::styled(
2745                    ch.to_string(),
2746                    Style::default()
2747                        .fg(Color::Rgb(22, 22, 30))
2748                        .bg(Color::Rgb(200, 210, 240)),
2749                ));
2750                seg_start = ci + 1;
2751            }
2752        }
2753        // 输出剩余部分
2754        if seg_start < line_chars.len() {
2755            let seg: String = line_chars[seg_start..].iter().collect();
2756            spans.push(Span::styled(seg, Style::default().fg(Color::White)));
2757        }
2758
2759        char_offset += line_chars.len();
2760        display_lines.push(Line::from(spans));
2761    }
2762
2763    if display_lines.is_empty() {
2764        display_lines.push(Line::from(vec![
2765            Span::styled(prompt_text, prompt_style),
2766            Span::styled(
2767                " ",
2768                Style::default()
2769                    .fg(Color::Rgb(22, 22, 30))
2770                    .bg(Color::Rgb(200, 210, 240)),
2771            ),
2772        ]));
2773    }
2774
2775    let input_widget = Paragraph::new(display_lines).block(
2776        Block::default()
2777            .borders(Borders::ALL)
2778            .border_type(ratatui::widgets::BorderType::Rounded)
2779            .border_style(if app.is_loading {
2780                Style::default().fg(Color::Rgb(120, 100, 50))
2781            } else {
2782                Style::default().fg(Color::Rgb(60, 100, 80))
2783            })
2784            .title(Span::styled(
2785                " 输入消息 ",
2786                Style::default().fg(Color::Rgb(140, 140, 170)),
2787            ))
2788            .style(Style::default().bg(Color::Rgb(26, 26, 38))),
2789    );
2790
2791    f.render_widget(input_widget, area);
2792
2793    // 设置终端光标位置,确保中文输入法 IME 候选窗口在正确位置
2794    // 计算光标在渲染后的坐标
2795    if !app.is_loading {
2796        let prompt_w: u16 = 4; // prompt 宽度
2797        let border_left: u16 = 1; // 左边框
2798
2799        // 光标在当前显示行中的列偏移
2800        let cursor_col_in_line = {
2801            let mut col = 0usize;
2802            let mut char_count = 0usize;
2803            // 跳过 line_scroll 之前的字符
2804            let mut skip_chars = 0usize;
2805            for wl in wrapped_lines.iter().take(line_scroll) {
2806                skip_chars += wl.chars().count();
2807            }
2808            // 找到光标在当前行的列
2809            for wl in wrapped_lines.iter().skip(line_scroll) {
2810                let line_len = wl.chars().count();
2811                if skip_chars + char_count + line_len > cursor_global_pos {
2812                    // 光标在这一行
2813                    let pos_in_line = cursor_global_pos - (skip_chars + char_count);
2814                    col = wl.chars().take(pos_in_line).map(|c| char_width(c)).sum();
2815                    break;
2816                }
2817                char_count += line_len;
2818            }
2819            col as u16
2820        };
2821
2822        // 光标在显示行中的行偏移
2823        let cursor_row_in_display = (cursor_line_idx - line_scroll) as u16;
2824
2825        let cursor_x = area.x + border_left + prompt_w + cursor_col_in_line;
2826        let cursor_y = area.y + 1 + cursor_row_in_display; // +1 跳过上边框
2827
2828        // 确保光标在区域内
2829        if cursor_x < area.x + area.width && cursor_y < area.y + area.height {
2830            f.set_cursor_position((cursor_x, cursor_y));
2831        }
2832    }
2833}
2834
2835/// 绘制底部操作提示栏(始终可见)
2836fn draw_hint_bar(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
2837    let hints = match app.mode {
2838        ChatMode::Chat => {
2839            vec![
2840                ("Enter", "发送"),
2841                ("↑↓", "滚动"),
2842                ("Ctrl+T", "切换模型"),
2843                ("Ctrl+L", "清空"),
2844                ("Ctrl+Y", "复制"),
2845                ("Ctrl+B", "浏览"),
2846                ("Ctrl+S", "流式切换"),
2847                ("?/F1", "帮助"),
2848                ("Esc", "退出"),
2849            ]
2850        }
2851        ChatMode::SelectModel => {
2852            vec![("↑↓/jk", "移动"), ("Enter", "确认"), ("Esc", "取消")]
2853        }
2854        ChatMode::Browse => {
2855            vec![("↑↓", "选择消息"), ("y/Enter", "复制"), ("Esc", "返回")]
2856        }
2857        ChatMode::Help => {
2858            vec![("任意键", "返回")]
2859        }
2860    };
2861
2862    let mut spans: Vec<Span> = Vec::new();
2863    spans.push(Span::styled(" ", Style::default()));
2864    for (i, (key, desc)) in hints.iter().enumerate() {
2865        if i > 0 {
2866            spans.push(Span::styled(
2867                "  │  ",
2868                Style::default().fg(Color::Rgb(50, 50, 65)),
2869            ));
2870        }
2871        spans.push(Span::styled(
2872            format!(" {} ", key),
2873            Style::default()
2874                .fg(Color::Rgb(22, 22, 30))
2875                .bg(Color::Rgb(100, 110, 140)),
2876        ));
2877        spans.push(Span::styled(
2878            format!(" {}", desc),
2879            Style::default().fg(Color::Rgb(120, 120, 150)),
2880        ));
2881    }
2882
2883    let hint_bar =
2884        Paragraph::new(Line::from(spans)).style(Style::default().bg(Color::Rgb(22, 22, 30)));
2885    f.render_widget(hint_bar, area);
2886}
2887
2888/// 绘制 Toast 弹窗(右上角浮层)
2889fn draw_toast(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
2890    if let Some((ref msg, is_error, _)) = app.toast {
2891        let text_width = display_width(msg);
2892        // toast 宽度 = 文字宽度 + 左右 padding(各2) + emoji(2) + border(2)
2893        let toast_width = (text_width + 10).min(area.width as usize).max(16) as u16;
2894        let toast_height: u16 = 3;
2895
2896        // 定位到右上角
2897        let x = area.width.saturating_sub(toast_width + 1);
2898        let y: u16 = 1;
2899
2900        if x + toast_width <= area.width && y + toast_height <= area.height {
2901            let toast_area = Rect::new(x, y, toast_width, toast_height);
2902
2903            // 先清空区域背景
2904            let clear = Block::default().style(Style::default().bg(if is_error {
2905                Color::Rgb(60, 20, 20)
2906            } else {
2907                Color::Rgb(20, 50, 30)
2908            }));
2909            f.render_widget(clear, toast_area);
2910
2911            let (icon, border_color, text_color) = if is_error {
2912                ("❌", Color::Rgb(200, 70, 70), Color::Rgb(255, 130, 130))
2913            } else {
2914                ("✅", Color::Rgb(60, 160, 80), Color::Rgb(140, 230, 160))
2915            };
2916
2917            let toast_widget = Paragraph::new(Line::from(vec![
2918                Span::styled(format!(" {} ", icon), Style::default()),
2919                Span::styled(msg.as_str(), Style::default().fg(text_color)),
2920            ]))
2921            .block(
2922                Block::default()
2923                    .borders(Borders::ALL)
2924                    .border_type(ratatui::widgets::BorderType::Rounded)
2925                    .border_style(Style::default().fg(border_color))
2926                    .style(Style::default().bg(if is_error {
2927                        Color::Rgb(50, 18, 18)
2928                    } else {
2929                        Color::Rgb(18, 40, 25)
2930                    })),
2931            );
2932            f.render_widget(toast_widget, toast_area);
2933        }
2934    }
2935}
2936
2937/// 绘制模型选择界面
2938fn draw_model_selector(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
2939    let items: Vec<ListItem> = app
2940        .agent_config
2941        .providers
2942        .iter()
2943        .enumerate()
2944        .map(|(i, p)| {
2945            let is_active = i == app.agent_config.active_index;
2946            let marker = if is_active { " ● " } else { " ○ " };
2947            let style = if is_active {
2948                Style::default()
2949                    .fg(Color::Rgb(120, 220, 160))
2950                    .add_modifier(Modifier::BOLD)
2951            } else {
2952                Style::default().fg(Color::Rgb(180, 180, 200))
2953            };
2954            let detail = format!("{}{}  ({})", marker, p.name, p.model);
2955            ListItem::new(Line::from(Span::styled(detail, style)))
2956        })
2957        .collect();
2958
2959    let list = List::new(items)
2960        .block(
2961            Block::default()
2962                .borders(Borders::ALL)
2963                .border_type(ratatui::widgets::BorderType::Rounded)
2964                .border_style(Style::default().fg(Color::Rgb(180, 160, 80)))
2965                .title(Span::styled(
2966                    " 🔄 选择模型 ",
2967                    Style::default()
2968                        .fg(Color::Rgb(230, 210, 120))
2969                        .add_modifier(Modifier::BOLD),
2970                ))
2971                .style(Style::default().bg(Color::Rgb(28, 28, 40))),
2972        )
2973        .highlight_style(
2974            Style::default()
2975                .bg(Color::Rgb(50, 55, 80))
2976                .fg(Color::White)
2977                .add_modifier(Modifier::BOLD),
2978        )
2979        .highlight_symbol("  ▸ ");
2980
2981    f.render_stateful_widget(list, area, &mut app.model_list_state);
2982}
2983
2984/// 绘制帮助界面
2985fn draw_help(f: &mut ratatui::Frame, area: Rect) {
2986    let separator = Line::from(Span::styled(
2987        "  ─────────────────────────────────────────",
2988        Style::default().fg(Color::Rgb(50, 55, 70)),
2989    ));
2990
2991    let help_lines = vec![
2992        Line::from(""),
2993        Line::from(Span::styled(
2994            "  📖 快捷键帮助",
2995            Style::default()
2996                .fg(Color::Rgb(120, 180, 255))
2997                .add_modifier(Modifier::BOLD),
2998        )),
2999        Line::from(""),
3000        separator.clone(),
3001        Line::from(""),
3002        Line::from(vec![
3003            Span::styled(
3004                "  Enter        ",
3005                Style::default()
3006                    .fg(Color::Rgb(230, 210, 120))
3007                    .add_modifier(Modifier::BOLD),
3008            ),
3009            Span::styled("发送消息", Style::default().fg(Color::Rgb(200, 200, 220))),
3010        ]),
3011        Line::from(vec![
3012            Span::styled(
3013                "  ↑ / ↓        ",
3014                Style::default()
3015                    .fg(Color::Rgb(230, 210, 120))
3016                    .add_modifier(Modifier::BOLD),
3017            ),
3018            Span::styled(
3019                "滚动对话记录",
3020                Style::default().fg(Color::Rgb(200, 200, 220)),
3021            ),
3022        ]),
3023        Line::from(vec![
3024            Span::styled(
3025                "  ← / →        ",
3026                Style::default()
3027                    .fg(Color::Rgb(230, 210, 120))
3028                    .add_modifier(Modifier::BOLD),
3029            ),
3030            Span::styled(
3031                "移动输入光标",
3032                Style::default().fg(Color::Rgb(200, 200, 220)),
3033            ),
3034        ]),
3035        Line::from(vec![
3036            Span::styled(
3037                "  Ctrl+T       ",
3038                Style::default()
3039                    .fg(Color::Rgb(230, 210, 120))
3040                    .add_modifier(Modifier::BOLD),
3041            ),
3042            Span::styled("切换模型", Style::default().fg(Color::Rgb(200, 200, 220))),
3043        ]),
3044        Line::from(vec![
3045            Span::styled(
3046                "  Ctrl+L       ",
3047                Style::default()
3048                    .fg(Color::Rgb(230, 210, 120))
3049                    .add_modifier(Modifier::BOLD),
3050            ),
3051            Span::styled(
3052                "清空对话历史",
3053                Style::default().fg(Color::Rgb(200, 200, 220)),
3054            ),
3055        ]),
3056        Line::from(vec![
3057            Span::styled(
3058                "  Ctrl+Y       ",
3059                Style::default()
3060                    .fg(Color::Rgb(230, 210, 120))
3061                    .add_modifier(Modifier::BOLD),
3062            ),
3063            Span::styled(
3064                "复制最后一条 AI 回复",
3065                Style::default().fg(Color::Rgb(200, 200, 220)),
3066            ),
3067        ]),
3068        Line::from(vec![
3069            Span::styled(
3070                "  Ctrl+B       ",
3071                Style::default()
3072                    .fg(Color::Rgb(230, 210, 120))
3073                    .add_modifier(Modifier::BOLD),
3074            ),
3075            Span::styled(
3076                "浏览消息 (↑↓选择, y/Enter复制)",
3077                Style::default().fg(Color::Rgb(200, 200, 220)),
3078            ),
3079        ]),
3080        Line::from(vec![
3081            Span::styled(
3082                "  Ctrl+S       ",
3083                Style::default()
3084                    .fg(Color::Rgb(230, 210, 120))
3085                    .add_modifier(Modifier::BOLD),
3086            ),
3087            Span::styled(
3088                "切换流式/整体输出",
3089                Style::default().fg(Color::Rgb(200, 200, 220)),
3090            ),
3091        ]),
3092        Line::from(vec![
3093            Span::styled(
3094                "  Esc / Ctrl+C ",
3095                Style::default()
3096                    .fg(Color::Rgb(230, 210, 120))
3097                    .add_modifier(Modifier::BOLD),
3098            ),
3099            Span::styled("退出对话", Style::default().fg(Color::Rgb(200, 200, 220))),
3100        ]),
3101        Line::from(vec![
3102            Span::styled(
3103                "  ? / F1       ",
3104                Style::default()
3105                    .fg(Color::Rgb(230, 210, 120))
3106                    .add_modifier(Modifier::BOLD),
3107            ),
3108            Span::styled(
3109                "显示 / 关闭此帮助",
3110                Style::default().fg(Color::Rgb(200, 200, 220)),
3111            ),
3112        ]),
3113        Line::from(""),
3114        separator,
3115        Line::from(""),
3116        Line::from(Span::styled(
3117            "  📁 配置文件:",
3118            Style::default()
3119                .fg(Color::Rgb(120, 180, 255))
3120                .add_modifier(Modifier::BOLD),
3121        )),
3122        Line::from(Span::styled(
3123            format!("     {}", agent_config_path().display()),
3124            Style::default().fg(Color::Rgb(100, 100, 130)),
3125        )),
3126    ];
3127
3128    let help_block = Block::default()
3129        .borders(Borders::ALL)
3130        .border_type(ratatui::widgets::BorderType::Rounded)
3131        .border_style(Style::default().fg(Color::Rgb(80, 100, 140)))
3132        .title(Span::styled(
3133            " 帮助 (按任意键返回) ",
3134            Style::default().fg(Color::Rgb(140, 140, 170)),
3135        ))
3136        .style(Style::default().bg(Color::Rgb(24, 24, 34)));
3137    let help_widget = Paragraph::new(help_lines).block(help_block);
3138    f.render_widget(help_widget, area);
3139}
3140
3141/// 对话模式按键处理,返回 true 表示退出
3142fn handle_chat_mode(app: &mut ChatApp, key: KeyEvent) -> bool {
3143    // Ctrl+C 强制退出
3144    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
3145        return true;
3146    }
3147
3148    // Ctrl+T 切换模型(替代 Ctrl+M,因为 Ctrl+M 在终端中等于 Enter)
3149    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('t') {
3150        if !app.agent_config.providers.is_empty() {
3151            app.mode = ChatMode::SelectModel;
3152            app.model_list_state
3153                .select(Some(app.agent_config.active_index));
3154        }
3155        return false;
3156    }
3157
3158    // Ctrl+L 清空对话
3159    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('l') {
3160        app.clear_session();
3161        return false;
3162    }
3163
3164    // Ctrl+Y 复制最后一条 AI 回复
3165    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('y') {
3166        if let Some(last_ai) = app
3167            .session
3168            .messages
3169            .iter()
3170            .rev()
3171            .find(|m| m.role == "assistant")
3172        {
3173            if copy_to_clipboard(&last_ai.content) {
3174                app.show_toast("已复制最后一条 AI 回复", false);
3175            } else {
3176                app.show_toast("复制到剪切板失败", true);
3177            }
3178        } else {
3179            app.show_toast("暂无 AI 回复可复制", true);
3180        }
3181        return false;
3182    }
3183
3184    // Ctrl+B 进入消息浏览模式(可选中历史消息并复制)
3185    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('b') {
3186        if !app.session.messages.is_empty() {
3187            // 默认选中最后一条消息
3188            app.browse_msg_index = app.session.messages.len() - 1;
3189            app.mode = ChatMode::Browse;
3190            app.msg_lines_cache = None; // 清除缓存以触发高亮重绘
3191        } else {
3192            app.show_toast("暂无消息可浏览", true);
3193        }
3194        return false;
3195    }
3196
3197    // Ctrl+S 切换流式/非流式输出
3198    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('s') {
3199        app.agent_config.stream_mode = !app.agent_config.stream_mode;
3200        let _ = save_agent_config(&app.agent_config);
3201        let mode_str = if app.agent_config.stream_mode {
3202            "流式输出"
3203        } else {
3204            "整体输出"
3205        };
3206        app.show_toast(&format!("已切换为: {}", mode_str), false);
3207        return false;
3208    }
3209
3210    let char_count = app.input.chars().count();
3211
3212    match key.code {
3213        KeyCode::Esc => return true,
3214
3215        KeyCode::Enter => {
3216            if !app.is_loading {
3217                app.send_message();
3218            }
3219        }
3220
3221        // 滚动消息
3222        KeyCode::Up => app.scroll_up(),
3223        KeyCode::Down => app.scroll_down(),
3224        KeyCode::PageUp => {
3225            for _ in 0..10 {
3226                app.scroll_up();
3227            }
3228        }
3229        KeyCode::PageDown => {
3230            for _ in 0..10 {
3231                app.scroll_down();
3232            }
3233        }
3234
3235        // 光标移动
3236        KeyCode::Left => {
3237            if app.cursor_pos > 0 {
3238                app.cursor_pos -= 1;
3239            }
3240        }
3241        KeyCode::Right => {
3242            if app.cursor_pos < char_count {
3243                app.cursor_pos += 1;
3244            }
3245        }
3246        KeyCode::Home => app.cursor_pos = 0,
3247        KeyCode::End => app.cursor_pos = char_count,
3248
3249        // 删除
3250        KeyCode::Backspace => {
3251            if app.cursor_pos > 0 {
3252                let start = app
3253                    .input
3254                    .char_indices()
3255                    .nth(app.cursor_pos - 1)
3256                    .map(|(i, _)| i)
3257                    .unwrap_or(0);
3258                let end = app
3259                    .input
3260                    .char_indices()
3261                    .nth(app.cursor_pos)
3262                    .map(|(i, _)| i)
3263                    .unwrap_or(app.input.len());
3264                app.input.drain(start..end);
3265                app.cursor_pos -= 1;
3266            }
3267        }
3268        KeyCode::Delete => {
3269            if app.cursor_pos < char_count {
3270                let start = app
3271                    .input
3272                    .char_indices()
3273                    .nth(app.cursor_pos)
3274                    .map(|(i, _)| i)
3275                    .unwrap_or(app.input.len());
3276                let end = app
3277                    .input
3278                    .char_indices()
3279                    .nth(app.cursor_pos + 1)
3280                    .map(|(i, _)| i)
3281                    .unwrap_or(app.input.len());
3282                app.input.drain(start..end);
3283            }
3284        }
3285
3286        // F1 任何时候都能唤起帮助
3287        KeyCode::F(1) => {
3288            app.mode = ChatMode::Help;
3289        }
3290        // 输入框为空时,? 也可唤起帮助
3291        KeyCode::Char('?') if app.input.is_empty() => {
3292            app.mode = ChatMode::Help;
3293        }
3294        KeyCode::Char(c) => {
3295            let byte_idx = app
3296                .input
3297                .char_indices()
3298                .nth(app.cursor_pos)
3299                .map(|(i, _)| i)
3300                .unwrap_or(app.input.len());
3301            app.input.insert_str(byte_idx, &c.to_string());
3302            app.cursor_pos += 1;
3303        }
3304
3305        _ => {}
3306    }
3307
3308    false
3309}
3310
3311/// 消息浏览模式按键处理:↑↓ 选择消息,y/Enter 复制选中消息,Esc 退出
3312fn handle_browse_mode(app: &mut ChatApp, key: KeyEvent) {
3313    let msg_count = app.session.messages.len();
3314    if msg_count == 0 {
3315        app.mode = ChatMode::Chat;
3316        app.msg_lines_cache = None;
3317        return;
3318    }
3319
3320    match key.code {
3321        KeyCode::Esc => {
3322            app.mode = ChatMode::Chat;
3323            app.msg_lines_cache = None; // 退出浏览模式时清除缓存,去掉高亮
3324        }
3325        KeyCode::Up | KeyCode::Char('k') => {
3326            if app.browse_msg_index > 0 {
3327                app.browse_msg_index -= 1;
3328                app.msg_lines_cache = None; // 选中变化时清缓存
3329            }
3330        }
3331        KeyCode::Down | KeyCode::Char('j') => {
3332            if app.browse_msg_index < msg_count - 1 {
3333                app.browse_msg_index += 1;
3334                app.msg_lines_cache = None; // 选中变化时清缓存
3335            }
3336        }
3337        KeyCode::Enter | KeyCode::Char('y') => {
3338            // 复制选中消息的原始内容到剪切板
3339            if let Some(msg) = app.session.messages.get(app.browse_msg_index) {
3340                let content = msg.content.clone();
3341                let role_label = if msg.role == "assistant" {
3342                    "AI"
3343                } else if msg.role == "user" {
3344                    "用户"
3345                } else {
3346                    "系统"
3347                };
3348                if copy_to_clipboard(&content) {
3349                    app.show_toast(
3350                        &format!("已复制第 {} 条{}消息", app.browse_msg_index + 1, role_label),
3351                        false,
3352                    );
3353                } else {
3354                    app.show_toast("复制到剪切板失败", true);
3355                }
3356            }
3357        }
3358        _ => {}
3359    }
3360}
3361
3362/// 模型选择模式按键处理
3363fn handle_select_model(app: &mut ChatApp, key: KeyEvent) {
3364    let count = app.agent_config.providers.len();
3365    match key.code {
3366        KeyCode::Esc => {
3367            app.mode = ChatMode::Chat;
3368        }
3369        KeyCode::Up | KeyCode::Char('k') => {
3370            if count > 0 {
3371                let i = app
3372                    .model_list_state
3373                    .selected()
3374                    .map(|i| if i == 0 { count - 1 } else { i - 1 })
3375                    .unwrap_or(0);
3376                app.model_list_state.select(Some(i));
3377            }
3378        }
3379        KeyCode::Down | KeyCode::Char('j') => {
3380            if count > 0 {
3381                let i = app
3382                    .model_list_state
3383                    .selected()
3384                    .map(|i| if i >= count - 1 { 0 } else { i + 1 })
3385                    .unwrap_or(0);
3386                app.model_list_state.select(Some(i));
3387            }
3388        }
3389        KeyCode::Enter => {
3390            app.switch_model();
3391        }
3392        _ => {}
3393    }
3394}
3395
3396/// 复制内容到系统剪切板
3397fn copy_to_clipboard(content: &str) -> bool {
3398    use std::process::{Command, Stdio};
3399
3400    let (cmd, args): (&str, Vec<&str>) = if cfg!(target_os = "macos") {
3401        ("pbcopy", vec![])
3402    } else if cfg!(target_os = "linux") {
3403        if Command::new("which")
3404            .arg("xclip")
3405            .output()
3406            .map(|o| o.status.success())
3407            .unwrap_or(false)
3408        {
3409            ("xclip", vec!["-selection", "clipboard"])
3410        } else {
3411            ("xsel", vec!["--clipboard", "--input"])
3412        }
3413    } else {
3414        return false;
3415    };
3416
3417    let child = Command::new(cmd).args(&args).stdin(Stdio::piped()).spawn();
3418
3419    match child {
3420        Ok(mut child) => {
3421            if let Some(ref mut stdin) = child.stdin {
3422                let _ = stdin.write_all(content.as_bytes());
3423            }
3424            child.wait().map(|s| s.success()).unwrap_or(false)
3425        }
3426        Err(_) => false,
3427    }
3428}