4445
/// modes are the Permissions dropdown under another name, so this changes
4446
/// the same state and then refreshes the dropdown for a client that shows
4447
/// both.
4448
+ /// `-32002` unless `session_id` is a session this process knows.
4449
+ fn require_known_session(&self, session_id: &SessionId) -> agent_client_protocol::Result<()> {
4450
+ let key = session_id.to_string();
4451
+ if self.sessions.lock().unwrap().contains_key(&key) {
4452
+ return Ok(());
4453
+ }
4454
+ Err(agent_client_protocol::Error::new(
4455
+ -32002,
4456
+ format!(
4457
+ "unknown session {key}; create it with session/new or restore it with session/load"
4458
+ ),
4459
+ ))
4460
+ }
4461
+
4462
+ /// The config options for `key`, built from that session's own model and
4463
+ /// not from whichever session is live, so it can be read without the
4464
+ /// workspace lock.
4465
+ fn config_options_for_session(&self, key: &str) -> Vec<SessionConfigOption> {
4466
+ let live = self.active_session.lock().unwrap().as_deref() == Some(key);
4467
+ let model_id = if live {
4468
+ self.current_model.lock().unwrap().model_id.clone()
4469
+ } else {
4470
+ self.sessions
4471
+ .lock()
4472
+ .unwrap()
4473
+ .get(key)
4474
+ .map(|state| {
4475
+ state
4476
+ .remote
4477
+ .as_ref()
4478
+ .map_or_else(|| state.model_id.clone(), |r| r.model.model_id.clone())
4479
+ })
4480
+ .unwrap_or_default()
4481
+ };
4482
+ build_config_options_for(&model_id, key)
4483
+ }
4484
+
4485
+ /// Apply a Permissions change for `session_id`, which can be mid-turn.
4486
+ ///
4487
+ /// Takes neither the session lock nor the workspace lock: the turn holds
4488
+ /// the first for its whole length, and a user flipping to Auto while a
4489
+ /// permission prompt is open has to be heard at once. It touches only
4490
+ /// state keyed by session (`permissions`), so the live session is left
4491
+ /// alone. Returns the new config options and the status line to show.
4492
+ fn change_permission_mode(
4493
+ &self,
4494
+ cx: &ConnectionTo<Client>,
4495
+ session_id: &SessionId,
4496
+ mode_id: Option<&str>,
4497
+ ) -> agent_client_protocol::Result<Vec<SessionConfigOption>> {
4498
+ self.require_known_session(session_id)?;
4499
+ let key = session_id.to_string();
4500
+ let Some(message) = mode_id.and_then(|id| apply_permission_mode(&key, id)) else {
4501
+ return Err(agent_client_protocol::Error::new(
4502
+ -32602,
4503
+ format!("unknown permission mode: {mode_id:?}"),
4504
+ ));
4505
+ };
4506
+ self.send_system_status(cx, session_id.clone(), message)
4507
+ .ok();
4508
+ Ok(self.config_options_for_session(&key))
4509
+ }
4510
+
4511
async fn handle_set_session_mode(
4512
&self,
4513
cx: &ConnectionTo<Client>,
4519
args.mode_id
4520
);
4521
4459
- self.activate_session(&args.session_id).await?;
4460
-
4461
- let session_key = args.session_id.to_string();
4462
- let Some(message) = apply_permission_mode(&session_key, args.mode_id.0.as_ref()) else {
4463
- return Err(agent_client_protocol::Error::new(
4464
- -32602,
4465
- format!("unknown session mode: {}", args.mode_id),
4466
- ));
4467
- };
4468
- self.send_system_status(cx, args.session_id.clone(), message)
4469
- .ok();
4470
- let config_options = {
4471
- let current = self.current_model.lock().unwrap();
4472
- build_config_options(¤t, &session_key)
4473
- };
4522
+ let config_options =
4523
+ self.change_permission_mode(cx, &args.session_id, Some(args.mode_id.0.as_ref()))?;
4524
self.send_tool_call_update(
4525
cx,
4526
args.session_id,
4541
args.value
4542
);
4543
4544
+ // ── Permissions dropdown (issue #76) ────────────────────────────────
4545
+ // Handled before the session is activated: the dispatch loop runs this
4546
+ // one without the session or workspace lock, so it can land mid-turn.
4547
+ if args.config_id.0.as_ref() == PERMISSION_MODE_CONFIG_ID {
4548
+ let picked = args.value.as_value_id().map(|v| v.0.as_ref());
4549
+ let config_options = self.change_permission_mode(cx, &args.session_id, picked)?;
4550
+ self.send_current_mode(cx, args.session_id);
4551
+ return Ok(SetSessionConfigOptionResponse::new(config_options));
4552
+ }
4553
+
4554
// A model switch carries the live conversation onto the new backend, so
4555
// the live conversation has to be the one whose picker changed.
4556
self.activate_session(&args.session_id).await?;
4591
return Ok(SetSessionConfigOptionResponse::new(config_options));
4592
}
4593
4534
- // ── Permissions dropdown (issue #76) ────────────────────────────────
4535
- if args.config_id.0.as_ref() == PERMISSION_MODE_CONFIG_ID {
4536
- let session_key = args.session_id.to_string();
4537
- let picked = args.value.as_value_id().map(|v| v.0.as_ref());
4538
- let Some(message) = picked.and_then(|id| apply_permission_mode(&session_key, id))
4539
- else {
4540
- return Err(agent_client_protocol::Error::new(
4541
- -32602,
4542
- format!("unknown Permissions value: {picked:?}"),
4543
- ));
4544
- };
4545
- self.send_current_mode(cx, args.session_id.clone());
4546
- self.send_system_status(cx, args.session_id.clone(), message)
4547
- .ok();
4548
- let current = self.current_model.lock().unwrap().clone();
4549
- let config_options = build_config_options(¤t, &session_key);
4550
- return Ok(SetSessionConfigOptionResponse::new(config_options));
4551
- }
4552
-
4594
if args.config_id.0.as_ref() != MODEL_CONFIG_ID {
4595
return Err(agent_client_protocol::Error::new(
4596
-32602,
5050
current_model: &GgufModelConfig,
5051
session_key: &str,
5052
) -> Vec<SessionConfigOption> {
5053
+ build_config_options_for(¤t_model.model_id, session_key)
5054
+}
5055
+
5056
+/// The config options for a session whose selected model is `current_model_id`.
5057
+/// Only the id matters, which is what lets a session that is not the live one
5058
+/// have its options built without installing it.
5059
+fn build_config_options_for(current_model_id: &str, session_key: &str) -> Vec<SessionConfigOption> {
5060
// The full list, including the siGit Code Cloud tiers, so the panel picker
5061
// mirrors the TUI `/models`. Cloud entries are sign-in gated at selection.
5062
let items = models::build_model_picker_items();
5183
return vec![local_option, permission_option];
5184
}
5185
5138
- let current_value = SessionConfigValueId::new(current_model.model_id.as_str());
5186
+ let current_value = SessionConfigValueId::new(current_model_id);
5187
5188
vec![
5189
SessionConfigOption::select(MODEL_CONFIG_ID, "Model", current_value, options)
6303
let state = Arc::clone(&state);
6304
let task_cx = cx.clone();
6305
cx.spawn(async move {
6306
+ // A Permissions change can arrive while a turn is
6307
+ // running, so it takes neither lock. Model and
6308
+ // Inference swap the live backend and wait for the turn.
6309
+ if req.config_id.0.as_ref() == PERMISSION_MODE_CONFIG_ID {
6310
+ return handle_response(
6311
+ responder,
6312
+ state.handle_set_session_config_option(&task_cx, req).await,
6313
+ );
6314
+ }
6315
let _session = state.lock_session(&req.session_id).await;
6316
let _workspace = state.workspace_lock.lock().await;
6317
handle_response(
6329
async move |req: SetSessionModeRequest, responder, cx: ConnectionTo<Client>| {
6330
let state = Arc::clone(&state);
6331
let task_cx = cx.clone();
6332
+ // No locks: the mode can change while a turn is running,
6333
+ // and the change touches only state keyed by session.
6334
cx.spawn(async move {
6276
- let _session = state.lock_session(&req.session_id).await;
6277
- let _workspace = state.workspace_lock.lock().await;
6335
handle_response(
6336
responder,
6337
state.handle_set_session_mode(&task_cx, req).await,