use super::types::{AskAnswer, AskQuestion};
use crate::command::chat::infra::archive::ChatArchive;
use crate::command::chat::markdown::image_cache::ImageCache;
use crate::command::chat::permission::queue::PendingAgentPerm;
use crate::command::chat::render::theme::Theme;
use crate::command::chat::storage::SessionMeta;
use crate::command::chat::tools::plan::PendingPlanApproval;
use crate::tui::editor_core::text_buffer::TextBuffer;
use ratatui::text::Line;
use ratatui::widgets::ListState;
use std::sync::{Arc, Mutex};
pub struct UIState {
pub input_buffer: TextBuffer,
pub mode: ChatMode,
pub scroll_offset: u16,
pub auto_scroll: bool,
pub browse_msg_index: usize,
pub browse_scroll_offset: u16,
pub browse_filter: String,
pub browse_role_filter: Option<String>,
pub model_list_state: ListState,
pub theme_list_state: ListState,
pub toast: Option<(String, bool, std::time::Instant)>,
pub msg_lines_cache: Option<MsgLinesCache>,
pub cached_mention_ranges: Option<(String, Vec<(usize, usize)>)>,
pub last_rendered_streaming_len: usize,
pub last_stream_render_time: std::time::Instant,
pub config_provider_idx: usize,
pub config_field_idx: usize,
pub config_editing: bool,
pub config_edit_buf: String,
pub config_edit_cursor: usize,
pub theme: Theme,
pub archives: Vec<ChatArchive>,
pub archive_list_index: usize,
pub archive_default_name: String,
pub archive_custom_name: String,
pub archive_editing_name: bool,
pub archive_edit_cursor: usize,
pub restore_confirm_needed: bool,
pub at_popup_active: bool,
pub at_popup_filter: String,
pub at_popup_start_pos: usize,
pub at_popup_selected: usize,
pub file_popup_active: bool,
pub file_popup_start_pos: usize,
pub file_popup_filter: String,
pub file_popup_selected: usize,
pub skill_popup_active: bool,
pub skill_popup_start_pos: usize,
pub skill_popup_filter: String,
pub skill_popup_selected: usize,
pub command_popup_active: bool,
pub command_popup_start_pos: usize,
pub command_popup_filter: String,
pub command_popup_selected: usize,
pub slash_popup_active: bool,
pub slash_popup_filter: String,
pub slash_popup_selected: usize,
pub tool_interact_selected: usize,
pub tool_interact_typing: bool,
pub tool_interact_input: String,
pub tool_interact_cursor: usize,
pub tool_ask_mode: bool,
pub tool_ask_questions: Vec<AskQuestion>,
pub tool_ask_current_idx: usize,
pub tool_ask_answers: Vec<AskAnswer>,
pub tool_ask_selections: Vec<bool>,
pub tool_ask_cursor: usize,
pub pending_system_prompt_edit: bool,
pub pending_agent_md_edit: bool,
pub pending_style_edit: bool,
pub image_cache: Arc<Mutex<ImageCache>>,
pub expand_tools: bool,
pub config_scroll_offset: u16,
pub config_provider_scroll_offset: usize,
pub config_tab: ConfigTab,
pub session_list: Vec<SessionMeta>,
pub session_list_index: usize,
pub session_restore_confirm: bool,
pub teammate_list_index: usize,
pub quote_idx: usize,
pub input_wrap_width: usize,
pub pending_agent_perm: Option<Arc<PendingAgentPerm>>,
pub pending_plan_approval: Option<Arc<PendingPlanApproval>>,
pub compact_exempt_sublist: bool,
pub compact_exempt_idx: usize,
}
pub struct MsgLinesCache {
pub msg_count: usize,
pub last_msg_len: usize,
pub streaming_len: usize,
pub is_loading: bool,
pub bubble_max_width: usize,
pub browse_index: Option<usize>,
pub tool_confirm_idx: Option<usize>,
pub total_line_count: usize,
pub history_line_count: usize,
pub msg_start_lines: Vec<(usize, usize)>, pub per_msg_lines: Vec<PerMsgCache>,
pub streaming_lines: Vec<Line<'static>>,
pub streaming_stable_lines: Arc<Vec<Line<'static>>>,
pub streaming_stable_offset: usize,
pub expand_tools: bool,
}
#[derive(Debug)]
pub struct PerMsgCache {
pub content_len: usize,
pub lines: Vec<Line<'static>>,
pub msg_index: usize,
pub is_selected: bool,
}
#[derive(PartialEq)]
pub enum ChatMode {
Chat,
SelectModel,
Browse,
Help,
Config,
ArchiveConfirm,
ArchiveList,
ToolConfirm,
SelectTheme,
AgentPermConfirm,
PlanApprovalConfirm,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigTab {
Model,
Global,
Tools,
Skills,
Hooks,
Commands,
Teammates,
Session,
Archive,
}
impl ConfigTab {
pub fn label(&self) -> &'static str {
match self {
ConfigTab::Model => "Model",
ConfigTab::Session => "Session",
ConfigTab::Global => "Global",
ConfigTab::Tools => "Tools",
ConfigTab::Skills => "Skills",
ConfigTab::Hooks => "Hooks",
ConfigTab::Commands => "Commands",
ConfigTab::Teammates => "Teammates",
ConfigTab::Archive => "归档",
}
}
pub fn index(&self) -> usize {
match self {
ConfigTab::Model => 0,
ConfigTab::Session => 1,
ConfigTab::Global => 2,
ConfigTab::Tools => 3,
ConfigTab::Skills => 4,
ConfigTab::Hooks => 5,
ConfigTab::Commands => 6,
ConfigTab::Teammates => 7,
ConfigTab::Archive => 8,
}
}
pub fn from_index(idx: usize) -> Self {
match idx {
0 => ConfigTab::Model,
1 => ConfigTab::Session,
2 => ConfigTab::Global,
3 => ConfigTab::Tools,
4 => ConfigTab::Skills,
5 => ConfigTab::Hooks,
6 => ConfigTab::Commands,
7 => ConfigTab::Teammates,
_ => ConfigTab::Archive,
}
}
pub const COUNT: usize = 9;
pub fn next(&self) -> Self {
ConfigTab::from_index((self.index() + 1) % Self::COUNT)
}
pub fn prev(&self) -> Self {
ConfigTab::from_index((self.index() + Self::COUNT - 1) % Self::COUNT)
}
}
impl UIState {
pub fn input_text(&self) -> String {
self.input_buffer.to_string()
}
pub fn cursor_char_idx(&self) -> usize {
let (row, col) = self.input_buffer.cursor();
let mut idx = 0;
for i in 0..row {
idx += self
.input_buffer
.line(i)
.map(|l: &String| l.chars().count())
.unwrap_or(0);
idx += 1; }
idx += col;
idx
}
pub fn set_cursor_from_char_idx(&mut self, char_idx: usize) {
let mut remaining = char_idx;
let lines = self.input_buffer.lines();
for (row, line) in lines.iter().enumerate() {
let line_len: usize = line.chars().count();
if remaining <= line_len {
self.input_buffer.set_cursor(row, remaining);
return;
}
remaining -= line_len + 1; }
let last_row = lines.len().saturating_sub(1);
let last_col = lines[last_row].chars().count();
self.input_buffer.set_cursor(last_row, last_col);
}
pub fn set_input_text(&mut self, text: &str, cursor_char_idx: usize) {
self.input_buffer = TextBuffer::from_content(text);
self.set_cursor_from_char_idx(cursor_char_idx);
}
pub fn clear_input(&mut self) {
self.input_buffer = TextBuffer::new();
}
pub fn is_input_empty(&self) -> bool {
self.input_buffer
.lines()
.iter()
.all(|l: &String| l.is_empty())
&& self.input_buffer.line_count() <= 1
}
}