fix(tui): forward cwd changes through app-server

Forward standalone turn-context overrides from the TUI to loaded app-server threads via `thread/turnContext/update`, so `/cwd` updates the core thread state instead of only local UI state.

Add protocol schemas and regression coverage showing thread-scoped shell commands use the updated cwd after the context override.
This commit is contained in:
Felipe Coury
2026-04-06 15:29:25 -03:00
parent ae617fc469
commit 93154c3378
17 changed files with 1271 additions and 2 deletions
@@ -3106,6 +3106,122 @@
},
"type": "object"
},
"ThreadTurnContextUpdateParams": {
"properties": {
"approvalPolicy": {
"anyOf": [
{
"$ref": "#/definitions/AskForApproval"
},
{
"type": "null"
}
],
"description": "Override the approval policy for subsequent turns."
},
"approvalsReviewer": {
"anyOf": [
{
"$ref": "#/definitions/ApprovalsReviewer"
},
{
"type": "null"
}
],
"description": "Override where approval requests are routed for review on subsequent turns."
},
"cwd": {
"description": "Override the working directory for subsequent turns and thread-scoped operations.",
"type": [
"string",
"null"
]
},
"effort": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
],
"description": "Override the reasoning effort for subsequent turns."
},
"model": {
"description": "Override the model for subsequent turns.",
"type": [
"string",
"null"
]
},
"personality": {
"anyOf": [
{
"$ref": "#/definitions/Personality"
},
{
"type": "null"
}
],
"description": "Override the personality for subsequent turns."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Override the sandbox policy for subsequent turns."
},
"serviceTier": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/ServiceTier"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
],
"description": "Override the service tier for subsequent turns."
},
"summary": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummary"
},
{
"type": "null"
}
],
"description": "Override the reasoning summary for subsequent turns."
},
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"type": "object"
},
"ThreadUnarchiveParams": {
"properties": {
"threadId": {
@@ -3605,6 +3721,30 @@
"title": "Thread/name/setRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"thread/turnContext/update"
],
"title": "Thread/turnContext/updateRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadTurnContextUpdateParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/turnContext/updateRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -386,6 +386,30 @@
"title": "Thread/name/setRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"thread/turnContext/update"
],
"title": "Thread/turnContext/updateRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ThreadTurnContextUpdateParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/turnContext/updateRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -14165,6 +14189,129 @@
"title": "ThreadTokenUsageUpdatedNotification",
"type": "object"
},
"ThreadTurnContextUpdateParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"approvalPolicy": {
"anyOf": [
{
"$ref": "#/definitions/v2/AskForApproval"
},
{
"type": "null"
}
],
"description": "Override the approval policy for subsequent turns."
},
"approvalsReviewer": {
"anyOf": [
{
"$ref": "#/definitions/v2/ApprovalsReviewer"
},
{
"type": "null"
}
],
"description": "Override where approval requests are routed for review on subsequent turns."
},
"cwd": {
"description": "Override the working directory for subsequent turns and thread-scoped operations.",
"type": [
"string",
"null"
]
},
"effort": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/v2/ReasoningEffort"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
],
"description": "Override the reasoning effort for subsequent turns."
},
"model": {
"description": "Override the model for subsequent turns.",
"type": [
"string",
"null"
]
},
"personality": {
"anyOf": [
{
"$ref": "#/definitions/v2/Personality"
},
{
"type": "null"
}
],
"description": "Override the personality for subsequent turns."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/v2/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Override the sandbox policy for subsequent turns."
},
"serviceTier": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/v2/ServiceTier"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
],
"description": "Override the service tier for subsequent turns."
},
"summary": {
"anyOf": [
{
"$ref": "#/definitions/v2/ReasoningSummary"
},
{
"type": "null"
}
],
"description": "Override the reasoning summary for subsequent turns."
},
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadTurnContextUpdateParams",
"type": "object"
},
"ThreadTurnContextUpdateResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ThreadTurnContextUpdateResponse",
"type": "object"
},
"ThreadUnarchiveParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -961,6 +961,30 @@
"title": "Thread/name/setRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"thread/turnContext/update"
],
"title": "Thread/turnContext/updateRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadTurnContextUpdateParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/turnContext/updateRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -12020,6 +12044,129 @@
"title": "ThreadTokenUsageUpdatedNotification",
"type": "object"
},
"ThreadTurnContextUpdateParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"approvalPolicy": {
"anyOf": [
{
"$ref": "#/definitions/AskForApproval"
},
{
"type": "null"
}
],
"description": "Override the approval policy for subsequent turns."
},
"approvalsReviewer": {
"anyOf": [
{
"$ref": "#/definitions/ApprovalsReviewer"
},
{
"type": "null"
}
],
"description": "Override where approval requests are routed for review on subsequent turns."
},
"cwd": {
"description": "Override the working directory for subsequent turns and thread-scoped operations.",
"type": [
"string",
"null"
]
},
"effort": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
],
"description": "Override the reasoning effort for subsequent turns."
},
"model": {
"description": "Override the model for subsequent turns.",
"type": [
"string",
"null"
]
},
"personality": {
"anyOf": [
{
"$ref": "#/definitions/Personality"
},
{
"type": "null"
}
],
"description": "Override the personality for subsequent turns."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Override the sandbox policy for subsequent turns."
},
"serviceTier": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/ServiceTier"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
],
"description": "Override the service tier for subsequent turns."
},
"summary": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummary"
},
{
"type": "null"
}
],
"description": "Override the reasoning summary for subsequent turns."
},
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadTurnContextUpdateParams",
"type": "object"
},
"ThreadTurnContextUpdateResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ThreadTurnContextUpdateResponse",
"type": "object"
},
"ThreadUnarchiveParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -0,0 +1,454 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"ApprovalsReviewer": {
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `guardian_subagent` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request.",
"enum": [
"user",
"guardian_subagent"
],
"type": "string"
},
"AskForApproval": {
"oneOf": [
{
"enum": [
"untrusted",
"on-failure",
"on-request",
"never"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"granular": {
"properties": {
"mcp_elicitations": {
"type": "boolean"
},
"request_permissions": {
"default": false,
"type": "boolean"
},
"rules": {
"type": "boolean"
},
"sandbox_approval": {
"type": "boolean"
},
"skill_approval": {
"default": false,
"type": "boolean"
}
},
"required": [
"mcp_elicitations",
"rules",
"sandbox_approval"
],
"type": "object"
}
},
"required": [
"granular"
],
"title": "GranularAskForApproval",
"type": "object"
}
]
},
"CollaborationMode": {
"description": "Collaboration mode for a Codex session.",
"properties": {
"mode": {
"$ref": "#/definitions/ModeKind"
},
"settings": {
"$ref": "#/definitions/Settings"
}
},
"required": [
"mode",
"settings"
],
"type": "object"
},
"ModeKind": {
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"default"
],
"type": "string"
},
"NetworkAccess": {
"enum": [
"restricted",
"enabled"
],
"type": "string"
},
"Personality": {
"enum": [
"none",
"friendly",
"pragmatic"
],
"type": "string"
},
"ReadOnlyAccess": {
"oneOf": [
{
"properties": {
"includePlatformDefaults": {
"default": true,
"type": "boolean"
},
"readableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
},
"type": {
"enum": [
"restricted"
],
"title": "RestrictedReadOnlyAccessType",
"type": "string"
}
},
"required": [
"type"
],
"title": "RestrictedReadOnlyAccess",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"fullAccess"
],
"title": "FullAccessReadOnlyAccessType",
"type": "string"
}
},
"required": [
"type"
],
"title": "FullAccessReadOnlyAccess",
"type": "object"
}
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string"
},
"ReasoningSummary": {
"description": "A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries",
"oneOf": [
{
"enum": [
"auto",
"concise",
"detailed"
],
"type": "string"
},
{
"description": "Option to disable reasoning summaries.",
"enum": [
"none"
],
"type": "string"
}
]
},
"SandboxPolicy": {
"oneOf": [
{
"properties": {
"type": {
"enum": [
"dangerFullAccess"
],
"title": "DangerFullAccessSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DangerFullAccessSandboxPolicy",
"type": "object"
},
{
"properties": {
"access": {
"allOf": [
{
"$ref": "#/definitions/ReadOnlyAccess"
}
],
"default": {
"type": "fullAccess"
}
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"readOnly"
],
"title": "ReadOnlySandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ReadOnlySandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"allOf": [
{
"$ref": "#/definitions/NetworkAccess"
}
],
"default": "restricted"
},
"type": {
"enum": [
"externalSandbox"
],
"title": "ExternalSandboxSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ExternalSandboxSandboxPolicy",
"type": "object"
},
{
"properties": {
"excludeSlashTmp": {
"default": false,
"type": "boolean"
},
"excludeTmpdirEnvVar": {
"default": false,
"type": "boolean"
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"readOnlyAccess": {
"allOf": [
{
"$ref": "#/definitions/ReadOnlyAccess"
}
],
"default": {
"type": "fullAccess"
}
},
"type": {
"enum": [
"workspaceWrite"
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"type"
],
"title": "WorkspaceWriteSandboxPolicy",
"type": "object"
}
]
},
"ServiceTier": {
"enum": [
"fast",
"flex"
],
"type": "string"
},
"Settings": {
"description": "Settings for a collaboration mode.",
"properties": {
"developer_instructions": {
"type": [
"string",
"null"
]
},
"model": {
"type": "string"
},
"reasoning_effort": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
}
},
"required": [
"model"
],
"type": "object"
}
},
"properties": {
"approvalPolicy": {
"anyOf": [
{
"$ref": "#/definitions/AskForApproval"
},
{
"type": "null"
}
],
"description": "Override the approval policy for subsequent turns."
},
"approvalsReviewer": {
"anyOf": [
{
"$ref": "#/definitions/ApprovalsReviewer"
},
{
"type": "null"
}
],
"description": "Override where approval requests are routed for review on subsequent turns."
},
"cwd": {
"description": "Override the working directory for subsequent turns and thread-scoped operations.",
"type": [
"string",
"null"
]
},
"effort": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
],
"description": "Override the reasoning effort for subsequent turns."
},
"model": {
"description": "Override the model for subsequent turns.",
"type": [
"string",
"null"
]
},
"personality": {
"anyOf": [
{
"$ref": "#/definitions/Personality"
},
{
"type": "null"
}
],
"description": "Override the personality for subsequent turns."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Override the sandbox policy for subsequent turns."
},
"serviceTier": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/ServiceTier"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
],
"description": "Override the service tier for subsequent turns."
},
"summary": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummary"
},
{
"type": "null"
}
],
"description": "Override the reasoning summary for subsequent turns."
},
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadTurnContextUpdateParams",
"type": "object"
}
@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ThreadTurnContextUpdateResponse",
"type": "object"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,48 @@
// 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 { CollaborationMode } from "../CollaborationMode";
import type { Personality } from "../Personality";
import type { ReasoningEffort } from "../ReasoningEffort";
import type { ReasoningSummary } from "../ReasoningSummary";
import type { ServiceTier } from "../ServiceTier";
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
import type { AskForApproval } from "./AskForApproval";
import type { SandboxPolicy } from "./SandboxPolicy";
export type ThreadTurnContextUpdateParams = {threadId: string, /**
* Override the working directory for subsequent turns and thread-scoped
* operations.
*/
cwd?: string | null, /**
* Override the approval policy for subsequent turns.
*/
approvalPolicy?: AskForApproval | null, /**
* Override where approval requests are routed for review on subsequent
* turns.
*/
approvalsReviewer?: ApprovalsReviewer | null, /**
* Override the sandbox policy for subsequent turns.
*/
sandboxPolicy?: SandboxPolicy | null, /**
* Override the model for subsequent turns.
*/
model?: string | null, /**
* Override the reasoning effort for subsequent turns.
*/
effort?: ReasoningEffort | null | null, /**
* Override the reasoning summary for subsequent turns.
*/
summary?: ReasoningSummary | null, /**
* Override the service tier for subsequent turns.
*/
serviceTier?: ServiceTier | null | null, /**
* EXPERIMENTAL - Set a pre-set collaboration mode for subsequent turns.
*
* Takes precedence over model, reasoning effort, and developer
* instructions if set.
*/
collaborationMode?: CollaborationMode | null, /**
* Override the personality for subsequent turns.
*/
personality?: Personality | null};
@@ -0,0 +1,5 @@
// 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.
export type ThreadTurnContextUpdateResponse = Record<string, never>;
@@ -310,6 +310,8 @@ export type { ThreadStatus } from "./ThreadStatus";
export type { ThreadStatusChangedNotification } from "./ThreadStatusChangedNotification";
export type { ThreadTokenUsage } from "./ThreadTokenUsage";
export type { ThreadTokenUsageUpdatedNotification } from "./ThreadTokenUsageUpdatedNotification";
export type { ThreadTurnContextUpdateParams } from "./ThreadTurnContextUpdateParams";
export type { ThreadTurnContextUpdateResponse } from "./ThreadTurnContextUpdateResponse";
export type { ThreadUnarchiveParams } from "./ThreadUnarchiveParams";
export type { ThreadUnarchiveResponse } from "./ThreadUnarchiveResponse";
export type { ThreadUnarchivedNotification } from "./ThreadUnarchivedNotification";
@@ -280,6 +280,11 @@ client_request_definitions! {
params: v2::ThreadSetNameParams,
response: v2::ThreadSetNameResponse,
},
ThreadTurnContextUpdate => "thread/turnContext/update" {
params: v2::ThreadTurnContextUpdateParams,
inspect_params: true,
response: v2::ThreadTurnContextUpdateResponse,
},
ThreadMetadataUpdate => "thread/metadata/update" {
params: v2::ThreadMetadataUpdateParams,
response: v2::ThreadMetadataUpdateResponse,
@@ -2891,6 +2891,69 @@ pub struct ThreadUnarchiveParams {
#[ts(export_to = "v2/")]
pub struct ThreadSetNameResponse {}
#[derive(
Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi,
)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadTurnContextUpdateParams {
pub thread_id: String,
/// Override the working directory for subsequent turns and thread-scoped
/// operations.
#[ts(optional = nullable)]
pub cwd: Option<PathBuf>,
/// Override the approval policy for subsequent turns.
#[experimental(nested)]
#[ts(optional = nullable)]
pub approval_policy: Option<AskForApproval>,
/// Override where approval requests are routed for review on subsequent
/// turns.
#[ts(optional = nullable)]
pub approvals_reviewer: Option<ApprovalsReviewer>,
/// Override the sandbox policy for subsequent turns.
#[ts(optional = nullable)]
pub sandbox_policy: Option<SandboxPolicy>,
/// Override the model for subsequent turns.
#[ts(optional = nullable)]
pub model: Option<String>,
/// Override the reasoning effort for subsequent turns.
#[serde(
default,
deserialize_with = "super::serde_helpers::deserialize_double_option",
serialize_with = "super::serde_helpers::serialize_double_option",
skip_serializing_if = "Option::is_none"
)]
#[ts(optional = nullable)]
pub effort: Option<Option<ReasoningEffort>>,
/// Override the reasoning summary for subsequent turns.
#[ts(optional = nullable)]
pub summary: Option<ReasoningSummary>,
/// Override the service tier for subsequent turns.
#[serde(
default,
deserialize_with = "super::serde_helpers::deserialize_double_option",
serialize_with = "super::serde_helpers::serialize_double_option",
skip_serializing_if = "Option::is_none"
)]
#[ts(optional = nullable)]
pub service_tier: Option<Option<ServiceTier>>,
/// EXPERIMENTAL - Set a pre-set collaboration mode for subsequent turns.
///
/// Takes precedence over model, reasoning effort, and developer
/// instructions if set.
#[experimental("thread/turnContext/update.collaborationMode")]
#[ts(optional = nullable)]
pub collaboration_mode: Option<CollaborationMode>,
/// Override the personality for subsequent turns.
#[ts(optional = nullable)]
pub personality: Option<Personality>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadTurnContextUpdateResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]