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
77 changes: 77 additions & 0 deletions crates/cli/src/compaction_exec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use std::sync::Arc;

use anyhow::{Result, anyhow};
use bb_core::types::{CompactionSettings, EntryBase, EntryId, SessionEntry};
use bb_provider::Provider;
use bb_session::store::EntryRow;
use chrono::Utc;
use tokio_util::sync::CancellationToken;

#[derive(Debug, Clone)]
pub(crate) struct ExecutedCompaction {
pub tokens_before: u64,
pub summarized_count: usize,
pub kept_count: usize,
}

pub(crate) async fn execute_session_compaction(
entries: Vec<EntryRow>,
parent_id: Option<EntryId>,
db_path: std::path::PathBuf,
session_id: &str,
provider: Arc<dyn Provider>,
model_id: &str,
api_key: &str,
base_url: &str,
headers: &std::collections::HashMap<String, String>,
settings: &CompactionSettings,
custom_instructions: Option<&str>,
cancel: CancellationToken,
) -> Result<ExecutedCompaction> {
let prep = bb_session::compaction::prepare_compaction(&entries, settings)
.ok_or_else(|| anyhow!("Nothing to compact"))?;

let summarized_count = prep.messages_to_summarize.len();
let kept_count = prep.kept_messages.len();

let result = bb_session::compaction::compact(
&prep,
provider.as_ref(),
model_id,
api_key,
base_url,
headers,
custom_instructions,
cancel,
)
.await?;

let details = serde_json::json!({
"summarizedCount": summarized_count,
"keptCount": kept_count,
"readFiles": result.read_files,
"modifiedFiles": result.modified_files,
});

let compaction_entry = SessionEntry::Compaction {
base: EntryBase {
id: EntryId::generate(),
parent_id,
timestamp: Utc::now(),
},
summary: result.summary.clone(),
first_kept_entry_id: EntryId(result.first_kept_entry_id.clone()),
tokens_before: result.tokens_before,
details: Some(details),
from_plugin: false,
};

let append_conn = bb_session::store::open_db(&db_path)?;
bb_session::store::append_entry(&append_conn, session_id, &compaction_entry)?;

Ok(ExecutedCompaction {
tokens_before: result.tokens_before,
summarized_count,
kept_count,
})
}
36 changes: 34 additions & 2 deletions crates/cli/src/fullscreen/controller.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::{
collections::{HashSet, VecDeque},
collections::{HashMap, HashSet, VecDeque},
time::SystemTime,
};

Expand All @@ -8,6 +8,7 @@ use bb_tools::{ToolApprovalOutcome, ToolApprovalRequest};
use tokio::sync::{mpsc, oneshot};
use tokio_util::sync::CancellationToken;

use crate::compaction_exec::ExecutedCompaction;
use crate::session_bootstrap::{SessionRuntimeSetup, SessionUiOptions};

mod loop_impl;
Expand Down Expand Up @@ -122,6 +123,11 @@ fn contains_shell_meta(line: &str) -> bool {
|| line.contains('`')
}

pub(super) enum QueuedPrompt {
Visible(String),
Hidden(String),
}

