mod approval;
mod chat;
mod conversation_list;
mod file_picker;
mod input;
mod plan_config;
mod question;
mod rewind_picker;
mod slash_palette;
mod status;
mod status_line;
mod tasks;
pub use approval::ApprovalModalWidget;
pub use chat::{ChatState, ChatWidget, ImageClickTarget};
pub use conversation_list::ConversationListWidget;
pub use file_picker::FilePickerWidget;
pub use input::{InputState, InputWidget};
pub use plan_config::{PLAN_CONFIG_HEIGHT, PLAN_CONFIG_ROWS, PlanConfigWidget, plan_config_rows};
pub use question::{QuestionModalWidget, question_modal_height};
pub use rewind_picker::RewindPickerWidget;
pub use slash_palette::SlashPaletteWidget;
pub use status::StatusWidget;
pub use status_line::{AgentPanelRow, build_status_lines};
pub use tasks::{build_task_lines, tasks_height, tasks_visible};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
pub(super) fn truncate_to_cells(s: &str, width: usize) -> String {
if UnicodeWidthStr::width(s) <= width {
return s.to_string();
}
if width == 0 {
return String::new();
}
let budget = width - 1; let mut out = String::new();
let mut w = 0usize;
for ch in s.chars() {
let cw = UnicodeWidthChar::width(ch).unwrap_or(0);
if w + cw > budget {
break;
}
out.push(ch);
w += cw;
}
out.push('…');
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GenerationStatus {
Idle,
Sending,
Thinking,
Streaming,
RunningTools,
Compacting,
Cancelling,
}
impl GenerationStatus {
pub fn display_text(&self) -> &str {
match self {
GenerationStatus::Idle => "Idle",
GenerationStatus::Sending => "Sending",
GenerationStatus::Thinking => "Thinking",
GenerationStatus::Streaming => "Streaming",
GenerationStatus::RunningTools => "Running tools",
GenerationStatus::Compacting => "Compacting",
GenerationStatus::Cancelling => "Cancelling",
}
}
pub fn from_turn(turn: &crate::domain::TurnState) -> Self {
use crate::domain::{GenPhase, TurnState};
match turn {
TurnState::Idle => GenerationStatus::Idle,
TurnState::Generating { phase, .. } => match phase {
GenPhase::Sending => GenerationStatus::Sending,
GenPhase::Thinking => GenerationStatus::Thinking,
GenPhase::Streaming => GenerationStatus::Streaming,
},
TurnState::ExecutingTools { .. } => GenerationStatus::RunningTools,
TurnState::Compacting { .. } => GenerationStatus::Compacting,
TurnState::Cancelling { .. } => GenerationStatus::Cancelling,
}
}
}
#[cfg(test)]
mod tests {
use super::truncate_to_cells;
use unicode_width::UnicodeWidthStr;
#[test]
fn fits_within_width_returns_unchanged() {
assert_eq!(truncate_to_cells("hello", 10), "hello");
assert_eq!(truncate_to_cells("hello", 5), "hello");
}
#[test]
fn ascii_truncates_with_ellipsis_within_budget() {
let out = truncate_to_cells("hello world", 5);
assert_eq!(out, "hell…");
assert!(UnicodeWidthStr::width(out.as_str()) <= 5);
}
#[test]
fn wide_glyphs_never_exceed_budget() {
let cjk = "你好世界你好世界"; let out = truncate_to_cells(cjk, 6);
assert!(
UnicodeWidthStr::width(out.as_str()) <= 6,
"width exceeded budget: {out:?}"
);
assert!(out.ends_with('…'));
}
#[test]
fn zero_width_is_empty() {
assert_eq!(truncate_to_cells("anything", 0), "");
}
}