Skip to main content

j_cli/command/
chat.rs

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