73
use agent_client_protocol::schema::v1::{
74
AgentCapabilities, AuthMethod, AuthMethodAgent, AuthenticateRequest, AuthenticateResponse,
75
AvailableCommand, AvailableCommandInput, AvailableCommandsUpdate, CancelNotification,
76
- ConfigOptionUpdate, ContentBlock, ContentChunk, EmbeddedResourceResource, ForkSessionRequest,
77
- ForkSessionResponse, Implementation, InitializeRequest, InitializeResponse,
76
+ ConfigOptionUpdate, ContentBlock, ContentChunk, CurrentModeUpdate, EmbeddedResourceResource,
77
+ ForkSessionRequest, ForkSessionResponse, Implementation, InitializeRequest, InitializeResponse,
78
ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, LoadSessionResponse,
79
McpCapabilities, McpServer, Meta, NewSessionRequest, NewSessionResponse, PermissionOption,
80
PermissionOptionKind, Plan, PlanEntry, PlanEntryPriority, PlanEntryStatus, PromptCapabilities,
82
RequestPermissionRequest, SessionAdditionalDirectoriesCapabilities, SessionCapabilities,
83
SessionConfigOption, SessionConfigOptionCategory, SessionConfigSelectOption,
84
SessionConfigValueId, SessionForkCapabilities, SessionId, SessionInfo, SessionListCapabilities,
85
- SessionNotification, SessionUpdate, SetSessionConfigOptionRequest,
86
- SetSessionConfigOptionResponse, StopReason, ToolCall, ToolCallContent, ToolCallLocation,
85
+ SessionMode, SessionModeState, SessionNotification, SessionUpdate,
86
+ SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest,
87
+ SetSessionModeResponse, StopReason, ToolCall, ToolCallContent, ToolCallLocation,
88
ToolCallStatus, ToolCallUpdate, ToolCallUpdateFields, ToolKind, UnstructuredCommandInput,
89
WriteTextFileRequest,
90
};
1353
cx.send_notification(SessionNotification::new(session_id, update))
1354
}
1355
1356
+ /// Tell a client that renders the legacy mode selector which mode is live.
1357
+ /// Sent wherever the Permissions config option is refreshed for a reason
1358
+ /// other than that client's own `session/set_mode`, so the two views of the
1359
+ /// same state never disagree.
1360
+ fn send_current_mode(&self, cx: &ConnectionTo<Client>, session_id: SessionId) {
1361
+ let mode = current_permission_mode(&session_id.to_string());
1362
+ self.send_tool_call_update(
1363
+ cx,
1364
+ session_id,
1365
+ SessionUpdate::CurrentModeUpdate(CurrentModeUpdate::new(mode)),
1366
+ )
1367
+ .ok();
1368
+ }
1369
+
1370
fn send_plan_update(
1371
&self,
1372
cx: &ConnectionTo<Client>,
2210
hooks::run_session_start_hooks(&hook_settings, &args.cwd, &args.session_id.to_string());
2211
}
2212
2198
- Ok(LoadSessionResponse::new().config_options(config_options))
2213
+ Ok(LoadSessionResponse::new()
2214
+ .config_options(config_options)
2215
+ .modes(build_session_modes(&args.session_id.to_string())))
2216
}
2217
2218
async fn handle_fork_session(
2284
hooks::run_session_start_hooks(&hook_settings, &args.cwd, &new_id.to_string());
2285
}
2286
2270
- Ok(ForkSessionResponse::new(new_id).config_options(config_options))
2287
+ let modes = build_session_modes(&new_id.to_string());
2288
+ Ok(ForkSessionResponse::new(new_id)
2289
+ .config_options(config_options)
2290
+ .modes(modes))
2291
}
2292
2293
async fn handle_new_session(
2325
hooks::run_session_start_hooks(&hook_settings, &args.cwd, &session_id.to_string());
2326
}
2327
2308
- Ok(NewSessionResponse::new(session_id).config_options(config_options))
2328
+ let modes = build_session_modes(&session_id.to_string());
2329
+ Ok(NewSessionResponse::new(session_id)
2330
+ .config_options(config_options)
2331
+ .modes(modes))
2332
}
2333
2334
async fn handle_prompt(
3316
SessionUpdate::ConfigOptionUpdate(ConfigOptionUpdate::new(config_options)),
3317
)
3318
.ok();
3319
+ // A reload rereads settings.toml, which can move the default mode.
3320
+ self.send_current_mode(cx, session_id.clone());
3321
self.advertise_commands(cx, session_id.clone());
3322
3323
self.send_assistant_message(
3328
.ok();
3329
}
3330
3331
+ /// `session/set_mode`, the selector ACP had before config options. The
3332
+ /// modes are the Permissions dropdown under another name, so this changes
3333
+ /// the same state and then refreshes the dropdown for a client that shows
3334
+ /// both.
3335
+ async fn handle_set_session_mode(
3336
+ &self,
3337
+ cx: &ConnectionTo<Client>,
3338
+ args: SetSessionModeRequest,
3339
+ ) -> agent_client_protocol::Result<SetSessionModeResponse> {
3340
+ log::info!(
3341
+ "set_session_mode: session={}, mode_id={}",
3342
+ args.session_id,
3343
+ args.mode_id
3344
+ );
3345
+
3346
+ self.activate_session(&args.session_id).await?;
3347
+
3348
+ let session_key = args.session_id.to_string();
3349
+ let Some(message) = apply_permission_mode(&session_key, args.mode_id.0.as_ref()) else {
3350
+ return Err(agent_client_protocol::Error::new(
3351
+ -32602,
3352
+ format!("unknown session mode: {}", args.mode_id),
3353
+ ));
3354
+ };
3355
+ self.send_system_status(cx, args.session_id.clone(), message)
3356
+ .ok();
3357
+ let config_options = {
3358
+ let current = self.current_model.lock().unwrap();
3359
+ build_config_options(¤t, &session_key)
3360
+ };
3361
+ self.send_tool_call_update(
3362
+ cx,
3363
+ args.session_id,
3364
+ SessionUpdate::ConfigOptionUpdate(ConfigOptionUpdate::new(config_options)),
3365
+ )
3366
+ .ok();
3367
+ Ok(SetSessionModeResponse::new())
3368
+ }
3369
+
3370
async fn handle_set_session_config_option(
3371
&self,
3372
cx: &ConnectionTo<Client>,
3416
// ── Permissions dropdown (issue #76) ────────────────────────────────
3417
if args.config_id.0.as_ref() == PERMISSION_MODE_CONFIG_ID {
3418
let session_key = args.session_id.to_string();
3355
- let message = match args.value.as_value_id().map(|v| v.0.as_ref()) {
3356
- Some(PERMISSION_MODE_MANUAL) => {
3357
- permissions::set_plan_mode(&session_key, false);
3358
- permissions::set_session_mode(
3359
- &session_key,
3360
- Some(settings::PermissionMode::Ask),
3361
- );
3362
- "Permissions: Manual — the agent asks before running mutating tools."
3363
- }
3364
- Some(PERMISSION_MODE_AUTO) => {
3365
- permissions::set_plan_mode(&session_key, false);
3366
- permissions::set_session_mode(
3367
- &session_key,
3368
- Some(settings::PermissionMode::Allow),
3369
- );
3370
- "Permissions: Auto — tools run without asking, subject to settings.toml rules."
3371
- }
3372
- Some(PERMISSION_MODE_PLAN) => {
3373
- permissions::set_plan_mode(&session_key, true);
3374
- "Permissions: Plan — the agent researches with read-only tools and presents \
3375
- a plan; edits and commands are blocked."
3376
- }
3377
- other => {
3378
- return Err(agent_client_protocol::Error::new(
3379
- -32602,
3380
- format!("unknown Permissions value: {other:?}"),
3381
- ));
3382
- }
3419
+ let picked = args.value.as_value_id().map(|v| v.0.as_ref());
3420
+ let Some(message) = picked.and_then(|id| apply_permission_mode(&session_key, id))
3421
+ else {
3422
+ return Err(agent_client_protocol::Error::new(
3423
+ -32602,
3424
+ format!("unknown Permissions value: {picked:?}"),
3425
+ ));
3426
};
3427
+ self.send_current_mode(cx, args.session_id.clone());
3428
self.send_system_status(cx, args.session_id.clone(), message)
3429
.ok();
3430
let current = self.current_model.lock().unwrap().clone();
3795
const PERMISSION_MODE_AUTO: &str = "permission-mode-auto";
3796
const PERMISSION_MODE_PLAN: &str = "permission-mode-plan";
3797
3798
+/// The Permissions modes as `(id, name, description)`, in display order. Both
3799
+/// the config option and the legacy `modes` selector are built from this, so
3800
+/// a client sees the same three entries whichever one it renders.
3801
+const PERMISSION_MODES: [(&str, &str, &str); 3] = [
3802
+ (
3803
+ PERMISSION_MODE_MANUAL,
3804
+ "Manual",
3805
+ "Ask before running mutating tools",
3806
+ ),
3807
+ (
3808
+ PERMISSION_MODE_AUTO,
3809
+ "Auto",
3810
+ "Run tools without asking, subject to settings.toml rules",
3811
+ ),
3812
+ (
3813
+ PERMISSION_MODE_PLAN,
3814
+ "Plan",
3815
+ "Research only; no edits or commands",
3816
+ ),
3817
+];
3818
+
3819
+/// The Permissions mode governing a session right now. Derived, never
3820
+/// stored — see `permissions.rs`.
3821
+fn current_permission_mode(session_key: &str) -> &'static str {
3822
+ if permissions::plan_mode(session_key) {
3823
+ return PERMISSION_MODE_PLAN;
3824
+ }
3825
+ match permissions::effective_mode(session_key) {
3826
+ settings::PermissionMode::Allow => PERMISSION_MODE_AUTO,
3827
+ settings::PermissionMode::Ask | settings::PermissionMode::Deny => PERMISSION_MODE_MANUAL,
3828
+ }
3829
+}
3830
+
3831
+/// Switch a session to one of [`PERMISSION_MODES`]. Returns the status line
3832
+/// to show the user, or `None` for an id that is not a mode.
3833
+fn apply_permission_mode(session_key: &str, mode_id: &str) -> Option<&'static str> {
3834
+ match mode_id {
3835
+ PERMISSION_MODE_MANUAL => {
3836
+ permissions::set_plan_mode(session_key, false);
3837
+ permissions::set_session_mode(session_key, Some(settings::PermissionMode::Ask));
3838
+ Some("Permissions: Manual — the agent asks before running mutating tools.")
3839
+ }
3840
+ PERMISSION_MODE_AUTO => {
3841
+ permissions::set_plan_mode(session_key, false);
3842
+ permissions::set_session_mode(session_key, Some(settings::PermissionMode::Allow));
3843
+ Some("Permissions: Auto — tools run without asking, subject to settings.toml rules.")
3844
+ }
3845
+ PERMISSION_MODE_PLAN => {
3846
+ permissions::set_plan_mode(session_key, true);
3847
+ Some(
3848
+ "Permissions: Plan — the agent researches with read-only tools and presents \
3849
+ a plan; edits and commands are blocked.",
3850
+ )
3851
+ }
3852
+ _ => None,
3853
+ }
3854
+}
3855
+
3856
+/// The same three modes as the Permissions config option, in the shape ACP
3857
+/// had before config options existed. The spec is retiring it, and a client
3858
+/// that reads config options is told to ignore it, so this is here for
3859
+/// clients that only render the mode selector (issue #148).
3860
+fn build_session_modes(session_key: &str) -> SessionModeState {
3861
+ let modes = PERMISSION_MODES
3862
+ .iter()
3863
+ .map(|&(id, name, description)| {
3864
+ SessionMode::new(id, name).description(description.to_string())
3865
+ })
3866
+ .collect();
3867
+ SessionModeState::new(current_permission_mode(session_key), modes)
3868
+}
3869
+
3870
/// Replace non-ASCII chars so a downstream byte-index truncation can't split a
3871
/// multi-byte char. Zed slices the model-picker label at a fixed byte offset
3872
/// (`agent_ui/src/config_options.rs`) and panics — crashing the whole editor —
3982
.description("Toggle on-device inference; changes which models are highlighted");
3983
3984
// Permissions dropdown (issue #76): Manual (ask), Auto (run unattended),
3869
- // Plan (research only). Derived, never stored — see `permissions.rs`.
3870
- let permission_current = SessionConfigValueId::new(if permissions::plan_mode(session_key) {
3871
- PERMISSION_MODE_PLAN
3872
- } else {
3873
- match permissions::effective_mode(session_key) {
3874
- settings::PermissionMode::Allow => PERMISSION_MODE_AUTO,
3875
- settings::PermissionMode::Ask | settings::PermissionMode::Deny => {
3876
- PERMISSION_MODE_MANUAL
3877
- }
3878
- }
3879
- });
3880
- let permission_options = vec![
3881
- SessionConfigSelectOption::new(
3882
- SessionConfigValueId::new(PERMISSION_MODE_MANUAL),
3883
- "Manual".to_string(),
3884
- )
3885
- .description("Ask before running mutating tools".to_string()),
3886
- SessionConfigSelectOption::new(
3887
- SessionConfigValueId::new(PERMISSION_MODE_AUTO),
3888
- "Auto".to_string(),
3889
- )
3890
- .description("Run tools without asking, subject to settings.toml rules".to_string()),
3891
- SessionConfigSelectOption::new(
3892
- SessionConfigValueId::new(PERMISSION_MODE_PLAN),
3893
- "Plan".to_string(),
3894
- )
3895
- .description("Research only; no edits or commands".to_string()),
3896
- ];
3985
+ // Plan (research only).
3986
+ let permission_current = SessionConfigValueId::new(current_permission_mode(session_key));
3987
+ let permission_options: Vec<_> = PERMISSION_MODES
3988
+ .iter()
3989
+ .map(|&(id, name, description)| {
3990
+ SessionConfigSelectOption::new(SessionConfigValueId::new(id), name.to_string())
3991
+ .description(description.to_string())
3992
+ })
3993
+ .collect();
3994
let permission_option = SessionConfigOption::select(
3995
PERMISSION_MODE_CONFIG_ID,
3996
"Permissions",
4293
SessionUpdate::ConfigOptionUpdate(ConfigOptionUpdate::new(config_options)),
4294
)
4295
.ok();
4296
+ agent.send_current_mode(cx, session_id.clone());
4297
agent
4298
.send_assistant_message(
4299
cx,
4326
SessionUpdate::ConfigOptionUpdate(ConfigOptionUpdate::new(config_options)),
4327
)
4328
.ok();
4329
+ agent.send_current_mode(cx, session_id.clone());
4330
agent.send_assistant_message(cx, session_id, message).ok();
4331
}
4332
SlashCommand::Permissions => {
5061
},
5062
agent_client_protocol::on_receive_request!(),
5063
)
5064
+ .on_receive_request(
5065
+ {
5066
+ let state = Arc::clone(&state);
5067
+ async move |req: SetSessionModeRequest, responder, cx: ConnectionTo<Client>| {
5068
+ let state = Arc::clone(&state);
5069
+ let task_cx = cx.clone();
5070
+ cx.spawn(async move {
5071
+ let _turn = state.turn_lock.lock().await;
5072
+ handle_response(
5073
+ responder,
5074
+ state.handle_set_session_mode(&task_cx, req).await,
5075
+ )
5076
+ })
5077
+ }
5078
+ },
5079
+ agent_client_protocol::on_receive_request!(),
5080
+ )
5081
.on_receive_notification(
5082
{
5083
let state = Arc::clone(&state);