diff --git a/code-rs/app-server/src/main.rs b/code-rs/app-server/src/main.rs index 2f19d32a67d..6d27d3c161b 100644 --- a/code-rs/app-server/src/main.rs +++ b/code-rs/app-server/src/main.rs @@ -6,6 +6,12 @@ use code_common::CliConfigOverrides; #[derive(Debug, Parser)] struct AppServerArgs { + /// Accepted for Codex Desktop compatibility. Every Code handles analytics + /// policy through its normal config path, so this flag is intentionally a + /// no-op for the app-server process. + #[arg(long = "analytics-default-enabled", default_value_t = false)] + _analytics_default_enabled: bool, + /// Transport endpoint URL. Supported values: `stdio://` (default), /// `ws://IP:PORT`. #[arg( diff --git a/code-rs/app-server/src/message_processor.rs b/code-rs/app-server/src/message_processor.rs index 5b0238051da..008c7c8257b 100644 --- a/code-rs/app-server/src/message_processor.rs +++ b/code-rs/app-server/src/message_processor.rs @@ -27,9 +27,13 @@ use code_app_server_protocol::ConfigWriteErrorCode; use code_app_server_protocol::ConfigWriteResponse; use code_app_server_protocol::ExternalAgentConfigDetectParams; use code_app_server_protocol::ExternalAgentConfigImportParams; +use code_app_server_protocol::ExperimentalFeatureListResponse; use code_app_server_protocol::GetAccountParams; +use code_app_server_protocol::ListMcpServerStatusResponse; use code_app_server_protocol::LoginAccountParams; use code_app_server_protocol::MergeStrategy; +use code_app_server_protocol::ModelListResponse; +use code_app_server_protocol::ThreadListResponse; use code_app_server_protocol::ToolsV2; use code_app_server_protocol::AskForApproval as V2AskForApproval; use code_app_server_protocol::WriteStatus; @@ -279,6 +283,17 @@ impl MessageProcessor { | "config/batchWrite" | "externalAgentConfig/detect" | "externalAgentConfig/import" + | "thread/list" + | "model/list" + | "skills/list" + | "plugin/list" + | "hooks/list" + | "mcpServerStatus/list" + | "remoteControl/status/read" + | "remoteControl/enable" + | "collaborationMode/list" + | "experimentalFeature/list" + | "experimentalFeature/enablement/set" | "account/read" | "account/login/start" | "account/login/cancel" @@ -453,6 +468,86 @@ impl MessageProcessor { } true } + "thread/list" => { + let response = ThreadListResponse { + data: Vec::new(), + next_cursor: None, + }; + self.outgoing.send_response(request_id, response).await; + true + } + "model/list" => { + let response = ModelListResponse { + data: Vec::new(), + next_cursor: None, + }; + self.outgoing.send_response(request_id, response).await; + true + } + "skills/list" => { + self.outgoing + .send_response(request_id, json!({ "data": [], "nextCursor": null })) + .await; + true + } + "plugin/list" | "hooks/list" => { + self.outgoing + .send_response(request_id, json!({ "data": [], "nextCursor": null })) + .await; + true + } + "mcpServerStatus/list" => { + let response = ListMcpServerStatusResponse { + data: Vec::new(), + next_cursor: None, + }; + self.outgoing.send_response(request_id, response).await; + true + } + "remoteControl/status/read" => { + self.outgoing + .send_response(request_id, json!({ "enabled": false })) + .await; + true + } + "remoteControl/enable" => { + self.outgoing + .send_response( + request_id, + json!({ + "enabled": false, + "unsupported": true, + }), + ) + .await; + true + } + "collaborationMode/list" => { + self.outgoing + .send_response(request_id, json!({ "data": [], "nextCursor": null })) + .await; + true + } + "experimentalFeature/list" => { + let response = ExperimentalFeatureListResponse { + data: Vec::new(), + next_cursor: None, + }; + self.outgoing.send_response(request_id, response).await; + true + } + "experimentalFeature/enablement/set" => { + self.outgoing + .send_response( + request_id, + json!({ + "enabled": false, + "unsupported": true, + }), + ) + .await; + true + } "account/read" => { let params_value = request.params.clone().unwrap_or_else(|| json!({})); let params: GetAccountParams = match serde_json::from_value(params_value) { diff --git a/code-rs/app-server/tests/binary_smoke.rs b/code-rs/app-server/tests/binary_smoke.rs index dc95f58f0b1..9a56cfffcbe 100644 --- a/code-rs/app-server/tests/binary_smoke.rs +++ b/code-rs/app-server/tests/binary_smoke.rs @@ -10,8 +10,9 @@ fn app_server_bin() -> PathBuf { PathBuf::from(assert_cmd::cargo::cargo_bin!("code-app-server")) } -fn run_jsonrpc_script(requests: &[Value]) -> BTreeMap { +fn run_jsonrpc_script_with_args(args: &[&str], requests: &[Value]) -> BTreeMap { let mut child = Command::new(app_server_bin()) + .args(args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -51,6 +52,10 @@ fn run_jsonrpc_script(requests: &[Value]) -> BTreeMap { .collect() } +fn run_jsonrpc_script(requests: &[Value]) -> BTreeMap { + run_jsonrpc_script_with_args(&[], requests) +} + #[test] fn binary_smoke_requires_init_and_executes_command() { let marker = "hello-from-app-server-binary-smoke"; @@ -122,3 +127,73 @@ fn binary_smoke_requires_init_and_executes_command() { "execOneOffCommand stdout missing marker. stdout was: {stdout}" ); } + +#[test] +fn binary_smoke_accepts_desktop_startup_polling_methods() { + let requests = vec![ + json!({ + "jsonrpc":"2.0", + "id":1, + "method":"initialize", + "params":{ + "clientInfo":{ + "name":"codex-desktop-smoke", + "version":"0.1.0" + } + } + }), + json!({"jsonrpc":"2.0","id":2,"method":"thread/list","params":{}}), + json!({"jsonrpc":"2.0","id":3,"method":"model/list","params":{}}), + json!({"jsonrpc":"2.0","id":4,"method":"skills/list","params":{}}), + json!({"jsonrpc":"2.0","id":5,"method":"plugin/list","params":{}}), + json!({"jsonrpc":"2.0","id":6,"method":"hooks/list","params":{}}), + json!({"jsonrpc":"2.0","id":7,"method":"mcpServerStatus/list","params":{}}), + json!({"jsonrpc":"2.0","id":8,"method":"remoteControl/status/read","params":{}}), + json!({"jsonrpc":"2.0","id":9,"method":"remoteControl/enable","params":{"enabled":true}}), + json!({"jsonrpc":"2.0","id":10,"method":"collaborationMode/list","params":{}}), + json!({"jsonrpc":"2.0","id":11,"method":"experimentalFeature/list","params":{}}), + json!({"jsonrpc":"2.0","id":12,"method":"experimentalFeature/enablement/set","params":{"featureId":"desktop-smoke","enabled":true}}), + ]; + + let responses = run_jsonrpc_script_with_args(&["--analytics-default-enabled"], &requests); + + for id in 2..=12 { + let response = responses + .get(&id) + .unwrap_or_else(|| panic!("missing response for request id {id}")); + assert!( + response.get("error").is_none(), + "desktop startup method returned error for id {id}: {response}" + ); + } + + for id in [2, 3, 4, 5, 6, 7, 10, 11] { + let result = responses + .get(&id) + .and_then(|response| response.get("result")) + .expect("expected list result"); + assert_eq!(result.get("data"), Some(&json!([]))); + assert_eq!(result.get("nextCursor"), Some(&json!(null))); + } + assert_eq!( + responses + .get(&8) + .and_then(|response| response.get("result")) + .and_then(|result| result.get("enabled")), + Some(&json!(false)) + ); + assert_eq!( + responses + .get(&9) + .and_then(|response| response.get("result")) + .and_then(|result| result.get("unsupported")), + Some(&json!(true)) + ); + assert_eq!( + responses + .get(&12) + .and_then(|response| response.get("result")) + .and_then(|result| result.get("unsupported")), + Some(&json!(true)) + ); +} diff --git a/code-rs/cli/src/main.rs b/code-rs/cli/src/main.rs index a808dd92082..0eb3403691e 100644 --- a/code-rs/cli/src/main.rs +++ b/code-rs/cli/src/main.rs @@ -20,6 +20,7 @@ mod llm; mod update; use llm::{LlmCli, run_llm}; use update::{UpdateCheckCommand, UpdateCommand, run_update, run_update_check}; +use code_app_server::AppServerTransport; use code_common::CliConfigOverrides; use code_core::{entry_to_rollout_path, SessionCatalog, SessionQuery}; use code_core::spawn::spawn_std_command_with_retry; @@ -122,7 +123,7 @@ enum Subcommand { McpServer, /// [experimental] Run the app server. - AppServer, + AppServer(AppServerArgs), /// Generate shell completion scripts. Completion(CompletionCommand), @@ -172,6 +173,24 @@ enum Subcommand { Bridge(BridgeCommand), } +#[derive(Debug, Parser)] +struct AppServerArgs { + /// Accepted for Codex Desktop compatibility. Every Code handles analytics + /// policy through its normal config path, so this flag is intentionally a + /// no-op for the app-server process. + #[arg(long = "analytics-default-enabled", default_value_t = false)] + _analytics_default_enabled: bool, + + /// Transport endpoint URL. Supported values: `stdio://` (default), + /// `ws://IP:PORT`. + #[arg( + long = "listen", + value_name = "URL", + default_value = AppServerTransport::DEFAULT_LISTEN_URL + )] + listen: AppServerTransport, +} + #[derive(Debug, Parser)] struct CompletionCommand { /// Shell to generate completions for @@ -499,8 +518,13 @@ async fn cli_main(code_linux_sandbox_exe: Option) -> anyhow::Result<()> prepend_config_flags(&mut mcp_cli.config_overrides, root_config_overrides.clone()); mcp_cli.run().await?; } - Some(Subcommand::AppServer) => { - code_app_server::run_main(code_linux_sandbox_exe, root_config_overrides).await?; + Some(Subcommand::AppServer(args)) => { + code_app_server::run_main_with_transport( + code_linux_sandbox_exe, + root_config_overrides, + args.listen, + ) + .await?; } Some(Subcommand::Resume(ResumeCommand { session_id,