diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 3f7950425b..6f040600d1 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -242,6 +242,7 @@ fn sample_thread_with_metadata( parent_thread_id: Option, ) -> Thread { Thread { + environments: None, id: thread_id.to_string(), extra: None, session_id: format!("session-{thread_id}"), diff --git a/codex-rs/analytics/src/client_tests.rs b/codex-rs/analytics/src/client_tests.rs index c5a7b193a5..b6da8cfb9f 100644 --- a/codex-rs/analytics/src/client_tests.rs +++ b/codex-rs/analytics/src/client_tests.rs @@ -588,6 +588,7 @@ fn sample_thread_archive_request() -> ClientRequest { fn sample_thread(thread_id: &str) -> Thread { Thread { + environments: None, id: thread_id.to_string(), extra: None, session_id: format!("session-{thread_id}"), diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index c37466943a..1ad6b768b3 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -4338,6 +4338,29 @@ ], "type": "object" }, + "ThreadEnvironment": { + "description": "An environment selected by a loaded thread, independent of connection status.", + "properties": { + "cwd": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "environmentId": { + "type": "string" + }, + "runtimeWorkspaceRoots": { + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + } + }, + "required": [ + "cwd", + "environmentId", + "runtimeWorkspaceRoots" + ], + "type": "object" + }, "ThreadExtra": { "description": "Extra app-server data for a thread.", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index b0867382e5..b4798737fa 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -20553,6 +20553,29 @@ "title": "ThreadDeletedNotification", "type": "object" }, + "ThreadEnvironment": { + "description": "An environment selected by a loaded thread, independent of connection status.", + "properties": { + "cwd": { + "$ref": "#/definitions/v2/LegacyAppPathString" + }, + "environmentId": { + "type": "string" + }, + "runtimeWorkspaceRoots": { + "items": { + "$ref": "#/definitions/v2/LegacyAppPathString" + }, + "type": "array" + } + }, + "required": [ + "cwd", + "environmentId", + "runtimeWorkspaceRoots" + ], + "type": "object" + }, "ThreadExtra": { "description": "Extra app-server data for a thread.", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index b5e819cb47..010719d896 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -18267,6 +18267,29 @@ "title": "ThreadDeletedNotification", "type": "object" }, + "ThreadEnvironment": { + "description": "An environment selected by a loaded thread, independent of connection status.", + "properties": { + "cwd": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "environmentId": { + "type": "string" + }, + "runtimeWorkspaceRoots": { + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + } + }, + "required": [ + "cwd", + "environmentId", + "runtimeWorkspaceRoots" + ], + "type": "object" + }, "ThreadExtra": { "description": "Extra app-server data for a thread.", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index e0aa43dd29..1d46ab0f40 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -1485,6 +1485,29 @@ ], "type": "string" }, + "ThreadEnvironment": { + "description": "An environment selected by a loaded thread, independent of connection status.", + "properties": { + "cwd": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "environmentId": { + "type": "string" + }, + "runtimeWorkspaceRoots": { + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + } + }, + "required": [ + "cwd", + "environmentId", + "runtimeWorkspaceRoots" + ], + "type": "object" + }, "ThreadExtra": { "description": "Extra app-server data for a thread.", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 576edc360a..d1f3b13378 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -1276,6 +1276,29 @@ ], "type": "string" }, + "ThreadEnvironment": { + "description": "An environment selected by a loaded thread, independent of connection status.", + "properties": { + "cwd": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "environmentId": { + "type": "string" + }, + "runtimeWorkspaceRoots": { + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + } + }, + "required": [ + "cwd", + "environmentId", + "runtimeWorkspaceRoots" + ], + "type": "object" + }, "ThreadExtra": { "description": "Extra app-server data for a thread.", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index 6db9cbc2e2..48fd9923d3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -1276,6 +1276,29 @@ ], "type": "string" }, + "ThreadEnvironment": { + "description": "An environment selected by a loaded thread, independent of connection status.", + "properties": { + "cwd": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "environmentId": { + "type": "string" + }, + "runtimeWorkspaceRoots": { + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + } + }, + "required": [ + "cwd", + "environmentId", + "runtimeWorkspaceRoots" + ], + "type": "object" + }, "ThreadExtra": { "description": "Extra app-server data for a thread.", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 8682470d11..22936d4d99 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -1276,6 +1276,29 @@ ], "type": "string" }, + "ThreadEnvironment": { + "description": "An environment selected by a loaded thread, independent of connection status.", + "properties": { + "cwd": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "environmentId": { + "type": "string" + }, + "runtimeWorkspaceRoots": { + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + } + }, + "required": [ + "cwd", + "environmentId", + "runtimeWorkspaceRoots" + ], + "type": "object" + }, "ThreadExtra": { "description": "Extra app-server data for a thread.", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 030a48b1b8..5bab756d40 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -1485,6 +1485,29 @@ ], "type": "string" }, + "ThreadEnvironment": { + "description": "An environment selected by a loaded thread, independent of connection status.", + "properties": { + "cwd": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "environmentId": { + "type": "string" + }, + "runtimeWorkspaceRoots": { + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + } + }, + "required": [ + "cwd", + "environmentId", + "runtimeWorkspaceRoots" + ], + "type": "object" + }, "ThreadExtra": { "description": "Extra app-server data for a thread.", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json index f92e7201a0..5116e6c1f3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json @@ -1276,6 +1276,29 @@ ], "type": "string" }, + "ThreadEnvironment": { + "description": "An environment selected by a loaded thread, independent of connection status.", + "properties": { + "cwd": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "environmentId": { + "type": "string" + }, + "runtimeWorkspaceRoots": { + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + } + }, + "required": [ + "cwd", + "environmentId", + "runtimeWorkspaceRoots" + ], + "type": "object" + }, "ThreadExtra": { "description": "Extra app-server data for a thread.", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index da1efc5a23..32f37efb5e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -1276,6 +1276,29 @@ ], "type": "string" }, + "ThreadEnvironment": { + "description": "An environment selected by a loaded thread, independent of connection status.", + "properties": { + "cwd": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "environmentId": { + "type": "string" + }, + "runtimeWorkspaceRoots": { + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + } + }, + "required": [ + "cwd", + "environmentId", + "runtimeWorkspaceRoots" + ], + "type": "object" + }, "ThreadExtra": { "description": "Extra app-server data for a thread.", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 984dc14b69..ef86fc274e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -1485,6 +1485,29 @@ ], "type": "string" }, + "ThreadEnvironment": { + "description": "An environment selected by a loaded thread, independent of connection status.", + "properties": { + "cwd": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "environmentId": { + "type": "string" + }, + "runtimeWorkspaceRoots": { + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + } + }, + "required": [ + "cwd", + "environmentId", + "runtimeWorkspaceRoots" + ], + "type": "object" + }, "ThreadExtra": { "description": "Extra app-server data for a thread.", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index 63cc9d2e9a..e751825148 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -1276,6 +1276,29 @@ ], "type": "string" }, + "ThreadEnvironment": { + "description": "An environment selected by a loaded thread, independent of connection status.", + "properties": { + "cwd": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "environmentId": { + "type": "string" + }, + "runtimeWorkspaceRoots": { + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + } + }, + "required": [ + "cwd", + "environmentId", + "runtimeWorkspaceRoots" + ], + "type": "object" + }, "ThreadExtra": { "description": "Extra app-server data for a thread.", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index 04e4301147..4f46d1f2ab 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -1276,6 +1276,29 @@ ], "type": "string" }, + "ThreadEnvironment": { + "description": "An environment selected by a loaded thread, independent of connection status.", + "properties": { + "cwd": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "environmentId": { + "type": "string" + }, + "runtimeWorkspaceRoots": { + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + } + }, + "required": [ + "cwd", + "environmentId", + "runtimeWorkspaceRoots" + ], + "type": "object" + }, "ThreadExtra": { "description": "Extra app-server data for a thread.", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index cedcdc1806..b1c0ed239f 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index e499e34c1f..fe3b2f464b 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEnvironment.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEnvironment.ts new file mode 100644 index 0000000000..8784eb1651 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEnvironment.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { LegacyAppPathString } from "../LegacyAppPathString"; + +/** + * An environment selected by a loaded thread, independent of connection status. + */ +export type ThreadEnvironment = { environmentId: string, cwd: LegacyAppPathString, runtimeWorkspaceRoots: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 532b3f8d0c..146a0fccfa 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -473,6 +473,7 @@ export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse"; export type { ThreadDeleteParams } from "./ThreadDeleteParams"; export type { ThreadDeleteResponse } from "./ThreadDeleteResponse"; export type { ThreadDeletedNotification } from "./ThreadDeletedNotification"; +export type { ThreadEnvironment } from "./ThreadEnvironment"; export type { ThreadExtra } from "./ThreadExtra"; export type { ThreadForkParams } from "./ThreadForkParams"; export type { ThreadForkResponse } from "./ThreadForkResponse"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 2660a332a2..eb4816fe71 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -3128,6 +3128,7 @@ mod tests { request_id: RequestId::Integer(7), response: v2::ThreadStartResponse { thread: v2::Thread { + environments: None, id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(), extra: None, session_id: "67e55044-10b1-426f-9247-bb680e5fe0c7".to_string(), @@ -3186,6 +3187,7 @@ mod tests { "response": { "thread": { "id": "67e55044-10b1-426f-9247-bb680e5fe0c8", + "environments": null, "extra": null, "sessionId": "67e55044-10b1-426f-9247-bb680e5fe0c7", "forkedFromId": null, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/environment.rs b/codex-rs/app-server-protocol/src/protocol/v2/environment.rs index 4351199224..bdf34fbc89 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/environment.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/environment.rs @@ -1,9 +1,36 @@ use crate::JsonSchema; use crate::TS; +use codex_protocol::protocol::TurnEnvironmentSelection; +use codex_utils_path_uri::LegacyAppPathString; use codex_utils_path_uri::PathUri; use serde::Deserialize; use serde::Serialize; +/// An environment selected by a loaded thread, independent of connection status. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadEnvironment { + pub environment_id: String, + pub cwd: LegacyAppPathString, + pub runtime_workspace_roots: Vec, +} + +impl From<&TurnEnvironmentSelection> for ThreadEnvironment { + fn from(selection: &TurnEnvironmentSelection) -> Self { + Self { + environment_id: selection.environment_id.clone(), + cwd: selection.cwd.clone().into(), + runtime_workspace_roots: selection + .workspace_roots + .iter() + .cloned() + .map(Into::into) + .collect(), + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 4144dac1c1..2870fbbb43 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -272,6 +272,13 @@ fn thread_resume_params_accept_turns_page_bootstrap() { fn thread_resume_response_round_trips_initial_turns_page() { let response = ThreadResumeResponse { thread: Thread { + environments: Some(vec![ThreadEnvironment { + environment_id: "remote".to_string(), + cwd: LegacyAppPathString::from_string(r"C:\workspace"), + runtime_workspace_roots: vec![LegacyAppPathString::from_string( + r"C:\workspace\src", + )], + }]), id: "thr_123".to_string(), extra: None, session_id: "thr_123".to_string(), @@ -328,6 +335,12 @@ fn thread_resume_response_round_trips_initial_turns_page() { }; let value = serde_json::to_value(&response).expect("serialize thread resume response"); + assert_eq!( + value["thread"]["environments"], + json!([{ + "environmentId": "remote", "cwd": r"C:\workspace", "runtimeWorkspaceRoots": [r"C:\workspace\src"] + }]) + ); assert_eq!( value["thread"]["section"], json!({ @@ -345,11 +358,13 @@ fn thread_resume_response_round_trips_initial_turns_page() { legacy_thread_fields.remove("section"); legacy_thread_fields.remove("sectionEnteredAt"); legacy_thread_fields.remove("projectId"); + legacy_thread_fields.remove("environments"); let legacy_thread = serde_json::from_value::(legacy_thread).expect("deserialize legacy thread"); assert_eq!(legacy_thread.section, None); assert_eq!(legacy_thread.section_entered_at, None); assert_eq!(legacy_thread.project_id, None); + assert_eq!(legacy_thread.environments, None); assert_eq!( value.get("initialTurnsPage"), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs index 649a94be49..1956095031 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs @@ -1,4 +1,5 @@ use super::CodexErrorInfo; +use super::ThreadEnvironment; use super::ThreadItem; use super::ThreadStatus; use super::TurnStatus; @@ -203,6 +204,12 @@ pub struct ThreadSectionAppearance { pub struct Thread { /// Identifier for this thread. Codex-generated thread IDs are UUIDv7. pub id: String, + /// Current environments for a loaded thread, in priority order, primary first. + /// `null` means the thread is not loaded or the server does not expose its selection. + /// An empty list means no environments are selected. This does not report connection status. + #[experimental("thread.environments")] + #[serde(default)] + pub environments: Option>, /// Optional implementation-specific thread data. #[experimental("thread.extra")] pub extra: Option, @@ -286,6 +293,8 @@ pub struct Thread { #[serde(rename_all = "camelCase")] struct ThreadCompatibility { id: String, + #[serde(default)] + environments: Option>, extra: Option, session_id: String, forked_from_id: Option, @@ -328,6 +337,7 @@ impl<'de> Deserialize<'de> for Thread { let thread = ThreadCompatibility::deserialize(deserializer)?; Ok(Self { id: thread.id, + environments: thread.environments, extra: thread.extra, session_id: thread.session_id, forked_from_id: thread.forked_from_id, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index c33ccbcc95..92044d0be0 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -370,6 +370,13 @@ subscribing to it, or dispatching queued work or goal continuations. Start a fresh thread when you need a new Codex conversation. +Experimental `Thread.environments` returns a loaded thread's current selection as `{ environmentId, cwd, runtimeWorkspaceRoots }` entries. +The first entry is the primary environment; paths use that environment's native syntax. +An empty list means no environments are selected; `null` means the thread is not loaded or the server does not expose its selection. +Start and resume responses report the resulting live selection, and read, list, and unarchive responses include it for loaded threads, even if the client missed `thread/environment/connected`. +The field is not persisted and does not change executor selection or resume behavior. +Reading an unloaded thread leaves it unloaded and returns `null`; use `environment/status` to check connection status separately. + ```json { "method": "thread/start", "id": 10, "params": { // Optionally set config settings. If not specified, will use the user's diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 52ef949436..1ef5b0d0f3 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -3,7 +3,7 @@ use crate::error_code::invalid_request; use crate::notification_media::without_notification_media; use crate::outgoing_message::ClientRequestResult; use crate::outgoing_message::ThreadScopedOutgoingMessageSender; -use crate::request_processors::apply_live_model_settings; +use crate::request_processors::apply_live_thread_settings; use crate::request_processors::populate_thread_turns_from_history; use crate::request_processors::thread_from_stored_thread; use crate::request_processors::thread_settings_from_config_snapshot; @@ -1287,7 +1287,7 @@ pub(crate) async fn apply_bespoke_event_handling( } }; - apply_live_model_settings(&mut response.thread, &config_snapshot); + apply_live_thread_settings(&mut response.thread, &config_snapshot); outgoing.send_response(request_id, response).await; } } diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index 60875fadaf..4b22acac21 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -605,12 +605,19 @@ use crate::thread_state::ThreadStateManager; use token_usage_replay::restored_token_usage_turn_id; use token_usage_replay::send_thread_token_usage_update_to_connection; -pub(crate) fn apply_live_model_settings( +pub(crate) fn apply_live_thread_settings( thread: &mut Thread, config_snapshot: &ThreadConfigSnapshot, ) { thread.model = Some(config_snapshot.model.clone()); thread.reasoning_effort = config_snapshot.reasoning_effort.clone(); + thread.environments = Some( + config_snapshot + .environment_selections() + .iter() + .map(Into::into) + .collect(), + ); } fn resolve_request_cwd(cwd: Option) -> Result, JSONRPCErrorError> { diff --git a/codex-rs/app-server/src/request_processors/thread_enrichment.rs b/codex-rs/app-server/src/request_processors/thread_enrichment.rs index e90db06499..eede10a28b 100644 --- a/codex-rs/app-server/src/request_processors/thread_enrichment.rs +++ b/codex-rs/app-server/src/request_processors/thread_enrichment.rs @@ -1,4 +1,4 @@ -use super::apply_live_model_settings; +use super::apply_live_thread_settings; use super::thread_input::can_accept_direct_input; use crate::thread_status::ThreadWatchManager; use crate::thread_status::resolve_thread_status; @@ -44,7 +44,7 @@ pub(super) async fn enrich_loaded_threads( return; }; let config_snapshot = loaded_thread.config_snapshot().await; - apply_live_model_settings(thread, &config_snapshot); + apply_live_thread_settings(thread, &config_snapshot); if !matches!( &thread.source, SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. }) diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index 520f184a76..d209c7055f 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -1994,7 +1994,7 @@ impl ThreadRequestProcessor { if let Ok(loaded_thread) = self.thread_manager.get_thread(thread_uuid).await { thread.session_id = loaded_thread.session_configured().session_id.to_string(); let config_snapshot = loaded_thread.config_snapshot().await; - apply_live_model_settings(&mut thread, &config_snapshot); + apply_live_thread_settings(&mut thread, &config_snapshot); } self.attach_thread_name(thread_uuid, &mut thread).await; thread.status = resolve_thread_status( @@ -2049,6 +2049,10 @@ impl ThreadRequestProcessor { let (mut thread, _) = thread_from_stored_thread(stored_thread, fallback_provider.as_str(), &self.config.cwd); + if let Ok(loaded_thread) = self.thread_manager.get_thread(thread_id).await { + let config_snapshot = loaded_thread.config_snapshot().await; + apply_live_thread_settings(&mut thread, &config_snapshot); + } thread.status = resolve_thread_status( self.thread_watch_manager .loaded_status_for_thread(&thread.id) @@ -2985,7 +2989,7 @@ impl ThreadRequestProcessor { } else { fallback_thread }; - apply_live_model_settings(&mut thread, &config_snapshot); + apply_live_thread_settings(&mut thread, &config_snapshot); self.apply_thread_read_store_fields(thread_id, &mut thread, include_turns, loaded_thread) .await?; Ok(thread) @@ -4284,7 +4288,7 @@ impl ThreadRequestProcessor { ); thread_summary.session_id = existing_thread.session_configured().session_id.to_string(); thread_summary.thread_source = config_snapshot.thread_source.clone().map(Into::into); - apply_live_model_settings(&mut thread_summary, &config_snapshot); + apply_live_thread_settings(&mut thread_summary, &config_snapshot); thread_summary.can_accept_direct_input = Some(can_accept_direct_input( existing_thread.multi_agent_version(), &config_snapshot.session_source, @@ -4649,7 +4653,7 @@ impl ThreadRequestProcessor { )), }; let mut thread = thread?; - apply_live_model_settings(&mut thread, &config_snapshot); + apply_live_thread_settings(&mut thread, &config_snapshot); thread.can_accept_direct_input = Some(can_accept_direct_input); thread.id = thread_id.to_string(); thread.session_id = session_id; @@ -5147,7 +5151,7 @@ impl ThreadRequestProcessor { )); thread.session_id = session_configured.session_id.to_string(); thread.thread_source = config_snapshot.thread_source.clone().map(Into::into); - apply_live_model_settings(&mut thread, &config_snapshot); + apply_live_thread_settings(&mut thread, &config_snapshot); if thread.path.is_none() { thread.project_id = inherited_project_id.clone(); } @@ -5908,6 +5912,7 @@ pub(crate) fn thread_from_stored_thread( let thread_id = thread.thread_id.to_string(); let thread = Thread { id: thread_id.clone(), + environments: None, extra: None, session_id: thread_id, forked_from_id: thread.forked_from_id.map(|id| id.to_string()), @@ -6096,6 +6101,13 @@ fn build_thread_from_snapshot( let now = time::OffsetDateTime::now_utc().unix_timestamp(); Thread { id: thread_id.to_string(), + environments: Some( + config_snapshot + .environment_selections() + .iter() + .map(Into::into) + .collect(), + ), extra: None, session_id, forked_from_id: None, diff --git a/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs b/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs index 74ad7db490..8969e90eac 100644 --- a/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs +++ b/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs @@ -198,6 +198,7 @@ mod tests { fn test_thread(items: Vec) -> Thread { Thread { + environments: None, id: "thread-1".to_string(), extra: None, session_id: "session-1".to_string(), diff --git a/codex-rs/app-server/src/request_processors/thread_summary.rs b/codex-rs/app-server/src/request_processors/thread_summary.rs index df5beea0cb..dba7ead185 100644 --- a/codex-rs/app-server/src/request_processors/thread_summary.rs +++ b/codex-rs/app-server/src/request_processors/thread_summary.rs @@ -259,6 +259,7 @@ pub(crate) fn summary_to_thread( let thread_id = conversation_id.to_string(); Thread { + environments: None, id: thread_id.clone(), extra: None, session_id: thread_id, diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index 21403d1c11..fa2a8ee463 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -1469,7 +1469,7 @@ impl TurnRequestProcessor { if let Some(mut thread) = stored_thread { let config_snapshot = review_thread.config_snapshot().await; - apply_live_model_settings(&mut thread, &config_snapshot); + apply_live_thread_settings(&mut thread, &config_snapshot); thread.session_id = review_thread.session_configured().session_id.to_string(); self.thread_watch_manager .upsert_thread_silently(&thread.id) diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 160bf3ccce..27e7fb00d3 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -97,6 +97,7 @@ mod skills_list; mod sleep; mod thread_archive; mod thread_delete; +mod thread_environments; mod thread_fork; mod thread_inject_items; mod thread_list; diff --git a/codex-rs/app-server/tests/suite/v2/thread_environments.rs b/codex-rs/app-server/tests/suite/v2/thread_environments.rs new file mode 100644 index 0000000000..c9bc999f6d --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/thread_environments.rs @@ -0,0 +1,231 @@ +use anyhow::Result; +use app_test_support::MockResponsesConfig; +use app_test_support::TestAppServer; +use app_test_support::create_mock_responses_server_repeating_assistant; +use codex_app_server_protocol::ThreadEnvironment; +use codex_app_server_protocol::ThreadListParams; +use codex_app_server_protocol::ThreadListResponse; +use codex_app_server_protocol::ThreadReadParams; +use codex_app_server_protocol::ThreadReadResponse; +use codex_app_server_protocol::ThreadResumeParams; +use codex_app_server_protocol::ThreadResumeResponse; +use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::TurnStartParams; +use codex_app_server_protocol::TurnStatus; +use codex_app_server_protocol::UserInput; +use codex_utils_absolute_path::AbsolutePathBuf; +use pretty_assertions::assert_eq; +use serde_json::json; +use tempfile::TempDir; +use tokio::time::timeout; + +use super::connection_handling_websocket::DEFAULT_READ_TIMEOUT; +use super::connection_handling_websocket::connect_websocket; +use super::connection_handling_websocket::create_config_toml; +use super::connection_handling_websocket::read_notification_for_method; +use super::connection_handling_websocket::read_response_for_id; +use super::connection_handling_websocket::send_request; +use super::connection_handling_websocket::spawn_websocket_server; + +#[tokio::test] +async fn thread_environments_follow_the_loaded_thread_selection() -> Result<()> { + let responses = create_mock_responses_server_repeating_assistant("Done").await; + let home = TempDir::new()?; + MockResponsesConfig::new(&responses.uri()).write(home.path())?; + let mut first = TestAppServer::builder() + .with_codex_home(home.path()) + .build_initialized() + .await?; + let selection = first.auto_env_params()?; + let expected = Some(vec![ThreadEnvironment { + environment_id: selection.environment_id, + cwd: selection.cwd.clone(), + runtime_workspace_roots: vec![selection.cwd], + }]); + let request = first + .send_thread_start_request_with_auto_env(ThreadStartParams::default()) + .await?; + let started: ThreadStartResponse = + timeout(DEFAULT_READ_TIMEOUT, first.read_response(request)).await??; + assert_eq!(started.thread.environments, expected); + let thread_id = started.thread.id; + let completed = timeout( + DEFAULT_READ_TIMEOUT, + first.start_turn_and_wait_for_completion(TurnStartParams { + thread_id: thread_id.clone(), + input: vec![UserInput::Text { + text: "hello".into(), + text_elements: vec![], + }], + ..Default::default() + }), + ) + .await??; + assert_eq!(completed.turn.status, TurnStatus::Completed); + assert_read_and_list_environments(&mut first, &thread_id, &expected).await?; + + // Report the thread's explicit selection rather than the server-wide defaults. + let completed = timeout( + DEFAULT_READ_TIMEOUT, + first.start_turn_and_wait_for_completion(TurnStartParams { + thread_id: thread_id.clone(), + input: vec![UserInput::Text { + text: "hello again".into(), + text_elements: vec![], + }], + environments: Some(vec![]), + ..Default::default() + }), + ) + .await??; + assert_eq!(completed.turn.status, TurnStatus::Completed); + assert_read_and_list_environments(&mut first, &thread_id, &Some(vec![])).await?; + let request = first + .send_thread_resume_request(ThreadResumeParams { + thread_id: thread_id.clone(), + exclude_turns: true, + ..Default::default() + }) + .await?; + let resumed: ThreadResumeResponse = + timeout(DEFAULT_READ_TIMEOUT, first.read_response(request)).await??; + assert_eq!(resumed.thread.environments, Some(vec![])); + timeout(DEFAULT_READ_TIMEOUT, first.shutdown_gracefully()).await??; + + // Unloaded reads have no live selection. Resume reports its newly selected defaults. + let mut second = TestAppServer::builder() + .with_codex_home(home.path()) + .build_initialized() + .await?; + assert_read_and_list_environments(&mut second, &thread_id, &None).await?; + let request = second + .send_thread_resume_request(ThreadResumeParams { + thread_id: thread_id.clone(), + exclude_turns: true, + ..Default::default() + }) + .await?; + let resumed: ThreadResumeResponse = + timeout(DEFAULT_READ_TIMEOUT, second.read_response(request)).await??; + let expected = Some(vec![ThreadEnvironment { + environment_id: second.auto_env_params()?.environment_id, + cwd: resumed.cwd.clone().into(), + runtime_workspace_roots: resumed + .runtime_workspace_roots + .iter() + .cloned() + .map(Into::into) + .collect(), + }]); + assert_eq!(resumed.thread.environments, expected); + assert_read_and_list_environments(&mut second, &thread_id, &expected).await?; + let request = second + .send_thread_resume_request(ThreadResumeParams { + thread_id, + exclude_turns: true, + ..Default::default() + }) + .await?; + let resumed: ThreadResumeResponse = + timeout(DEFAULT_READ_TIMEOUT, second.read_response(request)).await??; + assert_eq!(resumed.thread.environments, expected); + Ok(()) +} + +async fn assert_read_and_list_environments( + server: &mut TestAppServer, + thread_id: &str, + expected: &Option>, +) -> Result<()> { + let request = server + .send_thread_read_request(ThreadReadParams { + thread_id: thread_id.to_owned(), + include_turns: false, + }) + .await?; + let read: ThreadReadResponse = + timeout(DEFAULT_READ_TIMEOUT, server.read_response(request)).await??; + assert_eq!(&read.thread.environments, expected); + let request = server + .send_thread_list_request(serde_json::from_value::(json!({}))?) + .await?; + let listed: ThreadListResponse = + timeout(DEFAULT_READ_TIMEOUT, server.read_response(request)).await??; + assert_eq!( + &listed + .data + .iter() + .find(|thread| thread.id == thread_id) + .expect("listed thread") + .environments, + expected + ); + Ok(()) +} + +#[tokio::test] +async fn thread_environments_are_returned_to_a_late_subscriber() -> Result<()> { + let responses = create_mock_responses_server_repeating_assistant("Done").await; + let home = TempDir::new()?; + create_config_toml(home.path(), &responses.uri(), "never")?; + let workspace = TempDir::new()?; + let cwd = AbsolutePathBuf::from_absolute_path(workspace.path().canonicalize()?)?; + let expected = + json!([{ "environmentId": "local", "cwd": cwd, "runtimeWorkspaceRoots": [cwd] }]); + let (mut process, address) = spawn_websocket_server(home.path()).await?; + let mut first = connect_websocket(address).await?; + let initialize = json!({ + "clientInfo": { "name": "environment-test", "version": "1" }, + "capabilities": { "experimentalApi": true } + }); + send_request( + &mut first, + "initialize", + /*id*/ 1, + Some(initialize.clone()), + ) + .await?; + read_response_for_id(&mut first, /*id*/ 1).await?; + send_request( + &mut first, + "thread/start", + /*id*/ 2, + Some(json!({ + "environments": [{ "environmentId": "local", "cwd": cwd }] + })), + ) + .await?; + let started = read_response_for_id(&mut first, /*id*/ 2).await?; + assert_eq!(started.result["thread"]["environments"], expected); + let thread_id = started.result["thread"]["id"].as_str().unwrap(); + send_request( + &mut first, + "turn/start", + /*id*/ 3, + Some(json!({ + "threadId": thread_id, "input": [{ "type": "text", "text": "hello" }] + })), + ) + .await?; + read_response_for_id(&mut first, /*id*/ 3).await?; + read_notification_for_method(&mut first, "turn/completed").await?; + + let mut second = connect_websocket(address).await?; + send_request(&mut second, "initialize", /*id*/ 1, Some(initialize)).await?; + read_response_for_id(&mut second, /*id*/ 1).await?; + for (id, method, params) in [ + (2, "thread/read", json!({ "threadId": thread_id })), + ( + 3, + "thread/resume", + json!({ "threadId": thread_id, "excludeTurns": true }), + ), + ] { + send_request(&mut second, method, id, Some(params)).await?; + let response = read_response_for_id(&mut second, id).await?; + assert_eq!(response.result["thread"]["environments"], expected); + } + process.kill().await?; + Ok(()) +} diff --git a/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs b/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs index 9bbc6cad1b..ad690591d6 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs @@ -332,6 +332,37 @@ async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> { assert_eq!(thread.path, None); assert_eq!(thread.forked_from_id, Some(parent_thread_id.to_string())); assert_eq!(thread.name, Some("named pathless thread".to_string())); + assert_eq!(thread.environments, None); + + // Pathless stores can return an unarchived thread while it is still loaded. + for (request_id, environments) in [(2, None), (4, Some(vec![]))] { + let result = client + .request(ClientRequest::ThreadStart { + request_id: RequestId::Integer(request_id), + params: ThreadStartParams { + model: Some("mock-model".to_string()), + environments, + ..Default::default() + }, + }) + .await? + .expect("thread/start should succeed"); + let ThreadStartResponse { + thread: started, .. + } = serde_json::from_value(result)?; + assert!(started.environments.is_some()); + let result = client + .request(ClientRequest::ThreadUnarchive { + request_id: RequestId::Integer(request_id + 1), + params: ThreadUnarchiveParams { + thread_id: started.id, + }, + }) + .await? + .expect("thread/unarchive should succeed for a loaded pathless thread"); + let ThreadUnarchiveResponse { thread } = serde_json::from_value(result)?; + assert_eq!(thread.environments, started.environments); + } client.shutdown().await?; Ok(()) diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index 85de6137b4..c178e40c43 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -351,6 +351,7 @@ async fn resume_lookup_model_providers_filters_only_last_lookup() { #[test] fn turn_items_for_thread_returns_matching_turn_items() { let thread = AppServerThread { + environments: None, id: "thread-1".to_string(), extra: None, session_id: "thread-1".to_string(), @@ -841,6 +842,7 @@ async fn session_configured_from_thread_response_preserves_parent_thread_id() { fn sample_thread_start_response() -> ThreadStartResponse { ThreadStartResponse { thread: codex_app_server_protocol::Thread { + environments: None, id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(), extra: None, session_id: "67e55044-10b1-426f-9247-bb680e5fe0c7".to_string(), diff --git a/codex-rs/tui/src/app/agents_overview_tests.rs b/codex-rs/tui/src/app/agents_overview_tests.rs index 8725f22e14..262d623e54 100644 --- a/codex-rs/tui/src/app/agents_overview_tests.rs +++ b/codex-rs/tui/src/app/agents_overview_tests.rs @@ -39,6 +39,7 @@ fn overview_thread( status: ThreadStatus, ) -> Thread { Thread { + environments: None, id: thread_id.to_string(), extra: None, project_id: None, diff --git a/codex-rs/tui/src/app/loaded_threads.rs b/codex-rs/tui/src/app/loaded_threads.rs index e47dc1de35..1a3b431497 100644 --- a/codex-rs/tui/src/app/loaded_threads.rs +++ b/codex-rs/tui/src/app/loaded_threads.rs @@ -135,6 +135,7 @@ mod tests { fn test_thread(thread_id: ThreadId, source: SessionSource) -> Thread { Thread { + environments: None, id: thread_id.to_string(), extra: None, session_id: thread_id.to_string(), diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 41fb39c9a1..d9a2c21387 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -4039,6 +4039,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re agent_thread_id, ServerNotification::ThreadStarted(ThreadStartedNotification { thread: Thread { + environments: None, id: agent_thread_id.to_string(), extra: None, session_id: agent_thread_id.to_string(), @@ -4140,6 +4141,7 @@ async fn inactive_thread_started_notification_preserves_primary_model_when_path_ agent_thread_id, ServerNotification::ThreadStarted(ThreadStartedNotification { thread: Thread { + environments: None, id: agent_thread_id.to_string(), extra: None, session_id: agent_thread_id.to_string(), @@ -4208,6 +4210,7 @@ async fn thread_read_session_state_does_not_reuse_primary_permission_profile() { app.primary_session_configured = Some(primary_session); let thread = Thread { + environments: None, id: read_thread_id.to_string(), extra: None, session_id: read_thread_id.to_string(), diff --git a/codex-rs/tui/src/app/tests/startup.rs b/codex-rs/tui/src/app/tests/startup.rs index 07c0b04d50..d27bccc735 100644 --- a/codex-rs/tui/src/app/tests/startup.rs +++ b/codex-rs/tui/src/app/tests/startup.rs @@ -933,6 +933,7 @@ async fn known_thread_started_preserves_session_without_reading_unmaterialized_r ); let notification = ThreadStartedNotification { thread: Thread { + environments: None, id: thread_id.to_string(), extra: None, session_id: thread_id.to_string(), diff --git a/codex-rs/tui/src/app/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index 531ded8ab1..ba09d1a154 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -410,6 +410,7 @@ mod tests { ..test_thread_session(primary_thread_id, test_path_buf("/tmp/primary")) }; let read_thread = Thread { + environments: None, id: read_thread_id.to_string(), extra: None, session_id: read_thread_id.to_string(), diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index cd596e030a..16d171954b 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -3686,6 +3686,7 @@ mod tests { let read_only_profile = PermissionProfile::read_only(); let response = ThreadResumeResponse { thread: codex_app_server_protocol::Thread { + environments: None, id: thread_id.to_string(), extra: None, session_id: ThreadId::new().to_string(), diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index 3c03a43eb3..752ece9265 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -6266,6 +6266,7 @@ session_picker_view = "dense" fn app_server_row_keeps_pathless_threads() { let thread_id = ThreadId::new(); let thread = Thread { + environments: None, id: thread_id.to_string(), extra: None, session_id: thread_id.to_string(), @@ -6310,6 +6311,7 @@ session_picker_view = "dense" let thread_id = ThreadId::new(); let thread = Thread { + environments: None, id: thread_id.to_string(), extra: None, session_id: thread_id.to_string(), @@ -6394,6 +6396,7 @@ session_picker_view = "dense" let thread_id = ThreadId::new(); let thread = Thread { + environments: None, id: thread_id.to_string(), extra: None, session_id: thread_id.to_string(), @@ -6469,6 +6472,7 @@ session_picker_view = "dense" let thread_id = ThreadId::new(); let thread = Thread { + environments: None, id: thread_id.to_string(), extra: None, session_id: thread_id.to_string(),