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:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
))]
|
||||
};
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user