Offer the Inference switch as a toggle to clients that render one
A client that advertises session.configOptions.boolean now gets the local/cloud Inference option as a boolean "Local inference" toggle. Every other client keeps the two-value select it has today, and session/set_config_option accepts a boolean or one of the select's ids for it. The capability is read once in initialize, since one process serves one connection. Closes #198 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seto Elkahfi committed
Oct 9, 2026 at 00:21 UTC
fa9847015daa00c21bee1ebae7249de8a618934d
3 files changed
+110
-13
.agents/AGENTS.md
+3
@@ -122,6 +122,9 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
122
it, reasoning included, shares `StreamedReply::message_id` and a tool round starts a new id.
123
A standalone notice (`send_assistant_message`) gets an id of its own, and replayed messages
124
get fresh ones; ids are random, so none is stored in history.
125
+ The Inference (local/cloud) config option is a `boolean` for a client that advertises
126
+ `session.configOptions.boolean` (read into `CLIENT_BOOLEAN_CONFIG_OPTIONS` in `initialize`)
127
+ and a two-value select for any other; `handle_set_session_config_option` takes either value.
128
`session/load` and `session/resume` share `restore_session`; the only difference is that
129
resume must not replay the history as `session/update`. `session/close` is the one place a
130
`SessionState` is dropped. It runs in two halves: `begin_close` signals the session's turn
src/main.rs
+39
-13
@@ -2220,6 +2220,18 @@ impl SiGitAgent {
2220
client_terminal::register(Arc::new(AcpClientTerminal { cx: cx.clone() }), terminal);
2221
}
2222
2223
+ // A client that renders boolean config options gets the Inference
2224
+ // switch as a toggle; the rest keep the two-value select.
2225
+ let boolean_options = req
2226
+ .client_capabilities
2227
+ .session
2228
+ .as_ref()
2229
+ .and_then(|session| session.config_options.as_ref())
2230
+ .and_then(|config_options| config_options.boolean.as_ref())
2231
+ .is_some();
2232
+ log::info!("boolean config options: {boolean_options}");
2233
+ CLIENT_BOOLEAN_CONFIG_OPTIONS.store(boolean_options, Ordering::Relaxed);
2234
+
2235
// Agent-handled auth method. We don't use `AuthMethod::Terminal`: editors
2236
// like Zed advertise terminal-auth capability but don't actually spawn the
2237
// login terminal for *custom* ACP agents, so the button is a silent no-op.
@@ -4398,10 +4410,15 @@ impl SiGitAgent {
4410
4411
// ── Local Inference toggle ──────────────────────────────────────────
4412
if args.config_id.0.as_ref() == LOCAL_INFERENCE_CONFIG_ID {
4401
- let enabled = match args.value.as_value_id().map(|v| v.0.as_ref()) {
4402
- Some(LOCAL_INFERENCE_ON) => true,
4403
- Some(LOCAL_INFERENCE_OFF) => false,
4404
- other => {
4413
+ // A toggle sends a boolean, the select fallback one of its ids.
4414
+ let enabled = match (
4415
+ args.value.as_bool(),
4416
+ args.value.as_value_id().map(|v| v.0.as_ref()),
4417
+ ) {
4418
+ (Some(enabled), _) => enabled,
4419
+ (_, Some(LOCAL_INFERENCE_ON)) => true,
4420
+ (_, Some(LOCAL_INFERENCE_OFF)) => false,
4421
+ (_, other) => {
4422
return Err(agent_client_protocol::Error::new(
4423
-32602,
4424
format!("unknown Local Inference value: {other:?}"),
@@ -4790,11 +4807,16 @@ impl SiGitAgent {
4807
/// config option ID for the model picker in Zed's agent panel
4808
const MODEL_CONFIG_ID: &str = "sigit-model";
4809
4793
-/// config option ID for the Local Inference on/off toggle. Surfaced as a
4794
-/// two-option `select` so ACP clients without slash-command support (e.g. Xcode)
4795
-/// can still flip the mode from the agent panel.
4810
+/// config option ID for the Local Inference on/off toggle, so ACP clients
4811
+/// without slash-command support (e.g. Xcode) can still flip the mode from the
4812
+/// agent panel. A `boolean` option for clients that advertise
4813
+/// `session.configOptions.boolean`, a two-option `select` for the rest.
4814
const LOCAL_INFERENCE_CONFIG_ID: &str = "sigit-local-inference";
4815
4816
+/// Whether the connected client advertised boolean config options. One
4817
+/// process serves one ACP connection, so this is set once in `initialize`.
4818
+static CLIENT_BOOLEAN_CONFIG_OPTIONS: AtomicBool = AtomicBool::new(false);
4819
+
4820
/// `select` value ids for the Local Inference toggle.
4821
const LOCAL_INFERENCE_ON: &str = "local-inference-on";
4822
const LOCAL_INFERENCE_OFF: &str = "local-inference-off";
@@ -4987,12 +5009,16 @@ fn build_config_options(
5009
)
5010
.description("Use siGit Code Cloud; cloud tiers are highlighted".to_string()),
5011
];
4990
- let local_option = SessionConfigOption::select(
4991
- LOCAL_INFERENCE_CONFIG_ID,
4992
- "Inference",
4993
- local_current,
4994
- local_options,
4995
- )
5012
+ let local_option = if CLIENT_BOOLEAN_CONFIG_OPTIONS.load(Ordering::Relaxed) {
5013
+ SessionConfigOption::boolean(LOCAL_INFERENCE_CONFIG_ID, "Local inference", local_on)
5014
+ } else {
5015
+ SessionConfigOption::select(
5016
+ LOCAL_INFERENCE_CONFIG_ID,
5017
+ "Inference",
5018
+ local_current,
5019
+ local_options,
5020
+ )
5021
+ }
5022
.description("Toggle on-device inference; changes which models are highlighted");
5023
5024
// Permissions dropdown (issue #76): Manual (ask), Auto (run unattended),
tests/acp_permissions.rs
+68
@@ -723,6 +723,74 @@ fn a_call_denied_for_the_session_is_not_asked_about_again() {
723
let _ = std::fs::remove_dir_all(&scratch);
724
}
725
726
+/// A client that renders boolean config options gets the Inference switch as
727
+/// a toggle, and can flip it with a boolean value; any other client keeps the
728
+/// two-value select (issue #198).
729
+#[test]
730
+fn the_inference_switch_is_a_toggle_for_clients_that_render_one() {
731
+ let inference_option = |options: &Value| -> Value {
732
+ options
733
+ .as_array()
734
+ .expect("config options")
735
+ .iter()
736
+ .find(|option| option["id"] == "sigit-local-inference")
737
+ .cloned()
738
+ .expect("inference option")
739
+ };
740
+
741
+ for (capabilities, expected_type) in [
742
+ (
743
+ json!({"session": {"configOptions": {"boolean": {}}}}),
744
+ "boolean",
745
+ ),
746
+ (json!({}), "select"),
747
+ ] {
748
+ let endpoint = start_fake_endpoint(vec![]);
749
+ let scratch = std::env::temp_dir().join(format!(
750
+ "sigit_acp_bool_option_{expected_type}_{}",
751
+ std::process::id()
752
+ ));
753
+ let config_dir = scratch.join("config");
754
+ let cwd = scratch.join("cwd");
755
+ std::fs::create_dir_all(&config_dir).unwrap();
756
+ std::fs::create_dir_all(&cwd).unwrap();
757
+
758
+ let mut agent = spawn_agent(endpoint.port, &config_dir);
759
+ let id = agent.request(
760
+ "initialize",
761
+ json!({"protocolVersion": 1, "clientCapabilities": capabilities}),
762
+ );
763
+ agent.wait_for_response(id);
764
+
765
+ let id = agent.request("session/new", json!({"cwd": cwd, "mcpServers": []}));
766
+ let created = agent.wait_for_response(id);
767
+ let session_id = created["result"]["sessionId"]
768
+ .as_str()
769
+ .expect("session id")
770
+ .to_string();
771
+ let option = inference_option(&created["result"]["configOptions"]);
772
+ assert_eq!(option["type"], expected_type, "{option}");
773
+
774
+ if expected_type == "boolean" {
775
+ let id = agent.request(
776
+ "session/set_config_option",
777
+ json!({
778
+ "sessionId": session_id,
779
+ "configId": "sigit-local-inference",
780
+ "type": "boolean",
781
+ "value": true,
782
+ }),
783
+ );
784
+ let response = agent.wait_for_response(id);
785
+ let option = inference_option(&response["result"]["configOptions"]);
786
+ assert_eq!(option["currentValue"], true, "{option}");
787
+ }
788
+
789
+ drop(agent);
790
+ let _ = std::fs::remove_dir_all(&scratch);
791
+ }
792
+}
793
+
794
#[test]
795
fn successful_config_option_changes_are_rendered_as_system_status_cards() {
796
let endpoint = start_fake_endpoint(vec![]);