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
3 changes: 2 additions & 1 deletion src/apps/cli/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1077,7 +1077,8 @@ async fn handle_relay_auth_error(
}
let mut current_context = account_context.write().await;
if current_context
.as_ref().is_none_or(|context| context.session.token != expected_token)
.as_ref()
.is_none_or(|context| context.session.token != expected_token)
{
tracing::debug!("Ignoring auth error cleanup for a replaced account");
return;
Expand Down
122 changes: 119 additions & 3 deletions src/apps/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,22 @@ struct Cli {
/// Automation, desktop, and remote modes remain unchanged.
#[arg(long, verbatim_doc_comment)]
shared: bool,

/// Continue the most recent session (skip startup page)
#[arg(long = "continue", conflicts_with = "session")]
continue_last: bool,

/// Open a specific session by ID (or "last" for the most recent)
#[arg(long, conflicts_with = "continue_last")]
session: Option<String>,

/// Specify the model ID for this session
#[arg(long)]
model: Option<String>,

/// Specify the agent type for this session
#[arg(long)]
agent: Option<String>,
}

fn shared_tui_requested(shared: bool, command: &Option<Commands>) -> Result<bool> {
Expand Down Expand Up @@ -877,6 +893,9 @@ async fn run_interactive(
default_agent: String,
_workspace_str: String,
shared: bool,
agent_override: Option<String>,
model_id: Option<String>,
session_override: Option<String>,
) -> Result<()> {
use ui::startup::{StartupPage, StartupResult};

Expand Down Expand Up @@ -987,14 +1006,48 @@ async fn run_interactive(
account_sync::start_settings_sync_loop();
}

// Resolve agent override: validate against the agent registry AFTER core services init
let effective_agent = if let Some(ref override_val) = agent_override {
match resolve_agent_override(override_val).await {
Ok(valid_id) => valid_id,
Err(warning) => {
eprintln!("{warning}");
default_agent.clone()
}
}
} else {
default_agent.clone()
};

// If --continue or --session was given, skip the startup page and go directly
// to chat with the resolved session.
if let Some(ref session_spec) = session_override {
let restore_session_id = resolve_startup_session_override(&agent, session_spec).await?;

let mut chat_mode = ChatMode::new(config, effective_agent, workspace, agent, compatibility)
.with_restore_session(restore_session_id);
if let Some(mid) = model_id {
chat_mode = chat_mode.with_model(mid);
}
let chat_result = chat_mode.run(Some(terminal));

if !shared {
shutdown_mcp_servers().await;
}
let _exit_reason = chat_result?;
println!("Goodbye!");
return Ok(());
}

// 4. Show startup page (with full command support)
let mut startup_page = StartupPage::new(
config,
Arc::clone(&agent),
compatibility.clone(),
default_agent,
effective_agent,
workspace.clone(),
);
startup_page.set_model_override(model_id.clone());
let startup_result = startup_page.run(&mut terminal)?;

if let StartupResult::Exit = startup_result {
Expand Down Expand Up @@ -1031,6 +1084,9 @@ async fn run_interactive(
if let Some(prompt) = initial_prompt {
chat_mode = chat_mode.with_initial_prompt(prompt);
}
if let Some(mid) = model_id {
chat_mode = chat_mode.with_model(mid);
}
let chat_result = chat_mode.run(Some(terminal));

// 6. Cleanup, including fatal event-stream exits.
Expand All @@ -1043,6 +1099,40 @@ async fn run_interactive(
Ok(())
}

/// Resolve a `--session` / `--continue` override to a concrete session ID.
/// "last" (or empty for --continue) resolves to the most recent session.
async fn resolve_startup_session_override(
agent: &Arc<TuiAgentClient>,
session_spec: &str,
) -> Result<String> {
if session_spec == "last" || session_spec.is_empty() {
let sessions = agent.list_sessions().await?;
return sessions
.first()
.map(|s| s.session_id.clone())
.ok_or_else(|| anyhow!("No history sessions for current project"));
}
bitfun_agent_runtime::session_control::validate_session_id(session_spec)
.map_err(anyhow::Error::msg)?;
Ok(session_spec.to_string())
}

/// Validate an agent override against the agent registry.
/// Returns the valid agent ID, or an error with a warning message.
async fn resolve_agent_override(agent_override: &str) -> std::result::Result<String, String> {
let registry = bitfun_core::agentic::get_agent_registry();
let modes = registry.get_modes_info().await;
if modes.iter().any(|m| m.id == agent_override) {
Ok(agent_override.to_string())
} else {
let available: Vec<&str> = modes.iter().map(|m| m.id.as_str()).collect();
Err(format!(
"Warning: Agent '{agent_override}' not found. Available: {}. Using default.",
available.join(", ")
))
}
}

// ======================== Main ========================

#[derive(Debug)]
Expand Down Expand Up @@ -1149,7 +1239,16 @@ async fn run_cli() -> Result<()> {
match cli.command {
Some(Commands::Chat { agent, .. }) => {
// Interactive mode with startup page, scoped to the current directory.
run_interactive(config, agent, ".".to_string(), use_shared_runtime).await?;
run_interactive(
config,
agent,
".".to_string(),
use_shared_runtime,
cli.agent.clone(),
cli.model.clone(),
None,
)
.await?;
}

Some(Commands::SharedRuntime {
Expand Down Expand Up @@ -1410,7 +1509,24 @@ async fn run_cli() -> Result<()> {
let workspace_str = ".".to_string();

let default_agent = config.behavior.default_agent.clone();
run_interactive(config, default_agent, workspace_str, use_shared_runtime).await?;

// Resolve --continue / --session into a session override spec.
let session_override = if cli.continue_last {
Some("last".to_string())
} else {
cli.session.clone()
};

run_interactive(
config,
default_agent,
workspace_str,
use_shared_runtime,
cli.agent.clone(),
cli.model.clone(),
session_override,
)
.await?;
}
}

Expand Down
9 changes: 9 additions & 0 deletions src/apps/cli/src/modes/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,8 @@ pub(crate) struct ChatMode {
restore_session_id: Option<String>,
/// If set, send this prompt automatically when the session starts
initial_prompt: Option<crate::ui::composer::ComposerDraft>,
/// If set, override the session model after create/restore
model_id: Option<String>,
/// Pending MCP operation — set in key handler, executed after one render frame
pending_mcp_op: Option<PendingMcpOp>,
/// Running MCP tasks (non-blocking, polled in main loop)
Expand Down Expand Up @@ -614,6 +616,7 @@ impl ChatMode {
auto_approve_ask_override: None,
restore_session_id: None,
initial_prompt: None,
model_id: None,
pending_mcp_op: None,
pending_mcp_tasks: Vec::new(),
pending_session_operation: None,
Expand Down Expand Up @@ -666,6 +669,12 @@ impl ChatMode {
self
}

/// Set a model ID to override the session model after create/restore
pub(crate) fn with_model(mut self, model_id: String) -> Self {
self.model_id = Some(model_id);
self
}

fn action_state(&self, is_processing: bool, popup_open: bool) -> ActionState {
ActionState::chat(is_processing, popup_open)
.with_shared_tui(self.agent.is_shared())
Expand Down
16 changes: 16 additions & 0 deletions src/apps/cli/src/modes/chat/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,22 @@ impl ChatMode {
self.workspace = chat_state.workspace.clone();
self.refresh_workspace_git_status(&mut chat_state, &rt_handle);

// Apply model override (--model flag): update the session model.
// The backend validates the ID; an invalid ID logs a warning and
// falls back to the default model.
if let Some(ref model_override) = self.model_id {
let trimmed = model_override.trim();
let sid = chat_state.core_session_id.clone();
let mid = trimmed.to_string();
let agent = self.agent.clone();
if let Err(e) = tokio::task::block_in_place(|| {
rt_handle.block_on(async { agent.update_session_model(&sid, &mid).await })
}) {
tracing::warn!("Failed to apply model override '{mid}': {e}");
eprintln!("Warning: Model '{mid}' not found. Using default model.");
}
}

let mut external_source_rx = None;
if self.agent.is_shared() {
chat_view.set_status(Some(format!(
Expand Down
10 changes: 10 additions & 0 deletions src/apps/cli/src/ui/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,16 @@ impl StartupPage {
&self.agent_type
}

/// Set a model ID override (from `--model` flag) for display and session
/// composition. The ID is validated when applied to the session; an invalid
/// ID logs a warning and falls back to the default model.
pub(crate) fn set_model_override(&mut self, model_id: Option<String>) {
if model_id.is_some() {
self.selected_model_id = model_id;
}
self.load_current_model_name();
}

/// Return the model explicitly selected for the new Session, if any.
pub(crate) fn selected_model_id(&self) -> Option<&str> {
self.selected_model_id.as_deref()
Expand Down