style: format seluruh workspace dengan cargo fmt

Menyeragamkan format kode sesuai rustfmt (126 file). Sebelumnya
lefthook pre-commit 'cargo fmt --check' akan gagal pada commit apa pun.
This commit is contained in:
asepharyana
2026-08-27 22:10:28 +07:00
parent 884b19ccb5
commit 7b0b53671f
127 changed files with 1271 additions and 1156 deletions
+6 -7
View File
@@ -53,8 +53,7 @@ fn apply_turn_event(state: &mut AppStateRest, event: zesdex_infrastructure::Turn
rt.hive_mind_converged = true;
}
} else {
state
.push_transcript(ChatMessageDisplay::new(Role::System, message));
state.push_transcript(ChatMessageDisplay::new(Role::System, message));
}
}
zesdex_infrastructure::TurnEvent::AssistantMessage(msg) => {
@@ -82,8 +81,7 @@ fn apply_turn_event(state: &mut AppStateRest, event: zesdex_infrastructure::Turn
state.toast_error(msg);
}
zesdex_infrastructure::TurnEvent::StreamStart => {
state
.push_transcript(ChatMessageDisplay::new(Role::Assistant, String::new()));
state.push_transcript(ChatMessageDisplay::new(Role::Assistant, String::new()));
}
zesdex_infrastructure::TurnEvent::StreamToken(text) => {
state.append_to_last_transcript(&text, false);
@@ -150,7 +148,9 @@ fn apply_workflow_update(
state
.workflow_engine
.agents
.push(crate::state::SimpleAgent::with_display(agent_id, agent_name));
.push(crate::state::SimpleAgent::with_display(
agent_id, agent_name,
));
}
}
zesdex_infrastructure::AgentStatus::Running => {
@@ -160,8 +160,7 @@ fn apply_workflow_update(
state.workflow_engine.agents[i].display_name = agent_name;
state.workflow_engine.agents[i].started_at = Some(now);
} else {
let mut agent =
crate::state::SimpleAgent::with_display(agent_id, agent_name);
let mut agent = crate::state::SimpleAgent::with_display(agent_id, agent_name);
agent.state = crate::state::AgentState::Running;
agent.started_at = Some(now);
state.workflow_engine.agents.push(agent);
+3 -10
View File
@@ -122,8 +122,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
// ── Input ──────────────────────────────────────────────────────
Action::SubmitInput(text) => {
state
.push_transcript(ChatMessageDisplay::new(Role::User, text.clone()));
state.push_transcript(ChatMessageDisplay::new(Role::User, text.clone()));
state.input.submit();
crate::turn::spawn_agent_turn(state, text);
state.token_count_dirty = true;
@@ -176,8 +175,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
state.mark_dirty();
}
Action::SystemNote { kind: _, message } => {
state
.push_transcript(ChatMessageDisplay::new(Role::System, message));
state.push_transcript(ChatMessageDisplay::new(Role::System, message));
}
Action::ModelList => {
state.misc.overlay = Overlay::ModelSelector;
@@ -226,12 +224,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
state.mark_dirty();
}
Action::DiffScroll(amount) => {
let max_scroll = state
.misc
.diff_content
.lines()
.count()
.saturating_sub(1);
let max_scroll = state.misc.diff_content.lines().count().saturating_sub(1);
let new_scroll = (state.misc.diff_scroll as i32 + amount).max(0) as usize;
state.misc.diff_scroll = new_scroll.min(max_scroll);
state.mark_dirty();
@@ -1,10 +1,10 @@
//! Base trait for all TUI components.
use crate::action::Action;
use crate::state::AppStateRest;
use crossterm::event::Event;
use ratatui::layout::Rect;
use ratatui::Frame;
use crate::action::Action;
use crate::state::AppStateRest;
/// Base trait for modular UI components in the TUI.
pub trait Component {
@@ -13,7 +13,7 @@ pub trait Component {
/// Optional: Pre-render caching phase called before draw.
fn pre_render(&mut self, _state: &mut AppStateRest) {}
/// Handle an input event. Return an optional Action to dispatch.
fn handle_event(&mut self, _event: Event, _state: &mut AppStateRest) -> Option<Action> {
None
+18 -6
View File
@@ -104,13 +104,17 @@ pub fn parse_command(text: &str) -> Command {
pub fn apply_command(cmd: Command) -> Vec<crate::action::Action> {
match cmd {
Command::Help => {
vec![crate::action::Action::OpenOverlay(crate::state::Overlay::Help)]
vec![crate::action::Action::OpenOverlay(
crate::state::Overlay::Help,
)]
}
Command::Quit => {
vec![crate::action::Action::QuitConfirm]
}
Command::McpOpen => {
vec![crate::action::Action::OpenOverlay(crate::state::Overlay::Mcp)]
vec![crate::action::Action::OpenOverlay(
crate::state::Overlay::Mcp,
)]
}
Command::Clear => {
vec![crate::action::Action::SystemNote {
@@ -119,7 +123,9 @@ pub fn apply_command(cmd: Command) -> Vec<crate::action::Action> {
}]
}
Command::ClearConfirm => {
vec![crate::action::Action::OpenOverlay(crate::state::Overlay::ClearConfirm)]
vec![crate::action::Action::OpenOverlay(
crate::state::Overlay::ClearConfirm,
)]
}
Command::Login { provider } => {
vec![crate::action::Action::StartOAuth { provider }]
@@ -137,13 +143,19 @@ pub fn apply_command(cmd: Command) -> Vec<crate::action::Action> {
vec![crate::action::Action::Compact]
}
Command::TodoOpen => {
vec![crate::action::Action::OpenOverlay(crate::state::Overlay::Todo)]
vec![crate::action::Action::OpenOverlay(
crate::state::Overlay::Todo,
)]
}
Command::PlanOpen => {
vec![crate::action::Action::OpenOverlay(crate::state::Overlay::Plan)]
vec![crate::action::Action::OpenOverlay(
crate::state::Overlay::Plan,
)]
}
Command::UsageOpen => {
vec![crate::action::Action::OpenOverlay(crate::state::Overlay::Usage)]
vec![crate::action::Action::OpenOverlay(
crate::state::Overlay::Usage,
)]
}
Command::Diff => {
vec![crate::action::Action::ShowDiff]
+13 -7
View File
@@ -16,7 +16,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::action::Action;
use crate::controller::command::{apply_command, parse_command};
use crate::controller::overlay_enter::handle_overlay_enter;
use crate::state::{AutocompleteKind, Overlay, AppStateRest};
use crate::state::{AppStateRest, AutocompleteKind, Overlay};
/// Mark state dirty and return an empty action list.
fn mark(state: &mut AppStateRest) -> Vec<Action> {
@@ -120,13 +120,15 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
KeyCode::Up => {
let items = crate::state::get_learning_items(state);
let n = items.len();
state.misc.selected_index = crate::state::cycle_selected_index(state.misc.selected_index, n, false);
state.misc.selected_index =
crate::state::cycle_selected_index(state.misc.selected_index, n, false);
return mark(state);
}
KeyCode::Down => {
let items = crate::state::get_learning_items(state);
let n = items.len();
state.misc.selected_index = crate::state::cycle_selected_index(state.misc.selected_index, n, true);
state.misc.selected_index =
crate::state::cycle_selected_index(state.misc.selected_index, n, true);
return mark(state);
}
KeyCode::Enter | KeyCode::Char('a') => {
@@ -237,12 +239,14 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
Vec::new()
} else if state.misc.overlay == Overlay::Rewind {
let n = crate::state::rewind_count(state);
state.misc.selected_index = crate::state::cycle_selected_index(state.misc.selected_index, n, false);
state.misc.selected_index =
crate::state::cycle_selected_index(state.misc.selected_index, n, false);
state.mark_dirty();
Vec::new()
} else if state.misc.overlay == Overlay::ModelSelector {
let n = state.app_config.providers.len();
state.misc.selected_index = crate::state::cycle_selected_index(state.misc.selected_index, n, false);
state.misc.selected_index =
crate::state::cycle_selected_index(state.misc.selected_index, n, false);
state.mark_dirty();
Vec::new()
} else if key.modifiers.contains(KeyModifiers::CONTROL) {
@@ -261,12 +265,14 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
Vec::new()
} else if state.misc.overlay == Overlay::Rewind {
let n = crate::state::rewind_count(state);
state.misc.selected_index = crate::state::cycle_selected_index(state.misc.selected_index, n, true);
state.misc.selected_index =
crate::state::cycle_selected_index(state.misc.selected_index, n, true);
state.mark_dirty();
Vec::new()
} else if state.misc.overlay == Overlay::ModelSelector {
let n = state.app_config.providers.len();
state.misc.selected_index = crate::state::cycle_selected_index(state.misc.selected_index, n, true);
state.misc.selected_index =
crate::state::cycle_selected_index(state.misc.selected_index, n, true);
state.mark_dirty();
Vec::new()
} else if key.modifiers.contains(KeyModifiers::CONTROL) {
@@ -95,7 +95,10 @@ fn handle_model_selector_enter(state: &mut AppStateRest) -> Vec<Action> {
state.settings.provider.clone_from(provider);
state.settings.model.clone_from(&model);
if let Some(ref key) = cfg.default_api_key {
state.settings.api_keys.insert(provider.clone(), key.clone());
state
.settings
.api_keys
.insert(provider.clone(), key.clone());
} else if let Some(env_key) = cfg
.api_key_env
.as_ref()
+3 -4
View File
@@ -68,12 +68,11 @@ pub mod view;
// Re-exports for convenient access by consumers (main.rs / bin entry points)
// ---------------------------------------------------------------------------
pub use action::{Action, apply_action};
pub use action::{apply_action, Action};
pub use run::run_single_process;
pub use state::{
AgentState, AppStateRest, AutocompleteKind, ChatMessageDisplay, InputState,
MiscState, Overlay, ScrollState, SimpleAgent, SimpleWorkflowEngine,
TranscriptCache, EditorState,
AgentState, AppStateRest, AutocompleteKind, ChatMessageDisplay, EditorState, InputState,
MiscState, Overlay, ScrollState, SimpleAgent, SimpleWorkflowEngine, TranscriptCache,
};
/// Convenience: initialise a `Store` for data directory resolution.
+18 -11
View File
@@ -8,9 +8,13 @@
//! restore terminal → save settings → release lock.
use anyhow::Result;
use crossterm::event::{
DisableBracketedPaste, DisableMouseCapture, Event, KeyEventKind, MouseEventKind,
};
use crossterm::execute;
use crossterm::event::{DisableBracketedPaste, DisableMouseCapture, Event, KeyEventKind, MouseEventKind};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::io::{self, Write};
@@ -126,9 +130,9 @@ fn run_loop_inner(
// Adaptive poll: fast when animating, slow when idle.
let poll_timeout = if state.turn_in_flight() {
Duration::from_millis(50) // smooth spinner @ ~20fps
Duration::from_millis(50) // smooth spinner @ ~20fps
} else {
Duration::from_millis(200) // idle: 5fps, saves CPU
Duration::from_millis(200) // idle: 5fps, saves CPU
};
if crossterm::event::poll(poll_timeout)? {
@@ -140,7 +144,8 @@ fn run_loop_inner(
apply_action(state, action);
}
if let Some(text) = state.misc.pending_clipboard_copy.take() {
let _ = zesdex_infrastructure::utils::write_osc52(&mut io::stdout(), &text);
let _ =
zesdex_infrastructure::utils::write_osc52(&mut io::stdout(), &text);
state.push_toast(zesdex_infrastructure::Toast::new(
zesdex_infrastructure::ToastKind::Success,
"Copied to clipboard".to_string(),
@@ -195,13 +200,15 @@ fn create_local_session() -> Result<(zesdex_domain::core::Store, AppStateRest)>
// Load real settings from disk
use zesdex_domain::cms::{AppConfigRepository, SettingsRepository};
let settings = zesdex_infrastructure::persistence::cms::settings_repo::JsonSettingsRepository::new()
.load(&store.base_dir)
.unwrap_or_default();
let settings =
zesdex_infrastructure::persistence::cms::settings_repo::JsonSettingsRepository::new()
.load(&store.base_dir)
.unwrap_or_default();
let app_config = zesdex_infrastructure::persistence::cms::app_config_repo::JsonAppConfigRepository::new()
.load(&store.base_dir)
.unwrap_or_default();
let app_config =
zesdex_infrastructure::persistence::cms::app_config_repo::JsonAppConfigRepository::new()
.load(&store.base_dir)
.unwrap_or_default();
let workspace_roots = vec![std::env::current_dir()?];
let mut state = AppStateRest::new(workspace_roots, &session_dir, store.memory_dir.clone());
+14 -3
View File
@@ -13,9 +13,20 @@ pub enum AutocompleteKind {
/// Builtin slash-commands recognised by the chat input autocomplete.
const COMMANDS: &[&str] = &[
"/help", "/quit", "/clear", "/login", "/login zen", "/login openai",
"/edit", "/mcp add", "/model", "/model ls", "/model add",
"/todo", "/usage", "/compact",
"/help",
"/quit",
"/clear",
"/login",
"/login zen",
"/login openai",
"/edit",
"/mcp add",
"/model",
"/model ls",
"/model add",
"/todo",
"/usage",
"/compact",
];
/// The user's input buffer, cursor position, history, and autocomplete
+5 -1
View File
@@ -96,7 +96,11 @@ impl EditorState {
/// Create a new editor state for the given path.
pub fn new(path: PathBuf, content: String) -> Self {
let cursor = content.len();
EditorState { path, content, cursor }
EditorState {
path,
content,
cursor,
}
}
/// Return the full buffer content.
+21 -11
View File
@@ -25,8 +25,8 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tracing::warn;
use zesdex_domain::cms::{AppConfig, Settings};
use ratatui::text::Line;
use zesdex_domain::cms::{AppConfig, Settings};
use zesdex_infrastructure::{DirCache, MentionIndex, SessionRuntime, Toast, TurnEvent};
pub mod helpers;
@@ -47,7 +47,7 @@ pub use workflow::{AgentState, SimpleAgent, SimpleWorkflowEngine};
// Re-export the most commonly used helpers at the `state` level.
pub use helpers::{
count_tokens, current_effort, cycle_effort, cycle_selected_index, get_learning_items,
rewind_count, resolve_context_window, EFFORT_LEVELS, LearningItem,
resolve_context_window, rewind_count, LearningItem, EFFORT_LEVELS,
};
// ---------------------------------------------------------------------------
@@ -194,10 +194,7 @@ impl AppStateRest {
) -> Self {
let settings = Settings::default();
let app_config = AppConfig::default();
let worktrees_dir = memory_dir
.parent()
.unwrap_or(&memory_dir)
.join("worktrees");
let worktrees_dir = memory_dir.parent().unwrap_or(&memory_dir).join("worktrees");
let session_id = session_dir.file_name().map_or_else(
|| {
warn!("[state] session_dir has no file_name, using empty session_id");
@@ -272,29 +269,42 @@ impl AppStateRest {
/// Push an info toast.
pub fn toast_info(&mut self, msg: impl Into<String>) {
self.push_toast(Toast::new(zesdex_infrastructure::ToastKind::Info, msg.into()));
self.push_toast(Toast::new(
zesdex_infrastructure::ToastKind::Info,
msg.into(),
));
}
/// Push a success toast.
pub fn toast_success(&mut self, msg: impl Into<String>) {
self.push_toast(Toast::new(zesdex_infrastructure::ToastKind::Success, msg.into()));
self.push_toast(Toast::new(
zesdex_infrastructure::ToastKind::Success,
msg.into(),
));
}
/// Push a warning toast.
pub fn toast_warning(&mut self, msg: impl Into<String>) {
self.push_toast(Toast::new(zesdex_infrastructure::ToastKind::Warning, msg.into()));
self.push_toast(Toast::new(
zesdex_infrastructure::ToastKind::Warning,
msg.into(),
));
}
/// Push an error toast.
pub fn toast_error(&mut self, msg: impl Into<String>) {
self.push_toast(Toast::new(zesdex_infrastructure::ToastKind::Error, msg.into()));
self.push_toast(Toast::new(
zesdex_infrastructure::ToastKind::Error,
msg.into(),
));
}
/// Persist settings to disk.
pub fn save_settings(&self) {
if let Ok(store_dir) = std::fs::canonicalize(self.store_base_dir()) {
let repo =
zesdex_infrastructure::persistence::cms::settings_repo::JsonSettingsRepository::new();
zesdex_infrastructure::persistence::cms::settings_repo::JsonSettingsRepository::new(
);
use zesdex_domain::SettingsRepository;
if let Err(e) = repo.save(&store_dir, &self.settings) {
tracing::warn!("Failed to save settings: {e}");
+9 -8
View File
@@ -4,13 +4,13 @@
use std::sync::atomic::Ordering;
use tracing::info;
use zesdex_domain::core::ChatMessage;
use zesdex_application::agent::turn_service::AgentTurnServiceImpl;
use zesdex_application::agent::AgentTurnService;
use zesdex_domain::agent::AgentTurnParams;
use zesdex_domain::core::ChatMessage;
use zesdex_infrastructure::llm::provider::LlmClient;
use zesdex_infrastructure::tools::executor::InfrastructureToolExecutor;
use zesdex_infrastructure::tools::{all_tools, tool_defs, ToolCtx};
use zesdex_application::agent::turn_service::AgentTurnServiceImpl;
use zesdex_application::agent::AgentTurnService;
use crate::state::AppStateRest;
@@ -113,7 +113,9 @@ pub fn spawn_agent_turn(state: &mut AppStateRest, text: String) {
// Clone credentials before moving into LlmClient.
let explore_api_key = api_key.clone();
let explore_model = model.clone();
let explore_base_url = api_base.clone().unwrap_or_else(|| "https://api.openai.com/v1".to_string());
let explore_base_url = api_base
.clone()
.unwrap_or_else(|| "https://api.openai.com/v1".to_string());
let client = std::sync::Arc::new(LlmClient::new(api_key, model, api_base));
@@ -126,8 +128,7 @@ pub fn spawn_agent_turn(state: &mut AppStateRest, text: String) {
// Clone ToolCtx for the explore service (before moving into executor).
let explore_ctx = tool_ctx.clone();
let tool_executor =
std::sync::Arc::new(InfrastructureToolExecutor::new(tool_ctx));
let tool_executor = std::sync::Arc::new(InfrastructureToolExecutor::new(tool_ctx));
let tools = all_tools();
let defs = tool_defs(&tools);
@@ -145,8 +146,8 @@ pub fn spawn_agent_turn(state: &mut AppStateRest, text: String) {
),
);
let turn_service = AgentTurnServiceImpl::new(client, tool_executor, defs)
.with_explore(explore_service);
let turn_service =
AgentTurnServiceImpl::new(client, tool_executor, defs).with_explore(explore_service);
tokio::spawn(async move {
let _ = turn_service.run_turn(params).await;
+122 -116
View File
@@ -96,12 +96,16 @@ fn render_one_message(
if !msg.reasoning.trim().is_empty() {
let reasoning_str = format!("[Thinking...]\n{}", msg.reasoning.trim());
let reasoning_spans = super::markdown::render_markdown(&reasoning_str, content_width, false);
let reasoning_spans =
super::markdown::render_markdown(&reasoning_str, content_width, false);
// Dim the reasoning text
let dimmed_spans: Vec<Span> = reasoning_spans.into_iter().map(|mut s| {
s.style = s.style.fg(Theme::TEXT_DIM);
s
}).collect();
let dimmed_spans: Vec<Span> = reasoning_spans
.into_iter()
.map(|mut s| {
s.style = s.style.fg(Theme::TEXT_DIM);
s
})
.collect();
let reasoning_lines = split_spans_into_lines(dimmed_spans);
let mut lines_iter = reasoning_lines.into_iter();
@@ -156,7 +160,7 @@ fn render_one_message(
lines.push(Line::from(spans));
}
}
if !first_line_handled {
lines.push(Line::from(header_prefix));
}
@@ -166,134 +170,136 @@ fn render_one_message(
impl Component for ChatComponent {
fn pre_render(&mut self, state: &mut crate::state::AppStateRest) {
let content_width = state.last_render_width.saturating_sub(PREFIX_WIDTH as u16 + 2);
let msg_count = state.transcript_cache.messages.len();
let cached_count = state.cached_msg_count;
let content_width = state
.last_render_width
.saturating_sub(PREFIX_WIDTH as u16 + 2);
let msg_count = state.transcript_cache.messages.len();
let cached_count = state.cached_msg_count;
let needs_full_rebuild = state.transcript_cache.dirty
&& (state.last_render_width == 0
let needs_full_rebuild = state.transcript_cache.dirty
&& (state.last_render_width == 0
|| msg_count < cached_count // pesan di-evict dari depan
|| state.render_width_at_cache != state.last_render_width); // resize
if needs_full_rebuild {
// Full rebuild: parse semua pesan dari nol
let mut all_lines: Vec<Line<'static>> = Vec::new();
for msg in &state.transcript_cache.messages {
let msg_lines = render_one_message(msg, content_width);
all_lines.extend(msg_lines);
}
state.display_lines_cache = all_lines;
state.cached_msg_count = msg_count;
state.render_width_at_cache = state.last_render_width;
state.transcript_cache.dirty = false;
} else if state.transcript_cache.dirty && msg_count > cached_count {
// Incremental: hanya render pesan baru yang belum ada di cache
let new_msgs: Vec<_> = state
.transcript_cache
.messages
.iter()
.skip(cached_count)
.collect();
for msg in new_msgs {
let msg_lines = render_one_message(msg, content_width);
state.display_lines_cache.extend(msg_lines);
}
state.cached_msg_count = msg_count;
state.transcript_cache.dirty = false;
}
// Lazily recompute token count — hanya saat ada pesan baru
if state.token_count_dirty {
if let Some(ref rt) = state.session_runtime {
state.cached_token_count = rt
if needs_full_rebuild {
// Full rebuild: parse semua pesan dari nol
let mut all_lines: Vec<Line<'static>> = Vec::new();
for msg in &state.transcript_cache.messages {
let msg_lines = render_one_message(msg, content_width);
all_lines.extend(msg_lines);
}
state.display_lines_cache = all_lines;
state.cached_msg_count = msg_count;
state.render_width_at_cache = state.last_render_width;
state.transcript_cache.dirty = false;
} else if state.transcript_cache.dirty && msg_count > cached_count {
// Incremental: hanya render pesan baru yang belum ada di cache
let new_msgs: Vec<_> = state
.transcript_cache
.messages
.iter()
.filter_map(|m| m.content.as_deref())
.map(crate::state::count_tokens)
.sum();
} else {
state.cached_token_count = 0;
.skip(cached_count)
.collect();
for msg in new_msgs {
let msg_lines = render_one_message(msg, content_width);
state.display_lines_cache.extend(msg_lines);
}
state.cached_msg_count = msg_count;
state.transcript_cache.dirty = false;
}
// Lazily recompute token count — hanya saat ada pesan baru
if state.token_count_dirty {
if let Some(ref rt) = state.session_runtime {
state.cached_token_count = rt
.messages
.iter()
.filter_map(|m| m.content.as_deref())
.map(crate::state::count_tokens)
.sum();
} else {
state.cached_token_count = 0;
}
state.token_count_dirty = false;
}
state.token_count_dirty = false;
}
}
fn draw(&mut self, frame: &mut Frame, area: Rect, state: &crate::state::AppStateRest) {
let messages = &state.transcript_cache.messages;
let scroll_offset = state.scroll.offset;
let max_visible = (area.height as usize).saturating_sub(3);
let cache = &state.display_lines_cache;
let messages = &state.transcript_cache.messages;
let scroll_offset = state.scroll.offset;
let max_visible = (area.height as usize).saturating_sub(3);
let cache = &state.display_lines_cache;
// Hitung window tanpa clone
let total = cache.len();
let spinner_extra = usize::from(state.turn_in_flight());
let total_with_spinner = total + spinner_extra;
let max_offset = total_with_spinner.saturating_sub(max_visible);
let offset = scroll_offset.min(max_offset);
// Hitung window tanpa clone
let total = cache.len();
let spinner_extra = usize::from(state.turn_in_flight());
let total_with_spinner = total + spinner_extra;
let max_offset = total_with_spinner.saturating_sub(max_visible);
let offset = scroll_offset.min(max_offset);
let end_idx = total_with_spinner.saturating_sub(offset);
let start_idx = end_idx.saturating_sub(max_visible);
let end_idx = total_with_spinner.saturating_sub(offset);
let start_idx = end_idx.saturating_sub(max_visible);
// Kumpulkan hanya baris yang visible — tidak clone semua
let mut visible: Vec<Line> = Vec::with_capacity(max_visible);
let cache_end = end_idx.min(total);
let cache_start = start_idx.min(cache_end);
if cache_start < cache_end {
visible.extend_from_slice(&cache[cache_start..cache_end]);
}
// Kumpulkan hanya baris yang visible — tidak clone semua
let mut visible: Vec<Line> = Vec::with_capacity(max_visible);
let cache_end = end_idx.min(total);
let cache_start = start_idx.min(cache_end);
if cache_start < cache_end {
visible.extend_from_slice(&cache[cache_start..cache_end]);
}
// Spinner hanya ditambahkan jika visible window mencakup posisi terakhir
if state.turn_in_flight() && end_idx > total {
let spinner_frames = ["", "", "", "", "", "", "", "", "", ""];
let frame_idx = (state.misc.tick_count as usize / 2) % spinner_frames.len();
let spinner = spinner_frames[frame_idx];
visible.push(Line::from(vec![
Span::styled(
format!("{} ", format_role_label(&Role::Assistant)),
Style::default()
.fg(Theme::ROLE_ASSISTANT)
.add_modifier(Modifier::BOLD),
),
Span::styled(format!("{spinner} "), Style::default().fg(Theme::TEXT_DIM)),
Span::styled(
"generating...",
// Spinner hanya ditambahkan jika visible window mencakup posisi terakhir
if state.turn_in_flight() && end_idx > total {
let spinner_frames = ["", "", "", "", "", "", "", "", "", ""];
let frame_idx = (state.misc.tick_count as usize / 2) % spinner_frames.len();
let spinner = spinner_frames[frame_idx];
visible.push(Line::from(vec![
Span::styled(
format!("{} ", format_role_label(&Role::Assistant)),
Style::default()
.fg(Theme::ROLE_ASSISTANT)
.add_modifier(Modifier::BOLD),
),
Span::styled(format!("{spinner} "), Style::default().fg(Theme::TEXT_DIM)),
Span::styled(
"generating...",
Style::default()
.fg(Theme::TEXT_MUTED)
.add_modifier(Modifier::ITALIC),
),
]));
}
let scroll_pct = if total_with_spinner > max_visible && max_offset > 0 {
((offset as f64 / max_offset as f64) * 100.0) as u8
} else {
0
};
let title = if scroll_pct > 0 {
format!(" 💬 Chat [{} msgs] ── {}% ↑ ", messages.len(), scroll_pct)
} else if messages.is_empty() {
String::from(" 💬 Chat ")
} else {
format!(" 💬 Chat [{} msgs] ", messages.len())
};
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Theme::BORDER))
.title(Span::styled(
title,
Style::default()
.fg(Theme::TEXT_MUTED)
.add_modifier(Modifier::ITALIC),
),
]));
}
.add_modifier(Modifier::BOLD),
));
let scroll_pct = if total_with_spinner > max_visible && max_offset > 0 {
((offset as f64 / max_offset as f64) * 100.0) as u8
} else {
0
};
let paragraph = Paragraph::new(visible)
.block(block)
.style(Style::default().bg(Theme::BG));
let title = if scroll_pct > 0 {
format!(" 💬 Chat [{} msgs] ── {}% ↑ ", messages.len(), scroll_pct)
} else if messages.is_empty() {
String::from(" 💬 Chat ")
} else {
format!(" 💬 Chat [{} msgs] ", messages.len())
};
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Theme::BORDER))
.title(Span::styled(
title,
Style::default()
.fg(Theme::TEXT_MUTED)
.add_modifier(Modifier::BOLD),
));
let paragraph = Paragraph::new(visible)
.block(block)
.style(Style::default().bg(Theme::BG));
frame.render_widget(paragraph, area);
frame.render_widget(paragraph, area);
}
}
+5 -3
View File
@@ -33,7 +33,9 @@ impl Component for InputComponent {
.border_style(Style::default().fg(Theme::BORDER))
.title(Span::styled(
dropdown_title,
Style::default().fg(Theme::PRIMARY).add_modifier(Modifier::BOLD),
Style::default()
.fg(Theme::PRIMARY)
.add_modifier(Modifier::BOLD),
))
.style(Style::default().bg(Theme::SURFACE_ELEVATED));
@@ -98,7 +100,7 @@ impl Component for InputComponent {
let char_len = c.len_utf8();
(c.to_string(), &after[char_len..])
};
// Blinking cursor logic based on tick count
let cursor_style = if state.misc.tick_count % 10 < 5 {
Style::default()
@@ -111,7 +113,7 @@ impl Component for InputComponent {
.fg(Theme::BG)
.add_modifier(Modifier::BOLD)
};
spans.push(Span::styled(cursor_char, cursor_style));
if !after_char.is_empty() {
spans.push(Span::raw(after_char.to_string()));
+168 -173
View File
@@ -60,200 +60,195 @@ pub fn render_markdown(text: &str, width: u16, dim: bool) -> Vec<Span<'static>>
for event in parser {
match event {
pulldown_cmark::Event::Start(tag) => {
match tag {
pulldown_cmark::Tag::CodeBlock(kind) => {
in_code_block = true;
in_diff_block = matches!(
&kind,
pulldown_cmark::CodeBlockKind::Fenced(lang) if lang.as_ref() == "diff"
);
spans.push(Span::styled("\n", Style::default()));
spans.push(Span::styled(
" ┌─ code ",
apply_dim(
Style::default().fg(Theme::TEXT_MUTED).bg(Theme::CODE_BG),
dim,
),
));
spans.push(Span::styled("\n", Style::default()));
}
pulldown_cmark::Tag::Heading { level, .. } => {
in_heading = true;
heading_level = match level {
pulldown_cmark::HeadingLevel::H1 => 1,
pulldown_cmark::HeadingLevel::H2 => 2,
pulldown_cmark::HeadingLevel::H3 => 3,
_ => 4,
};
}
pulldown_cmark::Tag::Item => {
spans.push(Span::styled(
"",
apply_dim(Style::default().fg(Theme::PRIMARY), dim),
));
}
pulldown_cmark::Tag::Link { dest_url, .. } => {
spans.push(Span::styled(
"[",
apply_dim(Style::default().fg(Theme::INFO), dim),
));
spans.push(Span::styled(
format!("]({dest_url})"),
apply_dim(
Style::default()
.fg(Theme::TEXT_MUTED)
.add_modifier(Modifier::ITALIC),
dim,
),
));
}
pulldown_cmark::Tag::BlockQuote(_) => {
spans.push(Span::styled(
"",
apply_dim(Style::default().fg(Theme::BLOCKQUOTE_BAR), dim),
));
}
pulldown_cmark::Tag::Table(_) => {
table_rows.clear();
}
pulldown_cmark::Tag::TableHead | pulldown_cmark::Tag::TableRow => {
current_row.clear();
}
pulldown_cmark::Tag::TableCell => {
in_table_cell = true;
current_cell.clear();
}
_ => {}
pulldown_cmark::Event::Start(tag) => match tag {
pulldown_cmark::Tag::CodeBlock(kind) => {
in_code_block = true;
in_diff_block = matches!(
&kind,
pulldown_cmark::CodeBlockKind::Fenced(lang) if lang.as_ref() == "diff"
);
spans.push(Span::styled("\n", Style::default()));
spans.push(Span::styled(
" ┌─ code ",
apply_dim(
Style::default().fg(Theme::TEXT_MUTED).bg(Theme::CODE_BG),
dim,
),
));
spans.push(Span::styled("\n", Style::default()));
}
}
pulldown_cmark::Event::End(tag) => {
match tag {
pulldown_cmark::TagEnd::CodeBlock => {
in_code_block = false;
in_diff_block = false;
spans.push(Span::styled(
"\n └─\n",
apply_dim(
Style::default().fg(Theme::TEXT_MUTED).bg(Theme::CODE_BG),
dim,
),
));
pulldown_cmark::Tag::Heading { level, .. } => {
in_heading = true;
heading_level = match level {
pulldown_cmark::HeadingLevel::H1 => 1,
pulldown_cmark::HeadingLevel::H2 => 2,
pulldown_cmark::HeadingLevel::H3 => 3,
_ => 4,
};
}
pulldown_cmark::Tag::Item => {
spans.push(Span::styled(
"",
apply_dim(Style::default().fg(Theme::PRIMARY), dim),
));
}
pulldown_cmark::Tag::Link { dest_url, .. } => {
spans.push(Span::styled(
"[",
apply_dim(Style::default().fg(Theme::INFO), dim),
));
spans.push(Span::styled(
format!("]({dest_url})"),
apply_dim(
Style::default()
.fg(Theme::TEXT_MUTED)
.add_modifier(Modifier::ITALIC),
dim,
),
));
}
pulldown_cmark::Tag::BlockQuote(_) => {
spans.push(Span::styled(
"",
apply_dim(Style::default().fg(Theme::BLOCKQUOTE_BAR), dim),
));
}
pulldown_cmark::Tag::Table(_) => {
table_rows.clear();
}
pulldown_cmark::Tag::TableHead | pulldown_cmark::Tag::TableRow => {
current_row.clear();
}
pulldown_cmark::Tag::TableCell => {
in_table_cell = true;
current_cell.clear();
}
_ => {}
},
pulldown_cmark::Event::End(tag) => match tag {
pulldown_cmark::TagEnd::CodeBlock => {
in_code_block = false;
in_diff_block = false;
spans.push(Span::styled(
"\n └─\n",
apply_dim(
Style::default().fg(Theme::TEXT_MUTED).bg(Theme::CODE_BG),
dim,
),
));
}
pulldown_cmark::TagEnd::Heading(_) => {
in_heading = false;
heading_level = 0;
spans.push(Span::raw("\n"));
}
pulldown_cmark::TagEnd::Paragraph => {
spans.push(Span::raw("\n\n"));
}
pulldown_cmark::TagEnd::Item | pulldown_cmark::TagEnd::BlockQuote(_) => {
spans.push(Span::raw("\n"));
}
pulldown_cmark::TagEnd::TableCell => {
in_table_cell = false;
current_row.push(std::mem::take(&mut current_cell));
}
pulldown_cmark::TagEnd::TableHead | pulldown_cmark::TagEnd::TableRow => {
table_rows.push(std::mem::take(&mut current_row));
}
pulldown_cmark::TagEnd::Table => {
let cols_count = table_rows.first().map_or(0, std::vec::Vec::len);
if cols_count == 0 {
continue;
}
pulldown_cmark::TagEnd::Heading(_) => {
in_heading = false;
heading_level = 0;
spans.push(Span::raw("\n"));
}
pulldown_cmark::TagEnd::Paragraph => {
spans.push(Span::raw("\n\n"));
}
pulldown_cmark::TagEnd::Item | pulldown_cmark::TagEnd::BlockQuote(_) => {
spans.push(Span::raw("\n"));
}
pulldown_cmark::TagEnd::TableCell => {
in_table_cell = false;
current_row.push(std::mem::take(&mut current_cell));
}
pulldown_cmark::TagEnd::TableHead | pulldown_cmark::TagEnd::TableRow => {
table_rows.push(std::mem::take(&mut current_row));
}
pulldown_cmark::TagEnd::Table => {
let cols_count = table_rows.first().map_or(0, std::vec::Vec::len);
if cols_count == 0 {
continue;
}
let mut col_widths = vec![0; cols_count];
for row in &table_rows {
for (i, cell) in row.iter().enumerate() {
if i < cols_count {
let cell_width: usize =
cell.iter().map(|s| s.content.chars().count()).sum();
if cell_width > col_widths[i] {
col_widths[i] = cell_width;
}
let mut col_widths = vec![0; cols_count];
for row in &table_rows {
for (i, cell) in row.iter().enumerate() {
if i < cols_count {
let cell_width: usize =
cell.iter().map(|s| s.content.chars().count()).sum();
if cell_width > col_widths[i] {
col_widths[i] = cell_width;
}
}
}
let effective_width = if width > 0 {
(width as usize).saturating_sub(2)
} else {
0
};
let border_overhead = cols_count * 3 + 4;
let available_width = effective_width.saturating_sub(border_overhead);
let mut total_width: usize = col_widths.iter().sum();
}
let effective_width = if width > 0 {
(width as usize).saturating_sub(2)
} else {
0
};
let border_overhead = cols_count * 3 + 4;
let available_width = effective_width.saturating_sub(border_overhead);
let mut total_width: usize = col_widths.iter().sum();
if width > 0 && total_width > available_width && available_width > 0 {
while total_width > available_width {
let Some(max_idx) = col_widths
.iter()
.enumerate()
.max_by_key(|&(_, &w)| w)
.map(|(i, _)| i)
else {
break;
};
if col_widths[max_idx] <= 3 {
break;
}
col_widths[max_idx] -= 1;
total_width -= 1;
if width > 0 && total_width > available_width && available_width > 0 {
while total_width > available_width {
let Some(max_idx) = col_widths
.iter()
.enumerate()
.max_by_key(|&(_, &w)| w)
.map(|(i, _)| i)
else {
break;
};
if col_widths[max_idx] <= 3 {
break;
}
col_widths[max_idx] -= 1;
total_width -= 1;
}
}
spans.push(Span::raw("\n"));
for (r, row) in table_rows.iter().enumerate() {
let mut cell_lines = Vec::new();
for (i, cell) in row.iter().enumerate() {
if i < cols_count {
cell_lines.push(wrap_spans_to_lines(cell, col_widths[i]));
}
}
let max_height =
cell_lines.iter().map(std::vec::Vec::len).max().unwrap_or(1);
spans.push(Span::raw("\n"));
for (r, row) in table_rows.iter().enumerate() {
let mut cell_lines = Vec::new();
for (i, cell) in row.iter().enumerate() {
if i < cols_count {
cell_lines.push(wrap_spans_to_lines(cell, col_widths[i]));
for y in 0..max_height {
spans.push(Span::styled(
" | ",
apply_dim(Style::default().fg(Theme::BORDER), dim),
));
for (i, cl) in cell_lines.iter().enumerate() {
let line_spans = if y < cl.len() { &cl[y] } else { [].as_slice() };
let mut line_width = 0;
for span in line_spans {
line_width += span.content.chars().count();
spans.push(span.clone());
}
}
let max_height =
cell_lines.iter().map(std::vec::Vec::len).max().unwrap_or(1);
for y in 0..max_height {
let pad = col_widths[i].saturating_sub(line_width);
spans.push(Span::raw(" ".repeat(pad)));
spans.push(Span::styled(
" | ",
" | ",
apply_dim(Style::default().fg(Theme::BORDER), dim),
));
for (i, cl) in cell_lines.iter().enumerate() {
let line_spans =
if y < cl.len() { &cl[y] } else { [].as_slice() };
let mut line_width = 0;
for span in line_spans {
line_width += span.content.chars().count();
spans.push(span.clone());
}
let pad = col_widths[i].saturating_sub(line_width);
spans.push(Span::raw(" ".repeat(pad)));
spans.push(Span::styled(
" | ",
apply_dim(Style::default().fg(Theme::BORDER), dim),
));
}
spans.push(Span::raw("\n"));
}
if r == 0 {
spans.push(Span::raw("\n"));
}
if r == 0 {
spans.push(Span::styled(
" | ",
apply_dim(Style::default().fg(Theme::BORDER), dim),
));
for w in &col_widths {
spans.push(Span::styled(
" | ",
format!("{}| ", "-".repeat(*w + 1)),
apply_dim(Style::default().fg(Theme::BORDER), dim),
));
for w in &col_widths {
spans.push(Span::styled(
format!("{}| ", "-".repeat(*w + 1)),
apply_dim(Style::default().fg(Theme::BORDER), dim),
));
}
spans.push(Span::raw("\n"));
}
spans.push(Span::raw("\n"));
}
spans.push(Span::raw("\n"));
}
_ => {}
spans.push(Span::raw("\n"));
}
}
_ => {}
},
pulldown_cmark::Event::Text(text) => {
let s = text.to_string();
if in_code_block {
@@ -1,9 +1,9 @@
//! Overlay: list of active / completed bash background jobs.
use crate::view::theme::Theme;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph};
use ratatui::Frame;
use crate::view::theme::Theme;
use tracing::debug;
/// Render the Bash Jobs overlay.
@@ -1,9 +1,9 @@
//! Overlay: confirm-before-clear dialog for the chat transcript.
use crate::view::theme::Theme;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph};
use ratatui::Frame;
use crate::view::theme::Theme;
use tracing::debug;
/// Render the Clear Transcript confirmation dialog.
@@ -5,14 +5,14 @@
//! coloring (green `+`, red `-`, blue header, dim context) → render in a
//! scrollable paragraph widget.
use super::super::theme::Theme;
use super::overlay_block;
use crate::state::AppStateRest;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph, Wrap};
use ratatui::Frame;
use super::super::theme::Theme;
use super::overlay_block;
/// Render the diff preview overlay with color-coded +/- lines and scrolling.
///
@@ -54,9 +54,7 @@ pub fn render(frame: &mut Frame, area: Rect, block: Block<'static>, state: &AppS
// Hunk header — cyan
Line::from(Span::styled(
format!("{trimmed}\n"),
Style::default()
.fg(Theme::INFO)
.add_modifier(Modifier::DIM),
Style::default().fg(Theme::INFO).add_modifier(Modifier::DIM),
))
} else if trimmed.starts_with("diff --git")
|| trimmed.starts_with("index ")
@@ -1,10 +1,10 @@
//! Overlay: inline editor mode — shows the current input buffer with cursor
//! position and save/dismiss key hints.
use crate::view::theme::Theme;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph};
use ratatui::Frame;
use crate::view::theme::Theme;
use tracing::debug;
/// Render the Editor overlay.
@@ -15,7 +15,10 @@ pub fn render(
block: Block<'static>,
state: &crate::state::AppStateRest,
) {
debug!("Rendering Editor overlay, buffer length: {}", state.input.buffer.len());
debug!(
"Rendering Editor overlay, buffer length: {}",
state.input.buffer.len()
);
let block = block
.title(Span::styled(
" Editor ",
@@ -1,8 +1,8 @@
//! Overlay: keyboard shortcut reference.
use crate::view::theme::Theme;
use ratatui::style::Style;
use ratatui::widgets::{Block, Paragraph, Wrap};
use ratatui::Frame;
use crate::view::theme::Theme;
use tracing::debug;
/// Render the Help overlay.
@@ -13,7 +13,10 @@ pub fn render(
block: Block<'static>,
state: &crate::state::AppStateRest,
) {
debug!("Rendering Help overlay, content length: {}", state.help_text.len());
debug!(
"Rendering Help overlay, content length: {}",
state.help_text.len()
);
let block = super::overlay_block(block, "Help", Theme::INFO);
let content = state.help_text;
let paragraph = Paragraph::new(content)
@@ -1,10 +1,10 @@
//! Overlay: API key input dialog — prompts the user for a provider API key
//! with masked display (shows first 4 chars only).
use crate::view::theme::Theme;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph};
use ratatui::Frame;
use crate::view::theme::Theme;
use tracing::debug;
/// Render the API Key input overlay.
@@ -71,8 +71,14 @@ pub fn render(
Style::default().fg(Theme::WARNING)
},
),
LearningItem::Stored { name, lifecycle, .. } => {
let status = if lifecycle == "stale" { "Stale" } else { "Active" };
LearningItem::Stored {
name, lifecycle, ..
} => {
let status = if lifecycle == "stale" {
"Stale"
} else {
"Active"
};
(
format!("{prefix}[{status}] {name}"),
if is_selected {
@@ -110,7 +116,12 @@ pub fn render(
let mut right_lines = Vec::new();
if let Some(item) = items.get(selected) {
match item {
LearningItem::Pending { name, content, scope, confidence } => {
LearningItem::Pending {
name,
content,
scope,
confidence,
} => {
right_lines.push(Line::from(Span::styled(
" Name:",
Style::default().fg(Theme::TEXT_DIM),
@@ -151,7 +162,13 @@ pub fn render(
Style::default().fg(Theme::TEXT_DIM),
)));
}
LearningItem::Stored { name, content, lifecycle, scope, description } => {
LearningItem::Stored {
name,
content,
lifecycle,
scope,
description,
} => {
right_lines.push(Line::from(Span::styled(
" Name:",
Style::default().fg(Theme::TEXT_DIM),
@@ -163,7 +180,11 @@ pub fn render(
.add_modifier(Modifier::BOLD),
)));
right_lines.push(Line::from(Span::raw("")));
let status_color = if lifecycle == "stale" { Theme::WARNING } else { Theme::SUCCESS };
let status_color = if lifecycle == "stale" {
Theme::WARNING
} else {
Theme::SUCCESS
};
right_lines.push(Line::from(Span::styled(
format!(" Status: {lifecycle}"),
Style::default().fg(status_color),
@@ -1,9 +1,9 @@
//! Overlay: loading / processing spinner — shown during blocking operations.
use crate::view::theme::Theme;
use ratatui::style::{Modifier, Style};
use ratatui::text::Span;
use ratatui::widgets::{Block, Paragraph};
use ratatui::Frame;
use crate::view::theme::Theme;
use tracing::debug;
/// Render the Loading overlay.
+5 -2
View File
@@ -1,9 +1,9 @@
//! Overlay: MCP (Model Context Protocol) server management.
use crate::view::theme::Theme;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph};
use ratatui::Frame;
use crate::view::theme::Theme;
use tracing::debug;
/// Render the MCP Servers overlay.
@@ -14,7 +14,10 @@ pub fn render(
block: Block<'static>,
state: &crate::state::AppStateRest,
) {
debug!("Rendering MCP Servers overlay, session dir: {}", state.session_dir.display());
debug!(
"Rendering MCP Servers overlay, session dir: {}",
state.session_dir.display()
);
let block = block
.title(Span::styled(
" MCP Servers ",
+9 -12
View File
@@ -13,30 +13,32 @@ pub mod learning;
pub mod loading;
pub mod mcp;
pub mod model_selector;
pub mod plan;
pub mod quit_confirm;
pub mod rewind;
pub mod settings;
pub mod todo;
pub mod plan;
pub mod usage;
use super::theme::Theme;
use crate::state::{AppStateRest, Overlay};
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::Span;
use ratatui::widgets::{Block, Borders, Clear};
use ratatui::Frame;
use super::theme::Theme;
use tracing::debug;
/// Decorate an overlay block with a styled title and matching border color.
pub fn overlay_block(block: Block<'static>, title: &str, color: ratatui::style::Color) -> Block<'static> {
pub fn overlay_block(
block: Block<'static>,
title: &str,
color: ratatui::style::Color,
) -> Block<'static> {
block
.title(Span::styled(
format!(" {title} "),
Style::default()
.fg(color)
.add_modifier(Modifier::BOLD),
Style::default().fg(color).add_modifier(Modifier::BOLD),
))
.border_style(Style::default().fg(color))
}
@@ -59,12 +61,7 @@ pub fn centered_rect(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
/// Dispatches to the appropriate overlay module's `render` function based on the
/// `Overlay` variant. No-ops for `Overlay::None`.
#[tracing::instrument(skip(frame, state))]
pub fn render_overlay(
frame: &mut Frame,
area: Rect,
overlay: Overlay,
state: &AppStateRest,
) {
pub fn render_overlay(frame: &mut Frame, area: Rect, overlay: Overlay, state: &AppStateRest) {
debug!("Rendering overlay: {overlay:?}");
let overlay_area = centered_rect(area, 75, 70);
frame.render_widget(Clear, overlay_area);
@@ -15,7 +15,10 @@ pub fn render(
block: Block<'static>,
state: &crate::state::AppStateRest,
) {
debug!("Rendering Model Selector overlay, current provider: {}, model: {}", state.settings.provider, state.settings.model);
debug!(
"Rendering Model Selector overlay, current provider: {}, model: {}",
state.settings.provider, state.settings.model
);
let block = block
.title(Span::styled(
" Model Selector ",
@@ -1,9 +1,9 @@
//! Overlay: Project Plan view — shows the full project plan content.
use crate::view::theme::Theme;
use ratatui::style::{Modifier, Style};
use ratatui::text::Span;
use ratatui::widgets::{Block, Paragraph, Wrap};
use ratatui::Frame;
use crate::view::theme::Theme;
use tracing::debug;
/// Render the Project Plan overlay.
@@ -1,9 +1,9 @@
//! Overlay: quit confirmation dialog.
use crate::view::theme::Theme;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph};
use ratatui::Frame;
use crate::view::theme::Theme;
use tracing::debug;
/// Render the Quit confirmation overlay.
@@ -40,7 +40,11 @@ pub fn render(
Style::default().fg(Theme::TEXT_DIM),
)));
} else {
let start = if messages.len() > 8 { messages.len() - 8 } else { 0 };
let start = if messages.len() > 8 {
messages.len() - 8
} else {
0
};
for msg in messages.iter().skip(start) {
let role_str = match msg.role {
Role::User => "User",
@@ -51,7 +55,11 @@ pub fn render(
let preview: String = msg.content.chars().take(70).collect();
lines.push(Line::from(Span::styled(
format!(" [{role_str}] {preview}"),
Style::default().fg(if msg.role == Role::User { Theme::INFO } else { Theme::TEXT }),
Style::default().fg(if msg.role == Role::User {
Theme::INFO
} else {
Theme::TEXT
}),
)));
}
if messages.len() > 8 {
@@ -1,10 +1,10 @@
//! Overlay: settings overview — displays the current provider, model,
//! max tokens, temperature, internet mode, and review toggle.
use crate::view::theme::Theme;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph};
use ratatui::Frame;
use crate::view::theme::Theme;
use tracing::debug;
/// Render the Settings overlay.
@@ -15,7 +15,10 @@ pub fn render(
block: Block<'static>,
state: &crate::state::AppStateRest,
) {
debug!("Rendering Settings overlay, provider: {}, model: {}", state.settings.provider, state.settings.model);
debug!(
"Rendering Settings overlay, provider: {}, model: {}",
state.settings.provider, state.settings.model
);
let block = super::overlay_block(block, "Settings", Theme::PRIMARY);
let lines = vec![
Line::from(Span::styled(
@@ -29,14 +32,20 @@ pub fn render(
Line::from(Span::styled(
format!(
" Max tokens: {}",
state.settings.max_tokens.map_or_else(|| "auto".to_string(), |v| v.to_string())
state
.settings
.max_tokens
.map_or_else(|| "auto".to_string(), |v| v.to_string())
),
Style::default().fg(Theme::TEXT),
)),
Line::from(Span::styled(
format!(
" Temperature: {}",
state.settings.temperature.map_or_else(|| "auto".to_string(), |v| format!("{v:.1}"))
state
.settings
.temperature
.map_or_else(|| "auto".to_string(), |v| format!("{v:.1}"))
),
Style::default().fg(Theme::TEXT),
)),
@@ -1,9 +1,9 @@
//! Overlay: Tasks (todo) view — shows the full todo list content.
use crate::view::theme::Theme;
use ratatui::style::{Modifier, Style};
use ratatui::text::Span;
use ratatui::widgets::{Block, Paragraph, Wrap};
use ratatui::Frame;
use crate::view::theme::Theme;
use tracing::debug;
/// Render the Tasks / Todo overlay.
+15 -3
View File
@@ -31,7 +31,12 @@ pub fn render(
let summary = runtime.map(|r| compute_usage_summary(&r.usage, r.session_start, now_ms));
let (edit_count, lesson_count, review_count, consec_empty) =
runtime.map_or((0, 0, 0, 0), |r| {
(r.edit_count, r.lessons.total, r.review_count, r.consecutive_empty_reviews)
(
r.edit_count,
r.lessons.total,
r.review_count,
r.consecutive_empty_reviews,
)
});
let mut lines = vec![
Line::from(Span::styled(
@@ -88,12 +93,19 @@ pub fn render(
)));
lines.push(Line::from(Span::styled(
format!(" Empty reviews: {consec_empty}"),
Style::default().fg(if consec_empty > 3 { Theme::WARNING } else { Theme::TEXT_DIM }),
Style::default().fg(if consec_empty > 3 {
Theme::WARNING
} else {
Theme::TEXT_DIM
}),
)));
if let Some(s) = &summary {
lines.push(Line::from(Span::raw("")));
lines.push(Line::from(Span::styled(
format!(" Session: {}h {}m {}s", s.elapsed_hours, s.elapsed_minutes, s.elapsed_seconds),
format!(
" Session: {}h {}m {}s",
s.elapsed_hours, s.elapsed_minutes, s.elapsed_seconds
),
Style::default().fg(Theme::TEXT_DIM),
)));
}
+15 -13
View File
@@ -52,7 +52,7 @@ fn draw_tasks_widget(frame: &mut Frame, area: Rect, state: &crate::state::AppSta
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Theme::BORDER))
.style(Style::default().bg(Theme::SURFACE));
let inner_area = block.inner(area);
let budget = (inner_area.height as usize).max(1);
@@ -62,7 +62,9 @@ fn draw_tasks_widget(frame: &mut Frame, area: Rect, state: &crate::state::AppSta
let lines: Vec<Line> = if task_lines.is_empty() {
vec![Line::from(Span::styled(
" No tasks yet.",
Style::default().fg(Theme::TEXT_DIM).add_modifier(Modifier::ITALIC),
Style::default()
.fg(Theme::TEXT_DIM)
.add_modifier(Modifier::ITALIC),
))]
} else {
let show_hint = task_lines.len() > budget;
@@ -113,15 +115,13 @@ fn draw_usage_widget(frame: &mut Frame, area: Rect, state: &crate::state::AppSta
} else {
state.cached_token_count
};
let mut items = vec![
Line::from(Span::styled(
format!(" {:<6}: {} tok", "Total", summary.total_tokens),
Style::default()
.fg(Theme::TEXT)
.add_modifier(Modifier::BOLD),
)),
];
let mut items = vec![Line::from(Span::styled(
format!(" {:<6}: {} tok", "Total", summary.total_tokens),
Style::default()
.fg(Theme::TEXT)
.add_modifier(Modifier::BOLD),
))];
if summary.self_learning_tokens > 0 {
items.push(Line::from(Span::styled(
@@ -159,12 +159,14 @@ fn draw_usage_widget(frame: &mut Frame, area: Rect, state: &crate::state::AppSta
Style::default().fg(Theme::TEXT_DIM),
)),
]);
items
} else {
vec![Line::from(Span::styled(
" No active session.",
Style::default().fg(Theme::TEXT_DIM).add_modifier(Modifier::ITALIC),
Style::default()
.fg(Theme::TEXT_DIM)
.add_modifier(Modifier::ITALIC),
))]
};
+6 -2
View File
@@ -44,7 +44,10 @@ impl Component for StatusBarComponent {
status_badge,
];
let right_str = format!(" Provider: {} | Model: {} ", state.settings.provider, state.settings.model);
let right_str = format!(
" Provider: {} | Model: {} ",
state.settings.provider, state.settings.model
);
let left_line = Line::from(left_spans);
let right_line = Line::from(Span::styled(
@@ -72,7 +75,8 @@ impl Component for StatusBarComponent {
])
.split(area);
let block = Block::default().style(Style::default().bg(Theme::STATUS_BAR_BG).fg(Theme::TEXT));
let block =
Block::default().style(Style::default().bg(Theme::STATUS_BAR_BG).fg(Theme::TEXT));
let left_para = ratatui::widgets::Paragraph::new(left_line).block(block.clone());
frame.render_widget(left_para, chunks[0]);
+1 -5
View File
@@ -41,11 +41,7 @@ fn state_color(state: AgentState) -> Color {
/// and body (agent cards with icon/name/label/duration, or session stats
/// when no workflow is running).
#[instrument(skip_all)]
pub fn draw_workflow_panel(
frame: &mut Frame,
area: Rect,
state: &crate::state::AppStateRest,
) {
pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::state::AppStateRest) {
debug!("draw_workflow_panel — rendering workflow panel");
use ratatui::layout::{Constraint, Direction, Layout};