mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
Expose loaded thread environments in app-server responses (#42386)
## What changed - Add the experimental `Thread.environments` field with each selected environment's ID, working directory, and runtime workspace roots. - Populate the field from live thread state in start, resume, read, list, and unarchive responses. Use `null` for unloaded threads and an empty list when a loaded thread has no environment selection. - Document that the field reports selection rather than connection status and does not affect executor selection or resume behavior. ## Testing - Cover selection updates, unloaded and resumed threads, loaded pathless unarchives, and clients that subscribe after environment connection notifications. GitOrigin-RevId: a6daa06aff7089d7c5a1e4c7d03109b41f12fa3c
This commit is contained in:
committed by
copyberry
parent
cff76fa96f
commit
2b554fd3f9
@@ -242,6 +242,7 @@ fn sample_thread_with_metadata(
|
||||
parent_thread_id: Option<String>,
|
||||
) -> Thread {
|
||||
Thread {
|
||||
environments: None,
|
||||
id: thread_id.to_string(),
|
||||
extra: None,
|
||||
session_id: format!("session-{thread_id}"),
|
||||
|
||||
@@ -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}"),
|
||||
|
||||
@@ -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"
|
||||
|
||||
+23
@@ -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"
|
||||
|
||||
+23
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
+23
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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<LegacyAppPathString>, };
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<LegacyAppPathString>,
|
||||
}
|
||||
|
||||
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/")]
|
||||
|
||||
@@ -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::<Thread>(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"),
|
||||
|
||||
@@ -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<Vec<ThreadEnvironment>>,
|
||||
/// Optional implementation-specific thread data.
|
||||
#[experimental("thread.extra")]
|
||||
pub extra: Option<ThreadExtra>,
|
||||
@@ -286,6 +293,8 @@ pub struct Thread {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ThreadCompatibility {
|
||||
id: String,
|
||||
#[serde(default)]
|
||||
environments: Option<Vec<ThreadEnvironment>>,
|
||||
extra: Option<ThreadExtra>,
|
||||
session_id: String,
|
||||
forked_from_id: Option<String>,
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PathBuf>) -> Result<Option<AbsolutePathBuf>, JSONRPCErrorError> {
|
||||
|
||||
@@ -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<T>(
|
||||
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 { .. })
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -198,6 +198,7 @@ mod tests {
|
||||
|
||||
fn test_thread(items: Vec<ThreadItem>) -> Thread {
|
||||
Thread {
|
||||
environments: None,
|
||||
id: "thread-1".to_string(),
|
||||
extra: None,
|
||||
session_id: "session-1".to_string(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Vec<ThreadEnvironment>>,
|
||||
) -> 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::<ThreadListParams>(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(())
|
||||
}
|
||||
@@ -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(())
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -39,6 +39,7 @@ fn overview_thread(
|
||||
status: ThreadStatus,
|
||||
) -> Thread {
|
||||
Thread {
|
||||
environments: None,
|
||||
id: thread_id.to_string(),
|
||||
extra: None,
|
||||
project_id: None,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user