diff --git a/src/apps/cli/src/account.rs b/src/apps/cli/src/account.rs index b70e68e69..b1b7dc185 100644 --- a/src/apps/cli/src/account.rs +++ b/src/apps/cli/src/account.rs @@ -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; diff --git a/src/apps/cli/src/main.rs b/src/apps/cli/src/main.rs index 1b403aa63..4d31fce77 100644 --- a/src/apps/cli/src/main.rs +++ b/src/apps/cli/src/main.rs @@ -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, + + /// Specify the model ID for this session + #[arg(long)] + model: Option, + + /// Specify the agent type for this session + #[arg(long)] + agent: Option, } fn shared_tui_requested(shared: bool, command: &Option) -> Result { @@ -877,6 +893,9 @@ async fn run_interactive( default_agent: String, _workspace_str: String, shared: bool, + agent_override: Option, + model_id: Option, + session_override: Option, ) -> Result<()> { use ui::startup::{StartupPage, StartupResult}; @@ -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 { @@ -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. @@ -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, + session_spec: &str, +) -> Result { + 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 { + 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)] @@ -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 { @@ -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?; } } diff --git a/src/apps/cli/src/modes/chat.rs b/src/apps/cli/src/modes/chat.rs index 4fb56eaa9..eb768d53a 100644 --- a/src/apps/cli/src/modes/chat.rs +++ b/src/apps/cli/src/modes/chat.rs @@ -522,6 +522,8 @@ pub(crate) struct ChatMode { restore_session_id: Option, /// If set, send this prompt automatically when the session starts initial_prompt: Option, + /// If set, override the session model after create/restore + model_id: Option, /// Pending MCP operation — set in key handler, executed after one render frame pending_mcp_op: Option, /// Running MCP tasks (non-blocking, polled in main loop) @@ -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, @@ -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()) diff --git a/src/apps/cli/src/modes/chat/run.rs b/src/apps/cli/src/modes/chat/run.rs index bc1ac3847..1fc785cdd 100644 --- a/src/apps/cli/src/modes/chat/run.rs +++ b/src/apps/cli/src/modes/chat/run.rs @@ -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!( diff --git a/src/apps/cli/src/ui/startup.rs b/src/apps/cli/src/ui/startup.rs index 105d93c79..df87546a2 100644 --- a/src/apps/cli/src/ui/startup.rs +++ b/src/apps/cli/src/ui/startup.rs @@ -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) { + 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()