use super::ChatApp;
use crate::command::chat::app::action::CursorDirection;
use crate::command::chat::app::types::ToolExecStatus;
use crate::command::chat::app::ui_state::ChatMode;
use crate::command::chat::constants::{FINE_SCROLL_LINES, INPUT_BUFFER_MAX_LEN, PAGE_SCROLL_LINES};
use crate::command::chat::remote::protocol::{ToolConfirmInfo, WsOutbound};
use crate::command::chat::storage::{MessageRole, save_agent_config};
use crate::util::safe_lock;
impl ChatApp {
pub(super) fn update_insert_char(&mut self, ch: char) {
if self.ui.input_text().len() < INPUT_BUFFER_MAX_LEN {
self.ui.input_buffer.insert_char(ch);
}
}
pub(super) fn update_move_cursor(&mut self, dir: CursorDirection) {
match dir {
CursorDirection::Up => {
self.ui.input_buffer.move_cursor_up();
}
CursorDirection::Down => {
self.ui.input_buffer.move_cursor_down();
}
}
}
pub(super) fn update_at_popup_activate(&mut self) {
self.ui.at_popup_active = true;
self.ui.at_popup_filter.clear();
self.ui.at_popup_selected = 0;
}
pub(super) fn update_at_popup_filter(&mut self, text: String) {
self.ui.at_popup_filter = text;
self.ui.at_popup_selected = 0;
}
pub(super) fn update_at_popup_navigate(&mut self, dir: CursorDirection) {
match dir {
CursorDirection::Up => {
if self.ui.at_popup_selected > 0 {
self.ui.at_popup_selected -= 1;
}
}
CursorDirection::Down => {
self.ui.at_popup_selected += 1;
}
}
}
pub(super) fn update_file_popup_activate(&mut self) {
self.ui.file_popup_active = true;
self.ui.file_popup_filter.clear();
self.ui.file_popup_selected = 0;
}
pub(super) fn update_file_popup_filter(&mut self, text: String) {
self.ui.file_popup_filter = text;
self.ui.file_popup_selected = 0;
}
pub(super) fn update_skill_popup_activate(&mut self) {
self.ui.skill_popup_active = true;
self.ui.skill_popup_filter.clear();
self.ui.skill_popup_selected = 0;
}
pub(super) fn update_skill_popup_filter(&mut self, text: String) {
self.ui.skill_popup_filter = text;
self.ui.skill_popup_selected = 0;
}
pub(super) fn update_skill_popup_navigate(&mut self, dir: CursorDirection) {
match dir {
CursorDirection::Up => {
if self.ui.skill_popup_selected > 0 {
self.ui.skill_popup_selected -= 1;
}
}
CursorDirection::Down => {
self.ui.skill_popup_selected += 1;
}
}
}
pub(super) fn update_stream_chunk(&mut self) {
self.state.retry_hint = None;
if self.ui.auto_scroll {
self.ui.scroll_offset = u16::MAX;
}
if self.ws_bridge.is_some() {
let content = safe_lock(&self.state.streaming_content, "ws_stream_chunk").clone();
self.broadcast_ws(WsOutbound::StreamChunk { content });
}
}
pub(super) fn update_stream_done(&mut self) {
self.state.retry_hint = None;
if self.ws_bridge.is_some() {
if let Some(last_msg) =
crate::util::safe_lock(&self.display_messages, "StreamDone::ws_broadcast").last()
&& last_msg.role == MessageRole::Assistant
{
self.broadcast_ws(WsOutbound::Message {
role: MessageRole::Assistant.to_string(),
content: last_msg.content.clone(),
});
}
self.broadcast_ws(WsOutbound::Status {
state: "idle".to_string(),
});
}
self.finish_loading(false, false);
}
pub(super) fn update_stream_error(&mut self, e: &crate::command::chat::error::ChatError) {
self.state.retry_hint = None;
let msg = e.display_message();
self.broadcast_ws(WsOutbound::Error {
message: format!("请求失败: {}", msg),
});
self.broadcast_ws(WsOutbound::Status {
state: "idle".to_string(),
});
self.show_toast(format!("请求失败: {}", msg), true);
self.finish_loading(true, false);
}
pub(super) fn update_stream_cancelled(&mut self) {
self.state.retry_hint = None;
self.broadcast_ws(WsOutbound::Status {
state: "idle".to_string(),
});
self.finish_loading(false, true);
}
pub(super) fn update_stream_retrying(
&mut self,
attempt: u32,
max_attempts: u32,
delay_ms: u64,
error: String,
) {
let delay_s = delay_ms.div_ceil(1000);
self.state.retry_hint = Some(format!(
"⟳ 重试 {}/{} · {}s · {}",
attempt, max_attempts, delay_s, error
));
}
pub(super) fn update_stream_compacting(&mut self) {
self.state.retry_hint = Some("📦 压缩上下文中...".to_string());
}
pub(super) fn update_stream_compacted(&mut self) {
self.state.retry_hint = None;
{
let display =
crate::util::safe_lock(&self.display_messages, "StreamCompacted::sync_display");
self.display_read_offset = display.len();
let shared =
crate::util::safe_lock(&self.context_messages, "StreamCompacted::sync_ctx");
self.context_read_offset = shared.len();
}
self.ui.msg_lines_cache = None;
}
pub(super) fn update_enter_mode(&mut self, mode: ChatMode) {
if mode == ChatMode::ToolConfirm && self.ws_bridge.is_some() {
let tools: Vec<ToolConfirmInfo> = self
.tool_executor
.active_tool_calls
.iter()
.filter(|tc| matches!(tc.status, ToolExecStatus::PendingConfirm))
.map(|tc| ToolConfirmInfo {
id: tc.tool_call_id.clone(),
name: tc.tool_name.clone(),
arguments: tc.arguments.clone(),
confirm_message: tc.confirm_message.clone(),
})
.collect();
if !tools.is_empty() {
self.broadcast_ws(WsOutbound::ToolConfirmRequest { tools });
}
self.broadcast_ws(WsOutbound::Status {
state: "tool_confirm".to_string(),
});
}
if mode == ChatMode::Browse {
self.ui.browse_filter.clear();
self.ui.browse_role_filter = None;
}
self.ui.mode = mode;
}
pub(super) fn update_exit_to_chat(&mut self) {
self.ui.mode = ChatMode::Chat;
self.ui.browse_filter.clear();
self.ui.browse_role_filter = None;
}
pub(super) fn update_scroll(&mut self, dir: CursorDirection) {
match dir {
CursorDirection::Up => self.scroll_up(),
CursorDirection::Down => self.scroll_down(),
}
}
pub(super) fn update_page_scroll(&mut self, dir: CursorDirection) {
match dir {
CursorDirection::Up => {
for _ in 0..PAGE_SCROLL_LINES {
self.scroll_up();
}
}
CursorDirection::Down => {
for _ in 0..PAGE_SCROLL_LINES {
self.scroll_down();
}
}
}
}
pub(super) fn update_browse_navigate(&mut self, dir: CursorDirection) {
let filtered = self.browse_filtered_indices();
if filtered.is_empty() {
self.ui.mode = ChatMode::Chat;
self.ui.msg_lines_cache = None;
return;
}
let current_in_filtered = filtered.iter().position(|&i| i == self.ui.browse_msg_index);
match dir {
CursorDirection::Up => {
let new_idx = match current_in_filtered {
Some(pos) if pos > 0 => filtered[pos - 1],
Some(_) => filtered[filtered.len() - 1],
None => *filtered.last().expect("browse mode 下 filtered 不应为空"),
};
self.ui.browse_msg_index = new_idx;
self.ui.browse_scroll_offset = 0;
self.ui.msg_lines_cache = None;
}
CursorDirection::Down => {
let new_idx = match current_in_filtered {
Some(pos) if pos < filtered.len() - 1 => filtered[pos + 1],
Some(_) => filtered[0],
None => filtered[0],
};
self.ui.browse_msg_index = new_idx;
self.ui.browse_scroll_offset = 0;
self.ui.msg_lines_cache = None;
}
}
}
pub(super) fn update_browse_fine_scroll(&mut self, dir: CursorDirection) {
match dir {
CursorDirection::Up => {
self.ui.browse_scroll_offset = self
.ui
.browse_scroll_offset
.saturating_sub(FINE_SCROLL_LINES);
}
CursorDirection::Down => {
self.ui.browse_scroll_offset = self
.ui
.browse_scroll_offset
.saturating_add(FINE_SCROLL_LINES);
}
}
}
pub(super) fn update_browse_copy_message(&mut self) {
use crate::command::chat::render::cache::copy_to_clipboard;
let copy_result = {
let display = crate::util::safe_lock(&self.display_messages, "BrowseCopyMessage");
display.get(self.ui.browse_msg_index).map(|msg| {
let content = msg.content.clone();
let role_label = if msg.role == MessageRole::Assistant {
"AI"
} else if msg.role == MessageRole::User {
"用户"
} else {
"系统"
};
(content, role_label.to_string())
})
};
if let Some((content, role_label)) = copy_result {
let filtered = self.browse_filtered_indices();
let pos_in_filtered = filtered.iter().position(|&i| i == self.ui.browse_msg_index);
let extra = if let Some(pos) = pos_in_filtered {
format!(" ({}/{})", pos + 1, filtered.len())
} else {
String::new()
};
if copy_to_clipboard(&content) {
self.show_toast(format!("已复制{}消息{}", role_label, extra), false);
} else {
self.show_toast("复制到剪切板失败", true);
}
}
}
pub(super) fn update_browse_input_char(&mut self, c: char) {
self.ui.browse_filter.push(c);
self.browse_jump_to_first_match();
self.ui.msg_lines_cache = None;
}
pub(super) fn update_browse_delete_char(&mut self) {
self.ui.browse_filter.pop();
self.browse_jump_to_first_match();
self.ui.msg_lines_cache = None;
}
pub(super) fn update_browse_clear_filter(&mut self) {
self.ui.browse_filter.clear();
self.ui.browse_role_filter = None;
self.ui.msg_lines_cache = None;
}
pub(super) fn update_browse_toggle_role(&mut self) {
self.ui.browse_role_filter = match &self.ui.browse_role_filter {
None => Some("ai".to_string()),
Some(r) if r == "ai" => Some("user".to_string()),
_ => None,
};
self.browse_jump_to_first_match();
self.ui.msg_lines_cache = None;
}
pub(super) fn update_cancel_stream(&mut self) {
self.cancel_stream();
self.permission_queue.deny_all();
self.plan_approval_queue.deny_all();
if let Some(req) = self.ui.pending_agent_perm.take() {
req.resolve(false);
}
if let Some(req) = self.ui.pending_plan_approval.take() {
req.resolve(crate::command::chat::app::types::PlanDecision::Reject);
}
if matches!(
self.ui.mode,
ChatMode::AgentPermConfirm | ChatMode::PlanApprovalConfirm
) {
self.ui.mode = ChatMode::Chat;
}
}
pub(super) fn update_save_config(&mut self) {
let _ = save_agent_config(&self.state.agent_config);
self.ui.mode = ChatMode::Chat;
}
pub(super) fn update_copy_last_ai_reply(&mut self) {
use crate::command::chat::render::cache::copy_to_clipboard;
let last_ai_content = safe_lock(&self.context_messages, "CopyLastAiReply")
.iter()
.rev()
.find(|m| m.role == MessageRole::Assistant)
.map(|m| m.content.clone());
if let Some(content) = last_ai_content {
if copy_to_clipboard(&content) {
self.show_toast("已复制最后一条 AI 回复", false);
} else {
self.show_toast("复制到剪切板失败", true);
}
} else {
self.show_toast("暂无 AI 回复可复制", true);
}
}
pub(super) fn update_open_log_windows(&mut self) {
use crate::constants::{AGENT_DIR, AGENT_LOG_DIR, AGENT_LOG_ERROR, AGENT_LOG_INFO};
let log_dir = crate::config::YamlConfig::data_dir()
.join(AGENT_DIR)
.join(AGENT_LOG_DIR);
let info_log = log_dir.join(AGENT_LOG_INFO);
let error_log = log_dir.join(AGENT_LOG_ERROR);
let info_cmd = format!("tail -f '{}'; exit", info_log.to_string_lossy())
.replace('\\', "\\\\")
.replace('"', "\\\"");
let error_cmd = format!("tail -f '{}'; exit", error_log.to_string_lossy())
.replace('\\', "\\\\")
.replace('"', "\\\"");
let apple_script = format!(
"tell application \"Terminal\"\n\
do script \"{}\"\n\
do script \"{}\"\n\
activate\n\
end tell",
info_cmd, error_cmd
);
let _ = std::process::Command::new("osascript")
.arg("-e")
.arg(&apple_script)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
}
pub(super) fn update_toggle_expand_tools(&mut self) {
self.ui.expand_tools = !self.ui.expand_tools;
self.ui.msg_lines_cache = None;
self.ui.auto_scroll = true;
self.ui.scroll_offset = u16::MAX;
self.show_toast(
if self.ui.expand_tools {
"展开工具详情"
} else {
"折叠工具详情"
},
false,
);
}
pub(super) fn update_toggle_auto_approve(&mut self) {
self.ui.auto_approve = !self.ui.auto_approve;
let sid = self.session_id.clone();
if let Some(mut meta) = crate::command::chat::storage::load_session_meta_file(&sid) {
meta.auto_approve = self.ui.auto_approve;
crate::command::chat::storage::save_session_meta_file(&meta);
}
self.show_toast(
if self.ui.auto_approve {
"Bypass 模式已开启"
} else {
"Bypass 模式已关闭"
},
false,
);
}
}