Skip to main content

j_cli/command/
chat.rs

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