pub(super) struct FullscreenController {
pub(super) runtime_host: AgentSessionRuntimeHost,
pub(super) session_setup: SessionRuntimeSetup,
Expand All @@ -130,18 +136,29 @@ pub(super) struct FullscreenController {
pub(super) abort_token: CancellationToken,
pub(super) streaming: bool,
pub(super) retry_status: Option<String>,
pub(super) queued_prompts: VecDeque<String>,
pub(super) queued_prompts: VecDeque<QueuedPrompt>,
pub(super) pending_tree_summary_target: Option<String>,
pub(super) pending_tree_custom_prompt_target: Option<String>,
pub(super) pending_login_api_key_provider: Option<String>,
pub(super) pending_login_copilot_enterprise: bool,
pub(super) pending_images: Vec<PendingImage>,
pub(super) local_action_cancel: Option<CancellationToken>,
pub(super) manual_compaction_in_progress: bool,
pub(super) auto_compaction_in_progress: bool,
pub(super) manual_compaction_generation: u64,
pub(super) manual_compaction_tx: mpsc::UnboundedSender<ManualCompactionEvent>,
pub(super) manual_compaction_rx: mpsc::UnboundedReceiver<ManualCompactionEvent>,
pub(super) color_theme: bb_tui::fullscreen::spinner::ColorTheme,
pub(super) shutdown_requested: bool,
pub(super) approval_rx: mpsc::UnboundedReceiver<PendingApprovalRequest>,
pub(super) pending_approval: Option<PendingApprovalRequest>,
pub(super) session_approval_rules: HashSet<SessionApprovalRule>,
/// Menu IDs for `OpenSelectMenu` requests that came from an extension
/// command → originating command name. Used so that when the user picks
/// a value we can re-invoke `/<command> <value>`.
pub(super) pending_extension_menus: HashMap<String, String>,
/// Active auth-style input dialog owned by an extension command.
pub(super) pending_extension_prompt: Option<crate::extensions::ExtensionPromptSpec>,
resource_watch: ResourceWatchState,
suppress_next_resource_watch_reload: bool,
}
Expand All @@ -151,6 +168,13 @@ pub(super) struct PendingApprovalRequest {
pub response_tx: oneshot::Sender<ToolApprovalOutcome>,
}

pub(super) enum ManualCompactionEvent {
Finished {
generation: u64,
result: anyhow::Result<ExecutedCompaction>,
},
}

impl FullscreenController {
pub(super) fn new(
runtime_host: AgentSessionRuntimeHost,
Expand All @@ -160,6 +184,7 @@ impl FullscreenController {
approval_rx: mpsc::UnboundedReceiver<PendingApprovalRequest>,
) -> Self {
let resource_watch = ResourceWatchState::capture(&session_setup.tool_ctx.cwd);
let (manual_compaction_tx, manual_compaction_rx) = mpsc::unbounded_channel();
Self {
runtime_host,
session_setup,
Expand All @@ -175,11 +200,18 @@ impl FullscreenController {
pending_login_copilot_enterprise: false,
pending_images: Vec::new(),
local_action_cancel: None,
manual_compaction_in_progress: false,
auto_compaction_in_progress: false,
manual_compaction_generation: 0,
manual_compaction_tx,
manual_compaction_rx,
color_theme: bb_tui::fullscreen::spinner::ColorTheme::default(),
shutdown_requested: false,
approval_rx,
pending_approval: None,
session_approval_rules: HashSet::new(),
pending_extension_menus: HashMap::new(),
pending_extension_prompt: None,
resource_watch,
suppress_next_resource_watch_reload: false,
}
Expand Down
88 changes: 86 additions & 2 deletions crates/cli/src/fullscreen/controller/loop_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use bb_tui::fullscreen::{
};
use tokio::sync::mpsc;

use super::{FullscreenController, SessionApprovalRule, derive_session_approval_rule};
use super::{
FullscreenController, QueuedPrompt, SessionApprovalRule, derive_session_approval_rule,
};

impl FullscreenController {
pub(crate) async fn run(
Expand Down Expand Up @@ -64,6 +66,14 @@ impl FullscreenController {
};
self.present_approval_request(approval);
}
maybe_compaction = self.manual_compaction_rx.recv() => {
let Some(event) = maybe_compaction else {
continue;
};
if let Err(err) = self.handle_manual_compaction_event(event, &mut submission_rx).await {
self.report_error("manual compaction", &err);
}
}
_ = resource_watch_tick.tick() => {
if let Err(err) = self.maybe_auto_reload_resources().await {
self.report_error("auto reload", &err);
Expand Down Expand Up @@ -117,6 +127,17 @@ impl FullscreenController {
{
self.report_error("menu selection", &err);
}
// A menu pick may have queued a prompt for the agent (e.g.
// Shape's "Build agent" menu item dispatches a new turn).
// Drain the queue so it actually runs.
if !self.streaming
&& !self.manual_compaction_in_progress
&& !self.queued_prompts.is_empty()
{
if let Err(err) = self.drain_queued_prompts(submission_rx).await {
self.report_error("drain queued", &err);
}
}
Ok(())
}
FullscreenSubmission::ApprovalDecision { .. } => Ok(()),
Expand All @@ -142,6 +163,14 @@ impl FullscreenController {
self.send_command(FullscreenCommand::SetStatusLine(
"Authentication cancelled".to_string(),
));
} else if let Some(prompt) = self.pending_extension_prompt.take() {
self.send_command(FullscreenCommand::SetLocalActionActive(false));
self.send_command(FullscreenCommand::CloseAuthDialog);
self.send_command(FullscreenCommand::SetInput(String::new()));
self.send_command(FullscreenCommand::SetStatusLine(format!(
"Cancelled {}",
prompt.title
)));
} else if let Some(cancel) = self.local_action_cancel.take() {
cancel.cancel();
} else {
Expand All @@ -151,6 +180,25 @@ impl FullscreenController {
}
Ok(())
}
FullscreenSubmission::EditQueuedMessages => {
if self.queued_prompts.is_empty() {
self.send_command(FullscreenCommand::SetStatusLine(
"No queued messages to edit".to_string(),
));
} else {
let queued = self
.queued_prompts
.drain(..)
.map(|queued| match queued {
QueuedPrompt::Visible(text) | QueuedPrompt::Hidden(text) => text,
})
.collect::<Vec<_>>()
.join("\n\n");
self.send_command(FullscreenCommand::SetInput(queued));
self.publish_status();
}
Ok(())
}
}
}

Expand Down Expand Up @@ -232,12 +280,47 @@ impl FullscreenController {
return Ok(());
}

if let Some(prompt) = self.pending_extension_prompt.clone() {
let submitted = text.trim().to_string();
if submitted.is_empty() || submitted == "/" {
self.pending_extension_prompt = None;
self.send_command(FullscreenCommand::SetInput(String::new()));
self.send_command(FullscreenCommand::SetLocalActionActive(false));
self.send_command(FullscreenCommand::CloseAuthDialog);
self.send_command(FullscreenCommand::SetStatusLine(format!(
"Cancelled {}",
prompt.title
)));
return Ok(());
}
self.pending_extension_prompt = None;
self.send_command(FullscreenCommand::SetInput(String::new()));
let invocation = format!(
"/{} __resume {} -- {}",
prompt.command, prompt.resume, submitted
);
self.execute_extension_command_text(&invocation).await?;
return Ok(());
}

let text = text.trim().to_string();
if (text.is_empty() && self.pending_images.is_empty()) || text == "/" {
return Ok(());
}

if self.manual_compaction_in_progress {
self.queued_prompts.push_back(QueuedPrompt::Visible(text));
self.publish_status();
return Ok(());
}

if self.handle_local_submission(&text).await? {
if !self.streaming
&& !self.manual_compaction_in_progress
&& !self.queued_prompts.is_empty()
{
self.drain_queued_prompts(submission_rx).await?;
}
return Ok(());
}

Expand All @@ -260,7 +343,8 @@ impl FullscreenController {
let prompt_text = expanded.text;

if self.streaming {
self.queued_prompts.push_back(prompt_text);
self.queued_prompts
.push_back(QueuedPrompt::Visible(prompt_text));
self.publish_status();
return Ok(());
}
Expand Down
Loading