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    last_rendered_streaming_len: usize,
382    /// 流式节流:上次实际渲染流式内容的时间
383    last_stream_render_time: std::time::Instant,
384    /// 配置界面:当前选中的 provider 索引
385    config_provider_idx: usize,
386    /// 配置界面:当前选中的字段索引
387    config_field_idx: usize,
388    /// 配置界面:是否正在编辑某个字段
389    config_editing: bool,
390    /// 配置界面:编辑缓冲区
391    config_edit_buf: String,
392    /// 配置界面:编辑光标位置
393    config_edit_cursor: usize,
394    /// 流式输出时是否自动滚动到底部(用户手动上滚后关闭,发送新消息或滚到底部时恢复)
395    auto_scroll: bool,
396}
397
398/// 消息渲染行缓存
399struct MsgLinesCache {
400    /// 会话消息数量
401    msg_count: usize,
402    /// 最后一条消息的内容长度(用于检测流式更新)
403    last_msg_len: usize,
404    /// 流式内容长度
405    streaming_len: usize,
406    /// 是否正在加载
407    is_loading: bool,
408    /// 气泡最大宽度(窗口变化时需要重算)
409    bubble_max_width: usize,
410    /// 浏览模式选中索引(None 表示非浏览模式)
411    browse_index: Option<usize>,
412    /// 缓存的渲染行
413    lines: Vec<Line<'static>>,
414    /// 每条消息(按 msg_index)的起始行号(用于浏览模式自动滚动)
415    msg_start_lines: Vec<(usize, usize)>, // (msg_index, start_line)
416    /// 按消息粒度缓存:每条历史消息的渲染行(key: 消息索引)
417    per_msg_lines: Vec<PerMsgCache>,
418    /// 流式增量渲染缓存:已完成段落的渲染行
419    streaming_stable_lines: Vec<Line<'static>>,
420    /// 流式增量渲染缓存:已缓存到 streaming_content 的字节偏移
421    streaming_stable_offset: usize,
422}
423
424/// 单条消息的渲染缓存
425struct PerMsgCache {
426    /// 消息内容长度(用于检测变化)
427    content_len: usize,
428    /// 渲染好的行
429    lines: Vec<Line<'static>>,
430    /// 对应的 msg_start_line(此消息在全局行列表中的起始行号,需在拼装时更新)
431    msg_index: usize,
432}
433
434/// Toast 通知显示时长(秒)
435const TOAST_DURATION_SECS: u64 = 4;
436
437#[derive(PartialEq)]
438enum ChatMode {
439    /// 正常对话模式(焦点在输入框)
440    Chat,
441    /// 模型选择模式
442    SelectModel,
443    /// 消息浏览模式(可选中消息并复制)
444    Browse,
445    /// 帮助
446    Help,
447    /// 配置编辑模式
448    Config,
449}
450
451/// 配置编辑界面的字段列表
452const CONFIG_FIELDS: &[&str] = &["name", "api_base", "api_key", "model"];
453/// 全局配置字段
454const CONFIG_GLOBAL_FIELDS: &[&str] = &["system_prompt", "stream_mode"];
455/// 所有字段数 = provider 字段 + 全局字段
456fn config_total_fields() -> usize {
457    CONFIG_FIELDS.len() + CONFIG_GLOBAL_FIELDS.len()
458}
459
460impl ChatApp {
461    fn new() -> Self {
462        let agent_config = load_agent_config();
463        let session = load_chat_session();
464        let mut model_list_state = ListState::default();
465        if !agent_config.providers.is_empty() {
466            model_list_state.select(Some(agent_config.active_index));
467        }
468        Self {
469            agent_config,
470            session,
471            input: String::new(),
472            cursor_pos: 0,
473            mode: ChatMode::Chat,
474            scroll_offset: u16::MAX, // 默认滚动到底部
475            is_loading: false,
476            model_list_state,
477            toast: None,
478            stream_rx: None,
479            streaming_content: Arc::new(Mutex::new(String::new())),
480            msg_lines_cache: None,
481            browse_msg_index: 0,
482            last_rendered_streaming_len: 0,
483            last_stream_render_time: std::time::Instant::now(),
484            config_provider_idx: 0,
485            config_field_idx: 0,
486            config_editing: false,
487            config_edit_buf: String::new(),
488            config_edit_cursor: 0,
489            auto_scroll: true,
490        }
491    }
492
493    /// 显示一条 toast 通知
494    fn show_toast(&mut self, msg: impl Into<String>, is_error: bool) {
495        self.toast = Some((msg.into(), is_error, std::time::Instant::now()));
496    }
497
498    /// 清理过期的 toast
499    fn tick_toast(&mut self) {
500        if let Some((_, _, created)) = &self.toast {
501            if created.elapsed().as_secs() >= TOAST_DURATION_SECS {
502                self.toast = None;
503            }
504        }
505    }
506
507    /// 获取当前活跃的 provider
508    fn active_provider(&self) -> Option<&ModelProvider> {
509        if self.agent_config.providers.is_empty() {
510            return None;
511        }
512        let idx = self
513            .agent_config
514            .active_index
515            .min(self.agent_config.providers.len() - 1);
516        Some(&self.agent_config.providers[idx])
517    }
518
519    /// 获取当前模型名称
520    fn active_model_name(&self) -> String {
521        self.active_provider()
522            .map(|p| p.name.clone())
523            .unwrap_or_else(|| "未配置".to_string())
524    }
525
526    /// 构建发送给 API 的消息列表
527    fn build_api_messages(&self) -> Vec<ChatMessage> {
528        let mut messages = Vec::new();
529        if let Some(sys) = &self.agent_config.system_prompt {
530            messages.push(ChatMessage {
531                role: "system".to_string(),
532                content: sys.clone(),
533            });
534        }
535        for msg in &self.session.messages {
536            messages.push(msg.clone());
537        }
538        messages
539    }
540
541    /// 发送消息(非阻塞,启动后台线程流式接收)
542    fn send_message(&mut self) {
543        let text = self.input.trim().to_string();
544        if text.is_empty() {
545            return;
546        }
547
548        // 添加用户消息
549        self.session.messages.push(ChatMessage {
550            role: "user".to_string(),
551            content: text,
552        });
553        self.input.clear();
554        self.cursor_pos = 0;
555        // 发送新消息时恢复自动滚动并滚到底部
556        self.auto_scroll = true;
557        self.scroll_offset = u16::MAX;
558
559        // 调用 API
560        let provider = match self.active_provider() {
561            Some(p) => p.clone(),
562            None => {
563                self.show_toast("未配置模型提供方,请先编辑配置文件", true);
564                return;
565            }
566        };
567
568        self.is_loading = true;
569        // 重置流式节流状态和缓存
570        self.last_rendered_streaming_len = 0;
571        self.last_stream_render_time = std::time::Instant::now();
572        self.msg_lines_cache = None;
573
574        let api_messages = self.build_api_messages();
575
576        // 清空流式内容缓冲
577        {
578            let mut sc = self.streaming_content.lock().unwrap();
579            sc.clear();
580        }
581
582        // 创建 channel 用于后台线程 -> TUI 通信
583        let (tx, rx) = mpsc::channel::<StreamMsg>();
584        self.stream_rx = Some(rx);
585
586        let streaming_content = Arc::clone(&self.streaming_content);
587
588        let use_stream = self.agent_config.stream_mode;
589
590        // 启动后台线程执行 API 调用
591        std::thread::spawn(move || {
592            let rt = match tokio::runtime::Runtime::new() {
593                Ok(rt) => rt,
594                Err(e) => {
595                    let _ = tx.send(StreamMsg::Error(format!("创建异步运行时失败: {}", e)));
596                    return;
597                }
598            };
599
600            rt.block_on(async {
601                let client = create_openai_client(&provider);
602                let openai_messages = to_openai_messages(&api_messages);
603
604                let request = match CreateChatCompletionRequestArgs::default()
605                    .model(&provider.model)
606                    .messages(openai_messages)
607                    .build()
608                {
609                    Ok(req) => req,
610                    Err(e) => {
611                        let _ = tx.send(StreamMsg::Error(format!("构建请求失败: {}", e)));
612                        return;
613                    }
614                };
615
616                if use_stream {
617                    // 流式输出模式
618                    let mut stream = match client.chat().create_stream(request).await {
619                        Ok(s) => s,
620                        Err(e) => {
621                            let _ = tx.send(StreamMsg::Error(format!("API 请求失败: {}", e)));
622                            return;
623                        }
624                    };
625
626                    while let Some(result) = stream.next().await {
627                        match result {
628                            Ok(response) => {
629                                for choice in &response.choices {
630                                    if let Some(ref content) = choice.delta.content {
631                                        // 更新共享缓冲
632                                        {
633                                            let mut sc = streaming_content.lock().unwrap();
634                                            sc.push_str(content);
635                                        }
636                                        let _ = tx.send(StreamMsg::Chunk);
637                                    }
638                                }
639                            }
640                            Err(e) => {
641                                let _ = tx.send(StreamMsg::Error(format!("流式响应错误: {}", e)));
642                                return;
643                            }
644                        }
645                    }
646                } else {
647                    // 非流式输出模式:等待完整响应后一次性返回
648                    match client.chat().create(request).await {
649                        Ok(response) => {
650                            if let Some(choice) = response.choices.first() {
651                                if let Some(ref content) = choice.message.content {
652                                    {
653                                        let mut sc = streaming_content.lock().unwrap();
654                                        sc.push_str(content);
655                                    }
656                                    let _ = tx.send(StreamMsg::Chunk);
657                                }
658                            }
659                        }
660                        Err(e) => {
661                            let _ = tx.send(StreamMsg::Error(format!("API 请求失败: {}", e)));
662                            return;
663                        }
664                    }
665                }
666
667                let _ = tx.send(StreamMsg::Done);
668
669                let _ = tx.send(StreamMsg::Done);
670            });
671        });
672    }
673
674    /// 处理后台流式消息(在主循环中每帧调用)
675    fn poll_stream(&mut self) {
676        if self.stream_rx.is_none() {
677            return;
678        }
679
680        let mut finished = false;
681        let mut had_error = false;
682
683        // 非阻塞地取出所有可用的消息
684        if let Some(ref rx) = self.stream_rx {
685            loop {
686                match rx.try_recv() {
687                    Ok(StreamMsg::Chunk) => {
688                        // 内容已经通过 Arc<Mutex<String>> 更新
689                        // 只有在用户没有手动滚动的情况下才自动滚到底部
690                        if self.auto_scroll {
691                            self.scroll_offset = u16::MAX;
692                        }
693                    }
694                    Ok(StreamMsg::Done) => {
695                        finished = true;
696                        break;
697                    }
698                    Ok(StreamMsg::Error(e)) => {
699                        self.show_toast(format!("请求失败: {}", e), true);
700                        had_error = true;
701                        finished = true;
702                        break;
703                    }
704                    Err(mpsc::TryRecvError::Empty) => break,
705                    Err(mpsc::TryRecvError::Disconnected) => {
706                        finished = true;
707                        break;
708                    }
709                }
710            }
711        }
712
713        if finished {
714            self.stream_rx = None;
715            self.is_loading = false;
716            // 重置流式节流状态
717            self.last_rendered_streaming_len = 0;
718            // 清除缓存,流式结束后需要完整重建(新消息已加入 session)
719            self.msg_lines_cache = None;
720
721            if !had_error {
722                // 将流式内容作为完整回复添加到会话
723                let content = {
724                    let sc = self.streaming_content.lock().unwrap();
725                    sc.clone()
726                };
727                if !content.is_empty() {
728                    self.session.messages.push(ChatMessage {
729                        role: "assistant".to_string(),
730                        content,
731                    });
732                    // 清空流式缓冲
733                    self.streaming_content.lock().unwrap().clear();
734                    self.show_toast("回复完成 ✓", false);
735                }
736                if self.auto_scroll {
737                    self.scroll_offset = u16::MAX;
738                }
739            } else {
740                // 错误时也清空流式缓冲
741                self.streaming_content.lock().unwrap().clear();
742            }
743
744            // 自动保存对话历史
745            let _ = save_chat_session(&self.session);
746        }
747    }
748
749    /// 清空对话
750    fn clear_session(&mut self) {
751        self.session.messages.clear();
752        self.scroll_offset = 0;
753        self.msg_lines_cache = None; // 清除缓存
754        let _ = save_chat_session(&self.session);
755        self.show_toast("对话已清空", false);
756    }
757
758    /// 切换模型
759    fn switch_model(&mut self) {
760        if let Some(sel) = self.model_list_state.selected() {
761            self.agent_config.active_index = sel;
762            let _ = save_agent_config(&self.agent_config);
763            let name = self.active_model_name();
764            self.show_toast(format!("已切换到: {}", name), false);
765        }
766        self.mode = ChatMode::Chat;
767    }
768
769    /// 向上滚动消息
770    fn scroll_up(&mut self) {
771        self.scroll_offset = self.scroll_offset.saturating_sub(3);
772        // 用户手动上滚,关闭自动滚动
773        self.auto_scroll = false;
774    }
775
776    /// 向下滚动消息
777    fn scroll_down(&mut self) {
778        self.scroll_offset = self.scroll_offset.saturating_add(3);
779        // 注意:scroll_offset 可能超过 max_scroll,绘制时会校正。
780        // 如果用户滚到了底部(offset >= max_scroll),在绘制时会恢复 auto_scroll。
781    }
782}
783
784/// 启动 TUI 对话界面
785fn run_chat_tui() {
786    match run_chat_tui_internal() {
787        Ok(_) => {}
788        Err(e) => {
789            error!("❌ Chat TUI 启动失败: {}", e);
790        }
791    }
792}
793
794fn run_chat_tui_internal() -> io::Result<()> {
795    terminal::enable_raw_mode()?;
796    let mut stdout = io::stdout();
797    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
798
799    let backend = CrosstermBackend::new(stdout);
800    let mut terminal = Terminal::new(backend)?;
801
802    let mut app = ChatApp::new();
803
804    if app.agent_config.providers.is_empty() {
805        terminal::disable_raw_mode()?;
806        execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
807        info!("⚠️  尚未配置 LLM 模型提供方,请先运行 j chat 查看配置说明。");
808        return Ok(());
809    }
810
811    let mut needs_redraw = true; // 首次必须绘制
812
813    loop {
814        // 清理过期 toast(如果有 toast 被清理,需要重绘)
815        let had_toast = app.toast.is_some();
816        app.tick_toast();
817        if had_toast && app.toast.is_none() {
818            needs_redraw = true;
819        }
820
821        // 非阻塞地处理后台流式消息
822        let was_loading = app.is_loading;
823        app.poll_stream();
824        // 流式加载中使用节流策略:只在内容增长超过阈值或超时才重绘
825        if app.is_loading {
826            let current_len = app.streaming_content.lock().unwrap().len();
827            let bytes_delta = current_len.saturating_sub(app.last_rendered_streaming_len);
828            let time_elapsed = app.last_stream_render_time.elapsed();
829            // 每增加 200 字节或距离上次渲染超过 200ms 才重绘
830            if bytes_delta >= 200
831                || time_elapsed >= std::time::Duration::from_millis(200)
832                || current_len == 0
833            {
834                needs_redraw = true;
835            }
836        } else if was_loading {
837            // 加载刚结束时必须重绘一次
838            needs_redraw = true;
839        }
840
841        // 只在状态发生变化时才重绘,大幅降低 CPU 占用
842        if needs_redraw {
843            terminal.draw(|f| draw_chat_ui(f, &mut app))?;
844            needs_redraw = false;
845            // 更新流式节流状态
846            if app.is_loading {
847                app.last_rendered_streaming_len = app.streaming_content.lock().unwrap().len();
848                app.last_stream_render_time = std::time::Instant::now();
849            }
850        }
851
852        // 等待事件:加载中用短间隔以刷新流式内容,空闲时用长间隔节省 CPU
853        let poll_timeout = if app.is_loading {
854            std::time::Duration::from_millis(150)
855        } else {
856            std::time::Duration::from_millis(1000)
857        };
858
859        if event::poll(poll_timeout)? {
860            // 批量消费所有待处理事件,避免快速滚动/打字时事件堆积
861            let mut should_break = false;
862            loop {
863                let evt = event::read()?;
864                match evt {
865                    Event::Key(key) => {
866                        needs_redraw = true;
867                        match app.mode {
868                            ChatMode::Chat => {
869                                if handle_chat_mode(&mut app, key) {
870                                    should_break = true;
871                                    break;
872                                }
873                            }
874                            ChatMode::SelectModel => handle_select_model(&mut app, key),
875                            ChatMode::Browse => handle_browse_mode(&mut app, key),
876                            ChatMode::Help => {
877                                app.mode = ChatMode::Chat;
878                            }
879                            ChatMode::Config => handle_config_mode(&mut app, key),
880                        }
881                    }
882                    Event::Mouse(mouse) => match mouse.kind {
883                        MouseEventKind::ScrollUp => {
884                            app.scroll_up();
885                            needs_redraw = true;
886                        }
887                        MouseEventKind::ScrollDown => {
888                            app.scroll_down();
889                            needs_redraw = true;
890                        }
891                        _ => {}
892                    },
893                    Event::Resize(_, _) => {
894                        needs_redraw = true;
895                    }
896                    _ => {}
897                }
898                // 继续消费剩余事件(非阻塞,Duration::ZERO)
899                if !event::poll(std::time::Duration::ZERO)? {
900                    break;
901                }
902            }
903            if should_break {
904                break;
905            }
906        }
907    }
908
909    // 保存对话历史
910    let _ = save_chat_session(&app.session);
911
912    terminal::disable_raw_mode()?;
913    execute!(
914        terminal.backend_mut(),
915        LeaveAlternateScreen,
916        DisableMouseCapture
917    )?;
918    Ok(())
919}
920
921/// 绘制 TUI 界面
922fn draw_chat_ui(f: &mut ratatui::Frame, app: &mut ChatApp) {
923    let size = f.area();
924
925    // 整体背景
926    let bg = Block::default().style(Style::default().bg(Color::Rgb(22, 22, 30)));
927    f.render_widget(bg, size);
928
929    let chunks = Layout::default()
930        .direction(Direction::Vertical)
931        .constraints([
932            Constraint::Length(3), // 标题栏
933            Constraint::Min(5),    // 消息区
934            Constraint::Length(5), // 输入区
935            Constraint::Length(1), // 操作提示栏(始终可见)
936        ])
937        .split(size);
938
939    // ========== 标题栏 ==========
940    draw_title_bar(f, chunks[0], app);
941
942    // ========== 消息区 ==========
943    if app.mode == ChatMode::Help {
944        draw_help(f, chunks[1]);
945    } else if app.mode == ChatMode::SelectModel {
946        draw_model_selector(f, chunks[1], app);
947    } else if app.mode == ChatMode::Config {
948        draw_config_screen(f, chunks[1], app);
949    } else {
950        draw_messages(f, chunks[1], app);
951    }
952
953    // ========== 输入区 ==========
954    draw_input(f, chunks[2], app);
955
956    // ========== 底部操作提示栏(始终可见)==========
957    draw_hint_bar(f, chunks[3], app);
958
959    // ========== Toast 弹窗覆盖层(右上角)==========
960    draw_toast(f, size, app);
961}
962
963/// 绘制标题栏
964fn draw_title_bar(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
965    let model_name = app.active_model_name();
966    let msg_count = app.session.messages.len();
967    let loading = if app.is_loading {
968        " ⏳ 思考中..."
969    } else {
970        ""
971    };
972
973    let title_spans = vec![
974        Span::styled(" 💬 ", Style::default().fg(Color::Rgb(120, 180, 255))),
975        Span::styled(
976            "AI Chat",
977            Style::default()
978                .fg(Color::White)
979                .add_modifier(Modifier::BOLD),
980        ),
981        Span::styled("  │  ", Style::default().fg(Color::Rgb(60, 60, 80))),
982        Span::styled("🤖 ", Style::default()),
983        Span::styled(
984            model_name,
985            Style::default()
986                .fg(Color::Rgb(160, 220, 160))
987                .add_modifier(Modifier::BOLD),
988        ),
989        Span::styled("  │  ", Style::default().fg(Color::Rgb(60, 60, 80))),
990        Span::styled(
991            format!("📨 {} 条消息", msg_count),
992            Style::default().fg(Color::Rgb(180, 180, 200)),
993        ),
994        Span::styled(
995            loading,
996            Style::default()
997                .fg(Color::Rgb(255, 200, 80))
998                .add_modifier(Modifier::BOLD),
999        ),
1000    ];
1001
1002    let title_block = Paragraph::new(Line::from(title_spans)).block(
1003        Block::default()
1004            .borders(Borders::ALL)
1005            .border_type(ratatui::widgets::BorderType::Rounded)
1006            .border_style(Style::default().fg(Color::Rgb(80, 100, 140)))
1007            .style(Style::default().bg(Color::Rgb(28, 28, 40))),
1008    );
1009    f.render_widget(title_block, area);
1010}
1011
1012/// 绘制消息区
1013fn draw_messages(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
1014    let block = Block::default()
1015        .borders(Borders::ALL)
1016        .border_type(ratatui::widgets::BorderType::Rounded)
1017        .border_style(Style::default().fg(Color::Rgb(50, 55, 70)))
1018        .title(Span::styled(
1019            " 对话记录 ",
1020            Style::default()
1021                .fg(Color::Rgb(140, 140, 170))
1022                .add_modifier(Modifier::BOLD),
1023        ))
1024        .title_alignment(ratatui::layout::Alignment::Left)
1025        .style(Style::default().bg(Color::Rgb(22, 22, 30)));
1026
1027    // 空消息时显示欢迎界面
1028    if app.session.messages.is_empty() && !app.is_loading {
1029        let welcome_lines = vec![
1030            Line::from(""),
1031            Line::from(""),
1032            Line::from(Span::styled(
1033                "  ╭──────────────────────────────────────╮",
1034                Style::default().fg(Color::Rgb(60, 70, 90)),
1035            )),
1036            Line::from(Span::styled(
1037                "  │                                      │",
1038                Style::default().fg(Color::Rgb(60, 70, 90)),
1039            )),
1040            Line::from(vec![
1041                Span::styled("  │     ", Style::default().fg(Color::Rgb(60, 70, 90))),
1042                Span::styled(
1043                    "Hi! What can I help you?  ",
1044                    Style::default().fg(Color::Rgb(120, 140, 180)),
1045                ),
1046                Span::styled("     │", Style::default().fg(Color::Rgb(60, 70, 90))),
1047            ]),
1048            Line::from(Span::styled(
1049                "  │                                      │",
1050                Style::default().fg(Color::Rgb(60, 70, 90)),
1051            )),
1052            Line::from(Span::styled(
1053                "  │     Type a message, press Enter      │",
1054                Style::default().fg(Color::Rgb(80, 90, 110)),
1055            )),
1056            Line::from(Span::styled(
1057                "  │                                      │",
1058                Style::default().fg(Color::Rgb(60, 70, 90)),
1059            )),
1060            Line::from(Span::styled(
1061                "  ╰──────────────────────────────────────╯",
1062                Style::default().fg(Color::Rgb(60, 70, 90)),
1063            )),
1064        ];
1065        let empty = Paragraph::new(welcome_lines).block(block);
1066        f.render_widget(empty, area);
1067        return;
1068    }
1069
1070    // 内部可用宽度(减去边框和左右各1的 padding)
1071    let inner_width = area.width.saturating_sub(4) as usize;
1072    // 消息内容最大宽度为可用宽度的 75%
1073    let bubble_max_width = (inner_width * 75 / 100).max(20);
1074
1075    // 计算缓存 key:消息数 + 最后一条消息长度 + 流式内容长度 + is_loading + 气泡宽度 + 浏览模式索引
1076    let msg_count = app.session.messages.len();
1077    let last_msg_len = app
1078        .session
1079        .messages
1080        .last()
1081        .map(|m| m.content.len())
1082        .unwrap_or(0);
1083    let streaming_len = app.streaming_content.lock().unwrap().len();
1084    let current_browse_index = if app.mode == ChatMode::Browse {
1085        Some(app.browse_msg_index)
1086    } else {
1087        None
1088    };
1089    let cache_hit = if let Some(ref cache) = app.msg_lines_cache {
1090        cache.msg_count == msg_count
1091            && cache.last_msg_len == last_msg_len
1092            && cache.streaming_len == streaming_len
1093            && cache.is_loading == app.is_loading
1094            && cache.bubble_max_width == bubble_max_width
1095            && cache.browse_index == current_browse_index
1096    } else {
1097        false
1098    };
1099
1100    if !cache_hit {
1101        // 缓存未命中,增量构建渲染行
1102        let old_cache = app.msg_lines_cache.take();
1103        let (new_lines, new_msg_start_lines, new_per_msg, new_stable_lines, new_stable_offset) =
1104            build_message_lines_incremental(app, inner_width, bubble_max_width, old_cache.as_ref());
1105        app.msg_lines_cache = Some(MsgLinesCache {
1106            msg_count,
1107            last_msg_len,
1108            streaming_len,
1109            is_loading: app.is_loading,
1110            bubble_max_width,
1111            browse_index: current_browse_index,
1112            lines: new_lines,
1113            msg_start_lines: new_msg_start_lines,
1114            per_msg_lines: new_per_msg,
1115            streaming_stable_lines: new_stable_lines,
1116            streaming_stable_offset: new_stable_offset,
1117        });
1118    }
1119
1120    // 从缓存中借用 lines(零拷贝)
1121    let cached = app.msg_lines_cache.as_ref().unwrap();
1122    let all_lines = &cached.lines;
1123    let total_lines = all_lines.len() as u16;
1124
1125    // 渲染边框
1126    f.render_widget(block, area);
1127
1128    // 计算内部区域(去掉边框)
1129    let inner = area.inner(ratatui::layout::Margin {
1130        vertical: 1,
1131        horizontal: 1,
1132    });
1133    let visible_height = inner.height;
1134    let max_scroll = total_lines.saturating_sub(visible_height);
1135
1136    // 自动滚动到底部(非浏览模式下)
1137    if app.mode != ChatMode::Browse {
1138        if app.scroll_offset == u16::MAX || app.scroll_offset > max_scroll {
1139            app.scroll_offset = max_scroll;
1140            // 已经在底部,恢复自动滚动
1141            app.auto_scroll = true;
1142        }
1143    } else {
1144        // 浏览模式:自动滚动到选中消息的位置
1145        if let Some(target_line) = cached
1146            .msg_start_lines
1147            .iter()
1148            .find(|(idx, _)| *idx == app.browse_msg_index)
1149            .map(|(_, line)| *line as u16)
1150        {
1151            // 确保选中消息在可视区域内
1152            if target_line < app.scroll_offset {
1153                app.scroll_offset = target_line;
1154            } else if target_line >= app.scroll_offset + visible_height {
1155                app.scroll_offset = target_line.saturating_sub(visible_height / 3);
1156            }
1157            // 限制滚动范围
1158            if app.scroll_offset > max_scroll {
1159                app.scroll_offset = max_scroll;
1160            }
1161        }
1162    }
1163
1164    // 填充内部背景色(避免空白行没有背景)
1165    let bg_fill = Block::default().style(Style::default().bg(Color::Rgb(22, 22, 30)));
1166    f.render_widget(bg_fill, inner);
1167
1168    // 只渲染可见区域的行(逐行借用缓存,clone 单行开销极小)
1169    let start = app.scroll_offset as usize;
1170    let end = (start + visible_height as usize).min(all_lines.len());
1171    for (i, line_idx) in (start..end).enumerate() {
1172        let line = &all_lines[line_idx];
1173        let y = inner.y + i as u16;
1174        let line_area = Rect::new(inner.x, y, inner.width, 1);
1175        // 使用 Paragraph 渲染单行(clone 单行开销很小)
1176        let p = Paragraph::new(line.clone());
1177        f.render_widget(p, line_area);
1178    }
1179}
1180
1181/// 查找流式内容中最后一个安全的段落边界(双换行),
1182/// 但要排除代码块内部的双换行(未闭合的 ``` 之后的内容不能拆分)。
1183fn find_stable_boundary(content: &str) -> usize {
1184    // 统计 ``` 出现次数,奇数说明有未闭合的代码块
1185    let mut fence_count = 0usize;
1186    let mut last_safe_boundary = 0usize;
1187    let mut i = 0;
1188    let bytes = content.as_bytes();
1189    while i < bytes.len() {
1190        // 检测 ``` 围栏
1191        if i + 2 < bytes.len() && bytes[i] == b'`' && bytes[i + 1] == b'`' && bytes[i + 2] == b'`' {
1192            fence_count += 1;
1193            i += 3;
1194            // 跳过同行剩余内容(语言标识等)
1195            while i < bytes.len() && bytes[i] != b'\n' {
1196                i += 1;
1197            }
1198            continue;
1199        }
1200        // 检测 \n\n 段落边界
1201        if i + 1 < bytes.len() && bytes[i] == b'\n' && bytes[i + 1] == b'\n' {
1202            // 只有在代码块外才算安全边界
1203            if fence_count % 2 == 0 {
1204                last_safe_boundary = i + 2; // 指向下一段的起始位置
1205            }
1206            i += 2;
1207            continue;
1208        }
1209        i += 1;
1210    }
1211    last_safe_boundary
1212}
1213
1214/// 增量构建所有消息的渲染行(P0 + P1 优化版本)
1215/// - P0:按消息粒度缓存,历史消息内容未变时直接复用渲染行
1216/// - P1:流式消息增量段落渲染,只重新解析最后一个不完整段落
1217/// 返回 (渲染行列表, 消息起始行号映射, 按消息缓存, 流式稳定行缓存, 流式稳定偏移)
1218fn build_message_lines_incremental(
1219    app: &ChatApp,
1220    inner_width: usize,
1221    bubble_max_width: usize,
1222    old_cache: Option<&MsgLinesCache>,
1223) -> (
1224    Vec<Line<'static>>,
1225    Vec<(usize, usize)>,
1226    Vec<PerMsgCache>,
1227    Vec<Line<'static>>,
1228    usize,
1229) {
1230    struct RenderMsg {
1231        role: String,
1232        content: String,
1233        msg_index: Option<usize>,
1234    }
1235    let mut render_msgs: Vec<RenderMsg> = app
1236        .session
1237        .messages
1238        .iter()
1239        .enumerate()
1240        .map(|(i, m)| RenderMsg {
1241            role: m.role.clone(),
1242            content: m.content.clone(),
1243            msg_index: Some(i),
1244        })
1245        .collect();
1246
1247    // 如果正在流式接收,添加一条临时的 assistant 消息
1248    let streaming_content_str = if app.is_loading {
1249        let streaming = app.streaming_content.lock().unwrap().clone();
1250        if !streaming.is_empty() {
1251            render_msgs.push(RenderMsg {
1252                role: "assistant".to_string(),
1253                content: streaming.clone(),
1254                msg_index: None,
1255            });
1256            Some(streaming)
1257        } else {
1258            render_msgs.push(RenderMsg {
1259                role: "assistant".to_string(),
1260                content: "◍".to_string(),
1261                msg_index: None,
1262            });
1263            None
1264        }
1265    } else {
1266        None
1267    };
1268
1269    let is_browse_mode = app.mode == ChatMode::Browse;
1270    let mut lines: Vec<Line> = Vec::new();
1271    let mut msg_start_lines: Vec<(usize, usize)> = Vec::new();
1272    let mut per_msg_cache: Vec<PerMsgCache> = Vec::new();
1273
1274    // 判断旧缓存中的 per_msg_lines 是否可以复用(bubble_max_width 相同且浏览模式状态一致)
1275    let can_reuse_per_msg = old_cache
1276        .map(|c| c.bubble_max_width == bubble_max_width)
1277        .unwrap_or(false);
1278
1279    for msg in &render_msgs {
1280        let is_selected = is_browse_mode
1281            && msg.msg_index.is_some()
1282            && msg.msg_index.unwrap() == app.browse_msg_index;
1283
1284        // 记录消息起始行号
1285        if let Some(idx) = msg.msg_index {
1286            msg_start_lines.push((idx, lines.len()));
1287        }
1288
1289        // P0 优化:对于有 msg_index 的历史消息,尝试复用旧缓存
1290        if let Some(idx) = msg.msg_index {
1291            if can_reuse_per_msg {
1292                if let Some(old_c) = old_cache {
1293                    // 查找旧缓存中同索引的消息
1294                    if let Some(old_per) = old_c.per_msg_lines.iter().find(|p| p.msg_index == idx) {
1295                        // 内容长度相同 → 消息内容未变,且浏览选中状态一致
1296                        let old_was_selected = old_c.browse_index == Some(idx);
1297                        if old_per.content_len == msg.content.len()
1298                            && old_was_selected == is_selected
1299                        {
1300                            // 直接复用旧缓存的渲染行
1301                            lines.extend(old_per.lines.iter().cloned());
1302                            per_msg_cache.push(PerMsgCache {
1303                                content_len: old_per.content_len,
1304                                lines: old_per.lines.clone(),
1305                                msg_index: idx,
1306                            });
1307                            continue;
1308                        }
1309                    }
1310                }
1311            }
1312        }
1313
1314        // 缓存未命中 / 流式消息 → 重新渲染
1315        let msg_lines_start = lines.len();
1316        match msg.role.as_str() {
1317            "user" => {
1318                render_user_msg(
1319                    &msg.content,
1320                    is_selected,
1321                    inner_width,
1322                    bubble_max_width,
1323                    &mut lines,
1324                );
1325            }
1326            "assistant" => {
1327                if msg.msg_index.is_none() {
1328                    // 流式消息:P1 增量段落渲染(在后面单独处理)
1329                    // 这里先跳过,后面统一处理
1330                    // 先标记位置
1331                } else {
1332                    // 已完成的 assistant 消息:完整 Markdown 渲染
1333                    render_assistant_msg(&msg.content, is_selected, bubble_max_width, &mut lines);
1334                }
1335            }
1336            "system" => {
1337                lines.push(Line::from(""));
1338                let wrapped = wrap_text(&msg.content, inner_width.saturating_sub(8));
1339                for wl in wrapped {
1340                    lines.push(Line::from(Span::styled(
1341                        format!("    {}  {}", "sys", wl),
1342                        Style::default().fg(Color::Rgb(100, 100, 120)),
1343                    )));
1344                }
1345            }
1346            _ => {}
1347        }
1348
1349        // 流式消息的渲染在 assistant 分支中被跳过了,这里处理
1350        if msg.role == "assistant" && msg.msg_index.is_none() {
1351            // P1 增量段落渲染
1352            let bubble_bg = Color::Rgb(38, 38, 52);
1353            let pad_left_w = 3usize;
1354            let pad_right_w = 3usize;
1355            let md_content_w = bubble_max_width.saturating_sub(pad_left_w + pad_right_w);
1356            let bubble_total_w = bubble_max_width;
1357
1358            // AI 标签
1359            lines.push(Line::from(""));
1360            lines.push(Line::from(Span::styled(
1361                "  AI",
1362                Style::default()
1363                    .fg(Color::Rgb(120, 220, 160))
1364                    .add_modifier(Modifier::BOLD),
1365            )));
1366
1367            // 上边距
1368            lines.push(Line::from(vec![Span::styled(
1369                " ".repeat(bubble_total_w),
1370                Style::default().bg(bubble_bg),
1371            )]));
1372
1373            // 增量段落渲染:取旧缓存中的 stable_lines 和 stable_offset
1374            let (mut stable_lines, mut stable_offset) = if let Some(old_c) = old_cache {
1375                if old_c.bubble_max_width == bubble_max_width {
1376                    (
1377                        old_c.streaming_stable_lines.clone(),
1378                        old_c.streaming_stable_offset,
1379                    )
1380                } else {
1381                    (Vec::new(), 0)
1382                }
1383            } else {
1384                (Vec::new(), 0)
1385            };
1386
1387            let content = &msg.content;
1388            // 找到当前内容中最后一个安全的段落边界
1389            let boundary = find_stable_boundary(content);
1390
1391            // 如果有新的完整段落超过了上次缓存的偏移
1392            if boundary > stable_offset {
1393                // 增量解析:从上次偏移到新边界的新完成段落
1394                let new_stable_text = &content[stable_offset..boundary];
1395                let new_md_lines = markdown_to_lines(new_stable_text, md_content_w + 2);
1396                // 将新段落的渲染行包装成气泡样式并追加到 stable_lines
1397                for md_line in new_md_lines {
1398                    let bubble_line = wrap_md_line_in_bubble(
1399                        md_line,
1400                        bubble_bg,
1401                        pad_left_w,
1402                        pad_right_w,
1403                        bubble_total_w,
1404                    );
1405                    stable_lines.push(bubble_line);
1406                }
1407                stable_offset = boundary;
1408            }
1409
1410            // 追加已缓存的稳定段落行
1411            lines.extend(stable_lines.iter().cloned());
1412
1413            // 只对最后一个不完整段落做全量 Markdown 解析
1414            let tail = &content[boundary..];
1415            if !tail.is_empty() {
1416                let tail_md_lines = markdown_to_lines(tail, md_content_w + 2);
1417                for md_line in tail_md_lines {
1418                    let bubble_line = wrap_md_line_in_bubble(
1419                        md_line,
1420                        bubble_bg,
1421                        pad_left_w,
1422                        pad_right_w,
1423                        bubble_total_w,
1424                    );
1425                    lines.push(bubble_line);
1426                }
1427            }
1428
1429            // 下边距
1430            lines.push(Line::from(vec![Span::styled(
1431                " ".repeat(bubble_total_w),
1432                Style::default().bg(bubble_bg),
1433            )]));
1434
1435            // 记录最终的 stable 状态用于返回
1436            // (在函数末尾统一返回)
1437            // 先用局部变量暂存
1438            let _ = (stable_lines.clone(), stable_offset);
1439
1440            // 构建末尾留白和返回值时统一处理
1441        } else if let Some(idx) = msg.msg_index {
1442            // 缓存此历史消息的渲染行
1443            let msg_lines_end = lines.len();
1444            let this_msg_lines: Vec<Line<'static>> = lines[msg_lines_start..msg_lines_end].to_vec();
1445            per_msg_cache.push(PerMsgCache {
1446                content_len: msg.content.len(),
1447                lines: this_msg_lines,
1448                msg_index: idx,
1449            });
1450        }
1451    }
1452
1453    // 末尾留白
1454    lines.push(Line::from(""));
1455
1456    // 计算最终的流式稳定缓存
1457    let (final_stable_lines, final_stable_offset) = if let Some(ref sc) = streaming_content_str {
1458        let boundary = find_stable_boundary(sc);
1459        let bubble_bg = Color::Rgb(38, 38, 52);
1460        let pad_left_w = 3usize;
1461        let pad_right_w = 3usize;
1462        let md_content_w = bubble_max_width.saturating_sub(pad_left_w + pad_right_w);
1463        let bubble_total_w = bubble_max_width;
1464
1465        let (mut s_lines, s_offset) = if let Some(old_c) = old_cache {
1466            if old_c.bubble_max_width == bubble_max_width {
1467                (
1468                    old_c.streaming_stable_lines.clone(),
1469                    old_c.streaming_stable_offset,
1470                )
1471            } else {
1472                (Vec::new(), 0)
1473            }
1474        } else {
1475            (Vec::new(), 0)
1476        };
1477
1478        if boundary > s_offset {
1479            let new_text = &sc[s_offset..boundary];
1480            let new_md_lines = markdown_to_lines(new_text, md_content_w + 2);
1481            for md_line in new_md_lines {
1482                let bubble_line = wrap_md_line_in_bubble(
1483                    md_line,
1484                    bubble_bg,
1485                    pad_left_w,
1486                    pad_right_w,
1487                    bubble_total_w,
1488                );
1489                s_lines.push(bubble_line);
1490            }
1491        }
1492        (s_lines, boundary)
1493    } else {
1494        (Vec::new(), 0)
1495    };
1496
1497    (
1498        lines,
1499        msg_start_lines,
1500        per_msg_cache,
1501        final_stable_lines,
1502        final_stable_offset,
1503    )
1504}
1505
1506/// 将一行 Markdown 渲染结果包装成气泡样式行(左右内边距 + 背景色 + 填充到统一宽度)
1507fn wrap_md_line_in_bubble(
1508    md_line: Line<'static>,
1509    bubble_bg: Color,
1510    pad_left_w: usize,
1511    pad_right_w: usize,
1512    bubble_total_w: usize,
1513) -> Line<'static> {
1514    let pad_left = " ".repeat(pad_left_w);
1515    let pad_right = " ".repeat(pad_right_w);
1516    let mut styled_spans: Vec<Span> = Vec::new();
1517    styled_spans.push(Span::styled(pad_left, Style::default().bg(bubble_bg)));
1518    let target_content_w = bubble_total_w.saturating_sub(pad_left_w + pad_right_w);
1519    let mut content_w: usize = 0;
1520    for span in md_line.spans {
1521        let sw = display_width(&span.content);
1522        if content_w + sw > target_content_w {
1523            // 安全钳制:逐字符截断以适应目标宽度
1524            let remaining = target_content_w.saturating_sub(content_w);
1525            if remaining > 0 {
1526                let mut truncated = String::new();
1527                let mut tw = 0;
1528                for ch in span.content.chars() {
1529                    let cw = char_width(ch);
1530                    if tw + cw > remaining {
1531                        break;
1532                    }
1533                    truncated.push(ch);
1534                    tw += cw;
1535                }
1536                if !truncated.is_empty() {
1537                    content_w += tw;
1538                    let merged_style = span.style.bg(bubble_bg);
1539                    styled_spans.push(Span::styled(truncated, merged_style));
1540                }
1541            }
1542            // 跳过后续 span(已溢出)
1543            break;
1544        }
1545        content_w += sw;
1546        let merged_style = span.style.bg(bubble_bg);
1547        styled_spans.push(Span::styled(span.content.to_string(), merged_style));
1548    }
1549    let fill = target_content_w.saturating_sub(content_w);
1550    if fill > 0 {
1551        styled_spans.push(Span::styled(
1552            " ".repeat(fill),
1553            Style::default().bg(bubble_bg),
1554        ));
1555    }
1556    styled_spans.push(Span::styled(pad_right, Style::default().bg(bubble_bg)));
1557    Line::from(styled_spans)
1558}
1559
1560/// 渲染用户消息(提取为独立函数,供增量构建使用)
1561fn render_user_msg(
1562    content: &str,
1563    is_selected: bool,
1564    inner_width: usize,
1565    bubble_max_width: usize,
1566    lines: &mut Vec<Line<'static>>,
1567) {
1568    lines.push(Line::from(""));
1569    let label = if is_selected { "▶ You " } else { "You " };
1570    let pad = inner_width.saturating_sub(display_width(label) + 2);
1571    lines.push(Line::from(vec![
1572        Span::raw(" ".repeat(pad)),
1573        Span::styled(
1574            label,
1575            Style::default()
1576                .fg(if is_selected {
1577                    Color::Rgb(255, 200, 80)
1578                } else {
1579                    Color::Rgb(100, 160, 255)
1580                })
1581                .add_modifier(Modifier::BOLD),
1582        ),
1583    ]));
1584    let user_bg = if is_selected {
1585        Color::Rgb(55, 85, 140)
1586    } else {
1587        Color::Rgb(40, 70, 120)
1588    };
1589    let user_pad_lr = 3usize;
1590    let user_content_w = bubble_max_width.saturating_sub(user_pad_lr * 2);
1591    let mut all_wrapped_lines: Vec<String> = Vec::new();
1592    for content_line in content.lines() {
1593        let wrapped = wrap_text(content_line, user_content_w);
1594        all_wrapped_lines.extend(wrapped);
1595    }
1596    if all_wrapped_lines.is_empty() {
1597        all_wrapped_lines.push(String::new());
1598    }
1599    let actual_content_w = all_wrapped_lines
1600        .iter()
1601        .map(|l| display_width(l))
1602        .max()
1603        .unwrap_or(0);
1604    let actual_bubble_w = (actual_content_w + user_pad_lr * 2)
1605        .min(bubble_max_width)
1606        .max(user_pad_lr * 2 + 1);
1607    let actual_inner_content_w = actual_bubble_w.saturating_sub(user_pad_lr * 2);
1608    // 上边距
1609    {
1610        let bubble_text = " ".repeat(actual_bubble_w);
1611        let pad = inner_width.saturating_sub(actual_bubble_w);
1612        lines.push(Line::from(vec![
1613            Span::raw(" ".repeat(pad)),
1614            Span::styled(bubble_text, Style::default().bg(user_bg)),
1615        ]));
1616    }
1617    for wl in &all_wrapped_lines {
1618        let wl_width = display_width(wl);
1619        let fill = actual_inner_content_w.saturating_sub(wl_width);
1620        let text = format!(
1621            "{}{}{}{}",
1622            " ".repeat(user_pad_lr),
1623            wl,
1624            " ".repeat(fill),
1625            " ".repeat(user_pad_lr),
1626        );
1627        let text_width = display_width(&text);
1628        let pad = inner_width.saturating_sub(text_width);
1629        lines.push(Line::from(vec![
1630            Span::raw(" ".repeat(pad)),
1631            Span::styled(text, Style::default().fg(Color::White).bg(user_bg)),
1632        ]));
1633    }
1634    // 下边距
1635    {
1636        let bubble_text = " ".repeat(actual_bubble_w);
1637        let pad = inner_width.saturating_sub(actual_bubble_w);
1638        lines.push(Line::from(vec![
1639            Span::raw(" ".repeat(pad)),
1640            Span::styled(bubble_text, Style::default().bg(user_bg)),
1641        ]));
1642    }
1643}
1644
1645/// 渲染 AI 助手消息(提取为独立函数,供增量构建使用)
1646fn render_assistant_msg(
1647    content: &str,
1648    is_selected: bool,
1649    bubble_max_width: usize,
1650    lines: &mut Vec<Line<'static>>,
1651) {
1652    lines.push(Line::from(""));
1653    let ai_label = if is_selected { "  ▶ AI" } else { "  AI" };
1654    lines.push(Line::from(Span::styled(
1655        ai_label,
1656        Style::default()
1657            .fg(if is_selected {
1658                Color::Rgb(255, 200, 80)
1659            } else {
1660                Color::Rgb(120, 220, 160)
1661            })
1662            .add_modifier(Modifier::BOLD),
1663    )));
1664    let bubble_bg = if is_selected {
1665        Color::Rgb(48, 48, 68)
1666    } else {
1667        Color::Rgb(38, 38, 52)
1668    };
1669    let pad_left_w = 3usize;
1670    let pad_right_w = 3usize;
1671    let md_content_w = bubble_max_width.saturating_sub(pad_left_w + pad_right_w);
1672    let md_lines = markdown_to_lines(content, md_content_w + 2);
1673    let bubble_total_w = bubble_max_width;
1674    // 上边距
1675    lines.push(Line::from(vec![Span::styled(
1676        " ".repeat(bubble_total_w),
1677        Style::default().bg(bubble_bg),
1678    )]));
1679    for md_line in md_lines {
1680        let bubble_line =
1681            wrap_md_line_in_bubble(md_line, bubble_bg, pad_left_w, pad_right_w, bubble_total_w);
1682        lines.push(bubble_line);
1683    }
1684    // 下边距
1685    lines.push(Line::from(vec![Span::styled(
1686        " ".repeat(bubble_total_w),
1687        Style::default().bg(bubble_bg),
1688    )]));
1689}
1690
1691/// 将 Markdown 文本解析为 ratatui 的 Line 列表
1692/// 支持:标题(去掉 # 标记)、加粗、斜体、行内代码、代码块(语法高亮)、列表、分隔线
1693/// content_width:内容区可用宽度(不含外层 "  " 缩进和右侧填充)
1694fn markdown_to_lines(md: &str, max_width: usize) -> Vec<Line<'static>> {
1695    use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
1696
1697    // 内容区宽度 = max_width - 2(左侧 "  " 缩进由外层负责)
1698    let content_width = max_width.saturating_sub(2);
1699
1700    let options = Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TABLES;
1701    let parser = Parser::new_ext(md, options);
1702
1703    let mut lines: Vec<Line<'static>> = Vec::new();
1704    let mut current_spans: Vec<Span<'static>> = Vec::new();
1705    let mut style_stack: Vec<Style> = vec![Style::default().fg(Color::Rgb(220, 220, 230))];
1706    let mut in_code_block = false;
1707    let mut code_block_content = String::new();
1708    let mut code_block_lang = String::new();
1709    let mut list_depth: usize = 0;
1710    let mut ordered_index: Option<u64> = None;
1711    let mut heading_level: Option<u8> = None;
1712    // 跟踪是否在引用块中
1713    let mut in_blockquote = false;
1714    // 表格相关状态
1715    let mut in_table = false;
1716    let mut table_rows: Vec<Vec<String>> = Vec::new(); // 收集所有行(含表头)
1717    let mut current_row: Vec<String> = Vec::new();
1718    let mut current_cell = String::new();
1719    let mut table_alignments: Vec<pulldown_cmark::Alignment> = Vec::new();
1720
1721    let base_style = Style::default().fg(Color::Rgb(220, 220, 230));
1722
1723    let flush_line = |current_spans: &mut Vec<Span<'static>>, lines: &mut Vec<Line<'static>>| {
1724        if !current_spans.is_empty() {
1725            lines.push(Line::from(current_spans.drain(..).collect::<Vec<_>>()));
1726        }
1727    };
1728
1729    for event in parser {
1730        match event {
1731            Event::Start(Tag::Heading { level, .. }) => {
1732                flush_line(&mut current_spans, &mut lines);
1733                heading_level = Some(level as u8);
1734                if !lines.is_empty() {
1735                    lines.push(Line::from(""));
1736                }
1737                // 根据标题级别使用不同的颜色
1738                let heading_style = match level as u8 {
1739                    1 => Style::default()
1740                        .fg(Color::Rgb(100, 180, 255))
1741                        .add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
1742                    2 => Style::default()
1743                        .fg(Color::Rgb(130, 190, 255))
1744                        .add_modifier(Modifier::BOLD),
1745                    3 => Style::default()
1746                        .fg(Color::Rgb(160, 200, 255))
1747                        .add_modifier(Modifier::BOLD),
1748                    _ => Style::default()
1749                        .fg(Color::Rgb(180, 210, 255))
1750                        .add_modifier(Modifier::BOLD),
1751                };
1752                style_stack.push(heading_style);
1753            }
1754            Event::End(TagEnd::Heading(level)) => {
1755                flush_line(&mut current_spans, &mut lines);
1756                // h1/h2 下方加分隔线(完整填充 content_width)
1757                if (level as u8) <= 2 {
1758                    let sep_char = if (level as u8) == 1 { "━" } else { "─" };
1759                    lines.push(Line::from(Span::styled(
1760                        sep_char.repeat(content_width),
1761                        Style::default().fg(Color::Rgb(60, 70, 100)),
1762                    )));
1763                }
1764                style_stack.pop();
1765                heading_level = None;
1766            }
1767            Event::Start(Tag::Strong) => {
1768                let current = *style_stack.last().unwrap_or(&base_style);
1769                style_stack.push(
1770                    current
1771                        .add_modifier(Modifier::BOLD)
1772                        .fg(Color::Rgb(130, 220, 255)),
1773                );
1774            }
1775            Event::End(TagEnd::Strong) => {
1776                style_stack.pop();
1777            }
1778            Event::Start(Tag::Emphasis) => {
1779                let current = *style_stack.last().unwrap_or(&base_style);
1780                style_stack.push(current.add_modifier(Modifier::ITALIC));
1781            }
1782            Event::End(TagEnd::Emphasis) => {
1783                style_stack.pop();
1784            }
1785            Event::Start(Tag::Strikethrough) => {
1786                let current = *style_stack.last().unwrap_or(&base_style);
1787                style_stack.push(current.add_modifier(Modifier::CROSSED_OUT));
1788            }
1789            Event::End(TagEnd::Strikethrough) => {
1790                style_stack.pop();
1791            }
1792            Event::Start(Tag::CodeBlock(kind)) => {
1793                flush_line(&mut current_spans, &mut lines);
1794                in_code_block = true;
1795                code_block_content.clear();
1796                code_block_lang = match kind {
1797                    CodeBlockKind::Fenced(lang) => lang.to_string(),
1798                    CodeBlockKind::Indented => String::new(),
1799                };
1800                // 代码块上方边框(自适应宽度)
1801                let label = if code_block_lang.is_empty() {
1802                    " code ".to_string()
1803                } else {
1804                    format!(" {} ", code_block_lang)
1805                };
1806                let label_w = display_width(&label);
1807                let border_fill = content_width.saturating_sub(2 + label_w);
1808                let top_border = format!("┌─{}{}", label, "─".repeat(border_fill));
1809                lines.push(Line::from(Span::styled(
1810                    top_border,
1811                    Style::default().fg(Color::Rgb(80, 90, 110)),
1812                )));
1813            }
1814            Event::End(TagEnd::CodeBlock) => {
1815                // 渲染代码块内容(带语法高亮)
1816                let code_inner_w = content_width.saturating_sub(4); // "│ " 前缀 + 右侧 " │" 后缀占4
1817                for code_line in code_block_content.lines() {
1818                    let wrapped = wrap_text(code_line, code_inner_w);
1819                    for wl in wrapped {
1820                        let highlighted = highlight_code_line(&wl, &code_block_lang);
1821                        let text_w: usize =
1822                            highlighted.iter().map(|s| display_width(&s.content)).sum();
1823                        let fill = code_inner_w.saturating_sub(text_w);
1824                        let mut spans_vec = Vec::new();
1825                        spans_vec.push(Span::styled(
1826                            "│ ",
1827                            Style::default().fg(Color::Rgb(80, 90, 110)),
1828                        ));
1829                        for hs in highlighted {
1830                            spans_vec.push(Span::styled(
1831                                hs.content.to_string(),
1832                                hs.style.bg(Color::Rgb(30, 30, 42)),
1833                            ));
1834                        }
1835                        spans_vec.push(Span::styled(
1836                            format!("{} │", " ".repeat(fill)),
1837                            Style::default()
1838                                .fg(Color::Rgb(80, 90, 110))
1839                                .bg(Color::Rgb(30, 30, 42)),
1840                        ));
1841                        lines.push(Line::from(spans_vec));
1842                    }
1843                }
1844                let bottom_border = format!("└{}", "─".repeat(content_width.saturating_sub(1)));
1845                lines.push(Line::from(Span::styled(
1846                    bottom_border,
1847                    Style::default().fg(Color::Rgb(80, 90, 110)),
1848                )));
1849                in_code_block = false;
1850                code_block_content.clear();
1851                code_block_lang.clear();
1852            }
1853            Event::Code(text) => {
1854                if in_table {
1855                    // 表格中的行内代码也收集到当前单元格
1856                    current_cell.push('`');
1857                    current_cell.push_str(&text);
1858                    current_cell.push('`');
1859                } else {
1860                    // 行内代码:检查行宽,放不下则先换行
1861                    let code_str = format!(" {} ", text);
1862                    let code_w = display_width(&code_str);
1863                    let effective_prefix_w = if in_blockquote { 2 } else { 0 };
1864                    let full_line_w = content_width.saturating_sub(effective_prefix_w);
1865                    let existing_w: usize = current_spans
1866                        .iter()
1867                        .map(|s| display_width(&s.content))
1868                        .sum();
1869                    if existing_w + code_w > full_line_w && !current_spans.is_empty() {
1870                        flush_line(&mut current_spans, &mut lines);
1871                        if in_blockquote {
1872                            current_spans.push(Span::styled(
1873                                "| ".to_string(),
1874                                Style::default().fg(Color::Rgb(80, 100, 140)),
1875                            ));
1876                        }
1877                    }
1878                    current_spans.push(Span::styled(
1879                        code_str,
1880                        Style::default()
1881                            .fg(Color::Rgb(230, 190, 120))
1882                            .bg(Color::Rgb(45, 45, 60)),
1883                    ));
1884                }
1885            }
1886            Event::Start(Tag::List(start)) => {
1887                flush_line(&mut current_spans, &mut lines);
1888                list_depth += 1;
1889                ordered_index = start;
1890            }
1891            Event::End(TagEnd::List(_)) => {
1892                flush_line(&mut current_spans, &mut lines);
1893                list_depth = list_depth.saturating_sub(1);
1894                ordered_index = None;
1895            }
1896            Event::Start(Tag::Item) => {
1897                flush_line(&mut current_spans, &mut lines);
1898                let indent = "  ".repeat(list_depth);
1899                let bullet = if let Some(ref mut idx) = ordered_index {
1900                    let s = format!("{}{}. ", indent, idx);
1901                    *idx += 1;
1902                    s
1903                } else {
1904                    format!("{}• ", indent)
1905                };
1906                current_spans.push(Span::styled(
1907                    bullet,
1908                    Style::default().fg(Color::Rgb(100, 160, 255)),
1909                ));
1910            }
1911            Event::End(TagEnd::Item) => {
1912                flush_line(&mut current_spans, &mut lines);
1913            }
1914            Event::Start(Tag::Paragraph) => {
1915                if !lines.is_empty() && !in_code_block && heading_level.is_none() {
1916                    let last_empty = lines.last().map(|l| l.spans.is_empty()).unwrap_or(false);
1917                    if !last_empty {
1918                        lines.push(Line::from(""));
1919                    }
1920                }
1921            }
1922            Event::End(TagEnd::Paragraph) => {
1923                flush_line(&mut current_spans, &mut lines);
1924            }
1925            Event::Start(Tag::BlockQuote(_)) => {
1926                flush_line(&mut current_spans, &mut lines);
1927                in_blockquote = true;
1928                style_stack.push(Style::default().fg(Color::Rgb(150, 160, 180)));
1929            }
1930            Event::End(TagEnd::BlockQuote(_)) => {
1931                flush_line(&mut current_spans, &mut lines);
1932                in_blockquote = false;
1933                style_stack.pop();
1934            }
1935            Event::Text(text) => {
1936                if in_code_block {
1937                    code_block_content.push_str(&text);
1938                } else if in_table {
1939                    // 表格中的文本收集到当前单元格
1940                    current_cell.push_str(&text);
1941                } else {
1942                    let style = *style_stack.last().unwrap_or(&base_style);
1943                    let text_str = text.to_string();
1944
1945                    // 标题:添加可视化符号前缀代替 # 标记
1946                    if let Some(level) = heading_level {
1947                        let (prefix, prefix_style) = match level {
1948                            1 => (
1949                                ">> ",
1950                                Style::default()
1951                                    .fg(Color::Rgb(100, 180, 255))
1952                                    .add_modifier(Modifier::BOLD),
1953                            ),
1954                            2 => (
1955                                ">> ",
1956                                Style::default()
1957                                    .fg(Color::Rgb(130, 190, 255))
1958                                    .add_modifier(Modifier::BOLD),
1959                            ),
1960                            3 => (
1961                                "> ",
1962                                Style::default()
1963                                    .fg(Color::Rgb(160, 200, 255))
1964                                    .add_modifier(Modifier::BOLD),
1965                            ),
1966                            _ => (
1967                                "> ",
1968                                Style::default()
1969                                    .fg(Color::Rgb(180, 210, 255))
1970                                    .add_modifier(Modifier::BOLD),
1971                            ),
1972                        };
1973                        current_spans.push(Span::styled(prefix.to_string(), prefix_style));
1974                        heading_level = None; // 只加一次前缀
1975                    }
1976
1977                    // 引用块:加左侧竖线
1978                    let effective_prefix_w = if in_blockquote { 2 } else { 0 }; // "| " 宽度
1979                    let full_line_w = content_width.saturating_sub(effective_prefix_w);
1980
1981                    // 计算 current_spans 已有的显示宽度
1982                    let existing_w: usize = current_spans
1983                        .iter()
1984                        .map(|s| display_width(&s.content))
1985                        .sum();
1986
1987                    // 剩余可用宽度
1988                    let wrap_w = full_line_w.saturating_sub(existing_w);
1989
1990                    // 如果剩余宽度太小(不足整行的 1/4),先 flush 当前行再换行,
1991                    // 避免文字被挤到极窄的空间导致竖排
1992                    let min_useful_w = full_line_w / 4;
1993                    let wrap_w = if wrap_w < min_useful_w.max(4) && !current_spans.is_empty() {
1994                        flush_line(&mut current_spans, &mut lines);
1995                        if in_blockquote {
1996                            current_spans.push(Span::styled(
1997                                "| ".to_string(),
1998                                Style::default().fg(Color::Rgb(80, 100, 140)),
1999                            ));
2000                        }
2001                        // flush 后使用完整行宽
2002                        full_line_w
2003                    } else {
2004                        wrap_w
2005                    };
2006
2007                    for (i, line) in text_str.split('\n').enumerate() {
2008                        if i > 0 {
2009                            flush_line(&mut current_spans, &mut lines);
2010                            if in_blockquote {
2011                                current_spans.push(Span::styled(
2012                                    "| ".to_string(),
2013                                    Style::default().fg(Color::Rgb(80, 100, 140)),
2014                                ));
2015                            }
2016                        }
2017                        if !line.is_empty() {
2018                            // 第一行使用减去已有 span 宽度的 wrap_w,后续行使用完整 content_width
2019                            let effective_wrap = if i == 0 {
2020                                wrap_w
2021                            } else {
2022                                content_width.saturating_sub(effective_prefix_w)
2023                            };
2024                            let wrapped = wrap_text(line, effective_wrap);
2025                            for (j, wl) in wrapped.iter().enumerate() {
2026                                if j > 0 {
2027                                    flush_line(&mut current_spans, &mut lines);
2028                                    if in_blockquote {
2029                                        current_spans.push(Span::styled(
2030                                            "| ".to_string(),
2031                                            Style::default().fg(Color::Rgb(80, 100, 140)),
2032                                        ));
2033                                    }
2034                                }
2035                                current_spans.push(Span::styled(wl.clone(), style));
2036                            }
2037                        }
2038                    }
2039                }
2040            }
2041            Event::SoftBreak => {
2042                if in_table {
2043                    current_cell.push(' ');
2044                } else {
2045                    current_spans.push(Span::raw(" "));
2046                }
2047            }
2048            Event::HardBreak => {
2049                if in_table {
2050                    current_cell.push(' ');
2051                } else {
2052                    flush_line(&mut current_spans, &mut lines);
2053                }
2054            }
2055            Event::Rule => {
2056                flush_line(&mut current_spans, &mut lines);
2057                lines.push(Line::from(Span::styled(
2058                    "─".repeat(content_width),
2059                    Style::default().fg(Color::Rgb(70, 75, 90)),
2060                )));
2061            }
2062            // ===== 表格支持 =====
2063            Event::Start(Tag::Table(alignments)) => {
2064                flush_line(&mut current_spans, &mut lines);
2065                in_table = true;
2066                table_rows.clear();
2067                table_alignments = alignments;
2068            }
2069            Event::End(TagEnd::Table) => {
2070                // 表格结束:计算列宽,渲染完整表格
2071                flush_line(&mut current_spans, &mut lines);
2072                in_table = false;
2073
2074                if !table_rows.is_empty() {
2075                    let num_cols = table_rows.iter().map(|r| r.len()).max().unwrap_or(0);
2076                    if num_cols > 0 {
2077                        // 计算每列最大宽度
2078                        let mut col_widths: Vec<usize> = vec![0; num_cols];
2079                        for row in &table_rows {
2080                            for (i, cell) in row.iter().enumerate() {
2081                                let w = display_width(cell);
2082                                if w > col_widths[i] {
2083                                    col_widths[i] = w;
2084                                }
2085                            }
2086                        }
2087
2088                        // 限制总宽度不超过 content_width,等比缩放
2089                        let sep_w = num_cols + 1; // 竖线占用
2090                        let pad_w = num_cols * 2; // 每列左右各1空格
2091                        let avail = content_width.saturating_sub(sep_w + pad_w);
2092                        // 单列最大宽度限制(避免一列过宽)
2093                        let max_col_w = avail * 2 / 3;
2094                        for cw in col_widths.iter_mut() {
2095                            if *cw > max_col_w {
2096                                *cw = max_col_w;
2097                            }
2098                        }
2099                        let total_col_w: usize = col_widths.iter().sum();
2100                        if total_col_w > avail && total_col_w > 0 {
2101                            // 等比缩放
2102                            let mut remaining = avail;
2103                            for (i, cw) in col_widths.iter_mut().enumerate() {
2104                                if i == num_cols - 1 {
2105                                    // 最后一列取剩余宽度,避免取整误差
2106                                    *cw = remaining.max(1);
2107                                } else {
2108                                    *cw = ((*cw) * avail / total_col_w).max(1);
2109                                    remaining = remaining.saturating_sub(*cw);
2110                                }
2111                            }
2112                        }
2113
2114                        let table_style = Style::default().fg(Color::Rgb(180, 180, 200));
2115                        let header_style = Style::default()
2116                            .fg(Color::Rgb(120, 180, 255))
2117                            .add_modifier(Modifier::BOLD);
2118                        let border_style = Style::default().fg(Color::Rgb(60, 70, 100));
2119
2120                        // 表格行的实际字符宽度(用空格字符计算,不依赖 Box Drawing 字符宽度)
2121                        // table_row_w = 竖线数(num_cols+1) + 每列(cw+2) = sep_w + pad_w + total_col_w
2122                        let total_col_w_final: usize = col_widths.iter().sum();
2123                        let table_row_w = sep_w + pad_w + total_col_w_final;
2124                        // 表格行右侧需要补充的空格数,使整行宽度等于 content_width
2125                        let table_right_pad = content_width.saturating_sub(table_row_w);
2126
2127                        // 渲染顶边框 ┌─┬─┐
2128                        let mut top = String::from("┌");
2129                        for (i, cw) in col_widths.iter().enumerate() {
2130                            top.push_str(&"─".repeat(cw + 2));
2131                            if i < num_cols - 1 {
2132                                top.push('┬');
2133                            }
2134                        }
2135                        top.push('┐');
2136                        // 补充右侧空格,使宽度对齐 content_width
2137                        let mut top_spans = vec![Span::styled(top, border_style)];
2138                        if table_right_pad > 0 {
2139                            top_spans.push(Span::raw(" ".repeat(table_right_pad)));
2140                        }
2141                        lines.push(Line::from(top_spans));
2142
2143                        for (row_idx, row) in table_rows.iter().enumerate() {
2144                            // 数据行 │ cell │ cell │
2145                            let mut row_spans: Vec<Span> = Vec::new();
2146                            row_spans.push(Span::styled("│", border_style));
2147                            for (i, cw) in col_widths.iter().enumerate() {
2148                                let cell_text = row.get(i).map(|s| s.as_str()).unwrap_or("");
2149                                let cell_w = display_width(cell_text);
2150                                let text = if cell_w > *cw {
2151                                    // 截断
2152                                    let mut t = String::new();
2153                                    let mut w = 0;
2154                                    for ch in cell_text.chars() {
2155                                        let chw = char_width(ch);
2156                                        if w + chw > *cw {
2157                                            break;
2158                                        }
2159                                        t.push(ch);
2160                                        w += chw;
2161                                    }
2162                                    let fill = cw.saturating_sub(w);
2163                                    format!(" {}{} ", t, " ".repeat(fill))
2164                                } else {
2165                                    // 根据对齐方式填充
2166                                    let fill = cw.saturating_sub(cell_w);
2167                                    let align = table_alignments
2168                                        .get(i)
2169                                        .copied()
2170                                        .unwrap_or(pulldown_cmark::Alignment::None);
2171                                    match align {
2172                                        pulldown_cmark::Alignment::Center => {
2173                                            let left = fill / 2;
2174                                            let right = fill - left;
2175                                            format!(
2176                                                " {}{}{} ",
2177                                                " ".repeat(left),
2178                                                cell_text,
2179                                                " ".repeat(right)
2180                                            )
2181                                        }
2182                                        pulldown_cmark::Alignment::Right => {
2183                                            format!(" {}{} ", " ".repeat(fill), cell_text)
2184                                        }
2185                                        _ => {
2186                                            format!(" {}{} ", cell_text, " ".repeat(fill))
2187                                        }
2188                                    }
2189                                };
2190                                let style = if row_idx == 0 {
2191                                    header_style
2192                                } else {
2193                                    table_style
2194                                };
2195                                row_spans.push(Span::styled(text, style));
2196                                row_spans.push(Span::styled("│", border_style));
2197                            }
2198                            // 补充右侧空格,使宽度对齐 content_width
2199                            if table_right_pad > 0 {
2200                                row_spans.push(Span::raw(" ".repeat(table_right_pad)));
2201                            }
2202                            lines.push(Line::from(row_spans));
2203
2204                            // 表头行后加分隔线 ├─┼─┤
2205                            if row_idx == 0 {
2206                                let mut sep = String::from("├");
2207                                for (i, cw) in col_widths.iter().enumerate() {
2208                                    sep.push_str(&"─".repeat(cw + 2));
2209                                    if i < num_cols - 1 {
2210                                        sep.push('┼');
2211                                    }
2212                                }
2213                                sep.push('┤');
2214                                let mut sep_spans = vec![Span::styled(sep, border_style)];
2215                                if table_right_pad > 0 {
2216                                    sep_spans.push(Span::raw(" ".repeat(table_right_pad)));
2217                                }
2218                                lines.push(Line::from(sep_spans));
2219                            }
2220                        }
2221
2222                        // 底边框 └─┴─┘
2223                        let mut bottom = String::from("└");
2224                        for (i, cw) in col_widths.iter().enumerate() {
2225                            bottom.push_str(&"─".repeat(cw + 2));
2226                            if i < num_cols - 1 {
2227                                bottom.push('┴');
2228                            }
2229                        }
2230                        bottom.push('┘');
2231                        let mut bottom_spans = vec![Span::styled(bottom, border_style)];
2232                        if table_right_pad > 0 {
2233                            bottom_spans.push(Span::raw(" ".repeat(table_right_pad)));
2234                        }
2235                        lines.push(Line::from(bottom_spans));
2236                    }
2237                }
2238                table_rows.clear();
2239                table_alignments.clear();
2240            }
2241            Event::Start(Tag::TableHead) => {
2242                current_row.clear();
2243            }
2244            Event::End(TagEnd::TableHead) => {
2245                table_rows.push(current_row.clone());
2246                current_row.clear();
2247            }
2248            Event::Start(Tag::TableRow) => {
2249                current_row.clear();
2250            }
2251            Event::End(TagEnd::TableRow) => {
2252                table_rows.push(current_row.clone());
2253                current_row.clear();
2254            }
2255            Event::Start(Tag::TableCell) => {
2256                current_cell.clear();
2257            }
2258            Event::End(TagEnd::TableCell) => {
2259                current_row.push(current_cell.clone());
2260                current_cell.clear();
2261            }
2262            _ => {}
2263        }
2264    }
2265
2266    // 刷新最后一行
2267    if !current_spans.is_empty() {
2268        lines.push(Line::from(current_spans));
2269    }
2270
2271    // 如果解析结果为空,至少返回原始文本
2272    if lines.is_empty() {
2273        let wrapped = wrap_text(md, content_width);
2274        for wl in wrapped {
2275            lines.push(Line::from(Span::styled(wl, base_style)));
2276        }
2277    }
2278
2279    lines
2280}
2281
2282/// 简单的代码语法高亮(无需外部依赖)
2283/// 根据语言类型对常见关键字、字符串、注释、数字进行着色
2284fn highlight_code_line<'a>(line: &'a str, lang: &str) -> Vec<Span<'static>> {
2285    let lang_lower = lang.to_lowercase();
2286    let keywords: &[&str] = match lang_lower.as_str() {
2287        "rust" | "rs" => &[
2288            "fn", "let", "mut", "pub", "use", "mod", "struct", "enum", "impl", "trait", "for",
2289            "while", "loop", "if", "else", "match", "return", "self", "Self", "where", "async",
2290            "await", "move", "ref", "type", "const", "static", "crate", "super", "as", "in",
2291            "true", "false", "Some", "None", "Ok", "Err",
2292        ],
2293        "python" | "py" => &[
2294            "def", "class", "return", "if", "elif", "else", "for", "while", "import", "from", "as",
2295            "with", "try", "except", "finally", "raise", "pass", "break", "continue", "yield",
2296            "lambda", "and", "or", "not", "in", "is", "True", "False", "None", "global",
2297            "nonlocal", "assert", "del", "async", "await", "self", "print",
2298        ],
2299        "javascript" | "js" | "typescript" | "ts" | "jsx" | "tsx" => &[
2300            "function",
2301            "const",
2302            "let",
2303            "var",
2304            "return",
2305            "if",
2306            "else",
2307            "for",
2308            "while",
2309            "class",
2310            "new",
2311            "this",
2312            "import",
2313            "export",
2314            "from",
2315            "default",
2316            "async",
2317            "await",
2318            "try",
2319            "catch",
2320            "finally",
2321            "throw",
2322            "typeof",
2323            "instanceof",
2324            "true",
2325            "false",
2326            "null",
2327            "undefined",
2328            "of",
2329            "in",
2330            "switch",
2331            "case",
2332        ],
2333        "go" | "golang" => &[
2334            "func",
2335            "package",
2336            "import",
2337            "return",
2338            "if",
2339            "else",
2340            "for",
2341            "range",
2342            "struct",
2343            "interface",
2344            "type",
2345            "var",
2346            "const",
2347            "defer",
2348            "go",
2349            "chan",
2350            "select",
2351            "case",
2352            "switch",
2353            "default",
2354            "break",
2355            "continue",
2356            "map",
2357            "true",
2358            "false",
2359            "nil",
2360            "make",
2361            "append",
2362            "len",
2363            "cap",
2364        ],
2365        "java" | "kotlin" | "kt" => &[
2366            "public",
2367            "private",
2368            "protected",
2369            "class",
2370            "interface",
2371            "extends",
2372            "implements",
2373            "return",
2374            "if",
2375            "else",
2376            "for",
2377            "while",
2378            "new",
2379            "this",
2380            "import",
2381            "package",
2382            "static",
2383            "final",
2384            "void",
2385            "int",
2386            "String",
2387            "boolean",
2388            "true",
2389            "false",
2390            "null",
2391            "try",
2392            "catch",
2393            "throw",
2394            "throws",
2395            "fun",
2396            "val",
2397            "var",
2398            "when",
2399            "object",
2400            "companion",
2401        ],
2402        "sh" | "bash" | "zsh" | "shell" => &[
2403            "if",
2404            "then",
2405            "else",
2406            "elif",
2407            "fi",
2408            "for",
2409            "while",
2410            "do",
2411            "done",
2412            "case",
2413            "esac",
2414            "function",
2415            "return",
2416            "exit",
2417            "echo",
2418            "export",
2419            "local",
2420            "readonly",
2421            "set",
2422            "unset",
2423            "shift",
2424            "source",
2425            "in",
2426            "true",
2427            "false",
2428            "read",
2429            "declare",
2430            "typeset",
2431            "trap",
2432            "eval",
2433            "exec",
2434            "test",
2435            "select",
2436            "until",
2437            "break",
2438            "continue",
2439            "printf",
2440            // Go 命令
2441            "go",
2442            "build",
2443            "run",
2444            "test",
2445            "fmt",
2446            "vet",
2447            "mod",
2448            "get",
2449            "install",
2450            "clean",
2451            "doc",
2452            "list",
2453            "version",
2454            "env",
2455            "generate",
2456            "tool",
2457            "proxy",
2458            "GOPATH",
2459            "GOROOT",
2460            "GOBIN",
2461            "GOMODCACHE",
2462            "GOPROXY",
2463            "GOSUMDB",
2464            // Cargo 命令
2465            "cargo",
2466            "new",
2467            "init",
2468            "add",
2469            "remove",
2470            "update",
2471            "check",
2472            "clippy",
2473            "rustfmt",
2474            "rustc",
2475            "rustup",
2476            "publish",
2477            "install",
2478            "uninstall",
2479            "search",
2480            "tree",
2481            "locate_project",
2482            "metadata",
2483            "audit",
2484            "watch",
2485            "expand",
2486        ],
2487        "c" | "cpp" | "c++" | "h" | "hpp" => &[
2488            "int",
2489            "char",
2490            "float",
2491            "double",
2492            "void",
2493            "long",
2494            "short",
2495            "unsigned",
2496            "signed",
2497            "const",
2498            "static",
2499            "extern",
2500            "struct",
2501            "union",
2502            "enum",
2503            "typedef",
2504            "sizeof",
2505            "return",
2506            "if",
2507            "else",
2508            "for",
2509            "while",
2510            "do",
2511            "switch",
2512            "case",
2513            "break",
2514            "continue",
2515            "default",
2516            "goto",
2517            "auto",
2518            "register",
2519            "volatile",
2520            "class",
2521            "public",
2522            "private",
2523            "protected",
2524            "virtual",
2525            "override",
2526            "template",
2527            "namespace",
2528            "using",
2529            "new",
2530            "delete",
2531            "try",
2532            "catch",
2533            "throw",
2534            "nullptr",
2535            "true",
2536            "false",
2537            "this",
2538            "include",
2539            "define",
2540            "ifdef",
2541            "ifndef",
2542            "endif",
2543        ],
2544        "sql" => &[
2545            "SELECT",
2546            "FROM",
2547            "WHERE",
2548            "INSERT",
2549            "UPDATE",
2550            "DELETE",
2551            "CREATE",
2552            "DROP",
2553            "ALTER",
2554            "TABLE",
2555            "INDEX",
2556            "INTO",
2557            "VALUES",
2558            "SET",
2559            "AND",
2560            "OR",
2561            "NOT",
2562            "NULL",
2563            "JOIN",
2564            "LEFT",
2565            "RIGHT",
2566            "INNER",
2567            "OUTER",
2568            "ON",
2569            "GROUP",
2570            "BY",
2571            "ORDER",
2572            "ASC",
2573            "DESC",
2574            "HAVING",
2575            "LIMIT",
2576            "OFFSET",
2577            "UNION",
2578            "AS",
2579            "DISTINCT",
2580            "COUNT",
2581            "SUM",
2582            "AVG",
2583            "MIN",
2584            "MAX",
2585            "LIKE",
2586            "IN",
2587            "BETWEEN",
2588            "EXISTS",
2589            "CASE",
2590            "WHEN",
2591            "THEN",
2592            "ELSE",
2593            "END",
2594            "BEGIN",
2595            "COMMIT",
2596            "ROLLBACK",
2597            "PRIMARY",
2598            "KEY",
2599            "FOREIGN",
2600            "REFERENCES",
2601            "select",
2602            "from",
2603            "where",
2604            "insert",
2605            "update",
2606            "delete",
2607            "create",
2608            "drop",
2609            "alter",
2610            "table",
2611            "index",
2612            "into",
2613            "values",
2614            "set",
2615            "and",
2616            "or",
2617            "not",
2618            "null",
2619            "join",
2620            "left",
2621            "right",
2622            "inner",
2623            "outer",
2624            "on",
2625            "group",
2626            "by",
2627            "order",
2628            "asc",
2629            "desc",
2630            "having",
2631            "limit",
2632            "offset",
2633            "union",
2634            "as",
2635            "distinct",
2636            "count",
2637            "sum",
2638            "avg",
2639            "min",
2640            "max",
2641            "like",
2642            "in",
2643            "between",
2644            "exists",
2645            "case",
2646            "when",
2647            "then",
2648            "else",
2649            "end",
2650            "begin",
2651            "commit",
2652            "rollback",
2653            "primary",
2654            "key",
2655            "foreign",
2656            "references",
2657        ],
2658        "yaml" | "yml" => &["true", "false", "null", "yes", "no", "on", "off"],
2659        "toml" => &[
2660            "true",
2661            "false",
2662            "true",
2663            "false",
2664            // Cargo.toml 常用
2665            "name",
2666            "version",
2667            "edition",
2668            "authors",
2669            "dependencies",
2670            "dev-dependencies",
2671            "build-dependencies",
2672            "features",
2673            "workspace",
2674            "members",
2675            "exclude",
2676            "include",
2677            "path",
2678            "git",
2679            "branch",
2680            "tag",
2681            "rev",
2682            "package",
2683            "lib",
2684            "bin",
2685            "example",
2686            "test",
2687            "bench",
2688            "doc",
2689            "profile",
2690            "release",
2691            "debug",
2692            "opt-level",
2693            "lto",
2694            "codegen-units",
2695            "panic",
2696            "strip",
2697            "default",
2698            "features",
2699            "optional",
2700            // 常见配置项
2701            "repository",
2702            "homepage",
2703            "documentation",
2704            "license",
2705            "license-file",
2706            "keywords",
2707            "categories",
2708            "readme",
2709            "description",
2710            "resolver",
2711        ],
2712        "css" | "scss" | "less" => &[
2713            "color",
2714            "background",
2715            "border",
2716            "margin",
2717            "padding",
2718            "display",
2719            "position",
2720            "width",
2721            "height",
2722            "font",
2723            "text",
2724            "flex",
2725            "grid",
2726            "align",
2727            "justify",
2728            "important",
2729            "none",
2730            "auto",
2731            "inherit",
2732            "initial",
2733            "unset",
2734        ],
2735        "dockerfile" | "docker" => &[
2736            "FROM",
2737            "RUN",
2738            "CMD",
2739            "LABEL",
2740            "EXPOSE",
2741            "ENV",
2742            "ADD",
2743            "COPY",
2744            "ENTRYPOINT",
2745            "VOLUME",
2746            "USER",
2747            "WORKDIR",
2748            "ARG",
2749            "ONBUILD",
2750            "STOPSIGNAL",
2751            "HEALTHCHECK",
2752            "SHELL",
2753            "AS",
2754        ],
2755        "ruby" | "rb" => &[
2756            "def", "end", "class", "module", "if", "elsif", "else", "unless", "while", "until",
2757            "for", "do", "begin", "rescue", "ensure", "raise", "return", "yield", "require",
2758            "include", "attr", "self", "true", "false", "nil", "puts", "print",
2759        ],
2760        _ => &[
2761            "fn", "function", "def", "class", "return", "if", "else", "for", "while", "import",
2762            "export", "const", "let", "var", "true", "false", "null", "nil", "None", "self",
2763            "this",
2764        ],
2765    };
2766
2767    let comment_prefix = match lang_lower.as_str() {
2768        "python" | "py" | "sh" | "bash" | "zsh" | "shell" | "ruby" | "rb" | "yaml" | "yml"
2769        | "toml" | "dockerfile" | "docker" => "#",
2770        "sql" => "--",
2771        "css" | "scss" | "less" => "/*",
2772        _ => "//",
2773    };
2774
2775    // 默认代码颜色
2776    let code_style = Style::default().fg(Color::Rgb(200, 200, 210));
2777    // 关键字颜色
2778    let kw_style = Style::default().fg(Color::Rgb(198, 120, 221));
2779    // 字符串颜色
2780    let str_style = Style::default().fg(Color::Rgb(152, 195, 121));
2781    // 注释颜色
2782    let comment_style = Style::default()
2783        .fg(Color::Rgb(92, 99, 112))
2784        .add_modifier(Modifier::ITALIC);
2785    // 数字颜色
2786    let num_style = Style::default().fg(Color::Rgb(209, 154, 102));
2787    // 类型/大写开头标识符
2788    let type_style = Style::default().fg(Color::Rgb(229, 192, 123));
2789
2790    let trimmed = line.trim_start();
2791
2792    // 注释行
2793    if trimmed.starts_with(comment_prefix) {
2794        return vec![Span::styled(line.to_string(), comment_style)];
2795    }
2796
2797    // 逐词解析
2798    let mut spans = Vec::new();
2799    let mut chars = line.chars().peekable();
2800    let mut buf = String::new();
2801
2802    while let Some(&ch) = chars.peek() {
2803        // 字符串
2804        if ch == '"' || ch == '\'' || ch == '`' {
2805            // 先刷新 buf
2806            if !buf.is_empty() {
2807                spans.extend(colorize_tokens(
2808                    &buf, keywords, code_style, kw_style, num_style, type_style,
2809                ));
2810                buf.clear();
2811            }
2812            let quote = ch;
2813            let mut s = String::new();
2814            s.push(ch);
2815            chars.next();
2816            while let Some(&c) = chars.peek() {
2817                s.push(c);
2818                chars.next();
2819                if c == quote && !s.ends_with("\\\\") {
2820                    break;
2821                }
2822            }
2823            spans.push(Span::styled(s, str_style));
2824            continue;
2825        }
2826        // Shell 变量 ($VAR, ${VAR}, $1 等)
2827        if ch == '$'
2828            && matches!(
2829                lang_lower.as_str(),
2830                "sh" | "bash" | "zsh" | "shell" | "dockerfile" | "docker"
2831            )
2832        {
2833            if !buf.is_empty() {
2834                spans.extend(colorize_tokens(
2835                    &buf, keywords, code_style, kw_style, num_style, type_style,
2836                ));
2837                buf.clear();
2838            }
2839            let var_style = Style::default().fg(Color::Rgb(86, 182, 194));
2840            let mut var = String::new();
2841            var.push(ch);
2842            chars.next();
2843            if let Some(&next_ch) = chars.peek() {
2844                if next_ch == '{' {
2845                    // ${VAR}
2846                    var.push(next_ch);
2847                    chars.next();
2848                    while let Some(&c) = chars.peek() {
2849                        var.push(c);
2850                        chars.next();
2851                        if c == '}' {
2852                            break;
2853                        }
2854                    }
2855                } else if next_ch == '(' {
2856                    // $(cmd)
2857                    var.push(next_ch);
2858                    chars.next();
2859                    let mut depth = 1;
2860                    while let Some(&c) = chars.peek() {
2861                        var.push(c);
2862                        chars.next();
2863                        if c == '(' {
2864                            depth += 1;
2865                        }
2866                        if c == ')' {
2867                            depth -= 1;
2868                            if depth == 0 {
2869                                break;
2870                            }
2871                        }
2872                    }
2873                } else if next_ch.is_alphanumeric()
2874                    || next_ch == '_'
2875                    || next_ch == '@'
2876                    || next_ch == '#'
2877                    || next_ch == '?'
2878                    || next_ch == '!'
2879                {
2880                    // $VAR, $1, $@, $#, $? 等
2881                    while let Some(&c) = chars.peek() {
2882                        if c.is_alphanumeric() || c == '_' {
2883                            var.push(c);
2884                            chars.next();
2885                        } else {
2886                            break;
2887                        }
2888                    }
2889                }
2890            }
2891            spans.push(Span::styled(var, var_style));
2892            continue;
2893        }
2894        // 行内注释
2895        if ch == '/' || ch == '#' {
2896            let rest: String = chars.clone().collect();
2897            if rest.starts_with(comment_prefix) {
2898                if !buf.is_empty() {
2899                    spans.extend(colorize_tokens(
2900                        &buf, keywords, code_style, kw_style, num_style, type_style,
2901                    ));
2902                    buf.clear();
2903                }
2904                spans.push(Span::styled(rest, comment_style));
2905                break;
2906            }
2907        }
2908        buf.push(ch);
2909        chars.next();
2910    }
2911
2912    if !buf.is_empty() {
2913        spans.extend(colorize_tokens(
2914            &buf, keywords, code_style, kw_style, num_style, type_style,
2915        ));
2916    }
2917
2918    if spans.is_empty() {
2919        spans.push(Span::styled(line.to_string(), code_style));
2920    }
2921
2922    spans
2923}
2924
2925/// 将文本按照 word boundary 拆分并对关键字、数字、类型名着色
2926fn colorize_tokens<'a>(
2927    text: &str,
2928    keywords: &[&str],
2929    default_style: Style,
2930    kw_style: Style,
2931    num_style: Style,
2932    type_style: Style,
2933) -> Vec<Span<'static>> {
2934    let mut spans = Vec::new();
2935    let mut current_word = String::new();
2936    let mut current_non_word = String::new();
2937
2938    for ch in text.chars() {
2939        if ch.is_alphanumeric() || ch == '_' {
2940            if !current_non_word.is_empty() {
2941                spans.push(Span::styled(current_non_word.clone(), default_style));
2942                current_non_word.clear();
2943            }
2944            current_word.push(ch);
2945        } else {
2946            if !current_word.is_empty() {
2947                let style = if keywords.contains(&current_word.as_str()) {
2948                    kw_style
2949                } else if current_word
2950                    .chars()
2951                    .next()
2952                    .map(|c| c.is_ascii_digit())
2953                    .unwrap_or(false)
2954                {
2955                    num_style
2956                } else if current_word
2957                    .chars()
2958                    .next()
2959                    .map(|c| c.is_uppercase())
2960                    .unwrap_or(false)
2961                {
2962                    type_style
2963                } else {
2964                    default_style
2965                };
2966                spans.push(Span::styled(current_word.clone(), style));
2967                current_word.clear();
2968            }
2969            current_non_word.push(ch);
2970        }
2971    }
2972
2973    // 刷新剩余
2974    if !current_non_word.is_empty() {
2975        spans.push(Span::styled(current_non_word, default_style));
2976    }
2977    if !current_word.is_empty() {
2978        let style = if keywords.contains(&current_word.as_str()) {
2979            kw_style
2980        } else if current_word
2981            .chars()
2982            .next()
2983            .map(|c| c.is_ascii_digit())
2984            .unwrap_or(false)
2985        {
2986            num_style
2987        } else if current_word
2988            .chars()
2989            .next()
2990            .map(|c| c.is_uppercase())
2991            .unwrap_or(false)
2992        {
2993            type_style
2994        } else {
2995            default_style
2996        };
2997        spans.push(Span::styled(current_word, style));
2998    }
2999
3000    spans
3001}
3002
3003/// 简单文本自动换行
3004fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
3005    // 最小宽度保证至少能放下一个字符(中文字符宽度2),避免无限循环或不截断
3006    let max_width = max_width.max(2);
3007    let mut result = Vec::new();
3008    let mut current_line = String::new();
3009    let mut current_width = 0;
3010
3011    for ch in text.chars() {
3012        let ch_width = char_width(ch);
3013        if current_width + ch_width > max_width && !current_line.is_empty() {
3014            result.push(current_line.clone());
3015            current_line.clear();
3016            current_width = 0;
3017        }
3018        current_line.push(ch);
3019        current_width += ch_width;
3020    }
3021    if !current_line.is_empty() {
3022        result.push(current_line);
3023    }
3024    if result.is_empty() {
3025        result.push(String::new());
3026    }
3027    result
3028}
3029
3030/// 计算字符串的显示宽度(使用 unicode-width crate,比手动范围匹配更准确)
3031fn display_width(s: &str) -> usize {
3032    use unicode_width::UnicodeWidthStr;
3033    UnicodeWidthStr::width(s)
3034}
3035
3036/// 计算单个字符的显示宽度(使用 unicode-width crate)
3037fn char_width(c: char) -> usize {
3038    use unicode_width::UnicodeWidthChar;
3039    UnicodeWidthChar::width(c).unwrap_or(0)
3040}
3041
3042/// 绘制输入区
3043fn draw_input(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
3044    // 输入区可用宽度(减去边框2 + prompt 4)
3045    let usable_width = area.width.saturating_sub(2 + 4) as usize;
3046
3047    let chars: Vec<char> = app.input.chars().collect();
3048
3049    // 计算光标之前文本的显示宽度,决定是否需要水平滚动
3050    let before_all: String = chars[..app.cursor_pos].iter().collect();
3051    let before_width = display_width(&before_all);
3052
3053    // 如果光标超出可视范围,从光标附近开始显示
3054    let scroll_offset_chars = if before_width >= usable_width {
3055        // 往回找到一个合适的起始字符位置
3056        let target_width = before_width.saturating_sub(usable_width / 2);
3057        let mut w = 0;
3058        let mut skip = 0;
3059        for (i, &ch) in chars.iter().enumerate() {
3060            if w >= target_width {
3061                skip = i;
3062                break;
3063            }
3064            w += char_width(ch);
3065        }
3066        skip
3067    } else {
3068        0
3069    };
3070
3071    // 截取可见部分的字符
3072    let visible_chars = &chars[scroll_offset_chars..];
3073    let cursor_in_visible = app.cursor_pos - scroll_offset_chars;
3074
3075    let before: String = visible_chars[..cursor_in_visible].iter().collect();
3076    let cursor_ch = if cursor_in_visible < visible_chars.len() {
3077        visible_chars[cursor_in_visible].to_string()
3078    } else {
3079        " ".to_string()
3080    };
3081    let after: String = if cursor_in_visible < visible_chars.len() {
3082        visible_chars[cursor_in_visible + 1..].iter().collect()
3083    } else {
3084        String::new()
3085    };
3086
3087    let prompt_style = if app.is_loading {
3088        Style::default().fg(Color::Rgb(255, 200, 80))
3089    } else {
3090        Style::default().fg(Color::Rgb(100, 200, 130))
3091    };
3092    let prompt_text = if app.is_loading { " .. " } else { " >  " };
3093
3094    // 构建多行输入显示(手动换行)
3095    let full_visible = format!("{}{}{}", before, cursor_ch, after);
3096    let inner_height = area.height.saturating_sub(2) as usize; // 减去边框
3097    let wrapped_lines = wrap_text(&full_visible, usable_width);
3098
3099    // 找到光标所在的行索引
3100    let before_len = before.chars().count();
3101    let cursor_len = cursor_ch.chars().count();
3102    let cursor_global_pos = before_len; // 光标在全部可见字符中的位置
3103    let mut cursor_line_idx: usize = 0;
3104    {
3105        let mut cumulative = 0usize;
3106        for (li, wl) in wrapped_lines.iter().enumerate() {
3107            let line_char_count = wl.chars().count();
3108            if cumulative + line_char_count > cursor_global_pos {
3109                cursor_line_idx = li;
3110                break;
3111            }
3112            cumulative += line_char_count;
3113            cursor_line_idx = li; // 光标恰好在最后一行末尾
3114        }
3115    }
3116
3117    // 计算行滚动:确保光标所在行在可见区域内
3118    let line_scroll = if wrapped_lines.len() <= inner_height {
3119        0
3120    } else if cursor_line_idx < inner_height {
3121        0
3122    } else {
3123        // 让光标行显示在可见区域的最后一行
3124        cursor_line_idx.saturating_sub(inner_height - 1)
3125    };
3126
3127    // 构建带光标高亮的行
3128    let mut display_lines: Vec<Line> = Vec::new();
3129    let mut char_offset: usize = 0;
3130    // 跳过滚动行的字符数
3131    for wl in wrapped_lines.iter().take(line_scroll) {
3132        char_offset += wl.chars().count();
3133    }
3134
3135    for (_line_idx, wl) in wrapped_lines
3136        .iter()
3137        .skip(line_scroll)
3138        .enumerate()
3139        .take(inner_height.max(1))
3140    {
3141        let mut spans: Vec<Span> = Vec::new();
3142        if _line_idx == 0 && line_scroll == 0 {
3143            spans.push(Span::styled(prompt_text, prompt_style));
3144        } else {
3145            spans.push(Span::styled("    ", Style::default())); // 对齐 prompt
3146        }
3147
3148        // 对该行的每个字符分配样式
3149        let line_chars: Vec<char> = wl.chars().collect();
3150        let mut seg_start = 0;
3151        for (ci, &ch) in line_chars.iter().enumerate() {
3152            let global_idx = char_offset + ci;
3153            let is_cursor = global_idx >= before_len && global_idx < before_len + cursor_len;
3154
3155            if is_cursor {
3156                // 先把 cursor 前的部分输出
3157                if ci > seg_start {
3158                    let seg: String = line_chars[seg_start..ci].iter().collect();
3159                    spans.push(Span::styled(seg, Style::default().fg(Color::White)));
3160                }
3161                spans.push(Span::styled(
3162                    ch.to_string(),
3163                    Style::default()
3164                        .fg(Color::Rgb(22, 22, 30))
3165                        .bg(Color::Rgb(200, 210, 240)),
3166                ));
3167                seg_start = ci + 1;
3168            }
3169        }
3170        // 输出剩余部分
3171        if seg_start < line_chars.len() {
3172            let seg: String = line_chars[seg_start..].iter().collect();
3173            spans.push(Span::styled(seg, Style::default().fg(Color::White)));
3174        }
3175
3176        char_offset += line_chars.len();
3177        display_lines.push(Line::from(spans));
3178    }
3179
3180    if display_lines.is_empty() {
3181        display_lines.push(Line::from(vec![
3182            Span::styled(prompt_text, prompt_style),
3183            Span::styled(
3184                " ",
3185                Style::default()
3186                    .fg(Color::Rgb(22, 22, 30))
3187                    .bg(Color::Rgb(200, 210, 240)),
3188            ),
3189        ]));
3190    }
3191
3192    let input_widget = Paragraph::new(display_lines).block(
3193        Block::default()
3194            .borders(Borders::ALL)
3195            .border_type(ratatui::widgets::BorderType::Rounded)
3196            .border_style(if app.is_loading {
3197                Style::default().fg(Color::Rgb(120, 100, 50))
3198            } else {
3199                Style::default().fg(Color::Rgb(60, 100, 80))
3200            })
3201            .title(Span::styled(
3202                " 输入消息 ",
3203                Style::default().fg(Color::Rgb(140, 140, 170)),
3204            ))
3205            .style(Style::default().bg(Color::Rgb(26, 26, 38))),
3206    );
3207
3208    f.render_widget(input_widget, area);
3209
3210    // 设置终端光标位置,确保中文输入法 IME 候选窗口在正确位置
3211    // 计算光标在渲染后的坐标
3212    if !app.is_loading {
3213        let prompt_w: u16 = 4; // prompt 宽度
3214        let border_left: u16 = 1; // 左边框
3215
3216        // 光标在当前显示行中的列偏移
3217        let cursor_col_in_line = {
3218            let mut col = 0usize;
3219            let mut char_count = 0usize;
3220            // 跳过 line_scroll 之前的字符
3221            let mut skip_chars = 0usize;
3222            for wl in wrapped_lines.iter().take(line_scroll) {
3223                skip_chars += wl.chars().count();
3224            }
3225            // 找到光标在当前行的列
3226            for wl in wrapped_lines.iter().skip(line_scroll) {
3227                let line_len = wl.chars().count();
3228                if skip_chars + char_count + line_len > cursor_global_pos {
3229                    // 光标在这一行
3230                    let pos_in_line = cursor_global_pos - (skip_chars + char_count);
3231                    col = wl.chars().take(pos_in_line).map(|c| char_width(c)).sum();
3232                    break;
3233                }
3234                char_count += line_len;
3235            }
3236            col as u16
3237        };
3238
3239        // 光标在显示行中的行偏移
3240        let cursor_row_in_display = (cursor_line_idx - line_scroll) as u16;
3241
3242        let cursor_x = area.x + border_left + prompt_w + cursor_col_in_line;
3243        let cursor_y = area.y + 1 + cursor_row_in_display; // +1 跳过上边框
3244
3245        // 确保光标在区域内
3246        if cursor_x < area.x + area.width && cursor_y < area.y + area.height {
3247            f.set_cursor_position((cursor_x, cursor_y));
3248        }
3249    }
3250}
3251
3252/// 绘制底部操作提示栏(始终可见)
3253fn draw_hint_bar(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
3254    let hints = match app.mode {
3255        ChatMode::Chat => {
3256            vec![
3257                ("Enter", "发送"),
3258                ("↑↓", "滚动"),
3259                ("Ctrl+T", "切换模型"),
3260                ("Ctrl+L", "清空"),
3261                ("Ctrl+Y", "复制"),
3262                ("Ctrl+B", "浏览"),
3263                ("Ctrl+S", "流式切换"),
3264                ("Ctrl+E", "配置"),
3265                ("?/F1", "帮助"),
3266                ("Esc", "退出"),
3267            ]
3268        }
3269        ChatMode::SelectModel => {
3270            vec![("↑↓/jk", "移动"), ("Enter", "确认"), ("Esc", "取消")]
3271        }
3272        ChatMode::Browse => {
3273            vec![("↑↓", "选择消息"), ("y/Enter", "复制"), ("Esc", "返回")]
3274        }
3275        ChatMode::Help => {
3276            vec![("任意键", "返回")]
3277        }
3278        ChatMode::Config => {
3279            vec![
3280                ("↑↓", "切换字段"),
3281                ("Enter", "编辑"),
3282                ("Tab", "切换 Provider"),
3283                ("a", "新增"),
3284                ("d", "删除"),
3285                ("Esc", "保存返回"),
3286            ]
3287        }
3288    };
3289
3290    let mut spans: Vec<Span> = Vec::new();
3291    spans.push(Span::styled(" ", Style::default()));
3292    for (i, (key, desc)) in hints.iter().enumerate() {
3293        if i > 0 {
3294            spans.push(Span::styled(
3295                "  │  ",
3296                Style::default().fg(Color::Rgb(50, 50, 65)),
3297            ));
3298        }
3299        spans.push(Span::styled(
3300            format!(" {} ", key),
3301            Style::default()
3302                .fg(Color::Rgb(22, 22, 30))
3303                .bg(Color::Rgb(100, 110, 140)),
3304        ));
3305        spans.push(Span::styled(
3306            format!(" {}", desc),
3307            Style::default().fg(Color::Rgb(120, 120, 150)),
3308        ));
3309    }
3310
3311    let hint_bar =
3312        Paragraph::new(Line::from(spans)).style(Style::default().bg(Color::Rgb(22, 22, 30)));
3313    f.render_widget(hint_bar, area);
3314}
3315
3316/// 绘制 Toast 弹窗(右上角浮层)
3317fn draw_toast(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
3318    if let Some((ref msg, is_error, _)) = app.toast {
3319        let text_width = display_width(msg);
3320        // toast 宽度 = 文字宽度 + 左右 padding(各2) + emoji(2) + border(2)
3321        let toast_width = (text_width + 10).min(area.width as usize).max(16) as u16;
3322        let toast_height: u16 = 3;
3323
3324        // 定位到右上角
3325        let x = area.width.saturating_sub(toast_width + 1);
3326        let y: u16 = 1;
3327
3328        if x + toast_width <= area.width && y + toast_height <= area.height {
3329            let toast_area = Rect::new(x, y, toast_width, toast_height);
3330
3331            // 先清空区域背景
3332            let clear = Block::default().style(Style::default().bg(if is_error {
3333                Color::Rgb(60, 20, 20)
3334            } else {
3335                Color::Rgb(20, 50, 30)
3336            }));
3337            f.render_widget(clear, toast_area);
3338
3339            let (icon, border_color, text_color) = if is_error {
3340                ("❌", Color::Rgb(200, 70, 70), Color::Rgb(255, 130, 130))
3341            } else {
3342                ("✅", Color::Rgb(60, 160, 80), Color::Rgb(140, 230, 160))
3343            };
3344
3345            let toast_widget = Paragraph::new(Line::from(vec![
3346                Span::styled(format!(" {} ", icon), Style::default()),
3347                Span::styled(msg.as_str(), Style::default().fg(text_color)),
3348            ]))
3349            .block(
3350                Block::default()
3351                    .borders(Borders::ALL)
3352                    .border_type(ratatui::widgets::BorderType::Rounded)
3353                    .border_style(Style::default().fg(border_color))
3354                    .style(Style::default().bg(if is_error {
3355                        Color::Rgb(50, 18, 18)
3356                    } else {
3357                        Color::Rgb(18, 40, 25)
3358                    })),
3359            );
3360            f.render_widget(toast_widget, toast_area);
3361        }
3362    }
3363}
3364
3365/// 绘制模型选择界面
3366fn draw_model_selector(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
3367    let items: Vec<ListItem> = app
3368        .agent_config
3369        .providers
3370        .iter()
3371        .enumerate()
3372        .map(|(i, p)| {
3373            let is_active = i == app.agent_config.active_index;
3374            let marker = if is_active { " ● " } else { " ○ " };
3375            let style = if is_active {
3376                Style::default()
3377                    .fg(Color::Rgb(120, 220, 160))
3378                    .add_modifier(Modifier::BOLD)
3379            } else {
3380                Style::default().fg(Color::Rgb(180, 180, 200))
3381            };
3382            let detail = format!("{}{}  ({})", marker, p.name, p.model);
3383            ListItem::new(Line::from(Span::styled(detail, style)))
3384        })
3385        .collect();
3386
3387    let list = List::new(items)
3388        .block(
3389            Block::default()
3390                .borders(Borders::ALL)
3391                .border_type(ratatui::widgets::BorderType::Rounded)
3392                .border_style(Style::default().fg(Color::Rgb(180, 160, 80)))
3393                .title(Span::styled(
3394                    " 🔄 选择模型 ",
3395                    Style::default()
3396                        .fg(Color::Rgb(230, 210, 120))
3397                        .add_modifier(Modifier::BOLD),
3398                ))
3399                .style(Style::default().bg(Color::Rgb(28, 28, 40))),
3400        )
3401        .highlight_style(
3402            Style::default()
3403                .bg(Color::Rgb(50, 55, 80))
3404                .fg(Color::White)
3405                .add_modifier(Modifier::BOLD),
3406        )
3407        .highlight_symbol("  ▸ ");
3408
3409    f.render_stateful_widget(list, area, &mut app.model_list_state);
3410}
3411
3412/// 绘制帮助界面
3413fn draw_help(f: &mut ratatui::Frame, area: Rect) {
3414    let separator = Line::from(Span::styled(
3415        "  ─────────────────────────────────────────",
3416        Style::default().fg(Color::Rgb(50, 55, 70)),
3417    ));
3418
3419    let help_lines = vec![
3420        Line::from(""),
3421        Line::from(Span::styled(
3422            "  📖 快捷键帮助",
3423            Style::default()
3424                .fg(Color::Rgb(120, 180, 255))
3425                .add_modifier(Modifier::BOLD),
3426        )),
3427        Line::from(""),
3428        separator.clone(),
3429        Line::from(""),
3430        Line::from(vec![
3431            Span::styled(
3432                "  Enter        ",
3433                Style::default()
3434                    .fg(Color::Rgb(230, 210, 120))
3435                    .add_modifier(Modifier::BOLD),
3436            ),
3437            Span::styled("发送消息", Style::default().fg(Color::Rgb(200, 200, 220))),
3438        ]),
3439        Line::from(vec![
3440            Span::styled(
3441                "  ↑ / ↓        ",
3442                Style::default()
3443                    .fg(Color::Rgb(230, 210, 120))
3444                    .add_modifier(Modifier::BOLD),
3445            ),
3446            Span::styled(
3447                "滚动对话记录",
3448                Style::default().fg(Color::Rgb(200, 200, 220)),
3449            ),
3450        ]),
3451        Line::from(vec![
3452            Span::styled(
3453                "  ← / →        ",
3454                Style::default()
3455                    .fg(Color::Rgb(230, 210, 120))
3456                    .add_modifier(Modifier::BOLD),
3457            ),
3458            Span::styled(
3459                "移动输入光标",
3460                Style::default().fg(Color::Rgb(200, 200, 220)),
3461            ),
3462        ]),
3463        Line::from(vec![
3464            Span::styled(
3465                "  Ctrl+T       ",
3466                Style::default()
3467                    .fg(Color::Rgb(230, 210, 120))
3468                    .add_modifier(Modifier::BOLD),
3469            ),
3470            Span::styled("切换模型", Style::default().fg(Color::Rgb(200, 200, 220))),
3471        ]),
3472        Line::from(vec![
3473            Span::styled(
3474                "  Ctrl+L       ",
3475                Style::default()
3476                    .fg(Color::Rgb(230, 210, 120))
3477                    .add_modifier(Modifier::BOLD),
3478            ),
3479            Span::styled(
3480                "清空对话历史",
3481                Style::default().fg(Color::Rgb(200, 200, 220)),
3482            ),
3483        ]),
3484        Line::from(vec![
3485            Span::styled(
3486                "  Ctrl+Y       ",
3487                Style::default()
3488                    .fg(Color::Rgb(230, 210, 120))
3489                    .add_modifier(Modifier::BOLD),
3490            ),
3491            Span::styled(
3492                "复制最后一条 AI 回复",
3493                Style::default().fg(Color::Rgb(200, 200, 220)),
3494            ),
3495        ]),
3496        Line::from(vec![
3497            Span::styled(
3498                "  Ctrl+B       ",
3499                Style::default()
3500                    .fg(Color::Rgb(230, 210, 120))
3501                    .add_modifier(Modifier::BOLD),
3502            ),
3503            Span::styled(
3504                "浏览消息 (↑↓选择, y/Enter复制)",
3505                Style::default().fg(Color::Rgb(200, 200, 220)),
3506            ),
3507        ]),
3508        Line::from(vec![
3509            Span::styled(
3510                "  Ctrl+S       ",
3511                Style::default()
3512                    .fg(Color::Rgb(230, 210, 120))
3513                    .add_modifier(Modifier::BOLD),
3514            ),
3515            Span::styled(
3516                "切换流式/整体输出",
3517                Style::default().fg(Color::Rgb(200, 200, 220)),
3518            ),
3519        ]),
3520        Line::from(vec![
3521            Span::styled(
3522                "  Ctrl+E       ",
3523                Style::default()
3524                    .fg(Color::Rgb(230, 210, 120))
3525                    .add_modifier(Modifier::BOLD),
3526            ),
3527            Span::styled(
3528                "打开配置界面",
3529                Style::default().fg(Color::Rgb(200, 200, 220)),
3530            ),
3531        ]),
3532        Line::from(vec![
3533            Span::styled(
3534                "  Esc / Ctrl+C ",
3535                Style::default()
3536                    .fg(Color::Rgb(230, 210, 120))
3537                    .add_modifier(Modifier::BOLD),
3538            ),
3539            Span::styled("退出对话", Style::default().fg(Color::Rgb(200, 200, 220))),
3540        ]),
3541        Line::from(vec![
3542            Span::styled(
3543                "  ? / F1       ",
3544                Style::default()
3545                    .fg(Color::Rgb(230, 210, 120))
3546                    .add_modifier(Modifier::BOLD),
3547            ),
3548            Span::styled(
3549                "显示 / 关闭此帮助",
3550                Style::default().fg(Color::Rgb(200, 200, 220)),
3551            ),
3552        ]),
3553        Line::from(""),
3554        separator,
3555        Line::from(""),
3556        Line::from(Span::styled(
3557            "  📁 配置文件:",
3558            Style::default()
3559                .fg(Color::Rgb(120, 180, 255))
3560                .add_modifier(Modifier::BOLD),
3561        )),
3562        Line::from(Span::styled(
3563            format!("     {}", agent_config_path().display()),
3564            Style::default().fg(Color::Rgb(100, 100, 130)),
3565        )),
3566    ];
3567
3568    let help_block = Block::default()
3569        .borders(Borders::ALL)
3570        .border_type(ratatui::widgets::BorderType::Rounded)
3571        .border_style(Style::default().fg(Color::Rgb(80, 100, 140)))
3572        .title(Span::styled(
3573            " 帮助 (按任意键返回) ",
3574            Style::default().fg(Color::Rgb(140, 140, 170)),
3575        ))
3576        .style(Style::default().bg(Color::Rgb(24, 24, 34)));
3577    let help_widget = Paragraph::new(help_lines).block(help_block);
3578    f.render_widget(help_widget, area);
3579}
3580
3581/// 对话模式按键处理,返回 true 表示退出
3582fn handle_chat_mode(app: &mut ChatApp, key: KeyEvent) -> bool {
3583    // Ctrl+C 强制退出
3584    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
3585        return true;
3586    }
3587
3588    // Ctrl+T 切换模型(替代 Ctrl+M,因为 Ctrl+M 在终端中等于 Enter)
3589    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('t') {
3590        if !app.agent_config.providers.is_empty() {
3591            app.mode = ChatMode::SelectModel;
3592            app.model_list_state
3593                .select(Some(app.agent_config.active_index));
3594        }
3595        return false;
3596    }
3597
3598    // Ctrl+L 清空对话
3599    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('l') {
3600        app.clear_session();
3601        return false;
3602    }
3603
3604    // Ctrl+Y 复制最后一条 AI 回复
3605    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('y') {
3606        if let Some(last_ai) = app
3607            .session
3608            .messages
3609            .iter()
3610            .rev()
3611            .find(|m| m.role == "assistant")
3612        {
3613            if copy_to_clipboard(&last_ai.content) {
3614                app.show_toast("已复制最后一条 AI 回复", false);
3615            } else {
3616                app.show_toast("复制到剪切板失败", true);
3617            }
3618        } else {
3619            app.show_toast("暂无 AI 回复可复制", true);
3620        }
3621        return false;
3622    }
3623
3624    // Ctrl+B 进入消息浏览模式(可选中历史消息并复制)
3625    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('b') {
3626        if !app.session.messages.is_empty() {
3627            // 默认选中最后一条消息
3628            app.browse_msg_index = app.session.messages.len() - 1;
3629            app.mode = ChatMode::Browse;
3630            app.msg_lines_cache = None; // 清除缓存以触发高亮重绘
3631        } else {
3632            app.show_toast("暂无消息可浏览", true);
3633        }
3634        return false;
3635    }
3636
3637    // Ctrl+E 打开配置界面
3638    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('e') {
3639        // 初始化配置界面状态
3640        app.config_provider_idx = app
3641            .agent_config
3642            .active_index
3643            .min(app.agent_config.providers.len().saturating_sub(1));
3644        app.config_field_idx = 0;
3645        app.config_editing = false;
3646        app.config_edit_buf.clear();
3647        app.mode = ChatMode::Config;
3648        return false;
3649    }
3650
3651    // Ctrl+S 切换流式/非流式输出
3652    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('s') {
3653        app.agent_config.stream_mode = !app.agent_config.stream_mode;
3654        let _ = save_agent_config(&app.agent_config);
3655        let mode_str = if app.agent_config.stream_mode {
3656            "流式输出"
3657        } else {
3658            "整体输出"
3659        };
3660        app.show_toast(&format!("已切换为: {}", mode_str), false);
3661        return false;
3662    }
3663
3664    let char_count = app.input.chars().count();
3665
3666    match key.code {
3667        KeyCode::Esc => return true,
3668
3669        KeyCode::Enter => {
3670            if !app.is_loading {
3671                app.send_message();
3672            }
3673        }
3674
3675        // 滚动消息
3676        KeyCode::Up => app.scroll_up(),
3677        KeyCode::Down => app.scroll_down(),
3678        KeyCode::PageUp => {
3679            for _ in 0..10 {
3680                app.scroll_up();
3681            }
3682        }
3683        KeyCode::PageDown => {
3684            for _ in 0..10 {
3685                app.scroll_down();
3686            }
3687        }
3688
3689        // 光标移动
3690        KeyCode::Left => {
3691            if app.cursor_pos > 0 {
3692                app.cursor_pos -= 1;
3693            }
3694        }
3695        KeyCode::Right => {
3696            if app.cursor_pos < char_count {
3697                app.cursor_pos += 1;
3698            }
3699        }
3700        KeyCode::Home => app.cursor_pos = 0,
3701        KeyCode::End => app.cursor_pos = char_count,
3702
3703        // 删除
3704        KeyCode::Backspace => {
3705            if app.cursor_pos > 0 {
3706                let start = app
3707                    .input
3708                    .char_indices()
3709                    .nth(app.cursor_pos - 1)
3710                    .map(|(i, _)| i)
3711                    .unwrap_or(0);
3712                let end = app
3713                    .input
3714                    .char_indices()
3715                    .nth(app.cursor_pos)
3716                    .map(|(i, _)| i)
3717                    .unwrap_or(app.input.len());
3718                app.input.drain(start..end);
3719                app.cursor_pos -= 1;
3720            }
3721        }
3722        KeyCode::Delete => {
3723            if app.cursor_pos < char_count {
3724                let start = app
3725                    .input
3726                    .char_indices()
3727                    .nth(app.cursor_pos)
3728                    .map(|(i, _)| i)
3729                    .unwrap_or(app.input.len());
3730                let end = app
3731                    .input
3732                    .char_indices()
3733                    .nth(app.cursor_pos + 1)
3734                    .map(|(i, _)| i)
3735                    .unwrap_or(app.input.len());
3736                app.input.drain(start..end);
3737            }
3738        }
3739
3740        // F1 任何时候都能唤起帮助
3741        KeyCode::F(1) => {
3742            app.mode = ChatMode::Help;
3743        }
3744        // 输入框为空时,? 也可唤起帮助
3745        KeyCode::Char('?') if app.input.is_empty() => {
3746            app.mode = ChatMode::Help;
3747        }
3748        KeyCode::Char(c) => {
3749            let byte_idx = app
3750                .input
3751                .char_indices()
3752                .nth(app.cursor_pos)
3753                .map(|(i, _)| i)
3754                .unwrap_or(app.input.len());
3755            app.input.insert_str(byte_idx, &c.to_string());
3756            app.cursor_pos += 1;
3757        }
3758
3759        _ => {}
3760    }
3761
3762    false
3763}
3764
3765/// 消息浏览模式按键处理:↑↓ 选择消息,y/Enter 复制选中消息,Esc 退出
3766fn handle_browse_mode(app: &mut ChatApp, key: KeyEvent) {
3767    let msg_count = app.session.messages.len();
3768    if msg_count == 0 {
3769        app.mode = ChatMode::Chat;
3770        app.msg_lines_cache = None;
3771        return;
3772    }
3773
3774    match key.code {
3775        KeyCode::Esc => {
3776            app.mode = ChatMode::Chat;
3777            app.msg_lines_cache = None; // 退出浏览模式时清除缓存,去掉高亮
3778        }
3779        KeyCode::Up | KeyCode::Char('k') => {
3780            if app.browse_msg_index > 0 {
3781                app.browse_msg_index -= 1;
3782                app.msg_lines_cache = None; // 选中变化时清缓存
3783            }
3784        }
3785        KeyCode::Down | KeyCode::Char('j') => {
3786            if app.browse_msg_index < msg_count - 1 {
3787                app.browse_msg_index += 1;
3788                app.msg_lines_cache = None; // 选中变化时清缓存
3789            }
3790        }
3791        KeyCode::Enter | KeyCode::Char('y') => {
3792            // 复制选中消息的原始内容到剪切板
3793            if let Some(msg) = app.session.messages.get(app.browse_msg_index) {
3794                let content = msg.content.clone();
3795                let role_label = if msg.role == "assistant" {
3796                    "AI"
3797                } else if msg.role == "user" {
3798                    "用户"
3799                } else {
3800                    "系统"
3801                };
3802                if copy_to_clipboard(&content) {
3803                    app.show_toast(
3804                        &format!("已复制第 {} 条{}消息", app.browse_msg_index + 1, role_label),
3805                        false,
3806                    );
3807                } else {
3808                    app.show_toast("复制到剪切板失败", true);
3809                }
3810            }
3811        }
3812        _ => {}
3813    }
3814}
3815
3816/// 获取配置界面中当前字段的标签
3817fn config_field_label(idx: usize) -> &'static str {
3818    let total_provider = CONFIG_FIELDS.len();
3819    if idx < total_provider {
3820        match CONFIG_FIELDS[idx] {
3821            "name" => "显示名称",
3822            "api_base" => "API Base",
3823            "api_key" => "API Key",
3824            "model" => "模型名称",
3825            _ => CONFIG_FIELDS[idx],
3826        }
3827    } else {
3828        let gi = idx - total_provider;
3829        match CONFIG_GLOBAL_FIELDS[gi] {
3830            "system_prompt" => "系统提示词",
3831            "stream_mode" => "流式输出",
3832            _ => CONFIG_GLOBAL_FIELDS[gi],
3833        }
3834    }
3835}
3836
3837/// 获取配置界面中当前字段的值
3838fn config_field_value(app: &ChatApp, field_idx: usize) -> String {
3839    let total_provider = CONFIG_FIELDS.len();
3840    if field_idx < total_provider {
3841        if app.agent_config.providers.is_empty() {
3842            return String::new();
3843        }
3844        let p = &app.agent_config.providers[app.config_provider_idx];
3845        match CONFIG_FIELDS[field_idx] {
3846            "name" => p.name.clone(),
3847            "api_base" => p.api_base.clone(),
3848            "api_key" => {
3849                // 显示时隐藏 API Key 中间部分
3850                if p.api_key.len() > 8 {
3851                    format!(
3852                        "{}****{}",
3853                        &p.api_key[..4],
3854                        &p.api_key[p.api_key.len() - 4..]
3855                    )
3856                } else {
3857                    p.api_key.clone()
3858                }
3859            }
3860            "model" => p.model.clone(),
3861            _ => String::new(),
3862        }
3863    } else {
3864        let gi = field_idx - total_provider;
3865        match CONFIG_GLOBAL_FIELDS[gi] {
3866            "system_prompt" => app.agent_config.system_prompt.clone().unwrap_or_default(),
3867            "stream_mode" => {
3868                if app.agent_config.stream_mode {
3869                    "开启".into()
3870                } else {
3871                    "关闭".into()
3872                }
3873            }
3874            _ => String::new(),
3875        }
3876    }
3877}
3878
3879/// 获取配置字段的原始值(用于编辑时填入输入框)
3880fn config_field_raw_value(app: &ChatApp, field_idx: usize) -> String {
3881    let total_provider = CONFIG_FIELDS.len();
3882    if field_idx < total_provider {
3883        if app.agent_config.providers.is_empty() {
3884            return String::new();
3885        }
3886        let p = &app.agent_config.providers[app.config_provider_idx];
3887        match CONFIG_FIELDS[field_idx] {
3888            "name" => p.name.clone(),
3889            "api_base" => p.api_base.clone(),
3890            "api_key" => p.api_key.clone(),
3891            "model" => p.model.clone(),
3892            _ => String::new(),
3893        }
3894    } else {
3895        let gi = field_idx - total_provider;
3896        match CONFIG_GLOBAL_FIELDS[gi] {
3897            "system_prompt" => app.agent_config.system_prompt.clone().unwrap_or_default(),
3898            "stream_mode" => {
3899                if app.agent_config.stream_mode {
3900                    "true".into()
3901                } else {
3902                    "false".into()
3903                }
3904            }
3905            _ => String::new(),
3906        }
3907    }
3908}
3909
3910/// 将编辑结果写回配置
3911fn config_field_set(app: &mut ChatApp, field_idx: usize, value: &str) {
3912    let total_provider = CONFIG_FIELDS.len();
3913    if field_idx < total_provider {
3914        if app.agent_config.providers.is_empty() {
3915            return;
3916        }
3917        let p = &mut app.agent_config.providers[app.config_provider_idx];
3918        match CONFIG_FIELDS[field_idx] {
3919            "name" => p.name = value.to_string(),
3920            "api_base" => p.api_base = value.to_string(),
3921            "api_key" => p.api_key = value.to_string(),
3922            "model" => p.model = value.to_string(),
3923            _ => {}
3924        }
3925    } else {
3926        let gi = field_idx - total_provider;
3927        match CONFIG_GLOBAL_FIELDS[gi] {
3928            "system_prompt" => {
3929                if value.is_empty() {
3930                    app.agent_config.system_prompt = None;
3931                } else {
3932                    app.agent_config.system_prompt = Some(value.to_string());
3933                }
3934            }
3935            "stream_mode" => {
3936                app.agent_config.stream_mode = matches!(
3937                    value.trim().to_lowercase().as_str(),
3938                    "true" | "1" | "开启" | "on" | "yes"
3939                );
3940            }
3941            _ => {}
3942        }
3943    }
3944}
3945
3946/// 配置模式按键处理
3947fn handle_config_mode(app: &mut ChatApp, key: KeyEvent) {
3948    let total_fields = config_total_fields();
3949
3950    if app.config_editing {
3951        // 正在编辑某个字段
3952        match key.code {
3953            KeyCode::Esc => {
3954                // 取消编辑
3955                app.config_editing = false;
3956            }
3957            KeyCode::Enter => {
3958                // 确认编辑
3959                let val = app.config_edit_buf.clone();
3960                config_field_set(app, app.config_field_idx, &val);
3961                app.config_editing = false;
3962            }
3963            KeyCode::Backspace => {
3964                if app.config_edit_cursor > 0 {
3965                    let idx = app
3966                        .config_edit_buf
3967                        .char_indices()
3968                        .nth(app.config_edit_cursor - 1)
3969                        .map(|(i, _)| i)
3970                        .unwrap_or(0);
3971                    let end_idx = app
3972                        .config_edit_buf
3973                        .char_indices()
3974                        .nth(app.config_edit_cursor)
3975                        .map(|(i, _)| i)
3976                        .unwrap_or(app.config_edit_buf.len());
3977                    app.config_edit_buf = format!(
3978                        "{}{}",
3979                        &app.config_edit_buf[..idx],
3980                        &app.config_edit_buf[end_idx..]
3981                    );
3982                    app.config_edit_cursor -= 1;
3983                }
3984            }
3985            KeyCode::Left => {
3986                app.config_edit_cursor = app.config_edit_cursor.saturating_sub(1);
3987            }
3988            KeyCode::Right => {
3989                let char_count = app.config_edit_buf.chars().count();
3990                if app.config_edit_cursor < char_count {
3991                    app.config_edit_cursor += 1;
3992                }
3993            }
3994            KeyCode::Char(c) => {
3995                let byte_idx = app
3996                    .config_edit_buf
3997                    .char_indices()
3998                    .nth(app.config_edit_cursor)
3999                    .map(|(i, _)| i)
4000                    .unwrap_or(app.config_edit_buf.len());
4001                app.config_edit_buf.insert(byte_idx, c);
4002                app.config_edit_cursor += 1;
4003            }
4004            _ => {}
4005        }
4006        return;
4007    }
4008
4009    // 非编辑状态
4010    match key.code {
4011        KeyCode::Esc => {
4012            // 保存并返回
4013            let _ = save_agent_config(&app.agent_config);
4014            app.show_toast("配置已保存 ✅", false);
4015            app.mode = ChatMode::Chat;
4016        }
4017        KeyCode::Up | KeyCode::Char('k') => {
4018            if total_fields > 0 {
4019                if app.config_field_idx == 0 {
4020                    app.config_field_idx = total_fields - 1;
4021                } else {
4022                    app.config_field_idx -= 1;
4023                }
4024            }
4025        }
4026        KeyCode::Down | KeyCode::Char('j') => {
4027            if total_fields > 0 {
4028                app.config_field_idx = (app.config_field_idx + 1) % total_fields;
4029            }
4030        }
4031        KeyCode::Tab | KeyCode::Right => {
4032            // 切换 provider
4033            let count = app.agent_config.providers.len();
4034            if count > 1 {
4035                app.config_provider_idx = (app.config_provider_idx + 1) % count;
4036                // 切换后如果在 provider 字段区域,保持字段位置不变
4037            }
4038        }
4039        KeyCode::BackTab | KeyCode::Left => {
4040            // 反向切换 provider
4041            let count = app.agent_config.providers.len();
4042            if count > 1 {
4043                if app.config_provider_idx == 0 {
4044                    app.config_provider_idx = count - 1;
4045                } else {
4046                    app.config_provider_idx -= 1;
4047                }
4048            }
4049        }
4050        KeyCode::Enter => {
4051            // 进入编辑模式
4052            let total_provider = CONFIG_FIELDS.len();
4053            if app.config_field_idx < total_provider && app.agent_config.providers.is_empty() {
4054                app.show_toast("还没有 Provider,按 a 新增", true);
4055                return;
4056            }
4057            // stream_mode 字段直接切换,不进入编辑模式
4058            let gi = app.config_field_idx.checked_sub(total_provider);
4059            if let Some(gi) = gi {
4060                if CONFIG_GLOBAL_FIELDS[gi] == "stream_mode" {
4061                    app.agent_config.stream_mode = !app.agent_config.stream_mode;
4062                    return;
4063                }
4064            }
4065            app.config_edit_buf = config_field_raw_value(app, app.config_field_idx);
4066            app.config_edit_cursor = app.config_edit_buf.chars().count();
4067            app.config_editing = true;
4068        }
4069        KeyCode::Char('a') => {
4070            // 新增 Provider
4071            let new_provider = ModelProvider {
4072                name: format!("Provider-{}", app.agent_config.providers.len() + 1),
4073                api_base: "https://api.openai.com/v1".to_string(),
4074                api_key: String::new(),
4075                model: String::new(),
4076            };
4077            app.agent_config.providers.push(new_provider);
4078            app.config_provider_idx = app.agent_config.providers.len() - 1;
4079            app.config_field_idx = 0; // 跳到 name 字段
4080            app.show_toast("已新增 Provider,请填写配置", false);
4081        }
4082        KeyCode::Char('d') => {
4083            // 删除当前 Provider
4084            let count = app.agent_config.providers.len();
4085            if count == 0 {
4086                app.show_toast("没有可删除的 Provider", true);
4087            } else {
4088                let removed_name = app.agent_config.providers[app.config_provider_idx]
4089                    .name
4090                    .clone();
4091                app.agent_config.providers.remove(app.config_provider_idx);
4092                // 调整索引
4093                if app.config_provider_idx >= app.agent_config.providers.len()
4094                    && app.config_provider_idx > 0
4095                {
4096                    app.config_provider_idx -= 1;
4097                }
4098                // 调整 active_index
4099                if app.agent_config.active_index >= app.agent_config.providers.len()
4100                    && app.agent_config.active_index > 0
4101                {
4102                    app.agent_config.active_index -= 1;
4103                }
4104                app.show_toast(format!("已删除 Provider: {}", removed_name), false);
4105            }
4106        }
4107        KeyCode::Char('s') => {
4108            // 将当前 provider 设为活跃
4109            if !app.agent_config.providers.is_empty() {
4110                app.agent_config.active_index = app.config_provider_idx;
4111                let name = app.agent_config.providers[app.config_provider_idx]
4112                    .name
4113                    .clone();
4114                app.show_toast(format!("已设为活跃模型: {}", name), false);
4115            }
4116        }
4117        _ => {}
4118    }
4119}
4120
4121/// 绘制配置编辑界面
4122fn draw_config_screen(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
4123    let bg = Color::Rgb(28, 28, 40);
4124    let total_provider_fields = CONFIG_FIELDS.len();
4125
4126    let mut lines: Vec<Line> = Vec::new();
4127    lines.push(Line::from(""));
4128
4129    // 标题
4130    lines.push(Line::from(vec![Span::styled(
4131        "  ⚙️  模型配置",
4132        Style::default()
4133            .fg(Color::Rgb(120, 180, 255))
4134            .add_modifier(Modifier::BOLD),
4135    )]));
4136    lines.push(Line::from(""));
4137
4138    // Provider 标签栏
4139    let provider_count = app.agent_config.providers.len();
4140    if provider_count > 0 {
4141        let mut tab_spans: Vec<Span> = vec![Span::styled("  ", Style::default())];
4142        for (i, p) in app.agent_config.providers.iter().enumerate() {
4143            let is_current = i == app.config_provider_idx;
4144            let is_active = i == app.agent_config.active_index;
4145            let marker = if is_active { "● " } else { "○ " };
4146            let label = format!(" {}{} ", marker, p.name);
4147            if is_current {
4148                tab_spans.push(Span::styled(
4149                    label,
4150                    Style::default()
4151                        .fg(Color::Rgb(22, 22, 30))
4152                        .bg(Color::Rgb(120, 180, 255))
4153                        .add_modifier(Modifier::BOLD),
4154                ));
4155            } else {
4156                tab_spans.push(Span::styled(
4157                    label,
4158                    Style::default().fg(Color::Rgb(150, 150, 170)),
4159                ));
4160            }
4161            if i < provider_count - 1 {
4162                tab_spans.push(Span::styled(
4163                    " │ ",
4164                    Style::default().fg(Color::Rgb(50, 55, 70)),
4165                ));
4166            }
4167        }
4168        tab_spans.push(Span::styled(
4169            "    (● = 活跃模型, Tab 切换, s 设为活跃)",
4170            Style::default().fg(Color::Rgb(80, 80, 100)),
4171        ));
4172        lines.push(Line::from(tab_spans));
4173    } else {
4174        lines.push(Line::from(Span::styled(
4175            "  (无 Provider,按 a 新增)",
4176            Style::default().fg(Color::Rgb(180, 120, 80)),
4177        )));
4178    }
4179    lines.push(Line::from(""));
4180
4181    // 分隔线
4182    lines.push(Line::from(Span::styled(
4183        "  ─────────────────────────────────────────",
4184        Style::default().fg(Color::Rgb(50, 55, 70)),
4185    )));
4186    lines.push(Line::from(""));
4187
4188    // Provider 字段
4189    if provider_count > 0 {
4190        lines.push(Line::from(Span::styled(
4191            "  📦 Provider 配置",
4192            Style::default()
4193                .fg(Color::Rgb(160, 220, 160))
4194                .add_modifier(Modifier::BOLD),
4195        )));
4196        lines.push(Line::from(""));
4197
4198        for i in 0..total_provider_fields {
4199            let is_selected = app.config_field_idx == i;
4200            let label = config_field_label(i);
4201            let value = if app.config_editing && is_selected {
4202                // 编辑模式下显示编辑缓冲区
4203                app.config_edit_buf.clone()
4204            } else {
4205                config_field_value(app, i)
4206            };
4207
4208            let pointer = if is_selected { "  ▸ " } else { "    " };
4209            let pointer_style = if is_selected {
4210                Style::default().fg(Color::Rgb(255, 200, 80))
4211            } else {
4212                Style::default()
4213            };
4214
4215            let label_style = if is_selected {
4216                Style::default()
4217                    .fg(Color::Rgb(230, 210, 120))
4218                    .add_modifier(Modifier::BOLD)
4219            } else {
4220                Style::default().fg(Color::Rgb(140, 140, 160))
4221            };
4222
4223            let value_style = if app.config_editing && is_selected {
4224                Style::default().fg(Color::White).bg(Color::Rgb(50, 55, 80))
4225            } else if is_selected {
4226                Style::default().fg(Color::White)
4227            } else {
4228                // API Key 特殊处理
4229                if CONFIG_FIELDS[i] == "api_key" {
4230                    Style::default().fg(Color::Rgb(100, 100, 120))
4231                } else {
4232                    Style::default().fg(Color::Rgb(180, 180, 200))
4233                }
4234            };
4235
4236            let edit_indicator = if app.config_editing && is_selected {
4237                " ✏️"
4238            } else {
4239                ""
4240            };
4241
4242            lines.push(Line::from(vec![
4243                Span::styled(pointer, pointer_style),
4244                Span::styled(format!("{:<10}", label), label_style),
4245                Span::styled("  ", Style::default()),
4246                Span::styled(
4247                    if value.is_empty() {
4248                        "(空)".to_string()
4249                    } else {
4250                        value
4251                    },
4252                    value_style,
4253                ),
4254                Span::styled(edit_indicator, Style::default()),
4255            ]));
4256        }
4257    }
4258
4259    lines.push(Line::from(""));
4260    // 分隔线
4261    lines.push(Line::from(Span::styled(
4262        "  ─────────────────────────────────────────",
4263        Style::default().fg(Color::Rgb(50, 55, 70)),
4264    )));
4265    lines.push(Line::from(""));
4266
4267    // 全局配置
4268    lines.push(Line::from(Span::styled(
4269        "  🌐 全局配置",
4270        Style::default()
4271            .fg(Color::Rgb(160, 220, 160))
4272            .add_modifier(Modifier::BOLD),
4273    )));
4274    lines.push(Line::from(""));
4275
4276    for i in 0..CONFIG_GLOBAL_FIELDS.len() {
4277        let field_idx = total_provider_fields + i;
4278        let is_selected = app.config_field_idx == field_idx;
4279        let label = config_field_label(field_idx);
4280        let value = if app.config_editing && is_selected {
4281            app.config_edit_buf.clone()
4282        } else {
4283            config_field_value(app, field_idx)
4284        };
4285
4286        let pointer = if is_selected { "  ▸ " } else { "    " };
4287        let pointer_style = if is_selected {
4288            Style::default().fg(Color::Rgb(255, 200, 80))
4289        } else {
4290            Style::default()
4291        };
4292
4293        let label_style = if is_selected {
4294            Style::default()
4295                .fg(Color::Rgb(230, 210, 120))
4296                .add_modifier(Modifier::BOLD)
4297        } else {
4298            Style::default().fg(Color::Rgb(140, 140, 160))
4299        };
4300
4301        let value_style = if app.config_editing && is_selected {
4302            Style::default().fg(Color::White).bg(Color::Rgb(50, 55, 80))
4303        } else if is_selected {
4304            Style::default().fg(Color::White)
4305        } else {
4306            Style::default().fg(Color::Rgb(180, 180, 200))
4307        };
4308
4309        let edit_indicator = if app.config_editing && is_selected {
4310            " ✏️"
4311        } else {
4312            ""
4313        };
4314
4315        // stream_mode 用 toggle 样式
4316        if CONFIG_GLOBAL_FIELDS[i] == "stream_mode" {
4317            let toggle_on = app.agent_config.stream_mode;
4318            let toggle_style = if toggle_on {
4319                Style::default()
4320                    .fg(Color::Rgb(120, 220, 160))
4321                    .add_modifier(Modifier::BOLD)
4322            } else {
4323                Style::default().fg(Color::Rgb(200, 100, 100))
4324            };
4325            let toggle_text = if toggle_on {
4326                "● 开启"
4327            } else {
4328                "○ 关闭"
4329            };
4330
4331            lines.push(Line::from(vec![
4332                Span::styled(pointer, pointer_style),
4333                Span::styled(format!("{:<10}", label), label_style),
4334                Span::styled("  ", Style::default()),
4335                Span::styled(toggle_text, toggle_style),
4336                Span::styled(
4337                    if is_selected { "  (Enter 切换)" } else { "" },
4338                    Style::default().fg(Color::Rgb(80, 80, 100)),
4339                ),
4340            ]));
4341        } else {
4342            lines.push(Line::from(vec![
4343                Span::styled(pointer, pointer_style),
4344                Span::styled(format!("{:<10}", label), label_style),
4345                Span::styled("  ", Style::default()),
4346                Span::styled(
4347                    if value.is_empty() {
4348                        "(空)".to_string()
4349                    } else {
4350                        value
4351                    },
4352                    value_style,
4353                ),
4354                Span::styled(edit_indicator, Style::default()),
4355            ]));
4356        }
4357    }
4358
4359    lines.push(Line::from(""));
4360    lines.push(Line::from(""));
4361
4362    // 操作提示
4363    lines.push(Line::from(Span::styled(
4364        "  ─────────────────────────────────────────",
4365        Style::default().fg(Color::Rgb(50, 55, 70)),
4366    )));
4367    lines.push(Line::from(""));
4368    lines.push(Line::from(vec![
4369        Span::styled("    ", Style::default()),
4370        Span::styled(
4371            "↑↓/jk",
4372            Style::default()
4373                .fg(Color::Rgb(230, 210, 120))
4374                .add_modifier(Modifier::BOLD),
4375        ),
4376        Span::styled(
4377            " 切换字段  ",
4378            Style::default().fg(Color::Rgb(120, 120, 150)),
4379        ),
4380        Span::styled(
4381            "Enter",
4382            Style::default()
4383                .fg(Color::Rgb(230, 210, 120))
4384                .add_modifier(Modifier::BOLD),
4385        ),
4386        Span::styled(" 编辑  ", Style::default().fg(Color::Rgb(120, 120, 150))),
4387        Span::styled(
4388            "Tab/←→",
4389            Style::default()
4390                .fg(Color::Rgb(230, 210, 120))
4391                .add_modifier(Modifier::BOLD),
4392        ),
4393        Span::styled(
4394            " 切换 Provider  ",
4395            Style::default().fg(Color::Rgb(120, 120, 150)),
4396        ),
4397        Span::styled(
4398            "a",
4399            Style::default()
4400                .fg(Color::Rgb(230, 210, 120))
4401                .add_modifier(Modifier::BOLD),
4402        ),
4403        Span::styled(" 新增  ", Style::default().fg(Color::Rgb(120, 120, 150))),
4404        Span::styled(
4405            "d",
4406            Style::default()
4407                .fg(Color::Rgb(230, 210, 120))
4408                .add_modifier(Modifier::BOLD),
4409        ),
4410        Span::styled(" 删除  ", Style::default().fg(Color::Rgb(120, 120, 150))),
4411        Span::styled(
4412            "s",
4413            Style::default()
4414                .fg(Color::Rgb(230, 210, 120))
4415                .add_modifier(Modifier::BOLD),
4416        ),
4417        Span::styled(
4418            " 设为活跃  ",
4419            Style::default().fg(Color::Rgb(120, 120, 150)),
4420        ),
4421        Span::styled(
4422            "Esc",
4423            Style::default()
4424                .fg(Color::Rgb(230, 210, 120))
4425                .add_modifier(Modifier::BOLD),
4426        ),
4427        Span::styled(" 保存返回", Style::default().fg(Color::Rgb(120, 120, 150))),
4428    ]));
4429
4430    let content = Paragraph::new(lines)
4431        .block(
4432            Block::default()
4433                .borders(Borders::ALL)
4434                .border_type(ratatui::widgets::BorderType::Rounded)
4435                .border_style(Style::default().fg(Color::Rgb(80, 80, 110)))
4436                .title(Span::styled(
4437                    " ⚙️  模型配置编辑 ",
4438                    Style::default()
4439                        .fg(Color::Rgb(230, 210, 120))
4440                        .add_modifier(Modifier::BOLD),
4441                ))
4442                .style(Style::default().bg(bg)),
4443        )
4444        .scroll((0, 0));
4445    f.render_widget(content, area);
4446}
4447
4448/// 模型选择模式按键处理
4449fn handle_select_model(app: &mut ChatApp, key: KeyEvent) {
4450    let count = app.agent_config.providers.len();
4451    match key.code {
4452        KeyCode::Esc => {
4453            app.mode = ChatMode::Chat;
4454        }
4455        KeyCode::Up | KeyCode::Char('k') => {
4456            if count > 0 {
4457                let i = app
4458                    .model_list_state
4459                    .selected()
4460                    .map(|i| if i == 0 { count - 1 } else { i - 1 })
4461                    .unwrap_or(0);
4462                app.model_list_state.select(Some(i));
4463            }
4464        }
4465        KeyCode::Down | KeyCode::Char('j') => {
4466            if count > 0 {
4467                let i = app
4468                    .model_list_state
4469                    .selected()
4470                    .map(|i| if i >= count - 1 { 0 } else { i + 1 })
4471                    .unwrap_or(0);
4472                app.model_list_state.select(Some(i));
4473            }
4474        }
4475        KeyCode::Enter => {
4476            app.switch_model();
4477        }
4478        _ => {}
4479    }
4480}
4481
4482/// 复制内容到系统剪切板
4483fn copy_to_clipboard(content: &str) -> bool {
4484    use std::process::{Command, Stdio};
4485
4486    let (cmd, args): (&str, Vec<&str>) = if cfg!(target_os = "macos") {
4487        ("pbcopy", vec![])
4488    } else if cfg!(target_os = "linux") {
4489        if Command::new("which")
4490            .arg("xclip")
4491            .output()
4492            .map(|o| o.status.success())
4493            .unwrap_or(false)
4494        {
4495            ("xclip", vec!["-selection", "clipboard"])
4496        } else {
4497            ("xsel", vec!["--clipboard", "--input"])
4498        }
4499    } else {
4500        return false;
4501    };
4502
4503    let child = Command::new(cmd).args(&args).stdin(Stdio::piped()).spawn();
4504
4505    match child {
4506        Ok(mut child) => {
4507            if let Some(ref mut stdin) = child.stdin {
4508                let _ = stdin.write_all(content.as_bytes());
4509            }
4510            child.wait().map(|s| s.success()).unwrap_or(false)
4511        }
4512        Err(_) => false,
4513    }
4514}