fix: show agent mode list or fallback label in gigacode

This commit is contained in:
Nathan Flurry 2026-02-06 23:21:52 -08:00
parent 07a05fe1a7
commit 0f325c87dc
4 changed files with 132 additions and 45 deletions

View file

@ -1020,12 +1020,28 @@ fn extract_result_text(agent: AgentId, events: &[Value]) -> Option<String> {
} }
} }
fn amp_mode_from_agent_mode(agent_mode: Option<&str>) -> Option<String> {
let mode = agent_mode
.map(|value| value.trim())
.filter(|value| !value.is_empty())?;
let normalized = match mode {
"build" | "default" => "smart",
other => other,
};
Some(normalized.to_string())
}
fn spawn_amp( fn spawn_amp(
path: &Path, path: &Path,
working_dir: &Path, working_dir: &Path,
options: &SpawnOptions, options: &SpawnOptions,
) -> Result<std::process::Output, AgentError> { ) -> Result<std::process::Output, AgentError> {
let flags = detect_amp_flags(path, working_dir).unwrap_or_default(); let flags = detect_amp_flags(path, working_dir).unwrap_or_default();
let mode = if flags.mode {
amp_mode_from_agent_mode(options.agent_mode.as_deref())
} else {
None
};
let mut args: Vec<&str> = Vec::new(); let mut args: Vec<&str> = Vec::new();
if flags.execute { if flags.execute {
args.push("--execute"); args.push("--execute");
@ -1042,8 +1058,8 @@ fn spawn_amp(
let mut command = Command::new(path); let mut command = Command::new(path);
command.current_dir(working_dir); command.current_dir(working_dir);
if let Some(model) = options.model.as_deref() { if let Some(mode) = mode.as_deref() {
command.arg("--model").arg(model); command.arg("--mode").arg(mode);
} }
if let Some(session_id) = options.session_id.as_deref() { if let Some(session_id) = options.session_id.as_deref() {
command.arg("--continue").arg(session_id); command.arg("--continue").arg(session_id);
@ -1070,10 +1086,15 @@ fn spawn_amp(
fn build_amp_command(path: &Path, working_dir: &Path, options: &SpawnOptions) -> Command { fn build_amp_command(path: &Path, working_dir: &Path, options: &SpawnOptions) -> Command {
let flags = detect_amp_flags(path, working_dir).unwrap_or_default(); let flags = detect_amp_flags(path, working_dir).unwrap_or_default();
let mode = if flags.mode {
amp_mode_from_agent_mode(options.agent_mode.as_deref())
} else {
None
};
let mut command = Command::new(path); let mut command = Command::new(path);
command.current_dir(working_dir); command.current_dir(working_dir);
if let Some(model) = options.model.as_deref() { if let Some(mode) = mode.as_deref() {
command.arg("--model").arg(model); command.arg("--mode").arg(mode);
} }
if let Some(session_id) = options.session_id.as_deref() { if let Some(session_id) = options.session_id.as_deref() {
command.arg("--continue").arg(session_id); command.arg("--continue").arg(session_id);
@ -1102,6 +1123,7 @@ struct AmpFlags {
print: bool, print: bool,
output_format: bool, output_format: bool,
dangerously_skip_permissions: bool, dangerously_skip_permissions: bool,
mode: bool,
} }
fn detect_amp_flags(path: &Path, working_dir: &Path) -> Option<AmpFlags> { fn detect_amp_flags(path: &Path, working_dir: &Path) -> Option<AmpFlags> {
@ -1120,6 +1142,7 @@ fn detect_amp_flags(path: &Path, working_dir: &Path) -> Option<AmpFlags> {
print: text.contains("--print"), print: text.contains("--print"),
output_format: text.contains("--output-format"), output_format: text.contains("--output-format"),
dangerously_skip_permissions: text.contains("--dangerously-skip-permissions"), dangerously_skip_permissions: text.contains("--dangerously-skip-permissions"),
mode: text.contains("--mode"),
}) })
} }
@ -1128,6 +1151,12 @@ fn spawn_amp_fallback(
working_dir: &Path, working_dir: &Path,
options: &SpawnOptions, options: &SpawnOptions,
) -> Result<std::process::Output, AgentError> { ) -> Result<std::process::Output, AgentError> {
let flags = detect_amp_flags(path, working_dir).unwrap_or_default();
let mode = if flags.mode {
amp_mode_from_agent_mode(options.agent_mode.as_deref())
} else {
None
};
let mut attempts = vec![ let mut attempts = vec![
vec!["--execute"], vec!["--execute"],
vec!["--print", "--output-format", "stream-json"], vec!["--print", "--output-format", "stream-json"],
@ -1142,8 +1171,8 @@ fn spawn_amp_fallback(
for args in attempts { for args in attempts {
let mut command = Command::new(path); let mut command = Command::new(path);
command.current_dir(working_dir); command.current_dir(working_dir);
if let Some(model) = options.model.as_deref() { if let Some(mode) = mode.as_deref() {
command.arg("--model").arg(model); command.arg("--mode").arg(mode);
} }
if let Some(session_id) = options.session_id.as_deref() { if let Some(session_id) = options.session_id.as_deref() {
command.arg("--continue").arg(session_id); command.arg("--continue").arg(session_id);

View file

@ -27,7 +27,8 @@ use tracing::{info, warn};
use utoipa::{IntoParams, OpenApi, ToSchema}; use utoipa::{IntoParams, OpenApi, ToSchema};
use crate::router::{ use crate::router::{
is_question_tool_action, AgentModelInfo, AppState, CreateSessionRequest, PermissionReply, is_question_tool_action, AgentModeInfo, AgentModelInfo, AppState, CreateSessionRequest,
PermissionReply,
}; };
use sandbox_agent_agent_management::agents::AgentId; use sandbox_agent_agent_management::agents::AgentId;
use sandbox_agent_agent_management::credentials::{ use sandbox_agent_agent_management::credentials::{
@ -2834,16 +2835,64 @@ pub fn build_opencode_router(state: Arc<OpenCodeAppState>) -> Router {
)] )]
async fn oc_agent_list(State(state): State<Arc<OpenCodeAppState>>) -> impl IntoResponse { async fn oc_agent_list(State(state): State<Arc<OpenCodeAppState>>) -> impl IntoResponse {
let name = state.inner.branding.product_name(); let name = state.inner.branding.product_name();
let agent = json!({ let mut modes = match state
"name": name, .inner
"description": format!("{name} compatibility layer"), .session_manager()
"mode": "all", .agent_modes(AgentId::Opencode)
"native": false, .await
"hidden": false, {
"permission": [], Ok(modes) if !modes.is_empty() => modes,
"options": {}, _ => fallback_opencode_modes(),
}); };
(StatusCode::OK, Json(json!([agent]))) if modes.is_empty() {
let agent = json!({
"name": name,
"description": format!("{name} compatibility layer"),
"mode": "all",
"native": false,
"hidden": false,
"permission": [],
"options": {},
});
return (StatusCode::OK, Json(json!([agent])));
}
modes.sort_by(|a, b| a.id.cmp(&b.id));
let agents: Vec<Value> = modes
.into_iter()
.map(|mode| {
json!({
"id": mode.id,
"name": mode.id,
"description": mode.description,
"mode": "all",
"native": false,
"hidden": false,
"permission": [],
"options": {},
})
})
.collect();
(StatusCode::OK, Json(json!(agents)))
}
fn fallback_opencode_modes() -> Vec<AgentModeInfo> {
vec![
AgentModeInfo {
id: "build".to_string(),
name: "Build".to_string(),
description: "Default build mode".to_string(),
},
AgentModeInfo {
id: "plan".to_string(),
name: "Plan".to_string(),
description: "Planning mode".to_string(),
},
AgentModeInfo {
id: "custom".to_string(),
name: "Custom".to_string(),
description: "Any user-defined OpenCode agent name".to_string(),
},
]
} }
#[utoipa::path( #[utoipa::path(

View file

@ -1823,7 +1823,10 @@ impl SessionManager {
Ok(()) Ok(())
} }
async fn agent_modes(&self, agent: AgentId) -> Result<Vec<AgentModeInfo>, SandboxError> { pub(crate) async fn agent_modes(
&self,
agent: AgentId,
) -> Result<Vec<AgentModeInfo>, SandboxError> {
if agent != AgentId::Opencode { if agent != AgentId::Opencode {
return Ok(agent_modes_for(agent)); return Ok(agent_modes_for(agent));
} }
@ -4986,11 +4989,28 @@ fn agent_modes_for(agent: AgentId) -> Vec<AgentModeInfo> {
description: "Plan mode (prompt-only)".to_string(), description: "Plan mode (prompt-only)".to_string(),
}, },
], ],
AgentId::Amp => vec![AgentModeInfo { AgentId::Amp => vec![
id: "build".to_string(), AgentModeInfo {
name: "Build".to_string(), id: "smart".to_string(),
description: "Default build mode".to_string(), name: "Smart".to_string(),
}], description: "Default Amp mode".to_string(),
},
AgentModeInfo {
id: "rush".to_string(),
name: "Rush".to_string(),
description: "Fast mode with smaller context".to_string(),
},
AgentModeInfo {
id: "deep".to_string(),
name: "Deep".to_string(),
description: "Deep reasoning mode".to_string(),
},
AgentModeInfo {
id: "free".to_string(),
name: "Free".to_string(),
description: "Free/experimental mode".to_string(),
},
],
AgentId::Mock => vec![ AgentId::Mock => vec![
AgentModeInfo { AgentModeInfo {
id: "build".to_string(), id: "build".to_string(),
@ -5007,23 +5027,14 @@ fn agent_modes_for(agent: AgentId) -> Vec<AgentModeInfo> {
} }
fn amp_models_response() -> AgentModelsResponse { fn amp_models_response() -> AgentModelsResponse {
// NOTE: Amp models are hardcoded based on ampcode.com manual: AgentModelsResponse {
// - smart models: vec![AgentModelInfo {
// - rush id: "default".to_string(),
// - deep name: Some("Default".to_string()),
// - free
let models = ["smart", "rush", "deep", "free"]
.into_iter()
.map(|id| AgentModelInfo {
id: id.to_string(),
name: None,
variants: Some(amp_variants()), variants: Some(amp_variants()),
default_variant: Some("medium".to_string()), default_variant: Some("medium".to_string()),
}) }],
.collect(); default_model: Some("default".to_string()),
AgentModelsResponse {
models,
default_model: Some("smart".to_string()),
} }
} }
@ -5159,7 +5170,8 @@ fn normalize_agent_mode(agent: AgentId, agent_mode: Option<&str>) -> Result<Stri
.into()), .into()),
}, },
AgentId::Amp => match mode { AgentId::Amp => match mode {
"build" => Ok("build".to_string()), "build" => Ok("smart".to_string()),
"smart" | "rush" | "deep" | "free" => Ok(mode.to_string()),
value => Err(SandboxError::ModeNotSupported { value => Err(SandboxError::ModeNotSupported {
agent: agent.as_str().to_string(), agent: agent.as_str().to_string(),
mode: value.to_string(), mode: value.to_string(),

View file

@ -6,13 +6,10 @@ nonEmpty: true
hasDefault: true hasDefault: true
defaultInList: true defaultInList: true
hasVariants: true hasVariants: true
modelCount: 4 modelCount: 1
ids: ids:
- deep - default
- free defaultModel: default
- rush
- smart
defaultModel: smart
variants: variants:
- high - high
- medium - medium