diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index 54ba0c243..68d34a6f5 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -1091,7 +1091,7 @@ impl Engine { } else if messages.is_empty() && system_prompt.is_none() { self.session.id = uuid::Uuid::new_v4().to_string(); } - self.session.messages = messages; + self.session.messages = messages.into(); self.session.compaction_summary_prompt = extract_compaction_summary_prompt(system_prompt.clone()); self.session.system_prompt = system_prompt; @@ -1182,7 +1182,7 @@ impl Engine { .tx_event .send(Event::SessionUpdated { session_id: self.session.id.clone(), - messages: self.session.messages.clone(), + messages: self.session.messages.clone().into(), system_prompt: self.session.system_prompt.clone(), model: self.session.model.clone(), workspace: self.session.workspace.clone(), @@ -1673,7 +1673,7 @@ impl Engine { Ok(result) => { if !result.messages.is_empty() || self.session.messages.is_empty() { let messages_after = result.messages.len(); - self.session.messages = result.messages; + self.session.messages = result.messages.into(); self.merge_compaction_summary(result.summary_prompt); self.emit_session_updated().await; let removed = messages_before.saturating_sub(messages_after); @@ -1769,7 +1769,7 @@ impl Engine { { Ok(result) => { let messages_after = result.messages.len(); - self.session.messages = result.messages; + self.session.messages = result.messages.into(); self.emit_session_updated().await; let summary = format!( @@ -1864,7 +1864,7 @@ impl Engine { { Ok(result) => { retries_used = result.retries_used; - compacted_messages = result.messages; + compacted_messages = result.messages.into(); summary_prompt = result.summary_prompt; } Err(err) => { @@ -1952,7 +1952,7 @@ impl Engine { self.session.model.clone(), self.session.workspace.clone(), self.session.system_prompt.clone(), - self.session.messages.clone(), + self.session.messages.clone().into(), )) .with_cancel_token(self.cancel_token.clone()) .with_trusted_external_paths(trusted_external_paths); @@ -2310,7 +2310,7 @@ impl Engine { ); // 5. Atomic swap. - self.session.messages = seed_messages; + self.session.messages = seed_messages.into(); self.session.cycle_count = to; self.session.current_cycle_started = now; self.session.cycle_briefings.push(briefing.clone()); diff --git a/crates/tui/src/core/engine/capacity_flow.rs b/crates/tui/src/core/engine/capacity_flow.rs index fe3577622..a17375f01 100644 --- a/crates/tui/src/core/engine/capacity_flow.rs +++ b/crates/tui/src/core/engine/capacity_flow.rs @@ -418,7 +418,7 @@ impl Engine { { Ok(result) => { if !result.messages.is_empty() || self.session.messages.is_empty() { - self.session.messages = result.messages; + self.session.messages = result.messages.into(); self.merge_compaction_summary(result.summary_prompt); refreshed = true; } diff --git a/crates/tui/src/core/engine/tests.rs b/crates/tui/src/core/engine/tests.rs index 48491277e..e631cb45e 100644 --- a/crates/tui/src/core/engine/tests.rs +++ b/crates/tui/src/core/engine/tests.rs @@ -2104,7 +2104,7 @@ fn messages_with_turn_metadata_preserves_stored_messages_for_prefix_cache() { let first_user = engine.user_text_message_with_turn_metadata("inspect src/lib.rs".to_string()); engine.session.add_message(first_user.clone()); let first_request = engine.messages_with_turn_metadata(); - assert_eq!(first_request, engine.session.messages); + assert_eq!(&first_request, &*engine.session.messages); engine.session.add_message(Message { role: "assistant".to_string(), @@ -2121,7 +2121,7 @@ fn messages_with_turn_metadata_preserves_stored_messages_for_prefix_cache() { engine.session.add_message(second_user); let second_request = engine.messages_with_turn_metadata(); - assert_eq!(second_request, engine.session.messages); + assert_eq!(&second_request, &*engine.session.messages); assert_eq!(second_request.first(), Some(&first_user)); } diff --git a/crates/tui/src/core/engine/turn_loop.rs b/crates/tui/src/core/engine/turn_loop.rs index 6b7ffd1ff..4b87b279f 100644 --- a/crates/tui/src/core/engine/turn_loop.rs +++ b/crates/tui/src/core/engine/turn_loop.rs @@ -148,7 +148,7 @@ impl Engine { // Only update if we got valid messages (never corrupt state) if !result.messages.is_empty() || self.session.messages.is_empty() { let auto_messages_after = result.messages.len(); - self.session.messages = result.messages; + self.session.messages = result.messages.into(); self.merge_compaction_summary(result.summary_prompt); self.emit_session_updated().await; let removed = auto_messages_before.saturating_sub(auto_messages_after); @@ -2219,7 +2219,7 @@ impl Engine { // appended. Do not rewrite historical messages at request time: doing // so makes the API prefix differ from the bytes sent in earlier turns // and destroys DeepSeek's KV prefix cache reuse. - self.session.messages.clone() + self.session.messages.clone().into_inner() } } diff --git a/crates/tui/src/core/session.rs b/crates/tui/src/core/session.rs index df90caffe..9361045b0 100644 --- a/crates/tui/src/core/session.rs +++ b/crates/tui/src/core/session.rs @@ -6,7 +6,7 @@ use crate::cycle_manager::CycleBriefing; use crate::models::{Message, SystemPrompt, Usage}; use crate::prefix_cache::PrefixStabilityManager; use crate::project_context::{ProjectContext, load_project_context_with_parents}; -use crate::prompt_zones::FrozenPrefix; +use crate::prompt_zones::{AppendLog, FrozenPrefix}; use crate::tui::approval::ApprovalMode; use crate::working_set::WorkingSet; use chrono::{DateTime, Utc}; @@ -42,8 +42,8 @@ pub struct Session { /// Persisted summary blocks generated by context compaction. pub compaction_summary_prompt: Option, - /// Conversation history (API format) - pub messages: Vec, + /// Conversation history (API format), backed by AppendLog (#2264). + pub messages: AppendLog, /// Total tokens used in this session pub total_usage: SessionUsage, @@ -152,7 +152,7 @@ impl Session { system_prompt: None, system_prompt_override: false, compaction_summary_prompt: None, - messages: Vec::new(), + messages: AppendLog::new(), total_usage: SessionUsage::default(), allow_shell, trust_mode, diff --git a/crates/tui/src/prompt_zones.rs b/crates/tui/src/prompt_zones.rs index 581ac355d..c7162541d 100644 --- a/crates/tui/src/prompt_zones.rs +++ b/crates/tui/src/prompt_zones.rs @@ -195,16 +195,15 @@ impl std::fmt::Display for PrefixDrift { // ── AppendLog ────────────────────────────────────────────────────────── -/// Append-only conversation history. Only exposes `push`-style mutations. +/// Append-only conversation history. Derefs to [`Vec`] for +/// transparent read access; mutations go through `push()` / `From`. /// -/// **Phase 1 scaffolding** — not yet wired into the engine request path. -#[allow(dead_code)] +/// Phase 4: backing store for `Session.messages` (#2264). #[derive(Debug, Clone)] pub struct AppendLog { messages: Vec, } -#[allow(dead_code)] impl AppendLog { pub fn new() -> Self { Self { @@ -216,33 +215,47 @@ impl AppendLog { Self { messages } } + /// Append a message to the log. pub fn push(&mut self, message: Message) { self.messages.push(message); } + /// Consume and return the inner `Vec`. #[must_use] - pub fn len(&self) -> usize { - self.messages.len() + pub fn into_inner(self) -> Vec { + self.messages } +} - #[must_use] - pub fn is_empty(&self) -> bool { - self.messages.is_empty() +impl Default for AppendLog { + fn default() -> Self { + Self::new() + } +} + +impl From> for AppendLog { + fn from(messages: Vec) -> Self { + Self { messages } } +} - pub fn iter(&self) -> impl Iterator { - self.messages.iter() +impl From for Vec { + fn from(log: AppendLog) -> Self { + log.messages } +} - #[must_use] - pub fn as_slice(&self) -> &[Message] { +impl std::ops::Deref for AppendLog { + type Target = Vec; + + fn deref(&self) -> &Self::Target { &self.messages } } -impl Default for AppendLog { - fn default() -> Self { - Self::new() +impl std::ops::DerefMut for AppendLog { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.messages } } @@ -525,7 +538,6 @@ mod tests { let msgs = vec![make_message("user", "a"), make_message("assistant", "b")]; let log = AppendLog::from_messages(msgs); assert_eq!(log.len(), 2); - assert_eq!(log.as_slice().len(), 2); } // ── TurnScratch ───────────────────────────────────────────────