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(current.add_modifier(Modifier::BOLD));
1770            }
1771            Event::End(TagEnd::Strong) => {
1772                style_stack.pop();
1773            }
1774            Event::Start(Tag::Emphasis) => {
1775                let current = *style_stack.last().unwrap_or(&base_style);
1776                style_stack.push(current.add_modifier(Modifier::ITALIC));
1777            }
1778            Event::End(TagEnd::Emphasis) => {
1779                style_stack.pop();
1780            }
1781            Event::Start(Tag::Strikethrough) => {
1782                let current = *style_stack.last().unwrap_or(&base_style);
1783                style_stack.push(current.add_modifier(Modifier::CROSSED_OUT));
1784            }
1785            Event::End(TagEnd::Strikethrough) => {
1786                style_stack.pop();
1787            }
1788            Event::Start(Tag::CodeBlock(kind)) => {
1789                flush_line(&mut current_spans, &mut lines);
1790                in_code_block = true;
1791                code_block_content.clear();
1792                code_block_lang = match kind {
1793                    CodeBlockKind::Fenced(lang) => lang.to_string(),
1794                    CodeBlockKind::Indented => String::new(),
1795                };
1796                // 代码块上方边框(自适应宽度)
1797                let label = if code_block_lang.is_empty() {
1798                    " code ".to_string()
1799                } else {
1800                    format!(" {} ", code_block_lang)
1801                };
1802                let label_w = display_width(&label);
1803                let border_fill = content_width.saturating_sub(2 + label_w);
1804                let top_border = format!("┌─{}{}", label, "─".repeat(border_fill));
1805                lines.push(Line::from(Span::styled(
1806                    top_border,
1807                    Style::default().fg(Color::Rgb(80, 90, 110)),
1808                )));
1809            }
1810            Event::End(TagEnd::CodeBlock) => {
1811                // 渲染代码块内容(带语法高亮)
1812                let code_inner_w = content_width.saturating_sub(4); // "│ " 前缀 + 右侧 " │" 后缀占4
1813                for code_line in code_block_content.lines() {
1814                    let wrapped = wrap_text(code_line, code_inner_w);
1815                    for wl in wrapped {
1816                        let highlighted = highlight_code_line(&wl, &code_block_lang);
1817                        let text_w: usize =
1818                            highlighted.iter().map(|s| display_width(&s.content)).sum();
1819                        let fill = code_inner_w.saturating_sub(text_w);
1820                        let mut spans_vec = Vec::new();
1821                        spans_vec.push(Span::styled(
1822                            "│ ",
1823                            Style::default().fg(Color::Rgb(80, 90, 110)),
1824                        ));
1825                        for hs in highlighted {
1826                            spans_vec.push(Span::styled(
1827                                hs.content.to_string(),
1828                                hs.style.bg(Color::Rgb(30, 30, 42)),
1829                            ));
1830                        }
1831                        spans_vec.push(Span::styled(
1832                            format!("{} │", " ".repeat(fill)),
1833                            Style::default()
1834                                .fg(Color::Rgb(80, 90, 110))
1835                                .bg(Color::Rgb(30, 30, 42)),
1836                        ));
1837                        lines.push(Line::from(spans_vec));
1838                    }
1839                }
1840                let bottom_border = format!("└{}", "─".repeat(content_width.saturating_sub(1)));
1841                lines.push(Line::from(Span::styled(
1842                    bottom_border,
1843                    Style::default().fg(Color::Rgb(80, 90, 110)),
1844                )));
1845                in_code_block = false;
1846                code_block_content.clear();
1847                code_block_lang.clear();
1848            }
1849            Event::Code(text) => {
1850                if in_table {
1851                    // 表格中的行内代码也收集到当前单元格
1852                    current_cell.push('`');
1853                    current_cell.push_str(&text);
1854                    current_cell.push('`');
1855                } else {
1856                    // 行内代码:检查行宽,放不下则先换行
1857                    let code_str = format!(" {} ", text);
1858                    let code_w = display_width(&code_str);
1859                    let effective_prefix_w = if in_blockquote { 2 } else { 0 };
1860                    let full_line_w = content_width.saturating_sub(effective_prefix_w);
1861                    let existing_w: usize = current_spans
1862                        .iter()
1863                        .map(|s| display_width(&s.content))
1864                        .sum();
1865                    if existing_w + code_w > full_line_w && !current_spans.is_empty() {
1866                        flush_line(&mut current_spans, &mut lines);
1867                        if in_blockquote {
1868                            current_spans.push(Span::styled(
1869                                "| ".to_string(),
1870                                Style::default().fg(Color::Rgb(80, 100, 140)),
1871                            ));
1872                        }
1873                    }
1874                    current_spans.push(Span::styled(
1875                        code_str,
1876                        Style::default()
1877                            .fg(Color::Rgb(230, 190, 120))
1878                            .bg(Color::Rgb(45, 45, 60)),
1879                    ));
1880                }
1881            }
1882            Event::Start(Tag::List(start)) => {
1883                flush_line(&mut current_spans, &mut lines);
1884                list_depth += 1;
1885                ordered_index = start;
1886            }
1887            Event::End(TagEnd::List(_)) => {
1888                flush_line(&mut current_spans, &mut lines);
1889                list_depth = list_depth.saturating_sub(1);
1890                ordered_index = None;
1891            }
1892            Event::Start(Tag::Item) => {
1893                flush_line(&mut current_spans, &mut lines);
1894                let indent = "  ".repeat(list_depth);
1895                let bullet = if let Some(ref mut idx) = ordered_index {
1896                    let s = format!("{}{}. ", indent, idx);
1897                    *idx += 1;
1898                    s
1899                } else {
1900                    format!("{}- ", indent)
1901                };
1902                current_spans.push(Span::styled(
1903                    bullet,
1904                    Style::default().fg(Color::Rgb(160, 180, 220)),
1905                ));
1906            }
1907            Event::End(TagEnd::Item) => {
1908                flush_line(&mut current_spans, &mut lines);
1909            }
1910            Event::Start(Tag::Paragraph) => {
1911                if !lines.is_empty() && !in_code_block && heading_level.is_none() {
1912                    let last_empty = lines.last().map(|l| l.spans.is_empty()).unwrap_or(false);
1913                    if !last_empty {
1914                        lines.push(Line::from(""));
1915                    }
1916                }
1917            }
1918            Event::End(TagEnd::Paragraph) => {
1919                flush_line(&mut current_spans, &mut lines);
1920            }
1921            Event::Start(Tag::BlockQuote(_)) => {
1922                flush_line(&mut current_spans, &mut lines);
1923                in_blockquote = true;
1924                style_stack.push(Style::default().fg(Color::Rgb(150, 160, 180)));
1925            }
1926            Event::End(TagEnd::BlockQuote(_)) => {
1927                flush_line(&mut current_spans, &mut lines);
1928                in_blockquote = false;
1929                style_stack.pop();
1930            }
1931            Event::Text(text) => {
1932                if in_code_block {
1933                    code_block_content.push_str(&text);
1934                } else if in_table {
1935                    // 表格中的文本收集到当前单元格
1936                    current_cell.push_str(&text);
1937                } else {
1938                    let style = *style_stack.last().unwrap_or(&base_style);
1939                    let text_str = text.to_string();
1940
1941                    // 标题:添加可视化符号前缀代替 # 标记
1942                    if let Some(level) = heading_level {
1943                        let (prefix, prefix_style) = match level {
1944                            1 => (
1945                                ">> ",
1946                                Style::default()
1947                                    .fg(Color::Rgb(100, 180, 255))
1948                                    .add_modifier(Modifier::BOLD),
1949                            ),
1950                            2 => (
1951                                ">> ",
1952                                Style::default()
1953                                    .fg(Color::Rgb(130, 190, 255))
1954                                    .add_modifier(Modifier::BOLD),
1955                            ),
1956                            3 => (
1957                                "> ",
1958                                Style::default()
1959                                    .fg(Color::Rgb(160, 200, 255))
1960                                    .add_modifier(Modifier::BOLD),
1961                            ),
1962                            _ => (
1963                                "> ",
1964                                Style::default()
1965                                    .fg(Color::Rgb(180, 210, 255))
1966                                    .add_modifier(Modifier::BOLD),
1967                            ),
1968                        };
1969                        current_spans.push(Span::styled(prefix.to_string(), prefix_style));
1970                        heading_level = None; // 只加一次前缀
1971                    }
1972
1973                    // 引用块:加左侧竖线
1974                    let effective_prefix_w = if in_blockquote { 2 } else { 0 }; // "| " 宽度
1975                    let full_line_w = content_width.saturating_sub(effective_prefix_w);
1976
1977                    // 计算 current_spans 已有的显示宽度
1978                    let existing_w: usize = current_spans
1979                        .iter()
1980                        .map(|s| display_width(&s.content))
1981                        .sum();
1982
1983                    // 剩余可用宽度
1984                    let wrap_w = full_line_w.saturating_sub(existing_w);
1985
1986                    // 如果剩余宽度太小(不足整行的 1/4),先 flush 当前行再换行,
1987                    // 避免文字被挤到极窄的空间导致竖排
1988                    let min_useful_w = full_line_w / 4;
1989                    let wrap_w = if wrap_w < min_useful_w.max(4) && !current_spans.is_empty() {
1990                        flush_line(&mut current_spans, &mut lines);
1991                        if in_blockquote {
1992                            current_spans.push(Span::styled(
1993                                "| ".to_string(),
1994                                Style::default().fg(Color::Rgb(80, 100, 140)),
1995                            ));
1996                        }
1997                        // flush 后使用完整行宽
1998                        full_line_w
1999                    } else {
2000                        wrap_w
2001                    };
2002
2003                    for (i, line) in text_str.split('\n').enumerate() {
2004                        if i > 0 {
2005                            flush_line(&mut current_spans, &mut lines);
2006                            if in_blockquote {
2007                                current_spans.push(Span::styled(
2008                                    "| ".to_string(),
2009                                    Style::default().fg(Color::Rgb(80, 100, 140)),
2010                                ));
2011                            }
2012                        }
2013                        if !line.is_empty() {
2014                            // 第一行使用减去已有 span 宽度的 wrap_w,后续行使用完整 content_width
2015                            let effective_wrap = if i == 0 {
2016                                wrap_w
2017                            } else {
2018                                content_width.saturating_sub(effective_prefix_w)
2019                            };
2020                            let wrapped = wrap_text(line, effective_wrap);
2021                            for (j, wl) in wrapped.iter().enumerate() {
2022                                if j > 0 {
2023                                    flush_line(&mut current_spans, &mut lines);
2024                                    if in_blockquote {
2025                                        current_spans.push(Span::styled(
2026                                            "| ".to_string(),
2027                                            Style::default().fg(Color::Rgb(80, 100, 140)),
2028                                        ));
2029                                    }
2030                                }
2031                                current_spans.push(Span::styled(wl.clone(), style));
2032                            }
2033                        }
2034                    }
2035                }
2036            }
2037            Event::SoftBreak => {
2038                if in_table {
2039                    current_cell.push(' ');
2040                } else {
2041                    current_spans.push(Span::raw(" "));
2042                }
2043            }
2044            Event::HardBreak => {
2045                if in_table {
2046                    current_cell.push(' ');
2047                } else {
2048                    flush_line(&mut current_spans, &mut lines);
2049                }
2050            }
2051            Event::Rule => {
2052                flush_line(&mut current_spans, &mut lines);
2053                lines.push(Line::from(Span::styled(
2054                    "─".repeat(content_width),
2055                    Style::default().fg(Color::Rgb(70, 75, 90)),
2056                )));
2057            }
2058            // ===== 表格支持 =====
2059            Event::Start(Tag::Table(alignments)) => {
2060                flush_line(&mut current_spans, &mut lines);
2061                in_table = true;
2062                table_rows.clear();
2063                table_alignments = alignments;
2064            }
2065            Event::End(TagEnd::Table) => {
2066                // 表格结束:计算列宽,渲染完整表格
2067                flush_line(&mut current_spans, &mut lines);
2068                in_table = false;
2069
2070                if !table_rows.is_empty() {
2071                    let num_cols = table_rows.iter().map(|r| r.len()).max().unwrap_or(0);
2072                    if num_cols > 0 {
2073                        // 计算每列最大宽度
2074                        let mut col_widths: Vec<usize> = vec![0; num_cols];
2075                        for row in &table_rows {
2076                            for (i, cell) in row.iter().enumerate() {
2077                                let w = display_width(cell);
2078                                if w > col_widths[i] {
2079                                    col_widths[i] = w;
2080                                }
2081                            }
2082                        }
2083
2084                        // 限制总宽度不超过 content_width,等比缩放
2085                        let sep_w = num_cols + 1; // 竖线占用
2086                        let pad_w = num_cols * 2; // 每列左右各1空格
2087                        let avail = content_width.saturating_sub(sep_w + pad_w);
2088                        // 单列最大宽度限制(避免一列过宽)
2089                        let max_col_w = avail * 2 / 3;
2090                        for cw in col_widths.iter_mut() {
2091                            if *cw > max_col_w {
2092                                *cw = max_col_w;
2093                            }
2094                        }
2095                        let total_col_w: usize = col_widths.iter().sum();
2096                        if total_col_w > avail && total_col_w > 0 {
2097                            // 等比缩放
2098                            let mut remaining = avail;
2099                            for (i, cw) in col_widths.iter_mut().enumerate() {
2100                                if i == num_cols - 1 {
2101                                    // 最后一列取剩余宽度,避免取整误差
2102                                    *cw = remaining.max(1);
2103                                } else {
2104                                    *cw = ((*cw) * avail / total_col_w).max(1);
2105                                    remaining = remaining.saturating_sub(*cw);
2106                                }
2107                            }
2108                        }
2109
2110                        let table_style = Style::default().fg(Color::Rgb(180, 180, 200));
2111                        let header_style = Style::default()
2112                            .fg(Color::Rgb(120, 180, 255))
2113                            .add_modifier(Modifier::BOLD);
2114                        let border_style = Style::default().fg(Color::Rgb(60, 70, 100));
2115
2116                        // 表格行的实际字符宽度(用空格字符计算,不依赖 Box Drawing 字符宽度)
2117                        // table_row_w = 竖线数(num_cols+1) + 每列(cw+2) = sep_w + pad_w + total_col_w
2118                        let total_col_w_final: usize = col_widths.iter().sum();
2119                        let table_row_w = sep_w + pad_w + total_col_w_final;
2120                        // 表格行右侧需要补充的空格数,使整行宽度等于 content_width
2121                        let table_right_pad = content_width.saturating_sub(table_row_w);
2122
2123                        // 渲染顶边框 ┌─┬─┐
2124                        let mut top = String::from("┌");
2125                        for (i, cw) in col_widths.iter().enumerate() {
2126                            top.push_str(&"─".repeat(cw + 2));
2127                            if i < num_cols - 1 {
2128                                top.push('┬');
2129                            }
2130                        }
2131                        top.push('┐');
2132                        // 补充右侧空格,使宽度对齐 content_width
2133                        let mut top_spans = vec![Span::styled(top, border_style)];
2134                        if table_right_pad > 0 {
2135                            top_spans.push(Span::raw(" ".repeat(table_right_pad)));
2136                        }
2137                        lines.push(Line::from(top_spans));
2138
2139                        for (row_idx, row) in table_rows.iter().enumerate() {
2140                            // 数据行 │ cell │ cell │
2141                            let mut row_spans: Vec<Span> = Vec::new();
2142                            row_spans.push(Span::styled("│", border_style));
2143                            for (i, cw) in col_widths.iter().enumerate() {
2144                                let cell_text = row.get(i).map(|s| s.as_str()).unwrap_or("");
2145                                let cell_w = display_width(cell_text);
2146                                let text = if cell_w > *cw {
2147                                    // 截断
2148                                    let mut t = String::new();
2149                                    let mut w = 0;
2150                                    for ch in cell_text.chars() {
2151                                        let chw = char_width(ch);
2152                                        if w + chw > *cw {
2153                                            break;
2154                                        }
2155                                        t.push(ch);
2156                                        w += chw;
2157                                    }
2158                                    let fill = cw.saturating_sub(w);
2159                                    format!(" {}{} ", t, " ".repeat(fill))
2160                                } else {
2161                                    // 根据对齐方式填充
2162                                    let fill = cw.saturating_sub(cell_w);
2163                                    let align = table_alignments
2164                                        .get(i)
2165                                        .copied()
2166                                        .unwrap_or(pulldown_cmark::Alignment::None);
2167                                    match align {
2168                                        pulldown_cmark::Alignment::Center => {
2169                                            let left = fill / 2;
2170                                            let right = fill - left;
2171                                            format!(
2172                                                " {}{}{} ",
2173                                                " ".repeat(left),
2174                                                cell_text,
2175                                                " ".repeat(right)
2176                                            )
2177                                        }
2178                                        pulldown_cmark::Alignment::Right => {
2179                                            format!(" {}{} ", " ".repeat(fill), cell_text)
2180                                        }
2181                                        _ => {
2182                                            format!(" {}{} ", cell_text, " ".repeat(fill))
2183                                        }
2184                                    }
2185                                };
2186                                let style = if row_idx == 0 {
2187                                    header_style
2188                                } else {
2189                                    table_style
2190                                };
2191                                row_spans.push(Span::styled(text, style));
2192                                row_spans.push(Span::styled("│", border_style));
2193                            }
2194                            // 补充右侧空格,使宽度对齐 content_width
2195                            if table_right_pad > 0 {
2196                                row_spans.push(Span::raw(" ".repeat(table_right_pad)));
2197                            }
2198                            lines.push(Line::from(row_spans));
2199
2200                            // 表头行后加分隔线 ├─┼─┤
2201                            if row_idx == 0 {
2202                                let mut sep = String::from("├");
2203                                for (i, cw) in col_widths.iter().enumerate() {
2204                                    sep.push_str(&"─".repeat(cw + 2));
2205                                    if i < num_cols - 1 {
2206                                        sep.push('┼');
2207                                    }
2208                                }
2209                                sep.push('┤');
2210                                let mut sep_spans = vec![Span::styled(sep, border_style)];
2211                                if table_right_pad > 0 {
2212                                    sep_spans.push(Span::raw(" ".repeat(table_right_pad)));
2213                                }
2214                                lines.push(Line::from(sep_spans));
2215                            }
2216                        }
2217
2218                        // 底边框 └─┴─┘
2219                        let mut bottom = String::from("└");
2220                        for (i, cw) in col_widths.iter().enumerate() {
2221                            bottom.push_str(&"─".repeat(cw + 2));
2222                            if i < num_cols - 1 {
2223                                bottom.push('┴');
2224                            }
2225                        }
2226                        bottom.push('┘');
2227                        let mut bottom_spans = vec![Span::styled(bottom, border_style)];
2228                        if table_right_pad > 0 {
2229                            bottom_spans.push(Span::raw(" ".repeat(table_right_pad)));
2230                        }
2231                        lines.push(Line::from(bottom_spans));
2232                    }
2233                }
2234                table_rows.clear();
2235                table_alignments.clear();
2236            }
2237            Event::Start(Tag::TableHead) => {
2238                current_row.clear();
2239            }
2240            Event::End(TagEnd::TableHead) => {
2241                table_rows.push(current_row.clone());
2242                current_row.clear();
2243            }
2244            Event::Start(Tag::TableRow) => {
2245                current_row.clear();
2246            }
2247            Event::End(TagEnd::TableRow) => {
2248                table_rows.push(current_row.clone());
2249                current_row.clear();
2250            }
2251            Event::Start(Tag::TableCell) => {
2252                current_cell.clear();
2253            }
2254            Event::End(TagEnd::TableCell) => {
2255                current_row.push(current_cell.clone());
2256                current_cell.clear();
2257            }
2258            _ => {}
2259        }
2260    }
2261
2262    // 刷新最后一行
2263    if !current_spans.is_empty() {
2264        lines.push(Line::from(current_spans));
2265    }
2266
2267    // 如果解析结果为空,至少返回原始文本
2268    if lines.is_empty() {
2269        let wrapped = wrap_text(md, content_width);
2270        for wl in wrapped {
2271            lines.push(Line::from(Span::styled(wl, base_style)));
2272        }
2273    }
2274
2275    lines
2276}
2277
2278/// 简单的代码语法高亮(无需外部依赖)
2279/// 根据语言类型对常见关键字、字符串、注释、数字进行着色
2280fn highlight_code_line<'a>(line: &'a str, lang: &str) -> Vec<Span<'static>> {
2281    let lang_lower = lang.to_lowercase();
2282    let keywords: &[&str] = match lang_lower.as_str() {
2283        "rust" | "rs" => &[
2284            "fn", "let", "mut", "pub", "use", "mod", "struct", "enum", "impl", "trait", "for",
2285            "while", "loop", "if", "else", "match", "return", "self", "Self", "where", "async",
2286            "await", "move", "ref", "type", "const", "static", "crate", "super", "as", "in",
2287            "true", "false", "Some", "None", "Ok", "Err",
2288        ],
2289        "python" | "py" => &[
2290            "def", "class", "return", "if", "elif", "else", "for", "while", "import", "from", "as",
2291            "with", "try", "except", "finally", "raise", "pass", "break", "continue", "yield",
2292            "lambda", "and", "or", "not", "in", "is", "True", "False", "None", "global",
2293            "nonlocal", "assert", "del", "async", "await", "self", "print",
2294        ],
2295        "javascript" | "js" | "typescript" | "ts" | "jsx" | "tsx" => &[
2296            "function",
2297            "const",
2298            "let",
2299            "var",
2300            "return",
2301            "if",
2302            "else",
2303            "for",
2304            "while",
2305            "class",
2306            "new",
2307            "this",
2308            "import",
2309            "export",
2310            "from",
2311            "default",
2312            "async",
2313            "await",
2314            "try",
2315            "catch",
2316            "finally",
2317            "throw",
2318            "typeof",
2319            "instanceof",
2320            "true",
2321            "false",
2322            "null",
2323            "undefined",
2324            "of",
2325            "in",
2326            "switch",
2327            "case",
2328        ],
2329        "go" | "golang" => &[
2330            "func",
2331            "package",
2332            "import",
2333            "return",
2334            "if",
2335            "else",
2336            "for",
2337            "range",
2338            "struct",
2339            "interface",
2340            "type",
2341            "var",
2342            "const",
2343            "defer",
2344            "go",
2345            "chan",
2346            "select",
2347            "case",
2348            "switch",
2349            "default",
2350            "break",
2351            "continue",
2352            "map",
2353            "true",
2354            "false",
2355            "nil",
2356            "make",
2357            "append",
2358            "len",
2359            "cap",
2360        ],
2361        "java" | "kotlin" | "kt" => &[
2362            "public",
2363            "private",
2364            "protected",
2365            "class",
2366            "interface",
2367            "extends",
2368            "implements",
2369            "return",
2370            "if",
2371            "else",
2372            "for",
2373            "while",
2374            "new",
2375            "this",
2376            "import",
2377            "package",
2378            "static",
2379            "final",
2380            "void",
2381            "int",
2382            "String",
2383            "boolean",
2384            "true",
2385            "false",
2386            "null",
2387            "try",
2388            "catch",
2389            "throw",
2390            "throws",
2391            "fun",
2392            "val",
2393            "var",
2394            "when",
2395            "object",
2396            "companion",
2397        ],
2398        "sh" | "bash" | "zsh" | "shell" => &[
2399            "if",
2400            "then",
2401            "else",
2402            "elif",
2403            "fi",
2404            "for",
2405            "while",
2406            "do",
2407            "done",
2408            "case",
2409            "esac",
2410            "function",
2411            "return",
2412            "exit",
2413            "echo",
2414            "export",
2415            "local",
2416            "readonly",
2417            "set",
2418            "unset",
2419            "shift",
2420            "source",
2421            "in",
2422            "true",
2423            "false",
2424            "read",
2425            "declare",
2426            "typeset",
2427            "trap",
2428            "eval",
2429            "exec",
2430            "test",
2431            "select",
2432            "until",
2433            "break",
2434            "continue",
2435            "printf",
2436            // Go 命令
2437            "go",
2438            "build",
2439            "run",
2440            "test",
2441            "fmt",
2442            "vet",
2443            "mod",
2444            "get",
2445            "install",
2446            "clean",
2447            "doc",
2448            "list",
2449            "version",
2450            "env",
2451            "generate",
2452            "tool",
2453            "proxy",
2454            "GOPATH",
2455            "GOROOT",
2456            "GOBIN",
2457            "GOMODCACHE",
2458            "GOPROXY",
2459            "GOSUMDB",
2460            // Cargo 命令
2461            "cargo",
2462            "new",
2463            "init",
2464            "add",
2465            "remove",
2466            "update",
2467            "check",
2468            "clippy",
2469            "rustfmt",
2470            "rustc",
2471            "rustup",
2472            "publish",
2473            "install",
2474            "uninstall",
2475            "search",
2476            "tree",
2477            "locate_project",
2478            "metadata",
2479            "audit",
2480            "watch",
2481            "expand",
2482        ],
2483        "c" | "cpp" | "c++" | "h" | "hpp" => &[
2484            "int",
2485            "char",
2486            "float",
2487            "double",
2488            "void",
2489            "long",
2490            "short",
2491            "unsigned",
2492            "signed",
2493            "const",
2494            "static",
2495            "extern",
2496            "struct",
2497            "union",
2498            "enum",
2499            "typedef",
2500            "sizeof",
2501            "return",
2502            "if",
2503            "else",
2504            "for",
2505            "while",
2506            "do",
2507            "switch",
2508            "case",
2509            "break",
2510            "continue",
2511            "default",
2512            "goto",
2513            "auto",
2514            "register",
2515            "volatile",
2516            "class",
2517            "public",
2518            "private",
2519            "protected",
2520            "virtual",
2521            "override",
2522            "template",
2523            "namespace",
2524            "using",
2525            "new",
2526            "delete",
2527            "try",
2528            "catch",
2529            "throw",
2530            "nullptr",
2531            "true",
2532            "false",
2533            "this",
2534            "include",
2535            "define",
2536            "ifdef",
2537            "ifndef",
2538            "endif",
2539        ],
2540        "sql" => &[
2541            "SELECT",
2542            "FROM",
2543            "WHERE",
2544            "INSERT",
2545            "UPDATE",
2546            "DELETE",
2547            "CREATE",
2548            "DROP",
2549            "ALTER",
2550            "TABLE",
2551            "INDEX",
2552            "INTO",
2553            "VALUES",
2554            "SET",
2555            "AND",
2556            "OR",
2557            "NOT",
2558            "NULL",
2559            "JOIN",
2560            "LEFT",
2561            "RIGHT",
2562            "INNER",
2563            "OUTER",
2564            "ON",
2565            "GROUP",
2566            "BY",
2567            "ORDER",
2568            "ASC",
2569            "DESC",
2570            "HAVING",
2571            "LIMIT",
2572            "OFFSET",
2573            "UNION",
2574            "AS",
2575            "DISTINCT",
2576            "COUNT",
2577            "SUM",
2578            "AVG",
2579            "MIN",
2580            "MAX",
2581            "LIKE",
2582            "IN",
2583            "BETWEEN",
2584            "EXISTS",
2585            "CASE",
2586            "WHEN",
2587            "THEN",
2588            "ELSE",
2589            "END",
2590            "BEGIN",
2591            "COMMIT",
2592            "ROLLBACK",
2593            "PRIMARY",
2594            "KEY",
2595            "FOREIGN",
2596            "REFERENCES",
2597            "select",
2598            "from",
2599            "where",
2600            "insert",
2601            "update",
2602            "delete",
2603            "create",
2604            "drop",
2605            "alter",
2606            "table",
2607            "index",
2608            "into",
2609            "values",
2610            "set",
2611            "and",
2612            "or",
2613            "not",
2614            "null",
2615            "join",
2616            "left",
2617            "right",
2618            "inner",
2619            "outer",
2620            "on",
2621            "group",
2622            "by",
2623            "order",
2624            "asc",
2625            "desc",
2626            "having",
2627            "limit",
2628            "offset",
2629            "union",
2630            "as",
2631            "distinct",
2632            "count",
2633            "sum",
2634            "avg",
2635            "min",
2636            "max",
2637            "like",
2638            "in",
2639            "between",
2640            "exists",
2641            "case",
2642            "when",
2643            "then",
2644            "else",
2645            "end",
2646            "begin",
2647            "commit",
2648            "rollback",
2649            "primary",
2650            "key",
2651            "foreign",
2652            "references",
2653        ],
2654        "yaml" | "yml" => &["true", "false", "null", "yes", "no", "on", "off"],
2655        "toml" => &[
2656            "true",
2657            "false",
2658            "true",
2659            "false",
2660            // Cargo.toml 常用
2661            "name",
2662            "version",
2663            "edition",
2664            "authors",
2665            "dependencies",
2666            "dev-dependencies",
2667            "build-dependencies",
2668            "features",
2669            "workspace",
2670            "members",
2671            "exclude",
2672            "include",
2673            "path",
2674            "git",
2675            "branch",
2676            "tag",
2677            "rev",
2678            "package",
2679            "lib",
2680            "bin",
2681            "example",
2682            "test",
2683            "bench",
2684            "doc",
2685            "profile",
2686            "release",
2687            "debug",
2688            "opt-level",
2689            "lto",
2690            "codegen-units",
2691            "panic",
2692            "strip",
2693            "default",
2694            "features",
2695            "optional",
2696            // 常见配置项
2697            "repository",
2698            "homepage",
2699            "documentation",
2700            "license",
2701            "license-file",
2702            "keywords",
2703            "categories",
2704            "readme",
2705            "description",
2706            "resolver",
2707        ],
2708        "css" | "scss" | "less" => &[
2709            "color",
2710            "background",
2711            "border",
2712            "margin",
2713            "padding",
2714            "display",
2715            "position",
2716            "width",
2717            "height",
2718            "font",
2719            "text",
2720            "flex",
2721            "grid",
2722            "align",
2723            "justify",
2724            "important",
2725            "none",
2726            "auto",
2727            "inherit",
2728            "initial",
2729            "unset",
2730        ],
2731        "dockerfile" | "docker" => &[
2732            "FROM",
2733            "RUN",
2734            "CMD",
2735            "LABEL",
2736            "EXPOSE",
2737            "ENV",
2738            "ADD",
2739            "COPY",
2740            "ENTRYPOINT",
2741            "VOLUME",
2742            "USER",
2743            "WORKDIR",
2744            "ARG",
2745            "ONBUILD",
2746            "STOPSIGNAL",
2747            "HEALTHCHECK",
2748            "SHELL",
2749            "AS",
2750        ],
2751        "ruby" | "rb" => &[
2752            "def", "end", "class", "module", "if", "elsif", "else", "unless", "while", "until",
2753            "for", "do", "begin", "rescue", "ensure", "raise", "return", "yield", "require",
2754            "include", "attr", "self", "true", "false", "nil", "puts", "print",
2755        ],
2756        _ => &[
2757            "fn", "function", "def", "class", "return", "if", "else", "for", "while", "import",
2758            "export", "const", "let", "var", "true", "false", "null", "nil", "None", "self",
2759            "this",
2760        ],
2761    };
2762
2763    let comment_prefix = match lang_lower.as_str() {
2764        "python" | "py" | "sh" | "bash" | "zsh" | "shell" | "ruby" | "rb" | "yaml" | "yml"
2765        | "toml" | "dockerfile" | "docker" => "#",
2766        "sql" => "--",
2767        "css" | "scss" | "less" => "/*",
2768        _ => "//",
2769    };
2770
2771    // 默认代码颜色
2772    let code_style = Style::default().fg(Color::Rgb(200, 200, 210));
2773    // 关键字颜色
2774    let kw_style = Style::default().fg(Color::Rgb(198, 120, 221));
2775    // 字符串颜色
2776    let str_style = Style::default().fg(Color::Rgb(152, 195, 121));
2777    // 注释颜色
2778    let comment_style = Style::default()
2779        .fg(Color::Rgb(92, 99, 112))
2780        .add_modifier(Modifier::ITALIC);
2781    // 数字颜色
2782    let num_style = Style::default().fg(Color::Rgb(209, 154, 102));
2783    // 类型/大写开头标识符
2784    let type_style = Style::default().fg(Color::Rgb(229, 192, 123));
2785
2786    let trimmed = line.trim_start();
2787
2788    // 注释行
2789    if trimmed.starts_with(comment_prefix) {
2790        return vec![Span::styled(line.to_string(), comment_style)];
2791    }
2792
2793    // 逐词解析
2794    let mut spans = Vec::new();
2795    let mut chars = line.chars().peekable();
2796    let mut buf = String::new();
2797
2798    while let Some(&ch) = chars.peek() {
2799        // 字符串
2800        if ch == '"' || ch == '\'' || ch == '`' {
2801            // 先刷新 buf
2802            if !buf.is_empty() {
2803                spans.extend(colorize_tokens(
2804                    &buf, keywords, code_style, kw_style, num_style, type_style,
2805                ));
2806                buf.clear();
2807            }
2808            let quote = ch;
2809            let mut s = String::new();
2810            s.push(ch);
2811            chars.next();
2812            while let Some(&c) = chars.peek() {
2813                s.push(c);
2814                chars.next();
2815                if c == quote && !s.ends_with("\\\\") {
2816                    break;
2817                }
2818            }
2819            spans.push(Span::styled(s, str_style));
2820            continue;
2821        }
2822        // Shell 变量 ($VAR, ${VAR}, $1 等)
2823        if ch == '$'
2824            && matches!(
2825                lang_lower.as_str(),
2826                "sh" | "bash" | "zsh" | "shell" | "dockerfile" | "docker"
2827            )
2828        {
2829            if !buf.is_empty() {
2830                spans.extend(colorize_tokens(
2831                    &buf, keywords, code_style, kw_style, num_style, type_style,
2832                ));
2833                buf.clear();
2834            }
2835            let var_style = Style::default().fg(Color::Rgb(86, 182, 194));
2836            let mut var = String::new();
2837            var.push(ch);
2838            chars.next();
2839            if let Some(&next_ch) = chars.peek() {
2840                if next_ch == '{' {
2841                    // ${VAR}
2842                    var.push(next_ch);
2843                    chars.next();
2844                    while let Some(&c) = chars.peek() {
2845                        var.push(c);
2846                        chars.next();
2847                        if c == '}' {
2848                            break;
2849                        }
2850                    }
2851                } else if next_ch == '(' {
2852                    // $(cmd)
2853                    var.push(next_ch);
2854                    chars.next();
2855                    let mut depth = 1;
2856                    while let Some(&c) = chars.peek() {
2857                        var.push(c);
2858                        chars.next();
2859                        if c == '(' {
2860                            depth += 1;
2861                        }
2862                        if c == ')' {
2863                            depth -= 1;
2864                            if depth == 0 {
2865                                break;
2866                            }
2867                        }
2868                    }
2869                } else if next_ch.is_alphanumeric()
2870                    || next_ch == '_'
2871                    || next_ch == '@'
2872                    || next_ch == '#'
2873                    || next_ch == '?'
2874                    || next_ch == '!'
2875                {
2876                    // $VAR, $1, $@, $#, $? 等
2877                    while let Some(&c) = chars.peek() {
2878                        if c.is_alphanumeric() || c == '_' {
2879                            var.push(c);
2880                            chars.next();
2881                        } else {
2882                            break;
2883                        }
2884                    }
2885                }
2886            }
2887            spans.push(Span::styled(var, var_style));
2888            continue;
2889        }
2890        // 行内注释
2891        if ch == '/' || ch == '#' {
2892            let rest: String = chars.clone().collect();
2893            if rest.starts_with(comment_prefix) {
2894                if !buf.is_empty() {
2895                    spans.extend(colorize_tokens(
2896                        &buf, keywords, code_style, kw_style, num_style, type_style,
2897                    ));
2898                    buf.clear();
2899                }
2900                spans.push(Span::styled(rest, comment_style));
2901                break;
2902            }
2903        }
2904        buf.push(ch);
2905        chars.next();
2906    }
2907
2908    if !buf.is_empty() {
2909        spans.extend(colorize_tokens(
2910            &buf, keywords, code_style, kw_style, num_style, type_style,
2911        ));
2912    }
2913
2914    if spans.is_empty() {
2915        spans.push(Span::styled(line.to_string(), code_style));
2916    }
2917
2918    spans
2919}
2920
2921/// 将文本按照 word boundary 拆分并对关键字、数字、类型名着色
2922fn colorize_tokens<'a>(
2923    text: &str,
2924    keywords: &[&str],
2925    default_style: Style,
2926    kw_style: Style,
2927    num_style: Style,
2928    type_style: Style,
2929) -> Vec<Span<'static>> {
2930    let mut spans = Vec::new();
2931    let mut current_word = String::new();
2932    let mut current_non_word = String::new();
2933
2934    for ch in text.chars() {
2935        if ch.is_alphanumeric() || ch == '_' {
2936            if !current_non_word.is_empty() {
2937                spans.push(Span::styled(current_non_word.clone(), default_style));
2938                current_non_word.clear();
2939            }
2940            current_word.push(ch);
2941        } else {
2942            if !current_word.is_empty() {
2943                let style = if keywords.contains(&current_word.as_str()) {
2944                    kw_style
2945                } else if current_word
2946                    .chars()
2947                    .next()
2948                    .map(|c| c.is_ascii_digit())
2949                    .unwrap_or(false)
2950                {
2951                    num_style
2952                } else if current_word
2953                    .chars()
2954                    .next()
2955                    .map(|c| c.is_uppercase())
2956                    .unwrap_or(false)
2957                {
2958                    type_style
2959                } else {
2960                    default_style
2961                };
2962                spans.push(Span::styled(current_word.clone(), style));
2963                current_word.clear();
2964            }
2965            current_non_word.push(ch);
2966        }
2967    }
2968
2969    // 刷新剩余
2970    if !current_non_word.is_empty() {
2971        spans.push(Span::styled(current_non_word, default_style));
2972    }
2973    if !current_word.is_empty() {
2974        let style = if keywords.contains(&current_word.as_str()) {
2975            kw_style
2976        } else if current_word
2977            .chars()
2978            .next()
2979            .map(|c| c.is_ascii_digit())
2980            .unwrap_or(false)
2981        {
2982            num_style
2983        } else if current_word
2984            .chars()
2985            .next()
2986            .map(|c| c.is_uppercase())
2987            .unwrap_or(false)
2988        {
2989            type_style
2990        } else {
2991            default_style
2992        };
2993        spans.push(Span::styled(current_word, style));
2994    }
2995
2996    spans
2997}
2998
2999/// 简单文本自动换行
3000fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
3001    // 最小宽度保证至少能放下一个字符(中文字符宽度2),避免无限循环或不截断
3002    let max_width = max_width.max(2);
3003    let mut result = Vec::new();
3004    let mut current_line = String::new();
3005    let mut current_width = 0;
3006
3007    for ch in text.chars() {
3008        let ch_width = char_width(ch);
3009        if current_width + ch_width > max_width && !current_line.is_empty() {
3010            result.push(current_line.clone());
3011            current_line.clear();
3012            current_width = 0;
3013        }
3014        current_line.push(ch);
3015        current_width += ch_width;
3016    }
3017    if !current_line.is_empty() {
3018        result.push(current_line);
3019    }
3020    if result.is_empty() {
3021        result.push(String::new());
3022    }
3023    result
3024}
3025
3026/// 计算字符串的显示宽度(使用 unicode-width crate,比手动范围匹配更准确)
3027fn display_width(s: &str) -> usize {
3028    use unicode_width::UnicodeWidthStr;
3029    UnicodeWidthStr::width(s)
3030}
3031
3032/// 计算单个字符的显示宽度(使用 unicode-width crate)
3033fn char_width(c: char) -> usize {
3034    use unicode_width::UnicodeWidthChar;
3035    UnicodeWidthChar::width(c).unwrap_or(0)
3036}
3037
3038/// 绘制输入区
3039fn draw_input(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
3040    // 输入区可用宽度(减去边框2 + prompt 4)
3041    let usable_width = area.width.saturating_sub(2 + 4) as usize;
3042
3043    let chars: Vec<char> = app.input.chars().collect();
3044
3045    // 计算光标之前文本的显示宽度,决定是否需要水平滚动
3046    let before_all: String = chars[..app.cursor_pos].iter().collect();
3047    let before_width = display_width(&before_all);
3048
3049    // 如果光标超出可视范围,从光标附近开始显示
3050    let scroll_offset_chars = if before_width >= usable_width {
3051        // 往回找到一个合适的起始字符位置
3052        let target_width = before_width.saturating_sub(usable_width / 2);
3053        let mut w = 0;
3054        let mut skip = 0;
3055        for (i, &ch) in chars.iter().enumerate() {
3056            if w >= target_width {
3057                skip = i;
3058                break;
3059            }
3060            w += char_width(ch);
3061        }
3062        skip
3063    } else {
3064        0
3065    };
3066
3067    // 截取可见部分的字符
3068    let visible_chars = &chars[scroll_offset_chars..];
3069    let cursor_in_visible = app.cursor_pos - scroll_offset_chars;
3070
3071    let before: String = visible_chars[..cursor_in_visible].iter().collect();
3072    let cursor_ch = if cursor_in_visible < visible_chars.len() {
3073        visible_chars[cursor_in_visible].to_string()
3074    } else {
3075        " ".to_string()
3076    };
3077    let after: String = if cursor_in_visible < visible_chars.len() {
3078        visible_chars[cursor_in_visible + 1..].iter().collect()
3079    } else {
3080        String::new()
3081    };
3082
3083    let prompt_style = if app.is_loading {
3084        Style::default().fg(Color::Rgb(255, 200, 80))
3085    } else {
3086        Style::default().fg(Color::Rgb(100, 200, 130))
3087    };
3088    let prompt_text = if app.is_loading { " .. " } else { " >  " };
3089
3090    // 构建多行输入显示(手动换行)
3091    let full_visible = format!("{}{}{}", before, cursor_ch, after);
3092    let inner_height = area.height.saturating_sub(2) as usize; // 减去边框
3093    let wrapped_lines = wrap_text(&full_visible, usable_width);
3094
3095    // 找到光标所在的行索引
3096    let before_len = before.chars().count();
3097    let cursor_len = cursor_ch.chars().count();
3098    let cursor_global_pos = before_len; // 光标在全部可见字符中的位置
3099    let mut cursor_line_idx: usize = 0;
3100    {
3101        let mut cumulative = 0usize;
3102        for (li, wl) in wrapped_lines.iter().enumerate() {
3103            let line_char_count = wl.chars().count();
3104            if cumulative + line_char_count > cursor_global_pos {
3105                cursor_line_idx = li;
3106                break;
3107            }
3108            cumulative += line_char_count;
3109            cursor_line_idx = li; // 光标恰好在最后一行末尾
3110        }
3111    }
3112
3113    // 计算行滚动:确保光标所在行在可见区域内
3114    let line_scroll = if wrapped_lines.len() <= inner_height {
3115        0
3116    } else if cursor_line_idx < inner_height {
3117        0
3118    } else {
3119        // 让光标行显示在可见区域的最后一行
3120        cursor_line_idx.saturating_sub(inner_height - 1)
3121    };
3122
3123    // 构建带光标高亮的行
3124    let mut display_lines: Vec<Line> = Vec::new();
3125    let mut char_offset: usize = 0;
3126    // 跳过滚动行的字符数
3127    for wl in wrapped_lines.iter().take(line_scroll) {
3128        char_offset += wl.chars().count();
3129    }
3130
3131    for (_line_idx, wl) in wrapped_lines
3132        .iter()
3133        .skip(line_scroll)
3134        .enumerate()
3135        .take(inner_height.max(1))
3136    {
3137        let mut spans: Vec<Span> = Vec::new();
3138        if _line_idx == 0 && line_scroll == 0 {
3139            spans.push(Span::styled(prompt_text, prompt_style));
3140        } else {
3141            spans.push(Span::styled("    ", Style::default())); // 对齐 prompt
3142        }
3143
3144        // 对该行的每个字符分配样式
3145        let line_chars: Vec<char> = wl.chars().collect();
3146        let mut seg_start = 0;
3147        for (ci, &ch) in line_chars.iter().enumerate() {
3148            let global_idx = char_offset + ci;
3149            let is_cursor = global_idx >= before_len && global_idx < before_len + cursor_len;
3150
3151            if is_cursor {
3152                // 先把 cursor 前的部分输出
3153                if ci > seg_start {
3154                    let seg: String = line_chars[seg_start..ci].iter().collect();
3155                    spans.push(Span::styled(seg, Style::default().fg(Color::White)));
3156                }
3157                spans.push(Span::styled(
3158                    ch.to_string(),
3159                    Style::default()
3160                        .fg(Color::Rgb(22, 22, 30))
3161                        .bg(Color::Rgb(200, 210, 240)),
3162                ));
3163                seg_start = ci + 1;
3164            }
3165        }
3166        // 输出剩余部分
3167        if seg_start < line_chars.len() {
3168            let seg: String = line_chars[seg_start..].iter().collect();
3169            spans.push(Span::styled(seg, Style::default().fg(Color::White)));
3170        }
3171
3172        char_offset += line_chars.len();
3173        display_lines.push(Line::from(spans));
3174    }
3175
3176    if display_lines.is_empty() {
3177        display_lines.push(Line::from(vec![
3178            Span::styled(prompt_text, prompt_style),
3179            Span::styled(
3180                " ",
3181                Style::default()
3182                    .fg(Color::Rgb(22, 22, 30))
3183                    .bg(Color::Rgb(200, 210, 240)),
3184            ),
3185        ]));
3186    }
3187
3188    let input_widget = Paragraph::new(display_lines).block(
3189        Block::default()
3190            .borders(Borders::ALL)
3191            .border_type(ratatui::widgets::BorderType::Rounded)
3192            .border_style(if app.is_loading {
3193                Style::default().fg(Color::Rgb(120, 100, 50))
3194            } else {
3195                Style::default().fg(Color::Rgb(60, 100, 80))
3196            })
3197            .title(Span::styled(
3198                " 输入消息 ",
3199                Style::default().fg(Color::Rgb(140, 140, 170)),
3200            ))
3201            .style(Style::default().bg(Color::Rgb(26, 26, 38))),
3202    );
3203
3204    f.render_widget(input_widget, area);
3205
3206    // 设置终端光标位置,确保中文输入法 IME 候选窗口在正确位置
3207    // 计算光标在渲染后的坐标
3208    if !app.is_loading {
3209        let prompt_w: u16 = 4; // prompt 宽度
3210        let border_left: u16 = 1; // 左边框
3211
3212        // 光标在当前显示行中的列偏移
3213        let cursor_col_in_line = {
3214            let mut col = 0usize;
3215            let mut char_count = 0usize;
3216            // 跳过 line_scroll 之前的字符
3217            let mut skip_chars = 0usize;
3218            for wl in wrapped_lines.iter().take(line_scroll) {
3219                skip_chars += wl.chars().count();
3220            }
3221            // 找到光标在当前行的列
3222            for wl in wrapped_lines.iter().skip(line_scroll) {
3223                let line_len = wl.chars().count();
3224                if skip_chars + char_count + line_len > cursor_global_pos {
3225                    // 光标在这一行
3226                    let pos_in_line = cursor_global_pos - (skip_chars + char_count);
3227                    col = wl.chars().take(pos_in_line).map(|c| char_width(c)).sum();
3228                    break;
3229                }
3230                char_count += line_len;
3231            }
3232            col as u16
3233        };
3234
3235        // 光标在显示行中的行偏移
3236        let cursor_row_in_display = (cursor_line_idx - line_scroll) as u16;
3237
3238        let cursor_x = area.x + border_left + prompt_w + cursor_col_in_line;
3239        let cursor_y = area.y + 1 + cursor_row_in_display; // +1 跳过上边框
3240
3241        // 确保光标在区域内
3242        if cursor_x < area.x + area.width && cursor_y < area.y + area.height {
3243            f.set_cursor_position((cursor_x, cursor_y));
3244        }
3245    }
3246}
3247
3248/// 绘制底部操作提示栏(始终可见)
3249fn draw_hint_bar(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
3250    let hints = match app.mode {
3251        ChatMode::Chat => {
3252            vec![
3253                ("Enter", "发送"),
3254                ("↑↓", "滚动"),
3255                ("Ctrl+T", "切换模型"),
3256                ("Ctrl+L", "清空"),
3257                ("Ctrl+Y", "复制"),
3258                ("Ctrl+B", "浏览"),
3259                ("Ctrl+S", "流式切换"),
3260                ("Ctrl+E", "配置"),
3261                ("?/F1", "帮助"),
3262                ("Esc", "退出"),
3263            ]
3264        }
3265        ChatMode::SelectModel => {
3266            vec![("↑↓/jk", "移动"), ("Enter", "确认"), ("Esc", "取消")]
3267        }
3268        ChatMode::Browse => {
3269            vec![("↑↓", "选择消息"), ("y/Enter", "复制"), ("Esc", "返回")]
3270        }
3271        ChatMode::Help => {
3272            vec![("任意键", "返回")]
3273        }
3274        ChatMode::Config => {
3275            vec![
3276                ("↑↓", "切换字段"),
3277                ("Enter", "编辑"),
3278                ("Tab", "切换 Provider"),
3279                ("a", "新增"),
3280                ("d", "删除"),
3281                ("Esc", "保存返回"),
3282            ]
3283        }
3284    };
3285
3286    let mut spans: Vec<Span> = Vec::new();
3287    spans.push(Span::styled(" ", Style::default()));
3288    for (i, (key, desc)) in hints.iter().enumerate() {
3289        if i > 0 {
3290            spans.push(Span::styled(
3291                "  │  ",
3292                Style::default().fg(Color::Rgb(50, 50, 65)),
3293            ));
3294        }
3295        spans.push(Span::styled(
3296            format!(" {} ", key),
3297            Style::default()
3298                .fg(Color::Rgb(22, 22, 30))
3299                .bg(Color::Rgb(100, 110, 140)),
3300        ));
3301        spans.push(Span::styled(
3302            format!(" {}", desc),
3303            Style::default().fg(Color::Rgb(120, 120, 150)),
3304        ));
3305    }
3306
3307    let hint_bar =
3308        Paragraph::new(Line::from(spans)).style(Style::default().bg(Color::Rgb(22, 22, 30)));
3309    f.render_widget(hint_bar, area);
3310}
3311
3312/// 绘制 Toast 弹窗(右上角浮层)
3313fn draw_toast(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
3314    if let Some((ref msg, is_error, _)) = app.toast {
3315        let text_width = display_width(msg);
3316        // toast 宽度 = 文字宽度 + 左右 padding(各2) + emoji(2) + border(2)
3317        let toast_width = (text_width + 10).min(area.width as usize).max(16) as u16;
3318        let toast_height: u16 = 3;
3319
3320        // 定位到右上角
3321        let x = area.width.saturating_sub(toast_width + 1);
3322        let y: u16 = 1;
3323
3324        if x + toast_width <= area.width && y + toast_height <= area.height {
3325            let toast_area = Rect::new(x, y, toast_width, toast_height);
3326
3327            // 先清空区域背景
3328            let clear = Block::default().style(Style::default().bg(if is_error {
3329                Color::Rgb(60, 20, 20)
3330            } else {
3331                Color::Rgb(20, 50, 30)
3332            }));
3333            f.render_widget(clear, toast_area);
3334
3335            let (icon, border_color, text_color) = if is_error {
3336                ("❌", Color::Rgb(200, 70, 70), Color::Rgb(255, 130, 130))
3337            } else {
3338                ("✅", Color::Rgb(60, 160, 80), Color::Rgb(140, 230, 160))
3339            };
3340
3341            let toast_widget = Paragraph::new(Line::from(vec![
3342                Span::styled(format!(" {} ", icon), Style::default()),
3343                Span::styled(msg.as_str(), Style::default().fg(text_color)),
3344            ]))
3345            .block(
3346                Block::default()
3347                    .borders(Borders::ALL)
3348                    .border_type(ratatui::widgets::BorderType::Rounded)
3349                    .border_style(Style::default().fg(border_color))
3350                    .style(Style::default().bg(if is_error {
3351                        Color::Rgb(50, 18, 18)
3352                    } else {
3353                        Color::Rgb(18, 40, 25)
3354                    })),
3355            );
3356            f.render_widget(toast_widget, toast_area);
3357        }
3358    }
3359}
3360
3361/// 绘制模型选择界面
3362fn draw_model_selector(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
3363    let items: Vec<ListItem> = app
3364        .agent_config
3365        .providers
3366        .iter()
3367        .enumerate()
3368        .map(|(i, p)| {
3369            let is_active = i == app.agent_config.active_index;
3370            let marker = if is_active { " ● " } else { " ○ " };
3371            let style = if is_active {
3372                Style::default()
3373                    .fg(Color::Rgb(120, 220, 160))
3374                    .add_modifier(Modifier::BOLD)
3375            } else {
3376                Style::default().fg(Color::Rgb(180, 180, 200))
3377            };
3378            let detail = format!("{}{}  ({})", marker, p.name, p.model);
3379            ListItem::new(Line::from(Span::styled(detail, style)))
3380        })
3381        .collect();
3382
3383    let list = List::new(items)
3384        .block(
3385            Block::default()
3386                .borders(Borders::ALL)
3387                .border_type(ratatui::widgets::BorderType::Rounded)
3388                .border_style(Style::default().fg(Color::Rgb(180, 160, 80)))
3389                .title(Span::styled(
3390                    " 🔄 选择模型 ",
3391                    Style::default()
3392                        .fg(Color::Rgb(230, 210, 120))
3393                        .add_modifier(Modifier::BOLD),
3394                ))
3395                .style(Style::default().bg(Color::Rgb(28, 28, 40))),
3396        )
3397        .highlight_style(
3398            Style::default()
3399                .bg(Color::Rgb(50, 55, 80))
3400                .fg(Color::White)
3401                .add_modifier(Modifier::BOLD),
3402        )
3403        .highlight_symbol("  ▸ ");
3404
3405    f.render_stateful_widget(list, area, &mut app.model_list_state);
3406}
3407
3408/// 绘制帮助界面
3409fn draw_help(f: &mut ratatui::Frame, area: Rect) {
3410    let separator = Line::from(Span::styled(
3411        "  ─────────────────────────────────────────",
3412        Style::default().fg(Color::Rgb(50, 55, 70)),
3413    ));
3414
3415    let help_lines = vec![
3416        Line::from(""),
3417        Line::from(Span::styled(
3418            "  📖 快捷键帮助",
3419            Style::default()
3420                .fg(Color::Rgb(120, 180, 255))
3421                .add_modifier(Modifier::BOLD),
3422        )),
3423        Line::from(""),
3424        separator.clone(),
3425        Line::from(""),
3426        Line::from(vec![
3427            Span::styled(
3428                "  Enter        ",
3429                Style::default()
3430                    .fg(Color::Rgb(230, 210, 120))
3431                    .add_modifier(Modifier::BOLD),
3432            ),
3433            Span::styled("发送消息", Style::default().fg(Color::Rgb(200, 200, 220))),
3434        ]),
3435        Line::from(vec![
3436            Span::styled(
3437                "  ↑ / ↓        ",
3438                Style::default()
3439                    .fg(Color::Rgb(230, 210, 120))
3440                    .add_modifier(Modifier::BOLD),
3441            ),
3442            Span::styled(
3443                "滚动对话记录",
3444                Style::default().fg(Color::Rgb(200, 200, 220)),
3445            ),
3446        ]),
3447        Line::from(vec![
3448            Span::styled(
3449                "  ← / →        ",
3450                Style::default()
3451                    .fg(Color::Rgb(230, 210, 120))
3452                    .add_modifier(Modifier::BOLD),
3453            ),
3454            Span::styled(
3455                "移动输入光标",
3456                Style::default().fg(Color::Rgb(200, 200, 220)),
3457            ),
3458        ]),
3459        Line::from(vec![
3460            Span::styled(
3461                "  Ctrl+T       ",
3462                Style::default()
3463                    .fg(Color::Rgb(230, 210, 120))
3464                    .add_modifier(Modifier::BOLD),
3465            ),
3466            Span::styled("切换模型", Style::default().fg(Color::Rgb(200, 200, 220))),
3467        ]),
3468        Line::from(vec![
3469            Span::styled(
3470                "  Ctrl+L       ",
3471                Style::default()
3472                    .fg(Color::Rgb(230, 210, 120))
3473                    .add_modifier(Modifier::BOLD),
3474            ),
3475            Span::styled(
3476                "清空对话历史",
3477                Style::default().fg(Color::Rgb(200, 200, 220)),
3478            ),
3479        ]),
3480        Line::from(vec![
3481            Span::styled(
3482                "  Ctrl+Y       ",
3483                Style::default()
3484                    .fg(Color::Rgb(230, 210, 120))
3485                    .add_modifier(Modifier::BOLD),
3486            ),
3487            Span::styled(
3488                "复制最后一条 AI 回复",
3489                Style::default().fg(Color::Rgb(200, 200, 220)),
3490            ),
3491        ]),
3492        Line::from(vec![
3493            Span::styled(
3494                "  Ctrl+B       ",
3495                Style::default()
3496                    .fg(Color::Rgb(230, 210, 120))
3497                    .add_modifier(Modifier::BOLD),
3498            ),
3499            Span::styled(
3500                "浏览消息 (↑↓选择, y/Enter复制)",
3501                Style::default().fg(Color::Rgb(200, 200, 220)),
3502            ),
3503        ]),
3504        Line::from(vec![
3505            Span::styled(
3506                "  Ctrl+S       ",
3507                Style::default()
3508                    .fg(Color::Rgb(230, 210, 120))
3509                    .add_modifier(Modifier::BOLD),
3510            ),
3511            Span::styled(
3512                "切换流式/整体输出",
3513                Style::default().fg(Color::Rgb(200, 200, 220)),
3514            ),
3515        ]),
3516        Line::from(vec![
3517            Span::styled(
3518                "  Ctrl+E       ",
3519                Style::default()
3520                    .fg(Color::Rgb(230, 210, 120))
3521                    .add_modifier(Modifier::BOLD),
3522            ),
3523            Span::styled(
3524                "打开配置界面",
3525                Style::default().fg(Color::Rgb(200, 200, 220)),
3526            ),
3527        ]),
3528        Line::from(vec![
3529            Span::styled(
3530                "  Esc / Ctrl+C ",
3531                Style::default()
3532                    .fg(Color::Rgb(230, 210, 120))
3533                    .add_modifier(Modifier::BOLD),
3534            ),
3535            Span::styled("退出对话", Style::default().fg(Color::Rgb(200, 200, 220))),
3536        ]),
3537        Line::from(vec![
3538            Span::styled(
3539                "  ? / F1       ",
3540                Style::default()
3541                    .fg(Color::Rgb(230, 210, 120))
3542                    .add_modifier(Modifier::BOLD),
3543            ),
3544            Span::styled(
3545                "显示 / 关闭此帮助",
3546                Style::default().fg(Color::Rgb(200, 200, 220)),
3547            ),
3548        ]),
3549        Line::from(""),
3550        separator,
3551        Line::from(""),
3552        Line::from(Span::styled(
3553            "  📁 配置文件:",
3554            Style::default()
3555                .fg(Color::Rgb(120, 180, 255))
3556                .add_modifier(Modifier::BOLD),
3557        )),
3558        Line::from(Span::styled(
3559            format!("     {}", agent_config_path().display()),
3560            Style::default().fg(Color::Rgb(100, 100, 130)),
3561        )),
3562    ];
3563
3564    let help_block = Block::default()
3565        .borders(Borders::ALL)
3566        .border_type(ratatui::widgets::BorderType::Rounded)
3567        .border_style(Style::default().fg(Color::Rgb(80, 100, 140)))
3568        .title(Span::styled(
3569            " 帮助 (按任意键返回) ",
3570            Style::default().fg(Color::Rgb(140, 140, 170)),
3571        ))
3572        .style(Style::default().bg(Color::Rgb(24, 24, 34)));
3573    let help_widget = Paragraph::new(help_lines).block(help_block);
3574    f.render_widget(help_widget, area);
3575}
3576
3577/// 对话模式按键处理,返回 true 表示退出
3578fn handle_chat_mode(app: &mut ChatApp, key: KeyEvent) -> bool {
3579    // Ctrl+C 强制退出
3580    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
3581        return true;
3582    }
3583
3584    // Ctrl+T 切换模型(替代 Ctrl+M,因为 Ctrl+M 在终端中等于 Enter)
3585    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('t') {
3586        if !app.agent_config.providers.is_empty() {
3587            app.mode = ChatMode::SelectModel;
3588            app.model_list_state
3589                .select(Some(app.agent_config.active_index));
3590        }
3591        return false;
3592    }
3593
3594    // Ctrl+L 清空对话
3595    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('l') {
3596        app.clear_session();
3597        return false;
3598    }
3599
3600    // Ctrl+Y 复制最后一条 AI 回复
3601    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('y') {
3602        if let Some(last_ai) = app
3603            .session
3604            .messages
3605            .iter()
3606            .rev()
3607            .find(|m| m.role == "assistant")
3608        {
3609            if copy_to_clipboard(&last_ai.content) {
3610                app.show_toast("已复制最后一条 AI 回复", false);
3611            } else {
3612                app.show_toast("复制到剪切板失败", true);
3613            }
3614        } else {
3615            app.show_toast("暂无 AI 回复可复制", true);
3616        }
3617        return false;
3618    }
3619
3620    // Ctrl+B 进入消息浏览模式(可选中历史消息并复制)
3621    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('b') {
3622        if !app.session.messages.is_empty() {
3623            // 默认选中最后一条消息
3624            app.browse_msg_index = app.session.messages.len() - 1;
3625            app.mode = ChatMode::Browse;
3626            app.msg_lines_cache = None; // 清除缓存以触发高亮重绘
3627        } else {
3628            app.show_toast("暂无消息可浏览", true);
3629        }
3630        return false;
3631    }
3632
3633    // Ctrl+E 打开配置界面
3634    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('e') {
3635        // 初始化配置界面状态
3636        app.config_provider_idx = app
3637            .agent_config
3638            .active_index
3639            .min(app.agent_config.providers.len().saturating_sub(1));
3640        app.config_field_idx = 0;
3641        app.config_editing = false;
3642        app.config_edit_buf.clear();
3643        app.mode = ChatMode::Config;
3644        return false;
3645    }
3646
3647    // Ctrl+S 切换流式/非流式输出
3648    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('s') {
3649        app.agent_config.stream_mode = !app.agent_config.stream_mode;
3650        let _ = save_agent_config(&app.agent_config);
3651        let mode_str = if app.agent_config.stream_mode {
3652            "流式输出"
3653        } else {
3654            "整体输出"
3655        };
3656        app.show_toast(&format!("已切换为: {}", mode_str), false);
3657        return false;
3658    }
3659
3660    let char_count = app.input.chars().count();
3661
3662    match key.code {
3663        KeyCode::Esc => return true,
3664
3665        KeyCode::Enter => {
3666            if !app.is_loading {
3667                app.send_message();
3668            }
3669        }
3670
3671        // 滚动消息
3672        KeyCode::Up => app.scroll_up(),
3673        KeyCode::Down => app.scroll_down(),
3674        KeyCode::PageUp => {
3675            for _ in 0..10 {
3676                app.scroll_up();
3677            }
3678        }
3679        KeyCode::PageDown => {
3680            for _ in 0..10 {
3681                app.scroll_down();
3682            }
3683        }
3684
3685        // 光标移动
3686        KeyCode::Left => {
3687            if app.cursor_pos > 0 {
3688                app.cursor_pos -= 1;
3689            }
3690        }
3691        KeyCode::Right => {
3692            if app.cursor_pos < char_count {
3693                app.cursor_pos += 1;
3694            }
3695        }
3696        KeyCode::Home => app.cursor_pos = 0,
3697        KeyCode::End => app.cursor_pos = char_count,
3698
3699        // 删除
3700        KeyCode::Backspace => {
3701            if app.cursor_pos > 0 {
3702                let start = app
3703                    .input
3704                    .char_indices()
3705                    .nth(app.cursor_pos - 1)
3706                    .map(|(i, _)| i)
3707                    .unwrap_or(0);
3708                let end = app
3709                    .input
3710                    .char_indices()
3711                    .nth(app.cursor_pos)
3712                    .map(|(i, _)| i)
3713                    .unwrap_or(app.input.len());
3714                app.input.drain(start..end);
3715                app.cursor_pos -= 1;
3716            }
3717        }
3718        KeyCode::Delete => {
3719            if app.cursor_pos < char_count {
3720                let start = app
3721                    .input
3722                    .char_indices()
3723                    .nth(app.cursor_pos)
3724                    .map(|(i, _)| i)
3725                    .unwrap_or(app.input.len());
3726                let end = app
3727                    .input
3728                    .char_indices()
3729                    .nth(app.cursor_pos + 1)
3730                    .map(|(i, _)| i)
3731                    .unwrap_or(app.input.len());
3732                app.input.drain(start..end);
3733            }
3734        }
3735
3736        // F1 任何时候都能唤起帮助
3737        KeyCode::F(1) => {
3738            app.mode = ChatMode::Help;
3739        }
3740        // 输入框为空时,? 也可唤起帮助
3741        KeyCode::Char('?') if app.input.is_empty() => {
3742            app.mode = ChatMode::Help;
3743        }
3744        KeyCode::Char(c) => {
3745            let byte_idx = app
3746                .input
3747                .char_indices()
3748                .nth(app.cursor_pos)
3749                .map(|(i, _)| i)
3750                .unwrap_or(app.input.len());
3751            app.input.insert_str(byte_idx, &c.to_string());
3752            app.cursor_pos += 1;
3753        }
3754
3755        _ => {}
3756    }
3757
3758    false
3759}
3760
3761/// 消息浏览模式按键处理:↑↓ 选择消息,y/Enter 复制选中消息,Esc 退出
3762fn handle_browse_mode(app: &mut ChatApp, key: KeyEvent) {
3763    let msg_count = app.session.messages.len();
3764    if msg_count == 0 {
3765        app.mode = ChatMode::Chat;
3766        app.msg_lines_cache = None;
3767        return;
3768    }
3769
3770    match key.code {
3771        KeyCode::Esc => {
3772            app.mode = ChatMode::Chat;
3773            app.msg_lines_cache = None; // 退出浏览模式时清除缓存,去掉高亮
3774        }
3775        KeyCode::Up | KeyCode::Char('k') => {
3776            if app.browse_msg_index > 0 {
3777                app.browse_msg_index -= 1;
3778                app.msg_lines_cache = None; // 选中变化时清缓存
3779            }
3780        }
3781        KeyCode::Down | KeyCode::Char('j') => {
3782            if app.browse_msg_index < msg_count - 1 {
3783                app.browse_msg_index += 1;
3784                app.msg_lines_cache = None; // 选中变化时清缓存
3785            }
3786        }
3787        KeyCode::Enter | KeyCode::Char('y') => {
3788            // 复制选中消息的原始内容到剪切板
3789            if let Some(msg) = app.session.messages.get(app.browse_msg_index) {
3790                let content = msg.content.clone();
3791                let role_label = if msg.role == "assistant" {
3792                    "AI"
3793                } else if msg.role == "user" {
3794                    "用户"
3795                } else {
3796                    "系统"
3797                };
3798                if copy_to_clipboard(&content) {
3799                    app.show_toast(
3800                        &format!("已复制第 {} 条{}消息", app.browse_msg_index + 1, role_label),
3801                        false,
3802                    );
3803                } else {
3804                    app.show_toast("复制到剪切板失败", true);
3805                }
3806            }
3807        }
3808        _ => {}
3809    }
3810}
3811
3812/// 获取配置界面中当前字段的标签
3813fn config_field_label(idx: usize) -> &'static str {
3814    let total_provider = CONFIG_FIELDS.len();
3815    if idx < total_provider {
3816        match CONFIG_FIELDS[idx] {
3817            "name" => "显示名称",
3818            "api_base" => "API Base",
3819            "api_key" => "API Key",
3820            "model" => "模型名称",
3821            _ => CONFIG_FIELDS[idx],
3822        }
3823    } else {
3824        let gi = idx - total_provider;
3825        match CONFIG_GLOBAL_FIELDS[gi] {
3826            "system_prompt" => "系统提示词",
3827            "stream_mode" => "流式输出",
3828            _ => CONFIG_GLOBAL_FIELDS[gi],
3829        }
3830    }
3831}
3832
3833/// 获取配置界面中当前字段的值
3834fn config_field_value(app: &ChatApp, field_idx: usize) -> String {
3835    let total_provider = CONFIG_FIELDS.len();
3836    if field_idx < total_provider {
3837        if app.agent_config.providers.is_empty() {
3838            return String::new();
3839        }
3840        let p = &app.agent_config.providers[app.config_provider_idx];
3841        match CONFIG_FIELDS[field_idx] {
3842            "name" => p.name.clone(),
3843            "api_base" => p.api_base.clone(),
3844            "api_key" => {
3845                // 显示时隐藏 API Key 中间部分
3846                if p.api_key.len() > 8 {
3847                    format!(
3848                        "{}****{}",
3849                        &p.api_key[..4],
3850                        &p.api_key[p.api_key.len() - 4..]
3851                    )
3852                } else {
3853                    p.api_key.clone()
3854                }
3855            }
3856            "model" => p.model.clone(),
3857            _ => String::new(),
3858        }
3859    } else {
3860        let gi = field_idx - total_provider;
3861        match CONFIG_GLOBAL_FIELDS[gi] {
3862            "system_prompt" => app.agent_config.system_prompt.clone().unwrap_or_default(),
3863            "stream_mode" => {
3864                if app.agent_config.stream_mode {
3865                    "开启".into()
3866                } else {
3867                    "关闭".into()
3868                }
3869            }
3870            _ => String::new(),
3871        }
3872    }
3873}
3874
3875/// 获取配置字段的原始值(用于编辑时填入输入框)
3876fn config_field_raw_value(app: &ChatApp, field_idx: usize) -> String {
3877    let total_provider = CONFIG_FIELDS.len();
3878    if field_idx < total_provider {
3879        if app.agent_config.providers.is_empty() {
3880            return String::new();
3881        }
3882        let p = &app.agent_config.providers[app.config_provider_idx];
3883        match CONFIG_FIELDS[field_idx] {
3884            "name" => p.name.clone(),
3885            "api_base" => p.api_base.clone(),
3886            "api_key" => p.api_key.clone(),
3887            "model" => p.model.clone(),
3888            _ => String::new(),
3889        }
3890    } else {
3891        let gi = field_idx - total_provider;
3892        match CONFIG_GLOBAL_FIELDS[gi] {
3893            "system_prompt" => app.agent_config.system_prompt.clone().unwrap_or_default(),
3894            "stream_mode" => {
3895                if app.agent_config.stream_mode {
3896                    "true".into()
3897                } else {
3898                    "false".into()
3899                }
3900            }
3901            _ => String::new(),
3902        }
3903    }
3904}
3905
3906/// 将编辑结果写回配置
3907fn config_field_set(app: &mut ChatApp, field_idx: usize, value: &str) {
3908    let total_provider = CONFIG_FIELDS.len();
3909    if field_idx < total_provider {
3910        if app.agent_config.providers.is_empty() {
3911            return;
3912        }
3913        let p = &mut app.agent_config.providers[app.config_provider_idx];
3914        match CONFIG_FIELDS[field_idx] {
3915            "name" => p.name = value.to_string(),
3916            "api_base" => p.api_base = value.to_string(),
3917            "api_key" => p.api_key = value.to_string(),
3918            "model" => p.model = value.to_string(),
3919            _ => {}
3920        }
3921    } else {
3922        let gi = field_idx - total_provider;
3923        match CONFIG_GLOBAL_FIELDS[gi] {
3924            "system_prompt" => {
3925                if value.is_empty() {
3926                    app.agent_config.system_prompt = None;
3927                } else {
3928                    app.agent_config.system_prompt = Some(value.to_string());
3929                }
3930            }
3931            "stream_mode" => {
3932                app.agent_config.stream_mode = matches!(
3933                    value.trim().to_lowercase().as_str(),
3934                    "true" | "1" | "开启" | "on" | "yes"
3935                );
3936            }
3937            _ => {}
3938        }
3939    }
3940}
3941
3942/// 配置模式按键处理
3943fn handle_config_mode(app: &mut ChatApp, key: KeyEvent) {
3944    let total_fields = config_total_fields();
3945
3946    if app.config_editing {
3947        // 正在编辑某个字段
3948        match key.code {
3949            KeyCode::Esc => {
3950                // 取消编辑
3951                app.config_editing = false;
3952            }
3953            KeyCode::Enter => {
3954                // 确认编辑
3955                let val = app.config_edit_buf.clone();
3956                config_field_set(app, app.config_field_idx, &val);
3957                app.config_editing = false;
3958            }
3959            KeyCode::Backspace => {
3960                if app.config_edit_cursor > 0 {
3961                    let idx = app
3962                        .config_edit_buf
3963                        .char_indices()
3964                        .nth(app.config_edit_cursor - 1)
3965                        .map(|(i, _)| i)
3966                        .unwrap_or(0);
3967                    let end_idx = app
3968                        .config_edit_buf
3969                        .char_indices()
3970                        .nth(app.config_edit_cursor)
3971                        .map(|(i, _)| i)
3972                        .unwrap_or(app.config_edit_buf.len());
3973                    app.config_edit_buf = format!(
3974                        "{}{}",
3975                        &app.config_edit_buf[..idx],
3976                        &app.config_edit_buf[end_idx..]
3977                    );
3978                    app.config_edit_cursor -= 1;
3979                }
3980            }
3981            KeyCode::Left => {
3982                app.config_edit_cursor = app.config_edit_cursor.saturating_sub(1);
3983            }
3984            KeyCode::Right => {
3985                let char_count = app.config_edit_buf.chars().count();
3986                if app.config_edit_cursor < char_count {
3987                    app.config_edit_cursor += 1;
3988                }
3989            }
3990            KeyCode::Char(c) => {
3991                let byte_idx = app
3992                    .config_edit_buf
3993                    .char_indices()
3994                    .nth(app.config_edit_cursor)
3995                    .map(|(i, _)| i)
3996                    .unwrap_or(app.config_edit_buf.len());
3997                app.config_edit_buf.insert(byte_idx, c);
3998                app.config_edit_cursor += 1;
3999            }
4000            _ => {}
4001        }
4002        return;
4003    }
4004
4005    // 非编辑状态
4006    match key.code {
4007        KeyCode::Esc => {
4008            // 保存并返回
4009            let _ = save_agent_config(&app.agent_config);
4010            app.show_toast("配置已保存 ✅", false);
4011            app.mode = ChatMode::Chat;
4012        }
4013        KeyCode::Up | KeyCode::Char('k') => {
4014            if total_fields > 0 {
4015                if app.config_field_idx == 0 {
4016                    app.config_field_idx = total_fields - 1;
4017                } else {
4018                    app.config_field_idx -= 1;
4019                }
4020            }
4021        }
4022        KeyCode::Down | KeyCode::Char('j') => {
4023            if total_fields > 0 {
4024                app.config_field_idx = (app.config_field_idx + 1) % total_fields;
4025            }
4026        }
4027        KeyCode::Tab | KeyCode::Right => {
4028            // 切换 provider
4029            let count = app.agent_config.providers.len();
4030            if count > 1 {
4031                app.config_provider_idx = (app.config_provider_idx + 1) % count;
4032                // 切换后如果在 provider 字段区域,保持字段位置不变
4033            }
4034        }
4035        KeyCode::BackTab | KeyCode::Left => {
4036            // 反向切换 provider
4037            let count = app.agent_config.providers.len();
4038            if count > 1 {
4039                if app.config_provider_idx == 0 {
4040                    app.config_provider_idx = count - 1;
4041                } else {
4042                    app.config_provider_idx -= 1;
4043                }
4044            }
4045        }
4046        KeyCode::Enter => {
4047            // 进入编辑模式
4048            let total_provider = CONFIG_FIELDS.len();
4049            if app.config_field_idx < total_provider && app.agent_config.providers.is_empty() {
4050                app.show_toast("还没有 Provider,按 a 新增", true);
4051                return;
4052            }
4053            // stream_mode 字段直接切换,不进入编辑模式
4054            let gi = app.config_field_idx.checked_sub(total_provider);
4055            if let Some(gi) = gi {
4056                if CONFIG_GLOBAL_FIELDS[gi] == "stream_mode" {
4057                    app.agent_config.stream_mode = !app.agent_config.stream_mode;
4058                    return;
4059                }
4060            }
4061            app.config_edit_buf = config_field_raw_value(app, app.config_field_idx);
4062            app.config_edit_cursor = app.config_edit_buf.chars().count();
4063            app.config_editing = true;
4064        }
4065        KeyCode::Char('a') => {
4066            // 新增 Provider
4067            let new_provider = ModelProvider {
4068                name: format!("Provider-{}", app.agent_config.providers.len() + 1),
4069                api_base: "https://api.openai.com/v1".to_string(),
4070                api_key: String::new(),
4071                model: String::new(),
4072            };
4073            app.agent_config.providers.push(new_provider);
4074            app.config_provider_idx = app.agent_config.providers.len() - 1;
4075            app.config_field_idx = 0; // 跳到 name 字段
4076            app.show_toast("已新增 Provider,请填写配置", false);
4077        }
4078        KeyCode::Char('d') => {
4079            // 删除当前 Provider
4080            let count = app.agent_config.providers.len();
4081            if count == 0 {
4082                app.show_toast("没有可删除的 Provider", true);
4083            } else {
4084                let removed_name = app.agent_config.providers[app.config_provider_idx]
4085                    .name
4086                    .clone();
4087                app.agent_config.providers.remove(app.config_provider_idx);
4088                // 调整索引
4089                if app.config_provider_idx >= app.agent_config.providers.len()
4090                    && app.config_provider_idx > 0
4091                {
4092                    app.config_provider_idx -= 1;
4093                }
4094                // 调整 active_index
4095                if app.agent_config.active_index >= app.agent_config.providers.len()
4096                    && app.agent_config.active_index > 0
4097                {
4098                    app.agent_config.active_index -= 1;
4099                }
4100                app.show_toast(format!("已删除 Provider: {}", removed_name), false);
4101            }
4102        }
4103        KeyCode::Char('s') => {
4104            // 将当前 provider 设为活跃
4105            if !app.agent_config.providers.is_empty() {
4106                app.agent_config.active_index = app.config_provider_idx;
4107                let name = app.agent_config.providers[app.config_provider_idx]
4108                    .name
4109                    .clone();
4110                app.show_toast(format!("已设为活跃模型: {}", name), false);
4111            }
4112        }
4113        _ => {}
4114    }
4115}
4116
4117/// 绘制配置编辑界面
4118fn draw_config_screen(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
4119    let bg = Color::Rgb(28, 28, 40);
4120    let total_provider_fields = CONFIG_FIELDS.len();
4121
4122    let mut lines: Vec<Line> = Vec::new();
4123    lines.push(Line::from(""));
4124
4125    // 标题
4126    lines.push(Line::from(vec![Span::styled(
4127        "  ⚙️  模型配置",
4128        Style::default()
4129            .fg(Color::Rgb(120, 180, 255))
4130            .add_modifier(Modifier::BOLD),
4131    )]));
4132    lines.push(Line::from(""));
4133
4134    // Provider 标签栏
4135    let provider_count = app.agent_config.providers.len();
4136    if provider_count > 0 {
4137        let mut tab_spans: Vec<Span> = vec![Span::styled("  ", Style::default())];
4138        for (i, p) in app.agent_config.providers.iter().enumerate() {
4139            let is_current = i == app.config_provider_idx;
4140            let is_active = i == app.agent_config.active_index;
4141            let marker = if is_active { "● " } else { "○ " };
4142            let label = format!(" {}{} ", marker, p.name);
4143            if is_current {
4144                tab_spans.push(Span::styled(
4145                    label,
4146                    Style::default()
4147                        .fg(Color::Rgb(22, 22, 30))
4148                        .bg(Color::Rgb(120, 180, 255))
4149                        .add_modifier(Modifier::BOLD),
4150                ));
4151            } else {
4152                tab_spans.push(Span::styled(
4153                    label,
4154                    Style::default().fg(Color::Rgb(150, 150, 170)),
4155                ));
4156            }
4157            if i < provider_count - 1 {
4158                tab_spans.push(Span::styled(
4159                    " │ ",
4160                    Style::default().fg(Color::Rgb(50, 55, 70)),
4161                ));
4162            }
4163        }
4164        tab_spans.push(Span::styled(
4165            "    (● = 活跃模型, Tab 切换, s 设为活跃)",
4166            Style::default().fg(Color::Rgb(80, 80, 100)),
4167        ));
4168        lines.push(Line::from(tab_spans));
4169    } else {
4170        lines.push(Line::from(Span::styled(
4171            "  (无 Provider,按 a 新增)",
4172            Style::default().fg(Color::Rgb(180, 120, 80)),
4173        )));
4174    }
4175    lines.push(Line::from(""));
4176
4177    // 分隔线
4178    lines.push(Line::from(Span::styled(
4179        "  ─────────────────────────────────────────",
4180        Style::default().fg(Color::Rgb(50, 55, 70)),
4181    )));
4182    lines.push(Line::from(""));
4183
4184    // Provider 字段
4185    if provider_count > 0 {
4186        lines.push(Line::from(Span::styled(
4187            "  📦 Provider 配置",
4188            Style::default()
4189                .fg(Color::Rgb(160, 220, 160))
4190                .add_modifier(Modifier::BOLD),
4191        )));
4192        lines.push(Line::from(""));
4193
4194        for i in 0..total_provider_fields {
4195            let is_selected = app.config_field_idx == i;
4196            let label = config_field_label(i);
4197            let value = if app.config_editing && is_selected {
4198                // 编辑模式下显示编辑缓冲区
4199                app.config_edit_buf.clone()
4200            } else {
4201                config_field_value(app, i)
4202            };
4203
4204            let pointer = if is_selected { "  ▸ " } else { "    " };
4205            let pointer_style = if is_selected {
4206                Style::default().fg(Color::Rgb(255, 200, 80))
4207            } else {
4208                Style::default()
4209            };
4210
4211            let label_style = if is_selected {
4212                Style::default()
4213                    .fg(Color::Rgb(230, 210, 120))
4214                    .add_modifier(Modifier::BOLD)
4215            } else {
4216                Style::default().fg(Color::Rgb(140, 140, 160))
4217            };
4218
4219            let value_style = if app.config_editing && is_selected {
4220                Style::default().fg(Color::White).bg(Color::Rgb(50, 55, 80))
4221            } else if is_selected {
4222                Style::default().fg(Color::White)
4223            } else {
4224                // API Key 特殊处理
4225                if CONFIG_FIELDS[i] == "api_key" {
4226                    Style::default().fg(Color::Rgb(100, 100, 120))
4227                } else {
4228                    Style::default().fg(Color::Rgb(180, 180, 200))
4229                }
4230            };
4231
4232            let edit_indicator = if app.config_editing && is_selected {
4233                " ✏️"
4234            } else {
4235                ""
4236            };
4237
4238            lines.push(Line::from(vec![
4239                Span::styled(pointer, pointer_style),
4240                Span::styled(format!("{:<10}", label), label_style),
4241                Span::styled("  ", Style::default()),
4242                Span::styled(
4243                    if value.is_empty() {
4244                        "(空)".to_string()
4245                    } else {
4246                        value
4247                    },
4248                    value_style,
4249                ),
4250                Span::styled(edit_indicator, Style::default()),
4251            ]));
4252        }
4253    }
4254
4255    lines.push(Line::from(""));
4256    // 分隔线
4257    lines.push(Line::from(Span::styled(
4258        "  ─────────────────────────────────────────",
4259        Style::default().fg(Color::Rgb(50, 55, 70)),
4260    )));
4261    lines.push(Line::from(""));
4262
4263    // 全局配置
4264    lines.push(Line::from(Span::styled(
4265        "  🌐 全局配置",
4266        Style::default()
4267            .fg(Color::Rgb(160, 220, 160))
4268            .add_modifier(Modifier::BOLD),
4269    )));
4270    lines.push(Line::from(""));
4271
4272    for i in 0..CONFIG_GLOBAL_FIELDS.len() {
4273        let field_idx = total_provider_fields + i;
4274        let is_selected = app.config_field_idx == field_idx;
4275        let label = config_field_label(field_idx);
4276        let value = if app.config_editing && is_selected {
4277            app.config_edit_buf.clone()
4278        } else {
4279            config_field_value(app, field_idx)
4280        };
4281
4282        let pointer = if is_selected { "  ▸ " } else { "    " };
4283        let pointer_style = if is_selected {
4284            Style::default().fg(Color::Rgb(255, 200, 80))
4285        } else {
4286            Style::default()
4287        };
4288
4289        let label_style = if is_selected {
4290            Style::default()
4291                .fg(Color::Rgb(230, 210, 120))
4292                .add_modifier(Modifier::BOLD)
4293        } else {
4294            Style::default().fg(Color::Rgb(140, 140, 160))
4295        };
4296
4297        let value_style = if app.config_editing && is_selected {
4298            Style::default().fg(Color::White).bg(Color::Rgb(50, 55, 80))
4299        } else if is_selected {
4300            Style::default().fg(Color::White)
4301        } else {
4302            Style::default().fg(Color::Rgb(180, 180, 200))
4303        };
4304
4305        let edit_indicator = if app.config_editing && is_selected {
4306            " ✏️"
4307        } else {
4308            ""
4309        };
4310
4311        // stream_mode 用 toggle 样式
4312        if CONFIG_GLOBAL_FIELDS[i] == "stream_mode" {
4313            let toggle_on = app.agent_config.stream_mode;
4314            let toggle_style = if toggle_on {
4315                Style::default()
4316                    .fg(Color::Rgb(120, 220, 160))
4317                    .add_modifier(Modifier::BOLD)
4318            } else {
4319                Style::default().fg(Color::Rgb(200, 100, 100))
4320            };
4321            let toggle_text = if toggle_on {
4322                "● 开启"
4323            } else {
4324                "○ 关闭"
4325            };
4326
4327            lines.push(Line::from(vec![
4328                Span::styled(pointer, pointer_style),
4329                Span::styled(format!("{:<10}", label), label_style),
4330                Span::styled("  ", Style::default()),
4331                Span::styled(toggle_text, toggle_style),
4332                Span::styled(
4333                    if is_selected { "  (Enter 切换)" } else { "" },
4334                    Style::default().fg(Color::Rgb(80, 80, 100)),
4335                ),
4336            ]));
4337        } else {
4338            lines.push(Line::from(vec![
4339                Span::styled(pointer, pointer_style),
4340                Span::styled(format!("{:<10}", label), label_style),
4341                Span::styled("  ", Style::default()),
4342                Span::styled(
4343                    if value.is_empty() {
4344                        "(空)".to_string()
4345                    } else {
4346                        value
4347                    },
4348                    value_style,
4349                ),
4350                Span::styled(edit_indicator, Style::default()),
4351            ]));
4352        }
4353    }
4354
4355    lines.push(Line::from(""));
4356    lines.push(Line::from(""));
4357
4358    // 操作提示
4359    lines.push(Line::from(Span::styled(
4360        "  ─────────────────────────────────────────",
4361        Style::default().fg(Color::Rgb(50, 55, 70)),
4362    )));
4363    lines.push(Line::from(""));
4364    lines.push(Line::from(vec![
4365        Span::styled("    ", Style::default()),
4366        Span::styled(
4367            "↑↓/jk",
4368            Style::default()
4369                .fg(Color::Rgb(230, 210, 120))
4370                .add_modifier(Modifier::BOLD),
4371        ),
4372        Span::styled(
4373            " 切换字段  ",
4374            Style::default().fg(Color::Rgb(120, 120, 150)),
4375        ),
4376        Span::styled(
4377            "Enter",
4378            Style::default()
4379                .fg(Color::Rgb(230, 210, 120))
4380                .add_modifier(Modifier::BOLD),
4381        ),
4382        Span::styled(" 编辑  ", Style::default().fg(Color::Rgb(120, 120, 150))),
4383        Span::styled(
4384            "Tab/←→",
4385            Style::default()
4386                .fg(Color::Rgb(230, 210, 120))
4387                .add_modifier(Modifier::BOLD),
4388        ),
4389        Span::styled(
4390            " 切换 Provider  ",
4391            Style::default().fg(Color::Rgb(120, 120, 150)),
4392        ),
4393        Span::styled(
4394            "a",
4395            Style::default()
4396                .fg(Color::Rgb(230, 210, 120))
4397                .add_modifier(Modifier::BOLD),
4398        ),
4399        Span::styled(" 新增  ", Style::default().fg(Color::Rgb(120, 120, 150))),
4400        Span::styled(
4401            "d",
4402            Style::default()
4403                .fg(Color::Rgb(230, 210, 120))
4404                .add_modifier(Modifier::BOLD),
4405        ),
4406        Span::styled(" 删除  ", Style::default().fg(Color::Rgb(120, 120, 150))),
4407        Span::styled(
4408            "s",
4409            Style::default()
4410                .fg(Color::Rgb(230, 210, 120))
4411                .add_modifier(Modifier::BOLD),
4412        ),
4413        Span::styled(
4414            " 设为活跃  ",
4415            Style::default().fg(Color::Rgb(120, 120, 150)),
4416        ),
4417        Span::styled(
4418            "Esc",
4419            Style::default()
4420                .fg(Color::Rgb(230, 210, 120))
4421                .add_modifier(Modifier::BOLD),
4422        ),
4423        Span::styled(" 保存返回", Style::default().fg(Color::Rgb(120, 120, 150))),
4424    ]));
4425
4426    let content = Paragraph::new(lines)
4427        .block(
4428            Block::default()
4429                .borders(Borders::ALL)
4430                .border_type(ratatui::widgets::BorderType::Rounded)
4431                .border_style(Style::default().fg(Color::Rgb(80, 80, 110)))
4432                .title(Span::styled(
4433                    " ⚙️  模型配置编辑 ",
4434                    Style::default()
4435                        .fg(Color::Rgb(230, 210, 120))
4436                        .add_modifier(Modifier::BOLD),
4437                ))
4438                .style(Style::default().bg(bg)),
4439        )
4440        .scroll((0, 0));
4441    f.render_widget(content, area);
4442}
4443
4444/// 模型选择模式按键处理
4445fn handle_select_model(app: &mut ChatApp, key: KeyEvent) {
4446    let count = app.agent_config.providers.len();
4447    match key.code {
4448        KeyCode::Esc => {
4449            app.mode = ChatMode::Chat;
4450        }
4451        KeyCode::Up | KeyCode::Char('k') => {
4452            if count > 0 {
4453                let i = app
4454                    .model_list_state
4455                    .selected()
4456                    .map(|i| if i == 0 { count - 1 } else { i - 1 })
4457                    .unwrap_or(0);
4458                app.model_list_state.select(Some(i));
4459            }
4460        }
4461        KeyCode::Down | KeyCode::Char('j') => {
4462            if count > 0 {
4463                let i = app
4464                    .model_list_state
4465                    .selected()
4466                    .map(|i| if i >= count - 1 { 0 } else { i + 1 })
4467                    .unwrap_or(0);
4468                app.model_list_state.select(Some(i));
4469            }
4470        }
4471        KeyCode::Enter => {
4472            app.switch_model();
4473        }
4474        _ => {}
4475    }
4476}
4477
4478/// 复制内容到系统剪切板
4479fn copy_to_clipboard(content: &str) -> bool {
4480    use std::process::{Command, Stdio};
4481
4482    let (cmd, args): (&str, Vec<&str>) = if cfg!(target_os = "macos") {
4483        ("pbcopy", vec![])
4484    } else if cfg!(target_os = "linux") {
4485        if Command::new("which")
4486            .arg("xclip")
4487            .output()
4488            .map(|o| o.status.success())
4489            .unwrap_or(false)
4490        {
4491            ("xclip", vec!["-selection", "clipboard"])
4492        } else {
4493            ("xsel", vec!["--clipboard", "--input"])
4494        }
4495    } else {
4496        return false;
4497    };
4498
4499    let child = Command::new(cmd).args(&args).stdin(Stdio::piped()).spawn();
4500
4501    match child {
4502        Ok(mut child) => {
4503            if let Some(ref mut stdin) = child.stdin {
4504                let _ = stdin.write_all(content.as_bytes());
4505            }
4506            child.wait().map(|s| s.success()).unwrap_or(false)
4507        }
4508        Err(_) => false,
4509    }
4510}