@sigitcode-old / sigit / commits / 656b6ce
Fork 1

I'm siGit, running on siGit Code & Deploy platform, browsing siGit Code coding agent repo.

Group the ACP model picker by inference type

The Model dropdown in Zed mixed on-device models and siGit Code Cloud tiers in one flat list, and any entry could be picked whatever the Local inference toggle said. The picker now shows two groups with the active one first: on-device models and Cloud tiers. Both stay visible so the user can see what runs locally next to what Cloud offers. ACP has no disabled flag on a select option, so the inactive group is labelled ("turn on/off Local inference to use") and the agent refuses a pick from it. handle_set_session_config_option returns the unchanged options and posts a status saying which toggle to flip. The current model stays selected even when it falls in the inactive group after a flip, since flipping never switches or loads a model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-Authored-By: siGit Code v1.7.5-acp <noreply@sigit.si>

Seto Elkahfi committed Oct 26, 2026 at 15:54 UTC 656b6ce2b723d60e87a73eafc0be702959fc4827
2 files changed +213 -26
crates/cli/src/main.rs
+212 -25
@@ -93,14 +93,14 @@ use agent_client_protocol::schema::v1::{
93 ReadTextFileRequest, ReleaseTerminalRequest, RequestId, RequestPermissionOutcome,
94 RequestPermissionRequest, ResumeSessionRequest, ResumeSessionResponse,
95 SessionAdditionalDirectoriesCapabilities, SessionCapabilities, SessionCloseCapabilities,
96 - SessionConfigOption, SessionConfigOptionCategory, SessionConfigSelectOption,
97 - SessionConfigValueId, SessionDeleteCapabilities, SessionForkCapabilities, SessionId,
98 - SessionInfo, SessionInfoUpdate, SessionListCapabilities, SessionMode, SessionModeState,
99 - SessionNotification, SessionResumeCapabilities, SessionUpdate, SetSessionConfigOptionRequest,
100 - SetSessionConfigOptionResponse, SetSessionModeRequest, SetSessionModeResponse, StopReason,
101 - Terminal, TerminalOutputRequest, ToolCall, ToolCallContent, ToolCallLocation, ToolCallStatus,
102 - ToolCallUpdate, ToolCallUpdateFields, ToolKind, UnstructuredCommandInput, UsageUpdate,
103 - WaitForTerminalExitRequest, WriteTextFileRequest,
96 + SessionConfigOption, SessionConfigOptionCategory, SessionConfigSelectGroup,
97 + SessionConfigSelectOption, SessionConfigValueId, SessionDeleteCapabilities,
98 + SessionForkCapabilities, SessionId, SessionInfo, SessionInfoUpdate, SessionListCapabilities,
99 + SessionMode, SessionModeState, SessionNotification, SessionResumeCapabilities, SessionUpdate,
100 + SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest,
101 + SetSessionModeResponse, StopReason, Terminal, TerminalOutputRequest, ToolCall, ToolCallContent,
102 + ToolCallLocation, ToolCallStatus, ToolCallUpdate, ToolCallUpdateFields, ToolKind,
103 + UnstructuredCommandInput, UsageUpdate, WaitForTerminalExitRequest, WriteTextFileRequest,
104 };
105 use agent_client_protocol::{Agent, ByteStreams, Client, ConnectionTo, Responder};
106 use onde::inference::{ChatEngine, GgufModelConfig};
@@ -4648,9 +4648,9 @@ impl SiGitAgent {
4648 ));
4649 }
4650 let message = if enabled {
4651 - "Local inference is on. On-device models are highlighted; pick one from Model."
4651 + "Local inference is on. On-device models are selectable in Model."
4652 } else {
4653 - "Local inference is off. siGit Code Cloud tiers are highlighted; pick one from Model."
4653 + "Local inference is off. siGit Code Cloud tiers are selectable in Model."
4654 };
4655 self.send_system_status(cx, args.session_id.clone(), message)
4656 .ok();
@@ -4711,6 +4711,31 @@ impl SiGitAgent {
4711 }
4712 }
4713
4714 + // Both groups are listed, but only the one matching Local inference can
4715 + // be picked. ACP has no disabled option, so refuse and answer with the
4716 + // unchanged options so the picker snaps back.
4717 + if model_kind_of(model_id) != models::active_inference_kind() {
4718 + let message = match model_id.strip_prefix("sigit-cloud:") {
4719 + Some(tier) => format!(
4720 + "{} runs on siGit Code Cloud. Turn off Local inference to use it.",
4721 + provider::tier_title(tier)
4722 + ),
4723 + None => {
4724 + let name = models::local_picker_items()
4725 + .into_iter()
4726 + .find(|item| item.config.model_id == model_id)
4727 + .map(|item| item.display_name)
4728 + .unwrap_or_else(|| model_id.to_string());
4729 + format!("{name} runs on-device. Turn on Local inference to use it.")
4730 + }
4731 + };
4732 + self.send_system_status(cx, args.session_id.clone(), message)
4733 + .ok();
4734 + let current = self.current_model.lock().unwrap().clone();
4735 + let config_options = build_config_options(&current, &args.session_id.to_string());
4736 + return Ok(SetSessionConfigOptionResponse::new(config_options));
4737 + }
4738 +
4739 // ── siGit Code Cloud tier: no local load; sign-in gated ─────────────
4740 if let Some(tier) = model_id.strip_prefix("sigit-cloud:") {
4741 match self.switch_to_cloud_tier(tier).await {
@@ -5131,15 +5156,21 @@ fn build_config_options_for(current_model_id: &str, session_key: &str) -> Vec<Se
5156 let items = models::build_model_picker_items();
5157 let active_kind = models::active_inference_kind();
5158
5134 - let options: Vec<SessionConfigSelectOption> = items
5159 + let tagged: Vec<(models::InferenceKind, SessionConfigSelectOption)> = items
5160 .iter()
5161 .filter(|item| item.cache_health != setup::ModelCacheHealth::Incomplete)
5162 .map(|item| {
5163 let mut desc_parts = Vec::new();
5139 - // Mark options in the inactive mode so the active group reads as the
5140 - // recommended set (the list is already ordered active-group-first).
5164 + // Options in the inactive group stay visible but cannot be picked;
5165 + // `handle_set_session_config_option` refuses them.
5166 if item.source.kind() != active_kind {
5142 - desc_parts.push("inactive mode".to_string());
5167 + desc_parts.push(
5168 + match active_kind {
5169 + models::InferenceKind::Local => "Unavailable while Local inference is on",
5170 + models::InferenceKind::Cloud => "Unavailable while Local inference is off",
5171 + }
5172 + .to_string(),
5173 + );
5174 }
5175 if item.tool_calling {
5176 desc_parts.push("tool calling".to_string());
@@ -5189,14 +5220,47 @@ fn build_config_options_for(current_model_id: &str, session_key: &str) -> Vec<Se
5220 "{base_name} - {}{source_badge}",
5221 models::format_context_window(item.context_window_tokens)
5222 ));
5192 - SessionConfigSelectOption::new(
5223 + let option = SessionConfigSelectOption::new(
5224 SessionConfigValueId::new(item.config.model_id.as_str()),
5225 name,
5226 )
5196 - .description(description)
5227 + .description(description);
5228 + (item.source.kind(), option)
5229 })
5230 .collect();
5231
5232 + // Two groups, the active one first. ACP has no disabled flag on an option,
5233 + // so the inactive group is labelled and the agent refuses a pick from it.
5234 + let mut groups: Vec<SessionConfigSelectGroup> = Vec::new();
5235 + for kind in [active_kind, other_inference_kind(active_kind)] {
5236 + let options: Vec<SessionConfigSelectOption> = tagged
5237 + .iter()
5238 + .filter(|(k, _)| *k == kind)
5239 + .map(|(_, option)| option.clone())
5240 + .collect();
5241 + if options.is_empty() {
5242 + continue;
5243 + }
5244 + let (id, base, hint) = match kind {
5245 + models::InferenceKind::Local => (
5246 + "sigit-on-device",
5247 + "siGit Code on-device",
5248 + "turn on Local inference to use",
5249 + ),
5250 + models::InferenceKind::Cloud => (
5251 + "sigit-cloud",
5252 + "siGit Code Cloud",
5253 + "turn off Local inference to use",
5254 + ),
5255 + };
5256 + let name = if kind == active_kind {
5257 + format!("{base} (active)")
5258 + } else {
5259 + ascii_safe(&format!("{base} - {hint}"))
5260 + };
5261 + groups.push(SessionConfigSelectGroup::new(id, name, options));
5262 + }
5263 +
5264 // Local Inference on/off toggle, modeled as a two-option select so panel-only
5265 // ACP clients (no slash commands) can flip the mode.
5266 let local_on = settings::local_inference_enabled();
@@ -5210,12 +5274,12 @@ fn build_config_options_for(current_model_id: &str, session_key: &str) -> Vec<Se
5274 SessionConfigValueId::new(LOCAL_INFERENCE_ON),
5275 "Local".to_string(),
5276 )
5213 - .description("Run inference on-device; on-device models are highlighted".to_string()),
5277 + .description("Run inference on-device; on-device models are selectable".to_string()),
5278 SessionConfigSelectOption::new(
5279 SessionConfigValueId::new(LOCAL_INFERENCE_OFF),
5280 "Cloud".to_string(),
5281 )
5218 - .description("Use siGit Code Cloud; cloud tiers are highlighted".to_string()),
5282 + .description("Use siGit Code Cloud; cloud tiers are selectable".to_string()),
5283 ];
5284 let local_option = if CLIENT_BOOLEAN_CONFIG_OPTIONS.load(Ordering::Relaxed) {
5285 SessionConfigOption::boolean(LOCAL_INFERENCE_CONFIG_ID, "Local inference", local_on)
@@ -5227,7 +5291,7 @@ fn build_config_options_for(current_model_id: &str, session_key: &str) -> Vec<Se
5291 local_options,
5292 )
5293 }
5230 - .description("Toggle on-device inference; changes which models are highlighted");
5294 + .description("Toggle on-device inference; changes which models are selectable");
5295
5296 // Permissions dropdown (issue #76): Manual (ask), Auto (run unattended),
5297 // Plan (research only).
@@ -5248,21 +5312,39 @@ fn build_config_options_for(current_model_id: &str, session_key: &str) -> Vec<Se
5312 .category(SessionConfigOptionCategory::Mode)
5313 .description("How tool calls are approved for this session");
5314
5251 - if options.is_empty() {
5315 + if groups.is_empty() {
5316 return vec![local_option, permission_option];
5317 }
5318
5319 let current_value = SessionConfigValueId::new(current_model_id);
5320
5321 vec![
5258 - SessionConfigOption::select(MODEL_CONFIG_ID, "Model", current_value, options)
5322 + SessionConfigOption::select(MODEL_CONFIG_ID, "Model", current_value, groups)
5323 .category(SessionConfigOptionCategory::Model)
5260 - .description("Select an on-device model or a siGit Code Cloud tier"),
5324 + .description(
5325 + "On-device models or siGit Code Cloud tiers, depending on Local inference",
5326 + ),
5327 local_option,
5328 permission_option,
5329 ]
5330 }
5331
5332 +fn other_inference_kind(kind: models::InferenceKind) -> models::InferenceKind {
5333 + match kind {
5334 + models::InferenceKind::Local => models::InferenceKind::Cloud,
5335 + models::InferenceKind::Cloud => models::InferenceKind::Local,
5336 + }
5337 +}
5338 +
5339 +/// Which inference group a picker model id belongs to.
5340 +fn model_kind_of(model_id: &str) -> models::InferenceKind {
5341 + if model_id.starts_with("sigit-cloud:") {
5342 + models::InferenceKind::Cloud
5343 + } else {
5344 + models::InferenceKind::Local
5345 + }
5346 +}
5347 +
5348 /// returns `(config, max_tokens, tool_calling)` for a picker model_id, or None
5349 fn resolve_model_config(model_id: &str) -> Option<(GgufModelConfig, u64, bool)> {
5350 let items = models::local_picker_items();
@@ -5758,10 +5840,10 @@ async fn exec_slash_acp(
5840 SlashCommand::Local(value) => {
5841 let enabled = value.unwrap_or(!settings::local_inference_enabled());
5842 let message = match settings::set_local_inference(enabled) {
5761 - Ok(()) if enabled => "Local inference is on. On-device models are highlighted; \
5843 + Ok(()) if enabled => "Local inference is on. On-device models are selectable; \
5844 pick one with /models."
5845 .to_string(),
5764 - Ok(()) => "Local inference is off. siGit Code Cloud tiers are highlighted; \
5846 + Ok(()) => "Local inference is off. siGit Code Cloud tiers are selectable; \
5847 pick one with /models."
5848 .to_string(),
5849 Err(error) => format!("error: could not save local inference setting: {error}"),
@@ -8283,6 +8365,110 @@ mod tests {
8365 }
8366 }
8367
8368 + #[test]
8369 + fn model_kind_of_follows_the_cloud_prefix() {
8370 + assert!(model_kind_of("sigit-cloud:oke") == models::InferenceKind::Cloud);
8371 + assert!(model_kind_of("Qwen/Qwen3-4B-GGUF") == models::InferenceKind::Local);
8372 + }
8373 +
8374 + #[test]
8375 + fn acp_model_picker_groups_by_inference_kind() {
8376 + let _guard = crate::ENV_TEST_LOCK
8377 + .lock()
8378 + .unwrap_or_else(|poisoned| poisoned.into_inner());
8379 + let config = std::env::temp_dir().join(format!("sigit-groups-{}", uuid::Uuid::new_v4()));
8380 + let saved: Vec<_> = ["SIGIT_CONFIG_DIR", "SIGIT_LOCAL_INFERENCE"]
8381 + .into_iter()
8382 + .map(|name| (name, std::env::var_os(name)))
8383 + .collect();
8384 + // SAFETY: serialized by ENV_TEST_LOCK and restored below.
8385 + unsafe { std::env::set_var("SIGIT_CONFIG_DIR", &config) };
8386 + let outcome = std::panic::catch_unwind(acp_model_picker_groups_once);
8387 + // SAFETY: serialized by ENV_TEST_LOCK.
8388 + unsafe {
8389 + for (name, value) in saved {
8390 + match value {
8391 + Some(value) => std::env::set_var(name, value),
8392 + None => std::env::remove_var(name),
8393 + }
8394 + }
8395 + }
8396 + let _ = std::fs::remove_dir_all(&config);
8397 + if let Err(panic) = outcome {
8398 + std::panic::resume_unwind(panic);
8399 + }
8400 + }
8401 +
8402 + fn acp_model_picker_groups_once() {
8403 + fn model_option(current: &str) -> serde_json::Value {
8404 + let options =
8405 + serde_json::to_value(build_config_options_for(current, "t-groups")).unwrap();
8406 + options
8407 + .as_array()
8408 + .unwrap()
8409 + .iter()
8410 + .find(|option| option["id"] == MODEL_CONFIG_ID)
8411 + .expect("model config option")
8412 + .clone()
8413 + }
8414 + fn ids(group: &serde_json::Value) -> Vec<&str> {
8415 + group["options"]
8416 + .as_array()
8417 + .unwrap()
8418 + .iter()
8419 + .filter_map(|option| option["value"].as_str())
8420 + .collect()
8421 + }
8422 +
8423 + // SAFETY: the caller holds ENV_TEST_LOCK.
8424 + unsafe { std::env::set_var("SIGIT_LOCAL_INFERENCE", "off") };
8425 + let model = model_option("sigit-cloud:oke");
8426 + let groups = model["options"].as_array().expect("groups");
8427 + assert_eq!(groups[0]["group"], "sigit-cloud");
8428 + assert!(
8429 + ids(&groups[0])
8430 + .iter()
8431 + .all(|id| id.starts_with("sigit-cloud:"))
8432 + );
8433 + if let Some(local) = groups.get(1) {
8434 + assert_eq!(local["group"], "sigit-on-device");
8435 + assert!(
8436 + local["name"]
8437 + .as_str()
8438 + .unwrap()
8439 + .contains("turn on Local inference")
8440 + );
8441 + for option in local["options"].as_array().unwrap() {
8442 + assert!(
8443 + option["description"]
8444 + .as_str()
8445 + .unwrap()
8446 + .starts_with("Unavailable while Local inference is on")
8447 + || option["description"]
8448 + .as_str()
8449 + .unwrap()
8450 + .starts_with("Unavailable while Local inference is off")
8451 + );
8452 + }
8453 + }
8454 +
8455 + // SAFETY: the caller holds ENV_TEST_LOCK.
8456 + unsafe { std::env::set_var("SIGIT_LOCAL_INFERENCE", "on") };
8457 + let model = model_option("sigit-cloud:oke");
8458 + let groups = model["options"].as_array().expect("groups");
8459 + assert_eq!(groups[0]["group"], "sigit-on-device");
8460 + assert!(
8461 + !ids(&groups[0])
8462 + .iter()
8463 + .any(|id| id.starts_with("sigit-cloud:"))
8464 + );
8465 + let cloud = groups.get(1).expect("cloud group second");
8466 + assert_eq!(cloud["group"], "sigit-cloud");
8467 + // The current model stays selected even though it sits in the inactive group.
8468 + assert_eq!(model["currentValue"], "sigit-cloud:oke");
8469 + assert!(ids(cloud).contains(&"sigit-cloud:oke"));
8470 + }
8471 +
8472 fn acp_config_option_labels_once() {
8473 let current = GgufModelConfig {
8474 model_id: "sigit-cloud:oke".to_string(),
@@ -8301,8 +8487,9 @@ mod tests {
8487 .expect("model config option");
8488 let model_names: Vec<&str> = model["options"]
8489 .as_array()
8304 - .expect("model select options")
8490 + .expect("model select groups")
8491 .iter()
8492 + .flat_map(|group| group["options"].as_array().expect("group options"))
8493 .filter_map(|option| option["name"].as_str())
8494 .collect();
8495 assert!(model_names.contains(&"Oke - 200K ctx"));
crates/cli/tests/acp_permissions.rs
+1 -1
@@ -850,7 +850,7 @@ fn successful_config_option_changes_are_rendered_as_system_status_cards() {
850 .expect("Local Inference change should render as a system-style status card");
851 assert_eq!(
852 status["title"],
853 - "Local inference is off. siGit Code Cloud tiers are highlighted; pick one from Model."
853 + "Local inference is off. siGit Code Cloud tiers are selectable in Model."
854 );
855
856 let id = agent.request(