Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 36 additions & 32 deletions src/acp_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ impl crate::app::App {
self.chat.recent_prompt_text = None;
self.chat.streaming_content.clear();
self.chat.streaming_content_message_id = None;
self.streaming_cache.invalidate();
self.render.invalidate_content_cache();
self.set_status(LogLevel::Info, "session", "undone - reloading session");
if let Some(ref sid) = self.sessions.session_id {
return Command::load_session_commands(
Expand Down Expand Up @@ -539,7 +539,7 @@ impl crate::app::App {
AcpAppEvent::PromptFailed { local_id, message } => {
self.chat.end_llm_request_span(None);
self.chat.rollback_pending_prompt(&local_id);
self.card_cache.invalidate();
self.render.invalidate_card_cache();
self.push_acp_error(&message);
self.set_status(LogLevel::Error, "acp", format!("error: {message}"));
vec![]
Expand Down Expand Up @@ -679,8 +679,10 @@ impl crate::app::App {

fn reset_active_session_view(&mut self) {
self.chat.reset_for_session_switch();
self.invalidate_streaming_caches();
self.card_cache.invalidate();
self.render.invalidate_content_cache();
self.chat.clear_streaming_thinking();
self.render.invalidate_thinking_cache();
self.render.invalidate_card_cache();
if self.delegates.parent_session_id.is_none() {
self.delegates.clear_for_root_session();
}
Expand Down Expand Up @@ -709,7 +711,7 @@ impl crate::app::App {
.delegates
.upsert_provisional_delegate(tool_call_id, target_agent_id, objective)
{
self.invalidate_delegate_render_cache();
self.render.invalidate_card_cache();
}
}

Expand Down Expand Up @@ -741,7 +743,7 @@ impl crate::app::App {
error: update.error,
})
{
self.invalidate_delegate_render_cache();
self.render.invalidate_card_cache();
}
}

Expand Down Expand Up @@ -815,7 +817,7 @@ impl crate::app::App {
.delegates
.apply_child_snapshot(session_id, state, stats)
{
self.invalidate_delegate_render_cache();
self.render.invalidate_card_cache();
}
}

Expand All @@ -835,8 +837,8 @@ impl crate::app::App {
match update {
AcpSessionUpdate::TurnStarted => {
self.chat.begin_turn(is_replay);
self.streaming_cache.invalidate();
self.streaming_thinking_cache.invalidate();
self.render.invalidate_content_cache();
self.render.invalidate_thinking_cache();
self.set_status(LogLevel::Debug, "activity", "thinking...");
}
AcpSessionUpdate::UserMessage {
Expand All @@ -849,9 +851,9 @@ impl crate::app::App {
} => {
let transition = self.chat.append_streaming_content(&content, message_id);
if transition.finalized_previous {
self.streaming_cache.invalidate();
self.streaming_thinking_cache.invalidate();
self.card_cache.invalidate();
self.render.invalidate_content_cache();
self.render.invalidate_thinking_cache();
self.render.invalidate_card_cache();
}
}
AcpSessionUpdate::AssistantThinkingDelta {
Expand All @@ -865,9 +867,9 @@ impl crate::app::App {
return;
}
if transition.finalized_previous {
self.streaming_cache.invalidate();
self.streaming_thinking_cache.invalidate();
self.card_cache.invalidate();
self.render.invalidate_content_cache();
self.render.invalidate_thinking_cache();
self.render.invalidate_card_cache();
}
}
AcpSessionUpdate::AssistantMessage {
Expand Down Expand Up @@ -899,13 +901,13 @@ impl crate::app::App {
detail.clone(),
) {
self.chat.clear_streaming_thinking();
self.streaming_thinking_cache.invalidate();
self.card_cache.invalidate();
self.render.invalidate_thinking_cache();
self.render.invalidate_card_cache();
return;
}
self.chat.record_tool_call();
if self.chat.push_streaming_thinking_entry() {
self.streaming_thinking_cache.invalidate();
self.render.invalidate_thinking_cache();
}
self.chat.push_tool_call(tool_call_id, name, false, detail);
}
Expand Down Expand Up @@ -941,7 +943,7 @@ impl crate::app::App {
}
}
if updated {
self.card_cache.invalidate();
self.render.invalidate_card_cache();
}
}
AcpSessionUpdate::UsageUpdate {
Expand Down Expand Up @@ -993,14 +995,14 @@ impl crate::app::App {
}
AcpSessionUpdate::Cancelled => {
self.chat.cancel_turn(is_replay);
self.streaming_cache.invalidate();
self.streaming_thinking_cache.invalidate();
self.render.invalidate_content_cache();
self.render.invalidate_thinking_cache();
self.set_status(LogLevel::Warn, "activity", "cancelled");
}
AcpSessionUpdate::Finished { finish_reason } => {
self.chat.finish_turn(is_replay);
self.streaming_cache.invalidate();
self.streaming_thinking_cache.invalidate();
self.render.invalidate_content_cache();
self.render.invalidate_thinking_cache();
self.set_status(
LogLevel::Debug,
"activity",
Expand All @@ -1022,14 +1024,16 @@ impl crate::app::App {
transition,
crate::chat_state::UserMessageTransition::Reconciled
) {
self.card_cache.invalidate();
self.render.invalidate_card_cache();
}
}

fn finalize_streaming_segment(&mut self) {
if self.chat.finalize_streaming_segment() {
self.invalidate_streaming_caches();
self.card_cache.invalidate();
self.render.invalidate_content_cache();
self.chat.clear_streaming_thinking();
self.render.invalidate_thinking_cache();
self.render.invalidate_card_cache();
}
}

Expand All @@ -1043,13 +1047,13 @@ impl crate::app::App {
thinking: Option<String>,
message_id: Option<String>,
) {
self.streaming_cache.invalidate();
self.render.invalidate_content_cache();
let replaced = self
.chat
.push_assistant_message(content, thinking, message_id);
self.streaming_thinking_cache.invalidate();
self.render.invalidate_thinking_cache();
if replaced {
self.card_cache.invalidate();
self.render.invalidate_card_cache();
}
}

Expand Down Expand Up @@ -1090,9 +1094,9 @@ impl crate::app::App {
return;
}
if transition.finalized_streaming {
self.streaming_cache.invalidate();
self.streaming_thinking_cache.invalidate();
self.card_cache.invalidate();
self.render.invalidate_content_cache();
self.render.invalidate_thinking_cache();
self.render.invalidate_card_cache();
}
if supported {
self.set_status(
Expand Down
96 changes: 6 additions & 90 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,54 +8,13 @@ use crate::connection_state::{ConnState, ConnectionState};
use crate::delegates_state::DelegatesState;
use crate::diagnostics::{AppLogEntry, DiagnosticsState, LogLevel};
use crate::domain::activity::{DelegateChildState, DelegateStats};
use crate::highlight::Highlighter;
use crate::markdown::CardBlock;
use crate::mesh_state::MeshState;
use crate::models_state::ModelsState;
use crate::navigation_state::{NavigationState, Popup};
use crate::profiles_state::ProfilesState;
use crate::protocol::audit::EventKind;
use crate::render_state::RenderState;
use crate::session_state::SessionsState;
use crate::ui::CardCache;

/// Cache for rendered streaming markdown to avoid re-parsing every frame.
/// Invalidated when `streaming_content` grows or is cleared.
pub struct StreamingCache {
/// Length of `streaming_content` at the time of last render.
rendered_len: usize,
/// Cached rendered blocks (without the spinner).
blocks: Vec<CardBlock>,
}

impl StreamingCache {
pub fn new() -> Self {
Self {
rendered_len: 0,
blocks: Vec::new(),
}
}

/// Returns cached blocks if content length hasn't changed, otherwise None.
pub fn get(&self, content_len: usize) -> Option<&[CardBlock]> {
if content_len > 0 && content_len == self.rendered_len {
Some(&self.blocks)
} else {
None
}
}

/// Store freshly rendered blocks and the content length they correspond to.
pub fn store(&mut self, content_len: usize, blocks: Vec<CardBlock>) {
self.rendered_len = content_len;
self.blocks = blocks;
}

/// Reset the cache (call when streaming_content is cleared).
pub fn invalidate(&mut self) {
self.rendered_len = 0;
self.blocks.clear();
}
}

// ── Delegation tracking ───────────────────────────────────────────────────────

Expand Down Expand Up @@ -177,12 +136,6 @@ pub struct App {
// chat
pub(crate) chat: ChatState,
pub(crate) composer: ComposerState,
/// Total content height (in rows) from the last render frame.
/// Used to compensate chat scroll when content grows while the user
/// is scrolled up, so the viewport stays at the same absolute position.
pub prev_total_height: u16,
pub streaming_cache: StreamingCache,
pub streaming_thinking_cache: StreamingCache,

// profile info
pub(crate) profiles: ProfilesState,
Expand All @@ -199,16 +152,12 @@ pub struct App {
// connection and server lifecycle
pub(crate) connection: ConnectionState,

// syntax highlighting
pub hl: Highlighter,

// card cache for incremental rendering
pub(crate) card_cache: CardCache,
// temporary render-local composition
pub(crate) render: RenderState,

// auth popup state
pub(crate) auth: AuthState,

pub tick: u64,
pub should_quit: bool,
}

Expand Down Expand Up @@ -246,18 +195,13 @@ impl App {
delegates: DelegatesState::new(),
chat: ChatState::new(),
composer: ComposerState::new(),
prev_total_height: 0,
streaming_cache: StreamingCache::new(),
streaming_thinking_cache: StreamingCache::new(),
profiles: ProfilesState::new(),
models: ModelsState::new(),
diagnostics: DiagnosticsState::new(),
mesh: MeshState::new(),
connection: ConnectionState::new(),
hl: Highlighter::new(),
card_cache: CardCache::new(),
render: RenderState::new(),
auth: AuthState::new(),
tick: 0,
should_quit: false,
}
}
Expand All @@ -272,21 +216,6 @@ impl App {
std::mem::take(&mut self.composer.input)
}

/// Invalidate both streaming caches and clear the thinking buffer.
///
/// Call this when a streaming turn ends (assistant message finalized,
/// new turn starts, session reloaded, etc.) so stale markdown renders
/// are discarded.
pub fn invalidate_streaming_caches(&mut self) {
self.streaming_cache.invalidate();
self.chat.clear_streaming_thinking();
self.streaming_thinking_cache.invalidate();
}

pub fn invalidate_delegate_render_cache(&mut self) {
self.card_cache.invalidate();
}

/// Cycle through `[auto, low, medium, high, max]` (wraps around).
/// Updates the nested reasoning state optimistically and
/// returns the [`Command`] to forward to the server.
Expand Down Expand Up @@ -369,27 +298,14 @@ impl App {
self.set_status(LogLevel::Warn, "input", "press Esc again to stop");
}

/// Adjust `scroll_offset` to compensate for content growth so the
/// viewport stays at the same absolute position when the user is
/// scrolled up. No-op when `scroll_offset == 0` (auto-following).
///
/// Call from the renderer after computing the new `total_height`.
pub fn compensate_scroll_for_growth(&mut self, total_height: u16) {
let growth = total_height.saturating_sub(self.prev_total_height);
if self.chat.scroll_offset > 0 && growth > 0 {
self.chat.scroll_offset = self.chat.scroll_offset.saturating_add(growth);
}
self.prev_total_height = total_height;
}

pub fn open_fork_turn_popup(&mut self) {
self.navigation.popup = Popup::ForkTurnSelect;
self.chat.reset_fork_selector();
}

pub fn push_pending_prompt(&mut self, text: String) -> String {
let local_id = self.chat.push_pending_prompt(text);
self.card_cache.invalidate();
self.render.invalidate_card_cache();
local_id
}

Expand Down Expand Up @@ -473,7 +389,7 @@ impl App {
/// Mark the pending elicitation chat card with an outcome and clear the active state.
pub fn resolve_elicitation(&mut self, elicitation_id: &str, outcome: &str) {
self.chat.resolve_elicitation(elicitation_id, outcome);
self.card_cache.invalidate();
self.render.invalidate_card_cache();
self.refresh_transient_status();
}

Expand Down
12 changes: 2 additions & 10 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1485,14 +1485,6 @@ pub(crate) fn handle_new_session_popup_key(
Ok(())
}

/// Invalidate every theme-dependent cache in `app` so that the next render
/// frame rebuilds styled lines with the current palette.
pub(crate) fn invalidate_theme_caches(app: &mut App) {
app.card_cache.invalidate();
app.streaming_cache.invalidate();
app.streaming_thinking_cache.invalidate();
}

pub(crate) fn handle_theme_popup_key(app: &mut App, key: KeyEvent) -> anyhow::Result<()> {
match key.code {
KeyCode::Esc => {
Expand All @@ -1513,7 +1505,7 @@ pub(crate) fn handle_theme_popup_key(app: &mut App, key: KeyEvent) -> anyhow::Re
{
theme::Theme::set_by_index(idx);
theme::Theme::begin_frame();
invalidate_theme_caches(app);
app.render.invalidate_theme_caches();
app.navigation.popup = Popup::None;
save_config(app);
}
Expand Down Expand Up @@ -1601,7 +1593,7 @@ pub(crate) fn handle_chat_key(
local_id: local_id.clone(),
}) {
app.chat.rollback_pending_prompt(&local_id);
app.card_cache.invalidate();
app.render.invalidate_card_cache();
return Err(error.into());
}
}
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod models_state;
mod navigation_state;
mod profiles_state;
mod protocol;
mod render_state;
pub mod runtime;
mod server_manager;
mod session;
Expand Down
Loading